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:
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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}>
|
||||
|
||||
+2256
-2244
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
Reference in New Issue
Block a user