feat(oberflaeche): Grundlayout, Navigation und Farbschema

Deutsche Oberfläche, Hell und Dunkel über next-themes, Navigation mit
Rollenfilter und die shadcn-Primitiven, die M1 braucht.

Der Farbschema-Knopf kommt ohne Effekt und ohne Zustand aus: welches
Symbol gilt, entscheidet die dark-Klasse und damit CSS. next-themes setzt
sie schon vor der Hydratation, deshalb blitzt nichts auf — und die
React-Compiler-Regel gegen setState im Effekt bleibt eingehalten.

Farben als OKLCH, damit Hell und Dunkel dieselbe wahrgenommene Helligkeit
halten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 13:02:34 +02:00
co-authored by Claude Opus 5
parent 19ca0826e6
commit 4ff67c15c2
13 changed files with 505 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import { ROLLEN_NAME, istAdmin, rolleReichtAus } from './rollen'
describe('rolleReichtAus', () => {
it('laesst jede Rolle das, was sie selbst ist', () => {
expect(rolleReichtAus('BENUTZER', 'BENUTZER')).toBe(true)
expect(rolleReichtAus('ADMIN', 'ADMIN')).toBe(true)
})
it('laesst die Verwaltung auch alles Niedrigere', () => {
expect(rolleReichtAus('ADMIN', 'BENUTZER')).toBe(true)
})
it('laesst einen Benutzer nicht in die Verwaltung', () => {
expect(rolleReichtAus('BENUTZER', 'ADMIN')).toBe(false)
})
})
describe('istAdmin', () => {
it('erkennt nur die Verwaltung', () => {
expect(istAdmin('ADMIN')).toBe(true)
expect(istAdmin('BENUTZER')).toBe(false)
})
})
describe('ROLLEN_NAME', () => {
it('benennt jede Rolle deutsch', () => {
expect(ROLLEN_NAME.BENUTZER).toBe('Benutzer')
expect(ROLLEN_NAME.ADMIN).toBe('Verwaltung')
})
})
+25
View File
@@ -0,0 +1,25 @@
import type { Rolle } from '@/generated/prisma/enums'
/**
* Rangfolge der Rollen. Reine Logik, damit Seite, Server Action und
* Oberflaeche dieselbe Antwort geben.
*/
const RANG: Record<Rolle, number> = {
BENUTZER: 1,
ADMIN: 2,
}
export const ROLLEN_NAME: Record<Rolle, string> = {
BENUTZER: 'Benutzer',
ADMIN: 'Verwaltung',
}
/** Reicht die vorhandene Rolle fuer das Verlangte? */
export function rolleReichtAus(vorhanden: Rolle, verlangt: Rolle): boolean {
return RANG[vorhanden] >= RANG[verlangt]
}
export function istAdmin(rolle: Rolle): boolean {
return rolleReichtAus(rolle, 'ADMIN')
}