diff --git a/specs/frontend/e1-settings-kachel.html b/specs/frontend/e1-settings-kachel.html new file mode 100644 index 0000000..e7aea6a --- /dev/null +++ b/specs/frontend/e1-settings-kachel.html @@ -0,0 +1,700 @@ + + +
+ + +Kachel-Ansicht · Finale Spezifikation · Route: /settings → /household/staples?ctx=settings
+ 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 context="settings".
+
grid-column: span 1 aber 2fr Spaltenbreite im 2-Spalten-Grid. Grüner Linksstreifen (border-left: 3px solid --green-dark).isStaple === true, aus dem gleichen Load-Call der D3-Seitelocals.haushalt oder separatem API-Call; navigiert zu /memberslocals.benutzer.name; Zielseite /profile (noch nicht implementiert — Link disabled oder Placeholder)box-shadow: --shadow-raised, leicht dunklerer Border<a>-Tags für korrekte Navigation und Accessibility/settingscontext="settings" (3-spaltig auf md+)+page.server.ts die +page.svelte bei /household/staples gibt es bereitsbox-shadow: --shadow-raised + border-color: #C0BFB8box-shadow 150ms ease, border-color 150ms easepointer auf allen Kachelnoutline: 2px solid --green-dark; outline-offset: 2pxstapleCount === 0: Stat-Zahl weglassen, stattdessen "Noch keine Vorräte eingerichtet" in mutedDiese Sektion enthält verbindliche Implementierungsregeln für den Coding-Agenten.
+ ++/* 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 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 + */ ++ +
| Property | Value | Notes |
|---|---|---|
| E1 Hub Layout | ||
| grid-desktop | 2fr 1fr / 1fr 1fr | top row / bottom row |
| grid-mobile | 1fr | full-width stack |
| gap | 16px desktop / 12px mobile | — |
| Vorräte Card | ||
| stat-font | --font-display, 36px, --green-dark | Fraunces |
| accent-border | border-left: 3px solid --green-dark | primary indicator |
| stat-source | count isStaple=true from /v1/ingredient-categories | load in page.server.ts |
| empty-state | hide stat; show muted text | when stapleCount === 0 |
| href | /household/staples?ctx=settings | D3 route |
| D3 Staples Page | ||
| component | StaplesManager context="settings" | existing, do not modify |
| breadcrumb | ← Einstellungen → /settings | above page title |
| active-nav | Einstellungen in sidebar | not a separate nav entry |
| save-hint | "Änderungen werden automatisch gespeichert." | below chip grid |
| debounce | 300ms (in StaplesManager) | do not add extra debounce |
Kachel-Ansicht · Finale Spezifikation · Route: /members
+ 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. +
+ +border: var(--green-light)), "Du"-Badge statt Kebab⋯): immer im DOM, opacity:0 bis hover/focus, dann opacity:1. Auf Touch-Geräten immer sichtbar.position: absolute; top: 44px; right: 12px relativ zur Kachelborder-color: #B5D4F4) als Editier-Indikatorborder-radius nur oben, kein max-width)rgba(28,28,24,.45), Klick außerhalb schließt nicht (explizite Bestätigung erforderlich)expiresAt in gelbem Badge wenn ≤ 24h verbleibendgrid-template-columns: repeat(3, 1fr) (kein leerer Slot für Einladen)Diese Sektion enthält verbindliche Implementierungsregeln für den Coding-Agenten.
+ +
+/* 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
+ */
+
+
+ | Property | Value | Notes |
|---|---|---|
| Component: MemberCard | ||
| card-width | 1fr (grid) | 4-col desktop, 2-col mobile |
| card-min-height | 180px | desktop; auto mobile |
| avatar-size | 56px / 44px | desktop / mobile |
| avatar-radius | 50% | full circle |
| kebab-target | 44×44px | WCAG 2.2 minimum touch target |
| dropdown-min-width | 160px | right-aligned to kebab |
| Role Control | ||
| control-height | 32px | segmented, full card width |
| active-bg | --green-dark | selected role button |
| api-endpoint | PATCH /v1/households/mine/members/{userId} | body: { role } |
| Remove Dialog | ||
| confirm-btn-bg | --color-error (#DC4C3E) | danger action |
| api-endpoint | DELETE /v1/households/mine/members/{userId} | — |
| backdrop | rgba(28,28,24,.45) | click-outside does NOT close |
| Invite | ||
| api-create | POST /v1/households/mine/invites | returns InviteResponse |
| api-list | GET /v1/households/mine/invites | backend gap |
| copy-feedback | "Kopiert ✓" for 2000ms | then revert to "Kopieren" |