feat(ui): Design-Tokens, Schriften und Datumsformate (#002)

Die Farben des Tresor-Designs stehen als CSS-Variablen in :root und
werden per @theme inline an Tailwind gereicht, damit Inline-SVGs und
Utility-Klassen dieselbe Quelle nutzen. Schriften kommen selbst gehostet
aus @fontsource, nur in den benutzten Gewichten und im Latin-Ausschnitt.

src/lib/format.ts rechnet fest in Europe/Berlin: der Server laeuft im
Container auf UTC, die Anzeige soll aber ueberall dieselbe Ortszeit
zeigen. relativ() nimmt den Bezugspunkt als Parameter, damit statische
Seiten und Tests deterministisch bleiben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:20:22 +02:00
co-authored by Claude Opus 5.5
parent a11ae7eef3
commit 88b5697cc9
4 changed files with 301 additions and 1 deletions
+91
View File
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { datum, datumZeit, relativ, tageBis, uhrzeit, ZEITZONE } from "@/lib/format";
// Alle Erwartungen gelten für Europe/Berlin, unabhängig von der Zeitzone des Testprozesses.
describe("Formate (#002)", () => {
it("nutzt fest Europe/Berlin", () => {
expect(ZEITZONE).toBe("Europe/Berlin");
});
describe("datum()", () => {
it("schreibt TT.MM.JJJJ mit führenden Nullen", () => {
expect(datum("2026-03-05T10:00:00Z")).toBe("05.03.2026");
});
it("rechnet in Berliner Zeit – 23:30 UTC ist dort schon der nächste Tag", () => {
expect(datum("2026-10-12T23:30:00Z")).toBe("13.10.2026");
});
it("nimmt Date, Zeitstempel und ISO-Text", () => {
const d = new Date("2026-01-01T12:00:00Z");
expect(datum(d)).toBe("01.01.2026");
expect(datum(d.getTime())).toBe("01.01.2026");
});
it("lehnt ungültige Eingaben ab", () => {
expect(() => datum("kein Datum")).toThrow(RangeError);
});
});
describe("uhrzeit() und datumZeit()", () => {
it("zeigt 24-h-Zeit in Sommerzeit (UTC+2)", () => {
expect(uhrzeit("2026-07-01T13:05:00Z")).toBe("15:05");
});
it("zeigt 24-h-Zeit in Winterzeit (UTC+1)", () => {
expect(uhrzeit("2026-12-01T22:59:00Z")).toBe("23:59");
});
it("Mitternacht ist 00:00, nicht 24:00", () => {
expect(uhrzeit("2026-12-01T23:00:00Z")).toBe("00:00");
});
it("verbindet Datum und Zeit", () => {
expect(datumZeit("2026-10-12T06:42:00Z")).toBe("12.10.2026, 08:42");
});
it("berücksichtigt die Zeitumstellung am 25.10.2026", () => {
expect(datumZeit("2026-10-25T00:30:00Z")).toBe("25.10.2026, 02:30");
expect(datumZeit("2026-10-25T01:30:00Z")).toBe("25.10.2026, 02:30");
});
});
describe("relativ()", () => {
const jetzt = new Date("2026-10-12T10:00:00Z");
const vor = (ms: number) => new Date(jetzt.getTime() - ms);
const MIN = 60_000;
const STD = 60 * MIN;
const TAG = 24 * STD;
it("unter einer Minute: gerade eben", () => {
expect(relativ(vor(30_000), jetzt)).toBe("gerade eben");
expect(relativ(jetzt, jetzt)).toBe("gerade eben");
});
it("Minuten", () => {
expect(relativ(vor(5 * MIN), jetzt)).toBe("vor 5 Min.");
expect(relativ(vor(59 * MIN), jetzt)).toBe("vor 59 Min.");
});
it("Stunden", () => {
expect(relativ(vor(3 * STD), jetzt)).toBe("vor 3 Std.");
expect(relativ(vor(23 * STD + 59 * MIN), jetzt)).toBe("vor 23 Std.");
});
it("Tage mit Einzahl und Mehrzahl", () => {
expect(relativ(vor(TAG), jetzt)).toBe("vor 1 Tag");
expect(relativ(vor(4 * TAG), jetzt)).toBe("vor 4 Tagen");
});
it("Zukunft", () => {
expect(relativ(vor(-12 * TAG), jetzt)).toBe("in 12 Tagen");
expect(relativ(vor(-2 * STD), jetzt)).toBe("in 2 Std.");
});
it("ab 30 Tagen das Datum", () => {
expect(relativ(vor(40 * TAG), jetzt)).toBe("am 02.09.2026");
});
it("nimmt ohne Bezugspunkt die aktuelle Zeit", () => {
expect(relativ(new Date())).toBe("gerade eben");
});
});
describe("tageBis()", () => {
const jetzt = "2026-10-12T21:00:00Z"; // in Berlin schon 23:00 am 12.10.
it("zählt Kalendertage in Berliner Zeit", () => {
expect(tageBis("2026-10-12T22:30:00Z", jetzt)).toBe(1); // 00:30 am 13.10.
expect(tageBis("2026-11-11T12:00:00Z", jetzt)).toBe(30);
});
it("ist negativ für Vergangenes", () => {
expect(tageBis("2026-10-10T12:00:00Z", jetzt)).toBe(-2);
});
});
});