Private
Public Access
Rebuild frontend from the Claude Design handoff, DarkSingularity brand
Replaces the Phase 1 placeholder UI with a single persistent app shell (top bar + sidebar + chat pane, 860px responsive breakpoint) matching the "PWA chat system UI" design handoff: message bubbles with consecutive-run avatar/name grouping, auto-growing composer, room search, and the real DarkSingularity logo (also used to regenerate the PWA icons). The handoff didn't cover Phase 2 (private rooms, roles, invites) or browsing/joining open rooms, so those are added using the same visual language: a room info panel with role badges, invite-by-username with a pending-invites list, member management (remove/promote/demote/transfer ownership), room settings (rename/describe/delete), and separate browse-rooms/invites-inbox modals. Unread badges, last-message preview, and the typing indicator are deliberately deferred -- both need new backend features (read-tracking, a WS typing event) that weren't in scope this pass. Two small backend additions round out data the new UI needs but the API didn't expose: MessageRead.username (historic messages had no sender name) and InviteRead.target_username / MyInviteRead.room_name+invited_by_username (a recipient's invite list can't otherwise resolve a room they're not in). Both are additive; 35 backend tests still pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import type { MyRoomItem } from '../types'
|
||||
import { RoomRow } from './RoomRow'
|
||||
import './Sidebar.css'
|
||||
|
||||
interface SidebarProps {
|
||||
rooms: MyRoomItem[]
|
||||
activeRoomId: string | undefined
|
||||
searchQuery: string
|
||||
onSearchChange: (value: string) => void
|
||||
onOpenNewRoom: () => void
|
||||
onOpenBrowse: () => void
|
||||
onOpenInvites: () => void
|
||||
inviteCount: number
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
rooms,
|
||||
activeRoomId,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
onOpenNewRoom,
|
||||
onOpenBrowse,
|
||||
onOpenInvites,
|
||||
inviteCount,
|
||||
}: SidebarProps) {
|
||||
const query = searchQuery.trim().toLowerCase()
|
||||
const filtered = query ? rooms.filter((r) => r.name.toLowerCase().includes(query)) : rooms
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-toolbar">
|
||||
<div className="sidebar-search">
|
||||
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.6" />
|
||||
<line x1="12.5" y1="12.5" x2="17" y2="17" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
||||
</svg>
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder="Search"
|
||||
aria-label="Search rooms"
|
||||
/>
|
||||
</div>
|
||||
<button type="button" className="sidebar-icon-btn" onClick={onOpenNewRoom} aria-label="New room" title="New room">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
||||
<line x1="7" y1="1" x2="7" y2="13" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<line x1="1" y1="7" x2="13" y2="7" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-scroll">
|
||||
<button type="button" className="sidebar-entry" onClick={onOpenInvites}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
|
||||
<path d="M22 6 12 13 2 6" />
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
</svg>
|
||||
Invites
|
||||
{inviteCount > 0 && <span className="sidebar-badge">{inviteCount}</span>}
|
||||
</button>
|
||||
|
||||
<button type="button" className="sidebar-entry" onClick={onOpenBrowse}>
|
||||
<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="6" />
|
||||
<line x1="13.5" y1="13.5" x2="18" y2="18" strokeLinecap="round" />
|
||||
</svg>
|
||||
Browse rooms
|
||||
</button>
|
||||
|
||||
{filtered.length > 0 && <div className="sidebar-section-label">Rooms</div>}
|
||||
<nav>
|
||||
{filtered.map((room, i) => (
|
||||
<RoomRow
|
||||
key={room.id}
|
||||
room={room}
|
||||
colorIndex={i}
|
||||
active={room.id === activeRoomId}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user