Files
sshkeyvault/src/lib/format.ts
T
florian.wildeandClaude Opus 5.5 88b5697cc9 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>
2026-10-12 09:20:22 +02:00

90 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Datums- und Zeitformate der Oberfläche (#002): TT.MM.JJJJ, 24-h-Zeit, deutsch.
*
* Die Zeitzone ist fest Europe/Berlin und nicht die des Prozesses: Server (UTC im
* Container) und Browser sollen dieselbe Zeit anzeigen, sonst springen Zeitstempel
* zwischen Server- und Client-Rendering um Stunden.
*/
export const ZEITZONE = "Europe/Berlin";
type Eingabe = Date | string | number;
const DATUM = new Intl.DateTimeFormat("de-DE", {
timeZone: ZEITZONE,
day: "2-digit",
month: "2-digit",
year: "numeric",
});
const UHRZEIT = new Intl.DateTimeFormat("de-DE", {
timeZone: ZEITZONE,
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
});
function alsDatum(wert: Eingabe): Date {
const d = wert instanceof Date ? wert : new Date(wert);
if (Number.isNaN(d.getTime())) throw new RangeError(`Ungültiges Datum: ${String(wert)}`);
return d;
}
/** „12.10.2026“ */
export function datum(wert: Eingabe): string {
return DATUM.format(alsDatum(wert));
}
/** „08:42“ */
export function uhrzeit(wert: Eingabe): string {
return UHRZEIT.format(alsDatum(wert));
}
/** „12.10.2026, 08:42“ */
export function datumZeit(wert: Eingabe): string {
const d = alsDatum(wert);
return `${DATUM.format(d)}, ${UHRZEIT.format(d)}`;
}
const MINUTE = 60_000;
const STUNDE = 60 * MINUTE;
const TAG = 24 * STUNDE;
/**
* Grobe Relativangabe für Listen: „gerade eben“, „vor 5 Min.“, „vor 3 Std.“,
* „vor 2 Tagen“, „in 12 Tagen“. Ab 30 Tagen Abstand wird das Datum gezeigt, weil
* „vor 47 Tagen“ schlechter einzuordnen ist als ein Datum.
*
* `jetzt` ist ein Parameter, damit Tests und statisch gerenderte Seiten einen
* festen Bezugspunkt haben.
*/
export function relativ(wert: Eingabe, jetzt: Eingabe = new Date()): string {
const abstand = alsDatum(wert).getTime() - alsDatum(jetzt).getTime();
const betrag = Math.abs(abstand);
const zukunft = abstand > 0;
if (betrag < MINUTE) return "gerade eben";
if (betrag >= 30 * TAG) return `am ${datum(wert)}`;
let text: string;
if (betrag < STUNDE) {
text = `${Math.floor(betrag / MINUTE)} Min.`;
} else if (betrag < TAG) {
text = `${Math.floor(betrag / STUNDE)} Std.`;
} else {
const tage = Math.floor(betrag / TAG);
// „vor“ und „in“ verlangen beide den Dativ: „vor 1 Tag“, „in 2 Tagen“.
text = tage === 1 ? "1 Tag" : `${tage} Tagen`;
}
return zukunft ? `in ${text}` : `vor ${text}`;
}
/** Ganze Kalendertage bis `wert` (negativ, wenn vorbei) – Grundlage für Ablaufhinweise. */
export function tageBis(wert: Eingabe, jetzt: Eingabe = new Date()): number {
const tagesnummer = (d: Date) => {
const [t, m, j] = DATUM.format(d).split(".").map(Number);
return Date.UTC(j ?? 0, (m ?? 1) - 1, t ?? 1) / TAG;
};
return Math.round(tagesnummer(alsDatum(wert)) - tagesnummer(alsDatum(jetzt)));
}