Unverified 提交 05e5403f authored 作者: wwwillchen-bot's avatar wwwillchen-bot 提交者: GitHub

move ActionHeader from TitleBar to right side bar (#2553)

## Summary - Move the ActionHeader component from TitleBar into PreviewPanel where it logically belongs - Improve component organization by separating preview-related controls from the global title bar - Update ActionHeader styling to fit its new location with sidebar background ## Test plan - Verify the ActionHeader appears at the top of the PreviewPanel instead of in the title bar - Confirm the tab switching (Preview/Code/Console) functionality works correctly - Test the refresh and external link buttons work as expected - Verify the title bar still renders correctly with the spacer element 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2553" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Moved the preview action header into the PreviewPanel and added a right action sidebar for quick mode switching. Maintenance actions moved to a TitleBar “More” menu so controls are closer to their content. - New Features - RightActionSidebar on the chat page with buttons for Preview, Problems (with count), Code, Configure, Security, Publish. - TitleBar actions: Chat activity button and a “More” menu for Clean Rebuild and Clear Preview Data. - Refactors - Render ActionHeader at the top of PreviewPanel; remove it from TitleBar, drop useLocation, and add a spacer. - Polish styles for ActionHeader (px-2, sidebar background) and sidebar active/hover states. <sup>Written for commit 5cc7c53453f280024838a43fdd89a357abca3523. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com> Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 552a705d
差异被折叠。
......@@ -39,5 +39,7 @@ gh api repos/dyad-sh/dyad/issues/{PR_NUMBER}/labels -f "labels[]=label-name"
## Rebase conflict resolution tips
- **Before rebasing:** If `npm install` modified `package-lock.json` (common in CI/local), discard changes with `git restore package-lock.json` to avoid "unstaged changes" errors
- When resolving import conflicts (e.g., `<<<<<<< HEAD` with different imports), keep **both** imports if both are valid and needed by the component
- When resolving conflicts in i18n-related commits, watch for duplicate constant definitions that conflict with imports from `@/lib/schemas` (e.g., `DEFAULT_ZOOM_LEVEL`)
- If both sides of a conflict have valid imports/hooks, keep both and remove any duplicate constant redefinitions
import { useAtom } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadApps } from "@/hooks/useLoadApps";
import { useRouter, useLocation } from "@tanstack/react-router";
import { useRouter } from "@tanstack/react-router";
import { useSettings } from "@/hooks/useSettings";
import { Button } from "@/components/ui/button";
// @ts-ignore
......@@ -9,7 +9,7 @@ import logo from "../../assets/logo.svg";
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
import { cn } from "@/lib/utils";
import { useDeepLink } from "@/contexts/DeepLinkContext";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog";
import { useTheme } from "@/contexts/ThemeContext";
import { ipc } from "@/ipc/types";
......@@ -21,13 +21,23 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ActionHeader } from "@/components/preview_panel/ActionHeader";
import { ChatActivityButton } from "@/components/chat/ChatActivity";
import { MoreVertical, Cog, Trash2 } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useRunApp } from "@/hooks/useRunApp";
import { showError, showSuccess } from "@/lib/toast";
import { useMutation } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
export const TitleBar = () => {
const [selectedAppId] = useAtom(selectedAppIdAtom);
const { apps } = useLoadApps();
const { navigate } = useRouter();
const location = useLocation();
const { settings, refreshSettings } = useSettings();
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false);
const platform = useSystemPlatform();
......@@ -83,12 +93,10 @@ export const TitleBar = () => {
</Button>
{isDyadPro && <DyadProButton isDyadProEnabled={isDyadProEnabled} />}
{/* Preview Header */}
{location.pathname === "/chat" && (
<div className="flex-1 flex justify-end">
<ActionHeader />
</div>
)}
{/* Spacer to push window controls to the right */}
<div className="flex-1" />
<TitleBarActions />
{showWindowControls && <WindowsControls />}
</div>
......@@ -181,6 +189,70 @@ function WindowsControls() {
);
}
function TitleBarActions() {
const { t } = useTranslation("home");
const { restartApp, refreshAppIframe } = useRunApp();
const onCleanRestart = useCallback(() => {
restartApp({ removeNodeModules: true });
}, [restartApp]);
const useClearSessionData = () => {
return useMutation({
mutationFn: () => {
return ipc.system.clearSessionData();
},
onSuccess: async () => {
await refreshAppIframe();
showSuccess("Preview data cleared");
},
onError: (error) => {
showError(`Error clearing preview data: ${error}`);
},
});
};
const { mutate: clearSessionData } = useClearSessionData();
const onClearSessionData = useCallback(() => {
clearSessionData();
}, [clearSessionData]);
return (
<div className="flex items-center gap-0.5 no-app-region-drag mr-2">
<ChatActivityButton />
<DropdownMenu>
<DropdownMenuTrigger
data-testid="preview-more-options-button"
className="flex items-center justify-center w-8 h-8 rounded-md text-sm hover:bg-sidebar-accent transition-colors"
>
<MoreVertical size={16} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}>
<Cog size={16} />
<div className="flex flex-col">
<span>{t("preview.rebuild")}</span>
<span className="text-xs text-muted-foreground">
{t("preview.rebuildDescription")}
</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={onClearSessionData}>
<Trash2 size={16} />
<div className="flex flex-col">
<span>{t("preview.clearCache")}</span>
<span className="text-xs text-muted-foreground">
{t("preview.clearCacheDescription")}
</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
export function DyadProButton({
isDyadProEnabled,
}: {
......
import { useAtom, useAtomValue } from "jotai";
import { previewModeAtom, selectedAppIdAtom } from "../atoms/appAtoms";
import { Eye, Code, AlertTriangle, Wrench, Globe, Shield } from "lucide-react";
import { motion } from "framer-motion";
import { useCheckProblems } from "@/hooks/useCheckProblems";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useTranslation } from "react-i18next";
import type { PreviewMode } from "./preview_panel/ActionHeader";
// Right Action Sidebar component - mirrors the left sidebar when collapsed
export const RightActionSidebar = () => {
const { t } = useTranslation("home");
const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
const selectedAppId = useAtomValue(selectedAppIdAtom);
const { problemReport } = useCheckProblems(selectedAppId);
const selectPanel = (panel: PreviewMode) => {
if (previewMode === panel) {
setIsPreviewOpen(!isPreviewOpen);
} else {
setPreviewMode(panel);
setIsPreviewOpen(true);
}
};
// Get the problem count for the selected app
const problemCount = problemReport ? problemReport.problems.length : 0;
// Format the problem count for display
const formatProblemCount = (count: number): string => {
if (count === 0) return "";
if (count > 100) return "100+";
return count.toString();
};
const displayCount = formatProblemCount(problemCount);
const iconSize = 18;
const renderButton = (
mode: PreviewMode,
icon: React.ReactNode,
text: string,
testId: string,
badge?: React.ReactNode,
) => {
const isActive = previewMode === mode && isPreviewOpen;
return (
<button
data-testid={testId}
className={`no-app-region-drag cursor-pointer relative flex flex-col items-center justify-center w-12 h-12 rounded-lg font-medium transition-colors duration-150 active:scale-90 ${
isActive
? "text-sidebar-accent-foreground"
: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
}`}
onClick={() => selectPanel(mode)}
>
{isActive && (
<motion.div
layoutId="active-sidebar-indicator"
className="absolute inset-0 rounded-lg bg-sidebar-accent"
transition={{ type: "spring", stiffness: 500, damping: 35 }}
/>
)}
<div className="relative z-10">
{icon}
{badge}
</div>
<span className="relative z-10 text-[10px] leading-tight mt-0.5 truncate max-w-full">
{text}
</span>
</button>
);
};
return (
<div className="flex flex-col h-full w-16 pl-1 -mr-1.5 bg-sidebar border-l border-sidebar-border">
{/* Main action buttons */}
<div className="flex flex-col items-center gap-1 pt-2 flex-1">
{renderButton(
"preview",
<Eye size={iconSize} />,
t("preview.title"),
"preview-mode-button",
)}
{renderButton(
"problems",
<AlertTriangle size={iconSize} />,
t("preview.problems"),
"problems-mode-button",
displayCount && (
<span className="absolute -top-1 -right-1 px-1 py-0.5 text-[10px] font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full min-w-[16px] text-center">
{displayCount}
</span>
),
)}
{renderButton(
"code",
<Code size={iconSize} />,
t("preview.code"),
"code-mode-button",
)}
{renderButton(
"configure",
<Wrench size={iconSize} />,
t("preview.configure"),
"configure-mode-button",
)}
{renderButton(
"security",
<Shield size={iconSize} />,
t("preview.security"),
"security-mode-button",
)}
{renderButton(
"publish",
<Globe size={iconSize} />,
t("preview.publish"),
"publish-mode-button",
)}
</div>
</div>
);
};
......@@ -201,7 +201,7 @@ export const ActionHeader = () => {
const iconSize = 15;
return (
<div className="flex items-center justify-between px-1 py-2 mt-1 border-b border-border">
<div className="flex items-center justify-between px-2 py-2 border-b border-border bg-(--sidebar)">
<div className="relative flex rounded-md p-0.5 gap-0.5">
<motion.div
className="absolute top-0.5 bottom-0.5 bg-[var(--background-lightest)] shadow rounded-md"
......
......@@ -7,6 +7,7 @@ import {
} from "react-resizable-panels";
import { ChatPanel } from "../components/ChatPanel";
import { PreviewPanel } from "../components/preview_panel/PreviewPanel";
import { RightActionSidebar } from "../components/RightActionSidebar";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
......@@ -135,6 +136,7 @@ export default function ChatPage() {
>
<PreviewPanel />
</Panel>
<RightActionSidebar />
</PanelGroup>
);
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论