Refocus the homepage on documents. The notification widget is removed from the dashboard — it already lives in the bell dropdown. The page is restructured into a two-column "Command Center": recent activity on the left, upload zone and missing-metadata queue on the right. Stats are demoted to a quiet footnote.
/api/stats already existsSearchFilterBar component. Full width, no upload button appended.
DashboardResumeStrip. Only renders when localStorage has a last-visited document. Hidden otherwise — no empty gap.
grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-4items-start — the CSS Grid default align-items: stretch makes both columns the same height for free. The right column wrapper needs h-full so the flex container fills that height, and the metadata card gets flex-1 to consume the space left after the upload zone. Both columns are always flush at the bottom.
DashboardRecentDocuments receives a new optional stats prop. The footnote only renders when stats?.totalDocuments != null.
DropZone component, no internal changes. Wrapped in {#if data.canWrite} — hidden for read-only users.
DashboardNeedsMetadata. Already renders nothing when incompleteDocs.length === 0. Amber top border signals "action required" without relying on color alone — the heading "Needs Metadata" and count pill are redundant cues.
<div>. The equal-height stretch still applies but an invisible column causes no visual artefact. When this case is detectable server-side, consider conditionally omitting the grid class so the left column runs full width — but this is a polish improvement, not a blocker.
| Element | Tailwind classes | Real size | Notes |
|---|---|---|---|
| Page wrapper | mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8 |
py 32px, max-w 1280px | Unchanged from current +page.svelte |
| Resume strip | mb-4 (existing component, no change) |
mb 16px | Unchanged |
| Dashboard grid wrapper | mt-4 grid grid-cols-1 gap-4 lg:grid-cols-[1fr_300px] |
gap 16px, right col 300px fixed | No items-start. CSS Grid default is align-items: stretch — both columns are automatically the same height. Replaces the current conditional mentions+metadata grid. |
| Right column inner wrapper | flex flex-col gap-4 h-full |
gap 16px, full grid cell height | h-full is required so the flex container fills the stretched grid cell. Without it the column stops at content height and flex-1 on the child has nothing to fill. |
| DashboardNeedsMetadata wrapper | flex-1 flex flex-col min-h-0 (wraps the component) |
grows to fill remaining height | flex-1 consumes the space left after the DropZone. min-h-0 prevents flex overflow. The component's inner card should be h-full so the card border fills the space — content sits at the top, not stretched. |
| DropZone guard | {#if data.canWrite} |
— | No changes to the DropZone component itself — wrapper condition only. When absent, the metadata card's flex-1 still fills the full right column height. |
text-ink-3 at 12px — acceptable for supplementary metadata but never below that. Sufficient line-height (1.6) and border separators provide clear row breaks without relying on color.| Element | Tailwind classes | Real size | Notes |
|---|---|---|---|
| Grid (mobile) | grid-cols-1 (default, overridden at lg) |
full width | No explicit mobile grid — single column is the default |
| Doc row touch target | flex items-center justify-between py-3 border-b border-line |
min-h 44px via py-3 + content | Most commonly undersized. py-3 (12px × 2) + 18px text = ~42px. Add min-h-[44px] to guarantee WCAG 2.5.5 |
| Metadata row touch target | flex items-center border-b border-line py-3 |
min-h 44px | Same rule — min-h-[44px] required |
| Upload zone (mobile) | Existing DropZone — no change to component | full width, py-6 | Entire zone is the click target — WCAG 2.5.5 satisfied by size |
stats?.totalDocuments != nullstats?.totalPersons != null· is a text separator — not a visual-only cuetext-ink-3 token — light enough to recede, but still WCAG AA (4.5:1) on white surfaceinterface Props {
recentDocs: Document[];
stats?: {
totalDocuments?: number;
totalPersons?: number;
} | null;
}
{#if stats?.totalDocuments != null}
<div class="mt-2 border-t border-line
pt-3 font-sans text-xs
text-ink-3">
{stats.totalDocuments}
{m.dashboard_stats_documents()}
{#if stats.totalPersons != null}
·
{stats.totalPersons}
{m.dashboard_stats_persons()}
{/if}
</div>
{/if}
| Key | de | en | es |
|---|---|---|---|
| dashboard_stats_documents | Dokumente | Documents | Documentos |
| dashboard_stats_persons | Personen | Persons | Personas |
| Element | Tailwind classes | Real size | Notes |
|---|---|---|---|
| Card container | rounded-sm border border-line bg-surface |
border 1px | Unchanged from existing DashboardRecentDocuments styles |
| Card heading row | flex items-center justify-between px-4 pt-4 pb-3 |
px 16px, pt 16px, pb 12px | Unchanged |
| Section heading text | font-sans text-xs font-bold tracking-widest text-gray-400 uppercase |
12px / 700 | Unchanged |
| Document title | font-serif text-lg text-ink hover:text-ink-2 hover:underline |
18px / 400 — most commonly undersized | Must not fall below 18px. Serves both readability (seniors) and visual hierarchy |
| Date label | ml-2 shrink-0 font-sans text-xs text-gray-400 |
12px | Minimum permitted size for supplementary metadata — do not reduce further |
| Document row | flex items-center justify-between border-b border-line py-2 px-4 last:border-0 |
py 8px, min-h ~44px with 18px text | Add min-h-[44px] to guarantee WCAG 2.5.5 touch target |
| Stats footnote wrapper | mt-2 border-t border-line px-4 pt-3 pb-4 font-sans text-xs text-ink-3 |
12px / 400, pt 12px, pb 16px | New addition. text-ink-3 token must pass 4.5:1 on bg-surface — verify in both light and dark mode |
/api/notifications fetch from Promise.allSettled — the bell component fetches its own data client-sidementions: NotificationDTO[] variable and its allSettled result handlingmentions key from the return objectNotificationDTO type import (no longer used in this file)/api/stats GET call inside the isDashboard allSettled blockstats: StatsDTO | null in the return — null on any failureStatsDTO import from generated types (already in src/lib/generated/api.ts)mt-4 grid grid-cols-1 gap-4 lg:grid-cols-[1fr_300px] grid in +page.sveltestats prop on DashboardRecentDocuments/api/stats fetch in +page.server.tsdashboard_stats_documents i18n key (de / en / es)dashboard_stats_persons i18n key (de / en / es)DashboardMentions import and usage in +page.svelte/api/notifications fetch from server loadmentions variable and return value in server loadNotificationDTO import in +page.server.tsDashboardMentions.svelte file (not deleted)DashboardNeedsMetadata.svelte (no changes)DashboardResumeStrip.svelte (no changes)DropZone.svelte (no changes)NotificationBell.svelte — already has "View all" linkSearchFilterBar.svelte (no changes)/notifications overview page<div class="flex flex-col gap-4"> is empty — the grid column produces no visual gap, recent activity expands naturally.incompleteDocs.length > 0). Combined with a canWrite user, the right column shows only the upload zone.recentDocs.length === 0). Stats footnote still renders as long as the API call succeeded — "0 Documents · 0 Persons" is valid and informative.Promise.allSettled isolates the failure. stats is returned as null. The {#if stats?.totalDocuments != null} guard silently suppresses the footnote. Everything else renders normally — no error banner, no visual regression.truncate Tailwind class (already present in existing component — verify). The date label has shrink-0 so it is never squeezed off-screen.text-lg). WCAG 1.4.4text-xs text-ink-3. It is absent when the /api/stats call fails or returns null. datanpm run check passes — no TypeScript errors. The new stats prop on DashboardRecentDocuments is typed as StatsDTO | null | undefined.npm run lint passes — no Prettier or ESLint errors.