feat: add edit mode toggle to show/hide drag handles

- Add edit mode button next to settings in header
- Edit button turns blue when active
- Drag handles fade in/out with edit mode toggle
- Add smooth 200ms transition animation
- Add i18n support for edit mode (en/zh)
- Maintain consistent spacing between header elements
This commit is contained in:
Jason
2025-10-19 22:12:12 +08:00
parent 43ed1c7533
commit 491bbff11d
5 changed files with 40 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { Plus, Settings } from "lucide-react"; import { Plus, Settings, Edit3 } from "lucide-react";
import type { Provider } from "@/types"; import type { Provider } from "@/types";
import { useProvidersQuery } from "@/lib/query"; import { useProvidersQuery } from "@/lib/query";
import { import {
@@ -27,6 +27,7 @@ function App() {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeApp, setActiveApp] = useState<AppType>("claude"); const [activeApp, setActiveApp] = useState<AppType>("claude");
const [isEditMode, setIsEditMode] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isAddOpen, setIsAddOpen] = useState(false); const [isAddOpen, setIsAddOpen] = useState(false);
const [isMcpOpen, setIsMcpOpen] = useState(false); const [isMcpOpen, setIsMcpOpen] = useState(false);
@@ -58,7 +59,7 @@ function App() {
if (event.appType === activeApp) { if (event.appType === activeApp) {
await refetch(); await refetch();
} }
}, }
); );
} catch (error) { } catch (error) {
console.error("[App] Failed to subscribe provider switch event", error); console.error("[App] Failed to subscribe provider switch event", error);
@@ -112,7 +113,7 @@ function App() {
<div className="flex h-screen flex-col bg-gray-50 dark:bg-gray-950"> <div className="flex h-screen flex-col bg-gray-50 dark:bg-gray-950">
<header className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-800 dark:bg-gray-900"> <header className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-800 dark:bg-gray-900">
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-1.5">
<a <a
href="https://github.com/farion1231/cc-switch" href="https://github.com/farion1231/cc-switch"
target="_blank" target="_blank"
@@ -125,9 +126,26 @@ function App() {
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setIsSettingsOpen(true)} onClick={() => setIsSettingsOpen(true)}
title={t("common.settings")}
className="ml-2"
> >
<Settings className="h-4 w-4" /> <Settings className="h-4 w-4" />
</Button> </Button>
<Button
variant="ghost"
size="icon"
onClick={() => setIsEditMode(!isEditMode)}
title={t(
isEditMode ? "header.exitEditMode" : "header.enterEditMode"
)}
className={
isEditMode
? "text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
: ""
}
>
<Edit3 className="h-4 w-4" />
</Button>
<UpdateBadge onClick={() => setIsSettingsOpen(true)} /> <UpdateBadge onClick={() => setIsSettingsOpen(true)} />
</div> </div>
@@ -155,6 +173,7 @@ function App() {
currentProviderId={currentProviderId} currentProviderId={currentProviderId}
appType={activeApp} appType={activeApp}
isLoading={isLoading} isLoading={isLoading}
isEditMode={isEditMode}
onSwitch={switchProvider} onSwitch={switchProvider}
onEdit={setEditingProvider} onEdit={setEditingProvider}
onDelete={setConfirmDelete} onDelete={setConfirmDelete}

View File

@@ -21,6 +21,7 @@ interface ProviderCardProps {
provider: Provider; provider: Provider;
isCurrent: boolean; isCurrent: boolean;
appType: AppType; appType: AppType;
isEditMode?: boolean;
onSwitch: (provider: Provider) => void; onSwitch: (provider: Provider) => void;
onEdit: (provider: Provider) => void; onEdit: (provider: Provider) => void;
onDelete: (provider: Provider) => void; onDelete: (provider: Provider) => void;
@@ -59,6 +60,7 @@ export function ProviderCard({
provider, provider,
isCurrent, isCurrent,
appType, appType,
isEditMode = false,
onSwitch, onSwitch,
onEdit, onEdit,
onDelete, onDelete,
@@ -101,7 +103,10 @@ export function ProviderCard({
<button <button
type="button" type="button"
className={cn( className={cn(
"mt-1 flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-muted-foreground transition-colors hover:border-muted hover:text-foreground", "mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border text-muted-foreground transition-all duration-200",
isEditMode
? "border-muted hover:border-primary hover:text-foreground opacity-100"
: "border-transparent opacity-0 pointer-events-none",
dragHandleProps?.isDragging && "border-primary text-primary", dragHandleProps?.isDragging && "border-primary text-primary",
)} )}
aria-label={t("provider.dragHandle")} aria-label={t("provider.dragHandle")}

View File

@@ -16,6 +16,7 @@ interface ProviderListProps {
providers: Record<string, Provider>; providers: Record<string, Provider>;
currentProviderId: string; currentProviderId: string;
appType: AppType; appType: AppType;
isEditMode?: boolean;
onSwitch: (provider: Provider) => void; onSwitch: (provider: Provider) => void;
onEdit: (provider: Provider) => void; onEdit: (provider: Provider) => void;
onDelete: (provider: Provider) => void; onDelete: (provider: Provider) => void;
@@ -29,6 +30,7 @@ export function ProviderList({
providers, providers,
currentProviderId, currentProviderId,
appType, appType,
isEditMode = false,
onSwitch, onSwitch,
onEdit, onEdit,
onDelete, onDelete,
@@ -76,6 +78,7 @@ export function ProviderList({
provider={provider} provider={provider}
isCurrent={provider.id === currentProviderId} isCurrent={provider.id === currentProviderId}
appType={appType} appType={appType}
isEditMode={isEditMode}
onSwitch={onSwitch} onSwitch={onSwitch}
onEdit={onEdit} onEdit={onEdit}
onDelete={onDelete} onDelete={onDelete}
@@ -93,6 +96,7 @@ interface SortableProviderCardProps {
provider: Provider; provider: Provider;
isCurrent: boolean; isCurrent: boolean;
appType: AppType; appType: AppType;
isEditMode: boolean;
onSwitch: (provider: Provider) => void; onSwitch: (provider: Provider) => void;
onEdit: (provider: Provider) => void; onEdit: (provider: Provider) => void;
onDelete: (provider: Provider) => void; onDelete: (provider: Provider) => void;
@@ -104,6 +108,7 @@ function SortableProviderCard({
provider, provider,
isCurrent, isCurrent,
appType, appType,
isEditMode,
onSwitch, onSwitch,
onEdit, onEdit,
onDelete, onDelete,
@@ -130,6 +135,7 @@ function SortableProviderCard({
provider={provider} provider={provider}
isCurrent={isCurrent} isCurrent={isCurrent}
appType={appType} appType={appType}
isEditMode={isEditMode}
onSwitch={onSwitch} onSwitch={onSwitch}
onEdit={onEdit} onEdit={onEdit}
onDelete={onDelete} onDelete={onDelete}

View File

@@ -47,7 +47,9 @@
"toggleLightMode": "Switch to Light Mode", "toggleLightMode": "Switch to Light Mode",
"addProvider": "Add Provider", "addProvider": "Add Provider",
"switchToChinese": "Switch to Chinese", "switchToChinese": "Switch to Chinese",
"switchToEnglish": "Switch to English" "switchToEnglish": "Switch to English",
"enterEditMode": "Enter Edit Mode",
"exitEditMode": "Exit Edit Mode"
}, },
"provider": { "provider": {
"noProviders": "No providers added yet", "noProviders": "No providers added yet",

View File

@@ -47,7 +47,9 @@
"toggleLightMode": "切换到亮色模式", "toggleLightMode": "切换到亮色模式",
"addProvider": "添加供应商", "addProvider": "添加供应商",
"switchToChinese": "切换到中文", "switchToChinese": "切换到中文",
"switchToEnglish": "切换到英文" "switchToEnglish": "切换到英文",
"enterEditMode": "进入编辑模式",
"exitEditMode": "退出编辑模式"
}, },
"provider": { "provider": {
"noProviders": "还没有添加任何供应商", "noProviders": "还没有添加任何供应商",