From 53d21381d98008d46e354c4fc2c04cd51aa2d80e Mon Sep 17 00:00:00 2001 From: hzhang Date: Fri, 15 May 2026 09:26:11 +0100 Subject: [PATCH] feat(frontend): Discord-style dark redesign - full design system rewrite (dark theme, system tokens) - chat shell: server rail / channel sidebar / message area / members - message rows with avatars, author + time; empty/loading states - login screen redesign; add-guild moved to a modal - removed debug v1/v2/v4 panel toggles; members toggle in topbar Co-Authored-By: Claude Opus 4.7 (1M context) --- src/index.css | 816 ++++++++++++++++++++++++++------------ src/layouts/AppLayout.tsx | 16 +- src/pages/ChatPage.tsx | 309 ++++++++++----- src/pages/LoginPage.tsx | 63 ++- 4 files changed, 822 insertions(+), 382 deletions(-) diff --git a/src/index.css b/src/index.css index bc7c327..8b7f744 100644 --- a/src/index.css +++ b/src/index.css @@ -1,314 +1,642 @@ :root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + --rail: #18191c; + --side: #1f2125; + --main: #26282d; + --elevated: #1a1b1e; + --input: #1a1b1e; + --hover: rgba(255, 255, 255, 0.04); + --active: rgba(168, 85, 247, 0.16); - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; + --text: #dbdee1; + --text-muted: #949ba4; + --text-faint: #6d7178; + --text-h: #f2f3f5; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); + --border: #303237; + --accent: #a855f7; + --accent-strong: #9333ea; + --accent-soft: rgba(168, 85, 247, 0.14); + --danger: #f23f42; + --online: #23a55a; + + --radius: 10px; + --radius-sm: 7px; + --sans: 'Inter', system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, 'SF Mono', Consolas, monospace; + + color-scheme: dark; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } } -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; +* { box-sizing: border-box; } -body { +html, +body, +#root { + height: 100%; margin: 0; - background: linear-gradient(180deg, rgba(170, 59, 255, 0.08), transparent 220px) no-repeat var(--bg); +} + +body { + font: 15px/1.5 var(--sans); + color: var(--text); + background: var(--main); +} + +#root { + width: 100%; + display: flex; + flex-direction: column; } a { color: var(--accent); text-decoration: none; } - a:hover { text-decoration: underline; } -.page-content { - padding: 18px; - text-align: left; -} - -.chat-layout { - display: grid; - gap: 12px; -} - -.chat-top-grid { - display: grid; - grid-template-columns: 220px 260px 1fr 220px; - gap: 12px; - min-height: 68vh; -} - -.chat-top-grid.hide-v1 { - grid-template-columns: 0 260px 1fr 220px; -} - -.chat-top-grid.hide-v2 { - grid-template-columns: 220px 0 1fr 220px; -} - -.chat-top-grid.hide-v4 { - grid-template-columns: 220px 260px 1fr 0; -} - -.chat-top-grid.hide-v1.hide-v2 { - grid-template-columns: 0 0 1fr 220px; -} - -.chat-top-grid.hide-v1.hide-v4 { - grid-template-columns: 0 260px 1fr 0; -} - -.chat-top-grid.hide-v2.hide-v4 { - grid-template-columns: 220px 0 1fr 0; -} - -.chat-top-grid.hide-v1.hide-v2.hide-v4 { - grid-template-columns: 0 0 1fr 0; -} - -.chat-top-grid.hide-v1 .col-v1, -.chat-top-grid.hide-v2 .col-v2, -.chat-top-grid.hide-v4 .col-v4 { - display: none; -} - -.chat-right { - display: grid; - grid-template-rows: 1fr auto; - gap: 12px; - min-height: 0; -} - -.chat-history { - overflow: auto; -} - -.composer { - display: flex; - gap: 8px; -} - -.composer .input { - flex: 1; -} - -.footer-actions { - display: flex; - gap: 8px; -} - -.modal-backdrop { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.45); - display: grid; - place-items: center; - z-index: 30; -} - -.modal-card { - width: min(560px, 92vw); - max-height: 80vh; - overflow: auto; - background: var(--bg); - border: 1px solid var(--border); - border-radius: 12px; - padding: 14px; -} - -.modal-list { - display: grid; - gap: 8px; - margin-top: 8px; -} - -.list-btn { - width: 100%; - text-align: left; - border: 1px solid var(--border); - background: transparent; - border-radius: 8px; - padding: 8px 10px; +h1, +h2, +h3, +h4 { color: var(--text-h); - cursor: pointer; + font-weight: 600; + margin: 0; } -.list-btn.active { - border-color: var(--accent-border); - background: var(--accent-bg); +p { + margin: 0; } -.panel { - border: 1px solid var(--border); - border-radius: 12px; - padding: 16px; - box-shadow: var(--shadow); - background: var(--bg); +button { + font-family: inherit; } -.form-grid { - display: grid; - gap: 10px; -} - -.row-wrap { - display: flex; - gap: 8px; - flex-wrap: wrap; +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-thumb { + background: #00000050; + border-radius: 8px; +} +::-webkit-scrollbar-track { + background: transparent; } +/* ---------- shared controls ---------- */ .input { - border: 1px solid var(--border); - border-radius: 8px; - padding: 8px 10px; - min-width: 120px; - background: transparent; + width: 100%; + border: 1px solid transparent; + border-radius: var(--radius-sm); + padding: 10px 12px; + background: var(--input); color: var(--text-h); + font-size: 14px; + outline: none; + transition: border-color 0.12s ease; +} +.input::placeholder { + color: var(--text-faint); } - .input:focus { - outline: 2px solid var(--accent-border); - outline-offset: 1px; + border-color: var(--accent); } .btn { - border: 1px solid var(--accent-border); - background: var(--accent); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + border: none; + border-radius: var(--radius-sm); + padding: 10px 16px; + font-size: 14px; + font-weight: 600; color: #fff; - border-radius: 8px; - padding: 8px 12px; + background: var(--accent); cursor: pointer; + transition: + background 0.12s ease, + opacity 0.12s ease, + transform 0.05s ease; +} +.btn:hover { + background: var(--accent-strong); +} +.btn:active { + transform: translateY(1px); +} +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; } - .btn-secondary { - background: transparent; + background: #3a3d44; + color: var(--text-h); +} +.btn-secondary:hover { + background: #44474f; +} +.btn-ghost { + background: transparent; + color: var(--text-muted); +} +.btn-ghost:hover { + background: var(--hover); color: var(--text-h); } - .btn-danger { - background: #dc2626; - border-color: #dc2626; + background: var(--danger); +} +.btn-danger:hover { + background: #d83a3d; } .muted { - color: var(--text); + color: var(--text-muted); } - .error-text { - color: #dc2626; - margin-top: 10px; + color: #f87171; } - .list-reset { list-style: none; padding: 0; margin: 0; } -.card { - border: 1px solid var(--border); - border-radius: 10px; - padding: 10px; +/* ---------- login ---------- */ +.login-screen { + flex: 1; + min-height: 100vh; display: grid; - gap: 8px; - background: color-mix(in srgb, var(--bg) 95%, var(--accent) 5%); + place-items: center; + padding: 24px; + background: + radial-gradient(900px 500px at 50% -10%, rgba(168, 85, 247, 0.22), transparent 60%), + var(--main); } - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); +.login-card { + width: min(420px, 100%); + background: var(--side); + border: 1px solid var(--border); + border-radius: 16px; + padding: 32px; + box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.6); } - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { +.login-card h1 { font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } + margin-bottom: 4px; } -p { - margin: 0; +.login-sub { + color: var(--text-muted); + font-size: 14px; + margin-bottom: 24px; +} +.field { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 16px; +} +.field label { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-muted); +} +.login-card .btn { + width: 100%; + margin-top: 4px; } -code, -.counter { - font-family: var(--mono); - display: inline-flex; +/* ---------- chat shell ---------- */ +.dc-shell { + flex: 1; + height: 100vh; + display: flex; + overflow: hidden; + background: var(--main); +} + +/* server rail */ +.dc-rail { + width: 72px; + background: var(--rail); + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px 0; + overflow-y: auto; +} +.dc-rail::-webkit-scrollbar { + display: none; +} +.rail-btn { + position: relative; + width: 48px; + height: 48px; + border: none; + border-radius: 24px; + background: var(--side); + color: var(--text-h); + font-weight: 700; + font-size: 15px; + cursor: pointer; + transition: + border-radius 0.15s ease, + background 0.15s ease; +} +.rail-btn:hover { + border-radius: 16px; + background: var(--accent); + color: #fff; +} +.rail-btn.active { + border-radius: 16px; + background: var(--accent); + color: #fff; +} +.rail-btn.active::before { + content: ''; + position: absolute; + left: -12px; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 28px; + background: #fff; + border-radius: 0 4px 4px 0; +} +.rail-add { + background: transparent; + color: var(--online); + border: 1px solid var(--border); + font-size: 22px; + line-height: 1; +} +.rail-add:hover { + background: var(--online); + color: #fff; +} +.rail-sep { + width: 32px; + height: 1px; + background: var(--border); + flex: none; +} + +/* channel sidebar */ +.dc-sidebar { + width: 248px; + background: var(--side); + display: flex; + flex-direction: column; +} +.dc-sidebar-head { + height: 50px; + padding: 0 16px; + display: flex; + align-items: center; + font-weight: 700; + color: var(--text-h); + border-bottom: 1px solid #00000040; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); +} +.dc-sidebar-scroll { + flex: 1; + overflow-y: auto; + padding: 12px 8px; +} +.dc-section-label { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 8px 4px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-faint); +} +.dc-section-label button { + border: none; + background: transparent; + color: var(--text-faint); + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} +.dc-section-label button:hover { + color: var(--text-h); +} +.chan-btn { + width: 100%; + display: flex; + align-items: center; + gap: 6px; + text-align: left; + border: none; + background: transparent; + border-radius: var(--radius-sm); + padding: 8px 10px; + margin: 1px 0; + color: var(--text-muted); + font-size: 15px; + cursor: pointer; +} +.chan-btn .hash { + color: var(--text-faint); + font-weight: 600; +} +.chan-btn:hover { + background: var(--hover); + color: var(--text); +} +.chan-btn.active { + background: var(--active); + color: var(--text-h); +} +.chan-tag { + margin-left: auto; + font-size: 10px; + font-weight: 700; + color: var(--accent); + border: 1px solid var(--accent); border-radius: 4px; + padding: 0 4px; +} +.dc-empty { + color: var(--text-faint); + font-size: 13px; + padding: 8px 10px; +} + +/* user bar */ +.dc-userbar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: var(--elevated); +} +.dc-userbar .who { + flex: 1; + min-width: 0; +} +.dc-userbar .who .nm { + color: var(--text-h); + font-size: 13px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.dc-userbar .who .sub { + color: var(--text-muted); + font-size: 11px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.icon-btn { + width: 32px; + height: 32px; + border: none; + border-radius: 7px; + background: transparent; + color: var(--text-muted); + font-size: 15px; + cursor: pointer; +} +.icon-btn:hover { + background: var(--hover); color: var(--text-h); } -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); +/* avatar */ +.avatar { + flex: none; + width: 32px; + height: 32px; + border-radius: 50%; + display: grid; + place-items: center; + font-size: 13px; + font-weight: 700; + color: #fff; + background: linear-gradient(135deg, var(--accent), #6d28d9); +} +.avatar.lg { + width: 40px; + height: 40px; + font-size: 15px; +} + +/* main column */ +.dc-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + background: var(--main); +} +.dc-topbar { + height: 50px; + flex: none; + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px; + border-bottom: 1px solid #00000040; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); +} +.dc-topbar .title { + font-weight: 700; + color: var(--text-h); +} +.dc-topbar .hash { + color: var(--text-faint); + margin-right: 2px; +} +.dc-topbar .spacer { + flex: 1; +} + +.dc-messages { + flex: 1; + overflow-y: auto; + padding: 18px 16px; + display: flex; + flex-direction: column; + gap: 2px; +} +.msg { + display: flex; + gap: 12px; + padding: 6px 8px; + border-radius: 8px; +} +.msg:hover { + background: rgba(0, 0, 0, 0.12); +} +.msg .body { + min-width: 0; +} +.msg .meta { + display: flex; + align-items: baseline; + gap: 8px; +} +.msg .author { + font-weight: 600; + color: var(--text-h); + font-size: 14px; +} +.msg .time { + font-size: 11px; + color: var(--text-faint); +} +.msg .text { + color: var(--text); + word-break: break-word; +} +.msg.deleted .text { + color: var(--text-faint); + font-style: italic; +} +.dc-empty-center { + margin: auto; + text-align: center; + color: var(--text-faint); +} + +/* composer */ +.dc-composer { + flex: none; + display: flex; + gap: 10px; + padding: 0 16px 20px; +} +.dc-composer .input { + background: #303237; + padding: 12px 14px; +} +.dc-composer .btn { + flex: none; +} + +/* members */ +.dc-members { + width: 232px; + background: var(--side); + padding: 16px 8px; + overflow-y: auto; +} +.dc-members .dc-section-label { + cursor: default; +} +.member-row { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 8px; + border-radius: var(--radius-sm); +} +.member-row:hover { + background: var(--hover); +} +.member-row .nm { + color: var(--text); + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.member-row .you { + color: var(--text-faint); + font-size: 11px; + margin-left: 4px; +} +.dot { + position: relative; +} +.dot::after { + content: ''; + position: absolute; + right: -1px; + bottom: -1px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--online); + border: 2px solid var(--side); +} + +/* ---------- modal ---------- */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(2px); + display: grid; + place-items: center; + z-index: 50; + padding: 20px; +} +.modal-card { + width: min(460px, 100%); + max-height: 84vh; + overflow: auto; + background: var(--side); + border: 1px solid var(--border); + border-radius: 14px; + padding: 22px; + box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.7); +} +.modal-card h3 { + font-size: 18px; + margin-bottom: 14px; +} +.modal-list { + display: flex; + flex-direction: column; + gap: 2px; + margin: 10px 0; + max-height: 240px; + overflow: auto; +} +.check-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: var(--radius-sm); + cursor: pointer; +} +.check-row:hover { + background: var(--hover); +} +.check-row input { + accent-color: var(--accent); + width: 16px; + height: 16px; +} +.modal-actions { + display: flex; + gap: 10px; + margin-top: 18px; +} +.modal-actions .btn { + flex: 1; } diff --git a/src/layouts/AppLayout.tsx b/src/layouts/AppLayout.tsx index fa9d91b..47c651b 100644 --- a/src/layouts/AppLayout.tsx +++ b/src/layouts/AppLayout.tsx @@ -1,17 +1,5 @@ -import { Link, Outlet } from 'react-router-dom' -import { useAuth } from '../auth/auth-context' +import { Outlet } from 'react-router-dom' export default function AppLayout() { - const { isAuthed } = useAuth() - - return ( -
- {!isAuthed ? ( - - ) : null} - -
- ) + return } diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 0f5071b..e48aee1 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -9,6 +9,20 @@ type MessageItem = { seq: number content: string isDeleted?: boolean + authorUserId?: string + createdAt?: string +} + +function initials(s: string): string { + const t = (s || '?').trim() + return t.slice(0, 2).toUpperCase() +} + +function timeOf(iso?: string): string { + if (!iso) return '' + const d = new Date(iso) + if (Number.isNaN(d.getTime())) return '' + return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) } type GuildChannel = { id: string; name: string; guildId?: string } @@ -29,9 +43,8 @@ export default function ChatPage() { const [newChannelPublic, setNewChannelPublic] = useState(false) const [showCreateChannelModal, setShowCreateChannelModal] = useState(false) const [showSettingsModal, setShowSettingsModal] = useState(false) + const [showAddGuildModal, setShowAddGuildModal] = useState(false) const [settingsName, setSettingsName] = useState('') - const [showGuilds, setShowGuilds] = useState(true) - const [showChannels, setShowChannels] = useState(true) const [showMembers, setShowMembers] = useState(true) const [loading, setLoading] = useState(false) const [error, setError] = useState('') @@ -143,6 +156,7 @@ export default function ChatPage() { await joinGuildCenter(session.centerApiBase, token, joinGuildNodeId.trim()) await refreshGuilds() setJoinGuildNodeId('') + setShowAddGuildModal(false) } catch { setError('Failed to add guild') } @@ -231,111 +245,197 @@ export default function ChatPage() { } }, [socket, selectedChannelId]) + const currentChannel = channels.find((c) => c.id === selectedChannelId) ?? null + const nameById = new Map(members.map((m) => [m.userId, m.name || m.email])) + const authorLabel = (uid?: string) => + uid ? (uid === session?.user.id ? session?.user.name || 'You' : nameById.get(uid) || uid.slice(0, 8)) : 'unknown' + return ( -
-
-
-

Guilds

-
- setJoinGuildNodeId(e.target.value)} placeholder="Guild nodeId" /> - +
+ + + + +
+
+ {currentChannel ? ( + + # + {currentChannel.name} + + ) : ( + Select a channel + )} + + +
+ +
+ {loading ?

Loading…

: null} + {!loading && !messages.length ? ( +
+ {currentChannel ? `This is the start of #${currentChannel.name}` : 'No channel selected'} +
+ ) : null} + {messages.map((m) => ( +
+
{initials(authorLabel(m.authorUserId))}
+
+
+ {authorLabel(m.authorUserId)} + {timeOf(m.createdAt)} · #{m.seq} +
+
{m.content}
+
+
+ ))}
-
-

Channels

-
- -
-
    - {channels.map((c) => ( -
  • - -
  • - ))} -
-
- -
-
-

Messages

- {loading ?

Loading...

: null} -
    - {messages.map((m) => ( -
  • -
    - #{m.seq} {m.content} -
    -
  • - ))} -
-
-
- setContent(e.target.value)} placeholder="Type a message" /> - -
-
- -
-

Members

-
    - {members.map((m) => ( -
  • - - {m.name || m.email} - {m.userId === session?.user.id ? ' (you)' : ''} - -
  • - ))} -
-
-
- -
- - - - - -
+ setContent(e.target.value)} + placeholder={currentChannel ? `Message #${currentChannel.name}` : 'Select a channel first'} + disabled={!currentChannel} + /> + + + + + {showMembers ? ( + + ) : null} + + {showAddGuildModal ? ( +
setShowAddGuildModal(false)}> +
e.stopPropagation()}> +

Add guild

+

Enter the guild node id to join.

+ setJoinGuildNodeId(e.target.value)} + placeholder="e.g. test-guild1" + /> +
+ + +
+
+
+ ) : null} {showCreateChannelModal ? (
setShowCreateChannelModal(false)}>
e.stopPropagation()}>

Create channel

- setNewChannelName(e.target.value)} placeholder="Channel name" /> -
+ ) } diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index bfdc8fc..f0054e4 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -23,27 +23,46 @@ export default function LoginPage() { } return ( -
-

Login

- {isAuthed ?

Current user: {session?.user.email}

: null} -
- setCenterApiBase(e.target.value)} - placeholder="Center API Base (e.g. http://localhost:7001/api)" - /> - setEmail(e.target.value)} placeholder="Email" type="email" /> - setPassword(e.target.value)} - placeholder="Password" - type="password" - /> - -
- {error ?

{error}

: null} -
+
+
+

Welcome back

+

+ {isAuthed ? `Signed in as ${session?.user.email}` : 'Sign in to continue to Fabric'} +

+
+
+ + setCenterApiBase(e.target.value)} + placeholder="http://localhost:7001/api" + /> +
+
+ + setEmail(e.target.value)} + placeholder="you@example.com" + type="email" + /> +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + type="password" + /> +
+ {error ?

{error}

: null} + +
+
+
) }