Files
ChatApp/design_handoff_chatapp/README.md
T
byGalax a04ecf7a19 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
2026-04-21 01:14:16 +02:00

20 KiB
Raw Blame History

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.jsxFriends, 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.cssvariant-clean light + dark token definitions
  • styles/rail.css — rail-layout specifics
  • styles/call.css — call UI styles
  • styles/pages.cssFriends/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: 1415px / 600
    • Body text, bubbles, settings hint: 1214px / 400500
    • Meta, timestamps, handles, stat labels: 1112px / 500, uppercase 1011px tracked 0.06em
    • Mono values: 1112px / 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 810, 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 ChatItems (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

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

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.

<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)

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.