diff --git a/PLAN_NEXT.md b/PLAN_NEXT.md
index dc760ca..c80d058 100644
--- a/PLAN_NEXT.md
+++ b/PLAN_NEXT.md
@@ -103,6 +103,17 @@ auth` + the LFX/stack repair commit:
`npm run build` green, routes + `/api` proxy return 200, served schema
validates under JSON-Schema Draft 7.
+## 0.3 Frontend design pass (Mantine)
+
+Plain-HTML UI replaced with a coherent Mantine v9 design system:
+`@mantine/core` + `@mantine/hooks` + `@rjsf/mantine` (v6, matches core)
++ `@tabler/icons-react`. `src/theme.ts` (kirchenblau primary, system
+fonts only, serif display headings), `AppShell` header/nav, per-page
+`Card`/`Table`/`Alert`/`Badge` via shared `StatusBadge`, dark mode via
+localStorage manager + toggle + inline `ColorSchemeScript` equivalent.
+RJSF swap was import-only; SSR smoke test proved all 10 intake labels +
+textarea/password/checkbox/slider widgets render. `npm run build` green.
+
## 1. Locked decisions (do not re-ask)* Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`,
store `sha256`, `used_at` set before session creation), session = opaque id
in **DB table**, cookie `HttpOnly; Secure; SameSite=Lax; Path=/`.
diff --git a/frontend/index.html b/frontend/index.html
index 01b9d1c..831f366 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,7 +3,26 @@
+
+
Kirche Einfach
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4719b52..a7f0deb 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,8 +8,12 @@
"name": "kirche-einfach-frontend",
"version": "0.1.0",
"dependencies": {
+ "@mantine/core": "^9.6.1",
+ "@mantine/hooks": "^9.6.1",
"@rjsf/core": "^6.10.0",
+ "@rjsf/mantine": "^6.10.0",
"@rjsf/validator-ajv8": "^6.10.0",
+ "@tabler/icons-react": "^3.46.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.18.3"
@@ -746,6 +750,59 @@
"node": ">=18"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
+ "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
+ "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.8.0",
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.27.20",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz",
+ "integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.9",
+ "@floating-ui/utils": "^0.2.12",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz",
+ "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.8.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
+ "license": "MIT"
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -796,6 +853,50 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mantine/core": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-9.6.1.tgz",
+ "integrity": "sha512-UkcPo8Zq0uSdodvyXV62HVmfIfHCcrjFrFrH0TggTy6Wtp8a78znGP8RJOz8zC9XSPpJmKjBQjg0M+nOvIzftA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react": "^0.27.20",
+ "clsx": "^2.1.1",
+ "react-number-format": "^5.4.5",
+ "react-remove-scroll": "^2.7.2",
+ "type-fest": "^5.8.0"
+ },
+ "peerDependencies": {
+ "@mantine/hooks": "9.6.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ }
+ },
+ "node_modules/@mantine/dates": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-9.6.1.tgz",
+ "integrity": "sha512-3NxfqrcwMuYB9wvxIhGhEhhazK3wcA2wG4AuNVFGJDd97ujWWAy+jGHqBF9jOusp/Mk4bn8u+3SuDICtLPDf5w==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "peerDependencies": {
+ "@mantine/core": "9.6.1",
+ "@mantine/hooks": "9.6.1",
+ "dayjs": ">=1.0.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ }
+ },
+ "node_modules/@mantine/hooks": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.6.1.tgz",
+ "integrity": "sha512-pfxM95WozngB0ZALw6h99QV1TQ0qniOkFhzi0+dmSnKpxkVSR0xeNsvPpGDj9Teu+M69fRoE4IkzyMnu5Xld3w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^19.2.0"
+ }
+ },
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@@ -829,6 +930,26 @@
"react": ">=18"
}
},
+ "node_modules/@rjsf/mantine": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/@rjsf/mantine/-/mantine-6.10.0.tgz",
+ "integrity": "sha512-tqD2v7rCEaI5Dme66MiaIMem/zTuKJBn9+B1d5J8cVCOh77FmEyn1neVvB6rPK+jXNVY6jJD8DRW8vAzK6OPRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dayjs": "^1.11.21"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@mantine/core": ">=8",
+ "@mantine/dates": ">=8",
+ "@mantine/hooks": ">=8",
+ "@rjsf/core": "^6.10.0",
+ "@rjsf/utils": "^6.10.0",
+ "react": ">=18"
+ }
+ },
"node_modules/@rjsf/utils": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.10.0.tgz",
@@ -1222,6 +1343,32 @@
"win32"
]
},
+ "node_modules/@tabler/icons": {
+ "version": "3.46.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz",
+ "integrity": "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ }
+ },
+ "node_modules/@tabler/icons-react": {
+ "version": "3.46.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.46.0.tgz",
+ "integrity": "sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tabler/icons": "3.46.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ },
+ "peerDependencies": {
+ "react": ">= 16"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1285,7 +1432,7 @@
"version": "19.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1449,6 +1596,15 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1473,7 +1629,13 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.23",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
+ "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
"license": "MIT"
},
"node_modules/debug": {
@@ -1494,6 +1656,12 @@
}
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.420",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
@@ -1629,6 +1797,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1829,6 +2006,16 @@
"license": "MIT",
"peer": true
},
+ "node_modules/react-number-format": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz",
+ "integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -1839,6 +2026,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz",
@@ -1877,6 +2111,28 @@
"react-dom": ">=18"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -1964,6 +2220,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/tabbable": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
+ "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
+ "license": "MIT"
+ },
+ "node_modules/tagged-tag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
+ "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -1981,6 +2255,27 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-fest": {
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.9.0.tgz",
+ "integrity": "sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==",
+ "license": "(MIT OR CC0-1.0)",
+ "dependencies": {
+ "tagged-tag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
@@ -2026,6 +2321,49 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vite": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 32ac6c4..9189d2b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,8 +9,12 @@
"preview": "vite preview"
},
"dependencies": {
+ "@mantine/core": "^9.6.1",
+ "@mantine/hooks": "^9.6.1",
"@rjsf/core": "^6.10.0",
+ "@rjsf/mantine": "^6.10.0",
"@rjsf/validator-ajv8": "^6.10.0",
+ "@tabler/icons-react": "^3.46.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.18.3"
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7d2c87e..fbbb7bb 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,17 @@
import { Link, Route, Routes, useNavigate } from "react-router-dom";
+import {
+ Anchor,
+ AppShell,
+ Button,
+ Center,
+ Container,
+ Group,
+ Loader,
+ Text,
+ Title,
+} from "@mantine/core";
import { AuthProvider, useAuth } from "./auth";
+import SchemeToggle from "./components/SchemeToggle";
import StartPage from "./pages/StartPage";
import AccessPage from "./pages/AccessPage";
import LoginPage from "./pages/LoginPage";
@@ -7,15 +19,8 @@ import WorkflowsPage from "./pages/WorkflowsPage";
import TasksPage from "./pages/TasksPage";
import AdminPage from "./pages/AdminPage";
-const navStyle: React.CSSProperties = {
- display: "flex",
- gap: 12,
- marginBottom: 16,
- alignItems: "center",
-};
-
-function Shell() {
- const { me, loading, logout } = useAuth();
+function Nav() {
+ const { me, logout } = useAuth();
const navigate = useNavigate();
async function onLogout() {
@@ -24,36 +29,89 @@ function Shell() {
}
return (
-
-
Kirche Einfach
-
KI-gestützte Verwaltungsplattform für Kirche
-
- Start
- {!me && Zugang}
- {me && (
- <>
- Workflows
- Aufgaben
- Verwaltung
- {me.email}
- void onLogout()}>Abmelden
- >
- )}
-
- {loading ? (
-
Wird geladen …
- ) : (
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
+
+
+ Start
+
+ {!me && (
+
+ Zugang
+
)}
-
+ {me && (
+ <>
+
+ Workflows
+
+
+ Aufgaben
+
+
+ Verwaltung
+
+ >
+ )}
+
+ {me && (
+ <>
+
+ {me.email}
+
+ void onLogout()}>
+ Abmelden
+
+ >
+ )}
+
+ );
+}
+
+function Shell() {
+ const { loading } = useAuth();
+
+ return (
+
+
+
+
+ Kirche Einfach
+
+
+
+
+
+
+
+
+
+ KI-gestützte Verwaltungsplattform für Kirche
+
+ {loading ? (
+
+
+
+ ) : (
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ )}
+
+
+
);
}
diff --git a/frontend/src/components/AccessQueue.tsx b/frontend/src/components/AccessQueue.tsx
index 21c57c1..71daf2e 100644
--- a/frontend/src/components/AccessQueue.tsx
+++ b/frontend/src/components/AccessQueue.tsx
@@ -1,5 +1,19 @@
import { useCallback, useEffect, useState } from "react";
+import {
+ Alert,
+ Button,
+ Card,
+ Group,
+ Stack,
+ Text,
+ Title,
+} from "@mantine/core";
+import {
+ IconAlertCircle,
+ IconCheck,
+} from "@tabler/icons-react";
import { ApiError, api, type AccessRequest } from "../api";
+import StatusBadge from "./StatusBadge";
export default function AccessQueue() {
const [requests, setRequests] = useState([]);
@@ -43,46 +57,72 @@ export default function AccessQueue() {
return (
-
Zugangsanfragen
- {error &&
{error}
}
- {info &&
{info}
}
- {pending.length === 0 &&
Keine offenen Anfragen.
}
-
+
+ Zugangsanfragen
+
+ {error && (
+ } mb="md">
+ {error}
+
+ )}
+ {info && (
+ } mb="md">
+ {info}
+
+ )}
+ {pending.length === 0 && (
+
+ Keine offenen Anfragen.
+
+ )}
+
{pending.map((r) => (
-
- {r.name} · {r.email}
+
+
+
+ {r.name}
+
+ {r.email}
+
+
{r.reason && (
- <>
-
+
{r.reason}
- >
+
)}
-
- void decide(r.id, true)}
- disabled={busyId === r.id}
- >
- Genehmigen
- {" "}
- void decide(r.id, false)}
- disabled={busyId === r.id}
- >
- Ablehnen
-
-
+
+ void decide(r.id, true)}
+ >
+ Genehmigen
+
+ void decide(r.id, false)}
+ >
+ Ablehnen
+
+
+
))}
-
+
{done.length > 0 && (
<>
-
Entschieden ({done.length})
-
- {done.map((r) => (
-
- {r.status} · {r.name} · {r.email}
-
- ))}
-
+
+ Entschieden ({done.length})
+
+ {done.map((r) => (
+
+
+
+ {r.name} · {r.email}
+
+
+ ))}
>
)}
diff --git a/frontend/src/components/AdminTab.tsx b/frontend/src/components/AdminTab.tsx
index 041ff96..9c068de 100644
--- a/frontend/src/components/AdminTab.tsx
+++ b/frontend/src/components/AdminTab.tsx
@@ -1,4 +1,22 @@
import { useCallback, useEffect, useState } from "react";
+import {
+ Alert,
+ Anchor,
+ Button,
+ Card,
+ Group,
+ Table,
+ Text,
+ TextInput,
+ Title,
+} from "@mantine/core";
+import {
+ IconAlertCircle,
+ IconCheck,
+ IconExternalLink,
+ IconRefresh,
+ IconUpload,
+} from "@tabler/icons-react";
import {
ApiError,
LANGFLOW_URL,
@@ -6,6 +24,7 @@ import {
type FlowDefinition,
type FlowVersion,
} from "../api";
+import StatusBadge from "./StatusBadge";
export default function AdminTab() {
const [flows, setFlows] = useState([]);
@@ -86,61 +105,110 @@ export default function AdminTab() {
return (
-
Administration
- {error &&
{error}
}
- {info &&
{info}
}
-
- Flows werden visuell in Langflow entworfen:{" "}
-
- Langflow öffnen
-
-
-
Import aus Langflow
-
- setLangflowId(e.target.value)}
- placeholder="Langflow Flow-ID (UUID)"
- style={{ flex: 1, fontFamily: "monospace" }}
- />
- setImportName(e.target.value)}
- placeholder="Name (optional)"
- />
- void onImport()} disabled={!langflowId.trim()}>
- Importieren + Deployen
-
-
-
Flows + Versionen
-
- void onSync()}>
- Aus backend/flows synchronisieren
-
-
- {flows.length === 0 &&
Keine Flows vorhanden.
}
-
- {flows.map((f) => (
-
- {f.name} {" "}
-
- {f.source_path ? `(${f.source_path})` : "(API)"}
-
-
- {(versions[f.id] ?? []).map((v) => (
-
- v{v.version_no} ·{" "}
- {v.deployed_at ? `deployed (${v.lfx_flow_id})` : "nicht deployed"} ·{" "}
- {v.author || "–"}{" "}
- void onDeploy(f.id, v.version_no)}>
- Deployen
-
-
- ))}
-
-
- ))}
-
+
+ Flows & Versionen
+
+ {error && (
+
} mb="md">
+ {error}
+
+ )}
+ {info && (
+
} mb="md">
+ {info}
+
+ )}
+
+
+ Flows werden visuell in Langflow entworfen:{" "}
+
+ Langflow öffnen
+
+
+
+ setLangflowId(e.target.value)}
+ style={{ flex: 1 }}
+ styles={{ input: { fontFamily: "monospace" } }}
+ />
+ setImportName(e.target.value)}
+ />
+ }
+ onClick={() => void onImport()}
+ disabled={!langflowId.trim()}
+ >
+ Importieren + Deployen
+
+
+
+
+
+ }
+ onClick={() => void onSync()}
+ >
+ Aus backend/flows synchronisieren
+
+
+
+ {flows.length === 0 ? (
+
Keine Flows vorhanden.
+ ) : (
+
+
+
+ Flow
+ Version
+ Status
+ Autor
+
+
+
+
+ {flows.map((f) =>
+ (versions[f.id] ?? []).map((v, i) => (
+
+
+ {i === 0 && (
+ <>
+ {f.name} {" "}
+
+ {f.source_path ? `(${f.source_path})` : "(API)"}
+
+ >
+ )}
+
+ v{v.version_no}
+
+ {v.deployed_at ? (
+
+ ) : (
+
+ )}
+
+ {v.author || "–"}
+
+ void onDeploy(f.id, v.version_no)}
+ >
+ Deployen
+
+
+
+ )),
+ )}
+
+
+ )}
);
}
diff --git a/frontend/src/components/ApprovalsTab.tsx b/frontend/src/components/ApprovalsTab.tsx
index 8c9c57e..ac525d0 100644
--- a/frontend/src/components/ApprovalsTab.tsx
+++ b/frontend/src/components/ApprovalsTab.tsx
@@ -1,5 +1,18 @@
import { useCallback, useEffect, useState } from "react";
+import {
+ Alert,
+ Button,
+ Card,
+ Code,
+ Group,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+} from "@mantine/core";
+import { IconAlertCircle, IconCheck, IconRefresh, IconX } from "@tabler/icons-react";
import { ApiError, api, type ApprovalTask } from "../api";
+import StatusBadge from "./StatusBadge";
export default function ApprovalsTab() {
const [tasks, setTasks] = useState([]);
@@ -40,56 +53,89 @@ export default function ApprovalsTab() {
return (
-
Freigaben
- {error &&
{error}
}
-
-
Offen ({open.length})
- {open.length === 0 &&
Keine offenen Freigaben.
}
-
+ }
+ onClick={() => void load()}
+ >
+ Aktualisieren
+
+
+
+ Offen ({open.length})
+
+ {open.length === 0 && (
+
+ Keine offenen Freigaben.
+
+ )}
+
{open.map((t) => (
-
- {t.title || t.id} · {t.tenant_id}
-
- {JSON.stringify(t.payload)}
-
- void decide(t.id, "approve")}
- disabled={busyId === t.id}
- >
- Freigeben
- {" "}
- void decide(t.id, "reject")}
- disabled={busyId === t.id}
- >
- Ablehnen
-
-
+
+
+
+ {t.title || t.id}
+
+ {t.tenant_id}
+
+
+
+ {JSON.stringify(t.payload)}
+
+
+ }
+ loading={busyId === t.id}
+ onClick={() => void decide(t.id, "approve")}
+ >
+ Freigeben
+
+ }
+ loading={busyId === t.id}
+ onClick={() => void decide(t.id, "reject")}
+ >
+ Ablehnen
+
+
+
))}
-
-
Entschieden ({done.length})
-
- {done.map((t) => (
-
- {t.status} · {t.title || t.id} ·{" "}
- {t.approver ?? "–"}
+
+
+ Entschieden ({done.length})
+
+ {done.map((t) => (
+
+
+
+ {t.title || t.id} · {t.approver ?? "–"}
{t.decision_note ? ` · „${t.decision_note}“` : ""}
-
- ))}
-
+
+
+ ))}
);
}
diff --git a/frontend/src/components/SchemeToggle.tsx b/frontend/src/components/SchemeToggle.tsx
new file mode 100644
index 0000000..4a326b3
--- /dev/null
+++ b/frontend/src/components/SchemeToggle.tsx
@@ -0,0 +1,18 @@
+import { ActionIcon, useMantineColorScheme } from "@mantine/core";
+import { IconMoon, IconSun } from "@tabler/icons-react";
+
+export default function SchemeToggle() {
+ const { colorScheme, toggleColorScheme } = useMantineColorScheme();
+ const dark = colorScheme === "dark";
+ return (
+ toggleColorScheme()}
+ aria-label={dark ? "Zum hellen Modus" : "Zum dunklen Modus"}
+ title={dark ? "Zum hellen Modus" : "Zum dunklen Modus"}
+ >
+ {dark ? : }
+
+ );
+}
diff --git a/frontend/src/components/StatusBadge.tsx b/frontend/src/components/StatusBadge.tsx
new file mode 100644
index 0000000..645c325
--- /dev/null
+++ b/frontend/src/components/StatusBadge.tsx
@@ -0,0 +1,21 @@
+import { Badge } from "@mantine/core";
+
+/* Single color language for every status in the app. */
+
+const COLORS: Record = {
+ completed: "green",
+ approved: "green",
+ genehmigt: "green",
+ deployed: "green",
+ running: "blue",
+ open: "yellow",
+ pending: "yellow",
+ pending_approval: "yellow",
+ failed: "red",
+ rejected: "red",
+ abgelehnt: "red",
+};
+
+export default function StatusBadge({ status }: { status: string }) {
+ return {status} ;
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 350a0bf..41f6e8b 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,12 +1,21 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
+import { MantineProvider } from "@mantine/core";
+import "@mantine/core/styles.css";
import App from "./App";
+import { colorSchemeManager, theme } from "./theme";
createRoot(document.getElementById("root")!).render(
-
-
-
+
+
+
+
+
,
);
diff --git a/frontend/src/pages/AccessPage.tsx b/frontend/src/pages/AccessPage.tsx
index 39c98cd..8248d6f 100644
--- a/frontend/src/pages/AccessPage.tsx
+++ b/frontend/src/pages/AccessPage.tsx
@@ -1,4 +1,15 @@
import { useState } from "react";
+import {
+ Alert,
+ Button,
+ Card,
+ Stack,
+ Text,
+ Textarea,
+ TextInput,
+ Title,
+} from "@mantine/core";
+import { IconAlertCircle, IconCheck } from "@tabler/icons-react";
import { ApiError, api } from "../api";
export default function AccessPage() {
@@ -29,60 +40,54 @@ export default function AccessPage() {
}
}
- if (done) {
- return (
-
-
Zugang
-
+ return (
+
+
+ Zugang anfragen
+
+ {done ? (
+ } title="Angefragt">
Danke! Ihre Anfrage ist eingegangen. Sie erhalten eine E-Mail, sobald
sie freigegeben wurde.
-
-
- );
- }
-
- return (
-
-
Zugang anfragen
- {error &&
{error}
}
-
-
+
+ ) : (
+ void onSubmit(e)}>
+
+ {error && (
+ }>
+ {error}
+
+ )}
+ setName(e.target.value)}
+ required
+ maxLength={200}
+ />
+ setEmail(e.target.value)}
+ required
+ maxLength={320}
+ />
+ setReason(e.target.value)}
+ rows={4}
+ />
+
+ Anfragen
+
+
+ Ihre Angaben werden ausschließlich zur Zugangsfreigabe verwendet.
+
+
+
+ )}
+
);
}
diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx
index 2786896..fc4113d 100644
--- a/frontend/src/pages/AdminPage.tsx
+++ b/frontend/src/pages/AdminPage.tsx
@@ -1,3 +1,5 @@
+import { Alert, Stack, Title } from "@mantine/core";
+import { IconAlertCircle } from "@tabler/icons-react";
import AdminTab from "../components/AdminTab";
import ApprovalsTab from "../components/ApprovalsTab";
import AccessQueue from "../components/AccessQueue";
@@ -8,25 +10,37 @@ export default function AdminPage() {
if (!me) {
return (
-
Verwaltung
-
Bitte melden Sie sich an, um die Verwaltung zu nutzen.
+
+ Verwaltung
+
+
+ Bitte melden Sie sich an, um die Verwaltung zu nutzen.
+
);
}
if (me.role !== "admin") {
return (
-
Verwaltung
-
Nur für Administratoren. Ihr Konto hat die Rolle „{me.role}“.
+
+ Verwaltung
+
+
}>
+ Nur für Administratoren. Ihr Konto hat die Rolle „{me.role}“.
+
);
}
return (
-
Verwaltung
-
-
-
+
+ Verwaltung
+
+
+
+
+
+
);
}
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx
index cc0db32..5113ac9 100644
--- a/frontend/src/pages/LoginPage.tsx
+++ b/frontend/src/pages/LoginPage.tsx
@@ -1,5 +1,7 @@
import { useEffect, useState } from "react";
import { Link, Navigate, useSearchParams } from "react-router-dom";
+import { Alert, Anchor, Card, Center, Loader, Title } from "@mantine/core";
+import { IconAlertCircle } from "@tabler/icons-react";
import { ApiError, api } from "../api";
import { useAuth } from "../auth";
@@ -29,16 +31,30 @@ export default function LoginPage() {
if (me) return ;
return (
-
-
Login
- {busy &&
Link wird geprüft …
}
- {error &&
{error}
}
- {!token && !error && (
-
- Kein Link vorhanden. Zugang anfragen — der
- Login-Link kommt per E-Mail.
-
- )}
-
+
+
+
+ Login
+
+ {busy && (
+
+
+
+ )}
+ {error && (
+ }>
+ {error}
+
+ )}
+ {!token && !error && (
+
+
+ Zugang anfragen
+ {" "}
+ — der Login-Link kommt per E-Mail.
+
+ )}
+
+
);
}
diff --git a/frontend/src/pages/StartPage.tsx b/frontend/src/pages/StartPage.tsx
index 6111e77..441e146 100644
--- a/frontend/src/pages/StartPage.tsx
+++ b/frontend/src/pages/StartPage.tsx
@@ -1,26 +1,71 @@
import { Link } from "react-router-dom";
+import { Button, Card, Grid, Group, Text, Title } from "@mantine/core";
+import {
+ IconClipboardCheck,
+ IconInbox,
+ IconKey,
+ IconSparkles,
+} from "@tabler/icons-react";
import { useAuth } from "../auth";
+const actions = [
+ {
+ to: "/access",
+ icon: ,
+ title: "Zugang anfragen",
+ text: "Neu hier? Fordern Sie Ihren persönlichen Zugang an.",
+ },
+ {
+ to: "/app/workflows",
+ icon: ,
+ title: "Workflows",
+ text: "Vorgänge über Formulare starten und verfolgen.",
+ },
+ {
+ to: "/app/tasks",
+ icon: ,
+ title: "Aufgaben",
+ text: "Eigene Läufe und offene Freigaben im Blick.",
+ },
+];
+
export default function StartPage() {
const { me } = useAuth();
return (
-
Willkommen
-
- Kirche Einfach unterstützt die Verwaltung Ihrer Gemeinde: Vorgänge
- starten, Freigaben erteilen, Zugänge verwalten.
-
- {me ? (
-
- Angemeldet als {me.email} ({me.tenant_id}). Weiter zu{" "}
- Workflows.
-
- ) : (
-
- Zugang anfragen, um loszulegen. Mit
- Einladungslink geht es direkt zum Login.
-
- )}
+
+
+
+ Willkommen
+
+
+ Kirche Einfach unterstützt die Verwaltung Ihrer Gemeinde: Vorgänge
+ starten, Freigaben erteilen, Zugänge verwalten.
+
+ {me && (
+
+ Angemeldet als {me.email} ({me.tenant_id}).
+
+ )}
+
+
+ {actions.map((a) => (
+
+
+ {a.icon}
+
+ {a.title}
+
+
+ {a.text}
+
+
+ Öffnen
+
+
+
+ ))}
+
);
}
diff --git a/frontend/src/pages/TasksPage.tsx b/frontend/src/pages/TasksPage.tsx
index df818b2..34554da 100644
--- a/frontend/src/pages/TasksPage.tsx
+++ b/frontend/src/pages/TasksPage.tsx
@@ -1,6 +1,9 @@
import { useCallback, useEffect, useState } from "react";
+import { Alert, Code, Table, Text, Title } from "@mantine/core";
+import { IconAlertCircle } from "@tabler/icons-react";
import { ApiError, api, type Run } from "../api";
import ApprovalsTab from "../components/ApprovalsTab";
+import StatusBadge from "../components/StatusBadge";
export default function TasksPage() {
const [runs, setRuns] = useState<{ flow: string; run: Run }[]>([]);
@@ -27,18 +30,45 @@ export default function TasksPage() {
return (
-
Aufgaben
- {error &&
{error}
}
-
Eigene Läufe
- {runs.length === 0 &&
Noch keine Läufe.
}
-
- {runs.map(({ flow, run }) => (
-
- {run.status} · {flow} ·{" "}
- {run.output?.result ?? run.output?.error ?? "–"}
-
- ))}
-
+
+ Aufgaben
+
+ {error && (
+
} mb="md">
+ {error}
+
+ )}
+
+ Eigene Läufe
+
+ {runs.length === 0 ? (
+
+ Noch keine Läufe.
+
+ ) : (
+
+
+
+ Status
+ Flow
+ Ergebnis
+
+
+
+ {runs.map(({ flow, run }) => (
+
+
+
+
+ {flow}
+
+ {run.output?.result ?? run.output?.error ?? "–"}
+
+
+ ))}
+
+
+ )}
);
diff --git a/frontend/src/pages/WorkflowsPage.tsx b/frontend/src/pages/WorkflowsPage.tsx
index ab78424..39fff24 100644
--- a/frontend/src/pages/WorkflowsPage.tsx
+++ b/frontend/src/pages/WorkflowsPage.tsx
@@ -1,9 +1,22 @@
import { useCallback, useEffect, useState } from "react";
-import Form from "@rjsf/core";
+import Form from "@rjsf/mantine";
import validator from "@rjsf/validator-ajv8";
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
+import {
+ Alert,
+ Button,
+ Card,
+ Code,
+ Select,
+ Table,
+ Text,
+ Textarea,
+ Title,
+} from "@mantine/core";
+import { IconAlertCircle, IconPlayerPlay } from "@tabler/icons-react";
import { ApiError, api, type FlowDefinition, type InputSchema, type Run } from "../api";
import { useAuth } from "../auth";
+import StatusBadge from "../components/StatusBadge";
export default function WorkflowsPage() {
const { me } = useAuth();
@@ -70,68 +83,107 @@ export default function WorkflowsPage() {
return (
-
Workflows
- {!me &&
Sie sind nicht angemeldet — Läufe nutzen den Standard-Mandanten.
}
- {error &&
{error}
}
-
- setFlowId(e.target.value)}>
- — Flow wählen —
- {flows.map((f) => (
-
- {f.name}
-
- ))}
-
-
+
+ Workflows
+
+ {!me && (
+
+ Sie sind nicht angemeldet — Läufe nutzen den Standard-Mandanten.
+
+ )}
+ {error && (
+
} mb="md">
+ {error}
+
+ )}
+
setFlowId(v ?? "")}
+ data={flows.map((f) => ({ value: f.id, label: f.name }))}
+ mb="md"
+ maw={480}
+ />
{schema && schema.fallback && (
-
-
+ setFreeText(e.target.value)}
- placeholder="Freitext-Eingabe"
+ mb="sm"
/>
- }
+ loading={busy}
+ disabled={!flowId}
onClick={() => void onRun({ text: freeText })}
- disabled={!flowId || busy}
>
- {busy ? "Läuft …" : "Starten"}
-
-
+ Starten
+
+
)}
{schema && !schema.fallback && (
-
- setFormData((e.formData as Record) ?? {})
- }
- onSubmit={(e) => void onRun(e.formData)}
- >
-
- {busy ? "Läuft …" : "Starten"}
-
-
+
+
+ setFormData((e.formData as Record) ?? {})
+ }
+ onSubmit={(e) => void onRun(e.formData)}
+ >
+ }
+ loading={busy}
+ mt="sm"
+ >
+ Starten
+
+
+
)}
{result && (
-
+
+
+ Ergebnis
+
+ {result}
+
+ )}
+
+ Letzte Läufe
+
+ {runs.length === 0 ? (
+ Noch keine Läufe.
+ ) : (
+
+
+
+ Status
+ Mandant
+ Ergebnis
+
+
+
+ {runs.map((r) => (
+
+
+
+
+ {r.tenant_id}
+
+ {r.output?.result ?? r.output?.error ?? "–"}
+
+
+ ))}
+
+
)}
- Letzte Läufe
- {runs.length === 0 && Noch keine Läufe.
}
-
- {runs.map((r) => (
-
- {r.status} · {r.tenant_id} ·{" "}
- {r.output?.result ?? r.output?.error ?? "–"}
-
- ))}
-
);
}
diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts
new file mode 100644
index 0000000..c96be19
--- /dev/null
+++ b/frontend/src/theme.ts
@@ -0,0 +1,54 @@
+import {
+ createTheme,
+ localStorageColorSchemeManager,
+ type MantineColorSchemeManager,
+} from "@mantine/core";
+
+/* Calm-traditional direction: deep kirchenblau primary, warm paper
+ * neutrals in light mode, deep slate in dark mode. System font stacks
+ * only — no external requests (privacy). Serif display for headings. */
+
+const kirchenblau: [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+] = [
+ "#eef4fb",
+ "#d9e6f5",
+ "#b3cbe9",
+ "#84add9",
+ "#548bc6",
+ "#356fae",
+ "#245a93",
+ "#1d4a79",
+ "#1a3f65",
+ "#162f4c",
+];
+
+export const colorSchemeManager: MantineColorSchemeManager =
+ localStorageColorSchemeManager({ key: "kirche-color-scheme" });
+
+export const theme = createTheme({
+ primaryColor: "kirchenblau",
+ colors: { kirchenblau },
+ fontFamily:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ headings: {
+ fontFamily:
+ 'Georgia, "Times New Roman", "Noto Serif", serif',
+ },
+ defaultRadius: "md",
+ focusRing: "auto",
+ components: {
+ Container: {
+ defaultProps: { size: 960 },
+ },
+ },
+});