Files
Dashboard/scripts/verify-layout.mjs
T
dejanandClaude Opus 5 6a0b36ddcc Geviertstriche aus Text, Kommentaren und Doku entfernen
467 Stellen. Nicht durch einen anderen Strich ersetzt, sondern durch das
Satzzeichen, das an der jeweiligen Stelle richtig ist: Doppelpunkt, wo eine
Erlaeuterung folgt; Komma, wo der Satz weiterlaeuft; Punkt, wo zwei
Hauptsaetze aneinanderstiessen. Ein Halbgeviertstrich waere nur derselbe
Griff mit kuerzerem Strich gewesen.

Regelbasiert ersetzt und danach nachgeprueft, weil eine Regel allein nicht
reicht. Gefunden und einzeln korrigiert:

- Eingeschobene Saetze mit Strichpaar. Nur der oeffnende Strich passte ins
  Muster, der schliessende blieb als "—," stehen. Drei Stellen, jetzt mit
  Klammern oder als eigener Satz.

- Doppelte Doppelpunkte, wo beide Striche eines Paars zu Doppelpunkten wurden
  ("Globale Regeln: Schrift, Groessen, Abstaende: kommen aus HiveGrid").
  Sieben Stellen in Doku und Oberflaeche, jetzt in Klammern.

- Pfade, denen ein Komma statt eines Doppelpunkts folgte.

- Ausgerichtete Kommentartabellen, in denen der Strich eine Spalte war.

Nicht angefasst: drei Geviertstriche, die keine Prosa sind, sondern
Tabellenplatzhalter fuer "kein Wert" (Trichter, Auftraege, Kunden). Dort ist
der Strich die Angabe selbst; ihn zu entfernen liesse leere Zellen zurueck.

tsc, lint, build, verify:carbon und verify:layout laufen sauber.

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

174 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Layout-Prüfer: findet kollidierenden und überlaufenden Text.
*
* Warum: Textkollisionen treten nur in bestimmten Fensterbreiten auf. Ein
* Screenshot bei 1600 px zeigt sie nicht, bei 1280 px schon. Auge und
* Einzelscreenshot reichen dafür nicht, das muss gemessen werden.
*
* Gemessen wird auf Textzeilen (Range.getClientRects), nicht auf Elementkästen:
* Sonst melden benachbarte Tabellenzellen mit Innenabstand falsch positiv.
*
* Mindestabstand 8 px = Carbon Spacing-Token $spacing-03, der kleinste Abstand,
* den Carbon zwischen benachbarten Textelementen vorsieht.
*
* Aufruf: npm run verify:layout (Dev-Server muss laufen)
*/
import { chromium } from "playwright-core";
const BASIS = process.env.DASHBOARD_URL ?? "http://localhost:4300";
const SEITEN = [
"/dashboard",
"/analytics",
"/analytics/finanzen",
"/analytics/auftraege",
"/analytics/kunden",
"/analytics/team",
"/komponenten",
];
// Von Handy bis großem Schirm. Die schmalen Breiten sind die interessanten:
// Dort fährt die Seitenleiste über den Inhalt, das Raster wird einspaltig und
// die Filterleiste bricht um.
const BREITEN = [375, 414, 640, 768, 834, 1024, 1280, 1440, 1920];
const MIN_ABSTAND = 8;
const EXE =
process.env.CHROME_PFAD ??
`${process.env.HOME}/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell`;
const messen = (MIN) => {
const zeilen = [];
const lauf = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let n;
while ((n = lauf.nextNode())) {
if (!n.textContent.trim()) continue;
const el = n.parentElement;
if (!el || el.closest("svg")) continue;
const st = getComputedStyle(el);
if (st.visibility === "hidden" || st.opacity === "0") continue;
// Absichtlich bündige Flächen (Segment-Control) ausnehmen
if (el.closest("[data-buendig]")) continue;
// Nächster Blockcontainer: Wörter innerhalb eines Satzes sind keine Kollision
let block = el;
while (block && block !== document.body && getComputedStyle(block).display.startsWith("inline")) {
block = block.parentElement;
}
const bereich = document.createRange();
bereich.selectNodeContents(n);
for (const r of bereich.getClientRects()) {
if (r.width < 1 || r.height < 1) continue;
zeilen.push({ r, text: n.textContent.trim().slice(0, 30), block, el });
}
}
const pfad = (el) => {
const t = [];
for (let x = el; x && x !== document.body && t.length < 3; x = x.parentElement) {
const k = typeof x.className === "string" && x.className
? "." + x.className.trim().split(/\s+/).slice(0, 2).join(".")
: "";
t.unshift(x.tagName.toLowerCase() + k);
}
return t.join(" > ");
};
const treffer = [];
for (let i = 0; i < zeilen.length; i++) {
for (let j = i + 1; j < zeilen.length; j++) {
const a = zeilen[i], b = zeilen[j];
if (a.block === b.block) continue; // gleicher Textfluss
if (a.el.contains(b.el) || b.el.contains(a.el)) continue;
const va = a.r, vb = b.r;
if (!(va.top < vb.bottom - 3 && vb.top < va.bottom - 3)) continue;
const luecke = vb.left >= va.left ? vb.left - va.right : va.left - vb.right;
if (luecke < MIN) {
treffer.push({
luecke: Math.round(luecke * 10) / 10,
a: a.text, b: b.text, wo: pfad(a.el),
ueberlappt: luecke < 0,
});
}
}
}
// Die Seite selbst darf nie seitlich scrollen, ein waagerechter
// Scrollbalken auf Dokumentebene ist immer ein Fehler, kein Kompromiss.
const doc = document.documentElement;
const seitenUeberlauf = doc.scrollWidth > doc.clientWidth + 1 ? doc.scrollWidth - doc.clientWidth : 0;
// Text, der aus seinem eigenen Kasten läuft
const ueberlauf = [...document.querySelectorAll("body *")]
.filter((el) => {
if (el.closest("svg") || el.children.length) return false;
const st = getComputedStyle(el);
if (st.overflow !== "visible" || !el.textContent.trim()) return false;
return el.scrollWidth > el.clientWidth + 1;
})
.map((el) => ({ text: el.textContent.trim().slice(0, 30), wo: pfad(el), um: el.scrollWidth - el.clientWidth }));
return { treffer, ueberlauf, seitenUeberlauf };
};
const browser = await chromium.launch({ executablePath: EXE });
const gesammelt = new Map();
for (const breite of BREITEN) {
const ctx = await browser.newContext({ viewport: { width: breite, height: 1200 } });
const seite = await ctx.newPage();
for (const pfad of SEITEN) {
const antwort = await seite.goto(BASIS + pfad, { waitUntil: "networkidle" });
if (!antwort || antwort.status() >= 400) {
console.error(`✗ ${pfad} antwortet mit ${antwort?.status() ?? "keiner Antwort"}: Route existiert nicht.`);
process.exit(1);
}
await seite.waitForTimeout(500);
const { treffer, ueberlauf, seitenUeberlauf } = await seite.evaluate(messen, MIN_ABSTAND);
if (seitenUeberlauf > 0) {
const key = `S|${pfad}|${breite}`;
gesammelt.set(key, {
art: "Seitenüberlauf",
text: `${pfad} läuft um ${seitenUeberlauf}px seitlich über`,
wo: "document",
um: seitenUeberlauf,
seiten: new Set([pfad]),
breiten: new Set([breite]),
});
}
for (const t of treffer) {
const key = `K|${t.a}|${t.b}|${t.wo}`;
if (!gesammelt.has(key)) gesammelt.set(key, { art: "Kollision", ...t, seiten: new Set(), breiten: new Set() });
gesammelt.get(key).seiten.add(pfad);
gesammelt.get(key).breiten.add(breite);
gesammelt.get(key).luecke = Math.min(gesammelt.get(key).luecke, t.luecke);
}
for (const u of ueberlauf) {
const key = `U|${u.text}|${u.wo}`;
if (!gesammelt.has(key)) gesammelt.set(key, { art: "Überlauf", ...u, seiten: new Set(), breiten: new Set() });
gesammelt.get(key).seiten.add(pfad);
gesammelt.get(key).breiten.add(breite);
}
}
await ctx.close();
}
await browser.close();
const liste = [...gesammelt.values()].sort((a, b) => (a.luecke ?? -99) - (b.luecke ?? -99));
for (const f of liste) {
const kopf =
f.art === "Kollision"
? `${f.ueberlappt ? "ÜBERLAPPT" : "zu eng"} (${f.luecke}px): "${f.a}" ⟷ "${f.b}"`
: f.art === "Seitenüberlauf"
? `SEITE SCROLLT (+${f.um}px): ${f.text}`
: `ÜBERLAUF (+${f.um}px): "${f.text}"`;
console.log(` ${kopf}`);
console.log(` ${f.wo}`);
console.log(` ${[...f.seiten].join(" ")} · ${[...f.breiten].join(" ")}`);
}
if (liste.length) {
console.error(`\n✗ ${liste.length} Layoutfehler. Mindestabstand ist ${MIN_ABSTAND} px (Carbon $spacing-03).`);
process.exit(1);
}
console.log(`✓ Kein kollidierender Text, kein Überlauf: ${SEITEN.length} Seiten × ${BREITEN.length} Breiten (${BREITEN[0]}–${BREITEN.at(-1)} px).`);