pageta/apps/web/scripts/audit-theme-tokens.mjs
Till JS fe6d9fbf74 fix(web): UI-Korrekturabzug 2026-07-26 — 30 von 37 Befunden
Visuelle Prüfung aller Routen gegen mana/docs/DESIGN.md, Bericht in
docs/ui-review-2026-07-26.html. Drei Ursachen erklärten die Hälfte:

1. Die fixierte Pill-Navigation war durchsichtig und lag mit z-index 1000
   über allem. Sie verschluckte Klicks (belegt am Modus-Schalter unter
   /konto), überdeckte den Modal-Fuß und die Fußzeile.
   → Scrim + Höhe als --bottom-chrome, z-index auf 100, Modal auf 9995.

2. Vier Seiten hatten sich am Theme vorbeigebaut — 44 Hex-Literale.
   /changelog war im Dunkelmodus mit ~1,05 : 1 unlesbar.
   → Alles auf hsl(var(--color-*)), plus audit:theme-tokens als Wächter
     (zwei dokumentierte Ausnahmen: Reader-Themes, Marker-Pigmente).

3. Kein gemeinsames Seiten-Gerüst — drei Inhaltsbreiten, fünf Titel-
   Schemata, Zurück-Link nur auf zwei Seiten.
   → PageShell (Breite, Titel, Untertitel, Dokumenttitel, Zurück-Link,
     Avatar-Korridor).

Weiter:
- Zusammenfassung gab den rohen Prompt mit [noop]-Präfix aus → isEmptyOrEcho
  fängt Echo-Antworten ab, ehrlicher Fehler statt Interna (8 Tests).
- Lese-Liste zeigte Domains statt Titel → looksUnextracted markiert
  Consent-Wall-Reste, Reparaturweg über das HTML-Bookmarklet (7 Tests).
- highlightToWire setzte createdAt: s.userId → deshalb stand die Konto-UUID
  als Datum in /markierungen und die Sortierung war wirkungslos.
  Zeitstempel kommt jetzt aus der ULID.
- Rohe Status-Enums (UNREAD) → statusLabel() über @mana/shared-i18n.
- Emojis und Textzeichen als Icons → Phosphor aus @mana/shared-icons.
- +error.svelte statt SvelteKits englischem „404 Not Found".
- Ruhende Routen /feed und /recherche leiten auf /lese-liste um.
- Labels über allen Formularfeldern, aria-label an Inline-Feldern.
- 44-px-Trefferflächen, ganze Karte klickbar, globaler Fokusring in
  @layer base, EmptyState-Komponente, Aktions-Rangfolge im Reader.
- Lesetypografie: `prose` war wirkungslos (Tailwind-Typography ist gar
  nicht installiert) → eigene, token-basierte Lesetypografie.

Offen (im Bericht begründet): Desktop-Breitennutzung, Bild-Nachladen für
Bestandsartikel, Filterleisten am unteren Rand, Varianten-Namen und die
WebDAV-Feldbeschriftungen in @mana/shared-ui.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 23:49:51 +02:00

99 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/**
* audit-theme-tokens — Drift-Wächter für das 12-Token-Vokabular.
*
* mana/docs/DESIGN.md Regel 4: „CSS-Variablen statt hardcoded Farben. Immer
* hsl(var(--color-*)), nie #hex. Dark Mode ist nicht optional."
*
* Der UI-Korrekturabzug vom 2026-07-26 (docs/ui-review-2026-07-26.html, P-04)
* fand 44 Hex-Literale in vier Dateien — /changelog war im Dunkelmodus mit
* ~1,05 : 1 Kontrast unlesbar. Genau das soll hier nie wieder unbemerkt
* einziehen.
*
* Geprüft wird jede .svelte-Datei unter src/ auf
* 1. Hex-Farbliterale (#abc, #aabbcc, #aabbccdd)
* 2. Tailwind-Palettenklassen (bg-gray-100, text-red-700, border-amber-200 …)
*
* Bewusste Ausnahmen stehen unten in ALLOWLIST — mit Begründung, nicht als
* stiller Freibrief.
*/
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
const SRC = join(ROOT, 'src');
/**
* Dokumentierte Ausnahmen. Jeder Eintrag braucht einen Grund — wer hier
* etwas einträgt, erklärt, warum die Farbe INHALT ist und nicht Theme.
*/
const ALLOWLIST = [
{
file: 'src/routes/article/[id]/+page.svelte',
reason:
'Reader-Themes (light/sepia/dark) und Textmarker-Pigmente sind Lesefarben, ' +
'die die globale Theme-Wahl bewusst überlagern.',
},
{
file: 'src/routes/markierungen/+page.svelte',
reason:
'Textmarker-Pigmente sind Inhalt (die Farbe, die der Mensch gewählt hat), ' +
'nicht Theme-Farbe. Die Tinte darauf ist fixiert, damit beide Modi lesbar bleiben.',
},
];
const HEX = /#[0-9a-fA-F]{3,8}\b/g;
const PALETTE =
/\b(?:bg|text|border|ring|fill|stroke|from|to|via|decoration|outline|divide|shadow)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}\b/g;
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
if (entry === 'node_modules' || entry === '.svelte-kit') continue;
const full = join(dir, entry);
if (statSync(full).isDirectory()) out.push(...walk(full));
else if (entry.endsWith('.svelte') || entry.endsWith('.css')) out.push(full);
}
return out;
}
const findings = [];
for (const file of walk(SRC)) {
const rel = relative(ROOT, file);
if (ALLOWLIST.some((a) => a.file === rel)) continue;
const lines = readFileSync(file, 'utf8').split('\n');
lines.forEach((line, i) => {
// `url(#…)` sind SVG-Referenzen, keine Farben.
const cleaned = line.replace(/url\(#[^)]*\)/g, '');
for (const m of cleaned.matchAll(HEX)) {
findings.push({ rel, line: i + 1, kind: 'hex', text: m[0], src: line.trim() });
}
for (const m of cleaned.matchAll(PALETTE)) {
findings.push({ rel, line: i + 1, kind: 'palette', text: m[0], src: line.trim() });
}
});
}
if (findings.length === 0) {
console.log(`audit:theme-tokens — grün. Ausnahmen: ${ALLOWLIST.length} (dokumentiert).`);
process.exit(0);
}
console.error(`audit:theme-tokens — ${findings.length} Verstoß/Verstöße gegen DESIGN.md Regel 4:\n`);
for (const f of findings) {
const hint =
f.kind === 'hex'
? 'hsl(var(--color-…)) verwenden'
: 'Token-Klasse verwenden, z. B. text-[hsl(var(--color-muted-foreground))]';
console.error(` ${f.rel}:${f.line} ${f.text} → ${hint}`);
console.error(` ${f.src.slice(0, 110)}`);
}
console.error(
'\nIst die Farbe wirklich Inhalt (Lesefarbe, Pigment, Markenlogo)? Dann in\n' +
'scripts/audit-theme-tokens.mjs in die ALLOWLIST — mit Begründung.',
);
process.exit(1);