Merge pull request #1748 from xKevIsDev/enhancements

feat: add inspector, design palette and redesign
This commit is contained in:
KevIsDev
2025-06-03 11:39:32 +01:00
committed by GitHub
29 changed files with 1506 additions and 295 deletions

View File

@@ -31,6 +31,8 @@ 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';
import type { ElementInfo } from '~/components/workbench/Inspector';
const TEXTAREA_MIN_HEIGHT = 76;
@@ -73,6 +75,10 @@ interface BaseChatProps {
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
append?: (message: Message) => void;
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
selectedElement?: ElementInfo | null;
setSelectedElement?: (element: ElementInfo | null) => void;
}
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -114,6 +120,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
chatMode,
setChatMode,
append,
designScheme,
setDesignScheme,
selectedElement,
setSelectedElement,
},
ref,
) => {
@@ -253,6 +263,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
const handleSendMessage = (event: React.UIEvent, messageInput?: string) => {
if (sendMessage) {
sendMessage(event, messageInput);
setSelectedElement?.(null);
if (recognition) {
recognition.abort(); // Stop current recognition
@@ -332,7 +343,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>
@@ -348,12 +359,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
resize="smooth"
initial="smooth"
>
<StickToBottom.Content className="flex flex-col gap-4">
<StickToBottom.Content className="flex flex-col gap-4 relative ">
<ClientOnly>
{() => {
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}
@@ -365,6 +376,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
) : null;
}}
</ClientOnly>
<ScrollToBottom />
</StickToBottom.Content>
<div
className={classNames('my-auto flex flex-col gap-2 w-full max-w-chat mx-auto z-prompt mb-6', {
@@ -403,7 +415,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
/>
)}
</div>
<ScrollToBottom />
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
<ChatBox
isModelSettingsCollapsed={isModelSettingsCollapsed}
@@ -442,6 +453,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
handleFileUpload={handleFileUpload}
chatMode={chatMode}
setChatMode={setChatMode}
designScheme={designScheme}
setDesignScheme={setDesignScheme}
selectedElement={selectedElement}
setSelectedElement={setSelectedElement}
/>
</div>
</StickToBottom>
@@ -472,6 +487,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
actionRunner={actionRunner ?? ({} as ActionRunner)}
chatStarted={chatStarted}
isStreaming={isStreaming}
setSelectedElement={setSelectedElement}
/>
)}
</ClientOnly>
@@ -488,13 +504,16 @@ function ScrollToBottom() {
return (
!isAtBottom && (
<button
className="absolute z-50 top-[0%] translate-y-[-100%] text-4xl rounded-lg left-[50%] translate-x-[-50%] px-1.5 py-0.5 flex items-center gap-2 bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor text-bolt-elements-textPrimary text-sm"
onClick={() => scrollToBottom()}
>
Go to last message
<span className="i-ph:arrow-down animate-bounce" />
</button>
<>
<div className="sticky bottom-0 left-0 right-0 bg-gradient-to-t from-bolt-elements-background-depth-1 to-transparent h-20 z-10" />
<button
className="sticky z-50 bottom-0 left-0 right-0 text-4xl rounded-lg px-1.5 py-0.5 flex items-center justify-center mx-auto gap-2 bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor text-bolt-elements-textPrimary text-sm"
onClick={() => scrollToBottom()}
>
Go to last message
<span className="i-ph:arrow-down animate-bounce" />
</button>
</>
)
);
}

View File

@@ -27,6 +27,8 @@ 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';
import type { ElementInfo } from '~/components/workbench/Inspector';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@@ -124,6 +126,7 @@ export const ChatImpl = memo(
const [searchParams, setSearchParams] = useSearchParams();
const [fakeLoading, setFakeLoading] = useState(false);
const files = useStore(workbenchStore.files);
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
const actionAlert = useStore(workbenchStore.alert);
const deployAlert = useStore(workbenchStore.deployAlert);
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection
@@ -132,7 +135,6 @@ export const ChatImpl = memo(
);
const supabaseAlert = useStore(workbenchStore.supabaseAlert);
const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
const [model, setModel] = useState(() => {
const savedModel = Cookies.get('selectedModel');
return savedModel || DEFAULT_MODEL;
@@ -141,14 +143,11 @@ export const ChatImpl = memo(
const savedProvider = Cookies.get('selectedProvider');
return (PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER) as ProviderInfo;
});
const { showChat } = useStore(chatStore);
const [animationScope, animate] = useAnimate();
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
const [chatMode, setChatMode] = useState<'discuss' | 'build'>('build');
const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(null);
const {
messages,
isLoading,
@@ -170,6 +169,7 @@ export const ChatImpl = memo(
promptId,
contextOptimization: contextOptimizationEnabled,
chatMode,
designScheme,
supabase: {
isConnected: supabaseConn.isConnected,
hasSelectedProject: !!selectedProject,
@@ -312,8 +312,14 @@ export const ChatImpl = memo(
return;
}
// If no locked items, proceed normally with the original message
const finalMessageContent = messageContent;
let finalMessageContent = messageContent;
if (selectedElement) {
console.log('Selected Element:', selectedElement);
const elementInfo = `<div class=\"__boltSelectedElement__\" data-element='${JSON.stringify(selectedElement)}'>${JSON.stringify(`${selectedElement.displayText}`)}</div>`;
finalMessageContent = messageContent + elementInfo;
}
runAnimation();
@@ -569,6 +575,10 @@ export const ChatImpl = memo(
chatMode={chatMode}
setChatMode={setChatMode}
append={append}
designScheme={designScheme}
setDesignScheme={setDesignScheme}
selectedElement={selectedElement}
setSelectedElement={setSelectedElement}
/>
);
},

View File

@@ -11,11 +11,13 @@ 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';
import type { ElementInfo } from '~/components/workbench/Inspector';
interface ChatBoxProps {
isModelSettingsCollapsed: boolean;
@@ -54,13 +56,17 @@ interface ChatBoxProps {
enhancePrompt?: (() => void) | undefined;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
selectedElement?: ElementInfo | null;
setSelectedElement?: ((element: ElementInfo | null) => void) | undefined;
}
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',
/*
* {
@@ -143,6 +149,22 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
/>
)}
</ClientOnly>
{props.selectedElement && (
<div className="flex mx-1.5 gap-2 items-center justify-between rounded-lg rounded-b-none border border-b-none border-bolt-elements-borderColor text-bolt-elements-textPrimary flex py-1 px-2.5 font-medium text-xs">
<div className="flex gap-2 items-center lowercase">
<code className="bg-accent-500 rounded-4px px-1.5 py-1 mr-0.5 text-white">
{props?.selectedElement?.tagName}
</code>
selected for inspection
</div>
<button
className="bg-transparent text-accent-500 pointer-auto"
onClick={() => props.setSelectedElement?.(null)}
>
Clear
</button>
</div>
)}
<div
className={classNames('relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg')}
>
@@ -237,6 +259,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 +302,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

@@ -42,6 +42,42 @@ export const Markdown = memo(
return <Artifact messageId={messageId} />;
}
if (className?.includes('__boltSelectedElement__')) {
const messageId = node?.properties.dataMessageId as string;
const elementDataAttr = node?.properties.dataElement as string;
// Parse the element data if it exists
let elementData: any = null;
if (elementDataAttr) {
try {
elementData = JSON.parse(elementDataAttr);
} catch (e) {
console.error('Failed to parse element data:', e);
}
}
if (!messageId) {
logger.error(`Invalid message id ${messageId}`);
}
return (
<div className="bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor rounded-lg p-3 my-2">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono bg-bolt-elements-background-depth-2 px-2 py-1 rounded text-bolt-elements-textTer">
{elementData?.tagName}
</span>
{elementData?.className && (
<span className="text-xs text-bolt-elements-textSecondary">.{elementData.className}</span>
)}
</div>
<code className="block text-sm !text-bolt-elements-textSecondary !bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor p-2 rounded">
{elementData?.displayText}
</code>
</div>
);
}
if (className?.includes('__boltThought__')) {
return <ThoughtBox title="Thought process">{children}</ThoughtBox>;
}

View File

@@ -7,8 +7,6 @@ import { useLocation } from '@remix-run/react';
import { db, chatId } from '~/lib/persistence/useChatHistory';
import { forkChat } from '~/lib/persistence/db';
import { toast } from 'react-toastify';
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';
@@ -29,7 +27,6 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
(props: MessagesProps, ref: ForwardedRef<HTMLDivElement> | undefined) => {
const { id, isStreaming = false, messages = [] } = props;
const location = useLocation();
const profile = useStore(profileStore);
const handleRewind = (messageId: string) => {
const searchParams = new URLSearchParams(location.search);
@@ -58,7 +55,6 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
const { role, content, id: messageId, annotations } = message;
const isUserMessage = role === 'user';
const isFirst = index === 0;
const isLast = index === messages.length - 1;
const isHidden = annotations?.includes('hidden');
if (isHidden) {
@@ -68,28 +64,10 @@ 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)]', {
'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
isStreaming && isLast,
className={classNames('flex gap-4 py-3 w-full rounded-lg', {
'mt-4': !isFirst,
})}
>
{isUserMessage && (
<div className="flex items-center justify-center w-[40px] h-[40px] overflow-hidden bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-500 rounded-full shrink-0 self-start">
{profile?.avatar ? (
<img
src={profile.avatar}
alt={profile?.username || 'User'}
className="w-full h-full object-cover"
loading="eager"
decoding="sync"
/>
) : (
<div className="i-ph:user-fill text-2xl" />
)}
</div>
)}
<div className="grid grid-col-1 w-full">
{isUserMessage ? (
<UserMessage content={content} />

View File

@@ -4,6 +4,8 @@
*/
import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
import { Markdown } from './Markdown';
import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile';
interface UserMessageProps {
content: string | Array<{ type: string; text?: string; image?: string }>;
@@ -14,10 +16,29 @@ export function UserMessage({ content }: UserMessageProps) {
const textItem = content.find((item) => item.type === 'text');
const textContent = stripMetadata(textItem?.text || '');
const images = content.filter((item) => item.type === 'image' && item.image);
const profile = useStore(profileStore);
return (
<div className="overflow-hidden flex items-center">
<div className="flex flex-col gap-4">
<div className="overflow-hidden flex flex-col gap-3 items-center ">
<div className="flex flex-row items-start justify-center overflow-hidden shrink-0 self-start">
{profile?.avatar || profile?.username ? (
<div className="flex items-end gap-2">
<img
src={profile.avatar}
alt={profile?.username || 'User'}
className="w-[25px] h-[25px] object-cover rounded-full"
loading="eager"
decoding="sync"
/>
<span className="text-bolt-elements-textPrimary text-sm">
{profile?.username ? profile.username : ''}
</span>
</div>
) : (
<div className="i-ph:user-fill text-accent-500 text-2xl" />
)}
</div>
<div className="flex flex-col gap-4 bg-accent-500/10 backdrop-blur-sm p-3 py-3 w-auto rounded-lg mr-auto">
{textContent && <Markdown html>{textContent}</Markdown>}
{images.map((item, index) => (
<img

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-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
{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,378 @@
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);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [activeSection, setActiveSection] = useState<'colors' | 'typography' | 'features'>('colors');
useEffect(() => {
if (designScheme) {
setPalette(() => ({ ...defaultDesignScheme.palette, ...designScheme.palette }));
setFeatures(designScheme.features || defaultDesignScheme.features);
setFont(designScheme.font || defaultDesignScheme.font);
} else {
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 handleSave = () => {
setDesignScheme?.({ palette, features, font });
setIsDialogOpen(false);
};
const handleReset = () => {
setPalette(defaultDesignScheme.palette);
setFeatures(defaultDesignScheme.features);
setFont(defaultDesignScheme.font);
};
const renderColorSection = () => (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-bolt-elements-item-contentAccent"></div>
Color Palette
</h3>
<button
onClick={handleReset}
className="text-sm bg-transparent hover:bg-bolt-elements-bg-depth-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary rounded-lg flex items-center gap-2 transition-all duration-200"
>
<span className="i-ph:arrow-clockwise text-sm" />
Reset
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-80 overflow-y-auto pr-2 custom-scrollbar">
{paletteRoles.map((role) => (
<div
key={role.key}
className="group flex items-center gap-4 p-4 rounded-xl bg-bolt-elements-bg-depth-3 hover:bg-bolt-elements-bg-depth-2 border border-transparent hover:border-bolt-elements-borderColor transition-all duration-200"
>
<div className="relative flex-shrink-0">
<div
className="w-12 h-12 rounded-xl shadow-md cursor-pointer transition-all duration-200 hover:scale-110 ring-2 ring-transparent hover:ring-bolt-elements-borderColorActive"
style={{ backgroundColor: palette[role.key] }}
onClick={() => document.getElementById(`color-input-${role.key}`)?.click()}
role="button"
tabIndex={0}
aria-label={`Change ${role.label} color`}
/>
<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 className="absolute -bottom-1 -right-1 w-4 h-4 bg-bolt-elements-bg-depth-1 rounded-full flex items-center justify-center shadow-sm">
<span className="i-ph:pencil-simple text-xs text-bolt-elements-textSecondary" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-bolt-elements-textPrimary transition-colors">{role.label}</div>
<div className="text-sm text-bolt-elements-textSecondary line-clamp-2 leading-relaxed">
{role.description}
</div>
<div className="text-xs text-bolt-elements-textTertiary font-mono mt-1 px-2 py-1 bg-bolt-elements-bg-depth-1 rounded-md inline-block">
{palette[role.key]}
</div>
</div>
</div>
))}
</div>
</div>
);
const renderTypographySection = () => (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-bolt-elements-item-contentAccent"></div>
Typography
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 max-h-80 overflow-y-auto pr-2 custom-scrollbar">
{designFonts.map((f) => (
<button
key={f.key}
type="button"
onClick={() => handleFontToggle(f.key)}
className={`group p-4 rounded-xl border-2 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColorActive ${
font.includes(f.key)
? 'bg-bolt-elements-item-backgroundAccent border-bolt-elements-borderColorActive shadow-lg'
: 'bg-bolt-elements-background-depth-3 border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive hover:bg-bolt-elements-bg-depth-2'
}`}
>
<div className="text-center space-y-2">
<div
className={`text-2xl font-medium transition-colors ${
font.includes(f.key) ? 'text-bolt-elements-item-contentAccent' : 'text-bolt-elements-textPrimary'
}`}
style={{ fontFamily: f.key }}
>
{f.preview}
</div>
<div
className={`text-sm font-medium transition-colors ${
font.includes(f.key) ? 'text-bolt-elements-item-contentAccent' : 'text-bolt-elements-textSecondary'
}`}
>
{f.label}
</div>
{font.includes(f.key) && (
<div className="w-6 h-6 mx-auto bg-bolt-elements-item-contentAccent rounded-full flex items-center justify-center">
<span className="i-ph:check text-white text-sm" />
</div>
)}
</div>
</button>
))}
</div>
</div>
);
const renderFeaturesSection = () => (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-bolt-elements-item-contentAccent"></div>
Design Features
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-h-80 overflow-y-auto pr-2 custom-scrollbar">
{designFeatures.map((f) => {
const isSelected = features.includes(f.key);
return (
<div key={f.key} className="feature-card-container p-2">
<button
type="button"
onClick={() => handleFeatureToggle(f.key)}
className={`group relative w-full p-6 text-sm font-medium transition-all duration-200 bg-bolt-elements-background-depth-3 text-bolt-elements-item-textSecondary ${
f.key === 'rounded'
? isSelected
? 'rounded-3xl'
: 'rounded-xl'
: f.key === 'border'
? 'rounded-lg'
: 'rounded-xl'
} ${
f.key === 'border'
? isSelected
? 'border-3 border-bolt-elements-borderColorActive bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent'
: 'border-2 border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive text-bolt-elements-textSecondary'
: f.key === 'gradient'
? ''
: isSelected
? 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent shadow-lg'
: 'bg-bolt-elements-bg-depth-3 hover:bg-bolt-elements-bg-depth-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary'
} ${f.key === 'shadow' ? (isSelected ? 'shadow-xl' : 'shadow-lg') : 'shadow-md'}`}
style={{
...(f.key === 'gradient' && {
background: isSelected
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'var(--bolt-elements-bg-depth-3)',
color: isSelected ? 'white' : 'var(--bolt-elements-textSecondary)',
}),
}}
>
<div className="flex flex-col items-center gap-4">
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-bolt-elements-bg-depth-1 bg-opacity-20">
{f.key === 'rounded' && (
<div
className={`w-6 h-6 bg-current transition-all duration-200 ${
isSelected ? 'rounded-full' : 'rounded'
} opacity-80`}
/>
)}
{f.key === 'border' && (
<div
className={`w-6 h-6 rounded-lg transition-all duration-200 ${
isSelected ? 'border-3 border-current opacity-90' : 'border-2 border-current opacity-70'
}`}
/>
)}
{f.key === 'gradient' && (
<div className="w-6 h-6 rounded-lg bg-gradient-to-br from-purple-400 via-pink-400 to-indigo-400 opacity-90" />
)}
{f.key === 'shadow' && (
<div className="relative">
<div
className={`w-6 h-6 bg-current rounded-lg transition-all duration-200 ${
isSelected ? 'opacity-90' : 'opacity-70'
}`}
/>
<div
className={`absolute top-1 left-1 w-6 h-6 bg-current rounded-lg transition-all duration-200 ${
isSelected ? 'opacity-40' : 'opacity-30'
}`}
/>
</div>
)}
{f.key === 'frosted-glass' && (
<div className="relative">
<div
className={`w-6 h-6 rounded-lg transition-all duration-200 backdrop-blur-sm bg-white/20 border border-white/30 ${
isSelected ? 'opacity-90' : 'opacity-70'
}`}
/>
<div
className={`absolute inset-0 w-6 h-6 rounded-lg transition-all duration-200 backdrop-blur-md bg-gradient-to-br from-white/10 to-transparent ${
isSelected ? 'opacity-60' : 'opacity-40'
}`}
/>
</div>
)}
</div>
<div className="text-center">
<div className="font-semibold">{f.label}</div>
{isSelected && <div className="mt-2 w-8 h-1 bg-current rounded-full mx-auto opacity-60" />}
</div>
</div>
</button>
</div>
);
})}
</div>
</div>
);
return (
<div>
<IconButton title="Design Palette" className="transition-all" onClick={() => setIsDialogOpen(!isDialogOpen)}>
<div className="i-ph:palette text-xl"></div>
</IconButton>
<DialogRoot open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<Dialog>
<div className="py-4 px-4 min-w-[480px] max-w-[90vw] max-h-[85vh] flex flex-col gap-6 overflow-hidden">
<div className="">
<DialogTitle className="text-2xl font-bold text-bolt-elements-textPrimary">
Design Palette & Features
</DialogTitle>
<DialogDescription className="text-bolt-elements-textSecondary leading-relaxed">
Customize your color palette, typography, and design features. These preferences will guide the AI in
creating designs that match your style.
</DialogDescription>
</div>
{/* Navigation Tabs */}
<div className="flex gap-1 p-1 bg-bolt-elements-bg-depth-3 rounded-xl">
{[
{ key: 'colors', label: 'Colors', icon: 'i-ph:palette' },
{ key: 'typography', label: 'Typography', icon: 'i-ph:text-aa' },
{ key: 'features', label: 'Features', icon: 'i-ph:magic-wand' },
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveSection(tab.key as any)}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg font-medium transition-all duration-200 ${
activeSection === tab.key
? 'bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary shadow-md'
: 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-bg-depth-2'
}`}
>
<span className={`${tab.icon} text-lg`} />
<span>{tab.label}</span>
</button>
))}
</div>
{/* Content Area */}
<div className=" min-h-92 overflow-y-auto">
{activeSection === 'colors' && renderColorSection()}
{activeSection === 'typography' && renderTypographySection()}
{activeSection === 'features' && renderFeaturesSection()}
</div>
{/* Action Buttons */}
<div className="flex justify-between items-center">
<div className="text-sm text-bolt-elements-textSecondary">
{Object.keys(palette).length} colors {font.length} fonts {features.length} features
</div>
<div className="flex gap-3">
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button
variant="ghost"
onClick={handleSave}
className="bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
>
Save Changes
</Button>
</div>
</div>
</div>
</Dialog>
</DialogRoot>
<style>{`
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--bolt-elements-textTertiary) transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--bolt-elements-textTertiary);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: var(--bolt-elements-textSecondary);
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.feature-card-container {
min-height: 140px;
display: flex;
align-items: stretch;
}
.feature-card-container button {
flex: 1;
}
`}</style>
</div>
);
};

View File

@@ -0,0 +1,126 @@
import { useEffect, useRef, useState } from 'react';
interface InspectorProps {
isActive: boolean;
iframeRef: React.RefObject<HTMLIFrameElement>;
onElementSelect: (elementInfo: ElementInfo) => void;
}
export interface ElementInfo {
displayText: string;
tagName: string;
className: string;
id: string;
textContent: string;
styles: Record<string, string>; // Changed from CSSStyleDeclaration
rect: {
x: number;
y: number;
width: number;
height: number;
top: number;
left: number;
};
}
export const Inspector = ({ isActive, iframeRef, onElementSelect }: InspectorProps) => {
const [hoveredElement, setHoveredElement] = useState<ElementInfo | null>(null);
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isActive || !iframeRef.current) {
return undefined;
}
const iframe = iframeRef.current;
// Listen for messages from the iframe
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'INSPECTOR_HOVER') {
const elementInfo = event.data.elementInfo;
// Adjust coordinates relative to iframe position
const iframeRect = iframe.getBoundingClientRect();
elementInfo.rect.x += iframeRect.x;
elementInfo.rect.y += iframeRect.y;
elementInfo.rect.top += iframeRect.y;
elementInfo.rect.left += iframeRect.x;
setHoveredElement(elementInfo);
} else if (event.data.type === 'INSPECTOR_CLICK') {
const elementInfo = event.data.elementInfo;
// Adjust coordinates relative to iframe position
const iframeRect = iframe.getBoundingClientRect();
elementInfo.rect.x += iframeRect.x;
elementInfo.rect.y += iframeRect.y;
elementInfo.rect.top += iframeRect.y;
elementInfo.rect.left += iframeRect.x;
onElementSelect(elementInfo);
} else if (event.data.type === 'INSPECTOR_LEAVE') {
setHoveredElement(null);
}
};
window.addEventListener('message', handleMessage);
// Send activation message to iframe
const sendActivationMessage = () => {
if (iframe.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'INSPECTOR_ACTIVATE',
active: isActive,
},
'*',
);
}
};
// Try to send activation message immediately and on load
sendActivationMessage();
iframe.addEventListener('load', sendActivationMessage);
return () => {
window.removeEventListener('message', handleMessage);
iframe.removeEventListener('load', sendActivationMessage);
// Deactivate inspector in iframe
if (iframe.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'INSPECTOR_ACTIVATE',
active: false,
},
'*',
);
}
};
}, [isActive, iframeRef, onElementSelect]);
// Render overlay for hovered element
return (
<>
{isActive && hoveredElement && (
<div
ref={overlayRef}
className="fixed pointer-events-none z-50 border-2 border-blue-500 bg-blue-500/10"
style={{
left: hoveredElement.rect.x,
top: hoveredElement.rect.y,
width: hoveredElement.rect.width,
height: hoveredElement.rect.height,
}}
>
{/* Element info tooltip */}
<div className="absolute -top-8 left-0 bg-gray-900 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
{hoveredElement.tagName.toLowerCase()}
{hoveredElement.id && `#${hoveredElement.id}`}
{hoveredElement.className && `.${hoveredElement.className.split(' ')[0]}`}
</div>
</div>
)}
</>
);
};

View File

@@ -0,0 +1,146 @@
import { useState } from 'react';
interface ElementInfo {
tagName: string;
className: string;
id: string;
textContent: string;
styles: Record<string, string>; // Changed from CSSStyleDeclaration
rect: {
x: number;
y: number;
width: number;
height: number;
top: number;
left: number;
};
}
interface InspectorPanelProps {
selectedElement: ElementInfo | null;
isVisible: boolean;
onClose: () => void;
}
export const InspectorPanel = ({ selectedElement, isVisible, onClose }: InspectorPanelProps) => {
const [activeTab, setActiveTab] = useState<'styles' | 'computed' | 'box'>('styles');
if (!isVisible || !selectedElement) {
return null;
}
const getRelevantStyles = (styles: Record<string, string>) => {
const relevantProps = [
'display',
'position',
'width',
'height',
'margin',
'padding',
'border',
'background',
'color',
'font-size',
'font-family',
'text-align',
'flex-direction',
'justify-content',
'align-items',
];
return relevantProps.reduce(
(acc, prop) => {
const value = styles[prop];
if (value) {
acc[prop] = value;
}
return acc;
},
{} as Record<string, string>,
);
};
return (
<div className="fixed right-4 top-20 w-80 bg-bolt-elements-bg-depth-1 border border-bolt-elements-borderColor rounded-lg shadow-lg z-40 max-h-[calc(100vh-6rem)] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b border-bolt-elements-borderColor">
<h3 className="font-medium text-bolt-elements-textPrimary">Element Inspector</h3>
<button onClick={onClose} className="text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary">
</button>
</div>
{/* Element Info */}
<div className="p-3 border-b border-bolt-elements-borderColor">
<div className="text-sm">
<div className="font-mono text-blue-500">
{selectedElement.tagName.toLowerCase()}
{selectedElement.id && <span className="text-green-500">#{selectedElement.id}</span>}
{selectedElement.className && (
<span className="text-yellow-500">.{selectedElement.className.split(' ')[0]}</span>
)}
</div>
{selectedElement.textContent && (
<div className="mt-1 text-bolt-elements-textSecondary text-xs truncate">
"{selectedElement.textContent}"
</div>
)}
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-bolt-elements-borderColor">
{(['styles', 'computed', 'box'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-3 py-2 text-sm capitalize ${
activeTab === tab
? 'border-b-2 border-blue-500 text-blue-500'
: 'text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary'
}`}
>
{tab}
</button>
))}
</div>
{/* Content */}
<div className="p-3 overflow-y-auto max-h-96">
{activeTab === 'styles' && (
<div className="space-y-2">
{Object.entries(getRelevantStyles(selectedElement.styles)).map(([prop, value]) => (
<div key={prop} className="flex justify-between text-sm">
<span className="text-bolt-elements-textSecondary">{prop}:</span>
<span className="text-bolt-elements-textPrimary font-mono">{value}</span>
</div>
))}
</div>
)}
{activeTab === 'box' && (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-bolt-elements-textSecondary">Width:</span>
<span className="text-bolt-elements-textPrimary">{Math.round(selectedElement.rect.width)}px</span>
</div>
<div className="flex justify-between">
<span className="text-bolt-elements-textSecondary">Height:</span>
<span className="text-bolt-elements-textPrimary">{Math.round(selectedElement.rect.height)}px</span>
</div>
<div className="flex justify-between">
<span className="text-bolt-elements-textSecondary">Top:</span>
<span className="text-bolt-elements-textPrimary">{Math.round(selectedElement.rect.top)}px</span>
</div>
<div className="flex justify-between">
<span className="text-bolt-elements-textSecondary">Left:</span>
<span className="text-bolt-elements-textPrimary">{Math.round(selectedElement.rect.left)}px</span>
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -6,9 +6,14 @@ import { PortDropdown } from './PortDropdown';
import { ScreenshotSelector } from './ScreenshotSelector';
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
import type { ElementInfo } from './Inspector';
type ResizeSide = 'left' | 'right' | null;
interface PreviewProps {
setSelectedElement?: (element: ElementInfo | null) => void;
}
interface WindowSize {
name: string;
width: number;
@@ -47,11 +52,10 @@ const WINDOW_SIZES: WindowSize[] = [
{ name: '4K Display', width: 3840, height: 2160, icon: 'i-ph:monitor', hasFrame: true, frameType: 'desktop' },
];
export const Preview = memo(() => {
export const Preview = memo(({ setSelectedElement }: PreviewProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
@@ -61,11 +65,8 @@ export const Preview = memo(() => {
const [displayPath, setDisplayPath] = useState('/');
const [iframeUrl, setIframeUrl] = useState<string | undefined>();
const [isSelectionMode, setIsSelectionMode] = useState(false);
// Toggle between responsive mode and device mode
const [isInspectorMode, setIsInspectorMode] = useState(false);
const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
// Use percentage for width
const [widthPercent, setWidthPercent] = useState<number>(37.5);
const [currentWidth, setCurrentWidth] = useState<number>(0);
@@ -618,6 +619,47 @@ export const Preview = memo(() => {
};
}, [showDeviceFrameInPreview]);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'INSPECTOR_READY') {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{
type: 'INSPECTOR_ACTIVATE',
active: isInspectorMode,
},
'*',
);
}
} else if (event.data.type === 'INSPECTOR_CLICK') {
const element = event.data.elementInfo;
navigator.clipboard.writeText(element.displayText).then(() => {
setSelectedElement?.(element);
});
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [isInspectorMode]);
const toggleInspectorMode = () => {
const newInspectorMode = !isInspectorMode;
setIsInspectorMode(newInspectorMode);
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{
type: 'INSPECTOR_ACTIVATE',
active: newInspectorMode,
},
'*',
);
}
};
return (
<div ref={containerRef} className={`w-full h-full flex flex-col relative`}>
{isPortDropdownOpen && (
@@ -697,7 +739,14 @@ export const Preview = memo(() => {
/>
</>
)}
<IconButton
icon="i-ph:cursor-click"
onClick={toggleInspectorMode}
className={
isInspectorMode ? 'bg-bolt-elements-background-depth-3 !text-bolt-elements-item-contentAccent' : ''
}
title={isInspectorMode ? 'Disable Element Inspector' : 'Enable Element Inspector'}
/>
<IconButton
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
onClick={toggleFullscreen}

View File

@@ -26,6 +26,8 @@ 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';
import type { ElementInfo } from './Inspector';
interface WorkspaceProps {
chatStarted?: boolean;
@@ -35,6 +37,7 @@ interface WorkspaceProps {
gitUrl?: string;
};
updateChatMestaData?: (metadata: any) => void;
setSelectedElement?: (element: ElementInfo | null) => void;
}
const viewTransition = { ease: cubicEasingFn };
@@ -278,7 +281,7 @@ const FileModifiedDropdown = memo(
);
export const Workbench = memo(
({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData }: WorkspaceProps) => {
({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => {
renderLogger.trace('Workbench');
const [isSyncing, setIsSyncing] = useState(false);
@@ -294,6 +297,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 +375,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 +384,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 mr-1`}
disabled={!canHideChat || isSmallViewport}
onClick={() => {
if (canHideChat) {
chatStore.setKey('showChat', !showChat);
}
}}
/>
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
<div className="ml-auto" />
{selectedView === 'code' && (
@@ -398,7 +412,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 +426,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',
@@ -488,7 +489,7 @@ export const Workbench = memo(
<DiffView fileHistory={fileHistory} setFileHistory={setFileHistory} actionRunner={actionRunner} />
</View>
<View initial={{ x: '100%' }} animate={{ x: selectedView === 'preview' ? '0%' : '100%' }}>
<Preview />
<Preview setSelectedElement={setSelectedElement} />
</View>
</div>
</div>

View File

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

View File

@@ -1,11 +1,13 @@
import { getSystemPrompt } from './prompts/prompts';
import optimized from './prompts/optimized';
import { getFineTunedPrompt } from './prompts/new-prompt';
import type { DesignScheme } from '~/types/design-scheme';
export interface PromptOptions {
cwd: string;
allowedHtmlElements: string[];
modificationTagName: string;
designScheme?: DesignScheme;
supabase?: {
isConnected: boolean;
hasSelectedProject: boolean;
@@ -28,12 +30,12 @@ export class PromptLibrary {
default: {
label: 'Default 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: {
label: 'Fine Tuned Prompt',
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: {
label: 'Optimized Prompt (experimental)',

View File

@@ -1,3 +1,4 @@
import type { DesignScheme } from '~/types/design-scheme';
import { WORK_DIR } from '~/utils/constants';
import { allowedHTMLElements } from '~/utils/markdown';
import { stripIndents } from '~/utils/stripIndent';
@@ -9,6 +10,7 @@ export const getFineTunedPrompt = (
hasSelectedProject: boolean;
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.
@@ -428,6 +430,15 @@ The year is 2025.
- Use CSS Grid and Flexbox for layouts
- Implement appropriate container queries when needed
- Structure mobile-first designs that progressively enhance for larger screens
<user_provided_design>
USER PROVIDED DESIGN SCHEME:
- ALWAYS use the user provided design scheme when creating designs ensuring it complies with the professionalism of design instructions we have provided, unless the user specifically requests otherwise.
- Ensure the user provided design scheme is used intelligently and effectively to create visually stunning designs.
FONT: ${JSON.stringify(designScheme?.font)}
COLOR PALETTE: ${JSON.stringify(designScheme?.palette)}
FEATURES: ${JSON.stringify(designScheme?.features)}
</user_provided_design>
</design_instructions>
<mobile_app_instructions>

View File

@@ -1,3 +1,4 @@
import type { DesignScheme } from '~/types/design-scheme';
import { WORK_DIR } from '~/utils/constants';
import { allowedHTMLElements } from '~/utils/markdown';
import { stripIndents } from '~/utils/stripIndent';
@@ -9,6 +10,7 @@ export const getSystemPrompt = (
hasSelectedProject: boolean;
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.
@@ -424,6 +426,14 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
- Ensure consistency in design language and interactions throughout.
- Pay meticulous attention to detail and polish.
- Always prioritize user needs and iterate based on feedback.
<user_provided_design>
USER PROVIDED DESIGN SCHEME:
- ALWAYS use the user provided design scheme when creating designs ensuring it complies with the professionalism of design instructions below, 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>
</design_instructions>
</artifact_info>

View File

@@ -133,6 +133,7 @@ function Content({ children, ...props }: StickToBottomContentProps) {
<div {...props} ref={context.contentRef}>
{typeof children === 'function' ? children(context) : children}
</div>
{/* Blur effect overlay */}
</div>
);
}

View File

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

View File

@@ -34,6 +34,10 @@ if (!import.meta.env.SSR) {
const { workbenchStore } = await import('~/lib/stores/workbench');
const response = await fetch('/inspector-script.js');
const inspectorScript = await response.text();
await webcontainer.setPreviewScript(inspectorScript);
// Listen for preview errors
webcontainer.on('preview-message', (message) => {
console.log('WebContainer preview message:', message);

View File

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

View File

@@ -11,6 +11,7 @@ html,
body {
height: 100%;
width: 100%;
background-color: var(--bolt-elements-bg-depth-1);
}
:root {

View File

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

View File

@@ -0,0 +1,93 @@
export interface DesignScheme {
palette: { [key: string]: string }; // Changed from string[] to object
features: string[];
font: string[];
}
export const defaultDesignScheme: DesignScheme = {
palette: {
primary: '#9E7FFF',
secondary: '#38bdf8',
accent: '#f472b6',
background: '#171717',
surface: '#262626',
text: '#FFFFFF',
textSecondary: '#A3A3A3',
border: '#2F2F2F',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
},
features: ['rounded'],
font: ['sans-serif'],
};
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' },
{ key: 'frosted-glass', label: 'Frosted Glass' },
];
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' },
];

View File

@@ -56,6 +56,7 @@ export const allowedHTMLElements = [
'ul',
'var',
'think',
'header',
];
// Add custom rehype plugin
@@ -85,7 +86,7 @@ const rehypeSanitizeOptions: RehypeSanitizeOptions = {
div: [
...(defaultSchema.attributes?.div ?? []),
'data*',
['className', '__boltArtifact__', '__boltThought__', '__boltQuickAction'],
['className', '__boltArtifact__', '__boltThought__', '__boltQuickAction', '__boltSelectedElement__'],
// ['className', '__boltThought__']
],