feat: move export/sync buttons to workbench and standardize styling

- Move Export and Sync buttons from header to workbench editor panel
- Position buttons in front of Toggle Terminal button
- Standardize button styling with consistent:
  * Accent colored background and white text
  * Same padding (px-3 py-1.5) and font size (text-xs)
  * Consistent border styling and hover states
- Fix font size conflicts by removing parent text-sm classes
- Clean up unused imports and fix prettier formatting
- Ensure dropdown items have consistent text-sm font size

Resolves button styling inconsistencies and improves UX
This commit is contained in:
Stijnus
2025-09-05 01:16:44 +02:00
parent 8d30017f25
commit 5517f7d6f1
3 changed files with 83 additions and 81 deletions

View File

@@ -4,7 +4,7 @@ import { classNames } from '~/utils/classNames';
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
return (
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
<DropdownMenu.Root>
<DropdownMenu.Trigger className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7">
Export

View File

@@ -1,88 +1,21 @@
import { useState } from 'react';
import { useStore } from '@nanostores/react';
import { workbenchStore } from '~/lib/stores/workbench';
import { useState, useCallback } from 'react';
import { streamingState } from '~/lib/stores/streaming';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { useChatHistory } from '~/lib/persistence';
import { DeployButton } from '~/components/deploy/DeployButton';
import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
interface HeaderActionButtonsProps {
chatStarted: boolean;
}
export function HeaderActionButtons({ chatStarted }: HeaderActionButtonsProps) {
export function HeaderActionButtons({ chatStarted: _chatStarted }: HeaderActionButtonsProps) {
const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
const isStreaming = useStore(streamingState);
const { exportChat } = useChatHistory();
const [isSyncing, setIsSyncing] = useState(false);
const shouldShowButtons = !isStreaming && activePreview;
const handleSyncFiles = useCallback(async () => {
setIsSyncing(true);
try {
const directoryHandle = await window.showDirectoryPicker();
await workbenchStore.syncFiles(directoryHandle);
toast.success('Files synced successfully');
} catch (error) {
console.error('Error syncing files:', error);
toast.error('Failed to sync files');
} finally {
setIsSyncing(false);
}
}, []);
const shouldShowButtons = activePreview;
return (
<div className="flex items-center gap-1">
{/* Export Chat Button */}
{chatStarted && shouldShowButtons && <ExportChatButton exportChat={exportChat} />}
{/* Sync Button */}
{shouldShowButtons && (
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden text-sm">
<DropdownMenu.Root>
<DropdownMenu.Trigger
disabled={isSyncing}
className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
{isSyncing ? 'Syncing...' : 'Sync'}
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'min-w-[240px] z-[250]',
'bg-white dark:bg-[#141414]',
'rounded-lg shadow-lg',
'border border-gray-200/50 dark:border-gray-800/50',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={handleSyncFiles}
disabled={isSyncing}
>
<div className="flex items-center gap-2">
{isSyncing ? <div className="i-ph:spinner" /> : <div className="i-ph:cloud-arrow-down" />}
<span>{isSyncing ? 'Syncing...' : 'Sync Files'}</span>
</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
)}
{/* Deploy Button */}
{shouldShowButtons && <DeployButton />}

View File

@@ -13,7 +13,6 @@ import {
type OnScrollCallback as OnEditorScroll,
} from '~/components/editor/codemirror/CodeMirrorEditor';
import { IconButton } from '~/components/ui/IconButton';
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
import { Slider, type SliderOptions } from '~/components/ui/Slider';
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
@@ -26,6 +25,10 @@ import useViewport from '~/lib/hooks';
import { usePreviewStore } from '~/lib/stores/previews';
import { chatStore } from '~/lib/stores/chat';
import type { ElementInfo } from './Inspector';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { useChatHistory } from '~/lib/persistence';
import { streamingState } from '~/lib/stores/streaming';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
interface WorkspaceProps {
chatStarted?: boolean;
@@ -302,6 +305,9 @@ export const Workbench = memo(
const canHideChat = showWorkbench || !showChat;
const isSmallViewport = useViewport(1024);
const streaming = useStore(streamingState);
const { exportChat } = useChatHistory();
const [isSyncing, setIsSyncing] = useState(false);
const setSelectedView = (view: WorkbenchViewType) => {
workbenchStore.currentView.set(view);
@@ -351,6 +357,21 @@ export const Workbench = memo(
workbenchStore.currentView.set('diff');
}, []);
const handleSyncFiles = useCallback(async () => {
setIsSyncing(true);
try {
const directoryHandle = await window.showDirectoryPicker();
await workbenchStore.syncFiles(directoryHandle);
toast.success('Files synced successfully');
} catch (error) {
console.error('Error syncing files:', error);
toast.error('Failed to sync files');
} finally {
setIsSyncing(false);
}
}, []);
return (
chatStarted && (
<motion.div
@@ -386,15 +407,63 @@ export const Workbench = memo(
<div className="ml-auto" />
{selectedView === 'code' && (
<div className="flex overflow-y-auto">
<PanelHeaderButton
className="mr-1 text-sm"
onClick={() => {
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
}}
>
<div className="i-ph:terminal" />
Toggle Terminal
</PanelHeaderButton>
{/* Export Chat Button */}
<ExportChatButton exportChat={exportChat} />
{/* Sync Button */}
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden ml-1">
<DropdownMenu.Root>
<DropdownMenu.Trigger
disabled={isSyncing || streaming}
className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
{isSyncing ? 'Syncing...' : 'Sync'}
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'min-w-[240px] z-[250]',
'bg-white dark:bg-[#141414]',
'rounded-lg shadow-lg',
'border border-gray-200/50 dark:border-gray-800/50',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={handleSyncFiles}
disabled={isSyncing}
>
<div className="flex items-center gap-2">
{isSyncing ? (
<div className="i-ph:spinner" />
) : (
<div className="i-ph:cloud-arrow-down" />
)}
<span>{isSyncing ? 'Syncing...' : 'Sync Files'}</span>
</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/* Toggle Terminal Button */}
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden ml-1">
<button
onClick={() => {
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
}}
className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
<div className="i-ph:terminal" />
Toggle Terminal
</button>
</div>
</div>
)}