docs(specs): add final frontend specs for members and settings Kachel views
Finalised implementation specs for /members (E2) and /settings (E1) pages using the chosen Kachel (card grid) variation. Members spec covers 6 states including role-change inline control and remove confirmation dialog; notes backend gaps (DELETE/PATCH member endpoints). Settings spec covers hub layout, D3 staples sub-page, hover and empty states. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
700
specs/frontend/e1-settings-kachel.html
Normal file
700
specs/frontend/e1-settings-kachel.html
Normal file
@@ -0,0 +1,700 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>E1 — Einstellungen · Kachel-Ansicht · Finale Spezifikation</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
||||
<!--
|
||||
spec:agent
|
||||
document: E1 Einstellungen – Kachel-Ansicht, Finale Spezifikation
|
||||
version: 1.0
|
||||
journey: J8 Edit pantry staples
|
||||
routes: /settings (E1 hub) → /household/staples?ctx=settings (D3)
|
||||
screens: E1, D3
|
||||
chosen-variation: V2 Kachel-Ansicht (Card sections)
|
||||
last-updated: 2026-04-09
|
||||
|
||||
NAVIGATION STRUCTURE:
|
||||
E1 (/settings) → Hub with 3 cards:
|
||||
Card 1 "Vorräte" → navigates to D3 (/household/staples?ctx=settings)
|
||||
Card 2 "Mitglieder" → navigates to E2 (/members)
|
||||
Card 3 "Profil" → navigates to /profile (not yet implemented)
|
||||
|
||||
DATA:
|
||||
Vorräte count: derived from GET /v1/ingredient-categories response
|
||||
(count ingredients where isStaple === true)
|
||||
Mitglieder count: from layout data (locals.haushalt via GET /v1/households/mine/members)
|
||||
Profil name/email: from locals.benutzer
|
||||
|
||||
NOTE: D3 = A3. StaplesManager component is reused with context="settings".
|
||||
StaplesManager renders categories as StapleChip pill grids, NOT checkboxes.
|
||||
Auto-save on toggle (debounced PATCH 300ms). No save button.
|
||||
-->
|
||||
<style>
|
||||
:root {
|
||||
--color-page: #FAFAF7;
|
||||
--color-surface: #F5F4EE;
|
||||
--color-subtle: #EDECEA;
|
||||
--color-border: #D8D7D0;
|
||||
--color-text-muted: #6B6A63;
|
||||
--color-text: #1C1C18;
|
||||
--green-tint: #E8F5EA;
|
||||
--green-light: #AEDCB0;
|
||||
--green: #3D8C4A;
|
||||
--green-dark: #2E6E39;
|
||||
--green-deeper: #1E4A26;
|
||||
--yellow-tint: #FDF6D8;
|
||||
--yellow-light: #F9E08A;
|
||||
--yellow-text: #8A6800;
|
||||
--color-error: #DC4C3E;
|
||||
--blue-tint: #E6F1FB;
|
||||
--blue: #185FA5;
|
||||
--blue-dark: #0C447C;
|
||||
--font-display: 'Fraunces', Georgia, serif;
|
||||
--font-sans: 'DM Sans', system-ui, sans-serif;
|
||||
--font-mono: 'DM Mono', monospace;
|
||||
--radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px; --radius-xl: 16px; --radius-full: 9999px;
|
||||
--shadow-card: 0 1px 3px rgba(28,28,24,.06), 0 1px 2px rgba(28,28,24,.04);
|
||||
--shadow-raised: 0 4px 12px rgba(28,28,24,.10), 0 2px 4px rgba(28,28,24,.05);
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--font-sans); background: var(--color-page); color: var(--color-text); font-size: 14px; line-height: 1.6; }
|
||||
|
||||
/* ── Doc layout ── */
|
||||
.doc { max-width: 1040px; margin: 0 auto; padding: 48px 40px 96px; }
|
||||
.doc-header { padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 48px; display: flex; justify-content: space-between; align-items: flex-end; }
|
||||
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }
|
||||
.doc-header p { font-size: 13px; color: var(--color-text-muted); margin-top: 4px; }
|
||||
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
|
||||
.section-label { font-size: 10px; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); padding-bottom: 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 32px; margin-top: 56px; }
|
||||
.intro { font-size: 14px; line-height: 1.75; max-width: 640px; margin-bottom: 40px; }
|
||||
|
||||
/* ── State sections ── */
|
||||
.state { margin-bottom: 64px; }
|
||||
.state-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
|
||||
.state-id { font-family: var(--font-mono); font-size: 10px; font-weight: 500; background: var(--color-subtle); color: var(--color-text-muted); padding: 2px 8px; border-radius: var(--radius-sm); white-space: nowrap; margin-top: 3px; }
|
||||
.state-title { font-size: 16px; font-weight: 500; letter-spacing: -0.01em; }
|
||||
.state-desc { font-size: 13px; color: var(--color-text-muted); margin-top: 2px; max-width: 540px; }
|
||||
|
||||
/* ── Preview containers ── */
|
||||
.preview-wrap { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
|
||||
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
|
||||
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
|
||||
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
|
||||
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
|
||||
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
|
||||
|
||||
/* ── Notes ── */
|
||||
.notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 14px 18px; }
|
||||
.notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
|
||||
.notes ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
||||
.notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
|
||||
.notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
|
||||
|
||||
/* ── AppShell chrome ── */
|
||||
.shell { display: flex; min-height: 100vh; background: var(--color-page); font-family: var(--font-sans); }
|
||||
.sidebar { width: 224px; min-width: 224px; background: white; border-right: 1px solid var(--color-border); display: flex; flex-direction: column; }
|
||||
.sidebar-brand { padding: 14px 18px; border-bottom: 1px solid var(--color-border); }
|
||||
.sidebar-brand-row { display: flex; align-items: center; gap: 8px; }
|
||||
.sidebar-logo { width: 22px; height: 22px; background: var(--green); border-radius: var(--radius-sm); }
|
||||
.sidebar-app { font-family: var(--font-display); font-size: 15px; font-weight: 500; }
|
||||
.sidebar-household { font-size: 10px; color: var(--color-text-muted); margin-top: 1px; }
|
||||
.sidebar-nav { flex: 1; padding: 4px 8px; }
|
||||
.sidebar-group-label { font-size: 8px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); padding: 16px 12px 4px; }
|
||||
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: var(--radius-md); font-size: 13px; color: var(--color-text); text-decoration: none; }
|
||||
.sidebar-item.active { background: var(--green-tint); color: var(--green-dark); font-weight: 500; }
|
||||
.sidebar-item:not(.active):hover { background: var(--color-subtle); }
|
||||
.sidebar-icon { width: 20px; text-align: center; font-size: 16px; }
|
||||
|
||||
/* ── Page content ── */
|
||||
.page-content { flex: 1; padding: 32px 40px; }
|
||||
.page-title { font-family: var(--font-display); font-size: 24px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
|
||||
.page-subtitle { font-size: 13px; color: var(--color-text-muted); margin-bottom: 28px; }
|
||||
|
||||
/* ── Settings card grid ── */
|
||||
.settings-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
|
||||
.settings-grid-bottom { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
|
||||
|
||||
/* ── Setting card ── */
|
||||
.setting-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px; box-shadow: var(--shadow-card); cursor: pointer; text-decoration: none; color: inherit; display: flex; flex-direction: column; }
|
||||
.setting-card:hover { box-shadow: var(--shadow-raised); border-color: #C0BFB8; }
|
||||
.setting-card.primary { border-left: 3px solid var(--green-dark); }
|
||||
.setting-card.primary:hover { border-left-color: var(--green-dark); }
|
||||
|
||||
.card-icon { font-size: 22px; margin-bottom: 12px; }
|
||||
.card-stat { font-family: var(--font-display); font-size: 36px; font-weight: 500; letter-spacing: -0.02em; color: var(--green-dark); line-height: 1; margin-bottom: 2px; }
|
||||
.card-stat-label { font-size: 11px; color: var(--color-text-muted); margin-bottom: 12px; }
|
||||
.card-title { font-size: 15px; font-weight: 500; margin-bottom: 4px; }
|
||||
.card-desc { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; flex: 1; }
|
||||
.card-cta { margin-top: 16px; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; color: var(--green-dark); }
|
||||
.card-cta-secondary { margin-top: 16px; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; color: var(--color-text-muted); }
|
||||
.card-meta { font-size: 12px; color: var(--color-text-muted); margin-bottom: 4px; }
|
||||
|
||||
/* ── D3 Staples page chrome ── */
|
||||
.breadcrumb { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--color-text-muted); margin-bottom: 20px; }
|
||||
.breadcrumb a { color: var(--color-text-muted); text-decoration: none; }
|
||||
.breadcrumb a:hover { color: var(--color-text); }
|
||||
.breadcrumb-sep { font-size: 10px; }
|
||||
|
||||
/* ── Staple chips ── */
|
||||
.category-block { margin-bottom: 24px; }
|
||||
.category-name { font-size: 10px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 10px; }
|
||||
.chip-wrap { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.chip { padding: 5px 12px; border-radius: var(--radius-full); border: 1px solid var(--color-border); font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; }
|
||||
.chip.on { background: var(--green-dark); color: white; border-color: var(--green-dark); }
|
||||
.chip.off { background: transparent; color: var(--color-text-muted); }
|
||||
.chip.off:hover { border-color: var(--green-light); color: var(--green-dark); }
|
||||
.save-note { font-size: 11px; color: var(--color-text-muted); margin-top: 16px; font-style: italic; }
|
||||
|
||||
/* ── Mobile shell ── */
|
||||
.m-shell { display: flex; flex-direction: column; background: var(--color-page); }
|
||||
.m-header { padding: 16px; background: white; border-bottom: 1px solid var(--color-border); }
|
||||
.m-header-title { font-size: 16px; font-weight: 500; }
|
||||
.m-content { flex: 1; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.m-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 16px; box-shadow: var(--shadow-card); }
|
||||
.m-card.primary { border-left: 3px solid var(--green-dark); }
|
||||
.m-card-stat { font-family: var(--font-display); font-size: 28px; font-weight: 500; color: var(--green-dark); line-height: 1; margin-bottom: 2px; }
|
||||
.m-card-stat-label { font-size: 10px; color: var(--color-text-muted); margin-bottom: 8px; }
|
||||
.m-card-title { font-size: 14px; font-weight: 500; margin-bottom: 3px; }
|
||||
.m-card-desc { font-size: 11px; color: var(--color-text-muted); }
|
||||
.m-card-cta { margin-top: 12px; font-size: 11px; font-weight: 500; color: var(--green-dark); }
|
||||
.m-tabbar { display: flex; border-top: 1px solid var(--color-border); background: white; }
|
||||
.m-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 8px 4px 4px; font-size: 10px; color: var(--color-text-muted); gap: 2px; }
|
||||
.m-tab.active { color: var(--green-dark); }
|
||||
.m-tab-icon { font-size: 20px; }
|
||||
|
||||
/* ── Agent section ── */
|
||||
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
|
||||
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
|
||||
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
|
||||
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
|
||||
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
|
||||
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
|
||||
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
|
||||
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
|
||||
.agent-table tr:last-child td { border-bottom: none; }
|
||||
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
|
||||
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
|
||||
.agent-table td:nth-child(3) { color: #5A5A55; }
|
||||
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="doc">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
<div>
|
||||
<h1>E1 — Einstellungen</h1>
|
||||
<p>Kachel-Ansicht · Finale Spezifikation · Route: <code>/settings</code> → <code>/household/staples?ctx=settings</code></p>
|
||||
</div>
|
||||
<div class="doc-meta">
|
||||
screens: E1, D3<br/>
|
||||
journey: J8<br/>
|
||||
variation: Kachel (V2)<br/>
|
||||
version: 1.0<br/>
|
||||
date: 2026-04-09
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="intro">
|
||||
Die Einstellungsseite dient als Hub mit drei Kacheln: Vorräte (primäre Aktion, navigiert zu D3),
|
||||
Mitglieder (navigiert zu E2) und Profil. Die Vorräte-Kachel zeigt die aktive Zutatenanzahl als
|
||||
Display-Font-Zahl. D3 verwendet die bestehende StaplesManager-Komponente mit <code>context="settings"</code>.
|
||||
</p>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S1 — Hub-Ansicht (E1 /settings)</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S1</div>
|
||||
<div>
|
||||
<div class="state-title">Einstellungs-Hub — drei Kacheln</div>
|
||||
<div class="state-desc">Vorräte-Kachel (2fr, primär mit grünem Akzentstreifen), Mitglieder-Kachel (1fr), Profil-Kachel (1fr). Desktop 2-spaltig oben, dann 2-spaltig unten.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div>
|
||||
<div class="sidebar-household">Familie Raddatz</div>
|
||||
</div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Plan</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Einstellungen</div>
|
||||
<div class="page-subtitle">Familie Raddatz</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<!-- Vorräte card (2fr, primary) -->
|
||||
<a class="setting-card primary" href="#">
|
||||
<div class="card-icon">🥫</div>
|
||||
<div class="card-stat">14</div>
|
||||
<div class="card-stat-label">von 32 Zutaten als Vorrat markiert</div>
|
||||
<div class="card-title">Vorräte</div>
|
||||
<div class="card-desc">Lege fest, welche Zutaten immer zu Hause sind. Sie werden beim Einkaufen automatisch herausgefiltert.</div>
|
||||
<div class="card-cta">Vorräte bearbeiten →</div>
|
||||
</a>
|
||||
<!-- Mitglieder card (1fr) -->
|
||||
<a class="setting-card" href="#">
|
||||
<div class="card-icon">👥</div>
|
||||
<div class="card-title">Mitglieder</div>
|
||||
<div class="card-meta" style="margin-top:4px;">3 Mitglieder</div>
|
||||
<div class="card-desc" style="margin-top:8px;">Haushaltsmitglieder einladen, Rollen verwalten.</div>
|
||||
<div class="card-cta-secondary">Mitglieder verwalten →</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid-bottom">
|
||||
<!-- Profil card -->
|
||||
<a class="setting-card" href="#">
|
||||
<div class="card-icon">👤</div>
|
||||
<div class="card-title">Profil</div>
|
||||
<div class="card-meta" style="margin-top:4px;">Marcel R.</div>
|
||||
<div class="card-desc" style="margin-top:8px;">Name und E-Mail-Adresse anpassen.</div>
|
||||
<div class="card-cta-secondary">Profil bearbeiten →</div>
|
||||
</a>
|
||||
<!-- Placeholder / future -->
|
||||
<div style="border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 24px; display:flex; align-items:center; justify-content:center; color: var(--color-text-muted); font-size: 12px;">Weitere Einstellungen folgen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-m-wrap">
|
||||
<div class="preview-label">Mobile</div>
|
||||
<div class="preview-m-clip">
|
||||
<div class="preview-m-scale">
|
||||
<div class="m-shell" style="min-height:680px;">
|
||||
<div class="m-header"><div class="m-header-title">Einstellungen</div></div>
|
||||
<div class="m-content">
|
||||
<div class="m-card primary">
|
||||
<div class="m-card-stat">14</div>
|
||||
<div class="m-card-stat-label">von 32 Vorräten aktiv</div>
|
||||
<div class="m-card-title">Vorräte</div>
|
||||
<div class="m-card-desc">Welche Zutaten hast du immer zu Hause?</div>
|
||||
<div class="m-card-cta">Vorräte bearbeiten →</div>
|
||||
</div>
|
||||
<div class="m-card">
|
||||
<div class="m-card-title">👥 Mitglieder</div>
|
||||
<div class="m-card-desc" style="margin-top:4px;">3 Mitglieder · Einladen & Rollen</div>
|
||||
<div class="m-card-cta">Verwalten →</div>
|
||||
</div>
|
||||
<div class="m-card">
|
||||
<div class="m-card-title">👤 Profil</div>
|
||||
<div class="m-card-desc" style="margin-top:4px;">Marcel R.</div>
|
||||
<div class="m-card-cta">Bearbeiten →</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-tabbar">
|
||||
<div class="m-tab"><div class="m-tab-icon">📅</div>Planer</div>
|
||||
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
|
||||
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
|
||||
<div class="m-tab active"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Vorräte-Kachel: <code>grid-column: span 1</code> aber <code>2fr</code> Spaltenbreite im 2-Spalten-Grid. Grüner Linksstreifen (<code>border-left: 3px solid --green-dark</code>).</li>
|
||||
<li>Stat-Zahl: Anzahl Zutaten mit <code>isStaple === true</code>, aus dem gleichen Load-Call der D3-Seite</li>
|
||||
<li>Mitglieder-Karte: Anzahl aus <code>locals.haushalt</code> oder separatem API-Call; navigiert zu <code>/members</code></li>
|
||||
<li>Profil-Karte: Name aus <code>locals.benutzer.name</code>; Zielseite <code>/profile</code> (noch nicht implementiert — Link disabled oder Placeholder)</li>
|
||||
<li>Hover: <code>box-shadow: --shadow-raised</code>, leicht dunklerer Border</li>
|
||||
<li>Alle Kacheln sind <code><a></code>-Tags für korrekte Navigation und Accessibility</li>
|
||||
<li>Mobile: Kacheln stapeln sich vertikal in voller Breite, kein Grid</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S2 — Vorräte-Seite (D3 /household/staples?ctx=settings)</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S2</div>
|
||||
<div>
|
||||
<div class="state-title">D3 — Vorräte bearbeiten (StaplesManager, context="settings")</div>
|
||||
<div class="state-desc">Navigiert man von der Vorräte-Kachel aus, erscheint die bestehende StaplesManager-Komponente mit Breadcrumb zurück zu Einstellungen.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Plan</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumb">
|
||||
<a href="#">← Einstellungen</a>
|
||||
<span class="breadcrumb-sep">/</span>
|
||||
<span>Vorräte</span>
|
||||
</div>
|
||||
<div class="page-title">Vorräte</div>
|
||||
<div class="page-subtitle">Markierte Zutaten werden beim Einkaufen herausgefiltert.</div>
|
||||
|
||||
<!-- StaplesManager content (context="settings") -->
|
||||
<div class="category-block">
|
||||
<div class="category-name">Gewürze & Öle</div>
|
||||
<div class="chip-wrap">
|
||||
<span class="chip on">Salz</span>
|
||||
<span class="chip on">Pfeffer</span>
|
||||
<span class="chip on">Olivenöl</span>
|
||||
<span class="chip off">Paprika</span>
|
||||
<span class="chip off">Kreuzkümmel</span>
|
||||
<span class="chip on">Knoblauch</span>
|
||||
<span class="chip off">Chili</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-block">
|
||||
<div class="category-name">Grundnahrung</div>
|
||||
<div class="chip-wrap">
|
||||
<span class="chip on">Reis</span>
|
||||
<span class="chip off">Nudeln</span>
|
||||
<span class="chip on">Mehl</span>
|
||||
<span class="chip on">Zucker</span>
|
||||
<span class="chip off">Linsen</span>
|
||||
<span class="chip off">Hülsenfrüchte</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-block">
|
||||
<div class="category-name">Kühlschrank</div>
|
||||
<div class="chip-wrap">
|
||||
<span class="chip on">Butter</span>
|
||||
<span class="chip on">Eier</span>
|
||||
<span class="chip off">Milch</span>
|
||||
<span class="chip off">Käse</span>
|
||||
<span class="chip off">Joghurt</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-note">Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-m-wrap">
|
||||
<div class="preview-label">Mobile</div>
|
||||
<div class="preview-m-clip">
|
||||
<div class="preview-m-scale">
|
||||
<div class="m-shell" style="min-height:680px;">
|
||||
<div class="m-header">
|
||||
<div style="font-size:11px;color:var(--color-text-muted);margin-bottom:2px;">← Einstellungen</div>
|
||||
<div class="m-header-title">Vorräte</div>
|
||||
</div>
|
||||
<div class="m-content" style="gap:16px;">
|
||||
<div>
|
||||
<div class="category-name">Gewürze & Öle</div>
|
||||
<div class="chip-wrap">
|
||||
<span class="chip on" style="font-size:11px;">Salz</span>
|
||||
<span class="chip on" style="font-size:11px;">Pfeffer</span>
|
||||
<span class="chip on" style="font-size:11px;">Olivenöl</span>
|
||||
<span class="chip off" style="font-size:11px;">Paprika</span>
|
||||
<span class="chip on" style="font-size:11px;">Knoblauch</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="category-name">Grundnahrung</div>
|
||||
<div class="chip-wrap">
|
||||
<span class="chip on" style="font-size:11px;">Reis</span>
|
||||
<span class="chip off" style="font-size:11px;">Nudeln</span>
|
||||
<span class="chip on" style="font-size:11px;">Mehl</span>
|
||||
<span class="chip on" style="font-size:11px;">Zucker</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-tabbar">
|
||||
<div class="m-tab"><div class="m-tab-icon">📅</div>Planer</div>
|
||||
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
|
||||
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
|
||||
<div class="m-tab active"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Breadcrumb "← Einstellungen" navigiert zurück zu <code>/settings</code></li>
|
||||
<li>"Einstellungen" bleibt in der Sidebar aktiv (kein eigener Nav-Eintrag für Vorräte)</li>
|
||||
<li>StaplesManager-Komponente unverändert mit <code>context="settings"</code> (3-spaltig auf md+)</li>
|
||||
<li>Kein Speichern-Button. Hinweistext "Änderungen werden automatisch gespeichert." unter den Chips</li>
|
||||
<li>Mobile: Chips statt 3-spaltig 1-spaltig (volle Breite), Flex-Wrap bleibt bestehen</li>
|
||||
<li>D3 hat eigene <code>+page.server.ts</code> die <code>+page.svelte</code> bei <code>/household/staples</code> gibt es bereits</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S3 — Hover-Zustand der Kacheln</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S3</div>
|
||||
<div>
|
||||
<div class="state-title">Kachel-Hover — visuelles Feedback</div>
|
||||
<div class="state-desc">Alle Kacheln sind anklickbare Links. Hover hebt die Kachel visuell an.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop — Vorräte-Kachel im Hover</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Einstellungen</div>
|
||||
<div class="page-subtitle">Familie Raddatz</div>
|
||||
<div class="settings-grid">
|
||||
<!-- Hovered Vorräte card -->
|
||||
<a class="setting-card primary" href="#" style="box-shadow:var(--shadow-raised);border-color:#C0BFB8;cursor:pointer;">
|
||||
<div class="card-icon">🥫</div>
|
||||
<div class="card-stat">14</div>
|
||||
<div class="card-stat-label">von 32 Zutaten als Vorrat markiert</div>
|
||||
<div class="card-title">Vorräte</div>
|
||||
<div class="card-desc">Lege fest, welche Zutaten immer zu Hause sind.</div>
|
||||
<div class="card-cta">Vorräte bearbeiten →</div>
|
||||
</a>
|
||||
<a class="setting-card" href="#">
|
||||
<div class="card-icon">👥</div>
|
||||
<div class="card-title">Mitglieder</div>
|
||||
<div class="card-meta" style="margin-top:4px;">3 Mitglieder</div>
|
||||
<div class="card-desc" style="margin-top:8px;">Haushaltsmitglieder einladen, Rollen verwalten.</div>
|
||||
<div class="card-cta-secondary">Mitglieder verwalten →</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="settings-grid-bottom">
|
||||
<a class="setting-card" href="#"><div class="card-icon">👤</div><div class="card-title">Profil</div><div class="card-meta" style="margin-top:4px;">Marcel R.</div><div class="card-desc" style="margin-top:8px;">Name und E-Mail anpassen.</div><div class="card-cta-secondary">Profil bearbeiten →</div></a>
|
||||
<div style="border:1.5px dashed var(--color-border);border-radius:var(--radius-xl);padding:24px;display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);font-size:12px;">Weitere folgen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Hover: <code>box-shadow: --shadow-raised</code> + <code>border-color: #C0BFB8</code></li>
|
||||
<li>Vorräte-Kachel behält den grünen Linksstreifen auch im Hover</li>
|
||||
<li>Transition: <code>box-shadow 150ms ease, border-color 150ms ease</code></li>
|
||||
<li>Cursor: <code>pointer</code> auf allen Kacheln</li>
|
||||
<li>Focus-visible: <code>outline: 2px solid --green-dark; outline-offset: 2px</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S4 — Leerer Zustand (kein Vorrat gesetzt)</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S4</div>
|
||||
<div>
|
||||
<div class="state-title">Vorräte-Kachel bei 0 aktiven Vorräten</div>
|
||||
<div class="state-desc">Wenn noch kein Vorrat gesetzt wurde, zeigt die Kachel eine Einladung zur Aktion statt der Zahl.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop — 0 Vorräte</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Einstellungen</div>
|
||||
<div class="page-subtitle">Familie Raddatz</div>
|
||||
<div class="settings-grid">
|
||||
<a class="setting-card primary" href="#">
|
||||
<div class="card-icon">🥫</div>
|
||||
<!-- Empty state: no big number, instead prompt -->
|
||||
<div style="font-size:13px;color:var(--color-text-muted);margin-bottom:8px;">Noch keine Vorräte eingerichtet</div>
|
||||
<div class="card-title">Vorräte</div>
|
||||
<div class="card-desc">Lege fest, welche Zutaten immer zu Hause sind. Sie werden beim Einkaufen automatisch herausgefiltert.</div>
|
||||
<div class="card-cta">Jetzt einrichten →</div>
|
||||
</a>
|
||||
<a class="setting-card" href="#">
|
||||
<div class="card-icon">👥</div>
|
||||
<div class="card-title">Mitglieder</div>
|
||||
<div class="card-meta" style="margin-top:4px;">1 Mitglied</div>
|
||||
<div class="card-desc" style="margin-top:8px;">Haushaltsmitglieder einladen, Rollen verwalten.</div>
|
||||
<div class="card-cta-secondary">Mitglieder verwalten →</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="settings-grid-bottom">
|
||||
<a class="setting-card" href="#"><div class="card-icon">👤</div><div class="card-title">Profil</div><div class="card-meta" style="margin-top:4px;">Marcel R.</div><div class="card-cta-secondary" style="margin-top:8px;">Bearbeiten →</div></a>
|
||||
<div style="border:1.5px dashed var(--color-border);border-radius:var(--radius-xl);padding:24px;display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);font-size:12px;">Weitere folgen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Wenn <code>stapleCount === 0</code>: Stat-Zahl weglassen, stattdessen "Noch keine Vorräte eingerichtet" in muted</li>
|
||||
<li>CTA-Text ändert sich: "Jetzt einrichten →" statt "Vorräte bearbeiten →"</li>
|
||||
<li>Kachel navigiert weiterhin zu D3 — StaplesManager lädt immer, unabhängig vom Count</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Machine-readable agent section ─── -->
|
||||
<div class="agent-section">
|
||||
<h2>Maschinen-lesbare Spezifikation</h2>
|
||||
<p>Diese Sektion enthält verbindliche Implementierungsregeln für den Coding-Agenten.</p>
|
||||
|
||||
<pre class="spec-comment">
|
||||
/* spec:rules — E1 Einstellungen Kachel
|
||||
*
|
||||
* ROUTE: /settings (E1 hub)
|
||||
* DATA LOAD (page.server.ts):
|
||||
* - GET /v1/ingredient-categories to count stapleCount
|
||||
* stapleCount = sum of ingredients where isStaple === true
|
||||
* - member count available from layout data (locals.haushalt)
|
||||
* or fetch GET /v1/households/mine/members and count length
|
||||
* - profile name from locals.benutzer.name
|
||||
*
|
||||
* LAYOUT: E1 HUB
|
||||
* grid: 2 columns (2fr 1fr) top row + 2 columns (1fr 1fr) bottom row; gap 16px
|
||||
* mobile: single column, full-width cards, gap 12px
|
||||
*
|
||||
* CARD: all cards are <a> tags (href to target route)
|
||||
* border-radius: --radius-xl
|
||||
* border: 1px solid --color-border
|
||||
* bg: white
|
||||
* padding: 24px desktop / 16px mobile
|
||||
* hover: box-shadow --shadow-raised, border-color #C0BFB8
|
||||
* transition: box-shadow 150ms ease, border-color 150ms ease
|
||||
* cursor: pointer
|
||||
* focus-visible: outline 2px solid --green-dark, offset 2px
|
||||
*
|
||||
* VORRÄTE CARD (primary)
|
||||
* border-left: 3px solid --green-dark
|
||||
* stat number: font-family --font-display, font-size 36px, color --green-dark
|
||||
* stat label: "von {total} Zutaten als Vorrat markiert", 11px, --color-text-muted
|
||||
* empty state (stapleCount === 0): hide stat, show "Noch keine Vorräte eingerichtet"
|
||||
* cta: "Vorräte bearbeiten →" (empty: "Jetzt einrichten →")
|
||||
* href: /household/staples?ctx=settings
|
||||
*
|
||||
* MITGLIEDER CARD
|
||||
* meta: "{memberCount} Mitglieder"
|
||||
* href: /members
|
||||
*
|
||||
* PROFIL CARD
|
||||
* meta: locals.benutzer.name
|
||||
* href: /profile (not yet implemented — render as disabled or placeholder)
|
||||
*
|
||||
* ROUTE: /household/staples?ctx=settings (D3)
|
||||
* component: StaplesManager with context="settings" (already exists)
|
||||
* breadcrumb: "← Einstellungen" linking back to /settings
|
||||
* sidebar: "Einstellungen" stays active (no separate nav item for staples)
|
||||
* no save button — StaplesManager auto-saves via debounced PATCH 300ms
|
||||
* hint text below grid: "Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste."
|
||||
* grid: 3-col on md+ (context="settings" already sets this in StaplesManager)
|
||||
*
|
||||
* CHIP STYLES (for reference — rendered by StapleChip, do NOT reimplement)
|
||||
* selected: bg --green-dark, color white, border-color --green-dark
|
||||
* unselected: bg transparent, color --color-text-muted, border 1px solid --color-border
|
||||
* hover unselected: border-color --green-light, color --green-dark
|
||||
*
|
||||
* CATEGORY LABEL TYPOGRAPHY
|
||||
* font-size: 10px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase
|
||||
* color: --color-text-muted; margin-bottom: 10px
|
||||
*/
|
||||
</pre>
|
||||
|
||||
<table class="agent-table">
|
||||
<thead>
|
||||
<tr><th>Property</th><th>Value</th><th>Notes</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="group-row"><td colspan="3">E1 Hub Layout</td></tr>
|
||||
<tr><td>grid-desktop</td><td>2fr 1fr / 1fr 1fr</td><td>top row / bottom row</td></tr>
|
||||
<tr><td>grid-mobile</td><td>1fr</td><td>full-width stack</td></tr>
|
||||
<tr><td>gap</td><td>16px desktop / 12px mobile</td><td>—</td></tr>
|
||||
<tr class="group-row"><td colspan="3">Vorräte Card</td></tr>
|
||||
<tr><td>stat-font</td><td>--font-display, 36px, --green-dark</td><td>Fraunces</td></tr>
|
||||
<tr><td>accent-border</td><td>border-left: 3px solid --green-dark</td><td>primary indicator</td></tr>
|
||||
<tr><td>stat-source</td><td>count isStaple=true from /v1/ingredient-categories</td><td>load in page.server.ts</td></tr>
|
||||
<tr><td>empty-state</td><td>hide stat; show muted text</td><td>when stapleCount === 0</td></tr>
|
||||
<tr><td>href</td><td>/household/staples?ctx=settings</td><td>D3 route</td></tr>
|
||||
<tr class="group-row"><td colspan="3">D3 Staples Page</td></tr>
|
||||
<tr><td>component</td><td>StaplesManager context="settings"</td><td>existing, do not modify</td></tr>
|
||||
<tr><td>breadcrumb</td><td>← Einstellungen → /settings</td><td>above page title</td></tr>
|
||||
<tr><td>active-nav</td><td>Einstellungen in sidebar</td><td>not a separate nav entry</td></tr>
|
||||
<tr><td>save-hint</td><td>"Änderungen werden automatisch gespeichert."</td><td>below chip grid</td></tr>
|
||||
<tr><td>debounce</td><td>300ms (in StaplesManager)</td><td>do not add extra debounce</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
905
specs/frontend/e2-members-kachel.html
Normal file
905
specs/frontend/e2-members-kachel.html
Normal file
@@ -0,0 +1,905 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>E2 — Mitglieder · Kachel-Ansicht · Finale Spezifikation</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
||||
<!--
|
||||
spec:agent
|
||||
document: E2 Mitglieder – Kachel-Ansicht, Finale Spezifikation
|
||||
version: 1.0
|
||||
journey: J7 Manage household members
|
||||
route: /members
|
||||
screen: E2
|
||||
chosen-variation: V2 Kachel-Ansicht (Card grid)
|
||||
last-updated: 2026-04-09
|
||||
|
||||
BACKEND GAPS (must be implemented before this page can ship):
|
||||
- DELETE /v1/households/mine/members/{userId} → remove member
|
||||
- PATCH /v1/households/mine/members/{userId} → body: { role: "planer"|"mitglied" }
|
||||
- GET /v1/households/mine/invites → list active invites with expiry
|
||||
These endpoints do not exist in the current API schema (schema.d.ts).
|
||||
Existing: GET /v1/households/mine/members, POST /v1/households/mine/invites
|
||||
|
||||
ROLE ACCESS:
|
||||
- rolle === 'planer': sees kebab menu on all cards except own
|
||||
- rolle === 'mitglied': sees all cards read-only, no kebab, no invite card CTA
|
||||
-->
|
||||
<style>
|
||||
:root {
|
||||
--color-page: #FAFAF7;
|
||||
--color-surface: #F5F4EE;
|
||||
--color-subtle: #EDECEA;
|
||||
--color-border: #D8D7D0;
|
||||
--color-text-muted: #6B6A63;
|
||||
--color-text: #1C1C18;
|
||||
--green-tint: #E8F5EA;
|
||||
--green-light: #AEDCB0;
|
||||
--green: #3D8C4A;
|
||||
--green-dark: #2E6E39;
|
||||
--yellow-tint: #FDF6D8;
|
||||
--yellow-light: #F9E08A;
|
||||
--yellow-text: #8A6800;
|
||||
--color-error: #DC4C3E;
|
||||
--error-tint: #FDECEA;
|
||||
--blue-tint: #E6F1FB;
|
||||
--blue: #185FA5;
|
||||
--blue-dark: #0C447C;
|
||||
--font-display: 'Fraunces', Georgia, serif;
|
||||
--font-sans: 'DM Sans', system-ui, sans-serif;
|
||||
--font-mono: 'DM Mono', monospace;
|
||||
--radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px; --radius-xl: 16px; --radius-full: 9999px;
|
||||
--shadow-card: 0 1px 3px rgba(28,28,24,.06), 0 1px 2px rgba(28,28,24,.04);
|
||||
--shadow-raised: 0 4px 12px rgba(28,28,24,.10), 0 2px 4px rgba(28,28,24,.05);
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--font-sans); background: var(--color-page); color: var(--color-text); font-size: 14px; line-height: 1.6; }
|
||||
|
||||
/* ── Doc layout ── */
|
||||
.doc { max-width: 1040px; margin: 0 auto; padding: 48px 40px 96px; }
|
||||
.doc-header { padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 48px; display: flex; justify-content: space-between; align-items: flex-end; }
|
||||
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }
|
||||
.doc-header p { font-size: 13px; color: var(--color-text-muted); margin-top: 4px; }
|
||||
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
|
||||
.section-label { font-size: 10px; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); padding-bottom: 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 32px; margin-top: 56px; }
|
||||
.intro { font-size: 14px; line-height: 1.75; max-width: 640px; margin-bottom: 40px; }
|
||||
|
||||
/* ── State sections ── */
|
||||
.state { margin-bottom: 64px; }
|
||||
.state-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
|
||||
.state-id { font-family: var(--font-mono); font-size: 10px; font-weight: 500; background: var(--color-subtle); color: var(--color-text-muted); padding: 2px 8px; border-radius: var(--radius-sm); white-space: nowrap; margin-top: 3px; }
|
||||
.state-title { font-size: 16px; font-weight: 500; letter-spacing: -0.01em; }
|
||||
.state-desc { font-size: 13px; color: var(--color-text-muted); margin-top: 2px; max-width: 540px; }
|
||||
|
||||
/* ── Preview ── */
|
||||
.preview-wrap { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
|
||||
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
|
||||
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
|
||||
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
|
||||
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
|
||||
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
|
||||
|
||||
/* ── Notes ── */
|
||||
.notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 14px 18px; }
|
||||
.notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
|
||||
.notes ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
||||
.notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
|
||||
.notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
|
||||
.notes li.warn::before { content: '⚠'; color: var(--yellow-text); }
|
||||
.notes li.gap::before { content: '✗'; color: var(--color-error); }
|
||||
|
||||
/* ── Warning banner ── */
|
||||
.backend-warning { background: var(--yellow-tint); border: 1px solid var(--yellow-light); border-radius: var(--radius-lg); padding: 14px 18px; margin-bottom: 40px; }
|
||||
.backend-warning h3 { font-size: 12px; font-weight: 600; color: var(--yellow-text); margin-bottom: 6px; }
|
||||
.backend-warning ul { list-style: none; display: flex; flex-direction: column; gap: 3px; }
|
||||
.backend-warning li { font-family: var(--font-mono); font-size: 11px; color: var(--yellow-text); display: flex; gap: 8px; }
|
||||
.backend-warning li::before { content: '○'; }
|
||||
|
||||
/* ── AppShell chrome ── */
|
||||
.shell { display: flex; min-height: 100vh; background: var(--color-page); font-family: var(--font-sans); }
|
||||
.sidebar { width: 224px; min-width: 224px; background: white; border-right: 1px solid var(--color-border); display: flex; flex-direction: column; }
|
||||
.sidebar-brand { padding: 14px 18px; border-bottom: 1px solid var(--color-border); }
|
||||
.sidebar-brand-row { display: flex; align-items: center; gap: 8px; }
|
||||
.sidebar-logo { width: 22px; height: 22px; background: var(--green); border-radius: var(--radius-sm); }
|
||||
.sidebar-app { font-family: var(--font-display); font-size: 15px; font-weight: 500; }
|
||||
.sidebar-household { font-size: 10px; color: var(--color-text-muted); margin-top: 1px; }
|
||||
.sidebar-nav { flex: 1; padding: 4px 8px; }
|
||||
.sidebar-group-label { font-size: 8px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); padding: 16px 12px 4px; }
|
||||
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: var(--radius-md); font-size: 13px; color: var(--color-text); text-decoration: none; }
|
||||
.sidebar-item.active { background: var(--green-tint); color: var(--green-dark); font-weight: 500; }
|
||||
.sidebar-item:not(.active):hover { background: var(--color-subtle); }
|
||||
.sidebar-icon { width: 20px; text-align: center; font-size: 16px; }
|
||||
|
||||
/* ── Page content ── */
|
||||
.page-content { flex: 1; padding: 32px 40px; }
|
||||
.page-title { font-family: var(--font-display); font-size: 24px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
|
||||
.page-subtitle { font-size: 13px; color: var(--color-text-muted); margin-bottom: 28px; }
|
||||
|
||||
/* ── Member card grid ── */
|
||||
.member-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
||||
.member-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px 20px 20px; box-shadow: var(--shadow-card); position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||||
.member-card.hovered { box-shadow: var(--shadow-raised); border-color: #C0BFB8; }
|
||||
.member-card.own { border-color: var(--green-light); }
|
||||
|
||||
.avatar { width: 56px; height: 56px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 20px; font-weight: 500; color: white; margin-bottom: 12px; flex-shrink: 0; }
|
||||
.avatar-planer { background: var(--green-dark); }
|
||||
.avatar-mitglied { background: var(--blue); }
|
||||
|
||||
.member-name { font-size: 14px; font-weight: 500; margin-bottom: 6px; }
|
||||
.role-badge { font-size: 10px; font-weight: 500; letter-spacing: 0.04em; padding: 2px 8px; border-radius: var(--radius-full); white-space: nowrap; }
|
||||
.role-badge.planer { background: var(--green-tint); color: var(--green-dark); }
|
||||
.role-badge.mitglied { background: var(--blue-tint); color: var(--blue-dark); }
|
||||
.join-date { font-size: 11px; color: var(--color-text-muted); margin-top: 8px; }
|
||||
.self-badge { font-size: 10px; font-weight: 500; letter-spacing: 0.04em; padding: 2px 8px; border-radius: var(--radius-full); background: var(--green-tint); color: var(--green-dark); }
|
||||
|
||||
/* ── Kebab button ── */
|
||||
.kebab-btn { position: absolute; top: 12px; right: 12px; width: 28px; height: 28px; border-radius: var(--radius-md); background: transparent; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--color-text-muted); }
|
||||
.kebab-btn:hover, .kebab-btn.open { background: var(--color-subtle); color: var(--color-text); }
|
||||
|
||||
/* ── Dropdown menu ── */
|
||||
.dropdown { position: absolute; top: 44px; right: 12px; background: white; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-raised); min-width: 160px; z-index: 10; overflow: hidden; }
|
||||
.dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 13px; color: var(--color-text); cursor: pointer; white-space: nowrap; }
|
||||
.dropdown-item:hover { background: var(--color-subtle); }
|
||||
.dropdown-item.danger { color: var(--color-error); }
|
||||
.dropdown-item.danger:hover { background: var(--error-tint); }
|
||||
.dropdown-icon { font-size: 14px; width: 16px; text-align: center; }
|
||||
.dropdown-divider { height: 1px; background: var(--color-border); margin: 2px 0; }
|
||||
|
||||
/* ── Role segmented control (inline on card) ── */
|
||||
.role-control { display: flex; border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; margin-top: 8px; width: 100%; }
|
||||
.role-control-btn { flex: 1; padding: 6px 8px; font-size: 11px; font-weight: 500; background: white; border: none; cursor: pointer; color: var(--color-text-muted); }
|
||||
.role-control-btn.active { background: var(--green-dark); color: white; }
|
||||
.role-control-btn:first-child { border-right: 1px solid var(--color-border); }
|
||||
|
||||
/* ── Invite card ── */
|
||||
.invite-card { background: white; border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 24px 20px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; cursor: pointer; min-height: 180px; gap: 10px; }
|
||||
.invite-card:hover { border-color: var(--green-light); background: var(--green-tint); }
|
||||
.invite-plus { width: 44px; height: 44px; border-radius: var(--radius-full); background: var(--color-subtle); display: flex; align-items: center; justify-content: center; font-size: 22px; color: var(--color-text-muted); }
|
||||
.invite-card:hover .invite-plus { background: var(--green-light); color: var(--green-dark); }
|
||||
.invite-label { font-size: 13px; font-weight: 500; color: var(--color-text-muted); }
|
||||
.invite-card:hover .invite-label { color: var(--green-dark); }
|
||||
|
||||
/* ── Invite panel (expanded inline) ── */
|
||||
.invite-panel { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px; margin-top: 8px; }
|
||||
.invite-panel-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
|
||||
.invite-panel-desc { font-size: 12px; color: var(--color-text-muted); margin-bottom: 16px; }
|
||||
.invite-link-row { display: flex; gap: 8px; align-items: center; }
|
||||
.invite-link-box { flex: 1; background: var(--color-subtle); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 8px 12px; font-family: var(--font-mono); font-size: 12px; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.btn-copy { padding: 8px 14px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; }
|
||||
.btn-copy:hover { background: var(--color-subtle); }
|
||||
.invite-expiry { font-size: 11px; color: var(--color-text-muted); margin-top: 8px; }
|
||||
.invite-expiry span { background: var(--yellow-tint); color: var(--yellow-text); padding: 1px 6px; border-radius: var(--radius-sm); font-weight: 500; }
|
||||
.btn-regen { margin-top: 12px; font-size: 12px; color: var(--color-text-muted); background: none; border: none; cursor: pointer; text-decoration: underline; }
|
||||
.btn-regen:hover { color: var(--color-text); }
|
||||
|
||||
/* ── Dialog overlay ── */
|
||||
.overlay { position: absolute; inset: 0; background: rgba(28,28,24,.45); display: flex; align-items: center; justify-content: center; z-index: 50; }
|
||||
.dialog { background: white; border-radius: var(--radius-xl); padding: 28px 32px; max-width: 380px; width: 100%; box-shadow: var(--shadow-raised); }
|
||||
.dialog-title { font-size: 16px; font-weight: 500; margin-bottom: 8px; }
|
||||
.dialog-body { font-size: 13px; color: var(--color-text-muted); line-height: 1.6; margin-bottom: 24px; }
|
||||
.dialog-body strong { color: var(--color-text); font-weight: 500; }
|
||||
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.btn-cancel { padding: 9px 18px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 13px; font-weight: 500; cursor: pointer; }
|
||||
.btn-cancel:hover { background: var(--color-subtle); }
|
||||
.btn-remove { padding: 9px 18px; border-radius: var(--radius-md); border: none; background: var(--color-error); color: white; font-size: 13px; font-weight: 500; cursor: pointer; }
|
||||
.btn-remove:hover { background: #C43A2E; }
|
||||
|
||||
/* ── Mobile shell ── */
|
||||
.m-shell { display: flex; flex-direction: column; background: var(--color-page); }
|
||||
.m-header { padding: 16px; background: white; border-bottom: 1px solid var(--color-border); display: flex; align-items: center; justify-content: space-between; }
|
||||
.m-header-title { font-size: 16px; font-weight: 500; }
|
||||
.m-header-btn { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--green-dark); display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; border: none; cursor: pointer; }
|
||||
.m-content { flex: 1; padding: 16px; }
|
||||
.m-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.m-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 16px; display: flex; flex-direction: column; align-items: center; text-align: center; position: relative; box-shadow: var(--shadow-card); }
|
||||
.m-avatar { width: 44px; height: 44px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 16px; font-weight: 500; color: white; margin-bottom: 8px; }
|
||||
.m-avatar.planer { background: var(--green-dark); }
|
||||
.m-avatar.mitglied { background: var(--blue); }
|
||||
.m-name { font-size: 12px; font-weight: 500; margin-bottom: 4px; }
|
||||
.m-role { font-size: 10px; font-weight: 500; padding: 2px 6px; border-radius: var(--radius-full); }
|
||||
.m-role.planer { background: var(--green-tint); color: var(--green-dark); }
|
||||
.m-role.mitglied { background: var(--blue-tint); color: var(--blue-dark); }
|
||||
.m-kebab { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--color-text-muted); background: none; border: none; }
|
||||
.m-invite-card { background: white; border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 120px; gap: 6px; }
|
||||
.m-invite-plus { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--color-subtle); display: flex; align-items: center; justify-content: center; font-size: 18px; color: var(--color-text-muted); }
|
||||
.m-invite-label { font-size: 11px; color: var(--color-text-muted); font-weight: 500; }
|
||||
.m-tabbar { display: flex; border-top: 1px solid var(--color-border); background: white; }
|
||||
.m-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 8px 4px 4px; font-size: 10px; color: var(--color-text-muted); gap: 2px; }
|
||||
.m-tab.active { color: var(--green-dark); }
|
||||
.m-tab-icon { font-size: 20px; }
|
||||
|
||||
/* ── Agent section ── */
|
||||
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
|
||||
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
|
||||
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
|
||||
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
|
||||
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
|
||||
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
|
||||
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
|
||||
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
|
||||
.agent-table tr:last-child td { border-bottom: none; }
|
||||
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
|
||||
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
|
||||
.agent-table td:nth-child(3) { color: #5A5A55; }
|
||||
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="doc">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
<div>
|
||||
<h1>E2 — Mitglieder</h1>
|
||||
<p>Kachel-Ansicht · Finale Spezifikation · Route: <code>/members</code></p>
|
||||
</div>
|
||||
<div class="doc-meta">
|
||||
screen: E2<br/>
|
||||
journey: J7<br/>
|
||||
variation: Kachel (V2)<br/>
|
||||
version: 1.0<br/>
|
||||
date: 2026-04-09
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="intro">
|
||||
Die Mitgliederseite zeigt alle Haushaltsmitglieder als Kacheln. Der Planer kann Rollen ändern und Mitglieder
|
||||
entfernen über ein Kebab-Menü auf jeder Kachel. Eine Einladekachel ermöglicht das Generieren und Kopieren des
|
||||
Einlade-Links. Mitglieder sehen alle Kacheln nur lesend.
|
||||
</p>
|
||||
|
||||
<div class="backend-warning">
|
||||
<h3>Backend-Lücken — vor Implementierung schließen</h3>
|
||||
<ul>
|
||||
<li>DELETE /v1/households/mine/members/{userId} — Mitglied entfernen</li>
|
||||
<li>PATCH /v1/households/mine/members/{userId} — Rolle ändern (body: { role })</li>
|
||||
<li>GET /v1/households/mine/invites — aktive Einladungen auflisten (inkl. expiresAt)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S1 — Standardansicht (Planer)</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S1</div>
|
||||
<div>
|
||||
<div class="state-title">Standardansicht — Planer sieht vollständige Kacheln</div>
|
||||
<div class="state-desc">Alle Mitglieder als Kacheln, dahinter die Einladekachel. Kebab-Button erscheint on hover.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-row">
|
||||
<div class="sidebar-logo"></div>
|
||||
<span class="sidebar-app">Mealplan</span>
|
||||
</div>
|
||||
<div class="sidebar-household">Familie Raddatz</div>
|
||||
</div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Plan</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Mitglieder</div>
|
||||
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
|
||||
<div class="member-grid">
|
||||
<!-- Own card -->
|
||||
<div class="member-card own">
|
||||
<div class="avatar avatar-planer">MR</div>
|
||||
<div class="member-name">Marcel R.</div>
|
||||
<span class="role-badge planer">Planer</span>
|
||||
<div class="join-date">seit 02.04.2026</div>
|
||||
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
|
||||
</div>
|
||||
<!-- Member 2 -->
|
||||
<div class="member-card">
|
||||
<button class="kebab-btn">⋯</button>
|
||||
<div class="avatar avatar-mitglied">SR</div>
|
||||
<div class="member-name">Sandra R.</div>
|
||||
<span class="role-badge mitglied">Mitglied</span>
|
||||
<div class="join-date">seit 03.04.2026</div>
|
||||
</div>
|
||||
<!-- Member 3 -->
|
||||
<div class="member-card">
|
||||
<button class="kebab-btn">⋯</button>
|
||||
<div class="avatar avatar-mitglied">LR</div>
|
||||
<div class="member-name">Lena R.</div>
|
||||
<span class="role-badge mitglied">Mitglied</span>
|
||||
<div class="join-date">seit 05.04.2026</div>
|
||||
</div>
|
||||
<!-- Invite card -->
|
||||
<div class="invite-card">
|
||||
<div class="invite-plus">+</div>
|
||||
<div class="invite-label">Mitglied einladen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-m-wrap">
|
||||
<div class="preview-label">Mobile</div>
|
||||
<div class="preview-m-clip">
|
||||
<div class="preview-m-scale">
|
||||
<div class="m-shell" style="min-height:680px;">
|
||||
<div class="m-header">
|
||||
<span class="m-header-title">Mitglieder</span>
|
||||
<button class="m-header-btn">+</button>
|
||||
</div>
|
||||
<div class="m-content">
|
||||
<div class="m-grid">
|
||||
<div class="m-card" style="border-color:var(--green-light);">
|
||||
<div class="m-avatar planer">MR</div>
|
||||
<div class="m-name">Marcel R.</div>
|
||||
<span class="m-role planer">Planer</span>
|
||||
<div style="margin-top:6px;font-size:10px;color:var(--color-text-muted);">Du</div>
|
||||
</div>
|
||||
<div class="m-card">
|
||||
<button class="m-kebab">⋯</button>
|
||||
<div class="m-avatar mitglied">SR</div>
|
||||
<div class="m-name">Sandra R.</div>
|
||||
<span class="m-role mitglied">Mitglied</span>
|
||||
</div>
|
||||
<div class="m-card">
|
||||
<button class="m-kebab">⋯</button>
|
||||
<div class="m-avatar mitglied">LR</div>
|
||||
<div class="m-name">Lena R.</div>
|
||||
<span class="m-role mitglied">Mitglied</span>
|
||||
</div>
|
||||
<div class="m-invite-card">
|
||||
<div class="m-invite-plus">+</div>
|
||||
<div class="m-invite-label">Einladen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-tabbar">
|
||||
<div class="m-tab"><div class="m-tab-icon">📅</div>Planer</div>
|
||||
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
|
||||
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
|
||||
<div class="m-tab active"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Eigene Kachel (Du): grüner Kartenrahmen (<code>border: var(--green-light)</code>), "Du"-Badge statt Kebab</li>
|
||||
<li>Kebab-Button (<code>⋯</code>): immer im DOM, <code>opacity:0</code> bis hover/focus, dann <code>opacity:1</code>. Auf Touch-Geräten immer sichtbar.</li>
|
||||
<li>Avatar-Initialen: erste zwei Buchstaben des displayName. Planer = green-dark, Mitglied = blue</li>
|
||||
<li>Kachel-Reihenfolge: eigene Kachel immer zuerst, dann joinedAt aufsteigend, Einladekachel immer zuletzt</li>
|
||||
<li>Mobile: "+" Button in der Header-Zeile öffnet Einlade-Panel. Einladekachel bleibt zusätzlich im Grid.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S2 — Kebab-Menü offen</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S2</div>
|
||||
<div>
|
||||
<div class="state-title">Kebab-Menü geöffnet</div>
|
||||
<div class="state-desc">Klick auf ⋯ öffnet Dropdown mit zwei Aktionen. Klick außerhalb schließt das Menü.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop — Menü offen auf "Sandra R."</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar" style="width:224px;min-width:224px;">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Plan</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Mitglieder</div>
|
||||
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
|
||||
<div class="member-grid">
|
||||
<div class="member-card own">
|
||||
<div class="avatar avatar-planer">MR</div>
|
||||
<div class="member-name">Marcel R.</div>
|
||||
<span class="role-badge planer">Planer</span>
|
||||
<div class="join-date">seit 02.04.2026</div>
|
||||
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
|
||||
</div>
|
||||
<!-- Card with open menu -->
|
||||
<div class="member-card hovered" style="z-index:20;">
|
||||
<button class="kebab-btn open">⋯</button>
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-item"><span class="dropdown-icon">🔄</span>Rolle ändern</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item danger"><span class="dropdown-icon">✕</span>Entfernen</div>
|
||||
</div>
|
||||
<div class="avatar avatar-mitglied">SR</div>
|
||||
<div class="member-name">Sandra R.</div>
|
||||
<span class="role-badge mitglied">Mitglied</span>
|
||||
<div class="join-date">seit 03.04.2026</div>
|
||||
</div>
|
||||
<div class="member-card">
|
||||
<button class="kebab-btn">⋯</button>
|
||||
<div class="avatar avatar-mitglied">LR</div>
|
||||
<div class="member-name">Lena R.</div>
|
||||
<span class="role-badge mitglied">Mitglied</span>
|
||||
<div class="join-date">seit 05.04.2026</div>
|
||||
</div>
|
||||
<div class="invite-card">
|
||||
<div class="invite-plus">+</div>
|
||||
<div class="invite-label">Mitglied einladen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- No mobile preview needed for this state; same as desktop but full-screen -->
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Dropdown: <code>position: absolute; top: 44px; right: 12px</code> relativ zur Kachel</li>
|
||||
<li>Zwei Einträge: "Rolle ändern" (neutrales Icon 🔄) und "Entfernen" (rot, Icon ✕)</li>
|
||||
<li>Klick außerhalb des Dropdowns schließt diesen (click-away listener)</li>
|
||||
<li>Nur ein Menü gleichzeitig offen. ESC schließt ebenfalls.</li>
|
||||
<li>Mobile: Tap auf ⋯ öffnet Bottom Sheet mit denselben zwei Einträgen (44px min-height pro Eintrag)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S3 — Rolle ändern (inline auf der Kachel)</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S3</div>
|
||||
<div>
|
||||
<div class="state-title">Rolle ändern — Segmented Control erscheint</div>
|
||||
<div class="state-desc">Wahl von "Rolle ändern" ersetzt das Rolle-Badge durch einen 2-Button-Schalter [Planer | Mitglied]. Aktive Rolle vorausgewählt. Bestätigung sofort mit PATCH-Request.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop — Rolle-Control auf "Sandra R." aktiv</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar" style="width:224px;min-width:224px;">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Mitglieder</div>
|
||||
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
|
||||
<div class="member-grid">
|
||||
<div class="member-card own">
|
||||
<div class="avatar avatar-planer">MR</div>
|
||||
<div class="member-name">Marcel R.</div>
|
||||
<span class="role-badge planer">Planer</span>
|
||||
<div class="join-date">seit 02.04.2026</div>
|
||||
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
|
||||
</div>
|
||||
<!-- Card in role-edit mode -->
|
||||
<div class="member-card" style="border-color:#B5D4F4;">
|
||||
<div class="avatar avatar-mitglied">SR</div>
|
||||
<div class="member-name">Sandra R.</div>
|
||||
<!-- Role control replaces badge -->
|
||||
<div class="role-control" style="width:100%;">
|
||||
<button class="role-control-btn">Planer</button>
|
||||
<button class="role-control-btn active">Mitglied</button>
|
||||
</div>
|
||||
<div class="join-date">seit 03.04.2026</div>
|
||||
<button style="margin-top:8px;font-size:11px;color:var(--color-text-muted);background:none;border:none;cursor:pointer;text-decoration:underline;">Abbrechen</button>
|
||||
</div>
|
||||
<div class="member-card">
|
||||
<button class="kebab-btn">⋯</button>
|
||||
<div class="avatar avatar-mitglied">LR</div>
|
||||
<div class="member-name">Lena R.</div>
|
||||
<span class="role-badge mitglied">Mitglied</span>
|
||||
<div class="join-date">seit 05.04.2026</div>
|
||||
</div>
|
||||
<div class="invite-card">
|
||||
<div class="invite-plus">+</div>
|
||||
<div class="invite-label">Mitglied einladen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Role-Control ersetzt das Badge in-place auf der Kachel. Kein Dialog, kein Page-Change.</li>
|
||||
<li>Klick auf die inaktive Rolle → optimistisches Update → PATCH /v1/households/mine/members/{userId} { role }</li>
|
||||
<li>Bei Erfolg: Role-Control durch neues Badge ersetzen</li>
|
||||
<li>Bei Fehler: Rollback + Toast "Rolle konnte nicht geändert werden."</li>
|
||||
<li>"Abbrechen" bringt ohne PATCH-Call das Badge zurück</li>
|
||||
<li>Der Planer kann seinen eigenen Planer-Status nicht abgeben, solange er der einzige Planer ist</li>
|
||||
<li>Kachel bekommt blauen Rahmen (<code>border-color: #B5D4F4</code>) als Editier-Indikator</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S4 — Entfernen-Bestätigung (Dialog)</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S4</div>
|
||||
<div>
|
||||
<div class="state-title">Bestätigungsdialog "Mitglied entfernen"</div>
|
||||
<div class="state-desc">Klick auf "Entfernen" im Dropdown öffnet einen modalen Dialog. Kein direktes Löschen ohne Bestätigung.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop — Dialog über der Seite</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale" style="position:relative;">
|
||||
<div class="shell" style="position:relative;">
|
||||
<div class="sidebar" style="width:224px;min-width:224px;">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content" style="opacity:0.4;pointer-events:none;">
|
||||
<div class="page-title">Mitglieder</div>
|
||||
<div class="member-grid">
|
||||
<div class="member-card own"><div class="avatar avatar-planer">MR</div><div class="member-name">Marcel R.</div><span class="role-badge planer">Planer</span></div>
|
||||
<div class="member-card"><div class="avatar avatar-mitglied">SR</div><div class="member-name">Sandra R.</div><span class="role-badge mitglied">Mitglied</span></div>
|
||||
<div class="member-card"><div class="avatar avatar-mitglied">LR</div><div class="member-name">Lena R.</div><span class="role-badge mitglied">Mitglied</span></div>
|
||||
<div class="invite-card"><div class="invite-plus">+</div><div class="invite-label">Mitglied einladen</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Dialog -->
|
||||
<div class="overlay" style="position:absolute;">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">Mitglied entfernen?</div>
|
||||
<div class="dialog-body"><strong>Sandra R.</strong> wird aus dem Haushalt entfernt und verliert sofort den Zugang zu allen Plänen und Rezepten.</div>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn-cancel">Abbrechen</button>
|
||||
<button class="btn-remove">Entfernen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-m-wrap">
|
||||
<div class="preview-label">Mobile</div>
|
||||
<div class="preview-m-clip">
|
||||
<div class="preview-m-scale">
|
||||
<div class="m-shell" style="min-height:680px;position:relative;">
|
||||
<div class="m-header"><span class="m-header-title">Mitglieder</span><button class="m-header-btn">+</button></div>
|
||||
<div class="m-content" style="opacity:0.35;pointer-events:none;">
|
||||
<div class="m-grid">
|
||||
<div class="m-card"><div class="m-avatar planer">MR</div><div class="m-name">Marcel R.</div><span class="m-role planer">Planer</span></div>
|
||||
<div class="m-card"><div class="m-avatar mitglied">SR</div><div class="m-name">Sandra R.</div><span class="m-role mitglied">Mitglied</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile dialog -->
|
||||
<div class="overlay" style="position:absolute;align-items:flex-end;padding-bottom:0;">
|
||||
<div class="dialog" style="border-radius:var(--radius-xl) var(--radius-xl) 0 0;max-width:100%;padding:24px 24px 32px;">
|
||||
<div class="dialog-title" style="font-size:15px;">Mitglied entfernen?</div>
|
||||
<div class="dialog-body" style="font-size:12px;"><strong>Sandra R.</strong> wird aus dem Haushalt entfernt.</div>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn-cancel" style="font-size:12px;">Abbrechen</button>
|
||||
<button class="btn-remove" style="font-size:12px;">Entfernen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Dialog zeigt den <strong>displayName</strong> des Mitglieds explizit</li>
|
||||
<li>Bestätigung → DELETE /v1/households/mine/members/{userId} → Kachel aus Grid entfernen</li>
|
||||
<li>Planer kann sich nicht selbst entfernen (eigene Kachel hat kein Kebab-Menü)</li>
|
||||
<li>Letzter verbleibender Planer kann nicht entfernt werden → Fehlermeldung im Dialog</li>
|
||||
<li>Mobile: Dialog als Bottom Sheet (<code>border-radius</code> nur oben, kein max-width)</li>
|
||||
<li>Hintergrund leicht gedimmt: <code>rgba(28,28,24,.45)</code>, Klick außerhalb schließt nicht (explizite Bestätigung erforderlich)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S5 — Einladekachel: Einlade-Panel</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S5</div>
|
||||
<div>
|
||||
<div class="state-title">Einlade-Panel — nach Klick auf die Einladekachel</div>
|
||||
<div class="state-desc">Kachel expandiert zum Panel unterhalb der Grid-Reihe. Zeigt generierten Link + Ablaufdatum + Regenerieren-Option.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar" style="width:224px;min-width:224px;">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Mitglieder</div>
|
||||
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
|
||||
<div class="member-grid">
|
||||
<div class="member-card own"><div class="avatar avatar-planer">MR</div><div class="member-name">Marcel R.</div><span class="role-badge planer">Planer</span><div class="join-date">seit 02.04.2026</div><div style="margin-top:8px;"><span class="self-badge">Du</span></div></div>
|
||||
<div class="member-card"><button class="kebab-btn">⋯</button><div class="avatar avatar-mitglied">SR</div><div class="member-name">Sandra R.</div><span class="role-badge mitglied">Mitglied</span><div class="join-date">seit 03.04.2026</div></div>
|
||||
<div class="member-card"><button class="kebab-btn">⋯</button><div class="avatar avatar-mitglied">LR</div><div class="member-name">Lena R.</div><span class="role-badge mitglied">Mitglied</span><div class="join-date">seit 05.04.2026</div></div>
|
||||
<div class="invite-card" style="border-color:var(--green-light);background:var(--green-tint);">
|
||||
<div class="invite-plus" style="background:var(--green-light);color:var(--green-dark);">+</div>
|
||||
<div class="invite-label" style="color:var(--green-dark);">Mitglied einladen</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Invite panel below grid -->
|
||||
<div class="invite-panel" style="margin-top:16px;">
|
||||
<div class="invite-panel-title">Einladelink teilen</div>
|
||||
<div class="invite-panel-desc">Wer diesen Link öffnet, kann dem Haushalt als Mitglied beitreten.</div>
|
||||
<div class="invite-link-row">
|
||||
<div class="invite-link-box">https://mealplan.app/join/X4K9-RZMQ</div>
|
||||
<button class="btn-copy">Kopieren</button>
|
||||
</div>
|
||||
<div class="invite-expiry">Läuft ab: <span>12.04.2026</span></div>
|
||||
<button class="btn-regen">Neuen Link generieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Klick auf Einladekachel → POST /v1/households/mine/invites (falls kein aktiver Code vorhanden) oder GET /v1/households/mine/invites</li>
|
||||
<li>Invite-Panel erscheint unterhalb der Grid-Reihe (kein Modal, kein Page-Change)</li>
|
||||
<li>"Kopieren" → navigator.clipboard.writeText(shareUrl) → Button zeigt kurz "Kopiert ✓"</li>
|
||||
<li>"Neuen Link generieren" → POST /v1/households/mine/invites → alten Code invalidieren → neuen Code anzeigen</li>
|
||||
<li>Ablaufdatum <code>expiresAt</code> in gelbem Badge wenn ≤ 24h verbleibend</li>
|
||||
<li>Nur Planer sehen den Einlade-CTA. Mitglied sieht keine Einladekachel.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<div class="section-label">S6 — Mitglied-Perspektive (read-only)</div>
|
||||
|
||||
<div class="state">
|
||||
<div class="state-header">
|
||||
<div class="state-id">S6</div>
|
||||
<div>
|
||||
<div class="state-title">Ansicht als Haushaltsmitglied (rolle = mitglied)</div>
|
||||
<div class="state-desc">Mitglieder sehen die Kacheln ohne Kebab-Menü und ohne Einladekachel.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div class="preview-d-wrap">
|
||||
<div class="preview-label">Desktop — Mitglied-Perspektive</div>
|
||||
<div class="preview-d-clip">
|
||||
<div class="preview-d-scale">
|
||||
<div class="shell">
|
||||
<div class="sidebar" style="width:224px;min-width:224px;">
|
||||
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-group-label">Plan</div>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
|
||||
<div class="sidebar-group-label">Haushalt</div>
|
||||
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
|
||||
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-title">Mitglieder</div>
|
||||
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
|
||||
<div class="member-grid" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="member-card own">
|
||||
<div class="avatar avatar-mitglied">SR</div>
|
||||
<div class="member-name">Sandra R.</div>
|
||||
<span class="role-badge mitglied">Mitglied</span>
|
||||
<div class="join-date">seit 03.04.2026</div>
|
||||
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
|
||||
</div>
|
||||
<div class="member-card">
|
||||
<div class="avatar avatar-planer">MR</div>
|
||||
<div class="member-name">Marcel R.</div>
|
||||
<span class="role-badge planer">Planer</span>
|
||||
<div class="join-date">seit 02.04.2026</div>
|
||||
</div>
|
||||
<div class="member-card">
|
||||
<div class="avatar avatar-mitglied">LR</div>
|
||||
<div class="member-name">Lena R.</div>
|
||||
<span class="role-badge mitglied">Mitglied</span>
|
||||
<div class="join-date">seit 05.04.2026</div>
|
||||
</div>
|
||||
<!-- No invite card for members -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="notes-label">Notizen</div>
|
||||
<ul>
|
||||
<li>Mitglied sieht keine Einladekachel und keine Kebab-Buttons auf anderen Kacheln</li>
|
||||
<li>Eigene Kachel zeigt "Du"-Badge (grüner Rahmen), aber kein Kebab</li>
|
||||
<li>Grid passt sich an: bei 3 Kacheln → <code>grid-template-columns: repeat(3, 1fr)</code> (kein leerer Slot für Einladen)</li>
|
||||
<li>Server-seitige Prüfung: Aktionen (DELETE, PATCH) geben 403 für nicht-Planer zurück</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- ─── Machine-readable agent section ─── -->
|
||||
<div class="agent-section">
|
||||
<h2>Maschinen-lesbare Spezifikation</h2>
|
||||
<p>Diese Sektion enthält verbindliche Implementierungsregeln für den Coding-Agenten.</p>
|
||||
|
||||
<pre class="spec-comment">
|
||||
/* spec:rules — E2 Mitglieder Kachel
|
||||
*
|
||||
* LAYOUT
|
||||
* grid: repeat(4, 1fr) gap 16px desktop; repeat(2, 1fr) gap 12px mobile
|
||||
* card: bg white, border 1px solid --color-border, border-radius --radius-xl
|
||||
* card padding: 24px 20px 20px desktop; 16px mobile
|
||||
*
|
||||
* AVATAR
|
||||
* size: 56px desktop / 44px mobile; border-radius 50%
|
||||
* initials: first two chars of displayName, uppercase
|
||||
* planer color: --green-dark (#2E6E39)
|
||||
* mitglied color: --blue (#185FA5)
|
||||
*
|
||||
* ROLE BADGE
|
||||
* planer: bg --green-tint, color --green-dark
|
||||
* mitglied: bg --blue-tint, color --blue-dark
|
||||
* font-size 10px, font-weight 500, padding 2px 8px, border-radius --radius-full
|
||||
*
|
||||
* OWN CARD (benutzer.id === member.userId)
|
||||
* border-color: --green-light
|
||||
* show "Du" badge below join-date
|
||||
* hide kebab button entirely
|
||||
*
|
||||
* KEBAB BUTTON
|
||||
* position absolute, top 12px, right 12px
|
||||
* opacity 0 by default; 1 on card:hover, card:focus-within, touch devices always 1
|
||||
* opens dropdown: [Rolle ändern, divider, Entfernen(danger)]
|
||||
* click-away closes; ESC closes
|
||||
*
|
||||
* ROLE CHANGE (S3)
|
||||
* replaces badge in-place with segmented control [Planer | Mitglied]
|
||||
* active button: bg --green-dark, color white
|
||||
* inactive button: bg white, color --color-text-muted
|
||||
* on select: PATCH /v1/households/mine/members/{userId} body { role }
|
||||
* optimistic update; on error: rollback + toast
|
||||
* Abbrechen link below control: reverts to badge without API call
|
||||
* guard: planer cannot demote self if sole planer
|
||||
*
|
||||
* REMOVE CONFIRM (S4)
|
||||
* modal dialog, backdrop rgba(28,28,24,.45), backdrop does NOT close on click
|
||||
* shows member displayName in body text
|
||||
* confirm → DELETE /v1/households/mine/members/{userId}
|
||||
* on success: remove card from grid with fade-out
|
||||
* mobile: bottom-sheet (border-radius top only)
|
||||
*
|
||||
* INVITE (S5)
|
||||
* invite card always last in grid, only visible to planer
|
||||
* click → POST /v1/households/mine/invites OR GET /v1/households/mine/invites
|
||||
* panel below grid (not modal)
|
||||
* copy: navigator.clipboard.writeText(shareUrl) → button text "Kopiert ✓" for 2s
|
||||
* regenerate: POST new invite → invalidate old
|
||||
* expiresAt badge yellow if ≤ 24h remaining
|
||||
*
|
||||
* MEMBER VIEW (S6)
|
||||
* rolle === 'mitglied': hide all kebab buttons, hide invite card
|
||||
* grid auto-adjusts columns (no empty slot)
|
||||
*
|
||||
* CARD ORDER
|
||||
* 1. own card (benutzer.id === userId)
|
||||
* 2. other members sorted by joinedAt ASC
|
||||
* 3. invite card (planer only)
|
||||
*
|
||||
* BACKEND GAPS (must exist before page ships)
|
||||
* DELETE /v1/households/mine/members/{userId}
|
||||
* PATCH /v1/households/mine/members/{userId} body: { role: "planer"|"mitglied" }
|
||||
* GET /v1/households/mine/invites
|
||||
*/
|
||||
</pre>
|
||||
|
||||
<table class="agent-table">
|
||||
<thead>
|
||||
<tr><th>Property</th><th>Value</th><th>Notes</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="group-row"><td colspan="3">Component: MemberCard</td></tr>
|
||||
<tr><td>card-width</td><td>1fr (grid)</td><td>4-col desktop, 2-col mobile</td></tr>
|
||||
<tr><td>card-min-height</td><td>180px</td><td>desktop; auto mobile</td></tr>
|
||||
<tr><td>avatar-size</td><td>56px / 44px</td><td>desktop / mobile</td></tr>
|
||||
<tr><td>avatar-radius</td><td>50%</td><td>full circle</td></tr>
|
||||
<tr><td>kebab-target</td><td>44×44px</td><td>WCAG 2.2 minimum touch target</td></tr>
|
||||
<tr><td>dropdown-min-width</td><td>160px</td><td>right-aligned to kebab</td></tr>
|
||||
<tr class="group-row"><td colspan="3">Role Control</td></tr>
|
||||
<tr><td>control-height</td><td>32px</td><td>segmented, full card width</td></tr>
|
||||
<tr><td>active-bg</td><td>--green-dark</td><td>selected role button</td></tr>
|
||||
<tr><td>api-endpoint</td><td>PATCH /v1/households/mine/members/{userId}</td><td>body: { role }</td></tr>
|
||||
<tr class="group-row"><td colspan="3">Remove Dialog</td></tr>
|
||||
<tr><td>confirm-btn-bg</td><td>--color-error (#DC4C3E)</td><td>danger action</td></tr>
|
||||
<tr><td>api-endpoint</td><td>DELETE /v1/households/mine/members/{userId}</td><td>—</td></tr>
|
||||
<tr><td>backdrop</td><td>rgba(28,28,24,.45)</td><td>click-outside does NOT close</td></tr>
|
||||
<tr class="group-row"><td colspan="3">Invite</td></tr>
|
||||
<tr><td>api-create</td><td>POST /v1/households/mine/invites</td><td>returns InviteResponse</td></tr>
|
||||
<tr><td>api-list</td><td>GET /v1/households/mine/invites</td><td>backend gap</td></tr>
|
||||
<tr><td>copy-feedback</td><td>"Kopiert ✓" for 2000ms</td><td>then revert to "Kopieren"</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user