Musterseite verworfen, Zahnrad-Regression behoben, Fotomenue als Aktionsblatt

Musterseite entfernt (companion-app/muster, vite.muster.config.ts,
/config/www/dmmuster). Ansage des Eigentuemers: sie fuehrt nur zu neuen
Fehlerquellen. Ausloeser war ein Befund, den ich als App-Fehler gemeldet hatte
- Ringe und Typenschild schwarz auf schwarz - und der nur dort bestand.

Behoben:

* Das Zahnrad im Autohaus-Kasten hatte seine Ecke verlassen (58/288 statt
  8/8). Ursache war mein HIG-Durchgang von gestern: .zahnrad kam in die
  Gruppe fuer die 44x44-Trefferflaeche und bekam damit position:relative,
  was das absolute aus dem Basisblatt ueberschrieb. Die Auflage war
  ueberfluessig - .zahnrad ist dort schon 44x44.
* Das Fotomenue hing mit position:absolute an der Kachel statt am
  Bildschirm und lag bei der hohen Raeder-Kachel ausserhalb des Sichtfelds;
  einen Abbrechen-Knopf hatte es nie. Das Panel benutzt jetzt dasselbe
  Aktionsblatt wie die App. .bildmenu ist samt Zustand, Markup, totem
  Handler und CSS entfernt.
* Der Blattkopf las sich als dritte Auswahl (linksbuendig, 15px, --fg).
  Jetzt zentriert, 13px, gedaempft - Apples Muster, in beiden Codebasen.
* "Anstehende Termine" stand 5px ueber "Oelwechsel" und wirkte als dessen
  Beischrift. Entfernt.
* Wartungsplan-Formular: Art nach oben, Datum 124px statt Browser-Vorgabe,
  Werkstatt ueber die volle Zeile (218 -> 306px), Kosten 116px. Zwei
  Erklaertexte entfernt.
* Thema folgt jetzt einer Systemumstellung im laufenden Betrieb. Vorher las
  es prefers-color-scheme genau einmal beim Laden. Drei Tests, die ohne die
  Behebung nachweislich fallen.

Geprueft: 295 App-Tests, Panel node --check, 0 Tracebacks, alle Aenderungen
live an der Testinstanz gemessen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-05 02:24:08 +02:00
co-authored by Claude Opus 5
parent 863284e540
commit e9c16325cc
16 changed files with 2674 additions and 2467 deletions
+19 -21
View File
@@ -130,7 +130,6 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
return (
<>
<Tile>
<span className="ads-eyebrow">Anstehende Termine</span>
<div className="dm-termingruppe">
<div className="dm-tgkopf">
@@ -504,17 +503,6 @@ export function Servicebuch({
<Tile>
<span className="ads-eyebrow">Eintrag</span>
<Feld label="Datum">
<Datumsfeld wert={entwurf.datum ?? ""} beiAenderung={(w) => setze("datum", w)} />
</Feld>
<Feld label="Kilometerstand" unit="km">
<input
className="dm-eingabe"
inputMode="numeric"
value={entwurf.km ?? ""}
onChange={(e) => setze("km", Number(e.target.value))}
/>
</Feld>
<Feld label="Art">
<select
className="dm-auswahl"
@@ -531,25 +519,38 @@ export function Servicebuch({
))}
</select>
</Feld>
<Feld label="Werkstatt">
<Feld label="Datum">
<Datumsfeld wert={entwurf.datum ?? ""} beiAenderung={(w) => setze("datum", w)} />
</Feld>
<Feld label="Kilometerstand" unit="km">
<input
className="dm-eingabe"
inputMode="numeric"
value={entwurf.km ?? ""}
onChange={(e) => setze("km", Number(e.target.value))}
/>
</Feld>
{/* Label oben, Feld über die ganze Zeile - wie der Reifen-Kommentar
und .feld--breit im Panel. Ein echter Werkstattname braucht 322px;
neben einem Label bleiben auf 375px nur 218 (gemessen 05.09.2026). */}
<div className="ads-feld" style={{ display: "block" }}>
<label style={{ display: "block", marginBottom: 8 }}>Werkstatt</label>
<input
className="dm-eingabe"
style={{ width: "100%", textAlign: "left" }}
value={entwurf.werkstatt ?? ""}
onChange={(e) => setze("werkstatt", e.target.value)}
/>
</Feld>
</div>
<Feld label="Kosten" unit="€" last>
<input
className="dm-eingabe"
className="dm-eingabe dm-eingabe--betrag"
inputMode="decimal"
placeholder="noch offen"
value={entwurf.kosten ?? ""}
onChange={(e) => setze("kosten", e.target.value === "" ? null : Number(e.target.value))}
/>
</Feld>
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
Kosten fließen in die Jahresübersicht ein
</span>
</Tile>
<Tile>
@@ -561,9 +562,6 @@ export function Servicebuch({
value={entwurf.arbeiten ?? ""}
onChange={(e) => setze("arbeiten", e.target.value)}
/>
<span className="ads-eyebrow" style={{ marginTop: "var(--sp-3)" }}>
Beim Verkauf ist diese Liste mehr wert als die Rechnung sie zeigt, dass gepflegt wurde
</span>
</Tile>
<ActionButton variant="primary" onClick={() => void speichern()} disabled={laeuft}>
File diff suppressed because it is too large Load Diff
+57
View File
@@ -38,3 +38,60 @@ describe("Theme", () => {
})
})
/* Stellt das Gerät im laufenden Betrieb um - etwa durch die automatische
Umschaltung bei Sonnenuntergang -, muss das Thema folgen. Bis zum
05.09.2026 las theme.ts die Systemeinstellung genau einmal beim Laden des
Moduls und horchte danach nie wieder; ein Wechsel wirkte erst nach dem
nächsten Neuladen. */
describe("Systemumstellung im laufenden Betrieb", () => {
function medienabfrageStellen(dunkel: boolean) {
const horcher: (() => void)[] = []
const stand = { dunkel }
// matches als Getter, nicht als kopierter Wert: theme.ts haelt die
// Abfrage vom Laden fest und liest matches erst im Horcher wieder.
vi.stubGlobal("matchMedia", (text: string) => ({
get matches() {
return text.includes("dark") ? stand.dunkel : !stand.dunkel
},
addEventListener: (_art: string, bei: () => void) => horcher.push(bei),
removeEventListener: () => {},
}))
return {
umstellenAuf(neuDunkel: boolean) {
stand.dunkel = neuDunkel
for (const bei of horcher) bei()
},
}
}
beforeEach(() => {
localStorage.clear()
vi.resetModules()
})
it("folgt dem Wechsel nach hell, ohne neu zu laden", async () => {
const system = medienabfrageStellen(true)
const { themeStartwert } = await import("./theme")
expect(themeStartwert()).toBe("nacht")
system.umstellenAuf(false)
expect(themeStartwert()).toBe("tag")
})
it("folgt dem Wechsel nach dunkel, ohne neu zu laden", async () => {
const system = medienabfrageStellen(false)
const { themeStartwert } = await import("./theme")
expect(themeStartwert()).toBe("tag")
system.umstellenAuf(true)
expect(themeStartwert()).toBe("nacht")
})
it("lässt eine selbst getroffene Wahl unangetastet", async () => {
localStorage.setItem("dm360.theme", "tag")
const system = medienabfrageStellen(false)
const { themeStartwert } = await import("./theme")
expect(themeStartwert()).toBe("tag")
system.umstellenAuf(true)
expect(themeStartwert()).toBe("tag")
})
})
+35
View File
@@ -68,6 +68,16 @@ function einstellung<T>(
return {
stand: () => wert,
/** Ob überhaupt etwas in der Ablage steht - also eine bewusste Wahl. */
gewaehlt: () => ablageLesen(schluessel) !== null,
/* Setzt den gehaltenen Wert, OHNE ihn zu speichern. Für Werte, die dem
System folgen, solange niemand selbst gewählt hat: eine Systemumstellung
ist keine Entscheidung des Nutzers und darf deshalb keine ablegen. */
ausSystem: (neu: T) => {
if (neu === wert) return
wert = neu
for (const bei of horcher) bei()
},
horchen: (bei: () => void) => {
horcher.add(bei)
return () => horcher.delete(bei)
@@ -95,6 +105,31 @@ const THEMA = einstellung<Theme>(
(wert) => wert,
)
/* Systemumstellung im laufenden Betrieb nachziehen.
Der Startwert oben liest `prefers-color-scheme` genau einmal, beim Laden des
Moduls. Stellt das Gerät danach um - etwa durch die automatische Umschaltung
bei Sonnenuntergang -, folgte das Stylesheet sofort (es hängt selbst an der
Medienabfrage), der React-Zustand aber nie. Alles, was sein Bild aus
`useTheme()` wählt statt aus CSS, blieb damit auf der alten Fassung stehen:
die Vier Ringe und das Typenschild sind `<img>` mit eingebettetem SVG und
je einer hellen und einer dunklen Fassung (siehe VierRinge in
Typenschild.tsx). Auf dunklem Grund stand dann die dunkle Fassung - schwarz
auf schwarz, vom Eigentümer am 05.09.2026 gemeldet. Ein Neuladen räumte es
auf, weshalb es sprunghaft wirkte.
Nur solange niemand selbst gewählt hat: eine gespeicherte Wahl in
"Einstellungen" bleibt unangetastet. */
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
const dunkel = window.matchMedia("(prefers-color-scheme: dark)")
const nachziehen = () => {
if (THEMA.gewaehlt()) return
THEMA.ausSystem(dunkel.matches ? "nacht" : "tag")
}
if (typeof dunkel.addEventListener === "function") dunkel.addEventListener("change", nachziehen)
else if (typeof dunkel.addListener === "function") dunkel.addListener(nachziehen)
}
const TABBESCHRIFTUNG = einstellung<boolean>(
"dm360.tabbeschriftung",