feat(web): Dashboard mit Begruessung, Befehlsleiste und ruhigeren Kacheln
Begruessung nach Tageszeit mit Datum, Befehlsleiste oben rechts statt schwebendem Knopf, einheitliche Kachel-Kopfzeilen mit Symbol, Kalender mit Pfeilknoepfen, runden Tagen und Terminpunkten, Favoriten als 36-px-Zeilen bzw. App-Starter-Kacheln, Notiz-Kaestchen im Akzent, leerer Zustand mit drei Vorschlaegen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,16 @@ const messages: Record<string, Record<string, string>> = {
|
||||
editMode: 'Dashboard bearbeiten',
|
||||
saveChanges: 'Änderungen speichern',
|
||||
addWidget: 'Widget hinzufügen',
|
||||
editShort: 'Bearbeiten',
|
||||
done: 'Fertig',
|
||||
toolbarLabel: 'Dashboard-Aktionen',
|
||||
layoutLoadError: 'Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.',
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: (ns: string) => (key: string) => messages[ns]?.[key] ?? key,
|
||||
useLocale: () => 'de',
|
||||
}));
|
||||
|
||||
const mockStore = {
|
||||
@@ -106,45 +110,46 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('DashboardPage (quick-260916-dyv)', () => {
|
||||
it('Test 1: Ansichtsmodus — Stift in fester Leiste unten rechts, kein "Widget hinzufuegen", kein mt-8/top-2, Grid direkt im Container p-2', async () => {
|
||||
it('Test 1: Ansichtsmodus — Befehlsleiste rechts oben mit „Bearbeiten“, kein "Widget hinzufuegen", Begruessung, Grid direkt im Container p-2 (Design „Mosaik“)', async () => {
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: 'Dashboard bearbeiten' });
|
||||
const bar = toggle.parentElement as HTMLElement;
|
||||
expect(bar.classList.contains('fixed')).toBe(true);
|
||||
expect(bar.classList.contains('bottom-6')).toBe(true);
|
||||
expect(bar.classList.contains('right-6')).toBe(true);
|
||||
expect(bar.classList.contains('z-20')).toBe(true);
|
||||
const toolbar = screen.getByRole('toolbar', { name: 'Dashboard-Aktionen' });
|
||||
const toggle = screen.getByRole('button', { name: 'Bearbeiten' });
|
||||
expect(toggle.parentElement).toBe(toolbar);
|
||||
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
||||
// kein schwebender Knopf mehr unten rechts
|
||||
expect(toolbar.classList.contains('fixed')).toBe(false);
|
||||
expect(document.querySelector('.bottom-6')).toBeNull();
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Widget hinzufügen' })).toBeNull();
|
||||
expect(document.querySelector('.mt-8')).toBeNull();
|
||||
expect(document.querySelector('.top-2')).toBeNull();
|
||||
await waitFor(() => expect(screen.getByTestId('dashboard-greeting')).toBeInTheDocument());
|
||||
|
||||
const grid = screen.getByTestId('dashboard-grid');
|
||||
expect((grid.parentElement as HTMLElement).classList.contains('p-2')).toBe(true);
|
||||
expect(grid.getAttribute('data-edit')).toBe('false');
|
||||
});
|
||||
|
||||
it('Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben dem Haekchen in derselben Leiste, Grid im Bearbeitungsmodus', async () => {
|
||||
it('Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben „Fertig“ in derselben Leiste, Grid im Bearbeitungsmodus', async () => {
|
||||
mockStore.isEditMode = true;
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: 'Änderungen speichern' });
|
||||
const toggle = screen.getByRole('button', { name: 'Fertig' });
|
||||
const add = screen.getByRole('button', { name: 'Widget hinzufügen' });
|
||||
expect(add.parentElement).toBe(toggle.parentElement);
|
||||
// "Widget hinzufuegen" steht im DOM VOR dem Umschalter (links daneben).
|
||||
expect(add.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
|
||||
expect(screen.getByTestId('dashboard-grid').getAttribute('data-edit')).toBe('true');
|
||||
});
|
||||
|
||||
it('Test 3: Umschalten — Klick auf "Dashboard bearbeiten" ruft setEditMode(true) genau einmal', async () => {
|
||||
it('Test 3: Umschalten — Klick auf "Bearbeiten" ruft setEditMode(true) genau einmal', async () => {
|
||||
const { default: DashboardPage } = await import('./page');
|
||||
render(<DashboardPage />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard bearbeiten' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Bearbeiten' }));
|
||||
expect(mockStore.setEditMode).toHaveBeenCalledTimes(1);
|
||||
expect(mockStore.setEditMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl';
|
||||
import { DashboardGrid } from '@/components/dashboard/dashboard-grid';
|
||||
import { DashboardTabs } from '@/components/dashboard/dashboard-tabs';
|
||||
import { EditModeToggle } from '@/components/dashboard/edit-mode-toggle';
|
||||
import { DashboardGreeting } from '@/components/dashboard/dashboard-greeting';
|
||||
import { BackgroundPicker, DashboardBackdrop, useDashboardBackground } from '@/components/dashboard/dashboard-background';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
import { WidgetCatalogModal } from '@/components/dashboard/widget-catalog-modal';
|
||||
@@ -138,34 +139,27 @@ export default function DashboardPage() {
|
||||
onReorder={reorderDashboards}
|
||||
/>
|
||||
|
||||
{isSwitchingDashboard ? (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">{t('tabs.switching')}</p>
|
||||
{/* Kopfzeile des Dashboards (Design „Mosaik“, Runde 2): Begruessung
|
||||
links, Befehlsleiste rechts (Fluent „command bar“) statt des
|
||||
frueheren schwebenden Knopfs unten rechts. Mit Hintergrund liegen
|
||||
beide auf der durchscheinenden Kachelflaeche (.dashboard-chrome). */}
|
||||
<div className="flex flex-wrap items-end justify-between gap-x-4 gap-y-2 px-3 pb-1 pt-2">
|
||||
<div className="dashboard-chrome min-w-0 rounded-lg">
|
||||
<DashboardGreeting />
|
||||
</div>
|
||||
) : (
|
||||
/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={widgets}
|
||||
isEditMode={isEditMode}
|
||||
onLayoutChange={updateLayouts}
|
||||
onRemoveWidget={removeWidget}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Feste Aktionsleiste unten rechts.
|
||||
quick-260916-dyv: Umschalter unten rechts statt oben rechts, damit das
|
||||
Grid direkt unter der Kopfzeile beginnt (12 + 8 + 8 = 28 px statt 60 px).
|
||||
Im Bearbeitungsmodus steht "Widget hinzufuegen" links neben dem Haekchen. */}
|
||||
<div className="fixed bottom-6 right-6 z-20 flex items-center gap-2">
|
||||
{isEditMode && <BackgroundPicker value={background} onChange={chooseBackground} />}
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
className="btn btn-primary shadow-lg"
|
||||
>
|
||||
<svg
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label={t('toolbarLabel')}
|
||||
className="dashboard-chrome flex items-center gap-1 rounded-lg"
|
||||
>
|
||||
{isEditMode && <BackgroundPicker value={background} onChange={chooseBackground} />}
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
className="btn btn-subtle"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@@ -180,15 +174,33 @@ export default function DashboardPage() {
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
)}
|
||||
<EditModeToggle
|
||||
isEditMode={isEditMode}
|
||||
onToggle={() => setEditMode(!isEditMode)}
|
||||
/>
|
||||
{t('addWidget')}
|
||||
</button>
|
||||
)}
|
||||
<EditModeToggle
|
||||
isEditMode={isEditMode}
|
||||
onToggle={() => setEditMode(!isEditMode)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSwitchingDashboard ? (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">{t('tabs.switching')}</p>
|
||||
</div>
|
||||
) : (
|
||||
/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={widgets}
|
||||
isEditMode={isEditMode}
|
||||
onLayoutChange={updateLayouts}
|
||||
onRemoveWidget={removeWidget}
|
||||
onOpenCatalog={() => setCatalogOpen(true)}
|
||||
onQuickAdd={(type) => addWidget(type)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Widget catalog modal */}
|
||||
<WidgetCatalogModal
|
||||
isOpen={catalogOpen}
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
--color-status-orphan-fg: var(--status-orphan-fg);
|
||||
--color-well: var(--well);
|
||||
--color-link: var(--link);
|
||||
--color-tile: var(--tile);
|
||||
--color-tile-foreground: var(--tile-foreground);
|
||||
--color-primary-strong: var(--primary-strong);
|
||||
--color-input-strong: var(--input-strong);
|
||||
--color-appbar: var(--appbar);
|
||||
@@ -118,6 +120,9 @@
|
||||
--ring: oklch(0.3 0.01 260);
|
||||
/* Verweise: nie gelbe Schrift auf Weiss. 5,9:1 auf --card. */
|
||||
--link: oklch(0.50 0.15 255);
|
||||
/* Neutrale Modul-Kachel (Runde 2: einfarbig statt bunt). */
|
||||
--tile: oklch(0.93 0.004 260);
|
||||
--tile-foreground: oklch(0.35 0.01 260);
|
||||
/* Akzent fuer duenne Markierungen (Auswahlpille, Reiter-Unterstrich) auf
|
||||
hellem Grund: die Akzentfarbe um knapp ein Drittel abgedunkelt, damit
|
||||
auch Gelb neben Hellgrau noch erkennbar bleibt. */
|
||||
@@ -177,6 +182,8 @@
|
||||
--input-strong: oklch(1 0 0 / 0.42);
|
||||
--ring: oklch(0.92 0.005 260);
|
||||
--link: oklch(0.78 0.11 250);
|
||||
--tile: oklch(0.32 0.01 260);
|
||||
--tile-foreground: oklch(0.88 0.005 260);
|
||||
--primary-strong: var(--primary);
|
||||
|
||||
--appbar: oklch(0.18 0.01 260);
|
||||
@@ -502,3 +509,41 @@ input[type="checkbox"], input[type="radio"] {
|
||||
.dashboard-has-bg [data-widget-id] {
|
||||
box-shadow: 0 0 2px oklch(0 0 0 / 0.14), 0 2px 6px oklch(0 0 0 / 0.16);
|
||||
}
|
||||
|
||||
/* Begruessung, Befehlsleiste und leerer Zustand: ohne Hintergrund frei auf
|
||||
der Arbeitsflaeche, mit Hintergrund auf derselben Kachelflaeche wie die
|
||||
Widgets (deckend als Rueckfall, durchscheinend wo moeglich). */
|
||||
.dashboard-has-bg .dashboard-chrome {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background-color: var(--card);
|
||||
box-shadow: 0 0 2px oklch(0 0 0 / 0.14), 0 2px 6px oklch(0 0 0 / 0.16);
|
||||
}
|
||||
.dashboard-has-bg [role="toolbar"].dashboard-chrome {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
||||
@media not (prefers-reduced-transparency: reduce) {
|
||||
.dashboard-has-bg .dashboard-chrome {
|
||||
background-color: oklch(1 0 0 / 0.86);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.2);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
}
|
||||
.dark .dashboard-has-bg .dashboard-chrome {
|
||||
background-color: oklch(0.24 0.01 260 / 0.88);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Notiz-Checklisten: Kaestchen in der Akzentfarbe; der Browser waehlt den
|
||||
Haken selbst hell oder dunkel passend zur Akzentfarbe. */
|
||||
:root .widget-note-body input[type="checkbox"] {
|
||||
accent-color: var(--primary);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0 0.5rem 0 0;
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
:root .widget-note-body .task-list-item {
|
||||
padding-block: 0.125rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user