feat: voice messages, offline queue, delivery ticks, volume slider, admin + scaling
- 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
This commit is contained in:
+312
-214
@@ -1,30 +1,38 @@
|
||||
# Handoff: ChatApp — Clean Rail Redesign + Discord-Style Call UI
|
||||
# Handoff: ChatApp — Clean Rail Redesign (Chat + Friends + Admin + Settings + Calls)
|
||||
|
||||
## Overview
|
||||
|
||||
This package contains the full design spec for redesigning the existing ChatApp (Netralax) interface around a **Clean Rail** layout aesthetic, plus a complete **Discord-style call system** (incoming call, in-call dock, picture-in-picture, fullscreen cinema mode).
|
||||
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:
|
||||
|
||||
The redesign covers:
|
||||
1. The main chat interface (icon-rail sidebar + chat list + conversation area) in both **light** and **dark** mode.
|
||||
2. The call UX: incoming call overlay → active call docked above the chat (so users can keep typing while on a call) → fullscreen cinema mode → minimizable PiP bubble.
|
||||
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.
|
||||
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.
|
||||
|
||||
The design files are organized as:
|
||||
- `ChatApp Redesign.html` — entry HTML that boots the prototype
|
||||
- `components/app.jsx` — main layout (rail + chat list + chat area, plus call state machine)
|
||||
- `components/call.jsx` — all call UI components (IncomingCallScreen, ActiveCallScreen, ParticipantTile, CallControls, PipCall)
|
||||
- `components/icons.jsx` — SVG icon set used
|
||||
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 (this is the look you're targeting)
|
||||
- `styles/variants.css` — `variant-clean` light + dark token definitions
|
||||
- `styles/rail.css` — rail-layout specifics
|
||||
- `styles/call.css` — **all call UI styles** (the most important file for the call feature)
|
||||
- `screenshots/` — reference images of each state
|
||||
- `styles/call.css` — call UI styles
|
||||
- `styles/pages.css` — **Friends/Admin/Settings page styles**
|
||||
- `screenshots/` — reference images
|
||||
|
||||
## Fidelity
|
||||
|
||||
@@ -36,13 +44,13 @@ The design files are organized as:
|
||||
|
||||
### Colors — Light mode (`.variant-clean`)
|
||||
```
|
||||
--bg: #fafaf9 (app background)
|
||||
--bg-2: #f4f4f2 (secondary surfaces, hover, search input)
|
||||
--bg-3: #ffffff (elevated surfaces — chat list, chat area, cards)
|
||||
--fg: #0a0a0f (primary text)
|
||||
--fg-muted: #737380 (secondary text, icons)
|
||||
--line: rgba(10,10,15,0.08) (dividers, borders)
|
||||
--accent: #4F46E5 (indigo — primary actions, active states, my-bubble)
|
||||
--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
|
||||
```
|
||||
|
||||
@@ -54,20 +62,19 @@ The design files are organized as:
|
||||
--fg: #f4f4f5
|
||||
--fg-muted: #8a8a99
|
||||
--line: rgba(255,255,255,0.07)
|
||||
--accent: #6D73FF (slightly lighter indigo for contrast)
|
||||
--accent: #6D73FF
|
||||
--accent-fg: #ffffff
|
||||
```
|
||||
|
||||
### Status / Semantic Colors (both modes)
|
||||
### Semantic Colors
|
||||
```
|
||||
Speaking/Online/Success: #16a34a light · #22c55e / #4ade80 dark
|
||||
Danger/Decline/Hangup: #dc2626 (hover #b91c1c) · #fb7185 dark accent
|
||||
Warning/Idle: #f59e0b
|
||||
Live/Recording: #ef4444
|
||||
E2EE indicator (light): #16a34a (dark: #4ade80)
|
||||
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 color swatches (assigned per chat)
|
||||
### Avatar swatches (assigned per user)
|
||||
```
|
||||
violet: bg #ddd6fe / fg #5b21b6 (dark: #312E81 / #C4B5FD)
|
||||
amber: bg #fde68a / fg #78350f
|
||||
@@ -76,239 +83,330 @@ teal: bg #99f6e4 / fg #134e4a (dark: #134e4a / #99f6e4)
|
||||
```
|
||||
|
||||
### Typography
|
||||
- Family: `'Inter', system-ui, sans-serif` — Inter is the base. Display/headline use `'Outfit'` (participant name in IncomingCall).
|
||||
- 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:
|
||||
- Chat title / participant name large: 24px / 700
|
||||
- Primary labels, chat name: 14–15px / 600
|
||||
- Body (bubbles, messages): 14px / 400–500
|
||||
- Meta, timestamps, presence: 11–12px / 400–500
|
||||
- Rail tooltips, badges: 10–11px / 500–600
|
||||
- Uppercase eyebrow (e.g. "EINGEHENDER ANRUF"): 12px / 600 / letter-spacing 0.12em
|
||||
- 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
|
||||
- Container padding: 16px (call-stage), 14–20px (topbars, composer)
|
||||
- Tile gap in grid: 8–10px
|
||||
- Control-button row gap: 8–10px
|
||||
- Border radius scale: 4 (small), 8 (tiles/badges), 10 (buttons), 14 (cards, large buttons), 18 (bubbles, bottom-fs controls), 24 (modal/card)
|
||||
### 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(22,163,74,0.3)` (green accept) / same pattern for accent
|
||||
- PiP glow: `0 12px 40px rgba(0,0,0,0.2), 0 0 0 3px rgba(79,70,229,0.15)`
|
||||
- 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`
|
||||
- Pulse ring (incoming call): `2s ease-out infinite`, scale 0.85→1.25, opacity 0.6→0
|
||||
- Audio pulse (speaking ring): `1.3s ease-out infinite`, scale 1→1.3
|
||||
- Live dot: `1.5s ease-in-out infinite`, opacity 1↔0.4
|
||||
- PiP entrance: `slideInCall 0.4s cubic-bezier(0.22,1,0.36,1)`
|
||||
- Fullscreen hint fade: 3.5s total (0–15% fade in, 75–100% fade out)
|
||||
- Toggle switch thumb: `transform 0.2s`
|
||||
- PTT hotkey capture pulse: `keyPulse 1.2s ease-in-out infinite` (opacity 1↔0.6)
|
||||
|
||||
---
|
||||
|
||||
## Screens / Views
|
||||
## Tab Switching Behavior
|
||||
|
||||
### 1. Main Chat — Clean Rail Layout
|
||||
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** (72px wide): brand logo at top, 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 wide): 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, fills rest): chat header (counterpart avatar + name + presence/handle + search/voice/video buttons), messages scroll area, composer at bottom.
|
||||
|
||||
**Chat header actions (right side):** search icon, phone icon, video icon. Phone triggers voice call (`callScreen → 'active'`, `callState.video = false`), Video triggers video call (`callState.video = true`).
|
||||
- **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 (outgoing): filled accent bg, white text, radius `18px 18px 4px 18px`.
|
||||
- Theirs (incoming): transparent bg, 1px line border, fg text, radius `18px 18px 18px 4px`.
|
||||
- Avatars appear only on the **last message of a run** from that sender.
|
||||
- Reactions: pill row below bubble, `.mine` = filled accent.
|
||||
- System events ("Kaiwandi hat einen Anruf gestartet — Anruf beitreten"): centered `event-pill`, muted bg, muted fg, 11px.
|
||||
|
||||
**Composer:** plus button (attach), rounded input bar with inline emoji + mic icons, send button.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
### 2. Incoming Call Overlay
|
||||
## Screen 2: Friends
|
||||
|
||||
Full-bleed overlay replacing the chat area (takes over `.chat-area` space), radial-gradient background tinted with accent.
|
||||
Component: **`FriendsPageContent`** (`components/pages.jsx`)
|
||||
|
||||
Centered card (420px wide):
|
||||
- Eyebrow "EINGEHENDER ANRUF" (uppercase, 12px, tracked)
|
||||
- Large avatar (96px) with **two pulsing rings** (staggered 1s)
|
||||
- Caller name (24px / 700 / Outfit)
|
||||
- Subtitle row: lock icon + "E2E verschlüsselt · Voice Call"
|
||||
- Two side-by-side action buttons:
|
||||
- **Ablehnen**: transparent, red border/text, hover = 8% red bg
|
||||
- **Annehmen**: green #16a34a, white, shadow
|
||||
### 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.
|
||||
|
||||
### 3. Active Call — Docked (Default)
|
||||
### 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.
|
||||
|
||||
**CRITICAL layout decision:** The active call does NOT take over the chat area. Instead it renders as a **dock above the messages**, taking roughly 50% of the chat-area height (min 280px, max 420px). The chat header is **hidden** while the dock is active (redundant — call topbar shows the channel name). Messages + composer remain fully visible and usable below, so users can keep chatting during the call. This is the Discord desktop pattern.
|
||||
### Tabs
|
||||
|
||||
**Dock content:**
|
||||
- **Call topbar**: left side shows group icon + "Die Squad" + duration (tabular-nums), with E2E verschlüsselt below in success-green. Right side has 3 mode toggles: Grid / Focus / Fullscreen.
|
||||
- **Call stage**: fills the remaining space, renders either:
|
||||
- **Grid**: CSS grid, 2×2 for 4 people, 3×2 for 5–6, 1fr 1fr for 2.
|
||||
- **Focus**: main speaker tile takes most height, bottom strip with 110px-tall smaller tiles for others.
|
||||
- **Fullscreen**: switches to `mode === 'fullscreen'` — see screen 4.
|
||||
- **Call controls bar** at the bottom of the dock: Mic, Video, Screen-share, Participants, **Hangup** (wider, red). Compact 36px buttons when docked.
|
||||
**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."
|
||||
|
||||
### 4. Active Call — Fullscreen Cinema Mode
|
||||
**Ausgehend** (`status === 'pending' && direction === 'outgoing'`):
|
||||
- "Wartet auf Antwort…" italicized + `btn-ghost` "Abbrechen" → `DELETE /api/friendships/{id}`.
|
||||
- Empty: "Keine ausgehenden Anfragen."
|
||||
|
||||
When mode = `fullscreen`:
|
||||
- Entire call-dock container becomes absolute-positioned and takes over everything below the window bar (chat list still visible on the left).
|
||||
- The **call topbar is hidden** entirely.
|
||||
- Main speaker fills the whole frame, no border/radius.
|
||||
- Participants strip moves to a **floating column in the bottom-right** (`.fs-strip`): 180px wide, 100px tall tiles, glass-blurred bg.
|
||||
- **Call controls bar floats** at bottom center — glass pill: `rgba(10,10,15,0.7)` + `backdrop-filter: blur(20px)` + subtle border + 18px radius.
|
||||
- **"Esc zum Verlassen" hint** fades in top-center on entry and auto-fades out over 3.5s. Pressing Esc or clicking Grid/Focus buttons exits.
|
||||
- **No visible X button** (intentional — keyboard-only exit, confirmed design decision).
|
||||
**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."
|
||||
|
||||
### 5. Participant Tile — Shared Component
|
||||
### 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.
|
||||
|
||||
A participant is either in **audio-only**, **video**, or **sharing screen** mode.
|
||||
- Tile border: 1px `var(--line)`, radius 14 (10 in dock). Hover → border becomes accent. `.speaking` → 2px green border + green glow shadow. `.focused` → accent border.
|
||||
- **Audio-only**: centered avatar (72px large, 44px small). When `speaking`, a green ring pulses around the avatar (`::before` pseudo).
|
||||
- **Video**: placeholder stub (gradient background + centered avatar, since real video not in scope here).
|
||||
- **Screenshare**: renders a "fake window" — chrome row with 3 dots, content row with grey skeleton lines + one accent-tinted block. A glass badge top-left: monitor icon + "Max teilt Bildschirm" (badge hidden on small tiles).
|
||||
- **Meta row** (absolute, bottom 8px, glass): left shows crown (if me) + name + lock (if E2EE), right shows muted mic icon (red chip) and/or sharing icon (green chip).
|
||||
|
||||
### 6. Picture-in-Picture (PiP)
|
||||
|
||||
A compact 260px pill docked bottom-right of the chat area:
|
||||
- 44px preview square (avatar of speaker, or mini shared-window placeholder if someone's sharing)
|
||||
- Title "Die Squad · {participantCount}"
|
||||
- Subtitle row: red pulsing dot + "Live · tippe zum Öffnen"
|
||||
- Circular red hangup button on the right
|
||||
- Whole thing is clickable → expands back to active call
|
||||
- Entrance animation: slide from bottom + fade in (`slideInCall`)
|
||||
- Shadow: standard + 3px accent glow ring
|
||||
### 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';
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactions & Behavior
|
||||
## Screen 3: Admin
|
||||
|
||||
### Call State Machine (lives in `ChatApp`)
|
||||
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
|
||||
// Primary state
|
||||
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 & Transitions
|
||||
- Chat-header **phone icon** click → `setCallScreen('active')`, `callState.video = false`
|
||||
- Chat-header **video icon** click → `setCallScreen('active')`, `callState.video = true`
|
||||
- Grid tile click → `setFocusedId(id); setCallMode('focus')`
|
||||
- Focus/Grid/Fullscreen buttons in call topbar → set mode directly
|
||||
- Incoming "Annehmen" → `setCallScreen('active')`
|
||||
- Incoming "Ablehnen" → `setCallScreen('none')`
|
||||
- Hangup → `setCallScreen('none')`
|
||||
- PiP clicked → `setCallScreen('active')`
|
||||
- PiP hangup (circle red btn) → `stopPropagation()` + `setCallScreen('none')`
|
||||
- **Esc key** (global listener while `callScreen === 'active' && callMode === 'fullscreen'`) → `setCallMode('grid')`
|
||||
|
||||
### Chat Header Visibility Rule
|
||||
```
|
||||
if (callScreen === 'active' && callMode !== 'fullscreen') → hide chat header
|
||||
else → show chat header
|
||||
```
|
||||
|
||||
### Keyboard
|
||||
- `Esc` exits fullscreen (only). No other global keybindings.
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
Data shapes to wire up (see `components/call.jsx`):
|
||||
|
||||
```ts
|
||||
type Participant = {
|
||||
id: string;
|
||||
name: string;
|
||||
initial: string;
|
||||
color: 'violet' | 'amber' | 'rose' | 'teal';
|
||||
speaking: boolean;
|
||||
muted: boolean;
|
||||
video: boolean;
|
||||
sharing?: boolean;
|
||||
me?: boolean;
|
||||
e2ee: boolean;
|
||||
};
|
||||
|
||||
type CallState = {
|
||||
muted: boolean;
|
||||
sharing: boolean;
|
||||
video: boolean;
|
||||
duration: string; // "HH:MM:SS"
|
||||
};
|
||||
```
|
||||
|
||||
In production these all come from your real-time signaling layer (WebRTC, LiveKit, etc.). The prototype uses static data for layout purposes only — wire them to your call store.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
No custom imagery. Everything is SVG icons (inlined in `components/icons.jsx`) and CSS. Fonts from Google Fonts: `Inter`, `Outfit`.
|
||||
|
||||
Icons used in this scope: Chat, Friends, Shield, Settings, Logo, AddUser, Search, Phone, Video, Plus, Smile, Mic, MicOff, Send, Sun, Moon, Monitor, Lock, Crown, Grid, Focus, Maximize, PhoneOff.
|
||||
|
||||
Replace with your codebase's icon library if one exists (e.g. lucide-react, heroicons) — names match lucide conventions.
|
||||
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. If you already have a token system, map them to your existing names; otherwise create `--bg`, `--bg-2`, `--bg-3`, `--fg`, `--fg-muted`, `--line`, `--accent`, `--accent-fg`.
|
||||
2. **Icon rail + chat list + chat area**: refactor the current top-nav layout into the 3-column rail layout. Use your existing `Avatar`, `ChatItem`, `Message` components — only restyle.
|
||||
3. **Message bubbles**: outlined `them` / filled `me`, asymmetric radius, avatars only on last-of-run.
|
||||
4. **Call state machine**: add the four `useState` hooks to whatever component owns the chat screen. Hook the Esc key effect.
|
||||
5. **IncomingCallScreen**: new component, full-bleed overlay of `.chat-area`.
|
||||
6. **Call dock**: new component, renders above `.messages`. Hide chat-header while docked.
|
||||
7. **ParticipantTile**: audio / video / sharing modes, speaking ring, meta overlay.
|
||||
8. **Fullscreen mode**: switch call container to position absolute, floating strip + floating controls + Esc hint.
|
||||
9. **PiP**: minimizable widget bottom-right of chat area.
|
||||
10. **Wire real data**: replace `CALL_PARTICIPANTS` dummy with your live call-session participants.
|
||||
|
||||
---
|
||||
|
||||
## Files in This Handoff
|
||||
|
||||
```
|
||||
design_handoff_chatapp/
|
||||
├── README.md (this file)
|
||||
├── ChatApp Redesign.html (entry — open in browser to see the prototype)
|
||||
├── components/
|
||||
│ ├── app.jsx (chat app + rail layout + call state machine)
|
||||
│ ├── call.jsx (Incoming / Active / Pip / ParticipantTile / CallControls)
|
||||
│ ├── icons.jsx (all SVG icons)
|
||||
│ └── data.jsx (dummy chats, messages)
|
||||
├── styles/
|
||||
│ ├── base.css
|
||||
│ ├── variants.css (Clean light + dark tokens — this is your theme)
|
||||
│ ├── rail.css (rail layout)
|
||||
│ └── call.css (all call UI — highest priority file)
|
||||
└── screenshots/
|
||||
├── 01-chat-light.png
|
||||
├── 03-chat-dark.png
|
||||
├── 04-incoming-dark.png
|
||||
├── 05-incall-grid-dark.png
|
||||
├── 06-incall-focus-dark.png
|
||||
├── 07-incall-fullscreen-dark.png
|
||||
├── 09-pip-dark.png
|
||||
└── 11-incall-light.png
|
||||
```
|
||||
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), they were design exploration and not the chosen direction.
|
||||
- Prefer composition over copying raw CSS. If the codebase has a `Card`, `Button`, `Tooltip`, use them.
|
||||
- Duration ("00:12:47") should tick live from the call start timestamp, not be static.
|
||||
- For WebRTC/media handling: this package specifies **UI only**. The media/signaling layer is out of scope and already exists in the codebase (see `CallUI.tsx`, `InCallPanel.tsx`).
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user