Files
Jyotisha/frontend/src/components/account-dialog-overlay.tsx
T
Jesse_ChenandCursor dc6598d7e4 fix(web): keep the settings dialog one size and move billing into it (BUG-554)
The four account panes now share a fixed frame, chart profiles open as list then detail, and membership lives in the homepage dialog instead of a separate page.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 18:29:51 +08:00

100 lines
3.4 KiB
TypeScript

"use client";
import { ChevronRight, Settings, UserRound, Users, WalletCards, X } from "lucide-react";
import { memo, type MutableRefObject, type ReactNode } from "react";
export type AccountSettingsDialog = "profile" | "chart-library" | "billing" | "general";
export type AccountOverlayModel = Readonly<{
title: string;
dialogClass: string;
signingOut: boolean;
close: () => void;
navigate: (dialog: AccountSettingsDialog) => void;
overlayRef: MutableRefObject<HTMLElement | null>;
closeButtonRef: MutableRefObject<HTMLButtonElement | null>;
renderProfile: () => ReactNode;
renderChartLibrary: () => ReactNode;
renderBilling: () => ReactNode;
renderGeneral: () => ReactNode;
renderLogout: () => ReactNode;
}>;
const settingsItems: ReadonlyArray<{
dialog: AccountSettingsDialog;
label: string;
icon: typeof UserRound;
}> = [
{ dialog: "profile", label: "个人资料", icon: UserRound },
{ dialog: "chart-library", label: "星盘资料", icon: Users },
{ dialog: "billing", label: "账户与点数", icon: WalletCards },
{ dialog: "general", label: "通用设置", icon: Settings },
];
export const AccountDialogOverlay = memo(function AccountDialogOverlay({
open,
dialog,
model,
}: Readonly<{
open: boolean;
dialog: "profile" | "chart-library" | "billing" | "general" | "logout" | null;
model: AccountOverlayModel | null;
}>) {
if (!open || dialog === null || model === null) return null;
const isSettingsDialog = dialog !== "logout";
const renderSettingsContent = () => {
if (dialog === "profile") return model.renderProfile();
if (dialog === "chart-library") return model.renderChartLibrary();
if (dialog === "billing") return model.renderBilling();
return model.renderGeneral();
};
return (
<div className="account-modal-overlay" onMouseDown={model.close}>
<section
className={`account-modal ${model.dialogClass}`}
ref={model.overlayRef}
role="dialog"
aria-modal="true"
aria-labelledby="account-dialog-title"
onMouseDown={(event) => event.stopPropagation()}
>
<header className="account-modal-header">
<h2 id="account-dialog-title">{model.title}</h2>
<button
className="dialog-close"
ref={model.closeButtonRef}
aria-label="关闭"
type="button"
onClick={model.close}
disabled={model.signingOut}
>
<X aria-hidden="true" />
</button>
</header>
{isSettingsDialog ? (
<div className="account-settings-shell">
<nav className="settings-dialog-nav" aria-label="设置分区">
{settingsItems.map(({ dialog: itemDialog, label, icon: Icon }) => (
<button
className="settings-dialog-nav-item"
key={itemDialog}
type="button"
aria-current={dialog === itemDialog ? "page" : undefined}
onClick={() => model.navigate(itemDialog)}
>
<Icon aria-hidden="true" />
<span>{label}</span>
<ChevronRight aria-hidden="true" />
</button>
))}
</nav>
<div className="settings-dialog-content">{renderSettingsContent()}</div>
</div>
) : model.renderLogout()}
</section>
</div>
);
});