Compare commits

..
2 Commits
Author SHA1 Message Date
florian.wilde 9c873a4069 Merge branch 'phase/034-mobil'
Prüfung / pruefung (push) Canceled after 0s
2026-10-03 15:55:59 +02:00
florian.wildeandClaude Opus 5 09bb98235b fix(ui): Seitenmenue auf dem Handy schliessbar machen (#034)
Das Markup stammt von AdminLTE, dessen JavaScript wir bewusst nicht
einbinden. Die Klasse sidebar-open liess sich damit setzen, aber nichts nahm
sie wieder weg: keine Flaeche zum Antippen, kein Schliessen beim Navigieren,
kein Escape. Auf dem Telefon blieb das Menue ueber dem Inhalt stehen.

Jetzt gibt es einen eigenen Zustand, auf den Kopfzeile und Menue gemeinsam
hoeren, eine abdunkelnde Flaeche dahinter, einen Schliessen-Knopf im Menuekopf
und das Schliessen beim Navigieren. Der Inhalt dahinter scrollt nicht mehr mit.

Ausserdem zeigt die Dashboard-Tabelle auf dem Telefon nur Host und Ablauf, den
Kunden klein darunter - vorher war rechts die Haelfte abgeschnitten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 15:55:59 +02:00
34 changed files with 286 additions and 25 deletions
+9
View File
@@ -17,6 +17,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
### Sicherheit ### Sicherheit
## [0.20.2] – 2026-10-03
### Behoben
- **Das Seitenmenü ließ sich auf dem Handy nicht schließen.** Es gab weder eine Fläche zum Antippen noch ein Schließen beim Navigieren – das Markup stammt von AdminLTE, dessen JavaScript wir bewusst nicht einbinden (#034)
- Die Dashboard-Tabelle war auf dem Telefon rechts abgeschnitten; sie zeigt dort jetzt Host und Ablauf, den Kunden klein darunter (#034)
### Hinzugefügt
- Schließen-Knopf im Menükopf, Schließen per Escape, und der Inhalt hinter dem offenen Menü scrollt nicht mehr mit (#034)
## [0.20.1] – 2026-10-03 ## [0.20.1] – 2026-10-03
### Behoben ### Behoben
+6 -2
View File
@@ -2,7 +2,7 @@
## Zeitübersicht ## Zeitübersicht
**Gesamt:** 16 h 58 min **Gesamt:** 17 h 38 min
| Issue | Zeit | | Issue | Zeit |
|---|---| |---|---|
@@ -39,6 +39,7 @@
| #031 Update für beide Varianten | 0 h 45 min | | #031 Update für beide Varianten | 0 h 45 min |
| #032 LXC-Installation korrigiert | 0 h 20 min | | #032 LXC-Installation korrigiert | 0 h 20 min |
| #033 Weiterleitung auf Lausch-Adresse | 0 h 30 min | | #033 Weiterleitung auf Lausch-Adresse | 0 h 30 min |
| #034 Bedienung auf dem Handy | 0 h 40 min |
| Meilenstein | Zeit | | Meilenstein | Zeit |
|---|---| |---|---|
@@ -61,7 +62,7 @@
## 2026-09-17 · Sitzung 1 · 19:36–laufend ## 2026-09-17 · Sitzung 1 · 19:36–laufend
**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0)
**Version:** 0.20.1 **Version:** 0.20.2
### Erledigt ### Erledigt
- Auftrag gelesen, Rückfragen gestellt - Auftrag gelesen, Rückfragen gestellt
@@ -179,6 +180,9 @@
- #033 war hausgemacht: Mein `HOSTNAME=0.0.0.0` aus #031 hat den Healthcheck repariert und dafür alle Weiterleitungen aus Server Actions auf `0.0.0.0` gelenkt. Vorher stand dort der Containername – derselbe Fehler, nur unauffälliger - #033 war hausgemacht: Mein `HOSTNAME=0.0.0.0` aus #031 hat den Healthcheck repariert und dafür alle Weiterleitungen aus Server Actions auf `0.0.0.0` gelenkt. Vorher stand dort der Containername – derselbe Fehler, nur unauffälliger
- Lehre: Weiterleitungen gehören aus der konfigurierten Adresse gebaut, nie aus dem, was der Server über sich selbst weiß - Lehre: Weiterleitungen gehören aus der konfigurierten Adresse gebaut, nie aus dem, was der Server über sich selbst weiß
- Mobile Bedienung (#034): Das Menü ließ sich auf dem Telefon nicht schließen – AdminLTE-Markup ohne AdminLTE-JavaScript. Eigener Zustand, abdunkelnde Fläche, Schließen beim Navigieren und mit Escape
- Lehre: Die Mobilansicht war mit Screenshots geprüft, nicht mit den Fingern. Ein Bild zeigt nicht, dass etwas nicht wieder weggeht
### Nächster Schritt ### Nächster Schritt
- #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline - #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline
- Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023) - Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023)
+5
View File
@@ -473,6 +473,11 @@ Server-ID stehen in der Detailansicht unter *Subscription-Historie*.
## Anhang: dunkles Design und Mobilansicht ## Anhang: dunkles Design und Mobilansicht
Auf dem Telefon öffnet das Symbol links oben das Menü; es schließt sich durch Antippen der
abgedunkelten Fläche, über das Kreuz im Menükopf, mit Escape – und von selbst, sobald ein Eintrag
gewählt wurde. Die Tabellen zeigen auf schmalen Bildschirmen nur die wichtigsten Spalten; die
übrigen Angaben stehen auf der jeweiligen Detailseite.
Das Dashboard folgt der Einstellung des Betriebssystems und lässt sich über das Mondsymbol in der Das Dashboard folgt der Einstellung des Betriebssystems und lässt sich über das Mondsymbol in der
Kopfleiste umschalten. Kopfleiste umschalten.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 252 KiB

+28
View File
@@ -0,0 +1,28 @@
# #034 Bedienung auf dem Handy
**Status:** erledigt
**Meilenstein:** v0.20.2
**Labels:** ui, bug
**Geschätzt:** 2 h **Tatsächlich:** 0 h 40 min
## Auslöser
Florian im produktiven Einsatz auf dem Telefon: „Das Menü geht nicht weg.“ Dazu eine Tabelle, von der
rechts die Hälfte abgeschnitten war.
## Ursache
Das Markup stammt von AdminLTE, dessen JavaScript binden wir bewusst nicht ein. Die Klasse
`sidebar-open` ließ sich damit setzen – aber nichts nahm sie wieder weg: keine Fläche zum Antippen,
kein Schließen beim Navigieren, kein Escape. Auf dem Telefon blieb das Menü über dem Inhalt stehen.
## Behebung
- Eigener, winziger Zustand (`menuZustand.ts`), auf den Kopfzeile und Menü gemeinsam hören
- Abdunkelnde Fläche hinter dem Menü – antippen schließt (ein `button`, damit auch per Tastatur erreichbar)
- Schließen-Knopf im Menükopf, nur auf schmalen Bildschirmen
- Schließt beim Navigieren und mit Escape
- Der Inhalt dahinter scrollt nicht mehr mit, solange das Menü offen ist
- Dashboard-Tabelle: auf dem Telefon nur Host und Ablauf, der Kunde steht klein unter dem Hostnamen.
Key und Rechnungsstände erscheinen ab Tablet-Breite – die Host-Tabelle machte das schon vorher so.
- Fünf Tests für die Zustandslogik
## Nicht geändert
Die abdunkelnde Fläche gilt nur unter 992 px. Auf breiten Bildschirmen bleibt es beim bisherigen
Verhalten: Das Menü klappt zu Symbolen zusammen, statt sich über den Inhalt zu legen.
+2 -1
View File
@@ -37,4 +37,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
| [#031](031-update-skript-beide-varianten.md) | Update- und Sicherungsskript für beide Varianten | erledigt | v0.20.0 | 2 h | 0 h 45 min | | [#031](031-update-skript-beide-varianten.md) | Update- und Sicherungsskript für beide Varianten | erledigt | v0.20.0 | 2 h | 0 h 45 min |
| [#032](032-lxc-installation-korrekturen.md) | LXC-Installation: Herkunft der Skripte und Speicher | erledigt | v0.20.1 | 1 h | 0 h 20 min | | [#032](032-lxc-installation-korrekturen.md) | LXC-Installation: Herkunft der Skripte und Speicher | erledigt | v0.20.1 | 1 h | 0 h 20 min |
| [#033](033-weiterleitung-lauschadresse.md) | Abmelden führte auf die Lausch-Adresse | erledigt | v0.20.1 | 1 h | 0 h 30 min | | [#033](033-weiterleitung-lauschadresse.md) | Abmelden führte auf die Lausch-Adresse | erledigt | v0.20.1 | 1 h | 0 h 30 min |
| | **Summe** | | | **151 h** | **16 h 58 min** | | [#034](034-mobile-bedienung.md) | Bedienung auf dem Handy | erledigt | v0.20.2 | 2 h | 0 h 40 min |
| | **Summe** | | | **153 h** | **17 h 38 min** |
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "proxledger", "name": "proxledger",
"version": "0.20.1", "version": "0.20.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
+22 -8
View File
@@ -121,12 +121,22 @@ export default async function DashboardSeite() {
<table className="table table-striped align-middle"> <table className="table table-striped align-middle">
<thead> <thead>
<tr> <tr>
{/* Auf dem Handy bleiben Host und Ablauf; der Rest käme ohnehin nur
per Seitwärtsscrollen ins Bild (#034). */}
<th scope="col">Host</th> <th scope="col">Host</th>
<th scope="col">Kunde</th> <th scope="col" className="d-none d-md-table-cell">
<th scope="col">Key</th> Kunde
</th>
<th scope="col" className="d-none d-lg-table-cell">
Key
</th>
<th scope="col">Ablauf</th> <th scope="col">Ablauf</th>
<th scope="col">Proxmox</th> <th scope="col" className="d-none d-sm-table-cell">
<th scope="col">Kundenrechnung</th> Proxmox
</th>
<th scope="col" className="d-none d-sm-table-cell">
Kundenrechnung
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -137,8 +147,12 @@ export default async function DashboardSeite() {
{host.hostname} {host.hostname}
</Link> </Link>
{host.meldetNicht ? <KeineMeldungBadge /> : null} {host.meldetNicht ? <KeineMeldungBadge /> : null}
{/* Auf schmalen Bildschirmen steht der Kunde hier mit darunter. */}
<div className="small text-body-secondary d-md-none">
{host.kunde ? host.kunde.name : "Nicht zugeordnet"}
</div>
</td> </td>
<td> <td className="d-none d-md-table-cell">
{host.kunde ? ( {host.kunde ? (
<Link href={`/kunden/${host.kunde.id}`} className="text-decoration-none"> <Link href={`/kunden/${host.kunde.id}`} className="text-decoration-none">
{host.kunde.name} {host.kunde.name}
@@ -152,16 +166,16 @@ export default async function DashboardSeite() {
</Link> </Link>
)} )}
</td> </td>
<td> <td className="d-none d-lg-table-cell">
<CopyableCode wert={host.subKey} /> <CopyableCode wert={host.subKey} />
</td> </td>
<td> <td>
<AblaufBadge datum={host.nextduedate?.toISOString() ?? null} heute={heute} /> <AblaufBadge datum={host.nextduedate?.toISOString() ?? null} heute={heute} />
</td> </td>
<td> <td className="d-none d-sm-table-cell">
<EinkaufBadge bezahlt={host.einkaufBezahlt} /> <EinkaufBadge bezahlt={host.einkaufBezahlt} />
</td> </td>
<td> <td className="d-none d-sm-table-cell">
<VerkaufBadge erstellt={host.verkaufErstellt} bezahlt={host.verkaufBezahlt} /> <VerkaufBadge erstellt={host.verkaufErstellt} bezahlt={host.verkaufBezahlt} />
</td> </td>
</tr> </tr>
+13
View File
@@ -186,6 +186,19 @@ body {
display: none; display: none;
} }
/*
* Fläche hinter dem geöffneten Menü auf schmalen Bildschirmen. Ein Knopf statt eines div,
* damit sie auch per Tastatur erreichbar ist.
*/
.pl-menu-hintergrund {
position: fixed;
inset: 0;
z-index: 1037; /* unter dem Menü (1038), über allem anderen */
padding: 0;
background-color: rgba(0, 0, 0, 0.5);
border: 0;
}
/* ---------- Inhaltsbereich ---------- */ /* ---------- Inhaltsbereich ---------- */
.app-content-header { .app-content-header {
+3 -9
View File
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { SearchDialog, type SuchEintrag } from "./SearchDialog"; import { SearchDialog, type SuchEintrag } from "./SearchDialog";
import type { Rolle } from "@/generated/prisma/enums"; import type { Rolle } from "@/generated/prisma/enums";
import { ROLLEN_TEXT } from "@/server/auth/rollen"; import { ROLLEN_TEXT } from "@/server/auth/rollen";
import { menuUmschalten } from "./menuZustand";
export interface KopfleistenAlert { export interface KopfleistenAlert {
id: string; id: string;
@@ -58,16 +59,9 @@ export function Header({
return () => window.removeEventListener("keydown", tasten); return () => window.removeEventListener("keydown", tasten);
}, []); }, []);
/** Sidebar umschalten: auf schmalen Bildschirmen als Overlay, sonst eingeklappt. */ /** Seitenmenü umschalten – der Zustand liegt in menuZustand.ts, damit das Menü selbst ihn kennt. */
const sidebarUmschalten = useCallback(() => { const sidebarUmschalten = useCallback(() => {
const body = document.body; menuUmschalten();
if (window.innerWidth < 992) {
body.classList.toggle("sidebar-open");
body.classList.remove("sidebar-collapse");
} else {
body.classList.toggle("sidebar-collapse");
body.classList.remove("sidebar-open");
}
}, []); }, []);
function themaUmschalten() { function themaUmschalten() {
+40 -4
View File
@@ -2,10 +2,11 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useState } from "react"; import { useEffect, useState } from "react";
import { LogoMitSchriftzug } from "@/components/Logo"; import { LogoMitSchriftzug } from "@/components/Logo";
import type { Rolle } from "@/generated/prisma/enums"; import type { Rolle } from "@/generated/prisma/enums";
import { darf } from "@/server/auth/rollen"; import { darf } from "@/server/auth/rollen";
import { menuSchliessen, useMenuOffen } from "./menuZustand";
interface MenuEintrag { interface MenuEintrag {
href: string; href: string;
@@ -30,6 +31,21 @@ export function Sidebar({
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const [suche, setSuche] = useState(""); const [suche, setSuche] = useState("");
const menuOffen = useMenuOffen();
// Nach dem Navigieren hat das Menü seinen Zweck erfüllt – auf dem Handy verdeckt es sonst alles.
useEffect(() => {
menuSchliessen();
}, [pathname]);
// Escape schließt, wie bei jedem anderen Overlay auch.
useEffect(() => {
function taste(ereignis: KeyboardEvent) {
if (ereignis.key === "Escape") menuSchliessen();
}
window.addEventListener("keydown", taste);
return () => window.removeEventListener("keydown", taste);
}, []);
const haupt: MenuEintrag[] = [ const haupt: MenuEintrag[] = [
{ href: "/", titel: "Dashboard", icon: "bi-speedometer2" }, { href: "/", titel: "Dashboard", icon: "bi-speedometer2" },
@@ -105,11 +121,30 @@ export function Sidebar({
} }
return ( return (
<aside className="app-sidebar shadow" data-bs-theme="dark"> <>
<div className="sidebar-brand"> {/* Fläche hinter dem Menü: antippen schließt. Ohne sie bleibt das Menü auf dem Handy stehen. */}
{menuOffen ? (
<button
type="button"
className="pl-menu-hintergrund d-lg-none"
aria-label="Menü schließen"
onClick={menuSchliessen}
/>
) : null}
<aside className="app-sidebar shadow" data-bs-theme="dark">
<div className="sidebar-brand d-flex align-items-center justify-content-between">
<Link href="/" className="brand-link"> <Link href="/" className="brand-link">
<LogoMitSchriftzug variante="sidebar" groesse={34} /> <LogoMitSchriftzug variante="sidebar" groesse={34} />
</Link> </Link>
<button
type="button"
className="btn btn-link text-white d-lg-none pl-touch-target me-2"
aria-label="Menü schließen"
onClick={menuSchliessen}
>
<i className="bi bi-x-lg" aria-hidden="true" />
</button>
</div> </div>
<div className="pl-sidebar-user"> <div className="pl-sidebar-user">
@@ -147,6 +182,7 @@ export function Sidebar({
</ul> </ul>
</nav> </nav>
</div> </div>
</aside> </aside>
</>
); );
} }
+66
View File
@@ -0,0 +1,66 @@
"use client";
import { useSyncExternalStore } from "react";
/**
* Zustand des Seitenmenüs auf schmalen Bildschirmen (#034).
*
* AdminLTE bringt dafür eigenes JavaScript mit, das wir bewusst nicht einbinden – die Klasse
* `sidebar-open` am `body` allein lässt sich aber nicht wieder loswerden: Kein Hintergrund zum
* Antippen, kein Schließen beim Navigieren. Deshalb ein eigener, winziger Zustand, auf den
* Kopfzeile und Menü gemeinsam hören.
*/
const SCHMAL = 992;
let offen = false;
const zuhoerer = new Set<() => void>();
function melden() {
for (const ruf of zuhoerer) ruf();
}
/** Setzt die Klassen, die das AdminLTE-Stylesheet auswertet. */
function klassenSetzen() {
const body = document.body;
body.classList.toggle("sidebar-open", offen);
// Auf schmalen Bildschirmen liegt das Menü über dem Inhalt; dahinter soll nichts mitscrollen.
body.style.overflow = offen && window.innerWidth < SCHMAL ? "hidden" : "";
}
export function menuUmschalten() {
if (window.innerWidth >= SCHMAL) {
// Breiter Bildschirm: Das Menü bleibt stehen und wird nur schmaler.
document.body.classList.toggle("sidebar-collapse");
document.body.classList.remove("sidebar-open");
return;
}
offen = !offen;
document.body.classList.remove("sidebar-collapse");
klassenSetzen();
melden();
}
export function menuSchliessen() {
if (!offen) return;
offen = false;
klassenSetzen();
melden();
}
function abonnieren(ruf: () => void) {
zuhoerer.add(ruf);
return () => {
zuhoerer.delete(ruf);
};
}
export function useMenuOffen(): boolean {
// Auf dem Server ist das Menü nie offen – sonst käme es beim ersten Rendern zu einem Unterschied.
return useSyncExternalStore(
abonnieren,
() => offen,
() => false,
);
}
+91
View File
@@ -0,0 +1,91 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
/**
* Prüft das Öffnen und Schließen des Seitenmenüs. Statt eines echten Browsers genügen hier
* Attrappen für `document.body` und `window` – geprüft wird, welche Klassen gesetzt werden.
*/
const klassen = new Set<string>();
let breite = 390;
function aufraeumen() {
klassen.clear();
breite = 390;
}
beforeEach(async () => {
aufraeumen();
vi.resetModules();
const body = {
classList: {
toggle(name: string, an?: boolean) {
const soll = an ?? !klassen.has(name);
if (soll) klassen.add(name);
else klassen.delete(name);
},
remove(name: string) {
klassen.delete(name);
},
contains: (name: string) => klassen.has(name),
},
style: { overflow: "" },
};
vi.stubGlobal("document", { body });
vi.stubGlobal("window", {
get innerWidth() {
return breite;
},
});
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe("Seitenmenü auf schmalen Bildschirmen", () => {
it("öffnet und schließt wieder", async () => {
const { menuUmschalten, menuSchliessen } = await import("@/components/layout/menuZustand");
menuUmschalten();
expect(klassen.has("sidebar-open")).toBe(true);
menuSchliessen();
expect(klassen.has("sidebar-open")).toBe(false);
});
it("sperrt den Inhalt hinter dem offenen Menü gegen Scrollen", async () => {
const { menuUmschalten, menuSchliessen } = await import("@/components/layout/menuZustand");
menuUmschalten();
expect((document.body as unknown as { style: { overflow: string } }).style.overflow).toBe("hidden");
menuSchliessen();
expect((document.body as unknown as { style: { overflow: string } }).style.overflow).toBe("");
});
it("schließt auch beim zweiten Antippen des Knopfes", async () => {
const { menuUmschalten } = await import("@/components/layout/menuZustand");
menuUmschalten();
menuUmschalten();
expect(klassen.has("sidebar-open")).toBe(false);
});
it("macht ein zweites Schließen zum Nichts", async () => {
const { menuSchliessen } = await import("@/components/layout/menuZustand");
menuSchliessen();
expect(klassen.has("sidebar-open")).toBe(false);
});
it("klappt auf breiten Bildschirmen nur ein, statt zu überlagern", async () => {
breite = 1400;
const { menuUmschalten } = await import("@/components/layout/menuZustand");
menuUmschalten();
expect(klassen.has("sidebar-collapse")).toBe(true);
expect(klassen.has("sidebar-open")).toBe(false);
});
});