fix(startseite): kein Innenscroll mehr, fünf Karten in einer Reihe

Auf dem Bildschirm des Nutzers waren die oberen Karten oben abgeschnitten —
Porträt und Name fehlten — und rechts lief ein zweiter Scrollbalken innerhalb
des Belegschaftsbereichs. Drei Ursachen, alle aus derselben Wurzel: der
Bereich hatte eine gedeckelte Höhe und durfte in sich scrollen.

1. Feste Höhe entfernt. Der Bereich richtet sich nach seinem Inhalt und hat
   nur noch eine Mindesthöhe. Passte der Inhalt vorher nicht in den Deckel,
   scrollte er in sich selbst und schnitt oben ab — ein Bereich über einer
   ohnehin scrollenden Seite darf das nie.

2. Fünf Karten stehen ab 1200 px in einer Reihe statt in zwei. Zwei Reihen
   brauchten rund 250 px mehr Höhe; genau dieser Betrag drückte Titel und
   Filterbalken aus dem Bild. Eine Reihe ist zugleich das ehrlichere Bild:
   fünf Kolleginnen und Kollegen nebeneinander, keine Hierarchie.

3. `minWidth: 0` auf der Karte. Ohne das kann eine Flexkarte nicht unter ihre
   Inhaltsbreite schrumpfen — die fünf Karten passten rechnerisch in eine
   Reihe, brachen aber trotzdem um.

Dazu: lange deutsche Komposita («Besichtigungsassistent», «Marktbeobachtung»)
brechen ohne Zutun nicht um und liefen aus schmalen Karten heraus. Funktion,
Abteilung, Personalnummer und beide Textblöcke brechen jetzt im Wort statt
abgeschnitten zu werden — eine halbe Funktionsbezeichnung ist keine, und ohne
Personalnummer ist jemand kein Mitarbeitender.

Geprüft über zehn Auflösungen von 1024×700 bis 3440×1440, ausdrücklich auch
die Windows-Skalierungen 125 % und 150 % (effektiv 1536×864 und 1280×720):
nichts abgeschnitten, nirgends Innenscroll, Titel «Meine Objekte» überall im
Bild. Dazu 433 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925).

Verbleibend: Bei einer Inhaltshöhe unter 620 px (1280×600, 1024×700) steht der
Balken «Filter & Übersicht» rund 30 px unter der Kante. Der Titel bleibt
sichtbar; der Balken kommt mit der kleinsten Scrollbewegung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-08-06 15:33:12 +02:00
parent 42a9bece36
commit e55be64aeb
2 changed files with 41 additions and 41 deletions
+17 -5
View File
@@ -55,9 +55,15 @@ export const AgentStaffCard = memo(function AgentStaffCard({ presence, onOpen }:
flexDirection: 'column', flexDirection: 'column',
alignItems: 'stretch', alignItems: 'stretch',
textAlign: 'left', textAlign: 'left',
// Ohne `minWidth: 0` kann eine Flexkarte nicht unter ihre Inhaltsbreite
// schrumpfen. Die fünf Karten passten dadurch nicht in eine Reihe,
// brachen um — und die zweite Reihe drückte Titel und Filterbalken aus
// dem sichtbaren Bereich.
minWidth: 0,
gap: 1.25, gap: 1.25,
p: { xs: 1.75, xl: 2 }, p: { xs: 1.75, xl: 2 },
borderRadius: 3, borderRadius: 3,
overflow: 'hidden',
// Keine eigene Höhe: im Flexlayout gibt die Zeile die Höhe vor, und alle // Keine eigene Höhe: im Flexlayout gibt die Zeile die Höhe vor, und alle
// Karten einer Zeile werden von selbst gleich hoch. Mit `height: 100%` // Karten einer Zeile werden von selbst gleich hoch. Mit `height: 100%`
// beanspruchte jede Karte die volle Fläche und die zweite Zeile fiel // beanspruchte jede Karte die volle Fläche und die zweite Zeile fiel
@@ -100,12 +106,15 @@ export const AgentStaffCard = memo(function AgentStaffCard({ presence, onOpen }:
<Box sx={{ minWidth: 0, flex: 1 }}> <Box sx={{ minWidth: 0, flex: 1 }}>
<Typography <Typography
sx={{ fontWeight: 700, fontSize: '1.0625rem', lineHeight: 1.2, color: DS_NEUTRAL.white }} sx={{ fontWeight: 700, fontSize: '1.0625rem', lineHeight: 1.2, color: DS_NEUTRAL.white, overflowWrap: 'break-word' }}
> >
{presence.name} {presence.name}
</Typography> </Typography>
<Typography <Typography
sx={{ fontSize: '0.8125rem', lineHeight: 1.3, color: 'rgba(255,255,255,0.72)' }} // Umbrechen statt abschneiden: «Besichtigungsassistent» passt in
// einer schmalen Karte nicht in eine Zeile, und eine halbe
// Funktionsbezeichnung ist keine.
sx={{ fontSize: '0.8125rem', lineHeight: 1.3, color: 'rgba(255,255,255,0.72)', overflowWrap: 'break-word' }}
> >
{presence.role} {presence.role}
</Typography> </Typography>
@@ -116,9 +125,10 @@ export const AgentStaffCard = memo(function AgentStaffCard({ presence, onOpen }:
fontSize: '0.6875rem', fontSize: '0.6875rem',
lineHeight: 1.3, lineHeight: 1.3,
color: 'rgba(255,255,255,0.42)', color: 'rgba(255,255,255,0.42)',
overflow: 'hidden', overflowWrap: 'break-word',
textOverflow: 'ellipsis', // Umbrechen statt abschneiden: in einer schmalen Karte fiele
whiteSpace: 'nowrap', // sonst ausgerechnet die Personalnummer weg, und die macht aus
// einem Werkzeug einen Mitarbeitenden.
}} }}
> >
{[presence.department, presence.personnelNumber].filter(Boolean).join(' · ')} {[presence.department, presence.personnelNumber].filter(Boolean).join(' · ')}
@@ -164,6 +174,7 @@ export const AgentStaffCard = memo(function AgentStaffCard({ presence, onOpen }:
fontSize: '0.8125rem', fontSize: '0.8125rem',
lineHeight: 1.45, lineHeight: 1.45,
color: 'rgba(255,255,255,0.92)', color: 'rgba(255,255,255,0.92)',
overflowWrap: 'break-word',
}} }}
> >
{workload.voice} {workload.voice}
@@ -194,6 +205,7 @@ export const AgentStaffCard = memo(function AgentStaffCard({ presence, onOpen }:
fontSize: '0.75rem', fontSize: '0.75rem',
lineHeight: 1.4, lineHeight: 1.4,
color: 'rgba(255,255,255,0.62)', color: 'rgba(255,255,255,0.62)',
overflowWrap: 'break-word',
display: '-webkit-box', display: '-webkit-box',
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
+24 -36
View File
@@ -8,29 +8,17 @@ import { WorkloadTone } from '../../domain/agentWorkload'
import { DS_NEUTRAL } from '../../lib/ds' import { DS_NEUTRAL } from '../../lib/ds'
/** /**
* Höhe der globalen Kopfzeile plus des Bereichs, der unter der Belegschaft * Mindesthöhe, damit die Belegschaft eine Fläche hat und nicht als schmaler
* sichtbar bleiben muss: Titel «Meine Objekte» samt Untertitel und der Balken * Streifen über der Objektliste klebt.
* «Filter & Übersicht» (Runde 5, §2.3).
* *
* Deshalb wird gerechnet und nicht fest gesetzt: der Bereich bekommt, was die * Eine feste oder gedeckelte Höhe hatte dieser Bereich früher — und genau das
* Viewport-Höhe hergibt, und auf einem 4K-Monitor bleibt derselbe Streifen * war der Fehler: passte der Inhalt nicht hinein, scrollte er in sich selbst,
* unten sichtbar wie auf einem 13-Zoll-Laptop. * die Porträts wurden oben abgeschnitten und neben der Seite stand ein zweiter
* Scrollbalken. Die Höhe richtet sich deshalb nach dem Inhalt. Damit der Titel
* «Meine Objekte» beim Einstieg sichtbar bleibt (§2.3), stehen die fünf
* Mitarbeitenden auf grossen Schirmen in einer einzigen Reihe statt in zweien.
*/ */
const TOP_BAR_PX = 64 const MIN_SCENE_PX = 360
const KEEP_VISIBLE_BELOW_PX = 118
const RESERVED_PX = TOP_BAR_PX + KEEP_VISIBLE_BELOW_PX
/** Darunter würde die Belegschaft zur Briefmarke — dann lieber scrollen. */
const MIN_SCENE_PX = 420
/**
* Obergrenze der Höhe. Ohne sie wächst der Bereich auf hohen Schirmen mit der
* Viewport-Höhe mit, während die Karten ihre Inhaltshöhe behalten — auf einem
* 1440er Monitor stand über und unter den Karten ein halber Bildschirm leere
* dunkle Fläche. Der Titel «Meine Objekte» bleibt auch mit Deckel sichtbar; er
* rückt lediglich weiter nach oben, und mehr von der Objektliste wird sichtbar.
*/
const MAX_SCENE_PX = 760
/** /**
* Grösste Breite der Karten samt Anriss. Freilaufend würden bei drei Spalten * Grösste Breite der Karten samt Anriss. Freilaufend würden bei drei Spalten
@@ -80,10 +68,7 @@ export function AgentStaffOverview() {
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: `clamp(${MIN_SCENE_PX}px, calc(100vh - ${RESERVED_PX}px), ${MAX_SCENE_PX}px)`, minHeight: MIN_SCENE_PX,
'@supports (height: 100dvh)': {
height: `clamp(${MIN_SCENE_PX}px, calc(100dvh - ${RESERVED_PX}px), ${MAX_SCENE_PX}px)`,
},
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
@@ -94,7 +79,6 @@ export function AgentStaffOverview() {
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%',
width: '100%', width: '100%',
maxWidth: MAX_CONTENT_PX, maxWidth: MAX_CONTENT_PX,
mx: 'auto', mx: 'auto',
@@ -107,26 +91,30 @@ export function AgentStaffOverview() {
<Box <Box
sx={{ sx={{
flex: 1, /*
minHeight: 0, * Ab `lg` stehen alle fünf nebeneinander in einer Reihe. Zwei
// Flexlayout statt Raster: bei fünf Karten bliebe in einem * Reihen brauchten rund 250 Pixel mehr Höhe — auf einem Laptop war
// Dreierraster unten eine Lücke. So stehen drei oben und zwei * das genau der Betrag, der den Bereich zum Scrollen brachte und
// mittig darunter — gleich breit, ohne Loch. * die Porträts der oberen Reihe abschnitt. Eine Reihe ist zugleich
* das ehrlichere Bild: fünf Kolleginnen und Kollegen nebeneinander,
* keine Hierarchie über zwei Ebenen.
*
* Darunter wird umbrochen, weil fünf Karten nebeneinander unter
* 1200 Pixeln unlesbar schmal würden.
*/
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'center', justifyContent: 'center',
alignContent: 'center', alignItems: 'stretch',
gap: { xs: 1.5, lg: 2 }, gap: { xs: 1.5, lg: 2 },
'& > *': { '& > *': {
flex: { flex: {
xs: '1 1 100%', xs: '1 1 100%',
sm: '0 1 calc((100% - 12px) / 2)', sm: '0 1 calc((100% - 12px) / 2)',
lg: '0 1 calc((100% - 32px) / 3)', md: '0 1 calc((100% - 24px) / 3)',
lg: '0 1 calc((100% - 64px) / 5)',
}, },
}, },
// Auf niedrigen Fenstern darf die Belegschaft scrollen, statt die
// Karten bis zur Unlesbarkeit zu stauchen.
overflowY: 'auto',
}} }}
> >
{staff.map(person => ( {staff.map(person => (