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

View File

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

View File

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

View File

@ -11,11 +11,12 @@ import { SendButton } from './SendButton.client';
import { IconButton } from '~/components/ui/IconButton'; import { IconButton } from '~/components/ui/IconButton';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition'; import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { SupabaseConnection } from './SupabaseConnection'; import { SupabaseConnection } from './SupabaseConnection';
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal'; import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
import styles from './BaseChat.module.scss'; import styles from './BaseChat.module.scss';
import type { ProviderInfo } from '~/types/model'; import type { ProviderInfo } from '~/types/model';
import { ColorSchemeDialog } from '~/components/ui/ColorSchemeDialog';
import type { DesignScheme } from '~/types/design-scheme';
interface ChatBoxProps { interface ChatBoxProps {
isModelSettingsCollapsed: boolean; isModelSettingsCollapsed: boolean;
@ -54,13 +55,15 @@ interface ChatBoxProps {
enhancePrompt?: (() => void) | undefined; enhancePrompt?: (() => void) | undefined;
chatMode?: 'discuss' | 'build'; chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void; setChatMode?: (mode: 'discuss' | 'build') => void;
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
} }
export const ChatBox: React.FC<ChatBoxProps> = (props) => { export const ChatBox: React.FC<ChatBoxProps> = (props) => {
return ( return (
<div <div
className={classNames( 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> </ClientOnly>
<div className="flex justify-between items-center text-sm p-4 pt-2"> <div className="flex justify-between items-center text-sm p-4 pt-2">
<div className="flex gap-1 items-center"> <div className="flex gap-1 items-center">
<ColorSchemeDialog designScheme={props.designScheme} setDesignScheme={props.setDesignScheme} />
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}> <IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
<div className="i-ph:paperclip text-xl"></div> <div className="i-ph:paperclip text-xl"></div>
</IconButton> </IconButton>
@ -279,7 +283,6 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
{props.chatMode === 'discuss' ? <span>Discuss</span> : <span />} {props.chatMode === 'discuss' ? <span>Discuss</span> : <span />}
</IconButton> </IconButton>
)} )}
{props.chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={props.exportChat} />}</ClientOnly>}
<IconButton <IconButton
title="Model Settings" title="Model Settings"
className={classNames('transition-all flex items-center gap-1', { 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 type { Message } from 'ai';
import styles from './Markdown.module.scss'; import styles from './Markdown.module.scss';
import ThoughtBox from './ThoughtBox'; import ThoughtBox from './ThoughtBox';
import type { ProviderInfo } from '~/types/model';
const logger = createScopedLogger('MarkdownComponent'); const logger = createScopedLogger('MarkdownComponent');
@ -18,10 +19,12 @@ interface MarkdownProps {
append?: (message: Message) => void; append?: (message: Message) => void;
chatMode?: 'discuss' | 'build'; chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void; setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
} }
export const Markdown = memo( 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'); logger.trace('Render');
const components = useMemo(() => { const components = useMemo(() => {
@ -106,17 +109,17 @@ export const Markdown = memo(
openArtifactInWorkbench(path); openArtifactInWorkbench(path);
} else if (type === 'message' && append) { } else if (type === 'message' && append) {
append({ append({
id: 'random-message', // Replace with your ID generation logic id: `quick-action-message-${Date.now()}`,
content: message as string, // The message content from the action content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
role: 'user', // Or another role as appropriate role: 'user',
}); });
console.log('Message appended:', message); // Log the appended message console.log('Message appended:', message);
} else if (type === 'implement' && append && setChatMode) { } else if (type === 'implement' && append && setChatMode) {
setChatMode('build'); setChatMode('build');
append({ append({
id: 'implement-message', // Replace with your ID generation logic id: `quick-action-implement-${Date.now()}`,
content: message as string, // The message content from the action content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
role: 'user', // Or another role as appropriate role: 'user',
}); });
} else if (type === 'link' && typeof href === 'string') { } else if (type === 'link' && typeof href === 'string') {
try { try {

View File

@ -11,6 +11,7 @@ import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile'; import { profileStore } from '~/lib/stores/profile';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { ForwardedRef } from 'react'; import type { ForwardedRef } from 'react';
import type { ProviderInfo } from '~/types/model';
interface MessagesProps { interface MessagesProps {
id?: string; id?: string;
@ -20,6 +21,8 @@ interface MessagesProps {
append?: (message: Message) => void; append?: (message: Message) => void;
chatMode?: 'discuss' | 'build'; chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void; setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
} }
export const Messages = forwardRef<HTMLDivElement, MessagesProps>( export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
@ -65,7 +68,7 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
return ( return (
<div <div
key={index} 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-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent': 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
isStreaming && isLast, isStreaming && isLast,
@ -100,6 +103,8 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
append={props.append} append={props.append}
chatMode={props.chatMode} chatMode={props.chatMode}
setChatMode={props.setChatMode} setChatMode={props.setChatMode}
model={props.model}
provider={props.provider}
/> />
)} )}
</div> </div>

View File

@ -1,13 +1,49 @@
import WithTooltip from '~/components/ui/Tooltip'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { IconButton } from '~/components/ui/IconButton'; import { workbenchStore } from '~/lib/stores/workbench';
import React from 'react'; import { classNames } from '~/utils/classNames';
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => { export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
return ( return (
<WithTooltip tooltip="Export Chat"> <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
<IconButton title="Export Chat" onClick={() => exportChat?.()}> <DropdownMenu.Root>
<div className="i-ph:download-simple text-xl"></div> <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">
</IconButton> Export
</WithTooltip> <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 ( return (
<header <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-transparent': !chat.started,
'border-bolt-elements-borderColor': chat.started, 'border-bolt-elements-borderColor': chat.started,
})} })}
@ -30,8 +30,8 @@ export function Header() {
</span> </span>
<ClientOnly> <ClientOnly>
{() => ( {() => (
<div className="mr-1"> <div className="">
<HeaderActionButtons /> <HeaderActionButtons chatStarted={chat.started} />
</div> </div>
)} )}
</ClientOnly> </ClientOnly>

View File

@ -1,206 +1,28 @@
import { useStore } from '@nanostores/react'; 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 { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames'; import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { streamingState } from '~/lib/stores/streaming'; import { streamingState } from '~/lib/stores/streaming';
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client'; import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client'; import { useChatHistory } from '~/lib/persistence';
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client'; import { DeployButton } from '~/components/deploy/DeployButton';
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
interface HeaderActionButtonsProps {} interface HeaderActionButtonsProps {
chatStarted: boolean;
}
export function HeaderActionButtons({}: HeaderActionButtonsProps) { export function HeaderActionButtons({ chatStarted }: HeaderActionButtonsProps) {
const showWorkbench = useStore(workbenchStore.showWorkbench);
const { showChat } = useStore(chatStore);
const netlifyConn = useStore(netlifyConnection);
const vercelConn = useStore(vercelConnection);
const [activePreviewIndex] = useState(0); const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews); const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex]; 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 isStreaming = useStore(streamingState);
const { handleVercelDeploy } = useVercelDeploy(); const { exportChat } = useChatHistory();
const { handleNetlifyDeploy } = useNetlifyDeploy();
useEffect(() => { const shouldShowButtons = !isStreaming && activePreview;
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);
}
};
return ( return (
<div className="flex"> <div className="flex items-center">
<div className="relative" ref={dropdownRef}> {chatStarted && shouldShowButtons && <ExportChatButton exportChat={exportChat} />}
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm"> {shouldShowButtons && <DeployButton />}
<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> </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]); }, [open, selectionMode]);
useEffect(() => { useEffect(() => {
const enterThreshold = 40; const enterThreshold = 20;
const exitThreshold = 40; const exitThreshold = 20;
function onMouseMove(event: MouseEvent) { function onMouseMove(event: MouseEvent) {
if (isSettingsOpen) { if (isSettingsOpen) {
@ -331,13 +331,13 @@ export const Menu = () => {
variants={menuVariants} variants={menuVariants}
style={{ width: '340px' }} style={{ width: '340px' }}
className={classNames( className={classNames(
'flex selection-accent flex-col side-menu fixed top-0 h-full', 'flex selection-accent flex-col side-menu fixed top-0 h-full rounded-r-2xl',
'bg-white dark:bg-gray-950 border-r border-gray-100 dark:border-gray-800/50', 'bg-white dark:bg-gray-950 border-r border-bolt-elements-borderColor',
'shadow-sm text-sm', 'shadow-sm text-sm',
isSettingsOpen ? 'z-40' : 'z-sidebar', 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="text-gray-900 dark:text-white font-medium"></div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="font-medium text-sm text-gray-900 dark:text-white truncate"> <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 { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { usePreviewStore } from '~/lib/stores/previews'; import { usePreviewStore } from '~/lib/stores/previews';
import { chatStore } from '~/lib/stores/chat';
interface WorkspaceProps { interface WorkspaceProps {
chatStarted?: boolean; chatStarted?: boolean;
@ -294,6 +295,8 @@ export const Workbench = memo(
const unsavedFiles = useStore(workbenchStore.unsavedFiles); const unsavedFiles = useStore(workbenchStore.unsavedFiles);
const files = useStore(workbenchStore.files); const files = useStore(workbenchStore.files);
const selectedView = useStore(workbenchStore.currentView); const selectedView = useStore(workbenchStore.currentView);
const { showChat } = useStore(chatStore);
const canHideChat = showWorkbench || !showChat;
const isSmallViewport = useViewport(1024); const isSmallViewport = useViewport(1024);
@ -370,7 +373,7 @@ export const Workbench = memo(
> >
<div <div
className={classNames( 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, 'w-full': isSmallViewport,
'left-0': showWorkbench && 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="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} /> <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
<div className="ml-auto" /> <div className="ml-auto" />
{selectedView === 'code' && ( {selectedView === 'code' && (
@ -398,7 +410,7 @@ export const Workbench = memo(
<DropdownMenu.Root> <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"> <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" /> <div className="i-ph:box-arrow-up" />
Sync & Export Sync
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content <DropdownMenu.Content
className={classNames( className={classNames(
@ -412,19 +424,6 @@ export const Workbench = memo(
sideOffset={5} sideOffset={5}
align="end" 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 <DropdownMenu.Item
className={classNames( 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', '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',

View File

@ -9,6 +9,7 @@ import { LLMManager } from '~/lib/modules/llm/manager';
import { createScopedLogger } from '~/utils/logger'; import { createScopedLogger } from '~/utils/logger';
import { createFilesContext, extractPropertiesFromMessage } from './utils'; import { createFilesContext, extractPropertiesFromMessage } from './utils';
import { discussPrompt } from '~/lib/common/prompts/discuss-prompt'; import { discussPrompt } from '~/lib/common/prompts/discuss-prompt';
import type { DesignScheme } from '~/types/design-scheme';
export type Messages = Message[]; export type Messages = Message[];
@ -38,6 +39,7 @@ export async function streamText(props: {
summary?: string; summary?: string;
messageSliceId?: number; messageSliceId?: number;
chatMode?: 'discuss' | 'build'; chatMode?: 'discuss' | 'build';
designScheme?: DesignScheme;
}) { }) {
const { const {
messages, messages,
@ -51,6 +53,7 @@ export async function streamText(props: {
contextFiles, contextFiles,
summary, summary,
chatMode, chatMode,
designScheme,
} = props; } = props;
let currentModel = DEFAULT_MODEL; let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name; let currentProvider = DEFAULT_PROVIDER.name;
@ -120,6 +123,7 @@ export async function streamText(props: {
cwd: WORK_DIR, cwd: WORK_DIR,
allowedHtmlElements: allowedHTMLElements, allowedHtmlElements: allowedHTMLElements,
modificationTagName: MODIFICATIONS_TAG_NAME, modificationTagName: MODIFICATIONS_TAG_NAME,
designScheme,
supabase: { supabase: {
isConnected: options?.supabaseConnection?.isConnected || false, isConnected: options?.supabaseConnection?.isConnected || false,
hasSelectedProject: options?.supabaseConnection?.hasSelectedProject || false, hasSelectedProject: options?.supabaseConnection?.hasSelectedProject || false,

View File

@ -1,11 +1,13 @@
import { getSystemPrompt } from './prompts/prompts'; import { getSystemPrompt } from './prompts/prompts';
import optimized from './prompts/optimized'; import optimized from './prompts/optimized';
import { getFineTunedPrompt } from './prompts/new-prompt'; import { getFineTunedPrompt } from './prompts/new-prompt';
import type { DesignScheme } from '~/types/design-scheme';
export interface PromptOptions { export interface PromptOptions {
cwd: string; cwd: string;
allowedHtmlElements: string[]; allowedHtmlElements: string[];
modificationTagName: string; modificationTagName: string;
designScheme?: DesignScheme;
supabase?: { supabase?: {
isConnected: boolean; isConnected: boolean;
hasSelectedProject: boolean; hasSelectedProject: boolean;
@ -28,12 +30,12 @@ export class PromptLibrary {
default: { default: {
label: 'Default Prompt', label: 'Default Prompt',
description: 'This is the battle tested default system Prompt', description: 'This is the battle tested default system Prompt',
get: (options) => getSystemPrompt(options.cwd, options.supabase), get: (options) => getSystemPrompt(options.cwd, options.supabase, options.designScheme),
}, },
enhanced: { enhanced: {
label: 'Fine Tuned Prompt', label: 'Fine Tuned Prompt',
description: 'An fine tuned prompt for better results', description: 'An fine tuned prompt for better results',
get: (options) => getFineTunedPrompt(options.cwd, options.supabase), get: (options) => getFineTunedPrompt(options.cwd, options.supabase, options.designScheme),
}, },
optimized: { optimized: {
label: 'Optimized Prompt (experimental)', label: 'Optimized Prompt (experimental)',

View File

@ -1,3 +1,4 @@
import type { DesignScheme } from '~/types/design-scheme';
import { WORK_DIR } from '~/utils/constants'; import { WORK_DIR } from '~/utils/constants';
import { allowedHTMLElements } from '~/utils/markdown'; import { allowedHTMLElements } from '~/utils/markdown';
import { stripIndents } from '~/utils/stripIndent'; import { stripIndents } from '~/utils/stripIndent';
@ -9,6 +10,7 @@ export const getFineTunedPrompt = (
hasSelectedProject: boolean; hasSelectedProject: boolean;
credentials?: { anonKey?: string; supabaseUrl?: string }; credentials?: { anonKey?: string; supabaseUrl?: string };
}, },
designScheme?: DesignScheme,
) => ` ) => `
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices, created by StackBlitz. You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices, created by StackBlitz.
@ -324,6 +326,14 @@ The year is 2025.
<design_instructions> <design_instructions>
When creating designs or UIs for applications, follow these guidelines indefinitely this is non-negotiable: When creating designs or UIs for applications, follow these guidelines indefinitely this is non-negotiable:
<user_provided_design>
USER PROVIDED DESIGN SCHEME:
- ALWAYS use the user provided design scheme when creating designs unless the user specifically requests otherwise.
FONT: ${JSON.stringify(designScheme?.font)}
COLOR PALETTE: ${JSON.stringify(designScheme?.palette)}
FEATURES: ${JSON.stringify(designScheme?.features)}
</user_provided_design>
CRITICAL: CRITICAL:
- Always strive for professional, beautiful, and unique designs - Always strive for professional, beautiful, and unique designs
- All designs should be fully featured and worthy of production use - All designs should be fully featured and worthy of production use

View File

@ -1,3 +1,4 @@
import type { DesignScheme } from '~/types/design-scheme';
import { WORK_DIR } from '~/utils/constants'; import { WORK_DIR } from '~/utils/constants';
import { allowedHTMLElements } from '~/utils/markdown'; import { allowedHTMLElements } from '~/utils/markdown';
import { stripIndents } from '~/utils/stripIndent'; import { stripIndents } from '~/utils/stripIndent';
@ -9,6 +10,7 @@ export const getSystemPrompt = (
hasSelectedProject: boolean; hasSelectedProject: boolean;
credentials?: { anonKey?: string; supabaseUrl?: string }; credentials?: { anonKey?: string; supabaseUrl?: string };
}, },
designScheme?: DesignScheme,
) => ` ) => `
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices. You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
@ -392,6 +394,14 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
<design_instructions> <design_instructions>
Overall Goal: Create visually stunning, unique, highly interactive, content-rich, and production-ready applications. Avoid generic templates. Overall Goal: Create visually stunning, unique, highly interactive, content-rich, and production-ready applications. Avoid generic templates.
<user_provided_design>
USER PROVIDED DESIGN SCHEME:
- ALWAYS use the user provided design scheme when creating designs unless the user specifically requests otherwise.
FONT: ${JSON.stringify(designScheme?.font)}
COLOR PALETTE: ${JSON.stringify(designScheme?.palette)}
FEATURES: ${JSON.stringify(designScheme?.features)}
</user_provided_design>
Visual Identity & Branding: Visual Identity & Branding:
- Establish a distinctive art direction (unique shapes, grids, illustrations). - Establish a distinctive art direction (unique shapes, grids, illustrations).
- Use premium typography with refined hierarchy and spacing. - Use premium typography with refined hierarchy and spacing.

View File

@ -49,16 +49,14 @@ export function ChatDescription() {
{currentDescription} {currentDescription}
<TooltipProvider> <TooltipProvider>
<WithTooltip tooltip="Rename chat"> <WithTooltip tooltip="Rename chat">
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent ml-2"> <button
<button type="button"
type="button" className="ml-2 i-ph:pencil-fill scale-110 hover:text-bolt-elements-item-contentAccent"
className="i-ph:pencil-fill scale-110 hover:text-bolt-elements-item-contentAccent" onClick={(event) => {
onClick={(event) => { event.preventDefault();
event.preventDefault(); toggleEditMode();
toggleEditMode(); }}
}} />
/>
</div>
</WithTooltip> </WithTooltip>
</TooltipProvider> </TooltipProvider>
</> </>

View File

@ -11,6 +11,7 @@ import type { ContextAnnotation, ProgressAnnotation } from '~/types/context';
import { WORK_DIR } from '~/utils/constants'; import { WORK_DIR } from '~/utils/constants';
import { createSummary } from '~/lib/.server/llm/create-summary'; import { createSummary } from '~/lib/.server/llm/create-summary';
import { extractPropertiesFromMessage } from '~/lib/.server/llm/utils'; import { extractPropertiesFromMessage } from '~/lib/.server/llm/utils';
import type { DesignScheme } from '~/types/design-scheme';
export async function action(args: ActionFunctionArgs) { export async function action(args: ActionFunctionArgs) {
return chatAction(args); return chatAction(args);
@ -37,12 +38,13 @@ function parseCookies(cookieHeader: string): Record<string, string> {
} }
async function chatAction({ context, request }: ActionFunctionArgs) { async function chatAction({ context, request }: ActionFunctionArgs) {
const { messages, files, promptId, contextOptimization, supabase, chatMode } = await request.json<{ const { messages, files, promptId, contextOptimization, supabase, chatMode, designScheme } = await request.json<{
messages: Messages; messages: Messages;
files: any; files: any;
promptId?: string; promptId?: string;
contextOptimization: boolean; contextOptimization: boolean;
chatMode: 'discuss' | 'build'; chatMode: 'discuss' | 'build';
designScheme?: DesignScheme;
supabase?: { supabase?: {
isConnected: boolean; isConnected: boolean;
hasSelectedProject: boolean; hasSelectedProject: boolean;
@ -250,6 +252,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextOptimization, contextOptimization,
contextFiles: filteredFiles, contextFiles: filteredFiles,
chatMode, chatMode,
designScheme,
summary, summary,
messageSliceId, messageSliceId,
}); });
@ -290,6 +293,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextOptimization, contextOptimization,
contextFiles: filteredFiles, contextFiles: filteredFiles,
chatMode, chatMode,
designScheme,
summary, summary,
messageSliceId, messageSliceId,
}); });

View File

@ -221,8 +221,8 @@
*/ */
:root { :root {
--header-height: 54px; --header-height: 54px;
--chat-max-width: 35rem; --chat-max-width: 33rem;
--chat-min-width: 575px; --chat-min-width: 533px;
--workbench-width: min(calc(100% - var(--chat-min-width)), 2536px); --workbench-width: min(calc(100% - var(--chat-min-width)), 2536px);
--workbench-inner-width: var(--workbench-width); --workbench-inner-width: var(--workbench-width);
--workbench-left: calc(100% - var(--workbench-width)); --workbench-left: calc(100% - var(--workbench-width));

100
app/types/design-scheme.ts Normal file
View File

@ -0,0 +1,100 @@
export interface DesignScheme {
palette: { [key: string]: string }; // Changed from string[] to object
features: string[];
font: string[];
}
export const defaultDesignScheme: DesignScheme = {
palette: {
primary: '#7c3aed',
secondary: '#38bdf8',
accent: '#f472b6',
background: '#f9fafb',
surface: '#ffffff',
text: '#18181b',
textSecondary: '#64748b',
border: '#e2e8f0',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
},
features: ['rounded', 'shadow'],
font: ['sans-serif'],
};
// Define the semantic color roles for the UI
export const paletteRoles = [
{
key: 'primary',
label: 'Primary',
description: 'Main brand color - use for primary buttons, active links, and key interactive elements',
},
{
key: 'secondary',
label: 'Secondary',
description: 'Supporting brand color - use for secondary buttons, inactive states, and complementary elements',
},
{
key: 'accent',
label: 'Accent',
description: 'Highlight color - use for badges, notifications, focus states, and call-to-action elements',
},
{
key: 'background',
label: 'Background',
description: 'Page backdrop - use for the main application/website background behind all content',
},
{
key: 'surface',
label: 'Surface',
description: 'Elevated content areas - use for cards, modals, dropdowns, and panels that sit above the background',
},
{ key: 'text', label: 'Text', description: 'Primary text - use for headings, body text, and main readable content' },
{
key: 'textSecondary',
label: 'Text Secondary',
description: 'Muted text - use for captions, placeholders, timestamps, and less important information',
},
{
key: 'border',
label: 'Border',
description: 'Separators - use for input borders, dividers, table lines, and element outlines',
},
{
key: 'success',
label: 'Success',
description: 'Positive feedback - use for success messages, completed states, and positive indicators',
},
{
key: 'warning',
label: 'Warning',
description: 'Caution alerts - use for warning messages, pending states, and attention-needed indicators',
},
{
key: 'error',
label: 'Error',
description: 'Error states - use for error messages, failed states, and destructive action indicators',
},
];
export const designFeatures = [
{ key: 'rounded', label: 'Rounded Corners' },
{ key: 'border', label: 'Subtle Border' },
{ key: 'gradient', label: 'Gradient Accent' },
{ key: 'shadow', label: 'Soft Shadow' },
];
// Add font options for easy reference
export const designFonts = [
{ key: 'sans-serif', label: 'Sans Serif', preview: 'Aa' },
{ key: 'serif', label: 'Serif', preview: 'Aa' },
{ key: 'monospace', label: 'Monospace', preview: 'Aa' },
{ key: 'cursive', label: 'Cursive', preview: 'Aa' },
{ key: 'fantasy', label: 'Fantasy', preview: 'Aa' },
/*
* Add custom fonts here if needed
* { key: 'Inter', label: 'Inter', preview: 'Aa' },
* { key: 'Roboto', label: 'Roboto', preview: 'Aa' },
*/
];