From 09de06e797477d755453d50713f6836943baad60 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 10:55:56 +0000 Subject: [PATCH] fix(web): Editor-Layout ab 1024 px und Scroll-Reset beim Seitenwechsel Editor: Seitenspalten geben nach (minmax), die Vorschau bleibt ab 1024 px mindestens 360 px breit, einspaltig erst unter 1024 px. Router: navigate() springt nach dem Rendern nach oben, Dokumenttitel je Route. Co-Authored-By: Claude Opus 5.5 --- web/src/editor/editor.css | 29 +++++++++++++++++++--- web/src/pages/router.test.ts | 47 ++++++++++++++++++++++++++++++++++++ web/src/pages/router.ts | 12 ++++++++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/web/src/editor/editor.css b/web/src/editor/editor.css index 4ff9695..6af003a 100644 --- a/web/src/editor/editor.css +++ b/web/src/editor/editor.css @@ -1,4 +1,4 @@ -/* Editor: drei Spalten (Vorlagen | Vorschau | Optionen), unter 900 px einspaltig. */ +/* Editor: drei Spalten (Vorlagen | Vorschau | Optionen), unter 1024 px einspaltig. */ .editor [hidden] { display: none !important; @@ -30,7 +30,8 @@ --card-radius: 3.711% / 5.882%; /* 57 px von 1536 x 969, nur in der Vorschau */ display: grid; - grid-template-columns: 300px 1fr 400px; + /* Die Seitenspalten geben nach, damit die Vorschau in der Mitte ab 1024 px mindestens 360 px breit bleibt. */ + grid-template-columns: minmax(210px, 280px) minmax(470px, 1fr) minmax(250px, 360px); align-items: stretch; border: 1px solid var(--line); border-radius: 24px; @@ -494,9 +495,29 @@ font-size: 1.1rem; } -/* Unter 900 px: Vorschau, Vorlagen als waagerechte Reihe, Optionen */ +/* Ab 1024 px: schmale Optionsspalte → zwei statt drei Netzwerk-Kacheln pro Reihe, damit Beschriftungen nicht mitten im Wort brechen */ -@media (max-width: 899px) { +@media (min-width: 1024px) { + .mark-grid { + grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); + } +} + +/* 1024 bis 1199 px: drei Spalten, aber die Mitte bekommt kompaktere Knöpfe, damit sie in einer Reihe bleiben */ + +@media (min-width: 1024px) and (max-width: 1199px) { + .editor-center { + padding: 20px; + } + + .editor-center .btn { + padding: 0 12px; + } +} + +/* Unter 1024 px: Vorschau, Vorlagen als waagerechte Reihe, Optionen */ + +@media (max-width: 1023px) { .editor { grid-template-columns: minmax(0, 1fr); gap: 16px; diff --git a/web/src/pages/router.test.ts b/web/src/pages/router.test.ts index 044adce..c90a21c 100644 --- a/web/src/pages/router.test.ts +++ b/web/src/pages/router.test.ts @@ -125,6 +125,53 @@ describe('startRouter', () => { expect(location.pathname).toBe('/'); }); + describe('Scrollposition und Titel', () => { + it('navigate springt nach dem Rendern an den Seitenanfang', () => { + const order: string[] = []; + const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => order.push('scroll')); + try { + startRouter(() => { + order.push('render'); + }); + order.length = 0; + navigate('/karten'); + expect(scrollTo).toHaveBeenCalledTimes(1); + expect(scrollTo).toHaveBeenCalledWith(0, 0); + expect(order).toEqual(['render', 'scroll']); + } finally { + scrollTo.mockRestore(); + } + }); + + it('beim Start und bei popstate wird nicht gescrollt (das übernimmt der Browser)', () => { + const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => {}); + try { + startRouter(vi.fn()); + history.pushState(null, '', '/karten'); + window.dispatchEvent(new PopStateEvent('popstate')); + expect(scrollTo).not.toHaveBeenCalled(); + } finally { + scrollTo.mockRestore(); + } + }); + + it('setzt den Dokumenttitel je Route', () => { + vi.spyOn(window, 'scrollTo').mockImplementation(() => {}); + try { + startRouter(vi.fn()); + expect(document.title).toBe('AirCard-IOS – Kartenmotiv gestalten'); + navigate('/karten'); + expect(document.title).toBe('Meine Karten – AirCard-IOS'); + navigate('/passwort-neu'); + expect(document.title).toBe('Passwort neu setzen – AirCard-IOS'); + navigate('/'); + expect(document.title).toBe('AirCard-IOS – Kartenmotiv gestalten'); + } finally { + vi.restoreAllMocks(); + } + }); + }); + it('rendert bei popstate die dann aktuelle Route', () => { const render = vi.fn(); startRouter(render); diff --git a/web/src/pages/router.ts b/web/src/pages/router.ts index 74bdc2c..ebe0d07 100644 --- a/web/src/pages/router.ts +++ b/web/src/pages/router.ts @@ -9,6 +9,12 @@ export function matchRoute(pathname: string): RouteId { return 'editor'; } +const TITLES: Record = { + editor: 'AirCard-IOS – Kartenmotiv gestalten', + cards: 'Meine Karten – AirCard-IOS', + reset: 'Passwort neu setzen – AirCard-IOS', +}; + // Beendet den laufenden Router (Seiten-Cleanup, Listener); null, solange keiner läuft. let stopCurrent: (() => void) | null = null; let renderCurrent: (() => void) | null = null; @@ -20,6 +26,8 @@ export function navigate(path: string): void { history.pushState(null, '', target); } renderCurrent?.(); + // Eine neue Seite beginnt oben (pushState setzt die Scrollposition nicht zurück); bei popstate bleibt es dem Browser überlassen. + if (renderCurrent) window.scrollTo(0, 0); } export function startRouter(render: Render): void { @@ -33,7 +41,9 @@ export function startRouter(render: Render): void { const renderPage = () => { cleanup?.(); cleanup = undefined; - cleanup = render(matchRoute(location.pathname), root); + const route = matchRoute(location.pathname); + document.title = TITLES[route]; + cleanup = render(route, root); }; const onClick = (event: MouseEvent) => {