Files
Sascha WolfandClaude Opus 5 87759a8a24 feat(ui): drei Farbwelten je hell und dunkel, gewaehlt pro Benutzer
Neben dem Design des Hauses ("perinco") gibt es "Pixel Art" und
"Outdoor", jedes in hell und dunkel. Die Wahl steht neben dem
Hell/Dunkel-Schalter im Kopf der App.

Sie gehoert dem BENUTZER und nicht dem Browser: crm_users.theme und
crm_users.theme_mode, gesetzt ueber PATCH /api/auth/me, das jeder fuer
sich selbst darf. Wer sich an einem anderen Rechner anmeldet, findet sein
Design wieder, und zwei Benutzer an derselben Maschine kommen sich nicht
in die Quere. Der localStorage bleibt als Zwischenspeicher daneben, damit
das Inline-Skript in index.html die Klassen vor dem ersten Frame setzen
kann - ohne das blitzt beim Laden das Standarddesign auf. Das hat eine
sichtbare Folge: melden sich zwei Leute an demselben Rechner an, sieht
der zweite fuer einen Augenblick das Design des ersten.

Der ThemeProvider steht deshalb in main.tsx jetzt unter dem AuthProvider
- er liest den Benutzer.

Keine Komponente wurde umgefaerbt. Im Markup steht nirgends eine feste
Farbe, sondern durchgaengig ink-*, brand-*, canvas und surface; Tailwind
v4 loest auch --radius-*, --shadow-* und die Schriften ueber Variablen
auf. Ein Theme tauscht also nur Werte, und deshalb kann Pixel Art alle
380 abgerundeten Ecken eckig machen, ohne eine Klasse anzufassen.

Zwei Dinge waren Voraussetzung:

- Der helle Block eines Themes braucht :not(.dark). html.theme-x hat
  eine Klasse mehr in der Spezifitaet als .dark und wuerde den
  Dunkelmodus sonst ueberstimmen.
- Auf brand-500/600 steht 69 Mal weisse Schrift. Jedes Theme braucht
  dort einen ausreichend dunklen Ton - ein helles Pastell waere
  unlesbar.

Die Pixelschrift (Silkscreen) sitzt nur in --font-display, also in den
Ueberschriften und den Feldbeschriftungen; labelClass tragt dafuer die
neue Klasse "field-label". Der Fliesstext bleibt Nunito Sans: die Listen
haben bis zu acht Spalten bei 11-13 px, und eine Pixelschrift waere darin
nicht mehr zu lesen. Outdoor bringt Cabin fuer beides mit.

server/test/userFields.test.mjs prueft die Werte und dass die Liste der
Farbwelten in userFields.js und src/types.ts nicht auseinanderlaeuft -
sonst zeigt die Auswahl ein Design an, dessen Speichern das Backend
abweist, und niemand weiss warum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 13:41:17 +02:00

68 lines
2.7 KiB
JavaScript

/*
* Pruefungen der Benutzerfelder: laeuft mit `npm test` im Verzeichnis server/.
*
* Der eigentliche Zweck ist der letzte Block: die Liste der Farbwelten steht
* zweimal - hier in userFields.js (das Backend weist Unbekanntes ab) und in
* src/types.ts (das Frontend baut daraus die Auswahl). Laufen die beiden
* auseinander, faellt es sonst niemandem auf: das Auswahlfeld zeigt ein Design
* an, das Speichern schlaegt fehl, und die Oberflaeche bleibt beim alten
* Aussehen.
*/
import { readFileSync } from 'node:fs';
const { normalizeTheme, normalizeThemeMode, normalizeColor, THEMES, THEME_MODES } = await import(
'../src/userFields.js'
);
let failures = 0;
function check(name, condition, detail = '') {
if (condition) {
console.log(` ok ${name}${detail ? ` - ${detail}` : ''}`);
} else {
console.log(` FAIL ${name}${detail ? ` - ${detail}` : ''}`);
failures += 1;
}
}
console.log('\nFarbwelt');
check('Bekanntes Design', normalizeTheme('pixel').value === 'pixel');
check('Gross geschrieben', normalizeTheme('Outdoor').value === 'outdoor');
check('Mit Leerzeichen', normalizeTheme(' perinco ').value === 'perinco');
check('Leer wird null', normalizeTheme('').value === null);
check('null bleibt null', normalizeTheme(null).value === null);
check('Unbekanntes wird abgewiesen', Boolean(normalizeTheme('barbie').error));
check(
'Die Fehlermeldung nennt die erlaubten Werte',
(normalizeTheme('barbie').error ?? '').includes('outdoor')
);
console.log('\nModus');
check('hell', normalizeThemeMode('light').value === 'light');
check('dunkel', normalizeThemeMode('dark').value === 'dark');
check('Leer heisst "folgt dem System"', normalizeThemeMode('').value === null);
check('Unbekanntes wird abgewiesen', Boolean(normalizeThemeMode('auto').error));
console.log('\nFarbe (unveraendert, als Regressionsschutz)');
check('Hex wird angenommen', normalizeColor('#6BA04A').value === '#6ba04a');
check('Kurzform wird abgewiesen', Boolean(normalizeColor('#fff').error));
console.log('\nBackend und Frontend kennen dieselben Designs');
const types = readFileSync(new URL('../../src/types.ts', import.meta.url), 'utf8');
const match = /export const THEME_PALETTES = \[([^\]]*)\]/.exec(types);
check('THEME_PALETTES in src/types.ts gefunden', Boolean(match));
if (match) {
const frontend = [...match[1].matchAll(/'([^']+)'/g)].map((entry) => entry[1]);
check(
'Beide Listen sind gleich',
JSON.stringify(frontend) === JSON.stringify(THEMES),
`Frontend ${JSON.stringify(frontend)} / Backend ${JSON.stringify(THEMES)}`
);
}
check('Es gibt genau zwei Modi', THEME_MODES.length === 2);
console.log(
failures === 0 ? '\nAlle Pruefungen bestanden.' : `\n${failures} Pruefung(en) fehlgeschlagen.`
);
process.exit(failures === 0 ? 0 : 1);