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 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 10:55:56 +00:00
1 parent 09832c2cf4
commit 09de06e797
3 files changed
+83 -5

No files matched your search

+25 -4
View File
@@ -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;
+47
View File
@@ -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);
+11 -1
View File
@@ -9,6 +9,12 @@ export function matchRoute(pathname: string): RouteId {
return 'editor';
}
const TITLES: Record<RouteId, string> = {
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) => {