diff --git a/frontend/src/breakpoints.ts b/frontend/src/breakpoints.ts new file mode 100644 index 0000000..8e6a463 --- /dev/null +++ b/frontend/src/breakpoints.ts @@ -0,0 +1,22 @@ +/** + * Die eine Breite, ab der die Bedienung „breit" ist. + * + * Sie entscheidet **zwei** Dinge, und beide müssen zusammen umschalten: + * die Seitenleiste steht fest im Fluss statt über dem Inhalt zu liegen, und + * die Kopfleiste kommt in eine Zeile – der Knopf zum Aufschieben verschwindet + * dabei, weil es nichts mehr aufzuschieben gibt. + * + * Vorher standen dafür zwei Zahlen an zwei Stellen: 720px in der Kopfleiste, + * 767px in der Seitenleiste. Dazwischen lag ein Band von 48 Pixeln, in dem der + * Knopf schon weg war, die Leiste aber noch nach links aus dem Bild geschoben – + * kein Menü, kein Weg dorthin. Ein aufgeklapptes Fold landet mit rund 757 px + * genau darin. + * + * Also: eine Zahl, ein Ort. Wer sie ändert, ändert beides zugleich. + */ +export const wideEnough = '(min-width: 720px)' + +/** Dieselbe Grenze zur Laufzeit – für Fälle, die kein CSS lösen kann. */ +export function isWide(): boolean { + return typeof matchMedia === 'function' && matchMedia(wideEnough).matches +} diff --git a/frontend/src/components/PageHeader.tsx b/frontend/src/components/PageHeader.tsx index b3f2bed..b1db095 100644 --- a/frontend/src/components/PageHeader.tsx +++ b/frontend/src/components/PageHeader.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import UserMenu, { useSidebarToggle } from './UserMenu' +import { wideEnough } from '../breakpoints' /** * Die Kopfleiste jeder Ansicht. @@ -23,8 +24,12 @@ export const headerHeight = 57 * Ansichts-Umschalter war ganz verschwunden und der Nutzerbereich am rechten * Rand abgeschnitten. Eine Zeile, die nicht umbrechen kann, quetscht – und * ausgerechnet auf dem Gerät, mit dem man unterwegs einen Beleg sucht. + * + * Dieselbe Grenze schaltet die Seitenleiste um; sie steht deshalb in + * `breakpoints.ts` und nicht mehr hier. Re-exportiert, weil sie hier gesucht + * wird. */ -export const wideEnough = '(min-width: 720px)' +export { wideEnough } interface Props { /** Strichsymbol, 18 × 18 – dasselbe wie in der Seitenleiste. */ diff --git a/frontend/src/components/Shell.tsx b/frontend/src/components/Shell.tsx index 468830f..a9168f9 100644 --- a/frontend/src/components/Shell.tsx +++ b/frontend/src/components/Shell.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { filesApi } from '../api/client' import type { FolderItem, User } from '../api/client' import { useNavHistory, type View } from '../hooks/useNavHistory' @@ -14,6 +14,7 @@ import ProfilePage from '../pages/ProfilePage' import TagsPage from '../pages/TagsPage' import CalendarPage from '../pages/CalendarPage' import { UserMenuProvider } from './UserMenu' +import { wideEnough } from '../breakpoints' export type { View } @@ -44,6 +45,19 @@ export default function Shell({ user, onLogout, onUserChanged }: Props) { onClose: useCallback(() => { setSidebarOpen(false) }, []), }) + // Wird das Gerät aufgeklappt, während die Leiste aufgeschoben war, steht sie + // ab da ohnehin fest im Bild. Bliebe „aufgeschoben" dabei stehen, schluckte + // die Zurück-Geste einmal ins Leere – sie zielte auf etwas, das nichts mehr + // verdeckt. + useEffect(() => { + if (typeof matchMedia !== 'function') return + const mq = matchMedia(wideEnough) + const auf = () => { if (mq.matches) setSidebarOpen(false) } + auf() + mq.addEventListener('change', auf) + return () => mq.removeEventListener('change', auf) + }, []) + // Hochgezählt, wenn etwas auf den Papierkorb gezogen wurde – die Dateiliste // hängt daran und holt sich daraufhin neu. const [trashBump, setTrashBump] = useState(0) @@ -81,9 +95,13 @@ export default function Shell({ user, onLogout, onUserChanged }: Props) { onOpenSidebar: () => setSidebarOpen(true), }}>
- {/* Mobile overlay */} + {/* Der abdunkelnde Grund hinter der aufgeschobenen Leiste. + Die Klasse blendet ihn aus, sobald die Leiste fest im Bild steht: + Dort liegt sie im Fluss und **ohne** z-index, ein Grund darüber + machte sie unanklickbar – sichtbar, aber taub. */} {sidebarOpen && (
setSidebarOpen(false)} /> diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 40657cd..253e3a4 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -8,6 +8,7 @@ import { RenameIcon, TrashIcon } from './icons' import type { User } from '../api/client' import type { View } from './Shell' import { useAppSettings } from '../hooks/useAppSettings' +import { wideEnough } from '../breakpoints' interface Props { user: User @@ -775,7 +776,11 @@ export default function Sidebar({ user, view, activeTagId, onOpenTag, onTrashDro overflow-y: auto; overflow-x: hidden; } - @media (max-width: 767px) { + /* Schmal: Die Leiste liegt über dem Inhalt statt neben ihm. + Die Grenze ist dieselbe, die in der Kopfleiste den Knopf zum + Aufschieben zeigt – hätte sie eine eigene, gäbe es dazwischen einen + Streifen ohne Menü und ohne Knopf dorthin. */ + @media not all and ${wideEnough} { .aether-sidebar { position: fixed; top: var(--honeybar-h); left: 0; diff --git a/frontend/src/hooks/useEdgeSwipe.ts b/frontend/src/hooks/useEdgeSwipe.ts index 37ba418..605f438 100644 --- a/frontend/src/hooks/useEdgeSwipe.ts +++ b/frontend/src/hooks/useEdgeSwipe.ts @@ -1,4 +1,5 @@ import { useEffect } from 'react' +import { isWide } from '../breakpoints' /** * Wischen vom linken Rand öffnet die Seitenleiste, Wischen nach links @@ -50,6 +51,10 @@ export function useEdgeSwipe({ open, onOpen, onClose }: Options) { function onStart(e: TouchEvent) { valid = false if (e.touches.length !== 1) return // zwei Finger heißt zoomen + // Steht die Leiste ohnehin fest im Bild – aufgeklapptes Faltgerät, + // Querformat –, gibt es nichts aufzuschieben. Bei jeder Geste neu + // gefragt, weil sich die Breite hier im Betrieb ändert. + if (isWide()) return const t = e.touches[0] // Nicht aus einer bildschirmfüllenden Ansicht heraus: Dort läge die diff --git a/frontend/src/index.css b/frontend/src/index.css index eb6a82e..d973bd3 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -126,3 +126,10 @@ body { an der Kante kleben zu lassen. */ .aether-content { padding: 12px; } } + +/* Der abgedunkelte Grund hinter der aufgeschobenen Seitenleiste. + Steht die Leiste fest im Bild, gibt es nichts abzudunkeln – und ein Grund + darüber würde sie unbedienbar machen, ohne dass man ihm das ansieht. */ +@media (min-width: 720px) { + .aether-scrim { display: none; } +}