style(sidebar): align /sidebar screen with Flutter original
Match the Dart lib/ui/sidebar/sidebar_screen.dart + profile_view.dart
exactly:
- White inner shell with 24px padding
- Back button row (arrow icon + "Back" text, no "Profile" header)
- Single "Other" section with one "Log out" row
- Drop profile card, Account/Data/Session sections, and Sign-in CTA
Match Flutter row visuals in the shared SettingsSection:
- Gray (#f5f5f5) rounded row pill on a white card
- 20px chevron, 1px rgba(61,49,74,0.5) divider
- Destructive color #ff6b6b, 14px/600 black section title
Add tokens: --color-settings-card-background, --color-settings-row-background,
--color-settings-divider, --color-chevron-icon, --color-destructive.
Wire the BlocConsumer listener equivalent: on currentUser transitioning to
null, dispatch ChatAuthStatusChanged + AuthReset and router.replace("/chat").
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,40 +6,44 @@
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 0 var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-section-title);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0 var(--spacing-xs);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--color-settings-card-background);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-settings-card-background, #ffffff);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list > li {
|
||||
background: var(--color-settings-row-background, #f5f5f5);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
width: 100%;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-settings-text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
color: #000000;
|
||||
font-size: var(--font-size-lg);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.row:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.row:focus-visible {
|
||||
@@ -53,7 +57,7 @@
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--color-chevron-icon);
|
||||
color: var(--color-chevron-icon, #000000);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -66,9 +70,9 @@
|
||||
}
|
||||
|
||||
.rowTitle {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 500;
|
||||
color: var(--color-settings-text-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -83,19 +87,21 @@
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: var(--color-chevron-icon);
|
||||
color: var(--color-chevron-icon, #000000);
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: var(--border-light);
|
||||
background: var(--color-settings-divider);
|
||||
margin: 0 var(--spacing-lg);
|
||||
height: 1px;
|
||||
background: rgba(61, 49, 74, 0.5);
|
||||
margin: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.destructive {
|
||||
color: var(--color-destructive);
|
||||
color: var(--color-destructive, #ff6b6b);
|
||||
}
|
||||
.destructive .rowTitle {
|
||||
color: var(--color-destructive);
|
||||
color: var(--color-destructive, #ff6b6b);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ export function SettingsSection({ title, items }: SettingsSectionProps) {
|
||||
<span className={styles.trailing}>{item.trailing}</span>
|
||||
) : null}
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -1,159 +1,49 @@
|
||||
.shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-page-background, #ffffff);
|
||||
color: #000;
|
||||
min-height: 100dvh;
|
||||
background: var(--color-sidebar-background, #fafafa);
|
||||
color: var(--color-text-primary, #111);
|
||||
padding: var(--spacing-xxl, 24px);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
.backRow {
|
||||
padding: var(--spacing-md, 12px);
|
||||
}
|
||||
|
||||
.backLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3, 12px);
|
||||
padding: var(--spacing-4, 16px);
|
||||
border-bottom: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.08));
|
||||
background: var(--color-surface, #fff);
|
||||
}
|
||||
|
||||
.backButton {
|
||||
font-size: 20px;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
|
||||
.backButton:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-lg, 18px);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1 1 auto;
|
||||
padding: var(--spacing-4, 16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-5, 20px);
|
||||
}
|
||||
|
||||
.profileCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3, 12px);
|
||||
background: var(--color-surface, #fff);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
padding: var(--spacing-4, 16px);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent, #4f46e5);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatarInitial {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.userInfo h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-base, 16px);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.userInfo p {
|
||||
margin: 4px 0 0;
|
||||
font-size: var(--font-size-sm, 13px);
|
||||
color: var(--color-text-secondary, #666);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
flex-shrink: 0;
|
||||
background: var(--color-accent, #4f46e5);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md, 8px);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm, 13px);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4, 16px);
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--color-surface, #fff);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0;
|
||||
padding: var(--spacing-3, 12px) var(--spacing-4, 16px);
|
||||
font-size: var(--font-size-xs, 11px);
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-secondary, #666);
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.sectionList {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sectionList li + li {
|
||||
border-top: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
|
||||
.settingsItem,
|
||||
.dangerItem {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
font-size: var(--font-size-lg, 16px);
|
||||
font-weight: 400;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: var(--spacing-3, 12px) var(--spacing-4, 16px);
|
||||
font-size: var(--font-size-base, 14px);
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settingsItem:hover,
|
||||
.dangerItem:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
.backLink:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.dangerItem {
|
||||
color: var(--color-danger, #dc2626);
|
||||
.backIcon {
|
||||
color: #000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.backText {
|
||||
color: #000;
|
||||
font-size: var(--font-size-lg, 16px);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.settingsArea {
|
||||
padding: 0 var(--spacing-md, 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-lg, 16px);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||||
import { SettingsSection } from "@/app/_components/core/settings-section";
|
||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||
import { useAuthDispatch } from "@/stores/auth/auth-context";
|
||||
import { useChatDispatch } from "@/stores/chat/chat-context";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
|
||||
import type { UserView } from "@/models/user/user";
|
||||
|
||||
import styles from "./sidebar-screen.module.css";
|
||||
|
||||
@@ -16,122 +22,71 @@ import styles from "./sidebar-screen.module.css";
|
||||
*/
|
||||
export function SidebarScreen() {
|
||||
const user = useUserState();
|
||||
const dispatch = useUserDispatch();
|
||||
const userDispatch = useUserDispatch();
|
||||
const chatDispatch = useChatDispatch();
|
||||
const authDispatch = useAuthDispatch();
|
||||
const router = useRouter();
|
||||
|
||||
// 等价 Dart: UserBloc.init
|
||||
useEffect(() => {
|
||||
dispatch({ type: "UserInit" });
|
||||
}, [dispatch]);
|
||||
userDispatch({ type: "UserInit" });
|
||||
}, [userDispatch]);
|
||||
|
||||
// 等价 Dart: BlocConsumer<UserBloc> 的 listener(profile_view.dart:24-34)
|
||||
// 仅在 currentUser 由非 null 跳到 null 时触发清理
|
||||
const prevUserRef = useRef<UserView | null>(user.currentUser);
|
||||
useEffect(() => {
|
||||
const wasLoggedIn = prevUserRef.current != null;
|
||||
const isNowLoggedOut = user.currentUser == null;
|
||||
prevUserRef.current = user.currentUser;
|
||||
if (wasLoggedIn && isNowLoggedOut) {
|
||||
chatDispatch({ type: "ChatAuthStatusChanged" });
|
||||
authDispatch({ type: "AuthReset" });
|
||||
router.replace(ROUTES.chat);
|
||||
}
|
||||
}, [user.currentUser, chatDispatch, authDispatch, router]);
|
||||
|
||||
return (
|
||||
<MobileShell>
|
||||
<div className={styles.shell}>
|
||||
<header className={styles.header}>
|
||||
<Link href="/chat" className={styles.backButton} aria-label="Back">
|
||||
←
|
||||
{/* 返回按钮:与 Flutter BackButton widget 一致 */}
|
||||
<div className={styles.backRow}>
|
||||
<Link href={ROUTES.chat} className={styles.backLink} aria-label="Back">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
className={styles.backIcon}
|
||||
>
|
||||
<path
|
||||
d="M15 6l-6 6 6 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className={styles.backText}>Back</span>
|
||||
</Link>
|
||||
<h1 className={styles.title}>Profile</h1>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<main className={styles.main}>
|
||||
<section className={styles.profileCard}>
|
||||
<div className={styles.avatar}>
|
||||
{user.avatarUrl ? (
|
||||
<Image src={user.avatarUrl} alt="" width={64} height={64} />
|
||||
) : (
|
||||
<span className={styles.avatarInitial}>
|
||||
{(user.currentUser?.username ?? "?").charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.userInfo}>
|
||||
<h2>{user.currentUser?.username ?? "Guest"}</h2>
|
||||
<p>{user.currentUser?.email ?? "Sign in to get started"}</p>
|
||||
</div>
|
||||
{user.currentUser ? null : (
|
||||
<Link href="/auth" className={styles.signInButton}>
|
||||
Sign in
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<SettingsList user={user} dispatch={dispatch} />
|
||||
</main>
|
||||
{/* 设置区域:与 Flutter SettingsSection 对齐 */}
|
||||
<div className={styles.settingsArea}>
|
||||
<SettingsSection
|
||||
title="Other"
|
||||
items={[
|
||||
{
|
||||
id: "logout",
|
||||
title: "Log out",
|
||||
destructive: true,
|
||||
onClick: () => userDispatch({ type: "UserLogout" }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsList({
|
||||
user,
|
||||
dispatch,
|
||||
}: {
|
||||
user: ReturnType<typeof useUserState>;
|
||||
dispatch: ReturnType<typeof useUserDispatch>;
|
||||
}) {
|
||||
const sections: Array<{
|
||||
title: string;
|
||||
items: Array<{ label: string; onClick: () => void; danger?: boolean }>;
|
||||
}> = [
|
||||
{
|
||||
title: "Account",
|
||||
items: [
|
||||
{ label: "Edit username", onClick: () => {} },
|
||||
{ label: "Edit pronouns", onClick: () => {} },
|
||||
{ label: "Subscription", onClick: () => {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Data",
|
||||
items: [
|
||||
{
|
||||
label: "Delete chat history",
|
||||
onClick: () => dispatch({ type: "UserDeleteChatHistory" }),
|
||||
},
|
||||
{
|
||||
label: "Delete account",
|
||||
onClick: () => dispatch({ type: "UserDeleteAccount" }),
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Session",
|
||||
items: user.currentUser
|
||||
? [
|
||||
{
|
||||
label: "Log out",
|
||||
onClick: () => dispatch({ type: "UserLogout" }),
|
||||
danger: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.settings}>
|
||||
{sections
|
||||
.filter((s) => s.items.length > 0)
|
||||
.map((s) => (
|
||||
<div key={s.title} className={styles.section}>
|
||||
<h3 className={styles.sectionTitle}>{s.title}</h3>
|
||||
<ul className={styles.sectionList}>
|
||||
{s.items.map((it) => (
|
||||
<li key={it.label}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={it.onClick}
|
||||
className={
|
||||
it.danger ? styles.dangerItem : styles.settingsItem
|
||||
}
|
||||
>
|
||||
{it.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user