feat(design): add design scheme support and UI improvements

- Implement design scheme system with palette, typography, and feature customization
- Add color scheme dialog for user customization
- Update chat UI components to use design scheme values
- Improve header actions with consolidated deploy and export buttons
- Adjust layout spacing and styling across multiple components (chat, workbench etc...)
- Add model and provider info to chat messages
- Refactor workbench and sidebar components for better responsiveness
This commit is contained in:
KevIsDev
2025-05-28 23:49:51 +01:00
parent 12f9f4dcdc
commit cd37599f3b
21 changed files with 701 additions and 255 deletions

View File

@@ -6,6 +6,7 @@ import { workbenchStore } from '~/lib/stores/workbench';
import { WORK_DIR } from '~/utils/constants';
import WithTooltip from '~/components/ui/Tooltip';
import type { Message } from 'ai';
import type { ProviderInfo } from '~/types/model';
interface AssistantMessageProps {
content: string;
@@ -16,6 +17,8 @@ interface AssistantMessageProps {
append?: (message: Message) => void;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
}
function openArtifactInWorkbench(filePath: string) {
@@ -43,7 +46,18 @@ function normalizedFilePath(path: string) {
}
export const AssistantMessage = memo(
({ content, annotations, messageId, onRewind, onFork, append, chatMode, setChatMode }: AssistantMessageProps) => {
({
content,
annotations,
messageId,
onRewind,
onFork,
append,
chatMode,
setChatMode,
model,
provider,
}: AssistantMessageProps) => {
const filteredAnnotations = (annotations?.filter(
(annotation: JSONValue) =>
annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
@@ -141,7 +155,7 @@ export const AssistantMessage = memo(
</div>
</div>
</>
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} html>
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} model={model} provider={provider} html>
{content}
</Markdown>
</div>

View File

@@ -31,6 +31,7 @@ import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { useStore } from '@nanostores/react';
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
import { ChatBox } from './ChatBox';
import type { DesignScheme } from '~/types/design-scheme';
const TEXTAREA_MIN_HEIGHT = 76;
@@ -73,6 +74,8 @@ interface BaseChatProps {
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
append?: (message: Message) => void;
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
}
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -114,6 +117,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
chatMode,
setChatMode,
append,
designScheme,
setDesignScheme,
},
ref,
) => {
@@ -332,7 +337,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<div className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
{!chatStarted && (
<div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
<div id="intro" className="mt-[16vh] max-w-2xl mx-auto text-center px-4 lg:px-0">
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
Where ideas begin
</h1>
@@ -353,12 +358,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
{() => {
return chatStarted ? (
<Messages
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
className="flex flex-col w-full flex-1 max-w-chat pb-4 mx-auto z-1"
messages={messages}
isStreaming={isStreaming}
append={append}
chatMode={chatMode}
setChatMode={setChatMode}
model={model}
provider={provider}
/>
) : null;
}}
@@ -440,6 +447,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
handleFileUpload={handleFileUpload}
chatMode={chatMode}
setChatMode={setChatMode}
designScheme={designScheme}
setDesignScheme={setDesignScheme}
/>
</div>
</StickToBottom>

View File

@@ -27,6 +27,7 @@ import { logStore } from '~/lib/stores/logs';
import { streamingState } from '~/lib/stores/streaming';
import { filesToArtifacts } from '~/utils/fileUtils';
import { supabaseConnection } from '~/lib/stores/supabase';
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@@ -124,6 +125,10 @@ export const ChatImpl = memo(
const [searchParams, setSearchParams] = useSearchParams();
const [fakeLoading, setFakeLoading] = useState(false);
const files = useStore(workbenchStore.files);
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
console.log(designScheme);
const actionAlert = useStore(workbenchStore.alert);
const deployAlert = useStore(workbenchStore.deployAlert);
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection
@@ -170,6 +175,7 @@ export const ChatImpl = memo(
promptId,
contextOptimization: contextOptimizationEnabled,
chatMode,
designScheme,
supabase: {
isConnected: supabaseConn.isConnected,
hasSelectedProject: !!selectedProject,
@@ -569,6 +575,8 @@ export const ChatImpl = memo(
chatMode={chatMode}
setChatMode={setChatMode}
append={append}
designScheme={designScheme}
setDesignScheme={setDesignScheme}
/>
);
},

View File

@@ -11,11 +11,12 @@ import { SendButton } from './SendButton.client';
import { IconButton } from '~/components/ui/IconButton';
import { toast } from 'react-toastify';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { SupabaseConnection } from './SupabaseConnection';
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
import styles from './BaseChat.module.scss';
import type { ProviderInfo } from '~/types/model';
import { ColorSchemeDialog } from '~/components/ui/ColorSchemeDialog';
import type { DesignScheme } from '~/types/design-scheme';
interface ChatBoxProps {
isModelSettingsCollapsed: boolean;
@@ -54,13 +55,15 @@ interface ChatBoxProps {
enhancePrompt?: (() => void) | undefined;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
}
export const ChatBox: React.FC<ChatBoxProps> = (props) => {
return (
<div
className={classNames(
'relative bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
'relative bg-bolt-elements-background-depth-2 backdrop-blur p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
/*
* {
@@ -237,6 +240,7 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
</ClientOnly>
<div className="flex justify-between items-center text-sm p-4 pt-2">
<div className="flex gap-1 items-center">
<ColorSchemeDialog designScheme={props.designScheme} setDesignScheme={props.setDesignScheme} />
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
<div className="i-ph:paperclip text-xl"></div>
</IconButton>
@@ -279,7 +283,6 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
{props.chatMode === 'discuss' ? <span>Discuss</span> : <span />}
</IconButton>
)}
{props.chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={props.exportChat} />}</ClientOnly>}
<IconButton
title="Model Settings"
className={classNames('transition-all flex items-center gap-1', {

View File

@@ -8,6 +8,7 @@ import { CodeBlock } from './CodeBlock';
import type { Message } from 'ai';
import styles from './Markdown.module.scss';
import ThoughtBox from './ThoughtBox';
import type { ProviderInfo } from '~/types/model';
const logger = createScopedLogger('MarkdownComponent');
@@ -18,10 +19,12 @@ interface MarkdownProps {
append?: (message: Message) => void;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
}
export const Markdown = memo(
({ children, html = false, limitedMarkdown = false, append, setChatMode }: MarkdownProps) => {
({ children, html = false, limitedMarkdown = false, append, setChatMode, model, provider }: MarkdownProps) => {
logger.trace('Render');
const components = useMemo(() => {
@@ -106,17 +109,17 @@ export const Markdown = memo(
openArtifactInWorkbench(path);
} else if (type === 'message' && append) {
append({
id: 'random-message', // Replace with your ID generation logic
content: message as string, // The message content from the action
role: 'user', // Or another role as appropriate
id: `quick-action-message-${Date.now()}`,
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
role: 'user',
});
console.log('Message appended:', message); // Log the appended message
console.log('Message appended:', message);
} else if (type === 'implement' && append && setChatMode) {
setChatMode('build');
append({
id: 'implement-message', // Replace with your ID generation logic
content: message as string, // The message content from the action
role: 'user', // Or another role as appropriate
id: `quick-action-implement-${Date.now()}`,
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
role: 'user',
});
} else if (type === 'link' && typeof href === 'string') {
try {

View File

@@ -11,6 +11,7 @@ import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile';
import { forwardRef } from 'react';
import type { ForwardedRef } from 'react';
import type { ProviderInfo } from '~/types/model';
interface MessagesProps {
id?: string;
@@ -20,6 +21,8 @@ interface MessagesProps {
append?: (message: Message) => void;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
}
export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
@@ -65,7 +68,7 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
return (
<div
key={index}
className={classNames('flex gap-4 p-6 py-5 w-full rounded-[calc(0.75rem-1px)]', {
className={classNames('flex gap-4 p-3 py-3 w-full rounded-lg', {
'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
isStreaming && isLast,
@@ -100,6 +103,8 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
append={props.append}
chatMode={props.chatMode}
setChatMode={props.setChatMode}
model={props.model}
provider={props.provider}
/>
)}
</div>

View File

@@ -1,13 +1,49 @@
import WithTooltip from '~/components/ui/Tooltip';
import { IconButton } from '~/components/ui/IconButton';
import React from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
return (
<WithTooltip tooltip="Export Chat">
<IconButton title="Export Chat" onClick={() => exportChat?.()}>
<div className="i-ph:download-simple text-xl"></div>
</IconButton>
</WithTooltip>
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
<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-bolt-elements-background-depth-2 text-bolt-elements-textPrimary [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7">
Export
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'z-[250]',
'bg-bolt-elements-background-depth-2',
'rounded-lg shadow-lg',
'border border-bolt-elements-borderColor',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-auto px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={() => {
workbenchStore.downloadZip();
}}
>
<div className="i-ph:code size-4.5"></div>
<span>Download Code</span>
</DropdownMenu.Item>
<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={() => exportChat?.()}
>
<div className="i-ph:chat size-4.5"></div>
<span>Export Chat</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
);
};

View File

@@ -0,0 +1,146 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useStore } from '@nanostores/react';
import { netlifyConnection } from '~/lib/stores/netlify';
import { vercelConnection } from '~/lib/stores/vercel';
import { workbenchStore } from '~/lib/stores/workbench';
import { streamingState } from '~/lib/stores/streaming';
import { classNames } from '~/utils/classNames';
import { useState } from 'react';
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
interface DeployButtonProps {
onVercelDeploy?: () => Promise<void>;
onNetlifyDeploy?: () => Promise<void>;
}
export const DeployButton = ({ onVercelDeploy, onNetlifyDeploy }: DeployButtonProps) => {
const netlifyConn = useStore(netlifyConnection);
const vercelConn = useStore(vercelConnection);
const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
const [isDeploying, setIsDeploying] = useState(false);
const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | null>(null);
const isStreaming = useStore(streamingState);
const { handleVercelDeploy } = useVercelDeploy();
const { handleNetlifyDeploy } = useNetlifyDeploy();
const handleVercelDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('vercel');
try {
if (onVercelDeploy) {
await onVercelDeploy();
} else {
await handleVercelDeploy();
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
const handleNetlifyDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('netlify');
try {
if (onNetlifyDeploy) {
await onNetlifyDeploy();
} else {
await handleNetlifyDeploy();
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
return (
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden text-sm">
<DropdownMenu.Root>
<DropdownMenu.Trigger
disabled={isDeploying || !activePreview || isStreaming}
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-bolt-elements-item-contentAccent 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"
>
{isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'}
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'z-[250]',
'bg-bolt-elements-background-depth-2',
'rounded-lg shadow-lg',
'border border-bolt-elements-borderColor',
'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',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !netlifyConn.user,
},
)}
disabled={isDeploying || !activePreview || !netlifyConn.user}
onClick={handleNetlifyDeployClick}
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/netlify"
/>
<span className="mx-auto">{!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}</span>
{netlifyConn.user && <NetlifyDeploymentLink />}
</DropdownMenu.Item>
<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',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !vercelConn.user,
},
)}
disabled={isDeploying || !activePreview || !vercelConn.user}
onClick={handleVercelDeployClick}
>
<img
className="w-5 h-5 bg-black p-1 rounded"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/vercel/white"
alt="vercel"
/>
<span className="mx-auto">{!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}</span>
{vercelConn.user && <VercelDeploymentLink />}
</DropdownMenu.Item>
<DropdownMenu.Item
disabled
className="flex items-center w-full rounded-md px-4 py-2 text-sm text-bolt-elements-textTertiary gap-2 opacity-60 cursor-not-allowed"
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/cloudflare"
alt="cloudflare"
/>
<span className="mx-auto">Deploy to Cloudflare (Coming Soon)</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
);
};

View File

@@ -10,7 +10,7 @@ export function Header() {
return (
<header
className={classNames('flex items-center p-5 border-b h-[var(--header-height)]', {
className={classNames('flex items-center px-4 border-b h-[var(--header-height)]', {
'border-transparent': !chat.started,
'border-bolt-elements-borderColor': chat.started,
})}
@@ -30,8 +30,8 @@ export function Header() {
</span>
<ClientOnly>
{() => (
<div className="mr-1">
<HeaderActionButtons />
<div className="">
<HeaderActionButtons chatStarted={chat.started} />
</div>
)}
</ClientOnly>

View File

@@ -1,206 +1,28 @@
import { useStore } from '@nanostores/react';
import useViewport from '~/lib/hooks';
import { chatStore } from '~/lib/stores/chat';
import { netlifyConnection } from '~/lib/stores/netlify';
import { vercelConnection } from '~/lib/stores/vercel';
import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
import { useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import { streamingState } from '~/lib/stores/streaming';
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { useChatHistory } from '~/lib/persistence';
import { DeployButton } from '~/components/deploy/DeployButton';
interface HeaderActionButtonsProps {}
interface HeaderActionButtonsProps {
chatStarted: boolean;
}
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
const showWorkbench = useStore(workbenchStore.showWorkbench);
const { showChat } = useStore(chatStore);
const netlifyConn = useStore(netlifyConnection);
const vercelConn = useStore(vercelConnection);
export function HeaderActionButtons({ chatStarted }: HeaderActionButtonsProps) {
const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
const [isDeploying, setIsDeploying] = useState(false);
const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | null>(null);
const isSmallViewport = useViewport(1024);
const canHideChat = showWorkbench || !showChat;
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const isStreaming = useStore(streamingState);
const { handleVercelDeploy } = useVercelDeploy();
const { handleNetlifyDeploy } = useNetlifyDeploy();
const { exportChat } = useChatHistory();
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const onVercelDeploy = async () => {
setIsDeploying(true);
setDeployingTo('vercel');
try {
await handleVercelDeploy();
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
const onNetlifyDeploy = async () => {
setIsDeploying(true);
setDeployingTo('netlify');
try {
await handleNetlifyDeploy();
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
const shouldShowButtons = !isStreaming && activePreview;
return (
<div className="flex">
<div className="relative" ref={dropdownRef}>
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
<Button
active
disabled={isDeploying || !activePreview || isStreaming}
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="px-4 hover:bg-bolt-elements-item-backgroundActive flex items-center gap-2"
>
{isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'}
<div
className={classNames('i-ph:caret-down w-4 h-4 transition-transform', isDropdownOpen ? 'rotate-180' : '')}
/>
</Button>
</div>
{isDropdownOpen && (
<div className="absolute right-2 flex flex-col gap-1 z-50 p-1 mt-1 min-w-[13.5rem] bg-bolt-elements-background-depth-2 rounded-md shadow-lg bg-bolt-elements-backgroundDefault border border-bolt-elements-borderColor">
<Button
active
onClick={() => {
onNetlifyDeploy();
setIsDropdownOpen(false);
}}
disabled={isDeploying || !activePreview || !netlifyConn.user}
className="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"
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/netlify"
/>
<span className="mx-auto">
{!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}
</span>
{netlifyConn.user && <NetlifyDeploymentLink />}
</Button>
<Button
active
onClick={() => {
onVercelDeploy();
setIsDropdownOpen(false);
}}
disabled={isDeploying || !activePreview || !vercelConn.user}
className="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"
>
<img
className="w-5 h-5 bg-black p-1 rounded"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/vercel/white"
alt="vercel"
/>
<span className="mx-auto">{!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}</span>
{vercelConn.user && <VercelDeploymentLink />}
</Button>
<Button
active={false}
disabled
className="flex items-center w-full rounded-md px-4 py-2 text-sm text-bolt-elements-textTertiary gap-2"
>
<span className="sr-only">Coming Soon</span>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/cloudflare"
alt="cloudflare"
/>
<span className="mx-auto">Deploy to Cloudflare (Coming Soon)</span>
</Button>
</div>
)}
</div>
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
<Button
active={showChat}
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
onClick={() => {
if (canHideChat) {
chatStore.setKey('showChat', !showChat);
}
}}
>
<div className="i-bolt:chat text-sm" />
</Button>
<div className="w-[1px] bg-bolt-elements-borderColor" />
<Button
active={showWorkbench}
onClick={() => {
if (showWorkbench && !showChat) {
chatStore.setKey('showChat', true);
}
workbenchStore.showWorkbench.set(!showWorkbench);
}}
>
<div className="i-ph:code-bold" />
</Button>
</div>
<div className="flex items-center">
{chatStarted && shouldShowButtons && <ExportChatButton exportChat={exportChat} />}
{shouldShowButtons && <DeployButton />}
</div>
);
}
interface ButtonProps {
active?: boolean;
disabled?: boolean;
children?: any;
onClick?: VoidFunction;
className?: string;
}
function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
return (
<button
className={classNames(
'flex items-center p-1.5',
{
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
!active,
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
disabled,
},
className,
)}
onClick={onClick}
>
{children}
</button>
);
}

View File

@@ -279,8 +279,8 @@ export const Menu = () => {
}, [open, selectionMode]);
useEffect(() => {
const enterThreshold = 40;
const exitThreshold = 40;
const enterThreshold = 20;
const exitThreshold = 20;
function onMouseMove(event: MouseEvent) {
if (isSettingsOpen) {
@@ -331,13 +331,13 @@ export const Menu = () => {
variants={menuVariants}
style={{ width: '340px' }}
className={classNames(
'flex selection-accent flex-col side-menu fixed top-0 h-full',
'bg-white dark:bg-gray-950 border-r border-gray-100 dark:border-gray-800/50',
'flex selection-accent flex-col side-menu fixed top-0 h-full rounded-r-2xl',
'bg-white dark:bg-gray-950 border-r border-bolt-elements-borderColor',
'shadow-sm text-sm',
isSettingsOpen ? 'z-40' : 'z-sidebar',
)}
>
<div className="h-12 flex items-center justify-between px-4 border-b border-gray-100 dark:border-gray-800/50 bg-gray-50/50 dark:bg-gray-900/50">
<div className="h-12 flex items-center justify-between px-4 border-b border-gray-100 dark:border-gray-800/50 bg-gray-50/50 dark:bg-gray-900/50 rounded-tr-2xl">
<div className="text-gray-900 dark:text-white font-medium"></div>
<div className="flex items-center gap-3">
<span className="font-medium text-sm text-gray-900 dark:text-white truncate">

View File

@@ -0,0 +1,273 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from './Dialog';
import { Button } from './Button';
import { IconButton } from './IconButton';
import type { DesignScheme } from '~/types/design-scheme';
import { defaultDesignScheme, designFeatures, designFonts, paletteRoles } from '~/types/design-scheme';
export interface ColorSchemeDialogProps {
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
}
export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignScheme, designScheme }) => {
const [palette, setPalette] = useState<{ [key: string]: string }>(() => {
if (designScheme?.palette) {
return { ...defaultDesignScheme.palette, ...designScheme.palette };
}
return defaultDesignScheme.palette;
});
const [features, setFeatures] = useState<string[]>(designScheme?.features || defaultDesignScheme.features);
const [font, setFont] = useState<string[]>(designScheme?.font || defaultDesignScheme.font);
useEffect(() => {
if (designScheme) {
setPalette(() => ({ ...defaultDesignScheme.palette, ...designScheme.palette }));
setFeatures(designScheme.features || defaultDesignScheme.features);
setFont(designScheme.font || defaultDesignScheme.font);
} else {
// Reset to defaults if no designScheme provided
setPalette(defaultDesignScheme.palette);
setFeatures(defaultDesignScheme.features);
setFont(defaultDesignScheme.font);
}
}, [designScheme]);
const handleColorChange = (role: string, value: string) => {
setPalette((prev) => ({
...prev,
[role]: value,
}));
};
const handleFeatureToggle = (key: string) => {
setFeatures((prev) => (prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key]));
};
const handleFontToggle = (key: string) => {
setFont((prev) => (prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key]));
};
const [isDialogOpen, setIsDialogOpen] = useState(false);
const handleSave = () => {
setDesignScheme?.({ palette, features, font });
setIsDialogOpen(false);
};
const handleReset = () => {
setPalette(defaultDesignScheme.palette);
setFeatures(defaultDesignScheme.features);
setFont(defaultDesignScheme.font);
};
return (
<div>
<IconButton title="Upload file" className="transition-all" onClick={() => setIsDialogOpen(!isDialogOpen)}>
<div className="i-ph:palette text-xl"></div>
</IconButton>
<DialogRoot open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<Dialog>
<div className="p-8 min-w-[380px] max-w-[95vw]">
<DialogTitle className="mb-2 text-lg font-bold">Design Palette & Features</DialogTitle>
<DialogDescription className="mb-6 text-sm text-bolt-elements-textPrimary">
Choose your color palette, typography, and key design features. These will be used as design instructions
for the LLM.
</DialogDescription>
<div className="mb-5">
<div className="w-full flex justify-between items-center mb-3">
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Color Palette</span>
<button
onClick={handleReset}
className="text-xs bg-transparent text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary flex items-center gap-1 transition-colors"
>
<span className="i-ph:arrow-clockwise" />
Reset to defaults
</button>
</div>
<div className="grid grid-cols-2 gap-4 max-h-48 overflow-y-auto">
{paletteRoles.map((role) => (
<div
key={role.key}
className="flex items-center gap-3 p-2 rounded-lg bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-2"
>
<div className="relative flex-shrink-0">
<div
className="w-10 h-10 rounded-lg shadow-sm cursor-pointer hover:scale-105 transition-transform"
style={{ backgroundColor: palette[role.key] }}
onClick={() => document.getElementById(`color-input-${role.key}`)?.click()}
/>
<input
id={`color-input-${role.key}`}
type="color"
value={palette[role.key]}
onChange={(e) => handleColorChange(role.key, e.target.value)}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
tabIndex={-1}
/>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm text-bolt-elements-textPrimary">{role.label}</div>
<div className="text-xs text-bolt-elements-textSecondary truncate">{role.description}</div>
<div className="text-xs text-bolt-elements-textSecondary opacity-50 font-mono">
{palette[role.key]}
</div>
</div>
</div>
))}
</div>
</div>
<div className="mb-5">
<div className="w-full flex justify-between items-center mb-3">
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Typography</span>
<span className="text-xs text-bolt-elements-textSecondary flex items-center gap-1">
<span className="i-ph:arrow-right" />
Scroll for more
</span>
</div>
<div className="flex gap-3 overflow-x-auto pb-2 px-0.5">
{designFonts.map((f) => (
<button
key={f.key}
type="button"
onClick={() => handleFontToggle(f.key)}
className={`flex-shrink-0 px-4 py-2 rounded-lg border text-xs font-medium transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-300 flex items-center gap-2 min-w-[120px] ${font.includes(f.key) ? 'bg-blue-100 border-blue-400 text-blue-700' : 'bg-gray-50 border-gray-200 text-gray-500 hover:bg-blue-50 hover:border-blue-300'}`}
style={{ fontFamily: f.key }}
>
<span className="text-lg" style={{ fontFamily: f.key }}>
{f.preview}
</span>
<span>{f.label}</span>
</button>
))}
</div>
</div>
<div className="mb-6">
<div className="w-full flex justify-between items-center mb-3">
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Design Features</span>
<span className="text-xs text-bolt-elements-textSecondary flex items-center gap-1">
<span className="i-ph:arrow-right" />
Scroll for more
</span>
</div>
<div className="flex gap-4 overflow-x-auto pb-2 px-0.5">
{designFeatures.map((f) => {
const isSelected = features.includes(f.key);
return (
<button
key={f.key}
type="button"
onClick={() => handleFeatureToggle(f.key)}
className={`
group relative px-4 py-2 text-xs font-medium transition-all duration-300
focus:outline-none focus:ring-2 focus:ring-purple-300 cursor-pointer
transform hover:scale-105 active:scale-95 flex-shrink-0 min-w-[140px]
${
f.key === 'rounded'
? isSelected
? 'rounded-2xl'
: 'rounded-lg hover:rounded-xl'
: f.key === 'border'
? 'rounded-md'
: 'rounded-lg'
}
${
f.key === 'border'
? isSelected
? 'border-2 border-purple-400 bg-purple-50'
: 'border-2 border-gray-200 hover:border-purple-300 bg-white'
: f.key === 'gradient'
? ''
: isSelected
? 'bg-purple-100 text-purple-700'
: 'bg-gray-50 hover:bg-purple-50 text-gray-600 hover:text-purple-600'
}
${
f.key === 'shadow'
? isSelected
? 'shadow-md shadow-purple-200'
: 'shadow-md hover:shadow-lg'
: 'shadow-sm hover:shadow-md'
}
`}
style={{
...(f.key === 'gradient' && {
background: isSelected
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%)',
color: isSelected ? 'white' : '#6b7280',
}),
}}
>
{/* Feature preview area */}
<div className="flex items-center gap-3">
{/* Visual preview */}
<div className="flex items-center justify-center w-6 h-6">
{f.key === 'rounded' && (
<div
className={`w-4 h-4 bg-current transition-all duration-300 ${
isSelected ? 'rounded-full' : 'rounded-sm group-hover:rounded-lg'
} opacity-70`}
></div>
)}
{f.key === 'border' && (
<div
className={`w-4 h-4 rounded transition-all duration-300 ${
isSelected
? 'border-2 border-current opacity-80'
: 'border border-current opacity-60 group-hover:border-2'
}`}
></div>
)}
{f.key === 'gradient' && (
<div className="w-4 h-4 rounded-sm bg-gradient-to-br from-purple-400 via-pink-400 to-indigo-400 opacity-90 transition-all duration-300 group-hover:scale-110"></div>
)}
{f.key === 'shadow' && (
<div className="relative">
<div
className={`w-4 h-4 bg-current rounded transition-all duration-300 ${
isSelected ? 'opacity-80' : 'opacity-60'
}`}
></div>
<div
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-current rounded transition-all duration-300 ${
isSelected ? 'opacity-30' : 'opacity-20'
}`}
></div>
</div>
)}
</div>
{/* Label */}
<span className="transition-all duration-300">{f.label}</span>
</div>
{/* Hover effect overlay (might replace this) */}
<div className="absolute inset-0 bg-purple-400 opacity-0 group-hover:opacity-5 transition-opacity duration-300 rounded-inherit"></div>
</button>
);
})}
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button variant="ghost" onClick={handleSave}>
Save
</Button>
</div>
</div>
</Dialog>
</DialogRoot>
</div>
);
};

View File

@@ -26,6 +26,7 @@ import useViewport from '~/lib/hooks';
import { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { usePreviewStore } from '~/lib/stores/previews';
import { chatStore } from '~/lib/stores/chat';
interface WorkspaceProps {
chatStarted?: boolean;
@@ -294,6 +295,8 @@ export const Workbench = memo(
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
const files = useStore(workbenchStore.files);
const selectedView = useStore(workbenchStore.currentView);
const { showChat } = useStore(chatStore);
const canHideChat = showWorkbench || !showChat;
const isSmallViewport = useViewport(1024);
@@ -370,7 +373,7 @@ export const Workbench = memo(
>
<div
className={classNames(
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
'fixed top-[calc(var(--header-height)+1.2rem)] bottom-6 w-[var(--workbench-inner-width)] z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
{
'w-full': isSmallViewport,
'left-0': showWorkbench && isSmallViewport,
@@ -379,9 +382,18 @@ export const Workbench = memo(
},
)}
>
<div className="absolute inset-0 px-2 lg:px-6">
<div className="absolute inset-0 px-2 lg:px-4">
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor gap-1">
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor gap-1.5">
<button
className={`${showChat ? 'i-ph:sidebar-simple-fill' : 'i-ph:sidebar-simple'} text-lg text-bolt-elements-textSecondary`}
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
onClick={() => {
if (canHideChat) {
chatStore.setKey('showChat', !showChat);
}
}}
/>
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
<div className="ml-auto" />
{selectedView === 'code' && (
@@ -398,7 +410,7 @@ export const Workbench = memo(
<DropdownMenu.Root>
<DropdownMenu.Trigger className="text-sm flex items-center gap-1 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed">
<div className="i-ph:box-arrow-up" />
Sync & Export
Sync
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
@@ -412,19 +424,6 @@ export const Workbench = memo(
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={() => {
workbenchStore.downloadZip();
}}
>
<div className="flex items-center gap-2">
<div className="i-ph:download-simple"></div>
<span>Download Code</span>
</div>
</DropdownMenu.Item>
<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',