01 / ZWEI RESPONSIVE PATTERNS
Übersicht oder Vergleichbarkeit?
In Lektion 4 ging es darum, welche Informationen sofort sichtbar sein sollten. Jetzt betrachten wir ein Materialmonitoring mit vier Datensätzen. Vergleiche die Ansichten auf einem schmalen Bildschirm: Welche Untergruppen haben den Status „Prüfen“, und welche Änderung ist am größten?
MATERIALMONITORING
Messwerte
Produktion
Linie A
- Materialwert
- 123 kg
- Veränderung
- +12 %
Produktion
Linie B
- Materialwert
- 98 kg
- Veränderung
- −4 %
Lager
Halle 1
- Materialwert
- 150 kg
- Veränderung
- +2 %
Lager
Halle 2
- Materialwert
- 100 kg
- Veränderung
- +18 %
Fiktive Daten. Die Statuswerte „Prüfen“ sind vorgegeben und lassen sich nicht allein aus den Prozentänderungen ableiten. Zeitraum und fachliche Schwellenwerte sind nicht spezifiziert.
Cards können helfen, einzelne Datensätze nacheinander zu lesen. Eine Tabelle hält Werte in gemeinsamen Spalten und erleichtert damit bestimmte Vergleiche. Auf Mobile entstehen jedoch zusätzliche Aufgaben: horizontal scrollen, den passenden Eintrag im Blick behalten und sich den Spaltenkontext erschließen.
Keine der Varianten ist allein wegen ihrer Bildschirmaufteilung die bessere Wahl. Wer nur einen Eintrag bearbeiten möchte, hat andere Bedürfnisse als jemand, der viele Änderungen gleichzeitig vergleichen muss.
02 / DIE AUFGABE BESTIMMT DAS PATTERN
Responsive Design verändert auch den Vergleich.
Eine Tabelle in Cards umzuwandeln ist mehr als ein Umbruch im Layout. Gemeinsame Spalten werden zu wiederholten Beschriftungen; Werte, die vorher nebeneinanderstanden, können mehrere Scrollbewegungen voneinander entfernt sein.
Eine dritte Möglichkeit ist eine kompakte Liste: Identität, Hauptwert, Änderung und Status bleiben sichtbar. Selten benötigter Kontext erscheint erst auf Nachfrage. Entscheidend ist, keine für die Aufgabe notwendige Information hinter „Details“ zu verstecken.
In unserem Beispiel ist ein Anstieg nicht automatisch ein Problem. Die Statuswerte sind fachliche Vorgaben des Szenarios. „Prüfen“ muss deshalb als Text erkennbar bleiben und darf nicht nur über Farbe oder ein Pluszeichen vermittelt werden.
03 / DEINE CHALLENGE · 10 MINUTEN
Materialmonitoring bei 375 px.
Entwirf in Figma oder React eine dritte Variante für die vier Beispieldatensätze. Die Person soll innerhalb von fünf Sekunden erkennen, welche Untergruppen Aufmerksamkeit benötigen und welcher Wert am stärksten gestiegen ist. Das ist dein Übungsziel, kein allgemeiner UX-Grenzwert.
- Priorisieren · 2 Minuten: Bestimme, welche zwei bis drei Informationsarten dauerhaft sichtbar sein müssen. Identität und Einheit müssen trotzdem eindeutig bleiben.
- Gestalten · 6 Minuten: Entwirf eine kompakte mobile Liste. Achte auf bewusste Typografie, rechtsbündige Zahlen, konsistente Abstände und verständliche Statusangaben.
- Randfälle · 2 Minuten: Beschreibe das Verhalten bei 200 Datensätzen, langen Gruppennamen und fehlenden Messwerten. Was darf dabei nie unbemerkt verschwinden?
Begründe mindestens zwei Entscheidungen und einen Nachteil deiner Wahl. Ein kompakter Entwurf kann gut aussehen und trotzdem einen häufig benötigten Vergleich erschweren.
04 / EINE MÖGLICHE MUSTERLÖSUNG
Aufmerksamkeit sichtbar priorisieren.
Für die vorgegebene Aufgabe wähle ich eine kompakte Liste. Der Status bleibt neben dem Namen sichtbar, Materialwert und Änderung stehen darunter in festen Bereichen. Probiere auch die konstruierten Randfälle und die Seitennavigation aus.
Interaktive Musterlösung öffnen
Materialmonitoring
2 von 4 Einträgen mit Status „Prüfen“. Zeilen 1–4. Statusgruppen sind jeweils nach Änderung absteigend sortiert.
- LagerPrüfen
Halle 2
- Materialwert
- 100 kg
- Änderung
- +18 %
Kontext zu Halle 2
Fiktive Messung ohne angegebenen Zeitraum oder fachlichen Grenzwert. Ein Anstieg allein begründet keine Alarmierung.
- ProduktionPrüfen
Linie A
- Materialwert
- 123 kg
- Änderung
- +12 %
Kontext zu Linie A
Fiktive Messung ohne angegebenen Zeitraum oder fachlichen Grenzwert. Ein Anstieg allein begründet keine Alarmierung.
- LagerNormal
Halle 1
- Materialwert
- 150 kg
- Änderung
- +2 %
Kontext zu Halle 1
Fiktive Messung ohne angegebenen Zeitraum oder fachlichen Grenzwert. Ein Anstieg allein begründet keine Alarmierung.
- ProduktionNormal
Linie B
- Materialwert
- 98 kg
- Änderung
- −4 %
Kontext zu Linie B
Fiktive Messung ohne angegebenen Zeitraum oder fachlichen Grenzwert. Ein Anstieg allein begründet keine Alarmierung.
Im Beispiel hat Halle 2 mit +18 % den stärksten Anstieg; Linie A und Halle 2 haben den vorgegebenen Status Prüfen. Die Vorlage enthält keine fachlichen Statusregeln.
Warum diese Entscheidungen?
- Status vor Dekoration: „Prüfen“ steht als Text am Eintrag. Die Hintergrundfarbe unterstützt, trägt aber nicht allein die Bedeutung.
- Eine begründete Reihenfolge: Zuerst erscheinen zu prüfende Einträge, innerhalb jeder Statusgruppe die größte Änderung zuerst. Wer ausschließlich Anstiege vergleichen möchte, kann die Reihenfolge wechseln. „Prüfen zuerst“ bedeutet nicht automatisch, dass der erste Eintrag insgesamt den größten Anstieg hat.
- Vergleichbare Zahlen: Einheit und Änderungszeichen bleiben erhalten; Ziffern nutzen gleich breite Zeichen. Die Änderung ist nicht erst in den Details sichtbar.
- Fehlend ist nicht null: Ein fehlender Wert heißt „Kein Messwert“, eine fehlende Änderung „Keine Vergleichsbasis“. Der Eintrag bleibt sichtbar. Fehlende Änderungen werden innerhalb der jeweiligen Sortiergruppe nach vorhandenen Änderungen eingeordnet.
- Lange Namen dürfen umbrechen: Die Identität bleibt vollständig lesbar. Der Status nimmt einen eigenen Bereich ein.
- 200 Einträge brauchen einen gezielten Weg: Das Beispiel sortiert alle Zeilen vor der Aufteilung auf Seiten mit jeweils 20 Einträgen. Seitenangabe und Zeilenbereich bleiben sichtbar. Bei einem echten Monitoring können zusätzlich Suche, Filter und eine gesonderte Übersicht kritischer Einträge nötig sein.
Der Nachteil: Mehrere frei gewählte Kennzahlen gleichzeitig zu vergleichen ist in einer Liste weniger flexibel als in einer Tabelle. Dafür sollte das Produkt gegebenenfalls eine zusätzliche Vergleichsansicht anbieten. Die Musterlösung ist ein eigener, ungetesteter Gestaltungsvorschlag.
React-Code, CSS und Beispieldaten ansehen
Hier steht der tatsächliche Code der Vorschau, Liste und Reflexion. CSS und Datenmodul gehören dazu; in einem anderen Projekt müssen die Importpfade angepasst werden.
coaching-mobile-tables.tsx
"use client";
import { useId, useRef, useState, type FormEvent } from "react";
import { getMonitoringRows, monitoringRows, monitoringChange, monitoringValue, sortMonitoringRows, type MonitoringScenario, type MonitoringOrder } from "@/lib/coaching-monitoring";
import "./coaching-mobile-tables.css";
export function CoachingMobileTableComparison() {
const [view, setView] = useState<"cards" | "table">("cards");
const id = useId();
return <div className="monitor-demo">
<div className="monitor-view" role="group" aria-label="Darstellung der Messwerte"><button type="button" aria-pressed={view === "cards"} onClick={() => setView("cards")}>Mobile Cards</button><button type="button" aria-pressed={view === "table"} onClick={() => setView("table")}>Scroll-Tabelle</button></div>
<div className="monitor-preview"><p className="monitor-kicker">MATERIALMONITORING</p><h3>Messwerte</h3>
{view === "cards" ? <div className="monitor-cards">{monitoringRows.map(row => <section key={row.id} aria-labelledby={`${id}-${row.id}`}><div className="monitor-card-title"><div><p>{row.group}</p><h4 id={`${id}-${row.id}`}>{row.name}</h4></div><span className={`monitor-status ${row.status === "Prüfen" ? "needs-attention" : ""}`}>{row.status}</span></div><dl><div><dt>Materialwert</dt><dd>{monitoringValue(row.value)}</dd></div><div><dt>Veränderung</dt><dd>{monitoringChange(row.change)}</dd></div></dl></section>)}</div> : <>
<p className="monitor-scroll-hint" id={`${id}-help`}>Auf schmalen Bildschirmen innerhalb der Tabelle horizontal scrollen. Per Tastatur: Scrollbereich fokussieren und Pfeiltasten nutzen.</p>
<div className="monitor-table-scroll" role="region" aria-label="Materialwerte, horizontal scrollbar" aria-describedby={`${id}-help`} tabIndex={0}>
<table><caption>Materialwerte nach Untergruppe · fiktive Übungsdaten</caption><thead><tr><th scope="col">Untergruppe</th><th scope="col">Wert</th><th scope="col">Änderung</th><th scope="col">Status</th></tr></thead><tbody>{monitoringRows.map(row => <tr key={row.id}><th scope="row"><span>{row.group}</span>{row.name}</th><td className="numeric">{monitoringValue(row.value)}</td><td className="numeric">{monitoringChange(row.change)}</td><td><span className={`monitor-status ${row.status === "Prüfen" ? "needs-attention" : ""}`}>{row.status}</span></td></tr>)}</tbody></table>
</div>
</>}
</div><p className="monitor-demo-note">Fiktive Daten. Die Statuswerte „Prüfen“ sind vorgegeben und lassen sich nicht allein aus den Prozentänderungen ableiten. Zeitraum und fachliche Schwellenwerte sind nicht spezifiziert.</p>
</div>;
}
export function CoachingMonitoringSolution() {
const [scenario, setScenario] = useState<MonitoringScenario>("example");
const [order, setOrder] = useState<MonitoringOrder>("attention");
const [page, setPage] = useState(0);
const id = useId();
const listRef = useRef<HTMLHeadingElement>(null);
const rows = sortMonitoringRows(getMonitoringRows(scenario), order);
const pageSize = 20;
const pages = Math.ceil(rows.length / pageSize);
const shown = rows.slice(page * pageSize, (page + 1) * pageSize);
const attention = rows.filter(row => row.status === "Prüfen").length;
function changePage(next: number) {
setPage(next);
listRef.current?.focus();
}
return <div className="monitor-solution">
<div className="monitor-settings"><div><label htmlFor={`${id}-scenario`}>Datensatz ausprobieren</label><select id={`${id}-scenario`} value={scenario} onChange={event => { setScenario(event.target.value as MonitoringScenario); setPage(0); }}><option value="example">4 Beispieldatensätze</option><option value="edges">Langer Name & fehlender Messwert</option><option value="many">200 synthetische Datensätze</option></select></div><div><label htmlFor={`${id}-order`}>Reihenfolge</label><select id={`${id}-order`} value={order} onChange={event => { setOrder(event.target.value as MonitoringOrder); setPage(0); }}><option value="attention">Prüfen zuerst, dann Anstieg</option><option value="increase">Stärkster Anstieg zuerst</option></select></div></div>
<h3 ref={listRef} tabIndex={-1}>Materialmonitoring</h3><p className="monitor-result-count" role="status">{attention} von {rows.length} Einträgen mit Status „Prüfen“. Zeilen {page * pageSize + 1}–{Math.min((page + 1) * pageSize, rows.length)}. {order === "attention" ? "Statusgruppen sind jeweils nach Änderung absteigend sortiert." : "Alle Einträge sind nach Änderung absteigend sortiert."}</p>
<ol className="monitor-compact-list" start={page * pageSize + 1}>{shown.map(row => <li key={row.id}><div className="monitor-compact-head"><div><span>{row.group}</span><h4>{row.name}</h4></div><span className={`monitor-status ${row.status === "Prüfen" ? "needs-attention" : ""}`}>{row.status}</span></div><dl><div><dt>Materialwert</dt><dd>{monitoringValue(row.value)}</dd></div><div><dt>Änderung</dt><dd>{monitoringChange(row.change)}</dd></div></dl><details><summary>Kontext zu {row.name}</summary><p>{row.value === null ? "Messwert und Vergleichsbasis fehlen. Der Eintrag bleibt sichtbar und darf nicht wie 0 kg behandelt werden." : "Fiktive Messung ohne angegebenen Zeitraum oder fachlichen Grenzwert. Ein Anstieg allein begründet keine Alarmierung."}</p></details></li>)}</ol>
{pages > 1 && <nav className="monitor-pagination" aria-label="Seiten der Materialliste"><button type="button" disabled={page === 0} onClick={() => changePage(page - 1)}>Vorherige Seite</button><span>Seite {page + 1} von {pages}</span><button type="button" disabled={page === pages - 1} onClick={() => changePage(page + 1)}>Nächste Seite</button></nav>}
<p className="monitor-demo-note">{scenario === "many" ? "Die 200 Zeilen wiederholen die vier Beispieldatensätze mit eindeutigen Messpunktnamen. Sie dienen ausschließlich zur Demonstration von Sortierung und Seitennavigation, nicht als echte Messreihe." : scenario === "edges" ? "Langer Name und fehlender Messwert sind zusätzlich konstruierte Randfälle für diese Musterlösung." : "Im Beispiel hat Halle 2 mit +18 % den stärksten Anstieg; Linie A und Halle 2 haben den vorgegebenen Status Prüfen."} Die Vorlage enthält keine fachlichen Statusregeln.</p>
</div>;
}
const reflectionOptions = [
{ id: "cards", label: "Cards mit allen Werten", feedback: "Cards können einzelne Einträge gut erklären. Prüfe aber, wie viel Scrollen und Merken nötig wird, um Änderungen zwischen Einträgen zu vergleichen." },
{ id: "table", label: "Horizontal scrollbare Tabelle", feedback: "Eine Tabelle erhält gemeinsame Spalten für den Vergleich. Prüfe auf Mobile, ob Untergruppe und Status gleichzeitig auffindbar bleiben und der Scrollbereich erkennbar und bedienbar ist." },
{ id: "list", label: "Kompakte Liste mit optionalen Details", feedback: "Für die konkrete Monitoring-Aufgabe ist das ein plausibler Ausgangspunkt: Status und Änderung bleiben sichtbar. Wer mehrere Kennzahlen parallel vergleichen muss, könnte trotzdem eine Tabelle benötigen." },
];
export function CoachingMonitoringReflection() {
const [choice, setChoice] = useState("");
const [reason, setReason] = useState("");
const [attempted, setAttempted] = useState(false);
const [evaluated, setEvaluated] = useState(false);
const id = useId();
function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setAttempted(true); setEvaluated(Boolean(choice && reason.trim())); }
return <form className="lesson-reflection-form" noValidate onSubmit={submit}>
<fieldset aria-describedby={attempted && !choice ? `${id}-choice-error` : undefined}><legend>Welche Darstellung wählst du für diese Monitoring-Aufgabe?</legend>{reflectionOptions.map(option => <label className="lesson-reflection-option" key={option.id}><input type="radio" name={`${id}-choice`} checked={choice === option.id} onChange={() => { setChoice(option.id); setEvaluated(false); }} required /><span>{option.label}</span></label>)}</fieldset>
{attempted && !choice && <p className="lesson-form-error" id={`${id}-choice-error`} role="alert">Wähle eine Darstellung aus.</p>}
<label className="lesson-reason-label" htmlFor={`${id}-reason`}>Wie überprüfst du, ob Menschen kritische Einträge schneller und korrekt erkennen?</label><textarea id={`${id}-reason`} rows={4} value={reason} required aria-invalid={attempted && !reason.trim() || undefined} aria-describedby={attempted && !reason.trim() ? `${id}-reason-error` : undefined} onChange={event => { setReason(event.target.value); setEvaluated(false); }} />
{attempted && !reason.trim() && <p className="lesson-form-error" id={`${id}-reason-error`} role="alert">Ergänze eine Testaufgabe und passende Beobachtungen oder Messgrößen.</p>}
<button className="lesson-primary-action" type="submit">Meine Lösung auswerten</button><div role="status" aria-live="polite">{evaluated && <div className="lesson-reflection-feedback"><h3>Die Aufgabe entscheidet.</h3><p>{reflectionOptions.find(option => option.id === choice)!.feedback}</p><h4>Prüfe deine Testidee</h4><p>Beispielaufgabe: „Welche Untergruppen benötigen Aufmerksamkeit, und welche hat den stärksten Anstieg?“ Vergleiche richtige Antworten und die Zeit bis zur vollständigen Antwort. Erfasse übersehene Prüfen-Status separat. Gleiche Daten und vergleichbare Bedingungen helfen, den Vergleich einzuordnen.</p><p className="lesson-example-note">Dein Freitext wird nicht automatisch bewertet. Die Fünf-Sekunden-Vorgabe ist ein Übungsziel, kein belegter Erfolg dieser Musterlösung.</p></div>}</div>
</form>;
}
coaching-mobile-tables.css
.monitor-demo,.monitor-solution{max-width:860px;margin-top:24px;min-width:0}
.monitor-view{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:18px}
.monitor-view button{min-height:44px;padding:10px 16px;border:1px solid #737e90;border-radius:6px;background:white;font-size:13px;line-height:1.7}
.monitor-view button[aria-pressed=true]{color:white;background:#2558ed;border-color:#2558ed}
.monitor-preview{padding:24px;background:#eef1f7;border:1px solid #cbd4e4;border-radius:12px;min-width:0}
.monitor-preview p.monitor-kicker{font:9px/1.8 monospace;letter-spacing:.1em;color:#536174}
.monitor-preview h3{font-size:26px;font-weight:500;margin:8px 0 22px}
.monitor-cards{display:grid;grid-template-columns:1fr 1fr;gap:18px}
.monitor-cards>section{padding:20px;background:white;border:1px solid #cbd4e4;border-radius:10px}
.monitor-card-title,.monitor-compact-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
.monitor-card-title p,.monitor-compact-head>div>span{font-size:11px;line-height:1.8;color:#536174}
.monitor-card-title h4{font-size:20px;font-weight:500;line-height:1.5;margin-top:4px}
.monitor-status{display:inline-block;flex-shrink:0;font-size:11px;font-weight:500;line-height:1.8;border:1px solid #ccd4df;border-radius:5px;padding:3px 8px;background:#edf0f5;color:#3f4e61}
.monitor-status.needs-attention{color:#79500d;background:#fff2d6;border-color:#d3ad60}
.monitor-cards dl{display:grid;grid-template-columns:1fr 1fr;gap:18px;border-top:1px solid #d5ddea;padding-top:18px;margin-top:18px}
.monitor-cards dt{font-size:11px;color:#536174;line-height:1.8}
.monitor-cards dd{font-size:23px;font-weight:500;font-variant-numeric:tabular-nums;line-height:1.6;margin-top:6px}
.monitor-cards dl>div:last-child dd{font-size:18px}
.monitor-preview .monitor-scroll-hint,.monitor-demo .monitor-demo-note,.monitor-solution .monitor-demo-note{font-size:12px;line-height:1.8;color:#536174;margin-top:16px}
.monitor-table-scroll{overflow-x:auto;max-width:100%;min-width:0;margin-top:18px;border:1px solid #bac5d8;border-radius:8px;background:white;scroll-margin-top:110px}
.monitor-table-scroll table{min-width:620px;width:100%;border-collapse:separate;border-spacing:0;font-size:14px;line-height:1.8}
.monitor-table-scroll caption{text-align:left;font-size:12px;color:#536174;padding:16px}
.monitor-table-scroll :is(th,td){text-align:left;padding:14px 16px;border-bottom:1px solid #d5ddea;vertical-align:middle}
.monitor-table-scroll thead th{background:#e7edf8;font-weight:500;font-size:12px}
.monitor-table-scroll thead th:first-child{position:sticky;left:0;z-index:2;border-right:1px solid #d5ddea}
.monitor-table-scroll tbody th{position:sticky;left:0;z-index:1;min-width:155px;background:white;border-right:1px solid #d5ddea;font-weight:500}
.monitor-table-scroll tbody th>span{display:block;font-size:10px;color:#536174;font-weight:400}
.monitor-table-scroll .numeric{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.monitor-solution{border:1px solid #cbd4e4;border-radius:10px;padding:24px;background:#f2f4f8}
.monitor-settings{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.monitor-settings>div{min-width:0}
.monitor-settings label{display:block;font-size:12px;font-weight:500;line-height:1.8;margin-bottom:8px}
.monitor-settings select{width:100%;min-width:0;max-width:100%;min-height:46px;border:1px solid #737e90;border-radius:6px;padding:10px 8px;font-size:13px;line-height:1.6;color:#253147;background:white}
.monitor-solution h3{scroll-margin-top:110px;font-size:24px;line-height:1.4;margin-top:26px}
.monitor-solution p.monitor-result-count{font-size:12px;line-height:1.8;color:#536174;margin-top:10px}
.monitor-compact-list{list-style:none;padding:0;margin:20px 0 0;border:1px solid #cbd4e4;border-radius:8px;overflow:hidden;background:white}
.monitor-compact-list li{padding:20px}
.monitor-compact-list li+li{border-top:1px solid #d5ddea}
.monitor-compact-head>div{min-width:0;overflow-wrap:anywhere}
.monitor-compact-head h4{font-size:17px;font-weight:500;line-height:1.5;margin-top:4px}
.monitor-compact-list dl{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px}
.monitor-compact-list dt{font-size:11px;color:#536174;line-height:1.8}
.monitor-compact-list dd{font-size:17px;font-weight:500;line-height:1.7;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.monitor-compact-list dl>div:last-child{text-align:right}
.monitor-compact-list details{margin-top:12px}
.monitor-compact-list summary{cursor:pointer;font-size:11px;line-height:1.8;min-height:44px;padding:12px 0;color:#254ab5;overflow-wrap:anywhere}
.monitor-compact-list details>p{font-size:12px;line-height:1.8;color:#536174}
.monitor-pagination{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-top:20px}
.monitor-pagination button{min-height:44px;padding:10px 12px;border:1px solid #737e90;border-radius:6px;font-size:12px;background:white}
.monitor-pagination button:disabled{background:#e5e9ef;color:#637084;border-color:#b9c3d2}
.monitor-pagination>span{font-size:12px;color:#536174}
.monitor-demo :is(button,select):focus-visible,.monitor-table-scroll:focus-visible,.monitor-solution :is(button,select,summary,h3):focus-visible{outline:3px solid #2558ed;outline-offset:3px}
@media(max-width:640px){.monitor-preview,.monitor-solution{padding:18px}.monitor-cards{grid-template-columns:1fr;gap:14px}.monitor-view button{font-size:12px;padding:10px 14px}.monitor-cards>section{padding:18px}.monitor-settings{grid-template-columns:1fr;gap:18px}.monitor-settings select{font-size:12px}.monitor-compact-list li{padding:16px}.monitor-compact-head{gap:8px}.monitor-compact-head h4{font-size:16px}.monitor-compact-head>div>span{font-size:10px}.monitor-status{font-size:10px;padding:3px 6px}.monitor-compact-list dd{font-size:15px}.monitor-pagination button{font-size:11px}.monitor-table-scroll{scroll-margin-top:90px}}
coaching-monitoring.ts
export type MonitoringRow = {
id: string; group: string; name: string; value: number | null;
change: number | null; status: "Prüfen" | "Normal";
};
export type MonitoringScenario = "example" | "edges" | "many";
export type MonitoringOrder = "attention" | "increase";
export const monitoringRows: MonitoringRow[] = [
{ id: "line-a", group: "Produktion", name: "Linie A", value: 123, change: 12, status: "Prüfen" },
{ id: "line-b", group: "Produktion", name: "Linie B", value: 98, change: -4, status: "Normal" },
{ id: "hall-1", group: "Lager", name: "Halle 1", value: 150, change: 2, status: "Normal" },
{ id: "hall-2", group: "Lager", name: "Halle 2", value: 100, change: 18, status: "Prüfen" },
];
export function getMonitoringRows(scenario: MonitoringScenario): MonitoringRow[] {
if (scenario === "many") return Array.from({ length: 200 }, (_, index) => {
const row = monitoringRows[index % monitoringRows.length]!;
return { ...row, id: `synthetic-${index + 1}`, name: `${row.name} · Messpunkt ${index + 1}` };
});
const rows = monitoringRows.map(row => ({ ...row }));
if (scenario === "edges") {
rows[0]!.group = "Produktion · Materialüberwachung für die standortübergreifende Fertigung";
rows[0]!.name = "Linie A · Nachbearbeitung und Qualitätssicherung";
rows.push({ id: "missing", group: "Produktion", name: "Linie C", value: null, change: null, status: "Prüfen" });
}
return rows;
}
export function sortMonitoringRows(rows: MonitoringRow[], order: MonitoringOrder): MonitoringRow[] {
return [...rows].sort((a, b) => {
if (order === "attention" && a.status !== b.status) return a.status === "Prüfen" ? -1 : 1;
if (a.change === null && b.change !== null) return 1;
if (b.change === null && a.change !== null) return -1;
return (b.change ?? 0) - (a.change ?? 0) || a.id.localeCompare(b.id, "de", { numeric: true });
});
}
export const monitoringValue = (value: number | null) => value === null ? "Kein Messwert" : `${value} kg`;
export const monitoringChange = (change: number | null) => change === null ? "Keine Vergleichsbasis" : `${change > 0 ? "+" : change < 0 ? "−" : ""}${Math.abs(change)} %`;
05 / TABELLENSTRUKTUR ERHALTEN
Horizontal scrollen, ohne Kontext aufzugeben.
Die Scroll-Tabelle nutzt eine echte HTML-Tabelle mit caption, Spaltenüberschriften und Zeilenüberschriften. scope="col" und scope="row" beschreiben die Zuordnung. Auf kleinen Displays bleibt die Untergruppe beim horizontalen Scrollen sichtbar.
Der Scrollbereich ist benannt, fokussierbar und hat einen sichtbaren Fokusindikator. Die notwendige Scrollbewegung bleibt innerhalb der Tabelle; die gesamte Seite soll nicht seitlich wandern. Prüfe die Bedienung zusätzlich mit Tastatur und Screenreader in den Zielbrowsern. W3C WAI: Zugängliche Datentabellen.
WCAG 1.4.10 erlaubt für Inhalte, deren Verständnis ein zweidimensionales Layout benötigt, Ausnahmen vom einspaltigen Umbruch – etwa für Datentabellen. Das ist kein pauschaler Freibrief für seitlich scrollende Seiten oder abgeschnittene Zellinhalte. W3C: Reflow.
06 / REFLEXION
Welche Variante hilft bei deiner Aufgabe?
Wähle ein Pattern und formuliere einen kleinen Test. Die Rückmeldung ordnet deine Auswahl ein; deine Freitextbegründung bewertest du selbst.
DEIN MERKSATZ
Optimiere nicht nur für die Bildschirmgröße, sondern für die Aufgabe.
Responsive Design bringt Daten nicht bloß platzsparend unter. Es erhält die Informationen und Beziehungen, die Menschen für ihre Aufgabe brauchen.