feat(voyage): scaffold playground/ with DS vendor sync — v4.2 Step 5

- playground/voyage-playground.html: minimal skeleton (DOCTYPE, app-header, guide-panel, aria-live region, skip-to-main link). Steps 8-11 will extend with render-pipeline + creation gestures + sidebar + export.
- playground/vendor/playground-design-system/: synced via 'node scripts/sync-design-system.mjs voyage' (27 files + MANIFEST.json with source_commit + sync_date + SHA-256 per file).
- tests/playground/voyage-playground.test.mjs: 8 tests pinning HTML existence, DOCTYPE, no-external-URLs, no-marked, A11Y skip-to-main + aria-live, MANIFEST.json structure, vendored DS files present.
- shared/PLAYGROUND-MAINTENANCE.md: consumer list updated 5 -> 6 (added voyage).
This commit is contained in:
Kjell Tore Guttormsen 2026-05-09 12:55:02 +02:00
commit 2f4330265c
30 changed files with 4947 additions and 1 deletions

View file

@ -0,0 +1,86 @@
# playground-design-system — CHANGELOG
## 0.4.0 — 2026-05-08
### Bug fixes
- **`.kanban-card__name`** (components-tier3-supplement.css): bytt `word-break: break-all` til `word-break: break-word` + `overflow-wrap: anywhere`. `break-all` knekker midt i ord ("Tekn isk dokumen tasjon"); ny verdi respekterer ordskjøt og brytter kun lange tokens (B-DS-1).
- **`.expansion__title-main`, `.expansion__title-sub`** (components-tier3-supplement.css): legg til `display: block`. Begge er `<span>`-elementer som flyter inline by default, noe som gir "dokumentertKilde: Art. 9" på samme linje. `display: block` sikrer vertikal stacking (B-DS-2).
- **`.matrix__bubble`** (components.css): legg til `cursor: pointer`, `transition`, `:hover { transform: scale(1.15) }` og `:focus-visible { outline + offset }`. Antar at consumer rendrer bobler som `<button>` for click-handlers — gir visuell + keyboard-fokus-feedback (B-DS-3).
### Påvirkning
Bugfixene er **backward-compatible** — alle eksisterende selectors og verdier som er endret, var bugfixes. Plugin-konsumenter som har lokal-overrides for disse mønstrene bør re-syncer og slette overridene:
- **ms-ai-architect:** re-sync i samme commit, sletter linje 191-193 (matrix-bubble), 208-211 (expansion-title), 213-216 (kanban-card-name) i `playground/ms-ai-architect-playground.html`.
- **llm-security, voyage, okr, config-audit:** re-sync på eget tempo (ikke breaking — gammel vendored DS fungerer fortsatt med eksisterende lokal-overrides).
### For å adoptere v0.4
```bash
node scripts/sync-design-system.mjs <plugin-name>
# --force hvis drift detected
```
Førsteadopter: `ms-ai-architect` v1.14.0 (planlagt 2026-05-08, multi-sesjons-løp som starter med DS-bump i sesjon 2).
## 0.3.0 — 2026-05-04
### Added — Playground/report-page foundation primitives (sections 13-25 in tier3-supplement)
Generiske mønstre som tidligere ble definert inline i plugin-playgrounds (først i ms-ai-architect v1.10) er hoisted hit slik at alle 5 plugin-konsumenter (`ms-ai-architect`, `okr`, `llm-security`, `ultraplan-local`, `config-audit`) kan dele samme vokabular og visuelle profil.
- **`.eyebrow` utility** — uppercase 11px monospace label med 0.08em letter-spacing. Bruk over seksjons-titler.
- **`.page__*` page-shell** (`.page__header`, `.page__header-main`, `.page__header-aside`, `.page__eyebrow`, `.page__title`, `.page__lede`, `.page__meta`) — standard rapport-side-header med eyebrow → h1 → lede → meta + verdict-slot side-by-side. Responsiv: kollapser til én kolonne under 720px.
- **`.key-stats` / `.key-stat`** — 2-5-kolonne responsivt grid av store tall-metrikker. `font-variant-numeric: tabular-nums`, `font-size-2xl` bold. Severity-modifiers (`.key-stat--critical/high/medium/low/positive/info`) tinter value-fargen.
- **`.verdict-pill-lg` 5-band utvidelse** — eksisterende `.verdict-pill-lg` aksepterer nå alle 5 severity-bånd: `critical/extreme/high/medium/low/positive` + neutral `n-a/info/neutral`. Bakoverkompatibel med eksisterende `block/warning/allow`.
- **`.tab-list` / `.tab` / `.tab-panel`** — generisk faneflate-komponent. ARIA-paritet: `role="tablist"`, `role="tab"`, `aria-current="true"`. `.tab__count` for badge-tall, `.tab-panel[hidden]` for skjuling.
- **`.top-risks` / `.top-risk[data-severity]`** — severity-ordnet liste over topp-risikoer med rank/desc/score-kolonner. Severity-attribut driver venstre-border + score-pill-bakgrunn.
- **`.recommendation-card[data-severity]`** — emphasized advisory-callout med label + body. 6 severity-modifiers.
- **`.card__*` subkomponenter** — komponerbare tillegg til eksisterende `.card` (base.css): `.card__head`, `.card__title`, `.card__desc`, `.card__id`, `.card__meta`, `.card__hint`, `.card__actions`, `.card__pill`. Pluss `.card--severity-{level}` for 4px venstre-border-modifier.
- **Form patterns**`.field-row` (vertikal flex), `.field-label` (medium weight), `.field-help` (xs tertiary), `.required-mark` (severity-critical asterisk), `.multi-select` (fieldset reset), `.checkbox-row` (inline-flex med hover). Mirrors Aksel/Digdir form-konvensjoner.
- **Section-spacing utilities**`.stack-lg` (margin-block: var(--space-8)), `.stack-md` (var(--space-5)), `.stack-sm` (var(--space-3)). Anvendes på parent for å gi konsistent vertikal rytme mellom barn-elementer.
- **`.pyramide-tier-detail`** — utvidbar `<details>`-blokk under `.pyramide`-visualisering. Custom chevron, ingen native marker. Brukes av AI Act-klassifiserings-renderer.
- **`.scenario-card-grid` / `.scenario-card[data-status="winner"]`** — auto-fit grid (minmax 240px) av scenario/alternativ-cards. Vinnerstatus får success-tinted bakgrunn + grønn count-pill.
- **`.app-shell` / `.app-shell--wide` / `.app-shell--narrow`** — sentralisert max-width page-wrapper. 1200/1400/880px varianter.
### Notes for vendor consumers
Versjon 0.3.0 er **rent additiv** — ingen eksisterende selector er endret eller fjernet. Alle eksisterende klasser (`.btn`, `.card`, `.expansion`, `.kanban-*`, `.mat-ladder`, `.read-more`, `.suppressed`, `.pair-before-after`, `.verdict-pill-lg` osv.) fungerer uendret.
For å adoptere v0.3:
1. Re-sync via `node scripts/sync-design-system.mjs <plugin-name>` (kreves `--force` hvis eksisterende drift)
2. Oppdater plugin HTML til å bruke nye klasser i stedet for inline CSS
3. Andre plugins kan vente med adopsjon — eksisterende DS-bruk fortsetter å fungere
Førsteadopter: `ms-ai-architect` v1.11.0 (planlagt 2026-05-04).
## 0.2.0 — 2026-05-04
### Added
- `[data-theme="light"]`-blokk i `tokens.css` (Aksel-aligned, WCAG AA-validert).
Full mirror av dark-blokken (26 vars) — alle theme-overridable tokens som
finnes i dark-blokken finnes nå også i light-blokken, slik at renderers ikke
faller gjennom til udefinerte verdier ved theme-switch.
- `color-scheme` CSS-property satt eksplisitt på `:root`, `[data-theme="light"]`
og `[data-theme="dark"]` for korrekt native form-controls/scrollbar-styling.
### Notes for vendor consumers
Andre plugins som vendrer design-systemet
(`okr`, `llm-security`, `ultraplan-local`, `config-audit`) får tilgang til
light-tokens etter neste re-sync. Adopsjon er valgfri — eksisterende dark-only
oppførsel er bakoverkompatibel siden ingen eksisterende verdi er endret.
For å adoptere light-mode i en konsument:
1. Re-sync via `node scripts/sync-design-system.mjs <plugin-name>`
2. Legg til en synkron `<script>`-IIFE i `<head>` før CSS-load som leser
`localStorage` og setter `data-theme` + `colorScheme``documentElement`.
3. Eksponere theme-toggle i UI som setter `documentElement.dataset.theme` +
persisterer i `localStorage`.
## 0.1.0 — 2026-04 (initial)
- Tier 1+2+3 design-system med Aksel/Digdir-aligned tokens, base, components.
- Dark mode default + `[data-theme="dark"]`-overrides.
- Self-hosted Inter, JetBrains Mono, Source Serif 4 fonts.
- Schemas for renderers + commands.

View file

@ -0,0 +1,36 @@
{
"generated_by": "scripts/sync-design-system.mjs",
"do_not_edit": true,
"source": "shared/playground-design-system/",
"source_commit": "f316cc1efa05776362b7bd31508f4e4e3c0e0b3a",
"sync_date": "2026-05-09T10:54:04.938Z",
"file_count": 26,
"files": {
"CHANGELOG.md": "dfbd75552c94848acba3e2503bfad56c1c4bc8cfdcbd638d9409149010913d28",
"README.md": "83de0e29b207c979b7b2a3327b7a4ec0c2e1b4d3705ee2677f26c28c3a3ee643",
"base.css": "604fe6839e2ed304bc0ba112a4e045f208b4b3f084f449a1abdb94ce0a1e5263",
"components-tier2.css": "c2cb7e9d76d6af28d50db654030413777feb2f2f2b93213e598de8b686b14523",
"components-tier3-supplement.css": "51fab10377d80029d6552613069d46fd14ce66af77fe6705b1c6bdf5c9e6481e",
"components-tier3.css": "c391ea387298ce864bc35078e7e044b2cdd4187e3130456347d91876599ff4b1",
"components.css": "56fa7392b8b20b567a46f72a8fe9e0205d78ce475eae6b22fc3f50b39b235545",
"fonts.css": "e3c3df581c6e4d66e25c555f125c745f6512a33038401089d2519a94ea63ee3d",
"fonts/Inter-Bold.woff2": "220976705fbec109f43c5cfdceca639e99ace7e51f3eb67292b105d3575eb39b",
"fonts/Inter-Medium.woff2": "8458f8afa67b5691c1fcbe51607a2dafb53a9839e48131c608a186b65415d96d",
"fonts/Inter-Regular.woff2": "b6f9db9e45be20f3c1312c97fbee7ec36b7d8280f8caa4d53c9ba0408cc9997a",
"fonts/Inter-SemiBold.woff2": "8e52a861dc26ff4608c50bd7ff89b65d0d6216a2afe7b47ce5d84544811ca400",
"fonts/JetBrainsMono-Medium.woff2": "086c48dfbea9ddaff1320f7e09399b8e2924e88ce67453721255db3bdbb5a353",
"fonts/JetBrainsMono-Regular.woff2": "a9cb1cd82332b23a47e3a1239d25d13c86d16c4220695e34b243effa999f45f2",
"fonts/JetBrainsMono-SemiBold.woff2": "918edad542a1da608fd2ba8daebaff9ac802309103fe760eed465b8b4e47faf1",
"fonts/LICENSE-Inter.txt": "262481e844521b326f5ecd053e59b98c8b2da78c8ee1bdbb6e8174305e54935a",
"fonts/LICENSE-JetBrainsMono.txt": "30f0c136e3c88e422d0791acd97238870f9054a9729bc34cf2ff0d4ed8cac4ad",
"fonts/LICENSE-SourceSerif4.md": "75784a295293a8992f5a8d99210566e0064a012e6dab6731305e3787f15896c7",
"fonts/LICENSES.md": "16ef4cb2f4d85233c27be390c3f52ee60d24f1a2a5f72886a0c5dbc8cfcf2c28",
"fonts/SourceSerif4-Regular.woff2": "d5f6830fbdb42425cb60b5cd61d91afa9a2f59b8e99057b1a1d4c2e43b1b06dc",
"fonts/SourceSerif4-Semibold.woff2": "dd00d4b1fea42ca7bd806175662ec51ec09494de986d85087861216cbcf17add",
"print.css": "cd62f08d1b13e0308b5083b6cb5135739eb834e85e88468bd349a642d92b7a6f",
"schemas/finding.schema.json": "0b24797373650582bac232d31a4dd9260593375a0d17259e18f1141a20de8d0c",
"schemas/okr-set.schema.json": "aa27347fb232a956ec9dcee1775115710e2715a665c8d729ac50b90c6884de26",
"schemas/ros-threat.schema.json": "e16497c1a6b79d6e78149d6cf1c28ac9df1e93234627a0c546814fb24d6c96d9",
"tokens.css": "1499bc2eea0178e35935413c79a10bbee7d49fdfa91bd33eeba3bb9e9acab809"
}
}

View file

@ -0,0 +1,234 @@
# Playground Design System
A shared design system for plugin Playgrounds — visual self-service UIs that complement terminal slash-commands. Built for Norwegian public sector with WCAG 2.1 AA compliance, Aksel/Digdir-aligned aesthetics, and self-contained HTML deployment.
**Version:** 0.1 (Phase 1 — 2026-05-02)
## Provenance
This design system was generated by **[claude.ai/design](https://claude.ai/design)** (Anthropic) in a dialog-based design session driven by a comprehensive brief covering five plugins (`ms-ai-architect`, `okr`, `llm-security`, `ultraplan-local`, `config-audit`), Norwegian public-sector design conventions (Aksel/Digdir), and domain-specific visual standards (NS 5814 risk matrices, EU AI Act 4-tier pyramide, Doerr OKR scoring, NIST CSF, OWASP threat modeling).
Integration into the marketplace (file organization, path normalization, README authoring, root-doc cross-references) was performed in a separate Claude Code session. Per Anthropic Consumer Terms §4, ownership of outputs is assigned to the user; this design system is licensed MIT alongside the rest of the marketplace.
## Directory layout
```
shared/
├── playground-design-system/ # The design system (this directory)
│ ├── README.md # This file
│ ├── tokens.css # CSS custom properties (Aksel/Digdir-aligned)
│ ├── base.css # Reset, typography, primitives, focus, print
│ ├── components.css # Tier 1: radar, matrix, findings-browser, critique-card, wizard, live-meter
│ ├── components-tier2.css # Tier 2: decision-tree, traffic-lights, diff-review, treemap, distribution, command-pipeline, pyramide, pipeline-cockpit, verdict-pill+risk-meter, codepoint-reveal, small-multiples, OWASP badges
│ ├── components-tier3.css # Tier 3 wave 1: pair-before-after, AI Act timeline, 3-track entry, FRIA rights-matrix, capability-matrix, parallel-agent-status, ErrorSummary, GuidePanel
│ ├── components-tier3-supplement.css # Tier 3 wave 2 (12): toxic-flow, fleet-overview, kanban Keep/Review/Remove, maturity-ladder, classify-and-transform, cycle-ribbon, persistent-antipattern, suppressed-signals, ExpansionCard, ReadMore, FormProgress, Aspirational-vs-Committed
│ ├── fonts.css # @font-face declarations for self-hosted fonts
│ ├── fonts/ # Self-hosted woff2 + license attribution
│ │ ├── Inter-{Regular,Medium,SemiBold,Bold}.woff2
│ │ ├── JetBrainsMono-{Regular,Medium,SemiBold}.woff2
│ │ ├── SourceSerif4-{Regular,Semibold}.woff2
│ │ └── LICENSES.md # All three are SIL OFL 1.1
│ ├── print.css # A4 print stylesheet with B/W severity patterns
│ └── schemas/ # Cross-plugin JSON schemas
│ ├── finding.schema.json # Used by llm-security, config-audit, ultraplan-review, ms-ai-review
│ ├── okr-set.schema.json # Used by OKR plugin
│ └── ros-threat.schema.json # Used by ms-ai-architect ROS workflow
└── playground-examples/ # Showcase + reference scenarios
├── index.html # System showcase (browse all components)
├── ros-lier-kommune.html # Scenario A — ms-ai-architect ROS report
├── okr-baerum.html # Scenario B — OKR live writer
├── security-direktorat.html # Scenario C — llm-security findings review
├── templates.html # Skeleton + print-template demos
├── tier3-preview.html # Tier 3 wave 1 visual preview
├── components/ # Tier 3 wave 2 — 12 isolated demo pages
│ ├── sankey-toxic-flow.html
│ ├── fleet-overview.html
│ ├── kanban.html
│ ├── maturity-ladder.html
│ ├── classify-transform.html
│ ├── cycle-ribbon.html
│ ├── persistent-antipattern.html
│ ├── suppressed-signals.html
│ ├── expansion-card.html
│ ├── read-more.html
│ ├── form-progress.html
│ └── aspirational-committed.html
├── ros-app.js # Scenario A interactivity
└── ros-data.js # Scenario A mock data
```
## Quick start
To use the design system from a plugin's Playground:
```html
<!doctype html>
<html lang="nb" data-theme="light">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../../shared/playground-design-system/tokens.css">
<link rel="stylesheet" href="../../shared/playground-design-system/base.css">
<link rel="stylesheet" href="../../shared/playground-design-system/components.css">
<link rel="stylesheet" href="../../shared/playground-design-system/components-tier2.css">
<!-- Optional: include components-tier3.css for Tier 3 wave 1 components -->
<!-- Optional: include components-tier3-supplement.css for Tier 3 wave 2 (12 additional components) -->
<!-- Optional: only include print.css if scenario produces a printable A4 report -->
<link rel="stylesheet" href="../../shared/playground-design-system/print.css">
<!-- Self-hosted fonts (no external requests) -->
<link rel="stylesheet" href="../../shared/playground-design-system/fonts.css">
</head>
<body>
<header class="app-header">
<a class="app-header__brand" href="...">
<span class="app-header__brand-mark">MS</span>
ms-ai-architect
</a>
<span class="app-header__breadcrumb">/ Playground</span>
<div class="app-header__spacer"></div>
<button class="theme-toggle" data-theme-toggle>Mørk modus</button>
</header>
<!-- Your Playground content using design-system classes -->
</body>
</html>
```
The relative path `../../shared/playground-design-system/` assumes the plugin's Playground lives at `plugins/{plugin-name}/playground/index.html`. Adjust the prefix to match your plugin's structure.
## Design principles
1. **Aksel/Digdir-aligned.** Inter font, body 17px, Digdir blue `#0062BA`, semantic CSS tokens. Norwegian public sector users recognize this DNA.
2. **WCAG 2.1 AA non-negotiable.** Required by `Forskrift om universell utforming av IKT` for Norwegian public sector. Every component ships with proper focus rings, ARIA attributes, keyboard navigation, and contrast that passes deuteranopia simulators.
3. **Vanilla HTML/CSS/JS.** No React, no Tailwind, no build step. A plugin can copy a Playground HTML file to disk and it will render correctly.
4. **Self-contained per Playground.** Each plugin's `playground/*.html` should be openable offline with only the design-system CSS files alongside.
5. **Print-aware.** The `print.css` stylesheet ensures matrix cells use B/W-safe hatching patterns when printed, severity badges become outlined boxes with patterns, and interactive chrome disappears. Designed for A4 reports going to Datatilsynet, kommunestyre, statsråd.
6. **Severity is universal.** All severity-coded UI uses the same five-level ramp (low/medium/high/critical/extreme) with deuteranopia-safe hex values defined in `tokens.css`. Distinct from "state" tokens (failed/blocked/queued/running) used in pipeline contexts — never mix severity-red with failure-red.
7. **Two-spor strategy.** The system supports both non-technical decision makers (Spor 1: ms-ai-architect, OKR, llm-security) and developer power-users (Spor 2: ultraplan-local, config-audit) — same component library, different information densities.
## Token system
See `tokens.css` for full reference. Highlights:
- **Typography:** `--font-family-sans` (Inter), `--font-size-md` (17px body), `--measure` (65ch line length)
- **Primary:** `--color-primary-500` = `#0062BA` (Digdir blue), with 50/100/300/500/700/900 ramp
- **Severity:** `--color-severity-{low,medium,high,critical,extreme}` + `-soft` (background) + `-on` (foreground) variants. Deuteranopia-safe.
- **State:** `--color-state-{success,warning,failed,blocked,info,running,queued,pending,done}` — distinct from severity
- **Surface:** Warm off-white `#FBFAF7` (light), graphite `#0F1419` (dark). Theme via `[data-theme="dark"]` on `<html>` or `<body>`
- **Plugin scope:** `--color-scope-{architect,okr,security,ultraplan,config}` for visual differentiation between plugins
- **Spacing:** 4px grid, scale 1-20 (4px to 80px)
- **Radius:** `--radius-sm` (3px) / `-md` (5px) / `-lg` (8px) / `-pill` (999px) — max 8px (no consumer-app rounded corners)
- **Motion:** Respects `prefers-reduced-motion`
## Component reference
### Tier 1 (`components.css`)
| Component | Class prefix | Used by |
|---|---|---|
| Radar / Spider chart | `.radar` | OKR maturity (7-axis), ms-ai security (6), ms-ai ROS dimensions (7), ultraplan plan-critic (7) |
| Matrix / 5×5 heatmap | `.matrix` | ms-ai ROS, DPIA, OKR coverage, security scanner, license map |
| Findings-browser | `.findings` | llm-security, ultraplan-review, config-audit, ms-ai-review |
| Critique-card | `.critique-card` | llm-security findings, ultraplan, config-audit feature-gap, OKR antipatterns |
| Wizard / Stepper | `.stepper`, `.wizard__panel` | ms-ai 5-step intake, security clean, config-audit audit, ultraplan, OKR onboarding |
| Live-meter | `.live-meter`, `.lint-annotation` | OKR writer, ultraplan brief-reviewer, cost, config-audit |
Plus app-shell primitives: `.app-header`, `.sidepanel`, `.scrim`, `.theme-toggle`.
### Tier 3 (`components-tier3.css`)
Critical components for ms-ai-architect Playground v3 plus universal Aksel patterns. Authored 2026-05-02 in Claude Code (not via claude.ai/design — visual coherence verified against Tier 1+2 in `playground-examples/tier3-preview.html`).
| Component | Class prefix | Used by |
|---|---|---|
| Inherent + residual pair | `.pair-before-after` | ms-ai ROS before/after, DPIA, AI Act mitigations, OKR check-ins |
| AI Act compliance-tidslinje | `.aiact-timeline`, `.aiact-countdown` | ms-ai-architect classify flow + dashboard |
| 3-track entry | `.tracks` | All plugins — entry-level UX choice (Guide/Explore/Expert) |
| FRIA rights-matrix | `.rights-matrix` | ms-ai-architect FRIA (Art. 27, 12 EU Charter rights × impact) |
| Capability-matrix | `.capability-matrix` | ms-ai-architect license × kapabilitet mapping |
| Parallel-agent-status | `.agent-grid`, `.agent-card` | ms-ai utredning multi-worker, ultraplan multi-wave execute |
| ErrorSummary | `.error-summary` | All plugins — Aksel/GOV.UK form-validation pattern |
| GuidePanel | `.guide-panel` | All plugins — Aksel friendly inline guidance with optional CTA |
### Tier 2 (`components-tier2.css`)
| Component | Class prefix | Used by |
|---|---|---|
| Decision-tree | `.decision-tree`, `.dt-node`, `.dt-edge` | ms-ai AI Act 4-step classifier, security MAESTRO drill |
| Traffic-lights | `.traffic-light` | ms-ai compliance, OKR KR-status, security pre-deploy, config-audit risk |
| Diff-review | `.diff` | security diff, config-audit drift, ultraplan triage |
| Treemap | `.treemap` | config-audit token-hotspots |
| Distribution / range-viz | `.distribution` | ms-ai cost P10/P50/P90, security risk-score, OKR progress |
| Command-pipeline | `.cmd-pipeline`, `.cmd-step` | All plugins — final export of slash-command sequence |
| Pyramide (4-tier) | `.pyramide` | ms-ai AI Act risk classification |
| Pipeline-cockpit | `.pipeline-cockpit`, `.pc-stage` | ultraplan 6-stage flow, ms-ai utredning, config-audit audit |
| Verdict-pill + risk-meter | `.verdict-pill-lg`, `.risk-meter` | llm-security BLOCK/WARNING/ALLOW + 0-100 risk-score |
| Codepoint-reveal | `.codepoint-reveal` | llm-security Unicode steganography demo |
| Small-multiples grid | `.small-multiples`, `.sm-card` | llm-security 16-category posture (alternative to overcrowded radar) |
| OWASP badges | `.badge--owasp-{llm,asi,ast,mcp}` | llm-security finding cross-mapping (4 frameworks) |
## Schemas
`schemas/` contains JSON schemas for cross-plugin data interchange:
- **`finding.schema.json`** — universal "finding" shape (id, title, severity, source, evidence, rationale, recommendation, status). Consumed by llm-security, config-audit, ultraplan-review, ms-ai-review. Maps directly to the `.critique-card` component.
- **`okr-set.schema.json`** — OKR shape (objectives + key results, scoring, antipattern annotations). Consumed by OKR plugin.
- **`ros-threat.schema.json`** — ROS threat shape (likelihood × consequence, mitigation references, residual risk). Consumed by ms-ai-architect.
A plugin command can output JSON conforming to these schemas, and a Playground can render the result without further translation.
## Theming
Default is light. Toggle dark via `data-theme="dark"` attribute on `<html>` or `<body>`. The system also respects `prefers-color-scheme: dark` when no explicit theme is set:
```js
// Toggle dark/light
document.documentElement.dataset.theme =
document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', document.documentElement.dataset.theme);
```
## Print mode
Include `print.css` if your scenario produces an A4 report. Then add `class="no-print"` to interactive chrome (header, buttons, theme toggle), and use `class="page-break"` to force page breaks. Severity-coded matrix cells will automatically render as B/W-safe hatching patterns when printed. The `.print-header` and `.print-footer` blocks support kommune-logo slots and signature lines for offentlige dokumenter.
## Known limitations
1. **No JavaScript framework.** Components are CSS-first. Interactivity (e.g. `aria-selected` toggling, sidepanel open/close, live-meter updates) must be wired by each Playground using vanilla JS. See `playground-examples/ros-app.js` for a reference implementation pattern.
2. **No icon set bundled.** The system assumes Lucide or Phosphor SVG sprites are inlined per Playground. Iconography is intentionally out-of-system to keep the shared system small.
3. **Mobile responsiveness is partial.** The 5×5 matrix, findings-browser, codepoint-reveal split-pane, and small-multiples grid have explicit `@media (max-width: ...)` rules. Other components may need polish for narrow viewports.
## Self-hosted fonts
All three font families (Inter, JetBrains Mono, Source Serif 4) are bundled as woff2 in `fonts/` and loaded via `fonts.css`. No external requests to Google Fonts or any CDN. All three are SIL OFL 1.1 — see `fonts/LICENSES.md` for full attribution.
## Versioning
This system follows semver:
- **Major:** Breaking token rename, component class rename, schema field removal/rename
- **Minor:** New tokens, new components, new schema fields, new variants
- **Patch:** Bugfixes, accessibility improvements, visual polish without contract changes
Every plugin Playground that consumes the design system should declare the version in a comment at the top of its HTML:
```html
<!-- playground-design-system v0.1 -->
```
## License
MIT, same as the parent ktg-plugin-marketplace. Reuse freely; attribution appreciated.
## Contributing
This is a solo project. PRs are not accepted, but issues and suggestions are welcome at the marketplace repo (Forgejo: `git.fromaitochitta.com/open/ktg-plugin-marketplace`).
When adding a new component:
1. Add CSS to `components.css` (Tier 1) or `components-tier2.css` (Tier 2)
2. Use BEM naming convention: `.component-name__element--modifier`
3. Reference only `tokens.css` custom properties — never hard-code colors, spacing, or fonts
4. Test in light + dark themes, with deuteranopia simulator (Stark, Sim Daltonism)
5. Test keyboard navigation and screen reader (NVDA on Windows, VoiceOver on Mac)
6. Add a print rule if the component appears in printable reports
7. Document in this README under the appropriate Tier table

View file

@ -0,0 +1,264 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/* =============================================================================
base.css reset, typography, layout primitives, focus, print
============================================================================= */
*, *::before, *::after { box-sizing: border-box; }
html {
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
font-family: var(--font-family-sans);
font-size: var(--font-size-md);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background: var(--color-bg);
font-feature-settings: "ss01", "cv11";
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-tight);
letter-spacing: -0.01em;
color: var(--color-text-primary);
text-wrap: balance;
}
h1 { font-size: var(--font-size-3xl); letter-spacing: -0.02em; }
h2 { font-size: var(--font-size-2xl); letter-spacing: -0.015em; }
h3 { font-size: var(--font-size-xl); }
h4 { font-size: var(--font-size-lg); }
h5 { font-size: var(--font-size-md); }
p {
margin: 0;
text-wrap: pretty;
max-width: var(--measure);
}
small { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
code, kbd, samp { font-family: var(--font-family-mono); font-size: 0.92em; }
kbd {
display: inline-block;
padding: 1px 6px;
font-size: 0.85em;
border: 1px solid var(--color-border-moderate);
border-bottom-width: 2px;
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text-secondary);
line-height: 1;
}
a {
color: var(--color-text-link);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
a:hover { color: var(--color-text-link-hover); text-decoration-thickness: 2px; }
button { font-family: inherit; }
/* Focus rings — WCAG */
:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) { outline: none; }
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: 9px 16px;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: 1.3;
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
white-space: nowrap;
text-decoration: none;
}
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
.btn--primary { background: var(--color-primary-500); color: var(--color-text-on-primary); }
.btn--primary:hover { background: var(--color-primary-700); }
.btn--secondary {
background: var(--color-surface);
color: var(--color-text-primary);
border-color: var(--color-border-moderate);
}
.btn--secondary:hover { background: var(--color-bg-soft); border-color: var(--color-border-strong); }
.btn--ghost {
background: transparent;
color: var(--color-text-primary);
border-color: transparent;
}
.btn--ghost:hover { background: var(--color-bg-soft); }
.btn--destructive { background: var(--color-severity-critical); color: #fff; }
.btn--destructive:hover { background: var(--color-severity-extreme); }
.btn--sm { padding: 5px 10px; font-size: var(--font-size-xs); }
.btn--lg { padding: 12px 20px; font-size: var(--font-size-md); }
/* ---------- Badges / pills ---------- */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
line-height: 1.4;
border-radius: var(--radius-pill);
border: 1px solid var(--color-border-subtle);
background: var(--color-bg-soft);
color: var(--color-text-secondary);
white-space: nowrap;
}
.badge--severity-low { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); border-color: transparent; }
.badge--severity-medium { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); border-color: transparent; }
.badge--severity-high { background: var(--color-severity-high-soft); color: var(--color-severity-high-on); border-color: transparent; }
.badge--severity-critical { background: var(--color-severity-critical); color: var(--color-severity-critical-on); border-color: transparent; }
.badge--severity-extreme { background: var(--color-severity-extreme); color: var(--color-severity-extreme-on); border-color: transparent; }
.badge--owasp { font-family: var(--font-family-mono); font-size: 11px; padding: 1px 6px; }
.badge--scope-architect { background: var(--color-scope-architect); color: #fff; border-color: transparent; }
.badge--scope-okr { background: var(--color-scope-okr); color: #fff; border-color: transparent; }
.badge--scope-security { background: var(--color-scope-security); color: #fff; border-color: transparent; }
.badge--scope-ultraplan { background: var(--color-scope-ultraplan); color: #fff; border-color: transparent; }
.badge--scope-config { background: var(--color-scope-config); color: #fff; border-color: transparent; }
/* ---------- Cards / surfaces ---------- */
.card {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
padding: var(--space-6);
}
.card--sunken { background: var(--color-surface-sunken); }
.card--raised { box-shadow: var(--shadow-sm); }
/* ---------- Inline messages (Aksel 3-tier) ---------- */
.inline-message {
display: flex;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
border-left: 4px solid;
background: var(--color-bg-soft);
font-size: var(--font-size-sm);
line-height: var(--line-height-snug);
}
.inline-message--info { border-color: var(--color-state-info); background: #EAF3FB; color: #08416B; }
.inline-message--success { border-color: var(--color-state-success); background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.inline-message--warning { border-color: var(--color-state-warning); background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.inline-message--error { border-color: var(--color-severity-critical); background: var(--color-surface); color: var(--color-text-primary); }
.inline-message--error strong, .inline-message--error b { color: var(--color-severity-critical); }
[data-theme="dark"] .inline-message--info { background: #0E2A3F; color: #9CC0EA; }
[data-theme="dark"] .inline-message--error { background: var(--color-surface); color: var(--color-text-primary); }
[data-theme="dark"] .inline-message--error strong, [data-theme="dark"] .inline-message--error b { color: #F09095; }
/* ---------- Form controls ---------- */
.input, .select, .textarea {
width: 100%;
padding: 9px 12px;
font-family: inherit;
font-size: var(--font-size-sm);
line-height: 1.4;
color: var(--color-text-primary);
background: var(--color-surface);
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-md);
transition: border-color var(--duration-fast) var(--ease-default),
box-shadow var(--duration-fast) var(--ease-default);
}
.input:hover, .select:hover, .textarea:hover { border-color: var(--color-border-strong); }
.input:focus, .select:focus, .textarea:focus {
outline: none;
border-color: var(--color-primary-500);
box-shadow: var(--shadow-focus);
}
.textarea { min-height: 96px; resize: vertical; line-height: var(--line-height-normal); }
.label {
display: block;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
margin-bottom: 6px;
}
.label__hint { display: block; font-size: var(--font-size-xs); color: var(--color-text-tertiary); font-weight: 400; margin-top: 2px; }
/* ---------- Layout primitives ---------- */
.stack { display: flex; flex-direction: column; gap: var(--space-4); }
.stack--lg { gap: var(--space-8); }
.stack--sm { gap: var(--space-2); }
.row { display: flex; gap: var(--space-4); align-items: center; }
.row--wrap { flex-wrap: wrap; }
.row--between { justify-content: space-between; }
.container { max-width: var(--container-default); margin: 0 auto; padding: 0 var(--space-6); }
.container--wide { max-width: var(--container-wide); }
.container--narrow { max-width: var(--container-narrow); }
.divider {
height: 1px;
background: var(--color-border-subtle);
border: none;
margin: 0;
}
/* ---------- Utilities ---------- */
.text-secondary { color: var(--color-text-secondary); }
.text-tertiary { color: var(--color-text-tertiary); }
.text-mono { font-family: var(--font-family-mono); }
.text-sm { font-size: var(--font-size-sm); }
.text-xs { font-size: var(--font-size-xs); }
.text-lg { font-size: var(--font-size-lg); }
.font-medium { font-weight: var(--font-weight-medium); }
.font-semibold { font-weight: var(--font-weight-semibold); }
.tabular { font-variant-numeric: tabular-nums; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* ---------- Print ---------- */
@media print {
body { background: #fff; color: #000; font-size: 11pt; }
.no-print, button.btn, nav, .nav, .toolbar, .tweaks-panel { display: none !important; }
.card { border: 1px solid #000; box-shadow: none; break-inside: avoid; }
a { color: #000; text-decoration: underline; }
h1, h2, h3 { break-after: avoid; }
.matrix-cell { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
@page { margin: 18mm; }
}

View file

@ -0,0 +1,352 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/* =============================================================================
components-tier2.css Tier 2 components (Phase 2)
7. Decision-tree (AI Act 4-step)
8. Traffic-lights
9. Diff-review
10. Treemap (config-audit token hotspots)
11. Distribution / range-viz (P10/P50/P90)
12. Command-pipeline output
13. Pyramide (AI Act 4-tier)
14. Pipeline-cockpit
15. Verdict-pill with risk-meter
16. Codepoint-reveal (security Unicode steg)
17. Inherent + residual pair (already partially in Tier 1, formalize)
18. Small-multiples grid
============================================================================= */
/* DECISION-TREE — vertical flowchart with 4 colored terminals */
.decision-tree { display: flex; flex-direction: column; align-items: center; gap: 0; }
.dt-node {
padding: 12px 18px;
background: var(--color-surface);
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
text-align: center;
min-width: 240px;
max-width: 340px;
}
.dt-edge {
width: 1px; height: 28px; background: var(--color-border-moderate);
position: relative;
}
.dt-edge__label {
position: absolute;
left: 8px; top: 50%; transform: translateY(-50%);
font-size: 11px; color: var(--color-text-tertiary);
white-space: nowrap;
font-family: var(--font-family-mono);
}
.dt-node--terminal { color: #fff; border: none; padding: 14px 20px; font-weight: var(--font-weight-semibold); }
.dt-node--forbidden { background: var(--color-severity-extreme); }
.dt-node--high { background: var(--color-severity-critical); }
.dt-node--limited { background: var(--color-severity-medium); color: var(--color-severity-medium-on); }
.dt-node--minimal { background: var(--color-severity-low); }
.dt-row { display: flex; gap: var(--space-3); }
/* TRAFFIC-LIGHTS */
.traffic-light {
display: inline-flex; align-items: center; gap: 8px;
padding: 6px 12px;
border-radius: var(--radius-md);
background: var(--color-bg-soft);
border: 1px solid var(--color-border-subtle);
font-size: var(--font-size-sm);
}
.traffic-light__dot {
width: 10px; height: 10px; border-radius: 50%;
flex-shrink: 0;
}
.traffic-light[data-status="green"] .traffic-light__dot { background: var(--color-state-success); }
.traffic-light[data-status="yellow"] .traffic-light__dot { background: var(--color-severity-medium); }
.traffic-light[data-status="red"] .traffic-light__dot { background: var(--color-severity-critical); }
.traffic-light[data-status="gray"] .traffic-light__dot { background: var(--color-text-tertiary); }
.traffic-light__label { font-weight: var(--font-weight-medium); }
.traffic-light__why { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
/* DIFF-REVIEW */
.diff { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); overflow: hidden; }
.diff__row { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--color-border-subtle); }
.diff__row:first-child { border-top: none; }
.diff__cell { padding: 10px 14px; font-size: var(--font-size-sm); font-family: var(--font-family-mono); }
.diff__cell--removed { background: var(--color-severity-critical-soft); color: var(--color-severity-critical-on); border-right: 1px solid var(--color-border-subtle); }
.diff__cell--added { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.diff__cell--unchanged { color: var(--color-text-secondary); border-right: 1px solid var(--color-border-subtle); }
.diff__summary { display: flex; gap: var(--space-4); padding: 12px 16px; background: var(--color-bg-soft); border-bottom: 1px solid var(--color-border-subtle); font-size: var(--font-size-sm); }
.diff__summary-item { display: flex; gap: 6px; align-items: baseline; }
.diff__summary-count { font-weight: var(--font-weight-semibold); font-variant-numeric: tabular-nums; }
/* TREEMAP — pure CSS treemap with grid */
.treemap {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-auto-rows: 36px;
gap: 2px;
background: var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
padding: 2px;
}
.treemap__tile {
padding: 8px 10px;
font-size: var(--font-size-xs);
display: flex;
flex-direction: column;
justify-content: space-between;
color: #fff;
overflow: hidden;
cursor: pointer;
position: relative;
}
.treemap__tile-label { font-weight: var(--font-weight-semibold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.treemap__tile-tokens { font-family: var(--font-family-mono); font-size: 11px; opacity: 0.85; }
.treemap__tile[data-kind="claudemd"] { background: #4338CA; }
.treemap__tile[data-kind="plugin"] { background: #0F6E76; }
.treemap__tile[data-kind="skill"] { background: #9A6700; }
.treemap__tile[data-kind="mcp"] { background: #3F5963; }
.treemap__tile[data-kind="hook"] { background: #A40E26; }
/* DISTRIBUTION / range-viz */
.distribution { display: flex; flex-direction: column; gap: var(--space-3); }
.distribution__row { display: grid; grid-template-columns: 140px 1fr; gap: var(--space-3); align-items: center; font-size: var(--font-size-sm); }
.distribution__label { color: var(--color-text-secondary); }
.distribution__track {
position: relative; height: 28px;
background: var(--color-surface-sunken);
border-radius: var(--radius-sm);
overflow: visible;
}
.distribution__band {
position: absolute; top: 6px; bottom: 6px;
background: var(--color-primary-300);
border-radius: var(--radius-pill);
opacity: 0.4;
}
.distribution__median {
position: absolute; top: 0; bottom: 0; width: 2px;
background: var(--color-primary-700);
}
.distribution__median-label {
position: absolute; top: -18px; left: 50%; transform: translateX(-50%);
font-size: 11px; font-family: var(--font-family-mono); white-space: nowrap;
color: var(--color-text-primary); font-weight: var(--font-weight-semibold);
}
.distribution__axis {
display: grid; grid-template-columns: 140px 1fr; gap: var(--space-3);
font-size: 11px; color: var(--color-text-tertiary); font-family: var(--font-family-mono);
margin-top: 4px;
}
.distribution__axis-ticks { display: flex; justify-content: space-between; }
/* COMMAND-PIPELINE OUTPUT */
.cmd-pipeline { display: flex; flex-direction: column; gap: var(--space-2); }
.cmd-step {
display: grid;
grid-template-columns: 32px 1fr auto;
gap: var(--space-3);
padding: 12px 14px;
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
align-items: center;
}
.cmd-step__num {
width: 24px; height: 24px;
border-radius: 50%;
background: var(--color-text-primary);
color: var(--color-bg);
display: flex; align-items: center; justify-content: center;
font-family: var(--font-family-mono);
font-size: 11px; font-weight: var(--font-weight-bold);
}
.cmd-step__cmd {
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
color: var(--color-text-primary);
word-break: break-all;
}
.cmd-step__cmd .cmd-flag { color: var(--color-state-info); }
.cmd-step__cmd .cmd-arg { color: var(--color-severity-medium-on); }
/* PYRAMIDE — AI Act 4-tier */
.pyramide { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.pyramide__tier {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 18px;
color: #fff;
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-sm);
border-radius: var(--radius-sm);
width: 100%;
}
.pyramide__tier--forbidden { background: var(--color-severity-extreme); max-width: 30%; }
.pyramide__tier--high { background: var(--color-severity-critical); max-width: 50%; }
.pyramide__tier--limited { background: var(--color-severity-medium); color: var(--color-severity-medium-on); max-width: 75%; }
.pyramide__tier--minimal { background: var(--color-severity-low); max-width: 100%; }
.pyramide__tier-label { display: flex; gap: var(--space-2); align-items: center; }
.pyramide__tier-share { font-family: var(--font-family-mono); font-size: 11px; opacity: 0.85; }
/* PIPELINE-COCKPIT */
.pipeline-cockpit {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0;
align-items: stretch;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
background: var(--color-surface);
}
.pc-stage {
padding: var(--space-3) var(--space-4);
border-right: 1px solid var(--color-border-subtle);
display: flex; flex-direction: column; gap: 4px;
position: relative;
}
.pc-stage:last-child { border-right: none; }
.pc-stage__num { font-family: var(--font-family-mono); font-size: 11px; color: var(--color-text-tertiary); }
.pc-stage__name { font-weight: var(--font-weight-semibold); font-size: var(--font-size-sm); }
.pc-stage__state {
font-size: 11px; padding: 2px 8px; border-radius: var(--radius-pill);
align-self: flex-start; margin-top: 4px;
font-weight: var(--font-weight-medium);
}
.pc-stage__state[data-state="done"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.pc-stage__state[data-state="running"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.pc-stage__state[data-state="empty"] { background: var(--color-bg-soft); color: var(--color-text-tertiary); }
.pc-stage__state[data-state="failed"] { background: var(--color-severity-critical); color: #fff; }
.pc-stage[data-current="true"] { background: var(--color-primary-50); }
[data-theme="dark"] .pc-stage[data-current="true"] { background: var(--color-primary-900); }
/* VERDICT-PILL with risk-meter */
.verdict-block {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-6);
align-items: center;
padding: var(--space-5) var(--space-6);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
}
.verdict-pill-lg {
display: flex; flex-direction: column; align-items: center; gap: 2px;
padding: var(--space-4) var(--space-5);
border-radius: var(--radius-md);
font-weight: var(--font-weight-bold);
letter-spacing: 0.04em;
}
.verdict-pill-lg__verdict { font-size: var(--font-size-xl); }
.verdict-pill-lg__sub { font-size: 11px; font-weight: var(--font-weight-medium); opacity: 0.8; text-transform: uppercase; letter-spacing: 0.1em; }
.verdict-pill-lg[data-verdict="block"] { background: var(--color-severity-critical); color: #fff; }
.verdict-pill-lg[data-verdict="warning"] { background: var(--color-severity-medium); color: var(--color-severity-medium-on); }
.verdict-pill-lg[data-verdict="allow"] { background: var(--color-severity-low); color: #fff; }
.risk-meter { display: flex; flex-direction: column; gap: 6px; }
.risk-meter__track {
position: relative;
height: 12px;
background: linear-gradient(to right,
var(--color-severity-low) 0%, var(--color-severity-low) 14%,
var(--color-severity-medium) 14%, var(--color-severity-medium) 39%,
var(--color-severity-high) 39%, var(--color-severity-high) 64%,
var(--color-severity-critical) 64%, var(--color-severity-critical) 84%,
var(--color-severity-extreme) 84%, var(--color-severity-extreme) 100%);
border-radius: var(--radius-pill);
}
.risk-meter__pointer {
position: absolute; top: -4px; bottom: -4px;
width: 4px;
background: var(--color-text-primary);
border-radius: 2px;
box-shadow: 0 0 0 2px var(--color-bg);
}
.risk-meter__scale {
display: flex; justify-content: space-between;
font-size: 11px; color: var(--color-text-tertiary);
font-family: var(--font-family-mono);
}
.risk-meter__bands {
display: flex; justify-content: space-between;
font-size: 11px; color: var(--color-text-secondary);
}
.risk-meter__readout {
display: flex; align-items: baseline; gap: 8px;
}
.risk-meter__score {
font-size: var(--font-size-3xl); font-weight: var(--font-weight-bold);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.risk-meter__band-label { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
/* CODEPOINT-REVEAL */
.codepoint-reveal { background: var(--color-surface-sunken); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); overflow: hidden; }
.codepoint-reveal__head { padding: 10px 14px; background: var(--color-bg-soft); border-bottom: 1px solid var(--color-border-subtle); display: flex; justify-content: space-between; align-items: center; }
.codepoint-reveal__body { padding: var(--space-4); display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.codepoint-reveal__col { display: flex; flex-direction: column; gap: 8px; }
.codepoint-reveal__col-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold); }
.codepoint-reveal__source {
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
padding: 12px;
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
min-height: 64px;
word-break: break-all;
white-space: pre-wrap;
}
.cp-tag { background: var(--color-severity-critical); color: #fff; padding: 1px 4px; border-radius: 2px; font-size: 11px; }
.cp-zw { background: var(--color-severity-medium); color: var(--color-severity-medium-on); padding: 1px 4px; border-radius: 2px; font-size: 11px; }
.cp-bidi { background: var(--color-severity-high); color: #fff; padding: 1px 4px; border-radius: 2px; font-size: 11px; }
.codepoint-reveal__decoded {
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
padding: 12px;
background: var(--color-text-primary);
color: var(--color-bg);
border-radius: var(--radius-sm);
word-break: break-all;
}
/* SMALL-MULTIPLES GRID (16-category posture) */
.small-multiples {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-3);
}
.sm-card {
padding: var(--space-3);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
display: flex; flex-direction: column; gap: 6px;
}
.sm-card__header { display: flex; justify-content: space-between; align-items: baseline; }
.sm-card__name { font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.04em; }
.sm-card__grade {
font-family: var(--font-family-mono);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
width: 28px; height: 28px;
display: flex; align-items: center; justify-content: center;
border-radius: var(--radius-sm);
}
.sm-card__grade[data-grade="A"] { background: var(--color-severity-low); color: #fff; }
.sm-card__grade[data-grade="B"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.sm-card__grade[data-grade="C"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.sm-card__grade[data-grade="D"] { background: var(--color-severity-high-soft); color: var(--color-severity-high-on); }
.sm-card__grade[data-grade="F"] { background: var(--color-severity-critical); color: #fff; }
.sm-card__bar { height: 4px; background: var(--color-surface-sunken); border-radius: var(--radius-pill); overflow: hidden; }
.sm-card__bar-fill { height: 100%; background: var(--color-primary-500); }
.sm-card__status { font-size: 11px; color: var(--color-text-tertiary); }
@media (max-width: 880px) { .small-multiples { grid-template-columns: repeat(2, 1fr); } }
/* OWASP badges (specific colors) */
.badge--owasp-llm { background: #1F2328; color: #fff; }
.badge--owasp-asi { background: #4338CA; color: #fff; }
.badge--owasp-ast { background: #9A6700; color: #fff; }
.badge--owasp-mcp { background: #0F6E76; color: #fff; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,717 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/* =============================================================================
components-tier3.css Tier 3 components (Phase 2)
Critical components for ms-ai-architect Playground v3 + universal Aksel patterns.
19. Inherent + residual pair (before/after matrix transition)
20. AI Act compliance-tidslinje (4-milepel timeline + countdown)
21. 3-track entry (Guide/Explore/Expert carried from Playground v2)
22. FRIA rights-matrix (12 EU Charter rights × impact level)
23. Capability-matrix (license × kapabilitet available/cost/missing/conditional)
24. Parallel-agent-status panel (multi-worker status grid)
25. ErrorSummary (Aksel/GOV.UK form error pattern)
26. GuidePanel (Aksel friendly inline guidance)
============================================================================= */
/* =============================================================================
19. INHERENT + RESIDUAL PAIR
Used by: ROS (before/after mitigation), DPIA, AI Act mitigations, OKR check-ins
Pattern: two cells/scores side-by-side with arrow showing transition.
============================================================================= */
.pair-before-after {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: var(--space-4);
align-items: center;
}
.pair-before-after__cell {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
display: flex;
flex-direction: column;
gap: 4px;
}
.pair-before-after__cell-label {
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
}
.pair-before-after__cell-value {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
line-height: 1;
}
.pair-before-after__cell-meta {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
.pair-before-after__cell--severity-low { border-left: 4px solid var(--color-severity-low); }
.pair-before-after__cell--severity-medium { border-left: 4px solid var(--color-severity-medium); }
.pair-before-after__cell--severity-high { border-left: 4px solid var(--color-severity-high); }
.pair-before-after__cell--severity-critical { border-left: 4px solid var(--color-severity-critical); }
.pair-before-after__cell--severity-extreme { border-left: 4px solid var(--color-severity-extreme); }
.pair-before-after__arrow {
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-2xl);
color: var(--color-text-tertiary);
line-height: 1;
user-select: none;
}
.pair-before-after__arrow::before { content: "→"; font-family: var(--font-family-sans); }
.pair-before-after__arrow--down::before { content: "↓"; }
.pair-before-after__delta {
display: inline-flex;
align-items: baseline;
gap: 4px;
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
padding: 2px 8px;
border-radius: var(--radius-pill);
margin-top: 2px;
}
.pair-before-after__delta--improved {
background: var(--color-severity-low-soft);
color: var(--color-severity-low-on);
}
.pair-before-after__delta--worsened {
background: var(--color-severity-critical-soft);
color: var(--color-severity-critical-on);
}
@media (max-width: 640px) {
.pair-before-after { grid-template-columns: 1fr; }
.pair-before-after__arrow { transform: rotate(90deg); }
}
/* =============================================================================
20. AI ACT COMPLIANCE-TIDSLINJE
Horizontal timeline with 4 fixed EU AI Act milestones (2025-02-02, 2025-08-02,
2026-08-02, 2027-08-02) plus a "today" marker and per-system countdown chips.
============================================================================= */
.aiact-timeline {
position: relative;
padding: var(--space-8) 0 var(--space-4);
margin: var(--space-4) 0;
}
.aiact-timeline__track {
position: relative;
height: 4px;
background: var(--color-border-subtle);
border-radius: var(--radius-pill);
margin: 0 12px;
}
.aiact-timeline__progress {
position: absolute;
top: 0; bottom: 0; left: 0;
background: var(--color-primary-500);
border-radius: var(--radius-pill);
/* width set inline based on today vs milestone span */
}
.aiact-timeline__milestone {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
/* left set inline as percentage based on date span */
}
.aiact-timeline__dot {
width: 16px; height: 16px;
border-radius: 50%;
background: var(--color-surface);
border: 3px solid var(--color-border-moderate);
cursor: pointer;
transition: transform var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
}
.aiact-timeline__dot:hover { transform: scale(1.15); }
.aiact-timeline__milestone[data-state="passed"] .aiact-timeline__dot {
background: var(--color-primary-500);
border-color: var(--color-primary-500);
}
.aiact-timeline__milestone[data-state="active"] .aiact-timeline__dot {
background: var(--color-severity-critical);
border-color: var(--color-severity-critical);
box-shadow: 0 0 0 4px var(--color-severity-critical-soft);
}
.aiact-timeline__milestone[data-state="upcoming"] .aiact-timeline__dot {
background: var(--color-surface);
border-color: var(--color-border-strong);
}
.aiact-timeline__today {
position: absolute;
top: -6px; bottom: -6px;
width: 2px;
background: var(--color-text-primary);
/* left set inline based on current date */
}
.aiact-timeline__today::after {
content: "I dag";
position: absolute;
top: -22px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
font-family: var(--font-family-mono);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
background: var(--color-bg);
padding: 2px 6px;
border-radius: var(--radius-sm);
white-space: nowrap;
}
.aiact-timeline__label {
position: absolute;
top: 22px; left: 50%;
transform: translateX(-50%);
text-align: center;
white-space: nowrap;
font-size: 11px;
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
}
.aiact-timeline__label-date { font-weight: var(--font-weight-semibold); display: block; }
.aiact-timeline__label-name { color: var(--color-text-tertiary); display: block; margin-top: 1px; max-width: 140px; white-space: normal; line-height: 1.2; }
.aiact-countdown {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
font-size: var(--font-size-xs);
font-family: var(--font-family-mono);
border-radius: var(--radius-pill);
background: var(--color-bg-soft);
border: 1px solid var(--color-border-subtle);
}
.aiact-countdown__days {
font-weight: var(--font-weight-bold);
font-variant-numeric: tabular-nums;
}
.aiact-countdown[data-urgency="urgent"] { background: var(--color-severity-critical-soft); color: var(--color-severity-critical-on); border-color: transparent; }
.aiact-countdown[data-urgency="soon"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); border-color: transparent; }
.aiact-countdown[data-urgency="distant"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); border-color: transparent; }
/* =============================================================================
21. 3-TRACK ENTRY (Guide / Explore / Expert)
Carried forward from Playground v2 the most-validated UX pattern in our
fleet. Three large cards as the very first decision the user makes.
============================================================================= */
.tracks {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-5);
margin: var(--space-8) 0;
}
.tracks__card {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-6);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
cursor: pointer;
transition: border-color var(--duration-fast) var(--ease-default),
transform var(--duration-fast) var(--ease-default),
box-shadow var(--duration-fast) var(--ease-default);
text-decoration: none;
color: inherit;
position: relative;
overflow: hidden;
}
.tracks__card::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 4px;
background: var(--color-border-moderate);
transition: background var(--duration-fast) var(--ease-default);
}
.tracks__card:hover {
border-color: var(--color-border-strong);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.tracks__card--guided::before { background: var(--color-state-success); }
.tracks__card--explore::before { background: var(--color-primary-500); }
.tracks__card--expert::before { background: var(--color-text-primary); }
.tracks__card-icon {
width: 40px; height: 40px;
border-radius: var(--radius-md);
background: var(--color-bg-soft);
display: flex; align-items: center; justify-content: center;
color: var(--color-text-secondary);
}
.tracks__card-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
margin: 0;
}
.tracks__card-desc {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: var(--line-height-snug);
margin: 0;
}
.tracks__card-meta {
margin-top: auto;
padding-top: var(--space-3);
display: flex; justify-content: space-between; align-items: baseline;
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
font-family: var(--font-family-mono);
}
.tracks__card-cta {
font-family: var(--font-family-sans);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
}
@media (max-width: 880px) {
.tracks { grid-template-columns: 1fr; }
}
/* =============================================================================
22. FRIA RIGHTS-MATRIX
12 EU Charter rights × impact level. Long left labels, compact right cells.
Each cell shows checkmark + severity color when right is impacted.
============================================================================= */
.rights-matrix {
display: grid;
grid-template-columns: 1fr;
gap: 1px;
background: var(--color-border-subtle);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.rights-matrix__head,
.rights-matrix__row {
display: grid;
grid-template-columns: 1fr repeat(5, 64px);
background: var(--color-surface);
}
.rights-matrix__head {
background: var(--color-bg-soft);
}
.rights-matrix__head-cell,
.rights-matrix__name,
.rights-matrix__cell {
padding: 10px 12px;
font-size: var(--font-size-sm);
display: flex;
align-items: center;
}
.rights-matrix__head-cell {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-secondary);
justify-content: center;
}
.rights-matrix__head-cell--name { justify-content: flex-start; }
.rights-matrix__name {
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
}
.rights-matrix__name-meta {
display: block;
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
font-weight: var(--font-weight-regular);
margin-top: 2px;
}
.rights-matrix__cell {
justify-content: center;
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
font-variant-numeric: tabular-nums;
color: var(--color-text-tertiary);
border-left: 1px solid var(--color-border-subtle);
}
.rights-matrix__cell[data-impact="0"]::before { content: "—"; color: var(--color-text-tertiary); }
.rights-matrix__cell[data-impact="1"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.rights-matrix__cell[data-impact="2"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.rights-matrix__cell[data-impact="3"] { background: var(--color-severity-high-soft); color: var(--color-severity-high-on); }
.rights-matrix__cell[data-impact="4"] { background: var(--color-severity-critical-soft); color: var(--color-severity-critical-on); }
.rights-matrix__cell[data-impact="5"] { background: var(--color-severity-critical); color: var(--color-severity-critical-on); }
@media (max-width: 720px) {
.rights-matrix__head,
.rights-matrix__row { grid-template-columns: 1fr repeat(5, 44px); }
.rights-matrix__head-cell,
.rights-matrix__cell { padding: 8px 6px; font-size: var(--font-size-xs); }
}
/* =============================================================================
23. CAPABILITY-MATRIX
Rows = capabilities (e.g. "Generate text via M365 Chat"), columns = licenses
(E3, E5, Copilot, etc.). Cells use one of four states with explicit icon +
color so meaning never depends solely on color.
============================================================================= */
.capability-matrix {
display: grid;
gap: 1px;
background: var(--color-border-subtle);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
font-size: var(--font-size-sm);
}
.capability-matrix__head,
.capability-matrix__row {
display: grid;
background: var(--color-surface);
/* grid-template-columns set inline based on license count */
}
.capability-matrix__head { background: var(--color-bg-soft); }
.capability-matrix__head-cell,
.capability-matrix__name,
.capability-matrix__cell {
padding: 10px 12px;
display: flex;
align-items: center;
gap: 6px;
}
.capability-matrix__head-cell {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-secondary);
justify-content: center;
}
.capability-matrix__head-cell--name { justify-content: flex-start; }
.capability-matrix__name {
font-weight: var(--font-weight-medium);
border-right: 1px solid var(--color-border-subtle);
}
.capability-matrix__cell {
justify-content: center;
font-family: var(--font-family-mono);
font-size: var(--font-size-md);
border-left: 1px solid var(--color-border-subtle);
}
.capability-matrix__cell-icon {
font-style: normal;
width: 22px; height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 13px;
font-weight: var(--font-weight-bold);
}
.capability-matrix__cell[data-status="available"] { background: var(--color-severity-low-soft); }
.capability-matrix__cell[data-status="available"] .capability-matrix__cell-icon { background: var(--color-severity-low); color: #fff; }
.capability-matrix__cell[data-status="available"] .capability-matrix__cell-icon::before { content: "✓"; }
.capability-matrix__cell[data-status="cost"] { background: var(--color-severity-medium-soft); }
.capability-matrix__cell[data-status="cost"] .capability-matrix__cell-icon { background: var(--color-severity-medium); color: #fff; }
.capability-matrix__cell[data-status="cost"] .capability-matrix__cell-icon::before { content: "kr"; font-size: 10px; }
.capability-matrix__cell[data-status="conditional"] { background: var(--color-severity-high-soft); }
.capability-matrix__cell[data-status="conditional"] .capability-matrix__cell-icon { background: var(--color-severity-high); color: #fff; }
.capability-matrix__cell[data-status="conditional"] .capability-matrix__cell-icon::before { content: "!"; }
.capability-matrix__cell[data-status="missing"] { background: var(--color-bg-soft); }
.capability-matrix__cell[data-status="missing"] .capability-matrix__cell-icon { background: var(--color-text-tertiary); color: #fff; }
.capability-matrix__cell[data-status="missing"] .capability-matrix__cell-icon::before { content: "×"; }
.capability-matrix__legend {
display: flex;
gap: var(--space-4);
flex-wrap: wrap;
font-size: var(--font-size-xs);
margin-top: var(--space-3);
color: var(--color-text-secondary);
}
.capability-matrix__legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
/* =============================================================================
24. PARALLEL-AGENT-STATUS PANEL
Used by ms-ai-architect utredning (4 parallel workers security-worker,
cost-worker, dpia-worker, diagram-worker writing to .work/-files) and
ultraplan-local multi-wave execute. Grid of agent cards with state pills,
progress bars, and per-agent metrics.
============================================================================= */
.agent-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-3);
}
.agent-card {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
position: relative;
}
.agent-card__head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-2);
}
.agent-card__name {
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-sm);
margin: 0;
}
.agent-card__role {
font-family: var(--font-family-mono);
font-size: 11px;
color: var(--color-text-tertiary);
}
.agent-card__state {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
font-size: 11px;
font-weight: var(--font-weight-medium);
border-radius: var(--radius-pill);
white-space: nowrap;
}
.agent-card__state[data-state="queued"] { background: var(--color-bg-soft); color: var(--color-text-tertiary); }
.agent-card__state[data-state="running"] { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.agent-card__state[data-state="done"] { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.agent-card__state[data-state="failed"] { background: var(--color-state-failed); color: #fff; }
.agent-card__state[data-state="blocked"] { background: var(--color-state-blocked); color: #fff; }
.agent-card__state-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: currentColor;
}
.agent-card__state[data-state="running"] .agent-card__state-dot {
animation: agent-pulse 1.4s var(--ease-default) infinite;
}
@keyframes agent-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.agent-card__progress {
height: 4px;
background: var(--color-surface-sunken);
border-radius: var(--radius-pill);
overflow: hidden;
}
.agent-card__progress-fill {
height: 100%;
background: var(--color-primary-500);
transition: width var(--duration-normal) var(--ease-default);
}
.agent-card__metrics {
display: flex;
gap: var(--space-3);
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
.agent-card__metric { display: flex; gap: 4px; align-items: baseline; }
.agent-card__metric-value {
font-variant-numeric: tabular-nums;
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.agent-card__output {
font-family: var(--font-family-mono);
font-size: 11px;
background: var(--color-surface-sunken);
padding: 6px 8px;
border-radius: var(--radius-sm);
max-height: 56px;
overflow: hidden;
color: var(--color-text-secondary);
white-space: pre-wrap;
word-break: break-word;
}
.agent-card__output::after {
content: "";
position: absolute;
bottom: var(--space-4);
left: var(--space-4);
right: var(--space-4);
height: 18px;
background: linear-gradient(to bottom, transparent, var(--color-surface));
pointer-events: none;
}
/* =============================================================================
25. ERROR-SUMMARY (Aksel/GOV.UK pattern)
Concentrated list of validation errors at top of a form. Each error
anchor-links to the offending field. Required for accessible long forms.
============================================================================= */
.error-summary {
background: var(--color-surface);
border: 1px solid var(--color-severity-critical);
border-left-width: 4px;
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.error-summary__heading {
display: flex;
align-items: center;
gap: 8px;
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
color: var(--color-severity-critical);
margin: 0;
}
[data-theme="dark"] .error-summary__heading { color: #F09095; }
.error-summary__heading::before {
content: "!";
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px; height: 20px;
border-radius: 50%;
background: var(--color-severity-critical);
color: #fff;
font-size: 14px;
font-weight: var(--font-weight-bold);
flex-shrink: 0;
}
.error-summary__body {
font-size: var(--font-size-sm);
color: var(--color-text-primary);
line-height: var(--line-height-snug);
}
.error-summary__list {
margin: var(--space-2) 0 0;
padding: 0 0 0 var(--space-5);
list-style: disc;
color: var(--color-text-primary);
}
.error-summary__item { margin-bottom: 4px; }
.error-summary__link {
color: var(--color-severity-critical);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
font-weight: var(--font-weight-medium);
}
.error-summary__link:hover { text-decoration-thickness: 2px; color: var(--color-severity-extreme); }
[data-theme="dark"] .error-summary__link { color: #F09095; }
[data-theme="dark"] .error-summary__link:hover { color: #FFB7BA; }
/* =============================================================================
26. GUIDE-PANEL (Aksel pattern)
Friendly inline guidance with optional illustration and CTA. Used to scaffold
first-time users through unfamiliar territory without scolding tone.
============================================================================= */
.guide-panel {
display: grid;
grid-template-columns: 56px 1fr auto;
gap: var(--space-4);
align-items: start;
background: var(--color-bg-soft);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
padding: var(--space-4) var(--space-5);
}
.guide-panel--info { background: #EAF3FB; border-color: rgba(9, 105, 218, 0.25); }
.guide-panel--success { background: var(--color-severity-low-soft); border-color: rgba(26, 127, 55, 0.3); }
.guide-panel--warn { background: var(--color-severity-medium-soft); border-color: rgba(191, 135, 0, 0.3); }
[data-theme="dark"] .guide-panel--info { background: #0E2A3F; border-color: rgba(111, 165, 221, 0.3); }
.guide-panel__icon {
width: 56px; height: 56px;
border-radius: var(--radius-md);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
display: flex; align-items: center; justify-content: center;
color: var(--color-primary-500);
}
.guide-panel--info .guide-panel__icon { color: var(--color-state-info); }
.guide-panel--success .guide-panel__icon { color: var(--color-state-success); }
.guide-panel--warn .guide-panel__icon { color: var(--color-severity-medium); }
.guide-panel__body {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.guide-panel__title {
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
margin: 0;
color: var(--color-text-primary);
}
.guide-panel__text {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: var(--line-height-snug);
margin: 0;
max-width: var(--measure);
}
.guide-panel__action {
align-self: center;
white-space: nowrap;
}
.guide-panel__dismiss {
position: absolute;
top: var(--space-2);
right: var(--space-2);
background: none;
border: none;
cursor: pointer;
width: 28px; height: 28px;
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
color: var(--color-text-tertiary);
font-family: inherit;
}
.guide-panel__dismiss:hover { background: rgba(0,0,0,0.06); color: var(--color-text-primary); }
@media (max-width: 640px) {
.guide-panel {
grid-template-columns: 40px 1fr;
gap: var(--space-3);
}
.guide-panel__icon { width: 40px; height: 40px; }
.guide-panel__action {
grid-column: 1 / -1;
align-self: stretch;
}
}
/* =============================================================================
Print rules for Tier 3
============================================================================= */
@media print {
.pair-before-after { page-break-inside: avoid; }
.aiact-timeline { page-break-inside: avoid; }
.agent-grid { page-break-inside: avoid; }
.tracks { display: none; } /* entry choice = screen-only */
.guide-panel__dismiss { display: none; } /* dismiss only meaningful on screen */
.error-summary {
background: #FFF !important;
border: 1pt solid #000 !important;
color: #000 !important;
}
.error-summary__heading,
.error-summary__body,
.error-summary__link { color: #000 !important; }
}

View file

@ -0,0 +1,659 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/* =============================================================================
components.css Tier 1 components (Phase 1)
1. Radar / Spider
2. Matrix / Heatmap (5x5 ROS)
3. Findings-browser
4. Critique-card
5. Wizard / Stepper
6. Live-meter / Quality-validator
============================================================================= */
/* =============================================================================
1. RADAR
============================================================================= */
.radar {
display: grid;
grid-template-columns: 1fr 240px;
gap: var(--space-6);
align-items: start;
}
.radar__chart {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
max-width: 460px;
}
.radar__svg { width: 100%; height: 100%; display: block; overflow: visible; }
.radar__grid-line { fill: none; stroke: var(--color-border-subtle); stroke-width: 1; }
.radar__axis { stroke: var(--color-border-moderate); stroke-width: 1; }
.radar__label {
font-family: var(--font-family-sans);
font-size: 12px;
font-weight: var(--font-weight-medium);
fill: var(--color-text-secondary);
text-anchor: middle;
}
.radar__tick { font-size: 10px; fill: var(--color-text-tertiary); }
.radar__series {
fill: var(--color-primary-500);
fill-opacity: 0.18;
stroke: var(--color-primary-500);
stroke-width: 2;
stroke-linejoin: round;
}
.radar__series--target {
fill: none;
stroke: var(--color-text-tertiary);
stroke-width: 1.5;
stroke-dasharray: 4 4;
}
.radar__point { fill: var(--color-primary-500); r: 4; }
.radar__point--target { fill: var(--color-bg); stroke: var(--color-text-tertiary); stroke-width: 1.5; r: 3; }
.radar__legend { display: flex; flex-direction: column; gap: var(--space-3); font-size: var(--font-size-sm); }
.radar__legend-item { display: flex; align-items: baseline; gap: var(--space-2); }
.radar__legend-swatch { width: 12px; height: 12px; border-radius: 2px; flex-shrink: 0; transform: translateY(1px); }
.radar__legend-swatch--current { background: var(--color-primary-500); }
.radar__legend-swatch--target {
background: transparent;
border: 1.5px dashed var(--color-text-tertiary);
}
.radar__scores {
margin-top: var(--space-4);
border-top: 1px solid var(--color-border-subtle);
padding-top: var(--space-3);
display: grid;
gap: 4px;
}
.radar__score-row { display: flex; justify-content: space-between; font-size: var(--font-size-xs); }
.radar__score-row dt { color: var(--color-text-secondary); }
.radar__score-row dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: var(--font-weight-medium); }
@media (max-width: 720px) {
.radar { grid-template-columns: 1fr; }
}
/* =============================================================================
2. MATRIX / HEATMAP (5x5 ROS)
============================================================================= */
.matrix {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-3);
}
.matrix__y-label {
writing-mode: vertical-rl;
transform: rotate(180deg);
text-align: center;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
letter-spacing: 0.06em;
text-transform: uppercase;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
}
.matrix__main { display: flex; flex-direction: column; gap: var(--space-2); }
.matrix__grid {
display: grid;
grid-template-columns: 32px repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr) 32px;
gap: 4px;
aspect-ratio: 5 / 5;
width: 100%;
}
.matrix__y-tick {
display: flex; align-items: center; justify-content: center;
font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
font-variant-numeric: tabular-nums;
}
.matrix__x-tick {
display: flex; align-items: center; justify-content: center;
font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
font-variant-numeric: tabular-nums;
}
.matrix__corner { /* empty bottom-left */ }
.matrix__cell {
position: relative;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
cursor: pointer;
border: 1px solid transparent;
transition: transform var(--duration-fast) var(--ease-default),
box-shadow var(--duration-fast) var(--ease-default);
min-height: 64px;
background: var(--color-severity-low-soft);
}
.matrix__cell:hover { transform: scale(1.02); box-shadow: var(--shadow-md); z-index: 2; }
.matrix__cell[aria-selected="true"] {
outline: 3px solid var(--color-primary-500);
outline-offset: 2px;
z-index: 3;
}
/* Severity zones based on score (sannsynlighet × konsekvens, 1-25) */
.matrix__cell[data-score="1"],
.matrix__cell[data-score="2"],
.matrix__cell[data-score="3"],
.matrix__cell[data-score="4"] { background: var(--color-severity-low-soft); }
.matrix__cell[data-score="5"],
.matrix__cell[data-score="6"],
.matrix__cell[data-score="8"] { background: var(--color-severity-low-soft); }
.matrix__cell[data-score="9"],
.matrix__cell[data-score="10"],
.matrix__cell[data-score="12"] { background: var(--color-severity-medium-soft); }
.matrix__cell[data-score="15"],
.matrix__cell[data-score="16"] { background: var(--color-severity-high-soft); }
.matrix__cell[data-score="20"],
.matrix__cell[data-score="25"] { background: var(--color-severity-critical-soft); }
.matrix__cell-score {
position: absolute;
top: 4px;
left: 6px;
font-size: 11px;
font-weight: var(--font-weight-semibold);
color: var(--color-text-tertiary);
font-variant-numeric: tabular-nums;
}
.matrix__cell-bubbles {
display: flex;
flex-wrap: wrap;
gap: 3px;
align-items: center;
justify-content: center;
padding: 12px 6px 6px;
}
.matrix__bubble {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
font-size: 10px;
font-weight: var(--font-weight-semibold);
font-family: var(--font-family-mono);
color: var(--color-text-primary);
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(15, 18, 22, 0.18);
border-radius: var(--radius-pill);
}
.matrix__bubble--count {
background: var(--color-text-primary);
color: var(--color-bg);
border: none;
}
/* B-DS-3 (v0.4.0): bobler rendres som <button> i renderMatrixHtml gi
visuell + keyboard-fokus-feedback. Antar at consumer bruker
<button class="matrix__bubble">, ellers bare-virkning ufarlig <span>. */
.matrix__bubble {
cursor: pointer;
transition: transform var(--duration-fast) var(--ease-default);
}
.matrix__bubble:hover { transform: scale(1.15); }
.matrix__bubble:focus-visible { outline: 2px solid var(--color-primary-500); outline-offset: 2px; }
[data-theme="dark"] .matrix__bubble { background: rgba(0,0,0,0.45); color: var(--color-text-primary); border-color: rgba(255,255,255,0.15); }
.matrix__x-label {
text-align: center;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-top: var(--space-1);
}
.matrix__legend {
display: flex; gap: var(--space-4); flex-wrap: wrap;
font-size: var(--font-size-xs);
margin-top: var(--space-3);
color: var(--color-text-secondary);
}
.matrix__legend-swatch {
display: inline-block; width: 14px; height: 14px;
border-radius: 3px; margin-right: 6px; vertical-align: -3px;
}
/* =============================================================================
3. FINDINGS-BROWSER
============================================================================= */
.findings {
display: grid;
grid-template-columns: 360px 1fr;
gap: var(--space-6);
align-items: start;
}
.findings__list {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
max-height: 640px;
display: flex;
flex-direction: column;
}
.findings__toolbar {
display: flex;
gap: var(--space-2);
padding: var(--space-3);
border-bottom: 1px solid var(--color-border-subtle);
background: var(--color-bg-soft);
align-items: center;
}
.findings__search {
flex: 1;
padding: 6px 10px;
font-size: var(--font-size-xs);
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-md);
background: var(--color-surface);
color: inherit;
font-family: inherit;
}
.findings__group {
border-bottom: 1px solid var(--color-border-subtle);
}
.findings__group-header {
padding: 8px 12px;
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
background: var(--color-bg-soft);
display: flex;
justify-content: space-between;
align-items: center;
}
.findings__items {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
}
.findings__item {
padding: 10px 12px;
border-top: 1px solid var(--color-border-subtle);
cursor: pointer;
display: grid;
grid-template-columns: auto 1fr;
gap: 8px 10px;
align-items: start;
transition: background var(--duration-fast) var(--ease-default);
}
.findings__item:first-child { border-top: none; }
.findings__item:hover { background: var(--color-bg-soft); }
.findings__item[aria-selected="true"] {
background: var(--color-primary-50);
box-shadow: inset 3px 0 0 var(--color-primary-500);
}
[data-theme="dark"] .findings__item[aria-selected="true"] { background: var(--color-primary-900); }
.findings__item-id {
font-family: var(--font-family-mono);
font-size: 11px;
color: var(--color-text-tertiary);
grid-column: 2;
}
.findings__item-title {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: 1.4;
color: var(--color-text-primary);
grid-column: 2;
}
.findings__item-meta {
display: flex;
gap: 6px;
flex-wrap: wrap;
grid-column: 2;
}
.findings__item-severity-dot {
width: 8px; height: 8px; border-radius: 50%;
margin-top: 7px;
grid-row: 1 / span 3;
}
.findings__item-severity-dot[data-severity="critical"] { background: var(--color-severity-critical); }
.findings__item-severity-dot[data-severity="high"] { background: var(--color-severity-high); }
.findings__item-severity-dot[data-severity="medium"] { background: var(--color-severity-medium); }
.findings__item-severity-dot[data-severity="low"] { background: var(--color-severity-low); }
.findings__item-severity-dot[data-severity="info"] { background: var(--color-text-tertiary); }
.findings__detail {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
padding: var(--space-6);
}
@media (max-width: 880px) { .findings { grid-template-columns: 1fr; } }
/* =============================================================================
4. CRITIQUE-CARD
============================================================================= */
.critique-card {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-left: 4px solid var(--color-border-moderate);
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.critique-card[data-severity="critical"] { border-left-color: var(--color-severity-critical); }
.critique-card[data-severity="high"] { border-left-color: var(--color-severity-high); }
.critique-card[data-severity="medium"] { border-left-color: var(--color-severity-medium); }
.critique-card[data-severity="low"] { border-left-color: var(--color-severity-low); }
.critique-card[data-severity="info"] { border-left-color: var(--color-state-info); }
.critique-card__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-3);
}
.critique-card__title {
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
margin: 0;
}
.critique-card__meta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.critique-card__id {
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
}
.critique-card__evidence {
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: 8px 10px;
white-space: pre-wrap;
word-break: break-word;
color: var(--color-text-secondary);
}
.critique-card__recommendation {
font-size: var(--font-size-sm);
color: var(--color-text-primary);
line-height: var(--line-height-snug);
}
.critique-card__actions {
display: flex;
gap: var(--space-2);
margin-top: 4px;
flex-wrap: wrap;
}
.critique-card[data-status="approved"] { opacity: 0.65; background: var(--color-bg-soft); }
.critique-card[data-status="rejected"] { opacity: 0.5; }
/* =============================================================================
5. WIZARD / STEPPER
============================================================================= */
.stepper {
display: flex;
gap: 0;
margin-bottom: var(--space-8);
border-bottom: 1px solid var(--color-border-subtle);
padding-bottom: var(--space-4);
overflow-x: auto;
}
.stepper__step {
flex: 1;
min-width: 140px;
display: flex;
align-items: center;
gap: var(--space-3);
padding: 0 var(--space-4) 0 0;
text-align: left;
background: none;
border: none;
cursor: pointer;
position: relative;
font-family: inherit;
color: var(--color-text-tertiary);
}
.stepper__step:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 1px;
background: var(--color-border-moderate);
}
.stepper__step-number {
display: flex;
align-items: center;
justify-content: center;
width: 28px; height: 28px;
border-radius: 50%;
border: 1.5px solid var(--color-border-moderate);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-tertiary);
background: var(--color-surface);
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.stepper__step-text {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.stepper__step-label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: inherit;
line-height: 1.3;
}
.stepper__step-hint {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
line-height: 1.3;
}
.stepper__step[data-state="active"] { color: var(--color-text-primary); }
.stepper__step[data-state="active"] .stepper__step-number { border-color: var(--color-primary-500); background: var(--color-primary-500); color: #fff; }
.stepper__step[data-state="complete"] { color: var(--color-text-secondary); }
.stepper__step[data-state="complete"] .stepper__step-number { border-color: var(--color-state-success); background: var(--color-state-success); color: #fff; }
.stepper__step[data-state="complete"] .stepper__step-number::before { content: '✓'; font-size: 14px; }
.stepper__step[data-state="complete"] .stepper__step-number-text { display: none; }
.wizard__panel { display: none; }
.wizard__panel[data-active="true"] { display: block; }
.wizard__nav {
display: flex;
justify-content: space-between;
margin-top: var(--space-8);
padding-top: var(--space-6);
border-top: 1px solid var(--color-border-subtle);
}
/* =============================================================================
6. LIVE-METER
============================================================================= */
.live-meter {
display: grid;
gap: var(--space-3);
}
.live-meter__row {
display: grid;
grid-template-columns: 180px 1fr 56px;
gap: var(--space-3);
align-items: center;
font-size: var(--font-size-sm);
}
.live-meter__label { color: var(--color-text-secondary); }
.live-meter__bar {
height: 8px;
background: var(--color-surface-sunken);
border-radius: var(--radius-pill);
overflow: hidden;
position: relative;
}
.live-meter__bar-fill {
height: 100%;
background: var(--color-primary-500);
border-radius: var(--radius-pill);
transition: width var(--duration-normal) var(--ease-default);
}
.live-meter__bar-fill[data-state="pass"] { background: var(--color-state-success); }
.live-meter__bar-fill[data-state="weak"] { background: var(--color-severity-medium); }
.live-meter__bar-fill[data-state="fail"] { background: var(--color-severity-critical); }
.live-meter__value {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-sm);
}
.live-meter__overall {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: var(--space-3) var(--space-4);
background: var(--color-bg-soft);
border-radius: var(--radius-md);
margin-top: var(--space-2);
}
.live-meter__overall-value {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
/* Antipattern annotations (inline, subtle) */
.lint-annotation {
display: inline-flex;
gap: 6px;
padding: 6px 10px;
margin-top: 6px;
background: var(--color-severity-medium-soft);
border-left: 3px solid var(--color-severity-medium);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
font-size: var(--font-size-xs);
color: var(--color-severity-medium-on);
line-height: var(--line-height-snug);
}
.lint-annotation--error {
background: var(--color-severity-critical-soft);
color: var(--color-severity-critical);
border-left-color: var(--color-severity-critical);
}
.lint-annotation__code {
font-family: var(--font-family-mono);
font-weight: var(--font-weight-semibold);
}
/* =============================================================================
App shell header / nav (used by Scenario A and showcase)
============================================================================= */
.app-header {
position: sticky;
top: 0;
z-index: 50;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border-subtle);
padding: var(--space-3) var(--space-6);
display: flex;
align-items: center;
gap: var(--space-4);
}
.app-header__brand {
display: flex;
align-items: center;
gap: var(--space-3);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-md);
text-decoration: none;
color: var(--color-text-primary);
}
.app-header__brand-mark {
width: 28px; height: 28px;
background: var(--color-primary-500);
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
color: #fff;
font-family: var(--font-family-mono);
font-size: 13px;
font-weight: 700;
}
.app-header__breadcrumb {
color: var(--color-text-tertiary);
font-size: var(--font-size-sm);
display: flex; gap: var(--space-2); align-items: center;
}
.app-header__spacer { flex: 1; }
.app-header__actions { display: flex; gap: var(--space-2); align-items: center; }
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border: 1px solid var(--color-border-moderate);
border-radius: var(--radius-md);
background: var(--color-surface);
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-family: inherit;
cursor: pointer;
}
.theme-toggle:hover { border-color: var(--color-border-strong); color: var(--color-text-primary); }
/* Detail sidepanel (slides from right) */
.sidepanel {
position: fixed;
inset: 0 0 0 auto;
width: min(560px, 92vw);
background: var(--color-surface);
border-left: 1px solid var(--color-border-subtle);
box-shadow: var(--shadow-lg);
transform: translateX(100%);
transition: transform var(--duration-normal) var(--ease-default);
z-index: 100;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidepanel[data-open="true"] { transform: translateX(0); }
.sidepanel__header {
padding: var(--space-4) var(--space-6);
border-bottom: 1px solid var(--color-border-subtle);
display: flex; justify-content: space-between; align-items: flex-start;
gap: var(--space-3);
}
.sidepanel__body {
flex: 1;
overflow-y: auto;
padding: var(--space-6);
}
.sidepanel__close {
background: none; border: none; cursor: pointer;
width: 32px; height: 32px;
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
color: var(--color-text-secondary);
}
.sidepanel__close:hover { background: var(--color-bg-soft); color: var(--color-text-primary); }
.scrim {
position: fixed; inset: 0;
background: var(--color-overlay);
opacity: 0;
pointer-events: none;
transition: opacity var(--duration-normal) var(--ease-default);
z-index: 99;
}
.scrim[data-open="true"] { opacity: 1; pointer-events: auto; }

View file

@ -0,0 +1,84 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/*
* Self-hosted web fonts for Playground Design System.
*
* All three families are licensed under SIL Open Font License 1.1.
* Full license text and provenance: ./fonts/LICENSES.md
*
* Why self-hosted:
* - No external requests (no fonts.googleapis.com, no IP/UA leakage).
* - Works offline / behind air-gapped firewalls.
* - GDPR-compliant for Norwegian public-sector deployments.
*
* Bundle size: ~940 KB total across 9 woff2 files.
* Loaded via font-display: swap to avoid FOIT.
*/
/* ========== Inter (UI / body) ========== */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("./fonts/Inter-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./fonts/Inter-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("./fonts/Inter-Bold.woff2") format("woff2");
}
/* ========== JetBrains Mono (code) ========== */
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/JetBrainsMono-Regular.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("./fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
}
/* ========== Source Serif 4 (occasional editorial accents) ========== */
@font-face {
font-family: "Source Serif 4";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/SourceSerif4-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Source Serif 4";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./fonts/SourceSerif4-Semibold.woff2") format("woff2");
}

View file

@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,93 @@
Copyright 2014 - 2023 Adobe (http://www.adobe.com/), with Reserved Font Name Source. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,42 @@
# Font Licenses
All three font families bundled with Playground Design System are licensed
under the SIL Open Font License, Version 1.1 (OFL-1.1). They are free to
use, modify, embed, and redistribute under the terms of OFL-1.1.
Full license text per family:
- **Inter** (Regular, Medium, SemiBold, Bold) — `LICENSE-Inter.txt`
Copyright (c) 2016 The Inter Project Authors
Source: https://github.com/rsms/inter
Version bundled: 4.0
- **JetBrains Mono** (Regular, Medium, SemiBold) — `LICENSE-JetBrainsMono.txt`
Copyright 2020 The JetBrains Mono Project Authors
Source: https://github.com/JetBrains/JetBrainsMono
Version bundled: 2.304
- **Source Serif 4** (Regular, Semibold) — `LICENSE-SourceSerif4.md`
Copyright 20142023 Adobe (Reserved Font Name "Source")
Source: https://github.com/adobe-fonts/source-serif
Version bundled: 4.005
## Provenance
Files in this directory were obtained from the upstream release artifacts
linked above on 2026-05-03. Source Serif 4 woff2 files were generated locally
from the desktop OTF release using `fonttools ttLib.woff2 compress`; all
others are unmodified from upstream webfont releases.
## Why bundled
These fonts ship with the design system to eliminate runtime requests to
external CDNs (e.g., fonts.googleapis.com). This guarantees:
- No data leakage about end-user IPs / User-Agents to third parties.
- GDPR compliance for Norwegian public-sector deployments.
- Functioning Playgrounds in offline / air-gapped environments.
Each Playground HTML loads `../shared/playground-design-system/fonts.css`,
which declares all `@font-face` rules pointing at the .woff2 files in this
directory.

View file

@ -0,0 +1,176 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/* =============================================================================
print.css A4 print stylesheet for offentlige dokumenter
- Severity-mønstre (skravur) som fungerer i B/W
- Header/footer med kommune-logo-slot, signaturfelt, paginering
- 12pt minimum kropp, 11pt for metadata
- Skjuler interaktiv chrome (header, knapper, toggles)
============================================================================= */
@page {
size: A4 portrait;
margin: 22mm 18mm 24mm 18mm;
@bottom-right { content: counter(page) " / " counter(pages); font-family: "Inter", sans-serif; font-size: 9pt; color: #555; }
}
@page :first { @top-left { content: none; } }
@page landscape { size: A4 landscape; }
/* SVG severity-mønstre (skravur) definert i print-only inline-svg.
For å bruke: legg til class .pattern-low/.pattern-medium/etc. elementet
som ellers fyller med severity-fargen. */
@media print {
:root {
--color-bg: #FFFFFF;
--color-surface: #FFFFFF;
--color-surface-sunken: #F5F5F5;
--color-bg-soft: #F7F7F7;
--color-border-subtle: #C7C7C7;
--color-border-moderate: #888888;
--color-text-primary: #000000;
--color-text-secondary: #2A2A2A;
--color-text-tertiary: #555555;
}
html, body { background: #FFFFFF !important; color: #000 !important; font-size: 11pt !important; }
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* Hide interactive chrome */
.app-header, header.app-header,
.theme-toggle, #theme-toggle, #themeToggle,
.filter-bar, .view-toggle, .screen-tabs,
.btn--primary, .btn--secondary, .btn--ghost,
.live-dot, .pane__head .badge,
.accept-banner button,
.scenario-card .btn,
.footer { display: none !important; }
/* Container = full width on print */
.container, .container--wide { max-width: none !important; padding: 0 !important; }
/* Body type */
body, p, li, dd, dt, td, th, .field__value {
font-family: "Inter", sans-serif;
font-size: 11pt; line-height: 1.45; color: #000;
}
h1 { font-size: 22pt; line-height: 1.2; margin: 0 0 6pt; }
h2 { font-size: 16pt; line-height: 1.25; margin: 18pt 0 6pt; page-break-after: avoid; }
h3 { font-size: 13pt; margin: 12pt 0 4pt; page-break-after: avoid; }
h4 { font-size: 11pt; margin: 10pt 0 3pt; }
/* Page breaks */
.page-break { page-break-before: always; }
.avoid-break, .finding, .critique, .scenario-card, table, figure {
page-break-inside: avoid;
}
/* Severity patterns (B/W-safe). Stack pattern-bg + dotted/diag border indicators. */
.matrix__cell[data-score],
.badge--severity-low, .badge--severity-medium, .badge--severity-high,
.badge--severity-critical, .badge--severity-extreme {
background-color: #FFF !important;
color: #000 !important;
border: 1px solid #000 !important;
}
.badge--severity-low::before, .badge--severity-medium::before,
.badge--severity-high::before, .badge--severity-critical::before,
.badge--severity-extreme::before {
content: ""; display: inline-block;
width: 7pt; height: 7pt; margin-right: 4pt;
border: 1px solid #000;
vertical-align: middle;
}
.badge--severity-low::before { background: #FFF; }
.badge--severity-medium::before { background: repeating-linear-gradient(45deg, #000 0 0.6pt, transparent 0.6pt 3pt); }
.badge--severity-high::before { background: repeating-linear-gradient(45deg, #000 0 1pt, transparent 1pt 2.5pt); }
.badge--severity-critical::before { background: repeating-linear-gradient(0deg, #000 0 0.5pt, transparent 0.5pt 2pt),
repeating-linear-gradient(90deg, #000 0 0.5pt, transparent 0.5pt 2pt); }
.badge--severity-extreme::before { background: #000; }
/* Matrix cells in print: skravur i stedet for farge */
.matrix__cell { color: #000 !important; border: 0.5pt solid #888 !important; }
.matrix__cell[data-score]:not([data-score="0"]) { background: #FFF !important; }
.matrix__cell[data-score="1"], .matrix__cell[data-score="2"],
.matrix__cell[data-score="3"], .matrix__cell[data-score="4"] {
background: #FFF !important;
}
.matrix__cell[data-score="5"], .matrix__cell[data-score="6"], .matrix__cell[data-score="8"] {
background: repeating-linear-gradient(45deg, rgba(0,0,0,0.18) 0 0.5pt, transparent 0.5pt 4pt) !important;
}
.matrix__cell[data-score="9"], .matrix__cell[data-score="10"], .matrix__cell[data-score="12"] {
background: repeating-linear-gradient(45deg, rgba(0,0,0,0.32) 0 0.7pt, transparent 0.7pt 3pt) !important;
}
.matrix__cell[data-score="15"], .matrix__cell[data-score="16"], .matrix__cell[data-score="20"] {
background: repeating-linear-gradient(45deg, rgba(0,0,0,0.48) 0 1pt, transparent 1pt 2pt) !important;
}
.matrix__cell[data-score="25"] { background: #000 !important; color: #FFF !important; }
.matrix__cell[data-score="25"] .matrix__cell-score { color: #FFF !important; }
/* Surfaces flat */
.card, .pane, .finding, .critique, .scenario-card, .posture-summary, .verdict-block {
background: #FFF !important;
border: 0.5pt solid #888 !important;
box-shadow: none !important;
border-radius: 0 !important;
}
/* Links visible but not underlined-everything */
a { color: #000; text-decoration: none; }
a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #555; }
a[href^="#"]::after, a[href^="/"]::after, a:not([href*="://"])::after { content: ""; }
/* Standard footer block: signaturfelt for offentlige dokumenter */
.print-footer {
margin-top: 24pt;
padding-top: 10pt;
border-top: 0.5pt solid #888;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18pt;
font-size: 10pt;
}
.print-signature { display: flex; flex-direction: column; gap: 28pt; }
.print-signature__line {
border-bottom: 0.5pt solid #000;
height: 28pt;
}
.print-signature__caption {
font-size: 9pt;
color: #555;
}
/* Header for offisielle rapporter — kommune-logo-slot */
.print-header {
display: grid;
grid-template-columns: auto 1fr;
gap: 14pt;
align-items: center;
padding-bottom: 10pt;
margin-bottom: 16pt;
border-bottom: 0.5pt solid #888;
}
.print-header__logo {
width: 40pt; height: 40pt;
border: 0.5pt solid #888;
display: flex; align-items: center; justify-content: center;
font-family: "Inter", sans-serif; font-size: 9pt; color: #888;
}
.print-header__meta { font-size: 9pt; color: #555; }
.print-header__meta strong { color: #000; }
/* Avoid orphan headings */
h2, h3, h4 { orphans: 3; widows: 3; }
p, li { orphans: 2; widows: 2; }
}
/* Screen-mode preview class — see print preview without actually printing */
.preview-print { background: #ddd; padding: var(--space-8); }
.preview-print .a4 {
width: 210mm; min-height: 297mm;
margin: 0 auto;
background: #fff;
padding: 22mm 18mm;
box-shadow: 0 6px 24px rgba(0,0,0,0.18);
font-size: 11pt; line-height: 1.45; color: #000;
}
.preview-print .a4 + .a4 { margin-top: 12mm; }

View file

@ -0,0 +1,88 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://playground-ds.no/schemas/finding.json",
"title": "Finding",
"description": "Et enkelt funn fra en plugin-skanning. Brukes av llm-security, config-audit, ultraplan-review og ms-ai-review.",
"type": "object",
"required": ["id", "title", "severity", "source"],
"properties": {
"id": {
"type": "string",
"description": "Stabil ID, f.eks. DDT-2026-118-F-001",
"pattern": "^[A-Z0-9-]{4,}$"
},
"title": { "type": "string", "minLength": 4, "maxLength": 140 },
"severity": {
"enum": ["info", "low", "medium", "high", "critical"],
"description": "Standard 5-trinns skala. Maps til CSS-tokens --color-severity-*."
},
"score": {
"type": "number", "minimum": 0, "maximum": 10,
"description": "CVSS-lignende numerisk score. Valgfri — severity er primær."
},
"rules": {
"type": "array",
"items": { "type": "string", "pattern": "^[A-Z]{2,4}[0-9]{2}(\\.[0-9]+)?$" },
"description": "Regler/categories truffet, f.eks. LLM01, ASI02, DDT01"
},
"source": {
"type": "object",
"required": ["kind", "ref"],
"properties": {
"kind": { "enum": ["document", "prompt-response", "code-file", "config-file", "okr-set"] },
"ref": { "type": "string", "description": "Filnavn / URL / sak-ID" },
"line": { "type": "integer", "minimum": 1 },
"col": { "type": "integer", "minimum": 0 },
"snippet": { "type": "string", "maxLength": 800 }
}
},
"evidence": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "value"],
"properties": {
"kind": { "enum": ["text", "codepoint", "metric", "url", "image"] },
"value": { "type": "string" },
"label": { "type": "string" }
}
}
},
"rationale": { "type": "string", "description": "Norsk forklaring av hvorfor dette er et problem i denne konteksten" },
"recommendation": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"steps": { "type": "array", "items": { "type": "string" } },
"ttf": { "type": "string", "description": "Tid til løsning, f.eks. '2 t', '1 d', '5 d'" },
"owner": { "type": "string", "description": "Foreslått eier (rolle eller person)" }
}
},
"references": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": { "type": "string" },
"url": { "type": "string", "format": "uri" }
}
}
},
"status": {
"enum": ["new", "acknowledged", "in-progress", "fixed", "accepted-risk", "false-positive"],
"default": "new"
},
"acceptance": {
"type": "object",
"description": "Påkrevd hvis status = accepted-risk og severity ≥ high",
"properties": {
"approver": { "type": "string" },
"date": { "type": "string", "format": "date" },
"rationale": { "type": "string" },
"review_by": { "type": "string", "format": "date" }
}
},
"created": { "type": "string", "format": "date-time" },
"updated": { "type": "string", "format": "date-time" }
}
}

View file

@ -0,0 +1,78 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://playground-ds.no/schemas/okr-set.json",
"title": "OKR-sett",
"description": "Et OKR-sett: ett mål (Objective) med 16 nøkkelresultater (KR). Brukes av OKR live-writer.",
"type": "object",
"required": ["id", "objective", "key_results", "owner", "period"],
"properties": {
"id": { "type": "string" },
"owner": {
"type": "object",
"required": ["name", "unit"],
"properties": {
"name": { "type": "string" },
"unit": { "type": "string", "description": "Avdeling/seksjon" },
"org": { "type": "string", "description": "Kommune/etat" }
}
},
"period": {
"type": "object",
"required": ["kind", "label", "start", "end"],
"properties": {
"kind": { "enum": ["tertial", "kvartal", "halvår", "år"] },
"label": { "type": "string", "description": "f.eks. 'T2 2026'" },
"start": { "type": "string", "format": "date" },
"end": { "type": "string", "format": "date" }
}
},
"objective": {
"type": "object",
"required": ["text"],
"properties": {
"text": { "type": "string", "minLength": 10, "maxLength": 240 },
"rationale": { "type": "string" }
}
},
"key_results": {
"type": "array", "minItems": 1, "maxItems": 6,
"items": {
"type": "object",
"required": ["id", "text"],
"properties": {
"id": { "type": "string", "pattern": "^KR[0-9]+$" },
"text": { "type": "string" },
"metric": {
"type": "object",
"properties": {
"name": { "type": "string" },
"unit": { "type": "string", "description": "%, dager, kr, antall, …" },
"baseline": { "type": "number" },
"target": { "type": "number" },
"stretch": { "type": "number" },
"source": { "type": "string", "description": "KPI-katalog ref / Tableau-sett / etc." }
}
},
"deadline": { "type": "string", "format": "date" }
}
}
},
"score": {
"type": "object",
"description": "Generert av OKR-writer ved kvalitetsanalyse",
"properties": {
"overall": { "type": "number", "minimum": 0, "maximum": 100 },
"measurability": { "type": "number" },
"specificity": { "type": "number" },
"ambition": { "type": "number" },
"actionability": { "type": "number" }
}
},
"critiques": {
"type": "array",
"items": { "$ref": "https://playground-ds.no/schemas/finding.json" }
},
"version": { "type": "string", "description": "Semver eller utkast 0.4-stil" },
"status": { "enum": ["draft", "in-review", "approved", "active", "closed"], "default": "draft" }
}
}

View file

@ -0,0 +1,59 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://playground-ds.no/schemas/ros-threat.json",
"title": "ROS-trussel",
"description": "Én identifisert trussel i en risiko- og sårbarhetsanalyse. NS 5814-justert.",
"type": "object",
"required": ["id", "title", "category", "inherent"],
"properties": {
"id": { "type": "string", "pattern": "^T-[0-9]{3,}$" },
"title": { "type": "string" },
"description": { "type": "string" },
"category": {
"enum": ["personvern", "informasjonssikkerhet", "datakvalitet",
"compliance", "dataintegritet", "leverandørrisiko",
"tilgjengelighet", "omdømme", "økonomi", "andre"]
},
"actors": {
"type": "array",
"items": { "enum": ["intern-bruker", "saksbehandler", "innbygger", "ekstern-aktør", "leverandør", "system", "ai-modell"] }
},
"inherent": {
"type": "object",
"required": ["likelihood", "consequence"],
"properties": {
"likelihood": { "type": "integer", "minimum": 1, "maximum": 5 },
"consequence": { "type": "integer", "minimum": 1, "maximum": 5 },
"rationale": { "type": "string" }
}
},
"controls": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title"],
"properties": {
"id": { "type": "string", "pattern": "^M-[0-9]{3,}$" },
"title": { "type": "string" },
"kind": { "enum": ["preventiv", "deteksjon", "korreksjon", "policy", "opplæring", "teknisk"] },
"status": { "enum": ["planlagt", "implementert", "validert", "ute-av-drift"] },
"owner": { "type": "string" },
"due": { "type": "string", "format": "date" }
}
}
},
"residual": {
"type": "object",
"properties": {
"likelihood": { "type": "integer", "minimum": 1, "maximum": 5 },
"consequence": { "type": "integer", "minimum": 1, "maximum": 5 },
"rationale": { "type": "string" }
}
},
"regulatory_refs": {
"type": "array",
"items": { "type": "string", "description": "GDPR Art. 35, AI Act Art. 6, NS 5814, …" }
},
"status": { "enum": ["open", "mitigating", "monitored", "closed", "transferred"], "default": "open" }
}
}

View file

@ -0,0 +1,232 @@
/* Code generated by sync-design-system.mjs; DO NOT EDIT. */
/* =============================================================================
Playground Design System tokens.css
v0.1 Phase 1
Aksel/Digdir-aligned. Norwegian public sector. WCAG 2.1 AA.
============================================================================= */
:root {
/* ---------- Typography -------------------------------------------------- */
--font-family-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
--font-family-mono: "JetBrains Mono", "SF Mono", "Fira Code", ui-monospace, monospace;
--font-family-serif: "Source Serif 4", Georgia, serif;
--font-size-xs: 13px;
--font-size-sm: 15px;
--font-size-md: 17px; /* body default */
--font-size-lg: 19px;
--font-size-xl: 23px;
--font-size-2xl: 28px;
--font-size-3xl: 34px;
--font-size-4xl: 44px;
--line-height-tight: 1.2;
--line-height-snug: 1.4;
--line-height-normal: 1.55;
--measure: 65ch;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* ---------- Primary (Digdir) ------------------------------------------- */
--color-primary-50: #E8F1FB;
--color-primary-100: #C6DCF4;
--color-primary-200: #9CC0EA;
--color-primary-300: #6FA5DD;
--color-primary-400: #3B83CB;
--color-primary-500: #0062BA; /* Digdir blue */
--color-primary-600: #00569F;
--color-primary-700: #004A8F;
--color-primary-800: #003A70;
--color-primary-900: #002F5C;
/* ---------- Severity ramp (deuteranopia-safe) ------------------------- */
--color-severity-low: #1A7F37;
--color-severity-medium: #BF8700;
--color-severity-high: #CC5A00;
--color-severity-critical: #A40E26;
--color-severity-extreme: #66050F;
/* Soft fills (matrix cells, badges) */
--color-severity-low-soft: #DDF4E4;
--color-severity-medium-soft: #FBF0CC;
--color-severity-high-soft: #FCE0CC;
--color-severity-critical-soft: #F8D7DC;
--color-severity-extreme-soft: #E8C7CC;
/* Foreground on severity bg */
--color-severity-low-on: #0E4A20;
--color-severity-medium-on: #5C3F00;
--color-severity-high-on: #5C2900;
--color-severity-critical-on: #FFFFFF;
--color-severity-extreme-on: #FFFFFF;
/* ---------- State (distinct from severity) --------------------------- */
--color-state-success: #1A7F37;
--color-state-warning: #BF8700;
--color-state-failed: #7D1A1A; /* dark desaturated red — "broke" */
--color-state-blocked: #5C2D91; /* purple — distinct */
--color-state-info: #0969DA;
--color-state-running: #BF8700;
--color-state-queued: #6E7781;
--color-state-pending: #4D7DAD;
--color-state-done: #1A7F37;
/* ---------- Surface / background ------------------------------------- */
--color-bg: #FBFAF7; /* warm off-white page */
--color-bg-soft: #F4F2EC; /* subtle section */
--color-surface: #FFFFFF;
--color-surface-raised: #FFFFFF;
--color-surface-sunken: #F1EEE7;
--color-overlay: rgba(15, 18, 22, 0.45);
/* ---------- Border --------------------------------------------------- */
--color-border-subtle: #E4E0D6;
--color-border-moderate: #C8C2B3;
--color-border-strong: #6E7781;
--color-border-focus: #0062BA;
/* ---------- Text ----------------------------------------------------- */
--color-text-primary: #1F2328;
--color-text-secondary: #4D5663;
--color-text-tertiary: #6E7781;
--color-text-on-primary: #FFFFFF;
--color-text-link: #00569F;
--color-text-link-hover: #002F5C;
/* ---------- Plugin scope colors -------------------------------------- */
--color-scope-architect: #0F6E76; /* ms-ai-architect — petrol */
--color-scope-okr: #9A6700; /* OKR — amber */
--color-scope-security: #A40E26; /* llm-security — crimson */
--color-scope-ultraplan: #4338CA; /* ultraplan-local — indigo */
--color-scope-config: #3F5963; /* config-audit — slate */
/* ---------- Spacing -------------------------------------------------- */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
/* ---------- Radius --------------------------------------------------- */
--radius-sm: 3px;
--radius-md: 5px;
--radius-lg: 8px;
--radius-pill: 999px;
/* ---------- Shadow --------------------------------------------------- */
--shadow-sm: 0 1px 2px rgba(15, 18, 22, 0.04), 0 0 0 1px rgba(15, 18, 22, 0.04);
--shadow-md: 0 2px 4px rgba(15, 18, 22, 0.06), 0 4px 12px rgba(15, 18, 22, 0.04);
--shadow-lg: 0 4px 8px rgba(15, 18, 22, 0.06), 0 12px 32px rgba(15, 18, 22, 0.06);
--shadow-focus: 0 0 0 3px rgba(0, 98, 186, 0.35);
/* ---------- Motion --------------------------------------------------- */
--duration-instant: 100ms;
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 400ms;
--ease-default: cubic-bezier(0.2, 0, 0, 1);
/* ---------- Layout --------------------------------------------------- */
--container-narrow: 720px;
--container-default: 1080px;
--container-wide: 1280px;
--sidebar-width: 280px;
}
:root { color-scheme: light; }
[data-theme="dark"] {
--color-bg: #0F1419;
--color-bg-soft: #161B22;
--color-surface: #1A2027;
--color-surface-raised: #232A33;
--color-surface-sunken: #0B1015;
--color-border-subtle: #2A323C;
--color-border-moderate: #3B4452;
--color-border-strong: #6E7781;
--color-text-primary: #E6EDF3;
--color-text-secondary: #B0BAC4;
--color-text-tertiary: #8B96A2;
--color-text-link: #6FA5DD;
--color-text-link-hover: #9CC0EA;
/* Severity soft fills tuned for dark surfaces */
--color-severity-low-soft: #163322;
--color-severity-medium-soft: #3A2C0A;
--color-severity-high-soft: #3D260F;
--color-severity-critical-soft: #3B0F18;
--color-severity-extreme-soft: #2A0408;
--color-severity-low-on: #7FE0A0;
--color-severity-medium-on: #F2C66B;
--color-severity-high-on: #F09060;
--color-severity-critical-on: #FFFFFF;
--color-severity-extreme-on: #FFFFFF;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.04);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.5), 0 12px 32px rgba(0, 0, 0, 0.4);
--shadow-focus: 0 0 0 3px rgba(111, 165, 221, 0.45);
color-scheme: dark;
}
/* Light theme overrides Aksel-aligned, WCAG AA-validated.
Full mirror of the dark block (26 vars) so renderers reading any
theme-overridable token in dark mode also resolve in light mode.
See research/04-wcag-dual-theme-tokens.md for hex sources + AA validation. */
[data-theme="light"] {
--color-bg: #ffffff;
--color-bg-soft: #ecedef;
--color-surface: #ffffff;
--color-surface-raised: #f5f6f7;
--color-surface-sunken: #ecedef;
--color-border-subtle: #cfd3d8;
--color-border-moderate: #6f7785;
--color-border-strong: #5d6573;
--color-text-primary: #202733;
--color-text-secondary: #49515e;
--color-text-tertiary: #6f7785; /* borderline 4.5:1 — reserve for non-body (eyebrows, labels) */
--color-text-link: #1a5f99;
--color-text-link-hover: #002459;
/* Severity soft fills + on-colors tuned for light surfaces (Aksel). */
--color-severity-low-soft: #e2fde8;
--color-severity-medium-soft: #fff5e4;
--color-severity-high-soft: #fff2f0;
--color-severity-critical-soft: #fff2f7;
--color-severity-extreme-soft: #fff0f3;
--color-severity-low-on: #002e00;
--color-severity-medium-on: #481700;
--color-severity-high-on: #560000;
--color-severity-critical-on: #560000;
--color-severity-extreme-on: #ffffff;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.04);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.06);
--shadow-focus: 0 0 0 3px rgba(26, 95, 153, 0.4);
color-scheme: light;
}
/* Auto dark when no override */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
color-scheme: dark;
}
}

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="nb">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Voyage Annotation Playground v4.2</title>
<link rel="stylesheet" href="vendor/playground-design-system/tokens.css">
<link rel="stylesheet" href="vendor/playground-design-system/base.css">
<link rel="stylesheet" href="vendor/playground-design-system/fonts.css">
<link rel="stylesheet" href="vendor/playground-design-system/components.css">
<link rel="stylesheet" href="vendor/playground-design-system/components-tier2.css">
<link rel="stylesheet" href="vendor/playground-design-system/components-tier3.css">
<link rel="stylesheet" href="vendor/playground-design-system/components-tier3-supplement.css">
<link rel="stylesheet" href="vendor/playground-design-system/print.css" media="print">
<style>
/* Voyage-specific overrides go here in Steps 8-11 */
body { margin: 0; }
main { max-width: 1280px; margin: 0 auto; padding: var(--space-6, 2rem); }
.voyage-skeleton-msg { padding: var(--space-6, 2rem); }
</style>
</head>
<body>
<a class="visually-hidden" href="#main">Skip to main content</a>
<header class="app-header">
<div class="app-header__title">Voyage Annotation Playground</div>
<div class="app-header__meta">v4.2 · brief / plan / review</div>
</header>
<main id="main">
<section class="guide-panel guide-panel--info" role="status" aria-label="Voyage playground status">
<div class="guide-panel__title">Voyage playground v4.2 — under construction</div>
<div class="guide-panel__body">
<p>Dette er skelettet. Render-pipeline (markdown-it + highlight.js), annotation creation gestures, sidebar, export-flow og A11Y-baseline kommer i Steps 811 av v4.2-implementasjonen.</p>
<p>Når komplett vil playgroundet la deg åpne en <code>brief.md</code>, <code>plan.md</code> eller <code>review.md</code>, legge til inline-annotations, og eksportere som <code>/trekrevise</code>-kommando.</p>
</div>
</section>
</main>
<div id="aria-live-region" aria-live="polite" aria-atomic="true" class="visually-hidden"></div>
</body>
</html>

View file

@ -0,0 +1,67 @@
// tests/playground/voyage-playground.test.mjs
// Filesystem + content tests for v4.2 voyage playground.
// Pure existence + grep checks — no browser launch.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, statSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, resolve, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..', '..');
const PLAYGROUND = join(ROOT, 'playground');
const HTML = join(PLAYGROUND, 'voyage-playground.html');
const VENDOR = join(PLAYGROUND, 'vendor', 'playground-design-system');
const MANIFEST = join(VENDOR, 'MANIFEST.json');
test('voyage-playground.html exists and has nonzero size', () => {
assert.ok(existsSync(HTML), 'voyage-playground.html must exist');
assert.ok(statSync(HTML).size > 0, 'must have content');
});
test('voyage-playground.html has DOCTYPE + html closing tag', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /^<!DOCTYPE html>/i);
assert.match(text, /<\/html>\s*$/);
});
test('voyage-playground.html does NOT contain external (http/https) URLs', () => {
// SC1 zero-network constraint: all assets must be relative to ./vendor/
const text = readFileSync(HTML, 'utf-8');
assert.ok(!/https?:\/\//.test(text), 'no external URLs allowed in playground HTML');
});
test('voyage-playground.html does NOT contain literal `marked` (renderer ban per risk-assessor H1)', () => {
const text = readFileSync(HTML, 'utf-8');
// marked is disqualified by issue #3515; markdown-it locked instead
// Allow comments mentioning "marked" as an explanatory artifact, but no actual import paths
assert.ok(!/from ['"].*marked/.test(text), 'no import from marked');
assert.ok(!/<script[^>]*marked\.min\.js/.test(text), 'no marked script tag');
});
test('voyage-playground.html includes skip-to-main link (A11Y baseline)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /Skip to main content/);
});
test('voyage-playground.html declares aria-live region', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /aria-live="polite"/);
});
test('playground/vendor/playground-design-system/MANIFEST.json exists and parses as JSON with expected keys', () => {
assert.ok(existsSync(MANIFEST), 'MANIFEST.json must be present from sync-design-system.mjs');
const obj = JSON.parse(readFileSync(MANIFEST, 'utf-8'));
assert.ok(obj.source_commit, 'source_commit field required');
assert.ok(obj.sync_date, 'sync_date field required');
assert.ok(obj.files && typeof obj.files === 'object', 'files map required');
});
test('playground/vendor/playground-design-system/ contains expected DS files', () => {
const files = readdirSync(VENDOR);
for (const expected of ['tokens.css', 'base.css', 'components.css', 'fonts.css', 'print.css']) {
assert.ok(files.includes(expected), `${expected} expected in vendor/`);
}
assert.ok(files.includes('fonts'), 'fonts/ subdirectory expected');
});