a04ecf7a19
- Voice messages: MediaRecorder → encrypted attachment, custom waveform player via OfflineAudioContext, 60s limit + live mic-level meter - Offline message queue: localStorage outbox, exponential backoff retries, optimistic pending bubble with retry/discard - Delivery indicator: message_deliveries table + RLS (reciprocal receipts), ✓ / ✓✓ / ✓✓-blue tick states, group-aware (all members must ack) - Per-participant volume slider in calls via right-click tile menu, persisted to localStorage, applied to attached audio elements - Group call scaling: grid up to 12 tiles with pagination, active-speaker auto-promotion in fullscreen - Push notifications scaffolding: service worker, VAPID subscription registration, notify-push edge function skeleton - Backup recovery code: 24-char base32 code (~120 bits entropy) as alternative decrypt path, restore UI with mode toggle - Admin panel: conversations list, audit log (admin_audit_log table + admin_log_action RPC), audit entry on user flag toggle - Search v2: sender filter, attachment-only toggle, date range - Reactions pop animation (scale 0.4→1.15→1 on count change) - Message list windowing (150 default, expand via IntersectionObserver) - Stub cleanup: removed dead ScreenshareStub from CallParticipantTile Fixes: - Focus-triggered flicker: dropped window.focus listeners in three spots, throttled visibilitychange/online wake-refreshes to 30s, keep existing data visible during background re-syncs (no more spinner on every click) - Voice attachment audio element collapsed to 0px on peer side — now forces 280px min-width on bubble Migrations (push required): 20260421000001_message_deliveries.sql 20260421000002_admin_audit_log.sql Server TODO: VAPID keys + notify-push edge function deploy
413 lines
20 KiB
Markdown
413 lines
20 KiB
Markdown
# Handoff: ChatApp — Clean Rail Redesign (Chat + Friends + Admin + Settings + Calls)
|
||
|
||
## Overview
|
||
|
||
Full design spec for redesigning the existing ChatApp (Netralax) interface around a **Clean Rail** layout aesthetic. Covers **four main screens** plus the complete call UX:
|
||
|
||
1. **Chat** — icon-rail + chat list + conversation area
|
||
2. **Friends** — friends list, outgoing requests, incoming requests, user search
|
||
3. **Admin** — stats, invite-code management, user administration
|
||
4. **Settings** — account, appearance, privacy, voice/audio, screenshare, devices, danger zone
|
||
5. **Calls** — incoming overlay → in-call dock → fullscreen → picture-in-picture
|
||
|
||
All screens work in **light** and **dark** mode.
|
||
|
||
---
|
||
|
||
## About the Design Files
|
||
|
||
The files in this bundle are **design references created in HTML/React/CSS** — prototypes showing the intended look, layout, and behavior. They are **not production code** to copy verbatim.
|
||
|
||
Your task is to **recreate these designs inside the existing ChatApp codebase** using its established patterns (existing React components, styling conventions, state management, icon library, etc.). If some tokens or helper components don't exist yet in the codebase, add them but keep them aligned with what's already there.
|
||
|
||
File layout:
|
||
- `ChatApp Redesign.html` — entry HTML (open in a browser to see the prototype)
|
||
- `components/app.jsx` — main layout (rail + chat list + chat area + call state machine + tab switching)
|
||
- `components/pages.jsx` — **Friends, Admin, Settings page components** plus shared page primitives (PageHeader, Section, Toggle, SegmentedControl, Row, Empty, Banner, FlagChip, FriendRow)
|
||
- `components/call.jsx` — call UI (IncomingCallScreen, ActiveCallScreen, ParticipantTile, CallControls, PipCall)
|
||
- `components/icons.jsx` — SVG icon set
|
||
- `components/data.jsx` — dummy chat/message data
|
||
- `styles/base.css` — shared base tokens + layout primitives
|
||
- `styles/variants.css` — `variant-clean` light + dark token definitions
|
||
- `styles/rail.css` — rail-layout specifics
|
||
- `styles/call.css` — call UI styles
|
||
- `styles/pages.css` — **Friends/Admin/Settings page styles**
|
||
- `screenshots/` — reference images
|
||
|
||
## Fidelity
|
||
|
||
**High-fidelity.** Exact colors, radii, spacing, typography and motion are specified. Recreate pixel-perfectly using the codebase's existing component library — but prefer the codebase's primitives over duplicating the prototype's raw CSS where equivalents exist.
|
||
|
||
---
|
||
|
||
## Design Tokens (Clean Rail)
|
||
|
||
### Colors — Light mode (`.variant-clean`)
|
||
```
|
||
--bg: #fafaf9
|
||
--bg-2: #f4f4f2
|
||
--bg-3: #ffffff
|
||
--fg: #0a0a0f
|
||
--fg-muted: #737380
|
||
--line: rgba(10,10,15,0.08)
|
||
--accent: #4F46E5
|
||
--accent-fg: #ffffff
|
||
```
|
||
|
||
### Colors — Dark mode (`.variant-clean.dark`)
|
||
```
|
||
--bg: #050507
|
||
--bg-2: #111118
|
||
--bg-3: #0A0A0F
|
||
--fg: #f4f4f5
|
||
--fg-muted: #8a8a99
|
||
--line: rgba(255,255,255,0.07)
|
||
--accent: #6D73FF
|
||
--accent-fg: #ffffff
|
||
```
|
||
|
||
### Semantic Colors
|
||
```
|
||
Success / Online / E2EE: #16a34a (light) · #4ade80 / #22c55e (dark)
|
||
Danger / Decline: #dc2626 / #b91c1c hover · #fb7185 (dark accent for borders/text)
|
||
Warning / Expired: #f59e0b · #d97706 light · #fbbf24 dark
|
||
Live / Recording: #ef4444
|
||
```
|
||
|
||
### Avatar swatches (assigned per user)
|
||
```
|
||
violet: bg #ddd6fe / fg #5b21b6 (dark: #312E81 / #C4B5FD)
|
||
amber: bg #fde68a / fg #78350f
|
||
rose: bg #fecdd3 / fg #881337
|
||
teal: bg #99f6e4 / fg #134e4a (dark: #134e4a / #99f6e4)
|
||
```
|
||
|
||
### Typography
|
||
- Family: `'Inter', system-ui, sans-serif` base; display `'Outfit'` (page titles, call participant name, stat-card values); mono `'JetBrains Mono'` (invite codes, device IDs, keyboard hotkeys, inline stats).
|
||
- Scale:
|
||
- Page title (`Freunde`, `Admin`, `Einstellungen`, call participant): 28px (page) / 24px (call) / 700 / Outfit / letter-spacing -0.02em
|
||
- Section label (uppercase eyebrow): 11px / 600 / tracked 0.08em
|
||
- Body labels, chat name, friend name: 14–15px / 600
|
||
- Body text, bubbles, settings hint: 12–14px / 400–500
|
||
- Meta, timestamps, handles, stat labels: 11–12px / 500, uppercase 10–11px tracked 0.06em
|
||
- Mono values: 11–12px / JetBrains Mono / 500
|
||
|
||
### Spacing / Radius
|
||
- Page content max-width: **820px**, centered, 28px top padding, 32px horizontal.
|
||
- Section card radius 16, section header padded 16×20 with bottom border, section body padded 16×20 gap 14.
|
||
- Button radius 8–10, chip/pill 999, stat card 12, device card 12.
|
||
- Toggle switch: 40×22 with 16×16 thumb, 2px inset, transitions all 0.2s.
|
||
|
||
### Shadows
|
||
- Elevated card light: `0 20px 60px rgba(0,0,0,0.1)`
|
||
- Elevated card dark: `0 20px 60px rgba(0,0,0,0.5)`
|
||
- Accent button: `0 4px 14px rgba(79,70,229,0.25)`
|
||
- Avatar-large glow: `0 2px 6px rgba(0,0,0,0.08)`
|
||
|
||
### Motion
|
||
- Standard UI transition: `all 0.15s`
|
||
- Toggle switch thumb: `transform 0.2s`
|
||
- PTT hotkey capture pulse: `keyPulse 1.2s ease-in-out infinite` (opacity 1↔0.6)
|
||
|
||
---
|
||
|
||
## Tab Switching Behavior
|
||
|
||
The icon rail (`.icon-rail`) holds 4 primary tabs: **Chats**, **Friends**, **Admin**, **Settings**. Clicking an icon sets `activeTab`. Layout adapts:
|
||
|
||
- **`activeTab === 'chats'`** → renders icon-rail + chat-list + chat-area (3 columns: `64px 280px 1fr`).
|
||
- **`activeTab !== 'chats'`** → renders icon-rail + page content (2 columns: `64px 1fr`). Chat list is removed. The page component (`FriendsPageContent`, `AdminPageContent`, `SettingsPageContent`) renders inside the chat-area slot with its own scroll container and 820px centered inner.
|
||
|
||
The `.rail-layout.page-mode` class toggles the grid-template-columns. **IMPORTANT:** this override is inlined in the entry HTML as a `<style>` block to avoid CSS-cache issues during dev. In production, put it in your main stylesheet.
|
||
|
||
The **call dock** and **call screen switcher** are only shown on the chats tab.
|
||
|
||
---
|
||
|
||
## Screen 1: Chat
|
||
|
||
**Structure (left → right):**
|
||
- **Icon Rail** (64px): brand logo, tab buttons (Chats/Friends/Admin), spacer, Settings + theme-toggle at bottom. Active tab shows a vertical pill indicator on the left edge.
|
||
- **Chat List** (280px): header with title + "new chat" icon, search input, list of `ChatItem`s (avatar + name + time + preview, optional unread dot), `rail-user` footer showing own profile + quick mute/settings.
|
||
- **Chat Area** (flex): chat header (counterpart avatar + name + presence/handle + search/voice/video), messages, composer.
|
||
|
||
**Message bubbles:**
|
||
- Mine: filled accent bg, white text, radius `18px 18px 4px 18px`.
|
||
- Theirs: transparent bg, 1px line border, fg text, radius `18px 18px 18px 4px`.
|
||
- Avatars only on the **last message of a run**.
|
||
- Reactions: pill row below, `.mine` = filled accent.
|
||
- System events: centered `event-pill`, muted bg/fg, 11px.
|
||
|
||
---
|
||
|
||
## Screen 2: Friends
|
||
|
||
Component: **`FriendsPageContent`** (`components/pages.jsx`)
|
||
|
||
### Layout
|
||
- `PageHeader` with title "Freunde" and subtitle `"{N} Freunde · {M} offene Anfragen"`.
|
||
- **Global search bar** (sticky at top of scroll): icon + input, placeholder "Benutzer suchen oder hinzufügen…". Focused state → border becomes accent.
|
||
- **Tabs** (only when not searching): 3 pill tabs with inline counts — `Freunde`, `Ausgehend`, `Anfragen`. When there are pending incoming requests, the tab count badge is red (`.tab-count.highlight`).
|
||
- **List area**: either tab content OR search results, mutually exclusive.
|
||
|
||
### Search behavior
|
||
- Trigger: input.length ≥ 2. Below that, tabs show.
|
||
- Filters a global user directory (API: `GET /api/users/search?q={query}` — returns up to 20 matches).
|
||
- Each result renders a `FriendRow` with a single primary button: "Anfrage senden" (icon + label). On click → `POST /api/friendships` with `{ targetUserId }`, optimistically updates the row to "Angefragt".
|
||
- Empty state: "Keine Ergebnisse für „{query}"" with Search icon.
|
||
|
||
### Tabs
|
||
|
||
**Freunde** (`status === 'accepted'`):
|
||
- FriendRow with: Nachricht (primary button opens conversation), Voice icon button (starts call), "Entfernen" (danger-ghost, confirms before `DELETE /api/friendships/{id}`).
|
||
- Empty: "Noch keine Freunde. Suche oben nach Benutzern um anzufangen."
|
||
|
||
**Ausgehend** (`status === 'pending' && direction === 'outgoing'`):
|
||
- "Wartet auf Antwort…" italicized + `btn-ghost` "Abbrechen" → `DELETE /api/friendships/{id}`.
|
||
- Empty: "Keine ausgehenden Anfragen."
|
||
|
||
**Anfragen** (`status === 'pending' && direction === 'incoming'`):
|
||
- `btn-primary` "Annehmen" (check icon) → `PATCH /api/friendships/{id}` with `{ accept: true }`; `btn-ghost` "Ablehnen" → same endpoint `{ accept: false }`.
|
||
- Empty: "Keine eingehenden Anfragen."
|
||
|
||
### FriendRow (shared)
|
||
12px gap row, hover → `bg-2`. Avatar (with presence indicator on accepted friends), name (14px/600), handle `@{username}` (12px muted), actions on the right.
|
||
|
||
### Data shapes
|
||
```ts
|
||
type Friendship = {
|
||
id: string;
|
||
userId: string; // the OTHER user's ID
|
||
displayName: string;
|
||
username: string; // @handle
|
||
avatarColor: 'violet'|'amber'|'rose'|'teal';
|
||
initial: string; // derived from displayName[0] uppercase
|
||
status: 'accepted' | 'pending';
|
||
direction: 'outgoing' | 'incoming' | null; // null when accepted
|
||
presence?: 'online' | 'idle' | 'dnd' | 'offline';
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Screen 3: Admin
|
||
|
||
Component: **`AdminPageContent`** (`components/pages.jsx`)
|
||
|
||
Gated route — only accessible if `currentUser.isAdmin === true`. If not, render 403 "Kein Zugriff" page.
|
||
|
||
### Layout
|
||
1. **`PageHeader`** "Admin" + subtitle `"{userCount} User · {activeInviteCount} aktive Invites"`.
|
||
2. **Stats grid** (4 cards, 1fr × 4 @ ≥1200px, 2×2 below):
|
||
- Gesamt User
|
||
- Admins
|
||
- Gebannt (value in `#dc2626` / dark `#fb7185`)
|
||
- Aktive Invites
|
||
- Each card: 11px uppercase tracked label, 28px Outfit bold value, tabular-nums.
|
||
3. **System section**: single `Toggle` for `invites_enabled`. Hint: "Wenn aus, können keine neuen User per Invite-Code beitreten."
|
||
- Maps to `PATCH /api/admin/settings { invitesEnabled: boolean }`.
|
||
4. **Invite-Codes section**:
|
||
- Header action: **+ Neuer Code** primary button → opens modal (out of scope here) with fields for `code` (optional; auto-generated if blank), `usesLimit` (int or ∞), `expiresAt` (date or never).
|
||
- Table columns: Code · Nutzungen · Ablauf · Status · actions.
|
||
- Code column: mono 12px.
|
||
- Status pill: `Aktiv` (green), `Abgelaufen` (amber), `Deaktiviert` (red).
|
||
- Actions (per row, right-aligned): Copy icon (shows check on copy, 1.4s timeout), "Deaktivieren/Aktivieren" ghost xs button, red Trash icon.
|
||
- API:
|
||
- List: `GET /api/admin/invites`
|
||
- Create: `POST /api/admin/invites`
|
||
- Toggle: `PATCH /api/admin/invites/{code} { disabled: boolean }`
|
||
- Delete: `DELETE /api/admin/invites/{code}`
|
||
5. **Benutzer section**:
|
||
- Row per user: avatar + name (with **Admin badge** if admin — pill with crown icon, accent bg + `accent-fg`) + `@handle · beigetreten {date}` meta.
|
||
- Flag chips on the right:
|
||
- **Admin** (toggle accent when active)
|
||
- **Invite-Block** (toggle accent when active) — prevents this user from creating invites
|
||
- **Bann** (`tone="danger"` — red when active)
|
||
- Click a chip → `PATCH /api/admin/users/{userId} { [flag]: boolean }`. Optimistic update.
|
||
- No delete — banning is the terminal state.
|
||
|
||
### Data shapes
|
||
```ts
|
||
type Invite = {
|
||
code: string;
|
||
usesCount: number;
|
||
usesLimit: number | null; // null = unlimited
|
||
expiresAt: string | null; // ISO date
|
||
disabled: boolean;
|
||
createdBy: string; // userId
|
||
createdAt: string;
|
||
};
|
||
|
||
type AdminUser = {
|
||
userId: string;
|
||
displayName: string;
|
||
username: string;
|
||
initial: string;
|
||
avatarColor: string;
|
||
isAdmin: boolean;
|
||
blockedFromInviting: boolean;
|
||
banned: boolean;
|
||
joinedAt: string; // display-formatted in list
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Screen 4: Settings
|
||
|
||
Component: **`SettingsPageContent`** (`components/pages.jsx`)
|
||
|
||
### Sections (in this exact order)
|
||
|
||
**1. Account**
|
||
- `avatar-control` row: 64px circular avatar with initial, label + hint, "Ändern" primary, "Entfernen" danger-ghost.
|
||
- Upload: 512×512 max, WebP only, client-side crop, `POST /api/me/avatar`.
|
||
- `row-stack` with 3 `Row` entries:
|
||
- Benutzername (editable — click to inline-edit, `PATCH /api/me { username }`, uniqueness check)
|
||
- Anzeigename (editable, `PATCH /api/me { displayName }`)
|
||
- User-ID (mono, read-only)
|
||
|
||
**2. Aussehen**
|
||
- Sprache: `SegmentedControl` Deutsch/English → `PATCH /api/me { locale }` and updates i18n context.
|
||
- Theme: `SegmentedControl` ☀ Light / ☾ Dark → toggles dark mode globally. Wire to your theme provider; persist in localStorage + `PATCH /api/me { theme }`.
|
||
|
||
**3. Datenschutz**
|
||
- `Toggle` Lesebestätigungen (bidirectional — hint explains).
|
||
- `Toggle` DMs von Fremden erlauben.
|
||
|
||
**4. Sprache & Audio**
|
||
- `SegmentedControl` Audio-Qualität: `Sprache` (default) vs `HiFi`.
|
||
- **Sprache**: 48 kbps Mono Opus, 48 kHz, DSP on (noise suppression + echo cancellation + auto-gain).
|
||
- **HiFi**: 510 kbps Stereo Opus, 48 kHz, DSP off.
|
||
- Hint text updates live with the mode.
|
||
- Inline `stats-inline` box showing the 4 resolved parameters (Bitrate / Channels / Sample / DSP).
|
||
- Divider.
|
||
- `Toggle` Push-to-Talk. When enabled, an extra row appears: label "Hotkey" + `.key-capture` button showing the current key. Click → enters capture mode (pulsing animation), next keydown sets the key. Esc cancels.
|
||
- Divider.
|
||
- `Toggle` E2EE (default on). Hint: all participants must have it on.
|
||
|
||
**5. Bildschirmfreigabe**
|
||
- `SegmentedControl` Flüssig / Balanced / Scharf:
|
||
- Flüssig: 3 Mbps, 1280×720, 60 fps
|
||
- Balanced: 6 Mbps, 1920×1080, 30 fps (default)
|
||
- Scharf: 12 Mbps, 2560×1440, 30 fps
|
||
- Inline `stats-inline` shows resolved params.
|
||
|
||
**6. Geräte**
|
||
- `device-card` (green-tinted): lock icon + "Dieses Gerät", followed by Row stack: Name, Plattform, Device-ID (mono).
|
||
- `device-backup` card: password input + "Export erstellen" button → generates an encrypted JSON file containing the device's private key + room keys, downloaded client-side (never sent to server).
|
||
|
||
**7. Danger Zone**
|
||
- Single `btn-danger` "Abmelden" (full red button). Triggers sign-out + clears all local state.
|
||
|
||
### SegmentedControl
|
||
Inline group with 3px padding, transparent buttons, active button elevated with white bg (light) or elevated surface (dark) + shadow. 12px/500.
|
||
|
||
### Toggle
|
||
40×22 track, 16×16 thumb, 999 radius. Off: `bg-2` bg + `line` border. On: `accent` bg+border. Thumb translates 18px on. Full click target = entire row (via `<label>`).
|
||
|
||
### State persistence
|
||
Every setting writes to `PATCH /api/me/settings { [key]: value }` on change, with a debounced save (300ms for toggles is fine, immediate for segmented controls). Show a subtle top-right toast "Gespeichert" on success (not shown in this prototype).
|
||
|
||
---
|
||
|
||
## Screen 5: Calls (unchanged from previous scope)
|
||
|
||
See previous sections — Incoming → Active Dock → Fullscreen Cinema → PiP flow with `callScreen` + `callMode` state machine, Esc key handling, Discord-style dock above messages.
|
||
|
||
---
|
||
|
||
## Shared Page Primitives
|
||
|
||
All in `components/pages.jsx`. Re-use these across Friends/Admin/Settings; they define the aesthetic rhythm of the whole app.
|
||
|
||
```jsx
|
||
<PageHeader title="…" subtitle="…" actions={<button />} />
|
||
<Section title="…" action={<button />}>…</Section>
|
||
<Toggle label="…" hint="…" checked={} onChange={} />
|
||
<SegmentedControl value={} onChange={} options={[{value, label}]} />
|
||
<Row label="…" value="…" mono /> // key/value line in a stack
|
||
<Stat label="…" value="…" /> // used inside stats-inline
|
||
<Banner kind="error|success">…</Banner>
|
||
<Empty icon={Icon.X}>…</Empty> // for empty states
|
||
<FlagChip active={} tone="danger" onToggle={}>Admin</FlagChip>
|
||
<FriendRow profile={}>{actions}</FriendRow>
|
||
```
|
||
|
||
---
|
||
|
||
## API Endpoints (used by pages)
|
||
|
||
Friends:
|
||
```
|
||
GET /api/friendships → { accepted, outgoing, incoming }
|
||
POST /api/friendships { targetUserId } → creates outgoing
|
||
PATCH /api/friendships/{id} { accept: boolean }
|
||
DELETE /api/friendships/{id}
|
||
GET /api/users/search?q=… → up to 20 users (friendships excluded)
|
||
```
|
||
|
||
Admin:
|
||
```
|
||
GET /api/admin/stats → { userCount, adminCount, bannedCount, activeInviteCount }
|
||
GET /api/admin/settings → { invitesEnabled }
|
||
PATCH /api/admin/settings { invitesEnabled }
|
||
GET /api/admin/invites → Invite[]
|
||
POST /api/admin/invites { code?, usesLimit?, expiresAt? } → Invite
|
||
PATCH /api/admin/invites/{code} { disabled }
|
||
DELETE /api/admin/invites/{code}
|
||
GET /api/admin/users → AdminUser[]
|
||
PATCH /api/admin/users/{userId} { isAdmin? | blockedFromInviting? | banned? }
|
||
```
|
||
|
||
Settings / Me:
|
||
```
|
||
GET /api/me → profile + settings
|
||
PATCH /api/me { username? | displayName? | locale? | theme? }
|
||
PATCH /api/me/settings { readReceipts? | allowDmsFromStrangers? | audioQuality? | ptt? | pttKey? | e2ee? | screenShareQuality? }
|
||
POST /api/me/avatar multipart → new avatar URL
|
||
DELETE /api/me/avatar
|
||
GET /api/me/devices → Device[]
|
||
POST /api/me/devices/backup { passphrase } → encrypted blob download
|
||
POST /api/auth/signout
|
||
```
|
||
|
||
Scope assumption: your codebase already has most of these (see `FriendshipsContext.tsx`, `AdminPage.tsx`, `SettingsPage.tsx`). Wire the new UI to the existing hooks — don't rebuild the data layer.
|
||
|
||
---
|
||
|
||
## Call State Machine (unchanged)
|
||
|
||
```js
|
||
const [callScreen, setCallScreen] = useState('none'); // 'none' | 'incoming' | 'active' | 'pip'
|
||
const [callMode, setCallMode] = useState('grid'); // 'grid' | 'focus' | 'fullscreen'
|
||
const [focusedId, setFocusedId] = useState(firstSharingOrSpeakerId);
|
||
const [callState, setCallState] = useState({ muted: false, sharing: false, video: false, duration });
|
||
```
|
||
|
||
Triggers, keyboard and visibility rules identical to previous spec.
|
||
|
||
---
|
||
|
||
## Implementation Checklist
|
||
|
||
1. **Tokens**: add the Clean variant light + dark CSS variables to your theme layer.
|
||
2. **Rail layout**: refactor top-nav to icon-rail + chat list + chat area (`.rail-layout`). Add `.rail-layout.page-mode` (2-column) for non-chat tabs.
|
||
3. **Tab switching**: lift `activeTab` state. Conditionally render chat area vs page content.
|
||
4. **Page primitives**: implement `PageHeader`, `Section`, `Toggle`, `SegmentedControl`, `Row`, `Stat`, `Banner`, `Empty`, `FlagChip`, `FriendRow` using your existing component library where possible.
|
||
5. **FriendsPage**: wire to `FriendshipsContext` + user-search endpoint. Tabs + search + optimistic mutations.
|
||
6. **AdminPage**: stats grid + system toggle + invites table (copy-to-clipboard, enable/disable, delete) + users list (flag chips with optimistic updates).
|
||
7. **SettingsPage**: all 7 sections with two-way binding to user profile + settings. Debounced saves. Theme toggle wired to theme context.
|
||
8. **Message bubbles + call UX** (already specified in previous scope — unchanged).
|
||
|
||
---
|
||
|
||
## Notes for Claude Code
|
||
|
||
- The Clean Rail variant is **Variante 2** in the prototype's variant switcher — ignore the other 5 aesthetic variants (Playful, Y2K, Cyberpunk, Warm, Brutal).
|
||
- `components/pages.jsx` uses **dummy data** for display. Replace with your live contexts (`FriendshipsContext`, admin API hooks, user/settings hooks).
|
||
- Prefer composition over copying raw CSS. If the codebase has a `Card`, `Button`, `Toggle`, use them.
|
||
- Keep German copy as-is — it matches the existing product. English translations via i18n are a separate task.
|
||
- The `.rail-layout.page-mode` grid override is inlined in the HTML in this prototype. Move it to your main stylesheet.
|
||
- All settings that affect media (audio quality, PTT, E2EE, screen-share quality) must be read at **call-start time** by the existing call code — they do not hot-apply to an active call.
|