chore: Projektgeruest mit Next.js, TypeScript und Tailwind
Grundgeruest der Anwendung: Next.js 15 (App Router), TypeScript im strict-Modus, Tailwind mit shadcn/ui-Farbtokens. Bewusste Festlegungen: - Nur Systemschriften, keine externen Fonts, kein CDN. - Grundschrift eine Stufe groesser, Fokusring immer sichtbar, Touchziele mindestens 44px. - Ampelfarben fuer Fristen als eigene Design-Tokens. - Betraege werden als Cent-Ganzzahl gefuehrt, damit die Rechnungslegung keine Rundungsfehler erbt. - .env.example dokumentiert alle Geheimnisse; npm run schluessel erzeugt sie. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
backups
|
||||
uploads
|
||||
postgres-data
|
||||
caddy-data
|
||||
caddy-config
|
||||
coverage
|
||||
test-results
|
||||
playwright-report
|
||||
README.md
|
||||
@@ -0,0 +1,63 @@
|
||||
# =============================================================================
|
||||
# Betreuungs-Cockpit - Beispielkonfiguration
|
||||
# Kopiere diese Datei nach .env und fuelle die Werte aus.
|
||||
# Alle Geheimnisse erzeugst du mit: npm run schluessel
|
||||
# =============================================================================
|
||||
|
||||
# --- Datenbank --------------------------------------------------------------
|
||||
# Lokal (Docker Compose): Host ist der Servicename "db".
|
||||
POSTGRES_USER=cockpit
|
||||
POSTGRES_PASSWORD=bitte-aendern
|
||||
POSTGRES_DB=betreuungs_cockpit
|
||||
DATABASE_URL="postgresql://cockpit:bitte-aendern@localhost:5432/betreuungs_cockpit?schema=public"
|
||||
|
||||
# --- Auth.js ----------------------------------------------------------------
|
||||
# 32 zufaellige Bytes, base64. Erzeugen: npm run schluessel
|
||||
AUTH_SECRET=
|
||||
# Oeffentliche Basis-URL der Anwendung (ohne abschliessenden Slash).
|
||||
AUTH_URL=http://localhost:3000
|
||||
# Hinter einem Reverse Proxy (Caddy) auf true setzen.
|
||||
AUTH_TRUST_HOST=true
|
||||
|
||||
# --- Anwendungsseitige Feldverschluesselung ---------------------------------
|
||||
# AES-256-GCM. 32 Bytes, base64. OHNE DIESEN SCHLUESSEL SIND DIE VERSCHLUESSELTEN
|
||||
# FELDER UNWIEDERBRINGLICH VERLOREN - unbedingt getrennt vom Backup sichern!
|
||||
# Schluesselrotation: neuen Schluessel als ENC_KEY_V2 ergaenzen und
|
||||
# ENC_KEY_AKTIV=2 setzen. Alte Schluessel bleiben zum Entschluesseln stehen.
|
||||
ENC_KEY_V1=
|
||||
ENC_KEY_AKTIV=1
|
||||
|
||||
# Separater Schluessel fuer Blind-Indizes (HMAC-SHA256).
|
||||
# Erlaubt die Suche nach z.B. einer IBAN, ohne sie im Klartext zu speichern.
|
||||
INDEX_KEY=
|
||||
|
||||
# --- Sicherheit -------------------------------------------------------------
|
||||
# Leerlauf bis zur automatischen Abmeldung (Sekunden). 30 Minuten.
|
||||
SESSION_MAX_AGE=1800
|
||||
# Wie lange eine erneute Anmeldung fuer sensible Aktionen gilt (Sekunden).
|
||||
REAUTH_MAX_AGE=300
|
||||
# Login-Sperre: max. Fehlversuche pro Zeitfenster (Sekunden).
|
||||
LOGIN_MAX_VERSUCHE=5
|
||||
LOGIN_FENSTER_SEKUNDEN=900
|
||||
LOGIN_SPERRE_SEKUNDEN=900
|
||||
|
||||
# --- Objektspeicher (S3-kompatibel, EU!) ------------------------------------
|
||||
# Wird ab Etappe 3 verwendet. Beispiel: Hetzner Object Storage, Falkenstein.
|
||||
# ACHTUNG: Mit dem Anbieter muss ein AV-Vertrag nach Art. 28 DSGVO bestehen.
|
||||
S3_ENDPOINT=https://fsn1.your-objectstorage.com
|
||||
S3_REGION=eu-central
|
||||
S3_BUCKET=betreuungs-cockpit
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
|
||||
# --- E-Mail-Erinnerungen (optional, ab Etappe 2) ----------------------------
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM="Betreuungs-Cockpit <cockpit@example.de>"
|
||||
|
||||
# --- Erstanmeldung ----------------------------------------------------------
|
||||
# Wird nur vom Seed benutzt, um den ersten Betreuer-Zugang anzulegen.
|
||||
SEED_ADMIN_EMAIL=fw@wildeflorian.de
|
||||
SEED_ADMIN_PASSWORT=bitte-beim-ersten-login-aendern
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Abhaengigkeiten
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
next-env.d.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# Umgebung - enthaelt Schluessel, gehoert NIE ins Repo
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
!.env.example
|
||||
|
||||
# Daten und Backups
|
||||
backups/
|
||||
uploads/
|
||||
postgres-data/
|
||||
caddy-data/
|
||||
caddy-config/
|
||||
*.dump
|
||||
*.sql.gz
|
||||
|
||||
# Test-Artefakte
|
||||
coverage/
|
||||
playwright-report/
|
||||
test-results/
|
||||
.playwright/
|
||||
|
||||
# Werkzeuge
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
npm-debug.log*
|
||||
@@ -0,0 +1,11 @@
|
||||
# Betreuungs-Cockpit
|
||||
|
||||
Arbeits- und Dokumentationswerkzeug für die ehrenamtliche rechtliche Betreuung
|
||||
eines Angehörigen — Fristen, Vorgänge, Belege, Vermögen, Sozialhilfe.
|
||||
|
||||
> Die Anwendung erteilt **keine Rechtsberatung**. Alle Hinweise mit
|
||||
> Paragraphen-Bezug ersetzen nicht die Rückfrage bei Betreuungsgericht,
|
||||
> Sozialhilfeträger oder Anwalt.
|
||||
|
||||
Die vollständige Installations-, Betriebs- und Backup-Anleitung folgt in
|
||||
diesem Dokument, sobald Etappe 1 steht.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Standalone-Build fuer ein schlankes Docker-Image.
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
// Argon2 ist eine native Bibliothek und darf nicht gebundelt werden.
|
||||
serverExternalPackages: ['@node-rs/argon2'],
|
||||
eslint: {
|
||||
ignoreDuringBuilds: false,
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: [
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
{ key: 'Referrer-Policy', value: 'no-referrer' },
|
||||
{ key: 'X-DNS-Prefetch-Control', value: 'off' },
|
||||
{
|
||||
key: 'Permissions-Policy',
|
||||
// Kamera bleibt erlaubt: Belege werden am Handy fotografiert.
|
||||
value: 'camera=(self), microphone=(self), geolocation=(), interest-cohort=()',
|
||||
},
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=63072000; includeSubDomains; preload',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "betreuungs-cockpit",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:deploy": "prisma migrate deploy",
|
||||
"db:volltext": "prisma db execute --schema prisma/schema.prisma --file prisma/sql/volltextsuche.sql",
|
||||
"db:seed": "prisma db seed",
|
||||
"db:studio": "prisma studio",
|
||||
"setup": "prisma migrate deploy && npm run db:volltext && npm run db:seed",
|
||||
"schluessel": "node scripts/schluessel-erzeugen.mjs",
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "2.0.2",
|
||||
"@prisma/client": "6.3.1",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-label": "2.1.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"otpauth": "9.3.6",
|
||||
"qrcode": "1.5.4",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.13.1",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "9.19.0",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"postcss": "8.5.1",
|
||||
"prisma": "6.3.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Erzeugt die Geheimnisse fuer die .env-Datei.
|
||||
*
|
||||
* Aufruf: npm run schluessel
|
||||
*
|
||||
* Die Werte werden bewusst nur ausgegeben und nirgends gespeichert.
|
||||
* Kopiere sie in die .env und sichere ENC_KEY_V1 zusaetzlich getrennt
|
||||
* vom Datenbank-Backup (z.B. Passwortmanager, Papier im Ordner).
|
||||
*/
|
||||
import { randomBytes } from 'node:crypto'
|
||||
|
||||
const b64 = (bytes) => randomBytes(bytes).toString('base64')
|
||||
|
||||
console.log(`
|
||||
# --- Erzeugt am ${new Date().toISOString()} ---
|
||||
AUTH_SECRET=${b64(32)}
|
||||
ENC_KEY_V1=${b64(32)}
|
||||
ENC_KEY_AKTIV=1
|
||||
INDEX_KEY=${b64(32)}
|
||||
POSTGRES_PASSWORD=${b64(24).replace(/[+/=]/g, 'x')}
|
||||
|
||||
# WICHTIG: ENC_KEY_V1 geht NICHT verloren gehen duerfen.
|
||||
# Ohne ihn sind Diagnosen, IBANs, Versicherungsnummern und Notizen
|
||||
# aus dem Backup nicht mehr lesbar.
|
||||
`)
|
||||
@@ -0,0 +1,139 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Helles Thema - bewusst hoher Kontrast, kein Grau-auf-Grau. */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222 47% 11%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222 47% 11%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222 47% 11%;
|
||||
|
||||
--primary: 214 80% 32%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 214 32% 94%;
|
||||
--secondary-foreground: 222 47% 11%;
|
||||
|
||||
--muted: 214 32% 95%;
|
||||
--muted-foreground: 215 20% 35%;
|
||||
|
||||
--accent: 214 32% 94%;
|
||||
--accent-foreground: 222 47% 11%;
|
||||
|
||||
--destructive: 0 72% 40%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 214 20% 84%;
|
||||
--input: 214 20% 80%;
|
||||
--ring: 214 80% 40%;
|
||||
|
||||
--ampel-rot: 0 72% 42%;
|
||||
--ampel-orange: 25 90% 38%;
|
||||
--ampel-gelb: 43 90% 30%;
|
||||
--ampel-gruen: 145 63% 27%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222 47% 9%;
|
||||
--foreground: 210 40% 96%;
|
||||
|
||||
--card: 222 45% 12%;
|
||||
--card-foreground: 210 40% 96%;
|
||||
|
||||
--popover: 222 45% 12%;
|
||||
--popover-foreground: 210 40% 96%;
|
||||
|
||||
--primary: 210 90% 66%;
|
||||
--primary-foreground: 222 47% 11%;
|
||||
|
||||
--secondary: 217 33% 18%;
|
||||
--secondary-foreground: 210 40% 96%;
|
||||
|
||||
--muted: 217 33% 17%;
|
||||
--muted-foreground: 215 25% 72%;
|
||||
|
||||
--accent: 217 33% 20%;
|
||||
--accent-foreground: 210 40% 96%;
|
||||
|
||||
--destructive: 0 72% 62%;
|
||||
--destructive-foreground: 222 47% 11%;
|
||||
|
||||
--border: 217 25% 26%;
|
||||
--input: 217 25% 30%;
|
||||
--ring: 210 90% 66%;
|
||||
|
||||
--ampel-rot: 0 84% 68%;
|
||||
--ampel-orange: 25 95% 62%;
|
||||
--ampel-gelb: 45 95% 60%;
|
||||
--ampel-gruen: 145 60% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
/* Verhindert das automatische Hochskalieren von Text in iOS-Safari. */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
}
|
||||
|
||||
/* Tastaturbedienung muss sichtbar sein - niemals outline:none. */
|
||||
:focus-visible {
|
||||
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
|
||||
}
|
||||
|
||||
/* Auf Touch-Geraeten sollen Bedienelemente mindestens 44px hoch sein. */
|
||||
@media (pointer: coarse) {
|
||||
button,
|
||||
[role='button'],
|
||||
a.btn,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Sprungmarke fuer Screenreader und Tastaturnutzer. */
|
||||
.skip-link {
|
||||
@apply absolute left-2 top-2 z-50 -translate-y-20 rounded-md bg-primary px-4 py-2
|
||||
text-primary-foreground transition-transform focus:translate-y-0;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
/* Berichte werden ausgedruckt und dem Gericht vorgelegt. */
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/** Tailwind-Klassen zusammenfuehren, spaetere gewinnen. */
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
const DATUM_FORMAT = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
const DATUM_ZEIT_FORMAT = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
const EURO_FORMAT = new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
})
|
||||
|
||||
/** 22.08.2026 - oder ein Gedankenstrich, wenn nichts da ist. */
|
||||
export function formatDatum(datum: Date | string | null | undefined): string {
|
||||
if (!datum) return '–'
|
||||
const d = typeof datum === 'string' ? new Date(datum) : datum
|
||||
if (Number.isNaN(d.getTime())) return '–'
|
||||
return DATUM_FORMAT.format(d)
|
||||
}
|
||||
|
||||
/** 22.08.2026, 14:30 */
|
||||
export function formatDatumZeit(datum: Date | string | null | undefined): string {
|
||||
if (!datum) return '–'
|
||||
const d = typeof datum === 'string' ? new Date(datum) : datum
|
||||
if (Number.isNaN(d.getTime())) return '–'
|
||||
return DATUM_ZEIT_FORMAT.format(d)
|
||||
}
|
||||
|
||||
/**
|
||||
* Betraege liegen in der Datenbank als Cent-Ganzzahl (BigInt), damit
|
||||
* keine Rundungsfehler in die Rechnungslegung wandern.
|
||||
*/
|
||||
export function formatCent(cent: bigint | number | null | undefined): string {
|
||||
if (cent === null || cent === undefined) return '–'
|
||||
const zahl = typeof cent === 'bigint' ? Number(cent) : cent
|
||||
return EURO_FORMAT.format(zahl / 100)
|
||||
}
|
||||
|
||||
/** Tage zwischen heute (Mitternacht) und einem Datum. Negativ = ueberfaellig. */
|
||||
export function tageBis(datum: Date | string): number {
|
||||
const ziel = typeof datum === 'string' ? new Date(datum) : datum
|
||||
const heute = new Date()
|
||||
heute.setHours(0, 0, 0, 0)
|
||||
const zielTag = new Date(ziel)
|
||||
zielTag.setHours(0, 0, 0, 0)
|
||||
return Math.round((zielTag.getTime() - heute.getTime()) / 86_400_000)
|
||||
}
|
||||
|
||||
/** "vor 3 Tagen", "in 2 Wochen" - fuer die Dashboard-Kacheln. */
|
||||
export function relativeTage(tage: number): string {
|
||||
if (tage === 0) return 'heute'
|
||||
if (tage === 1) return 'morgen'
|
||||
if (tage === -1) return 'gestern'
|
||||
if (tage < 0) return `vor ${Math.abs(tage)} Tagen`
|
||||
return `in ${tage} Tagen`
|
||||
}
|
||||
|
||||
/** Aus "Erika Mustermann" wird "EM" - fuer Avatare in der Navigation. */
|
||||
export function initialen(name: string): string {
|
||||
return name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((teil) => teil[0]?.toUpperCase() ?? '')
|
||||
.join('')
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
import animate from 'tailwindcss-animate'
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '1rem',
|
||||
screens: { '2xl': '1400px' },
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
// Bewusst nur Systemschriften - keine externen Fonts, kein CDN.
|
||||
sans: [
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'Segoe UI',
|
||||
'Roboto',
|
||||
'Helvetica Neue',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
],
|
||||
},
|
||||
fontSize: {
|
||||
// Grundschrift eine Stufe groesser als der Tailwind-Standard.
|
||||
base: ['1.0625rem', { lineHeight: '1.6' }],
|
||||
lg: ['1.1875rem', { lineHeight: '1.6' }],
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
// Ampelfarben fuer Fristen - als Token, damit sie ueberall gleich sind.
|
||||
ampel: {
|
||||
rot: 'hsl(var(--ampel-rot))',
|
||||
orange: 'hsl(var(--ampel-orange))',
|
||||
gelb: 'hsl(var(--ampel-gelb))',
|
||||
gruen: 'hsl(var(--ampel-gruen))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [animate],
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user