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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user