mirror of
https://github.com/stackblitz/bolt.new
synced 2024-11-27 14:32:46 +00:00
feat(layout): allow to minimize chat (#35)
This commit is contained in:
parent
8fd9d4477e
commit
d5a29c2427
@ -30,10 +30,11 @@ pnpm install
|
||||
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_DISABLE_AUTH=1
|
||||
```
|
||||
|
||||
If you want to run authentication against a local StackBlitz instance, add:
|
||||
|
19
packages/bolt/app/components/chat/BaseChat.module.scss
Normal file
19
packages/bolt/app/components/chat/BaseChat.module.scss
Normal 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;
|
||||
}
|
@ -8,10 +8,13 @@ import { classNames } from '~/utils/classNames';
|
||||
import { Messages } from './Messages.client';
|
||||
import { SendButton } from './SendButton.client';
|
||||
|
||||
import styles from './BaseChat.module.scss';
|
||||
|
||||
interface BaseChatProps {
|
||||
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
||||
messageRef?: RefCallback<HTMLDivElement> | undefined;
|
||||
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
||||
showChat?: boolean;
|
||||
chatStarted?: boolean;
|
||||
isStreaming?: boolean;
|
||||
messages?: Message[];
|
||||
@ -40,6 +43,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
textareaRef,
|
||||
messageRef,
|
||||
scrollRef,
|
||||
showChat = true,
|
||||
chatStarted = false,
|
||||
isStreaming = false,
|
||||
enhancingPrompt = false,
|
||||
@ -56,12 +60,19 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||||
|
||||
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>
|
||||
<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 && (
|
||||
<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">
|
||||
Where ideas begin
|
||||
</h1>
|
||||
@ -71,7 +82,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames('pt-6', {
|
||||
className={classNames('pt-6 px-6', {
|
||||
'h-full flex flex-col': chatStarted,
|
||||
})}
|
||||
>
|
||||
@ -80,7 +91,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
return chatStarted ? (
|
||||
<Messages
|
||||
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}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
@ -88,7 +99,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
}}
|
||||
</ClientOnly>
|
||||
<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,
|
||||
})}
|
||||
>
|
||||
@ -174,7 +185,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
</div>
|
||||
</div>
|
||||
{!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]">
|
||||
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {
|
||||
return (
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { Message } from 'ai';
|
||||
import { useChat } from 'ai/react';
|
||||
import { useAnimate } from 'framer-motion';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||
import { AnalyticsAction, AnalyticsTrackEvent, sendAnalyticsEvent } from '~/lib/analytics';
|
||||
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
|
||||
import { useChatHistory } from '~/lib/persistence';
|
||||
import { chatStore } from '~/lib/stores/chat';
|
||||
@ -11,7 +13,6 @@ import { fileModificationsToHTML } from '~/utils/diff';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { createScopedLogger, renderLogger } from '~/utils/logger';
|
||||
import { BaseChat } from './BaseChat';
|
||||
import { sendAnalyticsEvent, AnalyticsTrackEvent, AnalyticsAction } from '~/lib/analytics';
|
||||
|
||||
const toastAnimation = cssTransition({
|
||||
enter: 'animated fadeInRight',
|
||||
@ -71,6 +72,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
||||
|
||||
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
||||
|
||||
const { showChat } = useStore(chatStore);
|
||||
|
||||
const [animationScope, animate] = useAnimate();
|
||||
|
||||
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
|
||||
@ -213,6 +216,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
||||
ref={animationScope}
|
||||
textareaRef={textareaRef}
|
||||
input={input}
|
||||
showChat={showChat}
|
||||
chatStarted={chatStarted}
|
||||
isStreaming={isLoading}
|
||||
enhancingPrompt={enhancingPrompt}
|
||||
|
@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { chatStore } from '~/lib/stores/chat';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
||||
import { HeaderActionButtons } from './HeaderActionButtons.client';
|
||||
|
||||
export function Header() {
|
||||
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">
|
||||
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
{chat.started && (
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<div className="mr-1">
|
||||
<HeaderActionButtons />
|
||||
</div>
|
||||
)}
|
||||
</ClientOnly>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import path from 'path';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import sdk from '@stackblitz/sdk';
|
||||
import path from 'path';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import type { FileMap } from '~/lib/stores/files';
|
||||
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
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
|
||||
const extractContent = ([file, value]: [string, FileMap[string]]) => {
|
||||
@ -47,6 +48,8 @@ const useFirstArtifact = (): [boolean, ArtifactState | undefined] => {
|
||||
export const OpenStackBlitz = memo(() => {
|
||||
const [artifactLoaded, artifact] = useFirstArtifact();
|
||||
|
||||
const disabled = !artifactLoaded;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!artifact) {
|
||||
return;
|
||||
@ -66,13 +69,34 @@ export const OpenStackBlitz = memo(() => {
|
||||
});
|
||||
}, [artifact]);
|
||||
|
||||
if (!artifactLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a onClick={handleClick} className="cursor-pointer">
|
||||
<img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" />
|
||||
</a>
|
||||
<button
|
||||
className={classNames(
|
||||
'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>
|
||||
);
|
||||
});
|
||||
|
@ -50,7 +50,7 @@ export function Menu() {
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const enterThreshold = 80;
|
||||
const enterThreshold = 40;
|
||||
const exitThreshold = 40;
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
|
@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
|
||||
return (
|
||||
<button
|
||||
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,
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { memo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { genericMemo } from '~/utils/react';
|
||||
|
||||
interface SliderOption<T> {
|
||||
@ -23,7 +24,7 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
|
||||
const isLeftSelected = selected === options.left.value;
|
||||
|
||||
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)}>
|
||||
{options.left.text}
|
||||
</SliderButton>
|
||||
@ -55,7 +56,7 @@ const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProp
|
||||
{selected && (
|
||||
<motion.span
|
||||
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"
|
||||
></motion.span>
|
||||
)}
|
||||
|
@ -17,9 +17,10 @@ import type { FileMap } from '~/lib/stores/files';
|
||||
import { themeStore } from '~/lib/stores/theme';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { WORK_DIR } from '~/utils/constants';
|
||||
import { renderLogger } from '~/utils/logger';
|
||||
import { isMobile } from '~/utils/mobile';
|
||||
import { FileTreePanel } from './FileTreePanel';
|
||||
import { FileTree } from './FileTree';
|
||||
import { Terminal, type TerminalRef } from './terminal/Terminal';
|
||||
|
||||
interface EditorPanelProps {
|
||||
@ -124,22 +125,24 @@ export const EditorPanel = memo(
|
||||
<PanelGroup direction="vertical">
|
||||
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
||||
<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">
|
||||
<PanelHeader>
|
||||
<div className="i-ph:tree-structure-duotone shrink-0" />
|
||||
Files
|
||||
</PanelHeader>
|
||||
<FileTreePanel
|
||||
<FileTree
|
||||
className="h-full"
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
rootFolder={WORK_DIR}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={onFileSelect}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel className="flex flex-col" defaultSize={75} minSize={20}>
|
||||
<Panel className="flex flex-col" defaultSize={80} minSize={20}>
|
||||
<PanelHeader>
|
||||
{activeFile && (
|
||||
<div className="flex items-center flex-1 text-sm">
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -82,11 +82,11 @@ export const Preview = memo(() => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 bg-white border-t">
|
||||
<div className="flex-1 border-t border-bolt-elements-borderColor">
|
||||
{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>
|
||||
|
@ -11,6 +11,7 @@ import { IconButton } from '~/components/ui/IconButton';
|
||||
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
||||
import { Slider, type SliderOptions } from '~/components/ui/Slider';
|
||||
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { renderLogger } from '~/utils/logger';
|
||||
import { EditorPanel } from './EditorPanel';
|
||||
@ -43,7 +44,7 @@ const workbenchVariants = {
|
||||
},
|
||||
},
|
||||
open: {
|
||||
width: '100%',
|
||||
width: 'var(--workbench-width)',
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
ease: cubicEasingFn,
|
||||
@ -100,53 +101,71 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
||||
|
||||
return (
|
||||
chatStarted && (
|
||||
<motion.div initial="closed" animate={showWorkbench ? 'open' : 'closed'} variants={workbenchVariants}>
|
||||
<div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0">
|
||||
<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">
|
||||
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<PanelHeaderButton
|
||||
className="ml-auto mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
className="-mr-1"
|
||||
size="xl"
|
||||
onClick={() => {
|
||||
workbenchStore.showWorkbench.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View
|
||||
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
>
|
||||
<EditorPanel
|
||||
editorDocument={currentDocument}
|
||||
isStreaming={isStreaming}
|
||||
selectedFile={selectedFile}
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
onFileSelect={onFileSelect}
|
||||
onEditorScroll={onEditorScroll}
|
||||
onEditorChange={onEditorChange}
|
||||
onFileSave={onFileSave}
|
||||
onFileReset={onFileReset}
|
||||
<motion.div
|
||||
initial="closed"
|
||||
animate={showWorkbench ? 'open' : 'closed'}
|
||||
variants={workbenchVariants}
|
||||
className="z-workbench"
|
||||
>
|
||||
<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',
|
||||
{
|
||||
'left-[var(--workbench-left)]': showWorkbench,
|
||||
'left-[100%]': !showWorkbench,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 px-6">
|
||||
<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">
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<div className="ml-auto" />
|
||||
{selectedView === 'code' && (
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
)}
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
className="-mr-1"
|
||||
size="xl"
|
||||
onClick={() => {
|
||||
workbenchStore.showWorkbench.set(false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
>
|
||||
<Preview />
|
||||
</View>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View
|
||||
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
>
|
||||
<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>
|
||||
|
@ -3,4 +3,5 @@ import { map } from 'nanostores';
|
||||
export const chatStore = map({
|
||||
started: false,
|
||||
aborted: false,
|
||||
showChat: true,
|
||||
});
|
||||
|
@ -161,8 +161,8 @@
|
||||
--bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
|
||||
--bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
|
||||
|
||||
--bolt-elements-cta-background: theme('colors.gray.100');
|
||||
--bolt-elements-cta-text: theme('colors.gray.950');
|
||||
--bolt-elements-cta-background: theme('colors.alpha.white.10');
|
||||
--bolt-elements-cta-text: theme('colors.white');
|
||||
|
||||
/* Terminal Colors */
|
||||
--bolt-terminal-background: var(--bolt-elements-terminals-background);
|
||||
@ -193,6 +193,11 @@
|
||||
*/
|
||||
:root {
|
||||
--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 */
|
||||
--toastify-color-progress-success: var(--bolt-elements-icon-success);
|
||||
|
@ -8,6 +8,14 @@ $zIndexMax: 999;
|
||||
z-index: $zIndexMax - 2;
|
||||
}
|
||||
|
||||
.z-prompt {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.z-workbench {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.z-max {
|
||||
z-index: $zIndexMax;
|
||||
}
|
||||
|
@ -99,9 +99,10 @@ const COLOR_PRIMITIVES = {
|
||||
|
||||
export default defineConfig({
|
||||
shortcuts: {
|
||||
'transition-theme':
|
||||
'transition-[background-color,border-color,color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]',
|
||||
'bolt-ease-cubic-bezier': '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',
|
||||
'max-w-chat': 'max-w-[var(--chat-max-width)]',
|
||||
},
|
||||
theme: {
|
||||
colors: {
|
||||
|
Loading…
Reference in New Issue
Block a user