Dominic Elm
commited on
feat(layout): allow to minimize chat (#35)
Browse files- packages/bolt/README.md +2 -1
- packages/bolt/app/components/chat/BaseChat.module.scss +19 -0
- packages/bolt/app/components/chat/BaseChat.tsx +18 -7
- packages/bolt/app/components/chat/Chat.client.tsx +5 -1
- packages/bolt/app/components/header/Header.tsx +13 -5
- packages/bolt/app/components/header/HeaderActionButtons.client.tsx +72 -0
- packages/bolt/app/components/header/OpenStackBlitz.client.tsx +33 -9
- packages/bolt/app/components/sidebar/Menu.client.tsx +1 -1
- packages/bolt/app/components/ui/PanelHeaderButton.tsx +1 -1
- packages/bolt/app/components/ui/Slider.tsx +3 -2
- packages/bolt/app/components/workbench/EditorPanel.tsx +7 -4
- packages/bolt/app/components/workbench/FileTreePanel.tsx +0 -29
- packages/bolt/app/components/workbench/Preview.tsx +3 -3
- packages/bolt/app/components/workbench/Workbench.client.tsx +66 -47
- packages/bolt/app/lib/stores/chat.ts +1 -0
- packages/bolt/app/styles/variables.scss +7 -2
- packages/bolt/app/styles/z-index.scss +8 -0
- packages/bolt/uno.config.ts +3 -2
packages/bolt/README.md
CHANGED
@@ -30,10 +30,11 @@ pnpm install
|
|
30 |
ANTHROPIC_API_KEY=XXX
|
31 |
```
|
32 |
|
33 |
-
Optionally, you an set the debug level:
|
34 |
|
35 |
```
|
36 |
VITE_LOG_LEVEL=debug
|
|
|
37 |
```
|
38 |
|
39 |
If you want to run authentication against a local StackBlitz instance, add:
|
|
|
30 |
ANTHROPIC_API_KEY=XXX
|
31 |
```
|
32 |
|
33 |
+
Optionally, you an set the debug level or disable authentication:
|
34 |
|
35 |
```
|
36 |
VITE_LOG_LEVEL=debug
|
37 |
+
VITE_DISABLE_AUTH=1
|
38 |
```
|
39 |
|
40 |
If you want to run authentication against a local StackBlitz instance, add:
|
packages/bolt/app/components/chat/BaseChat.module.scss
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.BaseChat {
|
2 |
+
&[data-chat-visible='false'] {
|
3 |
+
--workbench-inner-width: 100%;
|
4 |
+
--workbench-left: 0;
|
5 |
+
|
6 |
+
.Chat {
|
7 |
+
--at-apply: bolt-ease-cubic-bezier;
|
8 |
+
transition-property: transform, opacity;
|
9 |
+
transition-duration: 0.3s;
|
10 |
+
will-change: transform, opacity;
|
11 |
+
transform: translateX(-50%);
|
12 |
+
opacity: 0;
|
13 |
+
}
|
14 |
+
}
|
15 |
+
}
|
16 |
+
|
17 |
+
.Chat {
|
18 |
+
opacity: 1;
|
19 |
+
}
|
packages/bolt/app/components/chat/BaseChat.tsx
CHANGED
@@ -8,10 +8,13 @@ import { classNames } from '~/utils/classNames';
|
|
8 |
import { Messages } from './Messages.client';
|
9 |
import { SendButton } from './SendButton.client';
|
10 |
|
|
|
|
|
11 |
interface BaseChatProps {
|
12 |
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
13 |
messageRef?: RefCallback<HTMLDivElement> | undefined;
|
14 |
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
|
|
15 |
chatStarted?: boolean;
|
16 |
isStreaming?: boolean;
|
17 |
messages?: Message[];
|
@@ -40,6 +43,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
40 |
textareaRef,
|
41 |
messageRef,
|
42 |
scrollRef,
|
|
|
43 |
chatStarted = false,
|
44 |
isStreaming = false,
|
45 |
enhancingPrompt = false,
|
@@ -56,12 +60,19 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
56 |
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
57 |
|
58 |
return (
|
59 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
<ClientOnly>{() => <Menu />}</ClientOnly>
|
61 |
<div ref={scrollRef} className="flex overflow-scroll w-full h-full">
|
62 |
-
<div className=
|
63 |
{!chatStarted && (
|
64 |
-
<div id="intro" className="mt-[26vh] max-w-
|
65 |
<h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
|
66 |
Where ideas begin
|
67 |
</h1>
|
@@ -71,7 +82,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
71 |
</div>
|
72 |
)}
|
73 |
<div
|
74 |
-
className={classNames('pt-6', {
|
75 |
'h-full flex flex-col': chatStarted,
|
76 |
})}
|
77 |
>
|
@@ -80,7 +91,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
80 |
return chatStarted ? (
|
81 |
<Messages
|
82 |
ref={messageRef}
|
83 |
-
className="flex flex-col w-full flex-1 max-w-
|
84 |
messages={messages}
|
85 |
isStreaming={isStreaming}
|
86 |
/>
|
@@ -88,7 +99,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
88 |
}}
|
89 |
</ClientOnly>
|
90 |
<div
|
91 |
-
className={classNames('relative w-full max-w-
|
92 |
'sticky bottom-0': chatStarted,
|
93 |
})}
|
94 |
>
|
@@ -174,7 +185,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
174 |
</div>
|
175 |
</div>
|
176 |
{!chatStarted && (
|
177 |
-
<div id="examples" className="relative w-full max-w-
|
178 |
<div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
|
179 |
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {
|
180 |
return (
|
|
|
8 |
import { Messages } from './Messages.client';
|
9 |
import { SendButton } from './SendButton.client';
|
10 |
|
11 |
+
import styles from './BaseChat.module.scss';
|
12 |
+
|
13 |
interface BaseChatProps {
|
14 |
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
15 |
messageRef?: RefCallback<HTMLDivElement> | undefined;
|
16 |
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
17 |
+
showChat?: boolean;
|
18 |
chatStarted?: boolean;
|
19 |
isStreaming?: boolean;
|
20 |
messages?: Message[];
|
|
|
43 |
textareaRef,
|
44 |
messageRef,
|
45 |
scrollRef,
|
46 |
+
showChat = true,
|
47 |
chatStarted = false,
|
48 |
isStreaming = false,
|
49 |
enhancingPrompt = false,
|
|
|
60 |
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
61 |
|
62 |
return (
|
63 |
+
<div
|
64 |
+
ref={ref}
|
65 |
+
className={classNames(
|
66 |
+
styles.BaseChat,
|
67 |
+
'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
|
68 |
+
)}
|
69 |
+
data-chat-visible={showChat}
|
70 |
+
>
|
71 |
<ClientOnly>{() => <Menu />}</ClientOnly>
|
72 |
<div ref={scrollRef} className="flex overflow-scroll w-full h-full">
|
73 |
+
<div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
|
74 |
{!chatStarted && (
|
75 |
+
<div id="intro" className="mt-[26vh] max-w-chat mx-auto">
|
76 |
<h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
|
77 |
Where ideas begin
|
78 |
</h1>
|
|
|
82 |
</div>
|
83 |
)}
|
84 |
<div
|
85 |
+
className={classNames('pt-6 px-6', {
|
86 |
'h-full flex flex-col': chatStarted,
|
87 |
})}
|
88 |
>
|
|
|
91 |
return chatStarted ? (
|
92 |
<Messages
|
93 |
ref={messageRef}
|
94 |
+
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
|
95 |
messages={messages}
|
96 |
isStreaming={isStreaming}
|
97 |
/>
|
|
|
99 |
}}
|
100 |
</ClientOnly>
|
101 |
<div
|
102 |
+
className={classNames('relative w-full max-w-chat mx-auto z-prompt', {
|
103 |
'sticky bottom-0': chatStarted,
|
104 |
})}
|
105 |
>
|
|
|
185 |
</div>
|
186 |
</div>
|
187 |
{!chatStarted && (
|
188 |
+
<div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
|
189 |
<div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
|
190 |
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {
|
191 |
return (
|
packages/bolt/app/components/chat/Chat.client.tsx
CHANGED
@@ -1,8 +1,10 @@
|
|
|
|
1 |
import type { Message } from 'ai';
|
2 |
import { useChat } from 'ai/react';
|
3 |
import { useAnimate } from 'framer-motion';
|
4 |
import { memo, useEffect, useRef, useState } from 'react';
|
5 |
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
|
|
6 |
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
|
7 |
import { useChatHistory } from '~/lib/persistence';
|
8 |
import { chatStore } from '~/lib/stores/chat';
|
@@ -11,7 +13,6 @@ import { fileModificationsToHTML } from '~/utils/diff';
|
|
11 |
import { cubicEasingFn } from '~/utils/easings';
|
12 |
import { createScopedLogger, renderLogger } from '~/utils/logger';
|
13 |
import { BaseChat } from './BaseChat';
|
14 |
-
import { sendAnalyticsEvent, AnalyticsTrackEvent, AnalyticsAction } from '~/lib/analytics';
|
15 |
|
16 |
const toastAnimation = cssTransition({
|
17 |
enter: 'animated fadeInRight',
|
@@ -71,6 +72,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
|
71 |
|
72 |
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
73 |
|
|
|
|
|
74 |
const [animationScope, animate] = useAnimate();
|
75 |
|
76 |
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
|
@@ -213,6 +216,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
|
213 |
ref={animationScope}
|
214 |
textareaRef={textareaRef}
|
215 |
input={input}
|
|
|
216 |
chatStarted={chatStarted}
|
217 |
isStreaming={isLoading}
|
218 |
enhancingPrompt={enhancingPrompt}
|
|
|
1 |
+
import { useStore } from '@nanostores/react';
|
2 |
import type { Message } from 'ai';
|
3 |
import { useChat } from 'ai/react';
|
4 |
import { useAnimate } from 'framer-motion';
|
5 |
import { memo, useEffect, useRef, useState } from 'react';
|
6 |
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
7 |
+
import { AnalyticsAction, AnalyticsTrackEvent, sendAnalyticsEvent } from '~/lib/analytics';
|
8 |
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
|
9 |
import { useChatHistory } from '~/lib/persistence';
|
10 |
import { chatStore } from '~/lib/stores/chat';
|
|
|
13 |
import { cubicEasingFn } from '~/utils/easings';
|
14 |
import { createScopedLogger, renderLogger } from '~/utils/logger';
|
15 |
import { BaseChat } from './BaseChat';
|
|
|
16 |
|
17 |
const toastAnimation = cssTransition({
|
18 |
enter: 'animated fadeInRight',
|
|
|
72 |
|
73 |
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
74 |
|
75 |
+
const { showChat } = useStore(chatStore);
|
76 |
+
|
77 |
const [animationScope, animate] = useAnimate();
|
78 |
|
79 |
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
|
|
|
216 |
ref={animationScope}
|
217 |
textareaRef={textareaRef}
|
218 |
input={input}
|
219 |
+
showChat={showChat}
|
220 |
chatStarted={chatStarted}
|
221 |
isStreaming={isLoading}
|
222 |
enhancingPrompt={enhancingPrompt}
|
packages/bolt/app/components/header/Header.tsx
CHANGED
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
|
|
2 |
import { ClientOnly } from 'remix-utils/client-only';
|
3 |
import { chatStore } from '~/lib/stores/chat';
|
4 |
import { classNames } from '~/utils/classNames';
|
5 |
-
import {
|
6 |
|
7 |
export function Header() {
|
8 |
const chat = useStore(chatStore);
|
@@ -17,14 +17,22 @@ export function Header() {
|
|
17 |
},
|
18 |
)}
|
19 |
>
|
20 |
-
<div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary">
|
|
|
21 |
<a href="/" className="text-2xl font-semibold text-accent flex items-center">
|
22 |
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
23 |
</a>
|
24 |
</div>
|
25 |
-
<div className="
|
26 |
-
|
27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
</header>
|
29 |
);
|
30 |
}
|
|
|
2 |
import { ClientOnly } from 'remix-utils/client-only';
|
3 |
import { chatStore } from '~/lib/stores/chat';
|
4 |
import { classNames } from '~/utils/classNames';
|
5 |
+
import { HeaderActionButtons } from './HeaderActionButtons.client';
|
6 |
|
7 |
export function Header() {
|
8 |
const chat = useStore(chatStore);
|
|
|
17 |
},
|
18 |
)}
|
19 |
>
|
20 |
+
<div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
|
21 |
+
<div className="i-ph:sidebar-simple-duotone text-xl" />
|
22 |
<a href="/" className="text-2xl font-semibold text-accent flex items-center">
|
23 |
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
24 |
</a>
|
25 |
</div>
|
26 |
+
<div className="flex-1" />
|
27 |
+
{chat.started && (
|
28 |
+
<ClientOnly>
|
29 |
+
{() => (
|
30 |
+
<div className="mr-1">
|
31 |
+
<HeaderActionButtons />
|
32 |
+
</div>
|
33 |
+
)}
|
34 |
+
</ClientOnly>
|
35 |
+
)}
|
36 |
</header>
|
37 |
);
|
38 |
}
|
packages/bolt/app/components/header/HeaderActionButtons.client.tsx
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useStore } from '@nanostores/react';
|
2 |
+
import { chatStore } from '~/lib/stores/chat';
|
3 |
+
import { workbenchStore } from '~/lib/stores/workbench';
|
4 |
+
import { classNames } from '~/utils/classNames';
|
5 |
+
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
6 |
+
|
7 |
+
interface HeaderActionButtonsProps {}
|
8 |
+
|
9 |
+
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
10 |
+
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
11 |
+
const { showChat } = useStore(chatStore);
|
12 |
+
|
13 |
+
const canHideChat = showWorkbench || !showChat;
|
14 |
+
|
15 |
+
return (
|
16 |
+
<div className="flex">
|
17 |
+
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
18 |
+
<Button
|
19 |
+
active={showChat}
|
20 |
+
disabled={!canHideChat}
|
21 |
+
onClick={() => {
|
22 |
+
if (canHideChat) {
|
23 |
+
chatStore.setKey('showChat', !showChat);
|
24 |
+
}
|
25 |
+
}}
|
26 |
+
>
|
27 |
+
<div className="i-bolt:chat text-sm" />
|
28 |
+
</Button>
|
29 |
+
<div className="w-[1px] bg-bolt-elements-borderColor" />
|
30 |
+
<Button
|
31 |
+
active={showWorkbench}
|
32 |
+
onClick={() => {
|
33 |
+
if (showWorkbench && !showChat) {
|
34 |
+
chatStore.setKey('showChat', true);
|
35 |
+
}
|
36 |
+
|
37 |
+
workbenchStore.showWorkbench.set(!showWorkbench);
|
38 |
+
}}
|
39 |
+
>
|
40 |
+
<div className="i-ph:code-bold" />
|
41 |
+
</Button>
|
42 |
+
</div>
|
43 |
+
<div className="flex ml-2">
|
44 |
+
<OpenStackBlitz />
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
);
|
48 |
+
}
|
49 |
+
|
50 |
+
interface ButtonProps {
|
51 |
+
active?: boolean;
|
52 |
+
disabled?: boolean;
|
53 |
+
children?: any;
|
54 |
+
onClick?: VoidFunction;
|
55 |
+
}
|
56 |
+
|
57 |
+
function Button({ active = false, disabled = false, children, onClick }: ButtonProps) {
|
58 |
+
return (
|
59 |
+
<button
|
60 |
+
className={classNames('flex items-center p-1.5', {
|
61 |
+
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
62 |
+
!active,
|
63 |
+
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
64 |
+
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
65 |
+
disabled,
|
66 |
+
})}
|
67 |
+
onClick={onClick}
|
68 |
+
>
|
69 |
+
{children}
|
70 |
+
</button>
|
71 |
+
);
|
72 |
+
}
|
packages/bolt/app/components/header/OpenStackBlitz.client.tsx
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
-
import path from 'path';
|
2 |
import { useStore } from '@nanostores/react';
|
3 |
import sdk from '@stackblitz/sdk';
|
|
|
|
|
4 |
import type { FileMap } from '~/lib/stores/files';
|
5 |
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
|
|
|
6 |
import { WORK_DIR } from '~/utils/constants';
|
7 |
-
import { memo, useCallback, useEffect, useState } from 'react';
|
8 |
|
9 |
// extract relative path and content from file, wrapped in array for flatMap use
|
10 |
const extractContent = ([file, value]: [string, FileMap[string]]) => {
|
@@ -47,6 +48,8 @@ const useFirstArtifact = (): [boolean, ArtifactState | undefined] => {
|
|
47 |
export const OpenStackBlitz = memo(() => {
|
48 |
const [artifactLoaded, artifact] = useFirstArtifact();
|
49 |
|
|
|
|
|
50 |
const handleClick = useCallback(() => {
|
51 |
if (!artifact) {
|
52 |
return;
|
@@ -66,13 +69,34 @@ export const OpenStackBlitz = memo(() => {
|
|
66 |
});
|
67 |
}, [artifact]);
|
68 |
|
69 |
-
if (!artifactLoaded) {
|
70 |
-
return null;
|
71 |
-
}
|
72 |
-
|
73 |
return (
|
74 |
-
<
|
75 |
-
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
);
|
78 |
});
|
|
|
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
import sdk from '@stackblitz/sdk';
|
3 |
+
import path from 'path';
|
4 |
+
import { memo, useCallback, useEffect, useState } from 'react';
|
5 |
import type { FileMap } from '~/lib/stores/files';
|
6 |
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
|
7 |
+
import { classNames } from '~/utils/classNames';
|
8 |
import { WORK_DIR } from '~/utils/constants';
|
|
|
9 |
|
10 |
// extract relative path and content from file, wrapped in array for flatMap use
|
11 |
const extractContent = ([file, value]: [string, FileMap[string]]) => {
|
|
|
48 |
export const OpenStackBlitz = memo(() => {
|
49 |
const [artifactLoaded, artifact] = useFirstArtifact();
|
50 |
|
51 |
+
const disabled = !artifactLoaded;
|
52 |
+
|
53 |
const handleClick = useCallback(() => {
|
54 |
if (!artifact) {
|
55 |
return;
|
|
|
69 |
});
|
70 |
}, [artifact]);
|
71 |
|
|
|
|
|
|
|
|
|
72 |
return (
|
73 |
+
<button
|
74 |
+
className={classNames(
|
75 |
+
'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',
|
76 |
+
{
|
77 |
+
'cursor-not-allowed opacity-50': disabled,
|
78 |
+
'group hover:bg-gradient-to-t from-accent-900 to-accent-500 hover:text-white': !disabled,
|
79 |
+
},
|
80 |
+
)}
|
81 |
+
onClick={handleClick}
|
82 |
+
disabled={disabled}
|
83 |
+
>
|
84 |
+
<div
|
85 |
+
className={classNames(
|
86 |
+
'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',
|
87 |
+
{
|
88 |
+
'opacity-50': disabled,
|
89 |
+
},
|
90 |
+
)}
|
91 |
+
>
|
92 |
+
<svg width="11" height="16">
|
93 |
+
<path
|
94 |
+
fill="currentColor"
|
95 |
+
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"
|
96 |
+
/>
|
97 |
+
</svg>
|
98 |
+
<span>Open in StackBlitz</span>
|
99 |
+
</div>
|
100 |
+
</button>
|
101 |
);
|
102 |
});
|
packages/bolt/app/components/sidebar/Menu.client.tsx
CHANGED
@@ -50,7 +50,7 @@ export function Menu() {
|
|
50 |
}, [open]);
|
51 |
|
52 |
useEffect(() => {
|
53 |
-
const enterThreshold =
|
54 |
const exitThreshold = 40;
|
55 |
|
56 |
function onMouseMove(event: MouseEvent) {
|
|
|
50 |
}, [open]);
|
51 |
|
52 |
useEffect(() => {
|
53 |
+
const enterThreshold = 40;
|
54 |
const exitThreshold = 40;
|
55 |
|
56 |
function onMouseMove(event: MouseEvent) {
|
packages/bolt/app/components/ui/PanelHeaderButton.tsx
CHANGED
@@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
|
|
14 |
return (
|
15 |
<button
|
16 |
className={classNames(
|
17 |
-
'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',
|
18 |
{
|
19 |
[classNames('opacity-30', disabledClassName)]: disabled,
|
20 |
},
|
|
|
14 |
return (
|
15 |
<button
|
16 |
className={classNames(
|
17 |
+
'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',
|
18 |
{
|
19 |
[classNames('opacity-30', disabledClassName)]: disabled,
|
20 |
},
|
packages/bolt/app/components/ui/Slider.tsx
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import { motion } from 'framer-motion';
|
2 |
import { memo } from 'react';
|
3 |
import { classNames } from '~/utils/classNames';
|
|
|
4 |
import { genericMemo } from '~/utils/react';
|
5 |
|
6 |
interface SliderOption<T> {
|
@@ -23,7 +24,7 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
|
|
23 |
const isLeftSelected = selected === options.left.value;
|
24 |
|
25 |
return (
|
26 |
-
<div className="flex items-center flex-wrap gap-1 bg-bolt-elements-background-depth-1 rounded-full p-1">
|
27 |
<SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
|
28 |
{options.left.text}
|
29 |
</SliderButton>
|
@@ -55,7 +56,7 @@ const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProp
|
|
55 |
{selected && (
|
56 |
<motion.span
|
57 |
layoutId="pill-tab"
|
58 |
-
transition={{
|
59 |
className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
|
60 |
></motion.span>
|
61 |
)}
|
|
|
1 |
import { motion } from 'framer-motion';
|
2 |
import { memo } from 'react';
|
3 |
import { classNames } from '~/utils/classNames';
|
4 |
+
import { cubicEasingFn } from '~/utils/easings';
|
5 |
import { genericMemo } from '~/utils/react';
|
6 |
|
7 |
interface SliderOption<T> {
|
|
|
24 |
const isLeftSelected = selected === options.left.value;
|
25 |
|
26 |
return (
|
27 |
+
<div className="flex items-center flex-wrap shrink-0 gap-1 bg-bolt-elements-background-depth-1 overflow-hidden rounded-full p-1">
|
28 |
<SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
|
29 |
{options.left.text}
|
30 |
</SliderButton>
|
|
|
56 |
{selected && (
|
57 |
<motion.span
|
58 |
layoutId="pill-tab"
|
59 |
+
transition={{ duration: 0.2, ease: cubicEasingFn }}
|
60 |
className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
|
61 |
></motion.span>
|
62 |
)}
|
packages/bolt/app/components/workbench/EditorPanel.tsx
CHANGED
@@ -17,9 +17,10 @@ import type { FileMap } from '~/lib/stores/files';
|
|
17 |
import { themeStore } from '~/lib/stores/theme';
|
18 |
import { workbenchStore } from '~/lib/stores/workbench';
|
19 |
import { classNames } from '~/utils/classNames';
|
|
|
20 |
import { renderLogger } from '~/utils/logger';
|
21 |
import { isMobile } from '~/utils/mobile';
|
22 |
-
import {
|
23 |
import { Terminal, type TerminalRef } from './terminal/Terminal';
|
24 |
|
25 |
interface EditorPanelProps {
|
@@ -124,22 +125,24 @@ export const EditorPanel = memo(
|
|
124 |
<PanelGroup direction="vertical">
|
125 |
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
126 |
<PanelGroup direction="horizontal">
|
127 |
-
<Panel defaultSize={
|
128 |
<div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
|
129 |
<PanelHeader>
|
130 |
<div className="i-ph:tree-structure-duotone shrink-0" />
|
131 |
Files
|
132 |
</PanelHeader>
|
133 |
-
<
|
|
|
134 |
files={files}
|
135 |
unsavedFiles={unsavedFiles}
|
|
|
136 |
selectedFile={selectedFile}
|
137 |
onFileSelect={onFileSelect}
|
138 |
/>
|
139 |
</div>
|
140 |
</Panel>
|
141 |
<PanelResizeHandle />
|
142 |
-
<Panel className="flex flex-col" defaultSize={
|
143 |
<PanelHeader>
|
144 |
{activeFile && (
|
145 |
<div className="flex items-center flex-1 text-sm">
|
|
|
17 |
import { themeStore } from '~/lib/stores/theme';
|
18 |
import { workbenchStore } from '~/lib/stores/workbench';
|
19 |
import { classNames } from '~/utils/classNames';
|
20 |
+
import { WORK_DIR } from '~/utils/constants';
|
21 |
import { renderLogger } from '~/utils/logger';
|
22 |
import { isMobile } from '~/utils/mobile';
|
23 |
+
import { FileTree } from './FileTree';
|
24 |
import { Terminal, type TerminalRef } from './terminal/Terminal';
|
25 |
|
26 |
interface EditorPanelProps {
|
|
|
125 |
<PanelGroup direction="vertical">
|
126 |
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
127 |
<PanelGroup direction="horizontal">
|
128 |
+
<Panel defaultSize={20} minSize={10} collapsible>
|
129 |
<div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
|
130 |
<PanelHeader>
|
131 |
<div className="i-ph:tree-structure-duotone shrink-0" />
|
132 |
Files
|
133 |
</PanelHeader>
|
134 |
+
<FileTree
|
135 |
+
className="h-full"
|
136 |
files={files}
|
137 |
unsavedFiles={unsavedFiles}
|
138 |
+
rootFolder={WORK_DIR}
|
139 |
selectedFile={selectedFile}
|
140 |
onFileSelect={onFileSelect}
|
141 |
/>
|
142 |
</div>
|
143 |
</Panel>
|
144 |
<PanelResizeHandle />
|
145 |
+
<Panel className="flex flex-col" defaultSize={80} minSize={20}>
|
146 |
<PanelHeader>
|
147 |
{activeFile && (
|
148 |
<div className="flex items-center flex-1 text-sm">
|
packages/bolt/app/components/workbench/FileTreePanel.tsx
DELETED
@@ -1,29 +0,0 @@
|
|
1 |
-
import { memo } from 'react';
|
2 |
-
import type { FileMap } from '~/lib/stores/files';
|
3 |
-
import { WORK_DIR } from '~/utils/constants';
|
4 |
-
import { renderLogger } from '~/utils/logger';
|
5 |
-
import { FileTree } from './FileTree';
|
6 |
-
|
7 |
-
interface FileTreePanelProps {
|
8 |
-
files?: FileMap;
|
9 |
-
selectedFile?: string;
|
10 |
-
unsavedFiles?: Set<string>;
|
11 |
-
onFileSelect?: (value?: string) => void;
|
12 |
-
}
|
13 |
-
|
14 |
-
export const FileTreePanel = memo(({ files, unsavedFiles, selectedFile, onFileSelect }: FileTreePanelProps) => {
|
15 |
-
renderLogger.trace('FileTreePanel');
|
16 |
-
|
17 |
-
return (
|
18 |
-
<div className="flex-1 overflow-y-scroll">
|
19 |
-
<FileTree
|
20 |
-
className="h-full"
|
21 |
-
files={files}
|
22 |
-
unsavedFiles={unsavedFiles}
|
23 |
-
rootFolder={WORK_DIR}
|
24 |
-
selectedFile={selectedFile}
|
25 |
-
onFileSelect={onFileSelect}
|
26 |
-
/>
|
27 |
-
</div>
|
28 |
-
);
|
29 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages/bolt/app/components/workbench/Preview.tsx
CHANGED
@@ -82,11 +82,11 @@ export const Preview = memo(() => {
|
|
82 |
/>
|
83 |
</div>
|
84 |
</div>
|
85 |
-
<div className="flex-1
|
86 |
{activePreview ? (
|
87 |
-
<iframe ref={iframeRef} className="border-none w-full h-full" src={iframeUrl} />
|
88 |
) : (
|
89 |
-
<div className="flex w-full h-full justify-center items-center">No preview available</div>
|
90 |
)}
|
91 |
</div>
|
92 |
</div>
|
|
|
82 |
/>
|
83 |
</div>
|
84 |
</div>
|
85 |
+
<div className="flex-1 border-t border-bolt-elements-borderColor">
|
86 |
{activePreview ? (
|
87 |
+
<iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} />
|
88 |
) : (
|
89 |
+
<div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
|
90 |
)}
|
91 |
</div>
|
92 |
</div>
|
packages/bolt/app/components/workbench/Workbench.client.tsx
CHANGED
@@ -11,6 +11,7 @@ import { IconButton } from '~/components/ui/IconButton';
|
|
11 |
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
12 |
import { Slider, type SliderOptions } from '~/components/ui/Slider';
|
13 |
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
|
|
|
14 |
import { cubicEasingFn } from '~/utils/easings';
|
15 |
import { renderLogger } from '~/utils/logger';
|
16 |
import { EditorPanel } from './EditorPanel';
|
@@ -43,7 +44,7 @@ const workbenchVariants = {
|
|
43 |
},
|
44 |
},
|
45 |
open: {
|
46 |
-
width: '
|
47 |
transition: {
|
48 |
duration: 0.2,
|
49 |
ease: cubicEasingFn,
|
@@ -100,53 +101,71 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|
100 |
|
101 |
return (
|
102 |
chatStarted && (
|
103 |
-
<motion.div
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
onFileReset={onFileReset}
|
142 |
/>
|
143 |
-
</
|
144 |
-
<
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
</div>
|
151 |
</div>
|
152 |
</div>
|
|
|
11 |
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
12 |
import { Slider, type SliderOptions } from '~/components/ui/Slider';
|
13 |
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
|
14 |
+
import { classNames } from '~/utils/classNames';
|
15 |
import { cubicEasingFn } from '~/utils/easings';
|
16 |
import { renderLogger } from '~/utils/logger';
|
17 |
import { EditorPanel } from './EditorPanel';
|
|
|
44 |
},
|
45 |
},
|
46 |
open: {
|
47 |
+
width: 'var(--workbench-width)',
|
48 |
transition: {
|
49 |
duration: 0.2,
|
50 |
ease: cubicEasingFn,
|
|
|
101 |
|
102 |
return (
|
103 |
chatStarted && (
|
104 |
+
<motion.div
|
105 |
+
initial="closed"
|
106 |
+
animate={showWorkbench ? 'open' : 'closed'}
|
107 |
+
variants={workbenchVariants}
|
108 |
+
className="z-workbench"
|
109 |
+
>
|
110 |
+
<div
|
111 |
+
className={classNames(
|
112 |
+
'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',
|
113 |
+
{
|
114 |
+
'left-[var(--workbench-left)]': showWorkbench,
|
115 |
+
'left-[100%]': !showWorkbench,
|
116 |
+
},
|
117 |
+
)}
|
118 |
+
>
|
119 |
+
<div className="absolute inset-0 px-6">
|
120 |
+
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
121 |
+
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
122 |
+
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
123 |
+
<div className="ml-auto" />
|
124 |
+
{selectedView === 'code' && (
|
125 |
+
<PanelHeaderButton
|
126 |
+
className="mr-1 text-sm"
|
127 |
+
onClick={() => {
|
128 |
+
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
129 |
+
}}
|
130 |
+
>
|
131 |
+
<div className="i-ph:terminal" />
|
132 |
+
Toggle Terminal
|
133 |
+
</PanelHeaderButton>
|
134 |
+
)}
|
135 |
+
<IconButton
|
136 |
+
icon="i-ph:x-circle"
|
137 |
+
className="-mr-1"
|
138 |
+
size="xl"
|
139 |
+
onClick={() => {
|
140 |
+
workbenchStore.showWorkbench.set(false);
|
141 |
+
}}
|
|
|
142 |
/>
|
143 |
+
</div>
|
144 |
+
<div className="relative flex-1 overflow-hidden">
|
145 |
+
<View
|
146 |
+
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
147 |
+
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
148 |
+
>
|
149 |
+
<EditorPanel
|
150 |
+
editorDocument={currentDocument}
|
151 |
+
isStreaming={isStreaming}
|
152 |
+
selectedFile={selectedFile}
|
153 |
+
files={files}
|
154 |
+
unsavedFiles={unsavedFiles}
|
155 |
+
onFileSelect={onFileSelect}
|
156 |
+
onEditorScroll={onEditorScroll}
|
157 |
+
onEditorChange={onEditorChange}
|
158 |
+
onFileSave={onFileSave}
|
159 |
+
onFileReset={onFileReset}
|
160 |
+
/>
|
161 |
+
</View>
|
162 |
+
<View
|
163 |
+
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
164 |
+
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
165 |
+
>
|
166 |
+
<Preview />
|
167 |
+
</View>
|
168 |
+
</div>
|
169 |
</div>
|
170 |
</div>
|
171 |
</div>
|
packages/bolt/app/lib/stores/chat.ts
CHANGED
@@ -3,4 +3,5 @@ import { map } from 'nanostores';
|
|
3 |
export const chatStore = map({
|
4 |
started: false,
|
5 |
aborted: false,
|
|
|
6 |
});
|
|
|
3 |
export const chatStore = map({
|
4 |
started: false,
|
5 |
aborted: false,
|
6 |
+
showChat: true,
|
7 |
});
|
packages/bolt/app/styles/variables.scss
CHANGED
@@ -161,8 +161,8 @@
|
|
161 |
--bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
|
162 |
--bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
|
163 |
|
164 |
-
--bolt-elements-cta-background: theme('colors.
|
165 |
-
--bolt-elements-cta-text: theme('colors.
|
166 |
|
167 |
/* Terminal Colors */
|
168 |
--bolt-terminal-background: var(--bolt-elements-terminals-background);
|
@@ -193,6 +193,11 @@
|
|
193 |
*/
|
194 |
:root {
|
195 |
--header-height: 54px;
|
|
|
|
|
|
|
|
|
|
|
196 |
|
197 |
/* Toasts */
|
198 |
--toastify-color-progress-success: var(--bolt-elements-icon-success);
|
|
|
161 |
--bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
|
162 |
--bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
|
163 |
|
164 |
+
--bolt-elements-cta-background: theme('colors.alpha.white.10');
|
165 |
+
--bolt-elements-cta-text: theme('colors.white');
|
166 |
|
167 |
/* Terminal Colors */
|
168 |
--bolt-terminal-background: var(--bolt-elements-terminals-background);
|
|
|
193 |
*/
|
194 |
:root {
|
195 |
--header-height: 54px;
|
196 |
+
--chat-max-width: 37rem;
|
197 |
+
--chat-min-width: 640px;
|
198 |
+
--workbench-width: min(calc(100% - var(--chat-min-width)), 1536px);
|
199 |
+
--workbench-inner-width: var(--workbench-width);
|
200 |
+
--workbench-left: calc(100% - var(--workbench-width));
|
201 |
|
202 |
/* Toasts */
|
203 |
--toastify-color-progress-success: var(--bolt-elements-icon-success);
|
packages/bolt/app/styles/z-index.scss
CHANGED
@@ -8,6 +8,14 @@ $zIndexMax: 999;
|
|
8 |
z-index: $zIndexMax - 2;
|
9 |
}
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
.z-max {
|
12 |
z-index: $zIndexMax;
|
13 |
}
|
|
|
8 |
z-index: $zIndexMax - 2;
|
9 |
}
|
10 |
|
11 |
+
.z-prompt {
|
12 |
+
z-index: 2;
|
13 |
+
}
|
14 |
+
|
15 |
+
.z-workbench {
|
16 |
+
z-index: 3;
|
17 |
+
}
|
18 |
+
|
19 |
.z-max {
|
20 |
z-index: $zIndexMax;
|
21 |
}
|
packages/bolt/uno.config.ts
CHANGED
@@ -99,9 +99,10 @@ const COLOR_PRIMITIVES = {
|
|
99 |
|
100 |
export default defineConfig({
|
101 |
shortcuts: {
|
102 |
-
'
|
103 |
-
|
104 |
kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
|
|
|
105 |
},
|
106 |
theme: {
|
107 |
colors: {
|
|
|
99 |
|
100 |
export default defineConfig({
|
101 |
shortcuts: {
|
102 |
+
'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
|
103 |
+
'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
|
104 |
kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
|
105 |
+
'max-w-chat': 'max-w-[var(--chat-max-width)]',
|
106 |
},
|
107 |
theme: {
|
108 |
colors: {
|