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 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 02:24:08 +02:00
parent 863284e540
commit e9c16325cc
16 changed files with 2674 additions and 2467 deletions
-2
View File
@@ -1,7 +1,5 @@
node_modules/
dist/
# Bau der Musterseite (vite.muster.config.ts) - reines Ergebnis, nie in der .ipa.
dist-muster/
*.local
.DS_Store
-12
View File
@@ -1,12 +0,0 @@
<!doctype html>
<html lang="de" data-theme="nacht">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>DataMetric360 Musterseite</title>
</head>
<body>
<div id="wurzel" class="ads-root"></div>
<script type="module" src="./muster.tsx"></script>
</body>
</html>
-79
View File
@@ -1,79 +0,0 @@
/**
* Musterseite: die Oberflaeche der App mit Beispieldaten, ohne Server.
*
* WOFUER
* ------
* Die App braucht sonst einen gespeicherten Zugang, um ueberhaupt etwas
* anzuzeigen - und damit war jede OPTISCHE Pruefung an ein Zugangstoken
* gebunden. Genau daran ist in diesem Projekt wiederholt die Verifikation
* haengengeblieben, obwohl fast jeder gefundene Fehler (CSS-Spezifitaet,
* Flex-Basis in Spalten, unsichtbare Rahmen) nur am gerenderten Bild
* sichtbar war und von tsc und den Tests nicht gefunden werden konnte.
*
* Diese Seite mountet dieselben Bildschirme mit demselben Stylesheet, aber
* mit `beispielApi()` aus den Tests. Kein Netz, kein Token, keine echten
* Daten - und trotzdem echtes CSS.
*
* NICHT TEIL DER APP
* ------------------
* Gebaut wird sie ueber `vite.muster.config.ts` nach `dist-muster/`. Der
* normale `npm run build` fasst sie nicht an, sie liegt also auch nicht in
* der .ipa.
*
* Aufruf: `?seite=einst` (Vorgabe), jeder Name aus navigation.ts TITEL.
* Dazu `?seite=einrichtung` fuer den Willkommensbildschirm - der ist keine
* Route (er steht VOR der Anmeldung) und braucht deshalb einen eigenen Zweig.
*/
import { StrictMode, useState } from "react"
import { createRoot } from "react-dom/client"
import "@audi-dash/ui/styles.css"
import "../src/stile/grundlage.css"
import "../src/stile/audi-schrift.css"
import "../src/stile/rahmen.css"
import "../src/stile/screens.css"
import { DatenAnbieter } from "../src/daten/DatenKontext"
import { Shell } from "../src/Shell"
import type { SeitenName } from "../src/navigation"
import { TITEL } from "../src/navigation"
import { Bestaetigungsblatt } from "../src/screens/bestaetigung"
import { Einrichtung } from "../src/screens/Einrichtung"
import { SeiteFuer } from "../src/screens/register"
import { beispielApi } from "../src/tests/beispieldaten"
function Muster() {
const gewaehlt = new URLSearchParams(location.search).get("seite") || "einst"
const start = gewaehlt as SeitenName
const [route, setzeRoute] = useState<{ name: SeitenName; id?: string }>({ name: start })
const geheZu = (name: SeitenName, id?: string) =>
setzeRoute(id === undefined ? { name } : { name, id })
// Der Willkommensbildschirm haengt an keinem Zugang und an keiner Route.
if (gewaehlt === "einrichtung") return <Einrichtung fertig={() => {}} />
return (
<DatenAnbieter api={beispielApi()}>
<Bestaetigungsblatt />
<Shell route={route} geheZu={geheZu} tabBeschriftung>
<SeiteFuer
route={route}
geheZu={geheZu}
tabBeschriftung
setzeTabBeschriftung={() => {}}
beiAbmeldung={() => {}}
/>
</Shell>
</DatenAnbieter>
)
}
// Zum schnellen Springen zwischen den Seiten, ohne die Adresse zu tippen.
;(window as unknown as { SEITEN: string[] }).SEITEN = Object.keys(TITEL)
const wurzel = document.getElementById("wurzel")
if (!wurzel) throw new Error("Wurzelelement #wurzel fehlt")
createRoot(wurzel).render(
<StrictMode>
<Muster />
</StrictMode>,
)
+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",
-17
View File
@@ -1,17 +0,0 @@
/**
* Bau der Musterseite (siehe muster/muster.tsx).
*
* Bewusst eine EIGENE Konfiguration mit eigenem Ausgabeordner: die Seite darf
* nicht in `dist/` landen, weil `dist/` die Huelle der iOS-App ist. Was dort
* liegt, wird mit ausgeliefert.
*/
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
root: "muster",
base: "./",
plugins: [react()],
define: { __APP_VERSION__: JSON.stringify("muster") },
build: { outDir: "../dist-muster", emptyOutDir: true },
})