Files
Dashboard/scripts/verify-carbon.mjs
T
dejanandClaude Opus 5 0875ae2688 feat(carbon): Carbon-Konformität nachprüfbar machen statt behaupten
Recherche an der Quelle statt an der Dokumentation. Zwei Ergebnisse:

1. Carbon regelt Datenvisualisierung nur teilweise. Farben, Gitter, Achsen,
   Tooltip und Tickanzahl sind verbindlich festgelegt — Paddings, Margins und
   Dashboard-Raster dagegen NICHT. Die Carbon-Seiten zu Chart-Anatomie und
   Dashboards tragen ausdrücklich den Hinweis "this guidance is a work in
   progress" und enthalten keine Zahlenwerte. Unsere Abstandswerte stammen
   deshalb aus der Carbon-Spacing-Skala und der Planung mit dem Branchenpartner,
   nicht aus der Data-Viz-Norm. Das steht jetzt so in DESIGN-SYSTEM.md §0.

2. Eine echte Abweichung gefunden: Der Tooltip ist seit Carbon v11 hell
   (--cds-layer-02 #ffffff auf --cds-text-primary #161616, abgesetzt allein
   durch den Schatten). Unser Theme führte den dunklen v10-Stand (#393939) und
   berief sich dabei auf das ausgelieferte Stylesheet — die Angabe war veraltet.
   Korrigiert.

Neu:
- docs/carbon-referenz.json — kanonische Werte mit Quelle, Version und
  Prüfdatum, gezogen aus @carbon/charts 1.23.0 styles.min.css und
  configuration.ts. Bewusste Abweichungen (Balkenbreite 48 statt 16, feste
  Kategoriefarben statt n-Serien-Paletten) sind dort begründet.
- scripts/verify-carbon.mjs + npm run verify:carbon — gleicht chartTheme.ts
  gegen die Referenz ab und bricht bei Drift ab.
- docs/PRUEFFRAGEN.md — Prüffragen für die Gestaltungsabnahme mit
  nachprüfbarem Ja/Nein, plus Leseliste (FT Visual Vocabulary, IBCS,
  Cleveland & McGill, Few, Datawrapper Academy).

Die 14er-Palette wurde gegen die CSS-Variablen --cds-charts-14-1-* geprüft und
stimmt exakt; ebenso Gitter #e0e0e0, Achse #8d8d8d und Achsentext #525252.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-29 17:53:51 +02:00

138 lines
5.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Prüft chartTheme.ts gegen docs/carbon-referenz.json.
*
* Warum das existiert: „Wir halten uns an IBM Carbon" ist eine Behauptung,
* solange sie niemand nachrechnet. Farbwerte driften beim Kopieren, und die
* Carbon-Dokumentation widerspricht stellenweise dem ausgelieferten Code.
* Dieser Prüfer macht aus der Behauptung eine Zusage.
*
* Aufruf: npm run verify:carbon
*/
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const wurzel = join(dirname(fileURLToPath(import.meta.url)), "..");
const referenz = JSON.parse(readFileSync(join(wurzel, "docs/carbon-referenz.json"), "utf8"));
const theme = readFileSync(join(wurzel, "features/dashboard/utils/chartTheme.ts"), "utf8");
const fehler = [];
const hinweise = [];
/** Liest ein exportiertes Array von Hex-Werten aus dem Theme. */
function hexArray(name) {
const block = theme.match(new RegExp(`export const ${name}[^=]*=\\s*\\[([\\s\\S]*?)\\]`));
if (!block) return null;
return [...block[1].matchAll(/#[0-9a-fA-F]{6}/g)].map((m) => m[0].toLowerCase());
}
/** Liest einen einzelnen Hex-Wert aus einem Objektfeld im Theme. */
function hexFeld(objekt, feld) {
const block = theme.match(new RegExp(`export const ${objekt}[^=]*=\\s*\\{([\\s\\S]*?)\\}`));
if (!block) return null;
const treffer = block[1].match(new RegExp(`${feld}\\s*:\\s*"(#[0-9a-fA-F]{6})"`));
return treffer ? treffer[1].toLowerCase() : null;
}
/* -------------------------------------------------- Kategoriale Palette */
const palette = hexArray("KATEGORIE_FARBEN");
const soll = referenz.kategorialePalette.werte.map((w) => w.toLowerCase());
if (!palette) {
fehler.push("KATEGORIE_FARBEN nicht gefunden");
} else if (palette.length !== soll.length) {
fehler.push(`Palette hat ${palette.length} Farben, Carbon hat ${soll.length}`);
} else {
palette.forEach((farbe, i) => {
if (farbe !== soll[i]) {
fehler.push(`Palette Position ${i + 1}: ${farbe} — Carbon: ${soll[i]}`);
}
});
}
/* ------------------------------------------------------- Neutrale Werte */
const neutraleZuordnung = {
grid: "gitterlinie",
achse: "achsenlinie",
textSekundaer: "achsenbeschriftung",
text: "textPrimaer",
};
for (const [feld, refName] of Object.entries(neutraleZuordnung)) {
const ist = hexFeld("CHART", feld);
const sollWert = referenz.neutraleWerte[refName].wert.toLowerCase();
if (ist === null) fehler.push(`CHART.${feld} nicht gefunden`);
else if (ist !== sollWert) {
fehler.push(
`CHART.${feld} = ${ist} — Carbon ${referenz.neutraleWerte[refName].token}: ${sollWert}`
);
}
}
/* ---------------------------------------------------------------- Tooltip */
const tooltipBg = hexFeld("CHART", "tooltipBg");
const tooltipFg = hexFeld("CHART", "tooltipFg");
if (tooltipBg !== referenz.tooltip.hintergrund.wert.toLowerCase()) {
fehler.push(
`CHART.tooltipBg = ${tooltipBg} — Carbon ${referenz.tooltip.hintergrund.token}: ${referenz.tooltip.hintergrund.wert}`
);
}
if (tooltipFg !== referenz.tooltip.text.wert.toLowerCase()) {
fehler.push(`CHART.tooltipFg = ${tooltipFg} — Carbon: ${referenz.tooltip.text.wert}`);
}
/* ------------------------------------------------- Achsen-Tickanzahl */
const raster = theme.match(/nettesRaster\(\s*\n?\s*max: number,\s*\n?\s*maxIntervalle = (\d+)/);
if (raster && Number(raster[1]) !== referenz.achsen.ticksYAchse) {
fehler.push(
`nettesRaster maxIntervalle = ${raster[1]} — Carbon numberOfTicks (y): ${referenz.achsen.ticksYAchse}`
);
}
/* -------------------------------------------- Bewusste Abweichungen melden */
const balken = theme.match(/maxBreite:\s*(\d+)/);
const abwBalken = referenz.bewusstAbweichend.balkenMaxBreitePx;
if (balken) {
if (Number(balken[1]) !== abwBalken.hier) {
fehler.push(
`BALKEN.maxBreite = ${balken[1]}, dokumentiert ist ${abwBalken.hier}. ` +
`Entweder den Wert zurücksetzen oder docs/carbon-referenz.json nachziehen.`
);
} else {
hinweises(`Balkenbreite ${abwBalken.hier} px statt Carbon ${abwBalken.carbon} px`, abwBalken.grund);
}
}
function hinweises(titel, grund) {
hinweise.push({ titel, grund });
}
hinweises(
"Feste Farbe je Kategorie statt n-Serien-Palette",
referenz.bewusstAbweichend.festeKategorieFarben.grund
);
/* ------------------------------------------------------------- Ausgabe */
console.log(`Carbon-Abgleich gegen ${referenz.quelle.version} (${referenz.quelle.geprueftAm})\n`);
if (hinweise.length) {
console.log("Bewusste Abweichungen (dokumentiert, kein Fehler):");
for (const h of hinweise) console.log(` · ${h.titel}\n ${h.grund}\n`);
}
if (fehler.length) {
console.error(`${fehler.length} Abweichung(en) von Carbon:`);
for (const f of fehler) console.error(` ✗ ${f}`);
console.error("\nEntweder chartTheme.ts korrigieren oder die Abweichung in");
console.error("docs/carbon-referenz.json unter 'bewusstAbweichend' begründen.");
process.exit(1);
}
console.log("✓ Palette, neutrale Werte, Tooltip und Achsenraster stimmen mit Carbon überein.");