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>
906 lines
54 KiB
HTML
906 lines
54 KiB
HTML
<!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>
|