feat(layout): allow to minimize chat (#35)

This commit is contained in:
Dominic Elm 2024-08-14 11:08:52 +02:00 committed by GitHub
parent 8fd9d4477e
commit d5a29c2427
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 262 additions and 114 deletions

View File

@ -30,10 +30,11 @@ pnpm install
ANTHROPIC_API_KEY=XXX ANTHROPIC_API_KEY=XXX
``` ```
Optionally, you an set the debug level: Optionally, you an set the debug level or disable authentication:
``` ```
VITE_LOG_LEVEL=debug VITE_LOG_LEVEL=debug
VITE_DISABLE_AUTH=1
``` ```
If you want to run authentication against a local StackBlitz instance, add: If you want to run authentication against a local StackBlitz instance, add:

View File

@ -0,0 +1,19 @@
.BaseChat {
&[data-chat-visible='false'] {
--workbench-inner-width: 100%;
--workbench-left: 0;
.Chat {
--at-apply: bolt-ease-cubic-bezier;
transition-property: transform, opacity;
transition-duration: 0.3s;
will-change: transform, opacity;
transform: translateX(-50%);
opacity: 0;
}
}
}
.Chat {
opacity: 1;
}

View File

@ -8,10 +8,13 @@ import { classNames } from '~/utils/classNames';
import { Messages } from './Messages.client'; import { Messages } from './Messages.client';
import { SendButton } from './SendButton.client'; import { SendButton } from './SendButton.client';
import styles from './BaseChat.module.scss';
interface BaseChatProps { interface BaseChatProps {
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined; textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
messageRef?: RefCallback<HTMLDivElement> | undefined; messageRef?: RefCallback<HTMLDivElement> | undefined;
scrollRef?: RefCallback<HTMLDivElement> | undefined; scrollRef?: RefCallback<HTMLDivElement> | undefined;
showChat?: boolean;
chatStarted?: boolean; chatStarted?: boolean;
isStreaming?: boolean; isStreaming?: boolean;
messages?: Message[]; messages?: Message[];
@ -40,6 +43,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
textareaRef, textareaRef,
messageRef, messageRef,
scrollRef, scrollRef,
showChat = true,
chatStarted = false, chatStarted = false,
isStreaming = false, isStreaming = false,
enhancingPrompt = false, enhancingPrompt = false,
@ -56,12 +60,19 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
return ( return (
<div ref={ref} className="relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1"> <div
ref={ref}
className={classNames(
styles.BaseChat,
'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
)}
data-chat-visible={showChat}
>
<ClientOnly>{() => <Menu />}</ClientOnly> <ClientOnly>{() => <Menu />}</ClientOnly>
<div ref={scrollRef} className="flex overflow-scroll w-full h-full"> <div ref={scrollRef} className="flex overflow-scroll w-full h-full">
<div className="flex flex-col w-full h-full px-6"> <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
{!chatStarted && ( {!chatStarted && (
<div id="intro" className="mt-[26vh] max-w-2xl mx-auto"> <div id="intro" className="mt-[26vh] max-w-chat mx-auto">
<h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2"> <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
Where ideas begin Where ideas begin
</h1> </h1>
@ -71,7 +82,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div> </div>
)} )}
<div <div
className={classNames('pt-6', { className={classNames('pt-6 px-6', {
'h-full flex flex-col': chatStarted, 'h-full flex flex-col': chatStarted,
})} })}
> >
@ -80,7 +91,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
return chatStarted ? ( return chatStarted ? (
<Messages <Messages
ref={messageRef} ref={messageRef}
className="flex flex-col w-full flex-1 max-w-2xl px-4 pb-6 mx-auto z-1" className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
messages={messages} messages={messages}
isStreaming={isStreaming} isStreaming={isStreaming}
/> />
@ -88,7 +99,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}} }}
</ClientOnly> </ClientOnly>
<div <div
className={classNames('relative w-full max-w-2xl md:mx-auto z-2', { className={classNames('relative w-full max-w-chat mx-auto z-prompt', {
'sticky bottom-0': chatStarted, 'sticky bottom-0': chatStarted,
})} })}
> >
@ -174,7 +185,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div> </div>
</div> </div>
{!chatStarted && ( {!chatStarted && (
<div id="examples" className="relative w-full max-w-2xl mx-auto mt-8 flex justify-center"> <div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
<div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]"> <div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
{EXAMPLE_PROMPTS.map((examplePrompt, index) => { {EXAMPLE_PROMPTS.map((examplePrompt, index) => {
return ( return (

View File

@ -1,8 +1,10 @@
import { useStore } from '@nanostores/react';
import type { Message } from 'ai'; import type { Message } from 'ai';
import { useChat } from 'ai/react'; import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion'; import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react'; import { memo, useEffect, useRef, useState } from 'react';
import { cssTransition, toast, ToastContainer } from 'react-toastify'; import { cssTransition, toast, ToastContainer } from 'react-toastify';
import { AnalyticsAction, AnalyticsTrackEvent, sendAnalyticsEvent } from '~/lib/analytics';
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks'; import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
import { useChatHistory } from '~/lib/persistence'; import { useChatHistory } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat'; import { chatStore } from '~/lib/stores/chat';
@ -11,7 +13,6 @@ import { fileModificationsToHTML } from '~/utils/diff';
import { cubicEasingFn } from '~/utils/easings'; import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger'; import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat'; import { BaseChat } from './BaseChat';
import { sendAnalyticsEvent, AnalyticsTrackEvent, AnalyticsAction } from '~/lib/analytics';
const toastAnimation = cssTransition({ const toastAnimation = cssTransition({
enter: 'animated fadeInRight', enter: 'animated fadeInRight',
@ -71,6 +72,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0); const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
const { showChat } = useStore(chatStore);
const [animationScope, animate] = useAnimate(); const [animationScope, animate] = useAnimate();
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({ const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
@ -213,6 +216,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
ref={animationScope} ref={animationScope}
textareaRef={textareaRef} textareaRef={textareaRef}
input={input} input={input}
showChat={showChat}
chatStarted={chatStarted} chatStarted={chatStarted}
isStreaming={isLoading} isStreaming={isLoading}
enhancingPrompt={enhancingPrompt} enhancingPrompt={enhancingPrompt}

View File

@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { ClientOnly } from 'remix-utils/client-only'; import { ClientOnly } from 'remix-utils/client-only';
import { chatStore } from '~/lib/stores/chat'; import { chatStore } from '~/lib/stores/chat';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
import { OpenStackBlitz } from './OpenStackBlitz.client'; import { HeaderActionButtons } from './HeaderActionButtons.client';
export function Header() { export function Header() {
const chat = useStore(chatStore); const chat = useStore(chatStore);
@ -17,14 +17,22 @@ export function Header() {
}, },
)} )}
> >
<div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary"> <div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
<div className="i-ph:sidebar-simple-duotone text-xl" />
<a href="/" className="text-2xl font-semibold text-accent flex items-center"> <a href="/" className="text-2xl font-semibold text-accent flex items-center">
<span className="i-bolt:logo-text?mask w-[46px] inline-block" /> <span className="i-bolt:logo-text?mask w-[46px] inline-block" />
</a> </a>
</div> </div>
<div className="ml-auto flex gap-2"> <div className="flex-1" />
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly> {chat.started && (
</div> <ClientOnly>
{() => (
<div className="mr-1">
<HeaderActionButtons />
</div>
)}
</ClientOnly>
)}
</header> </header>
); );
} }

View File

@ -0,0 +1,72 @@
import { useStore } from '@nanostores/react';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
import { OpenStackBlitz } from './OpenStackBlitz.client';
interface HeaderActionButtonsProps {}
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
const showWorkbench = useStore(workbenchStore.showWorkbench);
const { showChat } = useStore(chatStore);
const canHideChat = showWorkbench || !showChat;
return (
<div className="flex">
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
<Button
active={showChat}
disabled={!canHideChat}
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 ml-2">
<OpenStackBlitz />
</div>
</div>
);
}
interface ButtonProps {
active?: boolean;
disabled?: boolean;
children?: any;
onClick?: VoidFunction;
}
function Button({ active = false, disabled = false, children, onClick }: 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,
})}
onClick={onClick}
>
{children}
</button>
);
}

View File

@ -1,10 +1,11 @@
import path from 'path';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import sdk from '@stackblitz/sdk'; import sdk from '@stackblitz/sdk';
import path from 'path';
import { memo, useCallback, useEffect, useState } from 'react';
import type { FileMap } from '~/lib/stores/files'; import type { FileMap } from '~/lib/stores/files';
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench'; import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
import { WORK_DIR } from '~/utils/constants'; import { WORK_DIR } from '~/utils/constants';
import { memo, useCallback, useEffect, useState } from 'react';
// extract relative path and content from file, wrapped in array for flatMap use // extract relative path and content from file, wrapped in array for flatMap use
const extractContent = ([file, value]: [string, FileMap[string]]) => { const extractContent = ([file, value]: [string, FileMap[string]]) => {
@ -47,6 +48,8 @@ const useFirstArtifact = (): [boolean, ArtifactState | undefined] => {
export const OpenStackBlitz = memo(() => { export const OpenStackBlitz = memo(() => {
const [artifactLoaded, artifact] = useFirstArtifact(); const [artifactLoaded, artifact] = useFirstArtifact();
const disabled = !artifactLoaded;
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (!artifact) { if (!artifact) {
return; return;
@ -66,13 +69,34 @@ export const OpenStackBlitz = memo(() => {
}); });
}, [artifact]); }, [artifact]);
if (!artifactLoaded) {
return null;
}
return ( return (
<a onClick={handleClick} className="cursor-pointer"> <button
<img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" /> className={classNames(
</a> 'relative flex items-stretch p-[1px] overflow-hidden text-xs text-bolt-elements-cta-text rounded-lg bg-bolt-elements-borderColor dark:bg-gray-800',
{
'cursor-not-allowed opacity-50': disabled,
'group hover:bg-gradient-to-t from-accent-900 to-accent-500 hover:text-white': !disabled,
},
)}
onClick={handleClick}
disabled={disabled}
>
<div
className={classNames(
'flex items-center gap-1.5 px-3 bg-bolt-elements-cta-background dark:bg-alpha-gray-80 group-hover:bg-transparent rounded-[calc(0.5rem-1px)] group-hover:bg-opacity-0',
{
'opacity-50': disabled,
},
)}
>
<svg width="11" height="16">
<path
fill="currentColor"
d="M4.67 9.85a.3.3 0 0 0-.27-.4H.67a.3.3 0 0 1-.21-.49l7.36-7.9c.22-.24.6 0 .5.3l-1.75 4.8a.3.3 0 0 0 .28.39h3.72c.26 0 .4.3.22.49l-7.37 7.9c-.21.24-.6 0-.49-.3l1.74-4.8Z"
/>
</svg>
<span>Open in StackBlitz</span>
</div>
</button>
); );
}); });

View File

@ -50,7 +50,7 @@ export function Menu() {
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
const enterThreshold = 80; const enterThreshold = 40;
const exitThreshold = 40; const exitThreshold = 40;
function onMouseMove(event: MouseEvent) { function onMouseMove(event: MouseEvent) {

View File

@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
return ( return (
<button <button
className={classNames( className={classNames(
'flex items-center gap-1.5 px-1.5 rounded-md py-0.5 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed', 'flex items-center shrink-0 gap-1.5 px-1.5 rounded-md py-0.5 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
{ {
[classNames('opacity-30', disabledClassName)]: disabled, [classNames('opacity-30', disabledClassName)]: disabled,
}, },

View File

@ -1,6 +1,7 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { memo } from 'react'; import { memo } from 'react';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
import { cubicEasingFn } from '~/utils/easings';
import { genericMemo } from '~/utils/react'; import { genericMemo } from '~/utils/react';
interface SliderOption<T> { interface SliderOption<T> {
@ -23,7 +24,7 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
const isLeftSelected = selected === options.left.value; const isLeftSelected = selected === options.left.value;
return ( return (
<div className="flex items-center flex-wrap gap-1 bg-bolt-elements-background-depth-1 rounded-full p-1"> <div className="flex items-center flex-wrap shrink-0 gap-1 bg-bolt-elements-background-depth-1 overflow-hidden rounded-full p-1">
<SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}> <SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
{options.left.text} {options.left.text}
</SliderButton> </SliderButton>
@ -55,7 +56,7 @@ const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProp
{selected && ( {selected && (
<motion.span <motion.span
layoutId="pill-tab" layoutId="pill-tab"
transition={{ type: 'spring', duration: 0.5 }} transition={{ duration: 0.2, ease: cubicEasingFn }}
className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full" className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
></motion.span> ></motion.span>
)} )}

View File

@ -17,9 +17,10 @@ import type { FileMap } from '~/lib/stores/files';
import { themeStore } from '~/lib/stores/theme'; import { themeStore } from '~/lib/stores/theme';
import { workbenchStore } from '~/lib/stores/workbench'; import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
import { WORK_DIR } from '~/utils/constants';
import { renderLogger } from '~/utils/logger'; import { renderLogger } from '~/utils/logger';
import { isMobile } from '~/utils/mobile'; import { isMobile } from '~/utils/mobile';
import { FileTreePanel } from './FileTreePanel'; import { FileTree } from './FileTree';
import { Terminal, type TerminalRef } from './terminal/Terminal'; import { Terminal, type TerminalRef } from './terminal/Terminal';
interface EditorPanelProps { interface EditorPanelProps {
@ -124,22 +125,24 @@ export const EditorPanel = memo(
<PanelGroup direction="vertical"> <PanelGroup direction="vertical">
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}> <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
<PanelGroup direction="horizontal"> <PanelGroup direction="horizontal">
<Panel defaultSize={25} minSize={10} collapsible> <Panel defaultSize={20} minSize={10} collapsible>
<div className="flex flex-col border-r border-bolt-elements-borderColor h-full"> <div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
<PanelHeader> <PanelHeader>
<div className="i-ph:tree-structure-duotone shrink-0" /> <div className="i-ph:tree-structure-duotone shrink-0" />
Files Files
</PanelHeader> </PanelHeader>
<FileTreePanel <FileTree
className="h-full"
files={files} files={files}
unsavedFiles={unsavedFiles} unsavedFiles={unsavedFiles}
rootFolder={WORK_DIR}
selectedFile={selectedFile} selectedFile={selectedFile}
onFileSelect={onFileSelect} onFileSelect={onFileSelect}
/> />
</div> </div>
</Panel> </Panel>
<PanelResizeHandle /> <PanelResizeHandle />
<Panel className="flex flex-col" defaultSize={75} minSize={20}> <Panel className="flex flex-col" defaultSize={80} minSize={20}>
<PanelHeader> <PanelHeader>
{activeFile && ( {activeFile && (
<div className="flex items-center flex-1 text-sm"> <div className="flex items-center flex-1 text-sm">

View File

@ -1,29 +0,0 @@
import { memo } from 'react';
import type { FileMap } from '~/lib/stores/files';
import { WORK_DIR } from '~/utils/constants';
import { renderLogger } from '~/utils/logger';
import { FileTree } from './FileTree';
interface FileTreePanelProps {
files?: FileMap;
selectedFile?: string;
unsavedFiles?: Set<string>;
onFileSelect?: (value?: string) => void;
}
export const FileTreePanel = memo(({ files, unsavedFiles, selectedFile, onFileSelect }: FileTreePanelProps) => {
renderLogger.trace('FileTreePanel');
return (
<div className="flex-1 overflow-y-scroll">
<FileTree
className="h-full"
files={files}
unsavedFiles={unsavedFiles}
rootFolder={WORK_DIR}
selectedFile={selectedFile}
onFileSelect={onFileSelect}
/>
</div>
);
});

View File

@ -82,11 +82,11 @@ export const Preview = memo(() => {
/> />
</div> </div>
</div> </div>
<div className="flex-1 bg-white border-t"> <div className="flex-1 border-t border-bolt-elements-borderColor">
{activePreview ? ( {activePreview ? (
<iframe ref={iframeRef} className="border-none w-full h-full" src={iframeUrl} /> <iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} />
) : ( ) : (
<div className="flex w-full h-full justify-center items-center">No preview available</div> <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
)} )}
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@ import { IconButton } from '~/components/ui/IconButton';
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton'; import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
import { Slider, type SliderOptions } from '~/components/ui/Slider'; import { Slider, type SliderOptions } from '~/components/ui/Slider';
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench'; import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
import { cubicEasingFn } from '~/utils/easings'; import { cubicEasingFn } from '~/utils/easings';
import { renderLogger } from '~/utils/logger'; import { renderLogger } from '~/utils/logger';
import { EditorPanel } from './EditorPanel'; import { EditorPanel } from './EditorPanel';
@ -43,7 +44,7 @@ const workbenchVariants = {
}, },
}, },
open: { open: {
width: '100%', width: 'var(--workbench-width)',
transition: { transition: {
duration: 0.2, duration: 0.2,
ease: cubicEasingFn, ease: cubicEasingFn,
@ -100,53 +101,71 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
return ( return (
chatStarted && ( chatStarted && (
<motion.div initial="closed" animate={showWorkbench ? 'open' : 'closed'} variants={workbenchVariants}> <motion.div
<div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0"> initial="closed"
<div className="flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden absolute inset-0 right-8"> animate={showWorkbench ? 'open' : 'closed'}
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor"> variants={workbenchVariants}
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} /> className="z-workbench"
<PanelHeaderButton >
className="ml-auto mr-1 text-sm" <div
onClick={() => { className={classNames(
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get()); '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',
}} {
> 'left-[var(--workbench-left)]': showWorkbench,
<div className="i-ph:terminal" /> 'left-[100%]': !showWorkbench,
Toggle Terminal },
</PanelHeaderButton> )}
<IconButton >
icon="i-ph:x-circle" <div className="absolute inset-0 px-6">
className="-mr-1" <div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
size="xl" <div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
onClick={() => { <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
workbenchStore.showWorkbench.set(false); <div className="ml-auto" />
}} {selectedView === 'code' && (
/> <PanelHeaderButton
</div> className="mr-1 text-sm"
<div className="relative flex-1 overflow-hidden"> onClick={() => {
<View workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
initial={{ x: selectedView === 'code' ? 0 : '-100%' }} }}
animate={{ x: selectedView === 'code' ? 0 : '-100%' }} >
> <div className="i-ph:terminal" />
<EditorPanel Toggle Terminal
editorDocument={currentDocument} </PanelHeaderButton>
isStreaming={isStreaming} )}
selectedFile={selectedFile} <IconButton
files={files} icon="i-ph:x-circle"
unsavedFiles={unsavedFiles} className="-mr-1"
onFileSelect={onFileSelect} size="xl"
onEditorScroll={onEditorScroll} onClick={() => {
onEditorChange={onEditorChange} workbenchStore.showWorkbench.set(false);
onFileSave={onFileSave} }}
onFileReset={onFileReset}
/> />
</View> </div>
<View <div className="relative flex-1 overflow-hidden">
initial={{ x: selectedView === 'preview' ? 0 : '100%' }} <View
animate={{ x: selectedView === 'preview' ? 0 : '100%' }} initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
> animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
<Preview /> >
</View> <EditorPanel
editorDocument={currentDocument}
isStreaming={isStreaming}
selectedFile={selectedFile}
files={files}
unsavedFiles={unsavedFiles}
onFileSelect={onFileSelect}
onEditorScroll={onEditorScroll}
onEditorChange={onEditorChange}
onFileSave={onFileSave}
onFileReset={onFileReset}
/>
</View>
<View
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
>
<Preview />
</View>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,4 +3,5 @@ import { map } from 'nanostores';
export const chatStore = map({ export const chatStore = map({
started: false, started: false,
aborted: false, aborted: false,
showChat: true,
}); });

View File

@ -161,8 +161,8 @@
--bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1); --bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
--bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3); --bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
--bolt-elements-cta-background: theme('colors.gray.100'); --bolt-elements-cta-background: theme('colors.alpha.white.10');
--bolt-elements-cta-text: theme('colors.gray.950'); --bolt-elements-cta-text: theme('colors.white');
/* Terminal Colors */ /* Terminal Colors */
--bolt-terminal-background: var(--bolt-elements-terminals-background); --bolt-terminal-background: var(--bolt-elements-terminals-background);
@ -193,6 +193,11 @@
*/ */
:root { :root {
--header-height: 54px; --header-height: 54px;
--chat-max-width: 37rem;
--chat-min-width: 640px;
--workbench-width: min(calc(100% - var(--chat-min-width)), 1536px);
--workbench-inner-width: var(--workbench-width);
--workbench-left: calc(100% - var(--workbench-width));
/* Toasts */ /* Toasts */
--toastify-color-progress-success: var(--bolt-elements-icon-success); --toastify-color-progress-success: var(--bolt-elements-icon-success);

View File

@ -8,6 +8,14 @@ $zIndexMax: 999;
z-index: $zIndexMax - 2; z-index: $zIndexMax - 2;
} }
.z-prompt {
z-index: 2;
}
.z-workbench {
z-index: 3;
}
.z-max { .z-max {
z-index: $zIndexMax; z-index: $zIndexMax;
} }

View File

@ -99,9 +99,10 @@ const COLOR_PRIMITIVES = {
export default defineConfig({ export default defineConfig({
shortcuts: { shortcuts: {
'transition-theme': 'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
'transition-[background-color,border-color,color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]', 'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md', kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
'max-w-chat': 'max-w-[var(--chat-max-width)]',
}, },
theme: { theme: {
colors: { colors: {