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 {
|
.title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 var(--spacing-md);
|
padding: 0 var(--spacing-xs);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-md);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: var(--color-section-title);
|
color: #000000;
|
||||||
text-transform: uppercase;
|
text-transform: none;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: var(--color-settings-card-background);
|
background: var(--color-settings-card-background, #ffffff);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg, 12px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list > li {
|
||||||
|
background: var(--color-settings-row-background, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
padding: var(--spacing-md);
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-settings-text-primary);
|
color: #000000;
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-lg);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row:hover {
|
.row:hover {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row:focus-visible {
|
.row:focus-visible {
|
||||||
@@ -53,7 +57,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
color: var(--color-chevron-icon);
|
color: var(--color-chevron-icon, #000000);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,9 +70,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rowTitle {
|
.rowTitle {
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-lg);
|
||||||
font-weight: 500;
|
font-weight: 400;
|
||||||
color: var(--color-settings-text-primary);
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
@@ -83,19 +87,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
color: var(--color-chevron-icon);
|
color: var(--color-chevron-icon, #000000);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
height: var(--border-light);
|
height: 1px;
|
||||||
background: var(--color-settings-divider);
|
background: rgba(61, 49, 74, 0.5);
|
||||||
margin: 0 var(--spacing-lg);
|
margin: 0 var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.destructive {
|
.destructive {
|
||||||
color: var(--color-destructive);
|
color: var(--color-destructive, #ff6b6b);
|
||||||
}
|
}
|
||||||
.destructive .rowTitle {
|
.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>
|
<span className={styles.trailing}>{item.trailing}</span>
|
||||||
) : null}
|
) : null}
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="20"
|
||||||
height="16"
|
height="20"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -1,159 +1,49 @@
|
|||||||
.shell {
|
.shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background: var(--color-page-background, #ffffff);
|
||||||
|
color: #000;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
background: var(--color-sidebar-background, #fafafa);
|
padding: var(--spacing-xxl, 24px);
|
||||||
color: var(--color-text-primary, #111);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.backRow {
|
||||||
display: flex;
|
padding: var(--spacing-md, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backLink {
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-3, 12px);
|
gap: var(--spacing-xs, 4px);
|
||||||
padding: var(--spacing-4, 16px);
|
color: #000;
|
||||||
border-bottom: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.08));
|
|
||||||
background: var(--color-surface, #fff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.backButton {
|
|
||||||
font-size: 20px;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
font-size: var(--font-size-lg, 16px);
|
||||||
padding: 4px 8px;
|
font-weight: 400;
|
||||||
border-radius: var(--radius-sm, 4px);
|
background: none;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: var(--spacing-3, 12px) var(--spacing-4, 16px);
|
padding: 0;
|
||||||
font-size: var(--font-size-base, 14px);
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsItem:hover,
|
.backLink:hover {
|
||||||
.dangerItem:hover {
|
opacity: 0.7;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dangerItem {
|
.backIcon {
|
||||||
color: var(--color-danger, #dc2626);
|
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";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
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 { 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";
|
import styles from "./sidebar-screen.module.css";
|
||||||
|
|
||||||
@@ -16,122 +22,71 @@ import styles from "./sidebar-screen.module.css";
|
|||||||
*/
|
*/
|
||||||
export function SidebarScreen() {
|
export function SidebarScreen() {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const dispatch = useUserDispatch();
|
const userDispatch = useUserDispatch();
|
||||||
|
const chatDispatch = useChatDispatch();
|
||||||
|
const authDispatch = useAuthDispatch();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 等价 Dart: UserBloc.init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch({ type: "UserInit" });
|
userDispatch({ type: "UserInit" });
|
||||||
}, [dispatch]);
|
}, [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 (
|
return (
|
||||||
<MobileShell>
|
<MobileShell>
|
||||||
<div className={styles.shell}>
|
<div className={styles.shell}>
|
||||||
<header className={styles.header}>
|
{/* 返回按钮:与 Flutter BackButton widget 一致 */}
|
||||||
<Link href="/chat" className={styles.backButton} aria-label="Back">
|
<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>
|
</Link>
|
||||||
<h1 className={styles.title}>Profile</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<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>
|
||||||
<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} />
|
{/* 设置区域:与 Flutter SettingsSection 对齐 */}
|
||||||
</main>
|
<div className={styles.settingsArea}>
|
||||||
|
<SettingsSection
|
||||||
|
title="Other"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
id: "logout",
|
||||||
|
title: "Log out",
|
||||||
|
destructive: true,
|
||||||
|
onClick: () => userDispatch({ type: "UserLogout" }),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MobileShell>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -83,4 +83,11 @@
|
|||||||
|
|
||||||
/* 底部弹层 drag handle */
|
/* 底部弹层 drag handle */
|
||||||
--color-auth-drag-handle: #3a3a3a;
|
--color-auth-drag-handle: #3a3a3a;
|
||||||
|
|
||||||
|
/* Settings section (对齐 Flutter lib/ui/core/settings_section.dart) */
|
||||||
|
--color-settings-card-background: #ffffff;
|
||||||
|
--color-settings-row-background: #f5f5f5;
|
||||||
|
--color-settings-divider: #3d314a;
|
||||||
|
--color-chevron-icon: #000000;
|
||||||
|
--color-destructive: #ff6b6b;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user