style(web): Seiten auf Mosaik-Muster umgestellt

Einheitliche Seitenkoepfe mit Modul-Kachel, Knopf- und Linkklassen
statt gelber Schrift, Karten ohne harte Rahmen, Tabellenkoepfe ohne
Grossbuchstaben, Marktplatz mit Fluent-Reitern, Filterpillen und
Kachel-Karten, Einstellungs-/Verwaltungsnavigation mit Auswahlpille,
leere Zustaende mit Symbol im Kreis, Proxmox ohne leuchtende Schatten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 14:49:01 +02:00
parent 219038b3ee
commit 30e4bc3a4f
75 changed files with 571 additions and 521 deletions
@@ -93,23 +93,23 @@ export function AdminSidebar() {
// das <nav> darunter trAegt die Navigationssemantik schon; ein zweites
// Navigations-Landmark auf dem Rahmen waere ein doppeltes Landmark fuer
// dieselbe Sache. Der aria-label sitzt darum am <nav>, nicht am <aside>.
<aside className="flex w-[220px] shrink-0 flex-col border-r border-sidebar-border bg-sidebar">
<div className="p-4">
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<aside className="flex w-full shrink-0 flex-col border-b border-border py-2 md:w-[220px] md:border-b-0">
<div className="px-5 pb-1 pt-4">
<h2 className="text-xs font-semibold text-muted-foreground">
{t('admin.label')}
</h2>
</div>
<nav className="flex flex-col gap-1 px-3" aria-label={t('admin.label')}>
<nav className="flex flex-col gap-0.5 px-3" aria-label={t('admin.label')}>
{items
.filter((item) => item.show)
.map((item) => (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${
className={`nav-item flex h-9 items-center gap-2 rounded-lg px-2.5 text-sm transition-colors ${
pathname.startsWith(item.href)
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
: 'text-sidebar-foreground hover:bg-muted'
? 'bg-sidebar-accent text-sidebar-accent-foreground font-semibold'
: 'text-sidebar-foreground hover:bg-sidebar-hover'
}`}
aria-current={pathname.startsWith(item.href) ? 'page' : undefined}
>
@@ -103,7 +103,7 @@ export function BugReportDialog({ screenshot, isAdmin, onClose }: BugReportDialo
aria-labelledby="bug-report-title"
data-bug-report-ignore="true"
>
<div className="w-full max-w-lg rounded-lg border border-border bg-card p-6 shadow-lg">
<div className="w-full max-w-lg rounded-lg bg-card dark:border dark:border-border p-6 shadow-lg">
<h2 id="bug-report-title" className="text-lg font-semibold text-foreground mb-2">
{t('title')}
</h2>
@@ -115,7 +115,7 @@ export function BugReportDialog({ screenshot, isAdmin, onClose }: BugReportDialo
<button
type="button"
onClick={onClose}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
className="btn btn-primary"
>
{t('close')}
</button>
@@ -183,7 +183,7 @@ export function BugReportDialog({ screenshot, isAdmin, onClose }: BugReportDialo
type="button"
onClick={onClose}
disabled={busy}
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted disabled:opacity-50"
className="btn btn-secondary"
>
{t('cancel')}
</button>
@@ -191,7 +191,7 @@ export function BugReportDialog({ screenshot, isAdmin, onClose }: BugReportDialo
type="button"
onClick={handleSend}
disabled={busy}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
className="btn btn-primary"
>
{busy ? t('sending') : t('send')}
</button>
+1 -1
View File
@@ -194,7 +194,7 @@ export function Header() {
{/* Dropdown menu */}
{dropdownOpen && (
<div className="absolute right-0 top-full z-50 mt-2 w-60 rounded-lg border border-border bg-card py-1.5 text-card-foreground shadow-lg">
<div className="absolute right-0 top-full z-50 mt-2 w-60 rounded-lg bg-card dark:border dark:border-border py-1.5 text-card-foreground shadow-lg">
{/* User info */}
<div className="px-4 py-2 border-b border-border">
<p className="text-sm font-medium text-foreground truncate">
@@ -20,16 +20,16 @@ export function ModuleAccessDenied({ title, body, backToDashboard }: ModuleAcces
return (
<div className="mx-auto max-w-2xl space-y-6 p-6">
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="rounded-lg bg-muted p-4 mb-4">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
className="text-muted-foreground"
@@ -38,9 +38,9 @@ export function ModuleAccessDenied({ title, body, backToDashboard }: ModuleAcces
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">{title}</h2>
<h2 className="mb-1 text-base font-semibold">{title}</h2>
<p className="text-sm text-muted-foreground mb-6">{body}</p>
<Link href="/" className="text-sm font-medium text-primary hover:underline">
<Link href="/" className="btn btn-primary">
{backToDashboard}
</Link>
</div>
@@ -7,7 +7,8 @@ import type { MeterLevel, ServerHealth } from './proxmox-status';
*
* `fill` = Flaeche (Leiste, Punkt, Balken), `pill` = Statuspille (12 %
* Flaeche + Textvariante), `text` = Schrift in Statusfarbe, `shadow` =
* zweischichtiger Schatten, der untere im Statuston getoent.
* ruhender Fluent-Schatten (Design „Mosaik“: keine im Statuston
* leuchtenden Schatten mehr — der Zustand steht in Leiste und Pille).
*
* Seit quick-260924-i8v gemeinsam fuer Modulseite und Dashboard-Kachel.
*/
@@ -19,32 +20,31 @@ export const HEALTH_STYLE: Record<
fill: 'bg-status-ok',
pill: 'bg-status-ok/12 text-status-ok-fg',
text: 'text-status-ok-fg',
shadow: 'shadow-[0_1px_2px_oklch(0_0_0/0.06),0_12px_28px_-14px_var(--status-ok)]',
shadow: 'shadow-sm',
},
warn: {
fill: 'bg-status-warn',
pill: 'bg-status-warn/12 text-status-warn-fg',
text: 'text-status-warn-fg',
shadow: 'shadow-[0_1px_2px_oklch(0_0_0/0.06),0_12px_28px_-14px_var(--status-warn)]',
shadow: 'shadow-sm',
},
down: {
fill: 'bg-status-down',
pill: 'bg-status-down/12 text-status-down-fg',
text: 'text-status-down-fg',
shadow: 'shadow-[0_1px_2px_oklch(0_0_0/0.06),0_12px_28px_-14px_var(--status-down)]',
shadow: 'shadow-sm',
},
idle: {
fill: 'bg-status-idle',
pill: 'bg-status-idle/12 text-status-idle-fg',
text: 'text-status-idle-fg',
shadow: 'shadow-[0_1px_2px_oklch(0_0_0/0.06),0_12px_28px_-14px_var(--status-idle)]',
shadow: 'shadow-sm',
},
// Kein farbiger Schatten: ein verwaister Server soll nicht „leuchten“.
orphan: {
fill: 'bg-status-orphan',
pill: 'bg-status-orphan/12 text-status-orphan-fg',
text: 'text-status-orphan-fg',
shadow: 'shadow-[0_1px_2px_oklch(0_0_0/0.04)]',
shadow: 'shadow-sm',
},
};
@@ -64,4 +64,4 @@ export const METER_TEXT: Record<MeterLevel, string> = {
};
/** Eingelassenes Messfeld („Well“). */
export const WELL = 'rounded-lg bg-well shadow-[inset_0_1px_2px_oklch(0_0_0/0.06)]';
export const WELL = 'rounded-lg bg-well';
@@ -127,7 +127,7 @@ export function ReleaseNoticeDialog({ notice, onClose }: ReleaseNoticeDialogProp
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="relative z-50 mx-4 flex max-h-[85vh] w-full max-w-lg flex-col rounded-lg border border-border bg-card shadow-xl"
className="relative z-50 mx-4 flex max-h-[85vh] w-full max-w-lg flex-col rounded-lg bg-card dark:border dark:border-border shadow-xl"
>
<div className="flex items-center justify-between gap-4 border-b border-border px-6 py-4">
<h2
@@ -215,7 +215,7 @@ export function ReleaseNoticeDialog({ notice, onClose }: ReleaseNoticeDialogProp
<button
type="button"
onClick={close}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
className="btn btn-primary"
>
{t('confirm')}
</button>
@@ -197,7 +197,7 @@ export function AccountSettingsForm() {
disabled={isAvatarPending}
onChange={handleAvatarUpload}
/>
<span className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50">
<span className="btn btn-primary">
{isAvatarPending ? '...' : t('account.uploadCta')}
</span>
</label>
@@ -281,7 +281,7 @@ export function AccountSettingsForm() {
type="button"
onClick={handleSaveAccentColor}
disabled={isAccentPending || !isHexValid}
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
className="btn btn-primary"
>
{isAccentPending ? '...' : t('account.accentColorSave')}
</button>
@@ -365,7 +365,7 @@ export function AccountSettingsForm() {
<button
type="submit"
disabled={isPwPending}
className="flex items-center justify-center rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
className="btn btn-primary"
>
{isPwPending ? (
<svg aria-hidden="true" className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@@ -358,7 +358,7 @@ export function CalendarSettingsPanel() {
<button
type="button"
onClick={() => setShowAddForm(true)}
className="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
className="btn btn-primary"
>
{t('calendar.addSource')}
</button>
@@ -394,7 +394,7 @@ export function CalendarSettingsPanel() {
/>
<div
className="relative mx-4 max-w-md rounded-lg border border-border bg-card p-6 shadow-xl"
className="relative mx-4 max-w-md rounded-lg bg-card dark:border dark:border-border p-6 shadow-xl"
role="alertdialog"
aria-modal="true"
aria-label={t('calendar.deleteDialogLabel')}
@@ -409,7 +409,7 @@ export function CalendarSettingsPanel() {
<button
type="button"
onClick={() => setDeletingId(null)}
className="rounded border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted"
className="btn btn-secondary"
>
{ts('cancel')}
</button>
@@ -355,7 +355,7 @@ export function CalendarSourceForm({
type="submit"
data-testid="source-form-submit"
disabled={!isFormValid || isSaving}
className="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
className="btn btn-primary"
>
{isSaving ? t('calendar.formSaving') : t('calendar.formSave')}
</button>
@@ -364,14 +364,14 @@ export function CalendarSourceForm({
data-testid="source-form-test"
disabled={!isFormValid || testStatus === 'loading'}
onClick={handleTest}
className="rounded border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed"
className="btn btn-secondary"
>
{testStatus === 'loading' ? t('calendar.formTesting') : t('calendar.formTestConnection')}
</button>
<button
type="button"
onClick={onCancel}
className="rounded border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted"
className="btn btn-secondary"
>
{t('calendar.formCancel')}
</button>
@@ -210,7 +210,7 @@ export function SearchProviderForm() {
type="button"
onClick={handleAdd}
disabled={isSubmitting || !newName.trim() || !newUrlTemplate.trim()}
className="h-8 rounded bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
className="btn btn-primary transition-colors"
>
{isSubmitting ? 'Adding...' : t('search.addProviderButton')}
</button>
@@ -37,20 +37,20 @@ export function SettingsSidebar() {
// darunter stehen zwei <nav>-Bereiche (Allgemein, Dashboard); ein drittes
// Navigations-Landmark auf dem Rahmen waere doppelt. Jedes <nav> traegt
// stattdessen die Beschriftung seiner eigenen Kategorie.
<aside className="flex w-[220px] shrink-0 flex-col border-r border-sidebar-border bg-sidebar">
<aside className="flex w-full shrink-0 flex-col border-b border-border py-2 md:w-[220px] md:border-b-0">
{/* Allgemein category — above Dashboard (Surface C, 07-06) */}
<div className="p-4 pb-2">
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<div className="px-5 pb-1 pt-3">
<h2 className="text-xs font-semibold text-muted-foreground">
{t('categoryGeneral')}
</h2>
</div>
<nav className="mb-2 flex flex-col gap-1 px-3" aria-label={t('categoryGeneral')}>
<nav className="mb-2 flex flex-col gap-0.5 px-3" aria-label={t('categoryGeneral')}>
<Link
href="/settings/general/account"
className={`flex items-center rounded-md px-2 py-1.5 text-sm transition-colors ${
className={`nav-item flex h-9 items-center rounded-lg px-2.5 text-sm transition-colors ${
isActive('/settings/general/account')
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
: 'text-sidebar-foreground hover:bg-muted'
? 'bg-sidebar-accent text-sidebar-accent-foreground font-semibold'
: 'text-sidebar-foreground hover:bg-sidebar-hover'
}`}
aria-current={
isActive('/settings/general/account') ? 'page' : undefined
@@ -60,10 +60,10 @@ export function SettingsSidebar() {
</Link>
<Link
href="/settings/general/desktop"
className={`flex items-center rounded-md px-2 py-1.5 text-sm transition-colors ${
className={`nav-item flex h-9 items-center rounded-lg px-2.5 text-sm transition-colors ${
isActive('/settings/general/desktop')
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
: 'text-sidebar-foreground hover:bg-muted'
? 'bg-sidebar-accent text-sidebar-accent-foreground font-semibold'
: 'text-sidebar-foreground hover:bg-sidebar-hover'
}`}
aria-current={
isActive('/settings/general/desktop') ? 'page' : undefined
@@ -74,20 +74,20 @@ export function SettingsSidebar() {
</nav>
{/* Dashboard category — existing items unchanged */}
<div className="p-4">
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<div className="px-5 pb-1 pt-4">
<h2 className="text-xs font-semibold text-muted-foreground">
{t('categoryDashboard')}
</h2>
</div>
<nav className="flex flex-col gap-1 px-3" aria-label={t('categoryDashboard')}>
<nav className="flex flex-col gap-0.5 px-3" aria-label={t('categoryDashboard')}>
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className={`flex items-center rounded-md px-2 py-1.5 text-sm transition-colors ${
className={`nav-item flex h-9 items-center rounded-lg px-2.5 text-sm transition-colors ${
isActive(item.href)
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
: 'text-sidebar-foreground hover:bg-muted'
? 'bg-sidebar-accent text-sidebar-accent-foreground font-semibold'
: 'text-sidebar-foreground hover:bg-sidebar-hover'
}`}
aria-current={isActive(item.href) ? 'page' : undefined}
>
@@ -348,7 +348,7 @@ export function SmtpSettingsForm() {
<button
type="submit"
disabled={isSaving}
className="rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
className="btn btn-primary"
>
{t('smtp.save')}
</button>
@@ -356,7 +356,7 @@ export function SmtpSettingsForm() {
type="button"
disabled={isTesting}
onClick={handleTest}
className="rounded border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
className="btn btn-secondary"
>
{t('smtp.test')}
</button>
@@ -451,7 +451,7 @@ function CalendarConfig({
{t('calendar.configSourcesHint')}{' '}
<Link
href="/settings/dashboard/calendar"
className="text-primary underline hover:text-primary/90"
className="link"
>
{t('calendar.configSourcesLink')}
</Link>