extract sidebar

This commit is contained in:
2026-07-15 20:21:53 +08:00
parent 50d29d1e73
commit 979db26e6a
3 changed files with 328 additions and 414 deletions
+46 -234
View File
@@ -1,7 +1,15 @@
import {
Fragment,
FileText,
FolderKanban,
Gamepad2,
Image,
MessageSquare,
Zap,
} from 'lucide-react';
import {
type ChangeEvent,
type FormEvent,
Fragment,
type KeyboardEvent as ReactKeyboardEvent,
type MouseEvent as ReactMouseEvent,
type ReactNode,
@@ -11,33 +19,6 @@ import {
useRef,
useState,
} from 'react';
import {
Bell,
BookOpen,
CircleHelp,
Contact,
FileText,
FolderKanban,
Gamepad2,
Home,
Image,
LogOut,
MessageSquare,
Settings,
User,
Zap,
} from 'lucide-react';
import HomeView, {
type HomeDraft,
type HomeAgentMode,
type HomeAgentModeItem,
type HomeAttachmentDraft,
} from './view/home';
import { useLauncherHomeDraftStore } from './view/home/useHomeDraftStore';
import {
getClientProfileDashboard,
getClientProfileWalletLedger,
} from './services/clientApi';
import type {
AuthEntryResponse,
@@ -48,15 +29,6 @@ import type {
AuthUser,
LogoutResponse,
} from '../../../packages/shared/src/contracts/auth';
import {
API_RESPONSE_ENVELOPE_HEADER,
API_RESPONSE_ENVELOPE_VERSION,
unwrapApiResponse,
} from '../../../packages/shared/src/http';
import type {
ProfileDashboardSummary,
ProfileWalletLedgerEntry,
} from '../../../packages/shared/src/contracts/runtime';
import {
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
@@ -67,21 +39,42 @@ import {
GAME_CREATION_APP_COMMANDS,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
type GameCreationAgentCapabilityDescriptor,
type GameCreationAppAgentGroup,
type GameCreationAppCommandDescriptor,
type GameCreationAppLimitedRunCommandDescriptor,
type GameCreationAgentRepairRouteTrace,
type GameCreationAgentRunStep,
type GameCreationAgentToolCallTrace,
type GameCreationAgentRunTrace,
type GameCreationAgentToolCallTrace,
type GameCreationAppAgentGroup,
type GameCreationAppAssetSourceKind,
type GameCreationAppCommandDescriptor,
type GameCreationAppLimitedRunCommandDescriptor,
type GameCreationAppManifest,
type GameCreationAppPermission,
type GameCreationAppPreviewStatus,
type GameCreationAppTaskStatus,
type GameCreationAppTaskState,
type GameCreationAppTaskStatus,
selectGameCreationAppReadyTasks,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import type {
ProfileDashboardSummary,
ProfileWalletLedgerEntry,
} from '../../../packages/shared/src/contracts/runtime';
import {
API_RESPONSE_ENVELOPE_HEADER,
API_RESPONSE_ENVELOPE_VERSION,
unwrapApiResponse,
} from '../../../packages/shared/src/http';
import {
getClientProfileDashboard,
getClientProfileWalletLedger,
} from './services/clientApi';
import HomeView, {
type HomeAgentMode,
type HomeAgentModeItem,
type HomeAttachmentDraft,
type HomeDraft,
} from './view/home';
import { useLauncherHomeDraftStore } from './view/home/useHomeDraftStore';
import { Sidebar, type LauncherView } from './view/layout';
const seedManifest = createGameCreationAppManifest(
'local-project-draft',
@@ -103,14 +96,6 @@ const launcherNotifications: Array<{
detail: string;
actionLabel: string;
}> = [];
type LauncherView =
| 'home'
| 'projects'
| 'agent-chat'
| 'guide'
| 'contact'
| 'news'
| 'project-development';
type LauncherImportedAttachment = {
fileName: string;
mediaType: string;
@@ -2129,14 +2114,10 @@ export function WorkspaceLauncher({
);
const [walletLedgerStatus, setWalletLedgerStatus] =
useState('尚未读取明细');
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
const [helpMenuOpen, setHelpMenuOpen] = useState(false);
const [launcherNotice, setLauncherNotice] = useState<{
title: string;
message: string;
} | null>(null);
const accountMenuRef = useRef<HTMLDivElement | null>(null);
const helpMenuRef = useRef<HTMLDivElement | null>(null);
const resetLauncherHomeDraft = useLauncherHomeDraftStore(
(state) => state.reset,
);
@@ -2228,44 +2209,6 @@ export function WorkspaceLauncher({
void loadAgentChatLlmStatus();
}, [launcherView, agentChatSelectedAgentId]);
useEffect(() => {
function closeSidebarMenusIfOutside(target: EventTarget | null) {
if (!(target instanceof Node)) {
return;
}
if (
accountMenuRef.current?.contains(target) ||
helpMenuRef.current?.contains(target)
) {
return;
}
setAccountMenuOpen(false);
setHelpMenuOpen(false);
}
function handlePointerDown(event: PointerEvent) {
closeSidebarMenusIfOutside(event.target);
}
function handleFocusIn(event: FocusEvent) {
closeSidebarMenusIfOutside(event.target);
}
function handleWindowBlur() {
setAccountMenuOpen(false);
setHelpMenuOpen(false);
}
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('focusin', handleFocusIn);
window.addEventListener('blur', handleWindowBlur);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('focusin', handleFocusIn);
window.removeEventListener('blur', handleWindowBlur);
};
}, []);
function validateProjectPath(nextProjectPath: string) {
const trimmedProjectPath = nextProjectPath.trim();
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
@@ -2620,8 +2563,6 @@ export function WorkspaceLauncher({
}
function showLauncherNotice(title: string) {
setAccountMenuOpen(false);
setHelpMenuOpen(false);
setWalletPanelOpen(false);
setLauncherNotice({
title,
@@ -3096,144 +3037,17 @@ export function WorkspaceLauncher({
className="launcher-shell platform-theme platform-theme--light"
aria-label="GameAgent 客户端"
>
<aside className="launcher-sidebar">
<div className="launcher-brand" aria-label="陶泥儿">
<span className="launcher-logo">tn</span>
</div>
<nav aria-label="客户端导航">
<button
type="button"
aria-label="首页"
title="首页"
className={launcherView === 'home' ? 'launcher-nav-active' : ''}
onClick={() => setLauncherView('home')}
>
<Home size={17} aria-hidden="true" />
</button>
<button
type="button"
aria-label="项目组"
title="项目组"
className={launcherView === 'projects' ? 'launcher-nav-active' : ''}
onClick={() => setLauncherView('projects')}
>
<FolderKanban size={17} aria-hidden="true" />
</button>
</nav>
<div className="launcher-sidebar-bottom">
<div className="launcher-account-nav" ref={accountMenuRef}>
<button
type="button"
className="launcher-config-button"
onClick={() => {
setHelpMenuOpen(false);
setWalletPanelOpen(false);
setAccountMenuOpen((open) => !open);
}}
aria-expanded={accountMenuOpen}
aria-label="我的"
title="我的"
>
<User size={16} aria-hidden="true" />
</button>
{accountMenuOpen ? (
<div className="launcher-account-popover" role="menu">
<strong>{currentUser.displayName || '陶泥儿用户'}</strong>
<span>
{currentUser.phoneNumberMasked ||
currentUser.publicUserCode ||
'账号信息读取中'}
</span>
<button
type="button"
role="menuitem"
onClick={() => showLauncherNotice('会员与升级')}
>
</button>
<button
type="button"
role="menuitem"
onClick={() => showLauncherNotice('使用指南')}
>
使
</button>
<button
type="button"
role="menuitem"
onClick={() => showLauncherNotice('反馈联系')}
>
</button>
<button
type="button"
role="menuitem"
onClick={() => {
resetLauncherHomeDraft();
onLogout();
}}
>
<LogOut size={14} aria-hidden="true" />
退
</button>
</div>
) : null}
</div>
<div className="launcher-help-nav" ref={helpMenuRef}>
<button
type="button"
aria-label="帮助"
title="帮助"
className="launcher-config-button"
aria-expanded={helpMenuOpen}
onClick={() => {
setAccountMenuOpen(false);
setWalletPanelOpen(false);
setHelpMenuOpen((open) => !open);
}}
>
<CircleHelp size={17} aria-hidden="true" />
</button>
{helpMenuOpen ? (
<div className="launcher-help-menu" role="menu">
<button
type="button"
role="menuitem"
onClick={() => showLauncherNotice('使用指南')}
>
<BookOpen size={14} aria-hidden="true" />
使
</button>
<button
type="button"
role="menuitem"
onClick={() => showLauncherNotice('联系我们')}
>
<Contact size={14} aria-hidden="true" />
</button>
<button
type="button"
role="menuitem"
onClick={() => showLauncherNotice('最新动态')}
>
<Bell size={14} aria-hidden="true" />
</button>
</div>
) : null}
</div>
<button
type="button"
className="launcher-config-button"
onClick={() => setRuntimeConfigOpen(true)}
aria-label="配置"
title="配置"
>
<Settings size={16} aria-hidden="true" />
</button>
</div>
</aside>
<Sidebar
activeView={launcherView}
currentUser={currentUser}
onLogout={() => {
resetLauncherHomeDraft();
onLogout();
}}
onNoticeRequest={showLauncherNotice}
onRuntimeConfigOpen={() => setRuntimeConfigOpen(true)}
onViewChange={setLauncherView}
/>
<section
className={
launcherNotifications.length > 0
@@ -3260,8 +3074,6 @@ export function WorkspaceLauncher({
type="button"
aria-label="泥点余额"
onClick={() => {
setAccountMenuOpen(false);
setHelpMenuOpen(false);
setWalletPanelOpen((open) => !open);
}}
>
+1 -180
View File
@@ -157,38 +157,6 @@ textarea {
color: var(--platform-text-strong);
}
.launcher-shell.platform-theme .launcher-sidebar {
border-color: var(--platform-surface-border);
background: var(--platform-nav-fill);
}
.launcher-shell.platform-theme .launcher-logo {
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
box-shadow: var(--platform-profile-action-shadow);
}
.launcher-shell.platform-theme .launcher-sidebar nav button,
.launcher-shell.platform-theme .launcher-config-button {
color: var(--platform-nav-item-text);
}
.launcher-shell.platform-theme .launcher-sidebar .launcher-nav-active {
border: 1px solid var(--platform-nav-active-border);
background: var(--platform-nav-active-fill);
color: var(--platform-nav-item-text-active);
box-shadow: var(--platform-nav-active-shadow);
}
.launcher-shell.platform-theme .launcher-sidebar nav button:hover,
.launcher-shell.platform-theme .launcher-config-button:hover,
.launcher-shell.platform-theme .launcher-sidebar .launcher-help-menu button:hover,
.launcher-shell.platform-theme .launcher-sidebar .launcher-help-menu button:focus-visible,
.launcher-shell.platform-theme .launcher-sidebar .launcher-account-popover button:hover,
.launcher-shell.platform-theme .launcher-sidebar .launcher-account-popover button:focus-visible {
background: var(--platform-nav-item-hover-fill);
}
.launcher-shell.platform-theme .launcher-main {
background: var(--platform-body-fill);
}
@@ -205,16 +173,13 @@ textarea {
.launcher-shell.platform-theme .launcher-countdown,
.launcher-shell.platform-theme .launcher-account-bar button,
.launcher-shell.platform-theme .launcher-wallet-group,
.launcher-shell.platform-theme .launcher-account-popover,
.launcher-shell.platform-theme .launcher-wallet-popover,
.launcher-shell.platform-theme .launcher-help-menu {
.launcher-shell.platform-theme .launcher-wallet-popover {
border-color: var(--platform-subpanel-border);
background: var(--platform-subpanel-fill);
color: var(--platform-text-strong);
}
.launcher-shell.platform-theme .launcher-account-bar button,
.launcher-shell.platform-theme .launcher-account-popover button,
.launcher-shell.platform-theme .launcher-wallet-popover button,
.launcher-shell.platform-theme .launcher-wallet-ledger div span {
color: var(--platform-text-strong);
@@ -224,146 +189,17 @@ textarea {
border-left-color: var(--platform-subpanel-border);
}
.launcher-shell.platform-theme .launcher-account-popover span,
.launcher-shell.platform-theme .launcher-wallet-popover span,
.launcher-shell.platform-theme .launcher-account-popover small,
.launcher-shell.platform-theme .launcher-wallet-popover small {
color: var(--platform-text-soft);
}
.launcher-sidebar {
position: sticky;
top: 0;
z-index: 40;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
height: 100vh;
padding: 16px 0 18px;
border-right: 1px solid #eceef2;
background: #fff;
overflow: visible;
}
.launcher-brand {
display: grid;
place-items: center;
}
.launcher-logo {
display: grid;
width: 24px;
height: 24px;
border-radius: 50%;
background: #101010;
color: #fff;
font-size: 10px;
font-weight: 700;
letter-spacing: 0;
place-items: center;
}
.launcher-sidebar nav {
display: grid;
gap: 13px;
justify-items: center;
}
.launcher-sidebar-bottom {
position: relative;
z-index: 60;
display: grid;
justify-items: center;
gap: 13px;
margin-top: auto;
}
.launcher-sidebar button,
.launcher-promo button,
.launcher-account-bar button {
border: 0;
cursor: pointer;
}
.launcher-sidebar nav button,
.launcher-config-button {
display: grid;
width: 28px;
height: 28px;
padding: 0;
border-radius: 8px;
background: transparent;
color: #161616;
font-size: 17px;
place-items: center;
}
.launcher-sidebar .launcher-nav-active {
background: #f3f4f6;
}
.launcher-account-nav,
.launcher-help-nav {
position: relative;
}
.launcher-help-menu {
position: absolute;
top: auto;
bottom: -4px;
left: 36px;
display: grid;
min-width: 150px;
gap: 5px;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
box-shadow: 0 14px 34px rgb(15 23 42 / 14%);
z-index: 90;
}
.launcher-sidebar .launcher-help-menu button,
.launcher-sidebar .launcher-account-popover button {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
width: 100%;
height: 30px;
padding: 0 9px;
border-radius: 7px;
background: transparent;
color: #1f2937;
font-size: 12px;
white-space: nowrap;
}
.launcher-sidebar .launcher-help-menu button:hover,
.launcher-sidebar .launcher-help-menu button:focus-visible,
.launcher-sidebar .launcher-account-popover button:hover,
.launcher-sidebar .launcher-account-popover button:focus-visible {
background: #f3f4f6;
}
.launcher-config-button {
font-size: 15px;
}
.launcher-config-button:hover {
background: #f3f4f6;
}
.launcher-help-nav .launcher-config-button,
.launcher-help-nav .launcher-config-button:hover,
.launcher-help-nav .launcher-config-button:focus,
.launcher-help-nav .launcher-config-button:focus-visible,
.launcher-help-nav .launcher-config-button:active,
.launcher-help-nav .launcher-config-button[aria-expanded='true'] {
background: transparent !important;
}
.launcher-main {
position: relative;
display: grid;
@@ -469,7 +305,6 @@ textarea {
display: flex;
}
.launcher-account-popover,
.launcher-wallet-popover {
position: absolute;
top: 34px;
@@ -486,30 +321,16 @@ textarea {
z-index: 90;
}
.launcher-account-nav .launcher-account-popover {
top: auto;
right: auto;
bottom: -4px;
left: 36px;
width: 218px;
gap: 6px;
padding: 10px;
}
.launcher-account-popover strong,
.launcher-wallet-popover strong {
font-size: 13px;
}
.launcher-account-popover span,
.launcher-wallet-popover span,
.launcher-account-popover small,
.launcher-wallet-popover small {
color: #6b7280;
font-size: 11px;
}
.launcher-account-popover button,
.launcher-wallet-popover button {
justify-content: flex-start;
width: 100%;
@@ -0,0 +1,281 @@
import {
Bell,
BookOpen,
CircleHelp,
Contact,
FolderKanban,
Home,
LogOut,
Settings,
User,
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
function cx(...classNames: Array<string | false | null | undefined>) {
return classNames.filter(Boolean).join(' ');
}
const sidebarIconButtonClass =
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text) hover:bg-(--platform-nav-item-hover-fill)';
const sidebarHelpButtonClass =
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text)';
const sidebarMenuItemClass =
'flex h-[30px] w-full cursor-pointer items-center justify-start gap-2 whitespace-nowrap rounded-[7px] border-0 bg-transparent px-[9px] py-0 text-left text-xs text-(--platform-text-strong) hover:bg-(--platform-nav-item-hover-fill) focus-visible:bg-(--platform-nav-item-hover-fill)';
export type LauncherView =
| 'home'
| 'projects'
| 'agent-chat'
| 'guide'
| 'contact'
| 'news'
| 'project-development';
type SidebarUserInfo = {
displayName?: string | null;
phoneNumberMasked?: string | null;
publicUserCode?: string | null;
};
type LauncherSidebarProps = {
activeView: LauncherView;
currentUser: SidebarUserInfo;
onViewChange: (view: LauncherView) => void;
onNoticeRequest: (title: string) => void;
onRuntimeConfigOpen: () => void;
onLogout: () => void;
};
export function Sidebar({
activeView,
currentUser,
onViewChange,
onNoticeRequest,
onRuntimeConfigOpen,
onLogout,
}: LauncherSidebarProps) {
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
const [helpMenuOpen, setHelpMenuOpen] = useState(false);
const accountMenuRef = useRef<HTMLDivElement | null>(null);
const helpMenuRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
function closeSidebarMenusIfOutside(target: EventTarget | null) {
if (!(target instanceof Node)) {
return;
}
if (
accountMenuRef.current?.contains(target) ||
helpMenuRef.current?.contains(target)
) {
return;
}
setAccountMenuOpen(false);
setHelpMenuOpen(false);
}
function handlePointerDown(event: PointerEvent) {
closeSidebarMenusIfOutside(event.target);
}
function handleFocusIn(event: FocusEvent) {
closeSidebarMenusIfOutside(event.target);
}
function handleWindowBlur() {
setAccountMenuOpen(false);
setHelpMenuOpen(false);
}
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('focusin', handleFocusIn);
window.addEventListener('blur', handleWindowBlur);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('focusin', handleFocusIn);
window.removeEventListener('blur', handleWindowBlur);
};
}, []);
function closeMenus() {
setAccountMenuOpen(false);
setHelpMenuOpen(false);
}
function showNotice(title: string) {
closeMenus();
onNoticeRequest(title);
}
function changeView(view: LauncherView) {
closeMenus();
onViewChange(view);
}
return (
<aside className="w-16 sticky top-0 z-40 flex h-screen flex-col items-center gap-8 overflow-visible border-r border-(--platform-surface-border) bg-(--platform-nav-fill) px-0 pb-4.5 pt-4">
<div className="grid place-items-center" aria-label="陶泥儿">
<span className="grid h-6 w-6 place-items-center rounded-full bg-(--platform-button-primary-fill) text-[10px] font-bold tracking-normal text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow)">
{/*// TODO icon*/}
</span>
</div>
<nav className="grid justify-items-center gap-3.25" aria-label="客户端导航">
<button
type="button"
aria-label="首页"
title="首页"
className={cx(
sidebarIconButtonClass,
activeView === 'home' &&
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
)}
onClick={() => changeView('home')}
>
<Home size={17} aria-hidden="true" />
</button>
<button
type="button"
aria-label="项目组"
title="项目组"
className={cx(
sidebarIconButtonClass,
activeView === 'projects' &&
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
)}
onClick={() => changeView('projects')}
>
<FolderKanban size={17} aria-hidden="true" />
</button>
</nav>
<div className="relative z-[60] mt-auto grid justify-items-center gap-[13px]">
<div className="relative" ref={accountMenuRef}>
<button
type="button"
className={sidebarIconButtonClass}
onClick={() => {
setHelpMenuOpen(false);
setAccountMenuOpen((open) => !open);
}}
aria-expanded={accountMenuOpen}
aria-label="我的"
title="我的"
>
<User size={16} aria-hidden="true" />
</button>
{accountMenuOpen ? (
<div
className="absolute bottom-[-4px] left-14 z-[90] grid w-[218px] gap-1.5 rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-2.5 text-(--platform-text-strong) shadow-[0_16px_38px_rgb(15_23_42_/_14%)]"
role="menu"
>
<strong className="text-[13px]">
{currentUser.displayName || '陶泥儿用户'}
</strong>
<span className="text-[11px] text-(--platform-text-soft)">
{currentUser.phoneNumberMasked ||
currentUser.publicUserCode ||
'账号信息读取中'}
</span>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => showNotice('会员与升级')}
>
</button>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => showNotice('使用指南')}
>
使
</button>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => showNotice('反馈联系')}
>
</button>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => {
closeMenus();
onLogout();
}}
>
<LogOut size={14} aria-hidden="true" />
退
</button>
</div>
) : null}
</div>
<div className="relative" ref={helpMenuRef}>
<button
type="button"
aria-label="帮助"
title="帮助"
className={sidebarHelpButtonClass}
aria-expanded={helpMenuOpen}
onClick={() => {
setAccountMenuOpen(false);
setHelpMenuOpen((open) => !open);
}}
>
<CircleHelp size={17} aria-hidden="true" />
</button>
{helpMenuOpen ? (
<div
className="absolute bottom-[-4px] left-14 z-[90] grid min-w-[150px] gap-[5px] rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-2 text-(--platform-text-strong) shadow-[0_14px_34px_rgb(15_23_42_/_14%)]"
role="menu"
>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => showNotice('使用指南')}
>
<BookOpen size={14} aria-hidden="true" />
使
</button>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => showNotice('联系我们')}
>
<Contact size={14} aria-hidden="true" />
</button>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => showNotice('最新动态')}
>
<Bell size={14} aria-hidden="true" />
</button>
</div>
) : null}
</div>
<button
type="button"
className={sidebarIconButtonClass}
onClick={() => {
closeMenus();
onRuntimeConfigOpen();
}}
aria-label="配置"
title="配置"
>
<Settings size={16} aria-hidden="true" />
</button>
</div>
</aside>
);
}