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
This commit is contained in:
1 parent
99be627dad
commit
0875ae2688
8 files changed
+412
-20
No files matched your search
@@ -0,0 +1,137 @@
|
||||
#!/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.");
|
||||
Reference in new issue
Block a user