Dominic Elm commited on
Commit
4b59a79
·
unverified ·
1 Parent(s): e8447db

feat: implement light and dark theme (#30)

Browse files
Files changed (35) hide show
  1. packages/bolt/app/components/chat/Artifact.tsx +55 -46
  2. packages/bolt/app/components/chat/BaseChat.tsx +22 -20
  3. packages/bolt/app/components/chat/Chat.client.tsx +33 -5
  4. packages/bolt/app/components/chat/Markdown.module.scss +31 -30
  5. packages/bolt/app/components/chat/Messages.client.tsx +18 -26
  6. packages/bolt/app/components/chat/SendButton.client.tsx +1 -1
  7. packages/bolt/app/components/chat/UserMessage.tsx +1 -1
  8. packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx +1 -0
  9. packages/bolt/app/components/editor/codemirror/cm-theme.ts +26 -14
  10. packages/bolt/app/components/editor/codemirror/themes/vscode-dark.ts +0 -76
  11. packages/bolt/app/components/header/Header.tsx +10 -7
  12. packages/bolt/app/components/sidebar/HistoryItem.tsx +5 -5
  13. packages/bolt/app/components/sidebar/Menu.client.tsx +21 -16
  14. packages/bolt/app/components/ui/IconButton.tsx +4 -1
  15. packages/bolt/app/components/ui/PanelHeader.tsx +6 -1
  16. packages/bolt/app/components/ui/PanelHeaderButton.tsx +1 -1
  17. packages/bolt/app/components/ui/Slider.tsx +6 -4
  18. packages/bolt/app/components/ui/ThemeSwitch.tsx +29 -0
  19. packages/bolt/app/components/workbench/EditorPanel.tsx +46 -37
  20. packages/bolt/app/components/workbench/FileTree.tsx +17 -10
  21. packages/bolt/app/components/workbench/Preview.tsx +5 -5
  22. packages/bolt/app/components/workbench/Workbench.client.tsx +14 -4
  23. packages/bolt/app/components/workbench/terminal/Terminal.tsx +1 -1
  24. packages/bolt/app/root.tsx +1 -1
  25. packages/bolt/app/styles/components/code.scss +9 -0
  26. packages/bolt/app/{components/editor/codemirror/styles.css → styles/components/editor.scss} +44 -42
  27. packages/bolt/app/styles/components/toast.scss +17 -0
  28. packages/bolt/app/styles/index.scss +3 -0
  29. packages/bolt/app/styles/variables.scss +142 -46
  30. packages/bolt/app/styles/z-index.scss +8 -0
  31. packages/bolt/icons/logo-text.svg +1 -0
  32. packages/bolt/package.json +1 -0
  33. packages/bolt/public/logo_text.svg +0 -1
  34. packages/bolt/uno.config.ts +195 -80
  35. pnpm-lock.yaml +27 -0
packages/bolt/app/components/chat/Artifact.tsx CHANGED
@@ -4,7 +4,6 @@ import { computed } from 'nanostores';
4
  import { memo, useEffect, useRef, useState } from 'react';
5
  import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki';
6
  import type { ActionState } from '~/lib/runtime/action-runner';
7
- import { chatStore } from '~/lib/stores/chat';
8
  import { workbenchStore } from '~/lib/stores/workbench';
9
  import { classNames } from '~/utils/classNames';
10
  import { cubicEasingFn } from '~/utils/easings';
@@ -29,7 +28,6 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
29
  const userToggledActions = useRef(false);
30
  const [showActions, setShowActions] = useState(false);
31
 
32
- const chat = useStore(chatStore);
33
  const artifacts = useStore(workbenchStore.artifacts);
34
  const artifact = artifacts[messageId];
35
 
@@ -51,27 +49,21 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
51
  }, [actions]);
52
 
53
  return (
54
- <div className="flex flex-col overflow-hidden border rounded-lg w-full">
55
  <div className="flex">
56
  <button
57
- className="flex items-stretch bg-gray-50/25 w-full overflow-hidden"
58
  onClick={() => {
59
  const showWorkbench = workbenchStore.showWorkbench.get();
60
  workbenchStore.showWorkbench.set(!showWorkbench);
61
  }}
62
  >
63
- <div className="flex items-center px-6 bg-gray-100/50">
64
- {!artifact?.closed && !chat.aborted ? (
65
- <div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
66
- ) : (
67
- <div className="i-ph:code-bold scale-130 text-gray-600"></div>
68
- )}
69
- </div>
70
- <div className="px-4 p-3 w-full text-left">
71
- <div className="w-full">{artifact?.title}</div>
72
- <small className="inline-block w-full w-full">Click to open Workbench</small>
73
  </div>
74
  </button>
 
75
  <AnimatePresence>
76
  {actions.length && (
77
  <motion.button
@@ -79,7 +71,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
79
  animate={{ width: 'auto' }}
80
  exit={{ width: 0 }}
81
  transition={{ duration: 0.15, ease: cubicEasingFn }}
82
- className="hover:bg-gray-200"
83
  onClick={toggleActions}
84
  >
85
  <div className="p-4">
@@ -98,7 +90,8 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
98
  exit={{ height: '0px' }}
99
  transition={{ duration: 0.15 }}
100
  >
101
- <div className="p-4 text-left border-t">
 
102
  <ActionList actions={actions} />
103
  </div>
104
  </motion.div>
@@ -108,29 +101,6 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
108
  );
109
  });
110
 
111
- function getTextColor(status: ActionState['status']) {
112
- switch (status) {
113
- case 'pending': {
114
- return 'text-gray-500';
115
- }
116
- case 'running': {
117
- return 'text-gray-1000';
118
- }
119
- case 'complete': {
120
- return 'text-positive-600';
121
- }
122
- case 'aborted': {
123
- return 'text-gray-600';
124
- }
125
- case 'failed': {
126
- return 'text-negative-600';
127
- }
128
- default: {
129
- return undefined;
130
- }
131
- }
132
- }
133
-
134
  interface ShellCodeBlockProps {
135
  classsName?: string;
136
  code: string;
@@ -140,7 +110,12 @@ function ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) {
140
  return (
141
  <div
142
  className={classNames('text-xs', classsName)}
143
- dangerouslySetInnerHTML={{ __html: shellHighlighter.codeToHtml(code, { lang: 'shell', theme: 'dark-plus' }) }}
 
 
 
 
 
144
  ></div>
145
  );
146
  }
@@ -160,6 +135,7 @@ const ActionList = memo(({ actions }: ActionListProps) => {
160
  <ul className="list-none space-y-2.5">
161
  {actions.map((action, index) => {
162
  const { status, type, content } = action;
 
163
 
164
  return (
165
  <motion.li
@@ -172,21 +148,24 @@ const ActionList = memo(({ actions }: ActionListProps) => {
172
  ease: cubicEasingFn,
173
  }}
174
  >
175
- <div className="flex items-center gap-1.5">
176
- <div className={classNames('text-lg', getTextColor(action.status))}>
177
  {status === 'running' ? (
178
  <div className="i-svg-spinners:90-ring-with-bg"></div>
179
  ) : status === 'pending' ? (
180
  <div className="i-ph:circle-duotone"></div>
181
  ) : status === 'complete' ? (
182
- <div className="i-ph:check-circle-duotone"></div>
183
  ) : status === 'failed' || status === 'aborted' ? (
184
- <div className="i-ph:x-circle-duotone"></div>
185
  ) : null}
186
  </div>
187
  {type === 'file' ? (
188
  <div>
189
- Create <code className="bg-gray-100 text-gray-700">{action.filePath}</code>
 
 
 
190
  </div>
191
  ) : type === 'shell' ? (
192
  <div className="flex items-center w-full min-h-[28px]">
@@ -194,7 +173,14 @@ const ActionList = memo(({ actions }: ActionListProps) => {
194
  </div>
195
  ) : null}
196
  </div>
197
- {type === 'shell' && <ShellCodeBlock classsName="mt-1" code={content} />}
 
 
 
 
 
 
 
198
  </motion.li>
199
  );
200
  })}
@@ -202,3 +188,26 @@ const ActionList = memo(({ actions }: ActionListProps) => {
202
  </motion.div>
203
  );
204
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import { memo, useEffect, useRef, useState } from 'react';
5
  import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki';
6
  import type { ActionState } from '~/lib/runtime/action-runner';
 
7
  import { workbenchStore } from '~/lib/stores/workbench';
8
  import { classNames } from '~/utils/classNames';
9
  import { cubicEasingFn } from '~/utils/easings';
 
28
  const userToggledActions = useRef(false);
29
  const [showActions, setShowActions] = useState(false);
30
 
 
31
  const artifacts = useStore(workbenchStore.artifacts);
32
  const artifact = artifacts[messageId];
33
 
 
49
  }, [actions]);
50
 
51
  return (
52
+ <div className="artifact border border-bolt-elements-borderColor flex flex-col overflow-hidden rounded-lg w-full transition-border duration-150">
53
  <div className="flex">
54
  <button
55
+ className="flex items-stretch bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover w-full overflow-hidden"
56
  onClick={() => {
57
  const showWorkbench = workbenchStore.showWorkbench.get();
58
  workbenchStore.showWorkbench.set(!showWorkbench);
59
  }}
60
  >
61
+ <div className="px-5 p-3.5 w-full text-left">
62
+ <div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">{artifact?.title}</div>
63
+ <div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">Click to open Workbench</div>
 
 
 
 
 
 
 
64
  </div>
65
  </button>
66
+ <div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />
67
  <AnimatePresence>
68
  {actions.length && (
69
  <motion.button
 
71
  animate={{ width: 'auto' }}
72
  exit={{ width: 0 }}
73
  transition={{ duration: 0.15, ease: cubicEasingFn }}
74
+ className="bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover"
75
  onClick={toggleActions}
76
  >
77
  <div className="p-4">
 
90
  exit={{ height: '0px' }}
91
  transition={{ duration: 0.15 }}
92
  >
93
+ <div className="bg-bolt-elements-artifacts-borderColor h-[1px]" />
94
+ <div className="p-5 text-left bg-bolt-elements-actions-background">
95
  <ActionList actions={actions} />
96
  </div>
97
  </motion.div>
 
101
  );
102
  });
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  interface ShellCodeBlockProps {
105
  classsName?: string;
106
  code: string;
 
110
  return (
111
  <div
112
  className={classNames('text-xs', classsName)}
113
+ dangerouslySetInnerHTML={{
114
+ __html: shellHighlighter.codeToHtml(code, {
115
+ lang: 'shell',
116
+ theme: 'dark-plus',
117
+ }),
118
+ }}
119
  ></div>
120
  );
121
  }
 
135
  <ul className="list-none space-y-2.5">
136
  {actions.map((action, index) => {
137
  const { status, type, content } = action;
138
+ const isLast = index === actions.length - 1;
139
 
140
  return (
141
  <motion.li
 
148
  ease: cubicEasingFn,
149
  }}
150
  >
151
+ <div className="flex items-center gap-1.5 text-sm">
152
+ <div className={classNames('text-lg', getIconColor(action.status))}>
153
  {status === 'running' ? (
154
  <div className="i-svg-spinners:90-ring-with-bg"></div>
155
  ) : status === 'pending' ? (
156
  <div className="i-ph:circle-duotone"></div>
157
  ) : status === 'complete' ? (
158
+ <div className="i-ph:check"></div>
159
  ) : status === 'failed' || status === 'aborted' ? (
160
+ <div className="i-ph:x"></div>
161
  ) : null}
162
  </div>
163
  {type === 'file' ? (
164
  <div>
165
+ Create{' '}
166
+ <code className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md">
167
+ {action.filePath}
168
+ </code>
169
  </div>
170
  ) : type === 'shell' ? (
171
  <div className="flex items-center w-full min-h-[28px]">
 
173
  </div>
174
  ) : null}
175
  </div>
176
+ {type === 'shell' && (
177
+ <ShellCodeBlock
178
+ classsName={classNames('mt-1', {
179
+ 'mb-3.5': !isLast,
180
+ })}
181
+ code={content}
182
+ />
183
+ )}
184
  </motion.li>
185
  );
186
  })}
 
188
  </motion.div>
189
  );
190
  });
191
+
192
+ function getIconColor(status: ActionState['status']) {
193
+ switch (status) {
194
+ case 'pending': {
195
+ return 'text-bolt-elements-textTertiary';
196
+ }
197
+ case 'running': {
198
+ return 'text-bolt-elements-loader-progress';
199
+ }
200
+ case 'complete': {
201
+ return 'text-bolt-elements-icon-success';
202
+ }
203
+ case 'aborted': {
204
+ return 'text-bolt-elements-textSecondary';
205
+ }
206
+ case 'failed': {
207
+ return 'text-bolt-elements-icon-error';
208
+ }
209
+ default: {
210
+ return undefined;
211
+ }
212
+ }
213
+ }
packages/bolt/app/components/chat/BaseChat.tsx CHANGED
@@ -32,7 +32,7 @@ const EXAMPLE_PROMPTS = [
32
  { text: 'How do I center a div?' },
33
  ];
34
 
35
- const TEXTAREA_MIN_HEIGHT = 72;
36
 
37
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
38
  (
@@ -56,14 +56,18 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
56
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
57
 
58
  return (
59
- <div ref={ref} className="relative flex h-full w-full overflow-hidden ">
60
  <ClientOnly>{() => <Menu />}</ClientOnly>
61
  <div ref={scrollRef} className="flex overflow-scroll w-full h-full">
62
  <div className="flex flex-col w-full h-full px-6">
63
  {!chatStarted && (
64
  <div id="intro" className="mt-[26vh] max-w-2xl mx-auto">
65
- <h1 className="text-5xl text-center font-bold text-slate-800 mb-2">Where ideas begin</h1>
66
- <p className="mb-4 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
 
 
 
 
67
  </div>
68
  )}
69
  <div
@@ -76,7 +80,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
76
  return chatStarted ? (
77
  <Messages
78
  ref={messageRef}
79
- className="flex flex-col w-full flex-1 max-w-2xl px-4 pb-10 mx-auto z-1"
80
  messages={messages}
81
  isStreaming={isStreaming}
82
  />
@@ -90,12 +94,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
90
  >
91
  <div
92
  className={classNames(
93
- 'shadow-sm border border-gray-200 bg-white/85 backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
94
  )}
95
  >
96
  <textarea
97
  ref={textareaRef}
98
- className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none bg-transparent`}
99
  onKeyDown={(event) => {
100
  if (event.key === 'Enter') {
101
  if (event.shiftKey) {
@@ -136,44 +140,42 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
136
  </ClientOnly>
137
  <div className="flex justify-between text-sm p-4 pt-2">
138
  <div className="flex gap-1 items-center">
139
- <IconButton icon="i-ph:microphone-duotone" className="-ml-1" />
140
- <IconButton icon="i-ph:plus-circle-duotone" />
141
- <IconButton icon="i-ph:pencil-simple-duotone" />
142
  <IconButton
 
143
  disabled={input.length === 0 || enhancingPrompt}
144
  className={classNames({
145
  'opacity-100!': enhancingPrompt,
146
- 'text-accent! pr-1.5 enabled:hover:bg-accent/12!': promptEnhanced,
 
147
  })}
148
  onClick={() => enhancePrompt?.()}
149
  >
150
  {enhancingPrompt ? (
151
  <>
152
- <div className="i-svg-spinners:90-ring-with-bg text-black text-xl"></div>
153
  <div className="ml-1.5">Enhancing prompt...</div>
154
  </>
155
  ) : (
156
  <>
157
- <div className="i-blitz:stars text-xl"></div>
158
  {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
159
  </>
160
  )}
161
  </IconButton>
162
  </div>
163
  {input.length > 3 ? (
164
- <div className="text-xs">
165
- Use <kbd className="bg-gray-100 p-1 rounded-md">Shift</kbd> +{' '}
166
- <kbd className="bg-gray-100 p-1 rounded-md">Return</kbd> for a new line
167
  </div>
168
  ) : null}
169
  </div>
170
  </div>
171
- <div className="bg-white pb-6">{/* Ghost Element */}</div>
172
  </div>
173
  </div>
174
  {!chatStarted && (
175
- <div id="examples" className="relative w-full max-w-2xl mx-auto text-center mt-8 flex justify-center">
176
- <div className="flex flex-col items-center space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
177
  {EXAMPLE_PROMPTS.map((examplePrompt, index) => {
178
  return (
179
  <button
@@ -181,7 +183,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
181
  onClick={(event) => {
182
  sendMessage?.(event, examplePrompt.text);
183
  }}
184
- className="group flex items-center gap-2 bg-transparent text-gray-500 hover:text-gray-1000"
185
  >
186
  {examplePrompt.text}
187
  <div className="i-ph:arrow-bend-down-left" />
 
32
  { text: 'How do I center a div?' },
33
  ];
34
 
35
+ const TEXTAREA_MIN_HEIGHT = 76;
36
 
37
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
38
  (
 
56
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
57
 
58
  return (
59
+ <div ref={ref} className="relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1">
60
  <ClientOnly>{() => <Menu />}</ClientOnly>
61
  <div ref={scrollRef} className="flex overflow-scroll w-full h-full">
62
  <div className="flex flex-col w-full h-full px-6">
63
  {!chatStarted && (
64
  <div id="intro" className="mt-[26vh] max-w-2xl mx-auto">
65
+ <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
66
+ Where ideas begin
67
+ </h1>
68
+ <p className="mb-4 text-center text-bolt-elements-textSecondary">
69
+ Bring ideas to life in seconds or get help on existing projects.
70
+ </p>
71
  </div>
72
  )}
73
  <div
 
80
  return chatStarted ? (
81
  <Messages
82
  ref={messageRef}
83
+ className="flex flex-col w-full flex-1 max-w-2xl px-4 pb-6 mx-auto z-1"
84
  messages={messages}
85
  isStreaming={isStreaming}
86
  />
 
94
  >
95
  <div
96
  className={classNames(
97
+ 'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
98
  )}
99
  >
100
  <textarea
101
  ref={textareaRef}
102
+ className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}
103
  onKeyDown={(event) => {
104
  if (event.key === 'Enter') {
105
  if (event.shiftKey) {
 
140
  </ClientOnly>
141
  <div className="flex justify-between text-sm p-4 pt-2">
142
  <div className="flex gap-1 items-center">
 
 
 
143
  <IconButton
144
+ title="Enhance prompt"
145
  disabled={input.length === 0 || enhancingPrompt}
146
  className={classNames({
147
  'opacity-100!': enhancingPrompt,
148
+ 'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
149
+ promptEnhanced,
150
  })}
151
  onClick={() => enhancePrompt?.()}
152
  >
153
  {enhancingPrompt ? (
154
  <>
155
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
156
  <div className="ml-1.5">Enhancing prompt...</div>
157
  </>
158
  ) : (
159
  <>
160
+ <div className="i-bolt:stars text-xl"></div>
161
  {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
162
  </>
163
  )}
164
  </IconButton>
165
  </div>
166
  {input.length > 3 ? (
167
+ <div className="text-xs text-bolt-elements-textTertiary">
168
+ Use <kbd className="kdb">Shift</kbd> + <kbd className="kdb">Return</kbd> for a new line
 
169
  </div>
170
  ) : null}
171
  </div>
172
  </div>
173
+ <div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div>
174
  </div>
175
  </div>
176
  {!chatStarted && (
177
+ <div id="examples" className="relative w-full max-w-2xl mx-auto mt-8 flex justify-center">
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 (
181
  <button
 
183
  onClick={(event) => {
184
  sendMessage?.(event, examplePrompt.text);
185
  }}
186
+ className="group flex items-center w-full gap-2 justify-center bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-theme"
187
  >
188
  {examplePrompt.text}
189
  <div className="i-ph:arrow-bend-down-left" />
packages/bolt/app/components/chat/Chat.client.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import type { Message } from 'ai';
2
  import { useChat } from 'ai/react';
3
  import { useAnimate } from 'framer-motion';
4
- import { 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';
@@ -9,7 +9,7 @@ import { chatStore } from '~/lib/stores/chat';
9
  import { workbenchStore } from '~/lib/stores/workbench';
10
  import { fileModificationsToHTML } from '~/utils/diff';
11
  import { cubicEasingFn } from '~/utils/easings';
12
- import { createScopedLogger } from '~/utils/logger';
13
  import { BaseChat } from './BaseChat';
14
 
15
  const toastAnimation = cssTransition({
@@ -20,12 +20,40 @@ const toastAnimation = cssTransition({
20
  const logger = createScopedLogger('Chat');
21
 
22
  export function Chat() {
 
 
23
  const { ready, initialMessages, storeMessageHistory } = useChatHistory();
24
 
25
  return (
26
  <>
27
  {ready && <ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} />}
28
- <ToastContainer position="bottom-right" stacked pauseOnFocusLoss transition={toastAnimation} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </>
30
  );
31
  }
@@ -35,7 +63,7 @@ interface ChatProps {
35
  storeMessageHistory: (messages: Message[]) => Promise<void>;
36
  }
37
 
38
- export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
39
  useShortcuts();
40
 
41
  const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -199,4 +227,4 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
199
  }}
200
  />
201
  );
202
- }
 
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';
 
9
  import { workbenchStore } from '~/lib/stores/workbench';
10
  import { fileModificationsToHTML } from '~/utils/diff';
11
  import { cubicEasingFn } from '~/utils/easings';
12
+ import { createScopedLogger, renderLogger } from '~/utils/logger';
13
  import { BaseChat } from './BaseChat';
14
 
15
  const toastAnimation = cssTransition({
 
20
  const logger = createScopedLogger('Chat');
21
 
22
  export function Chat() {
23
+ renderLogger.trace('Chat');
24
+
25
  const { ready, initialMessages, storeMessageHistory } = useChatHistory();
26
 
27
  return (
28
  <>
29
  {ready && <ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} />}
30
+ <ToastContainer
31
+ closeButton={({ closeToast }) => {
32
+ return (
33
+ <button className="Toastify__close-button" onClick={closeToast}>
34
+ <div className="i-ph:x text-lg" />
35
+ </button>
36
+ );
37
+ }}
38
+ icon={({ type }) => {
39
+ /**
40
+ * @todo Handle more types if we need them. This may require extra color palettes.
41
+ */
42
+ switch (type) {
43
+ case 'success': {
44
+ return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />;
45
+ }
46
+ case 'error': {
47
+ return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />;
48
+ }
49
+ }
50
+
51
+ return undefined;
52
+ }}
53
+ position="bottom-right"
54
+ pauseOnFocusLoss
55
+ transition={toastAnimation}
56
+ />
57
  </>
58
  );
59
  }
 
63
  storeMessageHistory: (messages: Message[]) => Promise<void>;
64
  }
65
 
66
+ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProps) => {
67
  useShortcuts();
68
 
69
  const textareaRef = useRef<HTMLTextAreaElement>(null);
 
227
  }}
228
  />
229
  );
230
+ });
packages/bolt/app/components/chat/Markdown.module.scss CHANGED
@@ -1,10 +1,5 @@
1
- $font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace,
2
- ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
3
- $color-text: #333;
4
- $color-heading: #2c3e50;
5
- $color-link: #3498db;
6
- $color-code-bg: #f8f8f8;
7
- $color-blockquote-border: #dfe2e5;
8
 
9
  @mixin not-inside-actions {
10
  &:not(:has(:global(.actions)), :global(.actions *)) {
@@ -14,31 +9,35 @@ $color-blockquote-border: #dfe2e5;
14
 
15
  .MarkdownContent {
16
  line-height: 1.6;
17
- color: $color-text;
18
 
19
  > *:not(:last-child) {
20
- margin-bottom: 16px;
 
 
 
 
21
  }
22
 
23
  :is(h1, h2, h3, h4, h5, h6) {
24
  @include not-inside-actions {
25
- margin-top: 24px;
26
- margin-bottom: 16px;
27
  font-weight: 600;
28
  line-height: 1.25;
29
- color: $color-heading;
30
  }
31
  }
32
 
33
  h1 {
34
  font-size: 2em;
35
- border-bottom: 1px solid #eaecef;
36
  padding-bottom: 0.3em;
37
  }
38
 
39
  h2 {
40
  font-size: 1.5em;
41
- border-bottom: 1px solid #eaecef;
42
  padding-bottom: 0.3em;
43
  }
44
 
@@ -60,13 +59,14 @@ $color-blockquote-border: #dfe2e5;
60
  }
61
 
62
  p:not(:last-of-type) {
63
- margin-top: 0;
64
- margin-bottom: 16px;
65
  }
66
 
67
  a {
68
- color: $color-link;
69
  text-decoration: none;
 
70
 
71
  &:hover {
72
  text-decoration: underline;
@@ -75,12 +75,13 @@ $color-blockquote-border: #dfe2e5;
75
 
76
  :not(pre) > code {
77
  font-family: $font-mono;
78
- font-size: 14px;
79
- border-radius: 6px;
80
- padding: 0.2em 0.4em;
81
 
82
  @include not-inside-actions {
83
- background-color: $color-code-bg;
 
 
 
84
  }
85
  }
86
 
@@ -91,7 +92,7 @@ $color-blockquote-border: #dfe2e5;
91
 
92
  pre:has(> code) {
93
  font-family: $font-mono;
94
- font-size: 14px;
95
  background: transparent;
96
  overflow-x: auto;
97
  min-width: 0;
@@ -100,15 +101,15 @@ $color-blockquote-border: #dfe2e5;
100
  blockquote {
101
  margin: 0;
102
  padding: 0 1em;
103
- color: #6a737d;
104
- border-left: 0.25em solid $color-blockquote-border;
105
  }
106
 
107
  :is(ul, ol) {
108
  @include not-inside-actions {
109
  padding-left: 2em;
110
- margin-top: 0;
111
- margin-bottom: 24px;
112
  }
113
  }
114
 
@@ -127,11 +128,11 @@ $color-blockquote-border: #dfe2e5;
127
  li {
128
  @include not-inside-actions {
129
  & + li {
130
- margin-top: 8px;
131
  }
132
 
133
  > *:not(:last-child) {
134
- margin-bottom: 16px;
135
  }
136
  }
137
  }
@@ -145,14 +146,14 @@ $color-blockquote-border: #dfe2e5;
145
  height: 0.25em;
146
  padding: 0;
147
  margin: 24px 0;
148
- background-color: #e1e4e8;
149
  border: 0;
150
  }
151
 
152
  table {
153
  border-collapse: collapse;
154
  width: 100%;
155
- margin-bottom: 16px;
156
 
157
  :is(th, td) {
158
  padding: 6px 13px;
 
1
+ $font-mono: ui-monospace, 'Fira Code', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
2
+ $code-font-size: 13px;
 
 
 
 
 
3
 
4
  @mixin not-inside-actions {
5
  &:not(:has(:global(.actions)), :global(.actions *)) {
 
9
 
10
  .MarkdownContent {
11
  line-height: 1.6;
12
+ color: var(--bolt-elements-textPrimary);
13
 
14
  > *:not(:last-child) {
15
+ margin-block-end: 16px;
16
+ }
17
+
18
+ :global(.artifact) {
19
+ margin: 1.5em 0;
20
  }
21
 
22
  :is(h1, h2, h3, h4, h5, h6) {
23
  @include not-inside-actions {
24
+ margin-block-start: 24px;
25
+ margin-block-end: 16px;
26
  font-weight: 600;
27
  line-height: 1.25;
28
+ color: var(--bolt-elements-textPrimary);
29
  }
30
  }
31
 
32
  h1 {
33
  font-size: 2em;
34
+ border-bottom: 1px solid var(--bolt-elements-borderColor);
35
  padding-bottom: 0.3em;
36
  }
37
 
38
  h2 {
39
  font-size: 1.5em;
40
+ border-bottom: 1px solid var(--bolt-elements-borderColor);
41
  padding-bottom: 0.3em;
42
  }
43
 
 
59
  }
60
 
61
  p:not(:last-of-type) {
62
+ margin-block-start: 0;
63
+ margin-block-end: 16px;
64
  }
65
 
66
  a {
67
+ color: var(--bolt-elements-messages-linkColor);
68
  text-decoration: none;
69
+ cursor: pointer;
70
 
71
  &:hover {
72
  text-decoration: underline;
 
75
 
76
  :not(pre) > code {
77
  font-family: $font-mono;
78
+ font-size: $code-font-size;
 
 
79
 
80
  @include not-inside-actions {
81
+ border-radius: 6px;
82
+ padding: 0.2em 0.4em;
83
+ background-color: var(--bolt-elements-messages-inlineCode-background);
84
+ color: var(--bolt-elements-messages-inlineCode-text);
85
  }
86
  }
87
 
 
92
 
93
  pre:has(> code) {
94
  font-family: $font-mono;
95
+ font-size: $code-font-size;
96
  background: transparent;
97
  overflow-x: auto;
98
  min-width: 0;
 
101
  blockquote {
102
  margin: 0;
103
  padding: 0 1em;
104
+ color: var(--bolt-elements-textTertiary);
105
+ border-left: 0.25em solid var(--bolt-elements-borderColor);
106
  }
107
 
108
  :is(ul, ol) {
109
  @include not-inside-actions {
110
  padding-left: 2em;
111
+ margin-block-start: 0;
112
+ margin-block-end: 16px;
113
  }
114
  }
115
 
 
128
  li {
129
  @include not-inside-actions {
130
  & + li {
131
+ margin-block-start: 8px;
132
  }
133
 
134
  > *:not(:last-child) {
135
+ margin-block-end: 16px;
136
  }
137
  }
138
  }
 
146
  height: 0.25em;
147
  padding: 0;
148
  margin: 24px 0;
149
+ background-color: var(--bolt-elements-borderColor);
150
  border: 0;
151
  }
152
 
153
  table {
154
  border-collapse: collapse;
155
  width: 100%;
156
+ margin-block-end: 16px;
157
 
158
  :is(th, td) {
159
  padding: 6px 13px;
packages/bolt/app/components/chat/Messages.client.tsx CHANGED
@@ -17,49 +17,41 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
17
  return (
18
  <div id={id} ref={ref} className={props.className}>
19
  {messages.length > 0
20
- ? messages.map((message, i) => {
21
  const { role, content } = message;
22
- const isUser = role === 'user';
23
- const isFirst = i === 0;
24
- const isLast = i === messages.length - 1;
25
- const isUserMessage = message.role === 'user';
26
- const isAssistantMessage = message.role === 'assistant';
27
 
28
  return (
29
  <div
30
- key={message.id}
31
- className={classNames('relative overflow-hidden rounded-md p-[1px]', {
 
 
 
32
  'mt-4': !isFirst,
33
- 'bg-gray-200': isUserMessage || !isStreaming || (isStreaming && isAssistantMessage && !isLast),
34
- 'bg-gradient-to-b from-gray-200 to-transparent': isStreaming && isAssistantMessage && isLast,
35
  })}
36
  >
37
- <div
38
- className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.375rem-1px)]', {
39
- 'bg-white': isUserMessage || !isStreaming || (isStreaming && !isLast),
40
- 'bg-gradient-to-b from-white from-30% to-transparent': isStreaming && isLast,
41
- })}
42
- >
43
  <div
44
  className={classNames(
45
- 'flex items-center justify-center min-w-[34px] min-h-[34px] text-gray-600 rounded-md p-1 self-start',
46
- {
47
- 'bg-gray-100': isUserMessage,
48
- 'bg-accent text-xl': isAssistantMessage,
49
- },
50
  )}
51
  >
52
- <div className={isUserMessage ? 'i-ph:user-fill text-xl' : 'i-blitz:logo'}></div>
53
- </div>
54
- <div className="grid grid-col-1 w-full">
55
- {isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
56
  </div>
 
 
 
57
  </div>
58
  </div>
59
  );
60
  })
61
  : null}
62
- {isStreaming && <div className="text-center w-full i-svg-spinners:3-dots-fade text-4xl mt-4"></div>}
 
 
63
  </div>
64
  );
65
  });
 
17
  return (
18
  <div id={id} ref={ref} className={props.className}>
19
  {messages.length > 0
20
+ ? messages.map((message, index) => {
21
  const { role, content } = message;
22
+ const isUserMessage = role === 'user';
23
+ const isFirst = index === 0;
24
+ const isLast = index === messages.length - 1;
 
 
25
 
26
  return (
27
  <div
28
+ key={index}
29
+ className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
30
+ 'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
31
+ 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
32
+ isStreaming && isLast,
33
  'mt-4': !isFirst,
 
 
34
  })}
35
  >
36
+ {isUserMessage && (
 
 
 
 
 
37
  <div
38
  className={classNames(
39
+ 'flex items-center justify-center min-w-[34px] min-h-[34px] bg-white text-gray-600 rounded-full p-1 self-start',
 
 
 
 
40
  )}
41
  >
42
+ <div className="i-ph:user-fill text-xl"></div>
 
 
 
43
  </div>
44
+ )}
45
+ <div className="grid grid-col-1 w-full">
46
+ {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
47
  </div>
48
  </div>
49
  );
50
  })
51
  : null}
52
+ {isStreaming && (
53
+ <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
54
+ )}
55
  </div>
56
  );
57
  });
packages/bolt/app/components/chat/SendButton.client.tsx CHANGED
@@ -13,7 +13,7 @@ export function SendButton({ show, isStreaming, onClick }: SendButtonProps) {
13
  <AnimatePresence>
14
  {show ? (
15
  <motion.button
16
- className="absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent hover:brightness-110 color-white rounded-md w-[34px] h-[34px]"
17
  transition={{ ease: customEasingFn, duration: 0.17 }}
18
  initial={{ opacity: 0, y: 10 }}
19
  animate={{ opacity: 1, y: 0 }}
 
13
  <AnimatePresence>
14
  {show ? (
15
  <motion.button
16
+ className="absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent-500 hover:brightness-94 color-white rounded-md w-[34px] h-[34px] transition-theme"
17
  transition={{ ease: customEasingFn, duration: 0.17 }}
18
  initial={{ opacity: 0, y: 10 }}
19
  animate={{ opacity: 1, y: 0 }}
packages/bolt/app/components/chat/UserMessage.tsx CHANGED
@@ -7,7 +7,7 @@ interface UserMessageProps {
7
 
8
  export function UserMessage({ content }: UserMessageProps) {
9
  return (
10
- <div className="overflow-hidden">
11
  <Markdown>{sanitizeUserMessage(content)}</Markdown>
12
  </div>
13
  );
 
7
 
8
  export function UserMessage({ content }: UserMessageProps) {
9
  return (
10
+ <div className="overflow-hidden pt-[4px]">
11
  <Markdown>{sanitizeUserMessage(content)}</Markdown>
12
  </div>
13
  );
packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx CHANGED
@@ -34,6 +34,7 @@ export interface EditorDocument {
34
 
35
  export interface EditorSettings {
36
  fontSize?: string;
 
37
  tabSize?: number;
38
  }
39
 
 
34
 
35
  export interface EditorSettings {
36
  fontSize?: string;
37
+ gutterFontSize?: string;
38
  tabSize?: number;
39
  }
40
 
packages/bolt/app/components/editor/codemirror/cm-theme.ts CHANGED
@@ -1,11 +1,8 @@
1
- import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
2
  import { Compartment, type Extension } from '@codemirror/state';
3
  import { EditorView } from '@codemirror/view';
 
4
  import type { Theme } from '~/types/theme.js';
5
  import type { EditorSettings } from './CodeMirrorEditor.js';
6
- import { vscodeDarkTheme } from './themes/vscode-dark.js';
7
-
8
- import './styles.css';
9
 
10
  export const darkTheme = EditorView.theme({}, { dark: true });
11
  export const themeSelection = new Compartment();
@@ -23,11 +20,9 @@ export function reconfigureTheme(theme: Theme) {
23
 
24
  function getEditorTheme(settings: EditorSettings) {
25
  return EditorView.theme({
26
- ...(settings.fontSize && {
27
- '&': {
28
- fontSize: settings.fontSize,
29
- },
30
- }),
31
  '&.cm-editor': {
32
  height: '100%',
33
  background: 'var(--cm-backgroundColor)',
@@ -46,7 +41,7 @@ function getEditorTheme(settings: EditorSettings) {
46
  padding: '0 0 0 4px',
47
  },
48
  '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
49
- backgroundColor: 'var(--cm-selection-backgroundColorFocused)',
50
  opacity: 'var(--cm-selection-backgroundOpacityFocused, 0.3)',
51
  },
52
  '&:not(.cm-focused) > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
@@ -67,7 +62,7 @@ function getEditorTheme(settings: EditorSettings) {
67
  '.cm-gutter': {
68
  '&.cm-lineNumbers': {
69
  fontFamily: 'Roboto Mono, monospace',
70
- fontSize: '13px',
71
  minWidth: '40px',
72
  },
73
  '& .cm-activeLineGutter': {
@@ -91,6 +86,13 @@ function getEditorTheme(settings: EditorSettings) {
91
  },
92
  '.cm-panel.cm-search label': {
93
  marginLeft: '2px',
 
 
 
 
 
 
 
94
  },
95
  '.cm-panel.cm-search input[type=checkbox]': {
96
  position: 'relative',
@@ -100,10 +102,14 @@ function getEditorTheme(settings: EditorSettings) {
100
  '.cm-panels': {
101
  borderColor: 'var(--cm-panels-borderColor)',
102
  },
 
 
 
 
103
  '.cm-panel.cm-search': {
104
  background: 'var(--cm-search-backgroundColor)',
105
  color: 'var(--cm-search-textColor)',
106
- padding: '6px 8px',
107
  },
108
  '.cm-search .cm-button': {
109
  background: 'var(--cm-search-button-backgroundColor)',
@@ -130,6 +136,7 @@ function getEditorTheme(settings: EditorSettings) {
130
  top: '6px',
131
  right: '6px',
132
  padding: '0 6px',
 
133
  backgroundColor: 'var(--cm-search-closeButton-backgroundColor)',
134
  color: 'var(--cm-search-closeButton-textColor)',
135
  '&:hover': {
@@ -141,6 +148,7 @@ function getEditorTheme(settings: EditorSettings) {
141
  '.cm-search input': {
142
  background: 'var(--cm-search-input-backgroundColor)',
143
  borderColor: 'var(--cm-search-input-borderColor)',
 
144
  outline: 'none',
145
  borderRadius: '4px',
146
  '&:focus-visible': {
@@ -149,6 +157,7 @@ function getEditorTheme(settings: EditorSettings) {
149
  },
150
  '.cm-tooltip': {
151
  background: 'var(--cm-tooltip-backgroundColor)',
 
152
  borderColor: 'var(--cm-tooltip-borderColor)',
153
  color: 'var(--cm-tooltip-textColor)',
154
  },
@@ -156,13 +165,16 @@ function getEditorTheme(settings: EditorSettings) {
156
  background: 'var(--cm-tooltip-backgroundColorSelected)',
157
  color: 'var(--cm-tooltip-textColorSelected)',
158
  },
 
 
 
159
  });
160
  }
161
 
162
  function getLightTheme() {
163
- return syntaxHighlighting(defaultHighlightStyle);
164
  }
165
 
166
  function getDarkTheme() {
167
- return syntaxHighlighting(vscodeDarkTheme);
168
  }
 
 
1
  import { Compartment, type Extension } from '@codemirror/state';
2
  import { EditorView } from '@codemirror/view';
3
+ import { vscodeDark, vscodeLight } from '@uiw/codemirror-theme-vscode';
4
  import type { Theme } from '~/types/theme.js';
5
  import type { EditorSettings } from './CodeMirrorEditor.js';
 
 
 
6
 
7
  export const darkTheme = EditorView.theme({}, { dark: true });
8
  export const themeSelection = new Compartment();
 
20
 
21
  function getEditorTheme(settings: EditorSettings) {
22
  return EditorView.theme({
23
+ '&': {
24
+ fontSize: settings.fontSize ?? '12px',
25
+ },
 
 
26
  '&.cm-editor': {
27
  height: '100%',
28
  background: 'var(--cm-backgroundColor)',
 
41
  padding: '0 0 0 4px',
42
  },
43
  '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
44
+ backgroundColor: 'var(--cm-selection-backgroundColorFocused) !important',
45
  opacity: 'var(--cm-selection-backgroundOpacityFocused, 0.3)',
46
  },
47
  '&:not(.cm-focused) > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
 
62
  '.cm-gutter': {
63
  '&.cm-lineNumbers': {
64
  fontFamily: 'Roboto Mono, monospace',
65
+ fontSize: settings.gutterFontSize ?? settings.fontSize ?? '12px',
66
  minWidth: '40px',
67
  },
68
  '& .cm-activeLineGutter': {
 
86
  },
87
  '.cm-panel.cm-search label': {
88
  marginLeft: '2px',
89
+ fontSize: '12px',
90
+ },
91
+ '.cm-panel.cm-search .cm-button': {
92
+ fontSize: '12px',
93
+ },
94
+ '.cm-panel.cm-search .cm-textfield': {
95
+ fontSize: '12px',
96
  },
97
  '.cm-panel.cm-search input[type=checkbox]': {
98
  position: 'relative',
 
102
  '.cm-panels': {
103
  borderColor: 'var(--cm-panels-borderColor)',
104
  },
105
+ '.cm-panels-bottom': {
106
+ borderTop: '1px solid var(--cm-panels-borderColor)',
107
+ backgroundColor: 'transparent',
108
+ },
109
  '.cm-panel.cm-search': {
110
  background: 'var(--cm-search-backgroundColor)',
111
  color: 'var(--cm-search-textColor)',
112
+ padding: '8px',
113
  },
114
  '.cm-search .cm-button': {
115
  background: 'var(--cm-search-button-backgroundColor)',
 
136
  top: '6px',
137
  right: '6px',
138
  padding: '0 6px',
139
+ fontSize: '1rem',
140
  backgroundColor: 'var(--cm-search-closeButton-backgroundColor)',
141
  color: 'var(--cm-search-closeButton-textColor)',
142
  '&:hover': {
 
148
  '.cm-search input': {
149
  background: 'var(--cm-search-input-backgroundColor)',
150
  borderColor: 'var(--cm-search-input-borderColor)',
151
+ color: 'var(--cm-search-input-textColor)',
152
  outline: 'none',
153
  borderRadius: '4px',
154
  '&:focus-visible': {
 
157
  },
158
  '.cm-tooltip': {
159
  background: 'var(--cm-tooltip-backgroundColor)',
160
+ border: '1px solid transparent',
161
  borderColor: 'var(--cm-tooltip-borderColor)',
162
  color: 'var(--cm-tooltip-textColor)',
163
  },
 
165
  background: 'var(--cm-tooltip-backgroundColorSelected)',
166
  color: 'var(--cm-tooltip-textColorSelected)',
167
  },
168
+ '.cm-searchMatch': {
169
+ backgroundColor: 'var(--cm-searchMatch-backgroundColor)',
170
+ },
171
  });
172
  }
173
 
174
  function getLightTheme() {
175
+ return vscodeLight;
176
  }
177
 
178
  function getDarkTheme() {
179
+ return vscodeDark;
180
  }
packages/bolt/app/components/editor/codemirror/themes/vscode-dark.ts DELETED
@@ -1,76 +0,0 @@
1
- import { HighlightStyle } from '@codemirror/language';
2
- import { tags } from '@lezer/highlight';
3
-
4
- export const vscodeDarkTheme = HighlightStyle.define([
5
- {
6
- tag: [
7
- tags.keyword,
8
- tags.operatorKeyword,
9
- tags.modifier,
10
- tags.color,
11
- tags.constant(tags.name),
12
- tags.standard(tags.name),
13
- tags.standard(tags.tagName),
14
- tags.special(tags.brace),
15
- tags.atom,
16
- tags.bool,
17
- tags.special(tags.variableName),
18
- ],
19
- color: '#569cd6',
20
- },
21
- {
22
- tag: [tags.controlKeyword, tags.moduleKeyword],
23
- color: '#c586c0',
24
- },
25
- {
26
- tag: [
27
- tags.name,
28
- tags.deleted,
29
- tags.character,
30
- tags.macroName,
31
- tags.propertyName,
32
- tags.variableName,
33
- tags.labelName,
34
- tags.definition(tags.name),
35
- ],
36
- color: '#9cdcfe',
37
- },
38
- { tag: tags.heading, fontWeight: 'bold', color: '#9cdcfe' },
39
- {
40
- tag: [
41
- tags.typeName,
42
- tags.className,
43
- tags.tagName,
44
- tags.number,
45
- tags.changed,
46
- tags.annotation,
47
- tags.self,
48
- tags.namespace,
49
- ],
50
- color: '#4ec9b0',
51
- },
52
- {
53
- tag: [tags.function(tags.variableName), tags.function(tags.propertyName)],
54
- color: '#dcdcaa',
55
- },
56
- { tag: [tags.number], color: '#b5cea8' },
57
- {
58
- tag: [tags.operator, tags.punctuation, tags.separator, tags.url, tags.escape, tags.regexp],
59
- color: '#d4d4d4',
60
- },
61
- {
62
- tag: [tags.regexp],
63
- color: '#d16969',
64
- },
65
- {
66
- tag: [tags.special(tags.string), tags.processingInstruction, tags.string, tags.inserted],
67
- color: '#ce9178',
68
- },
69
- { tag: [tags.angleBracket], color: '#808080' },
70
- { tag: tags.strong, fontWeight: 'bold' },
71
- { tag: tags.emphasis, fontStyle: 'italic' },
72
- { tag: tags.strikethrough, textDecoration: 'line-through' },
73
- { tag: [tags.meta, tags.comment], color: '#6a9955' },
74
- { tag: tags.link, color: '#6a9955', textDecoration: 'underline' },
75
- { tag: tags.invalid, color: '#ff0000' },
76
- ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages/bolt/app/components/header/Header.tsx CHANGED
@@ -9,14 +9,17 @@ export function Header() {
9
 
10
  return (
11
  <header
12
- className={classNames('flex items-center bg-white p-5 border-b h-[var(--header-height)]', {
13
- 'border-transparent': !chat.started,
14
- 'border-gray-200': chat.started,
15
- })}
 
 
 
16
  >
17
- <div className="flex items-center gap-2">
18
- <a href="/" className="text-2xl font-semibold text-accent">
19
- <img src="/logo_text.svg" width="60px" alt="Bolt Logo" />
20
  </a>
21
  </div>
22
  <div className="ml-auto flex gap-2">
 
9
 
10
  return (
11
  <header
12
+ className={classNames(
13
+ 'flex items-center bg-bolt-elements-background-depth-1 p-5 border-b h-[var(--header-height)]',
14
+ {
15
+ 'border-transparent': !chat.started,
16
+ 'border-bolt-elements-borderColor': chat.started,
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="ml-auto flex gap-2">
packages/bolt/app/components/sidebar/HistoryItem.tsx CHANGED
@@ -53,18 +53,18 @@ export function HistoryItem({ item, loadEntries }: { item: ChatHistory; loadEntr
53
  return (
54
  <div
55
  ref={hoverRef}
56
- className="group rounded-md hover:bg-gray-100 overflow-hidden flex justify-between items-center px-2 py-1"
57
  >
58
  <a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
59
  {item.description}
60
- <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-white group-hover:from-gray-100 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
61
  {hovering && (
62
- <div className="flex items-center p-1">
63
  {requestingDelete ? (
64
- <button className="i-ph:check text-gray-600 hover:text-gray-1000 scale-110" onClick={deleteItem} />
65
  ) : (
66
  <button
67
- className="i-ph:trash text-gray-600 hover:text-gray-1000 scale-110"
68
  onClick={(event) => {
69
  event.preventDefault();
70
  setRequestingDelete(true);
 
53
  return (
54
  <div
55
  ref={hoverRef}
56
+ className="group rounded-md text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 overflow-hidden flex justify-between items-center px-2 py-1"
57
  >
58
  <a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
59
  {item.description}
60
+ <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
61
  {hovering && (
62
+ <div className="flex items-center p-1 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary">
63
  {requestingDelete ? (
64
+ <button className="i-ph:check scale-110" onClick={deleteItem} />
65
  ) : (
66
  <button
67
+ className="i-ph:trash scale-110"
68
  onClick={(event) => {
69
  event.preventDefault();
70
  setRequestingDelete(true);
packages/bolt/app/components/sidebar/Menu.client.tsx CHANGED
@@ -2,6 +2,7 @@ import { motion, type Variants } from 'framer-motion';
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { IconButton } from '~/components/ui/IconButton';
 
5
  import { db, getAll, type ChatHistory } from '~/lib/persistence';
6
  import { cubicEasingFn } from '~/utils/easings';
7
  import { HistoryItem } from './HistoryItem';
@@ -10,6 +11,7 @@ import { binDates } from './date-binning';
10
  const menuVariants = {
11
  closed: {
12
  opacity: 0,
 
13
  left: '-150px',
14
  transition: {
15
  duration: 0.2,
@@ -18,6 +20,7 @@ const menuVariants = {
18
  },
19
  open: {
20
  opacity: 1,
 
21
  left: 0,
22
  transition: {
23
  duration: 0.2,
@@ -47,21 +50,22 @@ export function Menu() {
47
  }, [open]);
48
 
49
  useEffect(() => {
 
 
 
50
  function onMouseMove(event: MouseEvent) {
51
- if (event.pageX < 80) {
52
  setOpen(true);
53
  }
54
- }
55
 
56
- function onMouseLeave(_event: MouseEvent) {
57
- setOpen(false);
 
58
  }
59
 
60
- menuRef.current?.addEventListener('mouseleave', onMouseLeave);
61
  window.addEventListener('mousemove', onMouseMove);
62
 
63
  return () => {
64
- menuRef.current?.removeEventListener('mouseleave', onMouseLeave);
65
  window.removeEventListener('mousemove', onMouseMove);
66
  };
67
  }, []);
@@ -72,34 +76,34 @@ export function Menu() {
72
  initial="closed"
73
  animate={open ? 'open' : 'closed'}
74
  variants={menuVariants}
75
- className="flex flex-col side-menu fixed top-0 w-[350px] h-full bg-white border-r rounded-r-3xl border-gray-200 z-max shadow-xl text-sm"
76
  >
77
- <div className="flex items-center p-4 h-[var(--header-height)]">
78
- <img src="/logo_text.svg" width="60px" alt="Bolt Logo" />
79
- </div>
80
  <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
81
  <div className="p-4">
82
  <a
83
  href="/"
84
- className="flex gap-2 items-center text-accent-600 rounded-md bg-accent-600/12 hover:bg-accent-600/15 p-2 font-medium"
85
  >
86
- <span className="inline-block i-blitz:chat scale-110" />
87
  Start new chat
88
  </a>
89
  </div>
90
- <div className="font-semibold pl-6 pr-5 my-2">Your Chats</div>
91
  <div className="flex-1 overflow-scroll pl-4 pr-5 pb-5">
92
- {list.length === 0 && <div className="pl-2 text-gray">No previous conversations</div>}
93
  {binDates(list).map(({ category, items }) => (
94
  <div key={category} className="mt-4 first:mt-0 space-y-1">
95
- <div className="text-gray sticky top-0 z-20 bg-white pl-2 pt-2 pb-1">{category}</div>
 
 
96
  {items.map((item) => (
97
  <HistoryItem key={item.id} item={item} loadEntries={loadEntries} />
98
  ))}
99
  </div>
100
  ))}
101
  </div>
102
- <div className="flex items-center border-t p-4">
103
  <a href="/logout">
104
  <IconButton className="p-1.5 gap-1.5">
105
  <>
@@ -107,6 +111,7 @@ export function Menu() {
107
  </>
108
  </IconButton>
109
  </a>
 
110
  </div>
111
  </div>
112
  </motion.div>
 
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { IconButton } from '~/components/ui/IconButton';
5
+ import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
  import { db, getAll, type ChatHistory } from '~/lib/persistence';
7
  import { cubicEasingFn } from '~/utils/easings';
8
  import { HistoryItem } from './HistoryItem';
 
11
  const menuVariants = {
12
  closed: {
13
  opacity: 0,
14
+ visibility: 'hidden',
15
  left: '-150px',
16
  transition: {
17
  duration: 0.2,
 
20
  },
21
  open: {
22
  opacity: 1,
23
+ visibility: 'initial',
24
  left: 0,
25
  transition: {
26
  duration: 0.2,
 
50
  }, [open]);
51
 
52
  useEffect(() => {
53
+ const enterThreshold = 80;
54
+ const exitThreshold = 40;
55
+
56
  function onMouseMove(event: MouseEvent) {
57
+ if (event.pageX < enterThreshold) {
58
  setOpen(true);
59
  }
 
60
 
61
+ if (menuRef.current && event.clientX > menuRef.current.getBoundingClientRect().right + exitThreshold) {
62
+ setOpen(false);
63
+ }
64
  }
65
 
 
66
  window.addEventListener('mousemove', onMouseMove);
67
 
68
  return () => {
 
69
  window.removeEventListener('mousemove', onMouseMove);
70
  };
71
  }, []);
 
76
  initial="closed"
77
  animate={open ? 'open' : 'closed'}
78
  variants={menuVariants}
79
+ className="flex flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
80
  >
81
+ <div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
 
 
82
  <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
83
  <div className="p-4">
84
  <a
85
  href="/"
86
+ className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
87
  >
88
+ <span className="inline-block i-bolt:chat scale-110" />
89
  Start new chat
90
  </a>
91
  </div>
92
+ <div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
93
  <div className="flex-1 overflow-scroll pl-4 pr-5 pb-5">
94
+ {list.length === 0 && <div className="pl-2 text-bolt-elements-textTertiary">No previous conversations</div>}
95
  {binDates(list).map(({ category, items }) => (
96
  <div key={category} className="mt-4 first:mt-0 space-y-1">
97
+ <div className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
98
+ {category}
99
+ </div>
100
  {items.map((item) => (
101
  <HistoryItem key={item.id} item={item} loadEntries={loadEntries} />
102
  ))}
103
  </div>
104
  ))}
105
  </div>
106
+ <div className="flex items-center border-t border-bolt-elements-borderColor p-4">
107
  <a href="/logout">
108
  <IconButton className="p-1.5 gap-1.5">
109
  <>
 
111
  </>
112
  </IconButton>
113
  </a>
114
+ <ThemeSwitch className="ml-auto" />
115
  </div>
116
  </div>
117
  </motion.div>
packages/bolt/app/components/ui/IconButton.tsx CHANGED
@@ -8,6 +8,7 @@ interface BaseIconButtonProps {
8
  className?: string;
9
  iconClassName?: string;
10
  disabledClassName?: string;
 
11
  disabled?: boolean;
12
  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
13
  }
@@ -32,18 +33,20 @@ export const IconButton = memo(
32
  iconClassName,
33
  disabledClassName,
34
  disabled = false,
 
35
  onClick,
36
  children,
37
  }: IconButtonProps) => {
38
  return (
39
  <button
40
  className={classNames(
41
- 'flex items-center text-gray-600 bg-transparent enabled:hover:text-gray-900 rounded-md p-1 enabled:hover:bg-gray-200/80 disabled:cursor-not-allowed',
42
  {
43
  [classNames('opacity-30', disabledClassName)]: disabled,
44
  },
45
  className,
46
  )}
 
47
  disabled={disabled}
48
  onClick={(event) => {
49
  if (disabled) {
 
8
  className?: string;
9
  iconClassName?: string;
10
  disabledClassName?: string;
11
+ title?: string;
12
  disabled?: boolean;
13
  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
14
  }
 
33
  iconClassName,
34
  disabledClassName,
35
  disabled = false,
36
+ title,
37
  onClick,
38
  children,
39
  }: IconButtonProps) => {
40
  return (
41
  <button
42
  className={classNames(
43
+ 'flex items-center 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',
44
  {
45
  [classNames('opacity-30', disabledClassName)]: disabled,
46
  },
47
  className,
48
  )}
49
+ title={title}
50
  disabled={disabled}
51
  onClick={(event) => {
52
  if (disabled) {
packages/bolt/app/components/ui/PanelHeader.tsx CHANGED
@@ -8,7 +8,12 @@ interface PanelHeaderProps {
8
 
9
  export const PanelHeader = memo(({ className, children }: PanelHeaderProps) => {
10
  return (
11
- <div className={classNames('flex items-center gap-2 bg-gray-50 border-b px-4 py-1 min-h-[34px]', className)}>
 
 
 
 
 
12
  {children}
13
  </div>
14
  );
 
8
 
9
  export const PanelHeader = memo(({ className, children }: PanelHeaderProps) => {
10
  return (
11
+ <div
12
+ className={classNames(
13
+ 'flex items-center gap-2 bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor px-4 py-1 min-h-[34px] text-sm',
14
+ className,
15
+ )}
16
+ >
17
  {children}
18
  </div>
19
  );
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 bg-transparent hover:bg-white disabled:cursor-not-allowed',
18
  {
19
  [classNames('opacity-30', disabledClassName)]: disabled,
20
  },
 
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
  },
packages/bolt/app/components/ui/Slider.tsx CHANGED
@@ -23,7 +23,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 border rounded-lg p-1">
27
  <SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
28
  {options.left.text}
29
  </SliderButton>
@@ -45,8 +45,10 @@ const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProp
45
  <button
46
  onClick={setSelected}
47
  className={classNames(
48
- 'bg-transparent text-sm transition-colors px-2.5 py-0.5 rounded-md relative',
49
- selected ? 'text-white' : 'text-gray-600 hover:text-accent-600 hover:bg-accent-600/10',
 
 
50
  )}
51
  >
52
  <span className="relative z-10">{children}</span>
@@ -54,7 +56,7 @@ const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProp
54
  <motion.span
55
  layoutId="pill-tab"
56
  transition={{ type: 'spring', duration: 0.5 }}
57
- className="absolute inset-0 z-0 bg-accent-600 rounded-md"
58
  ></motion.span>
59
  )}
60
  </button>
 
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>
 
45
  <button
46
  onClick={setSelected}
47
  className={classNames(
48
+ 'bg-transparent text-sm px-2.5 py-0.5 rounded-full relative',
49
+ selected
50
+ ? 'text-bolt-elements-item-contentAccent'
51
+ : 'text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive',
52
  )}
53
  >
54
  <span className="relative z-10">{children}</span>
 
56
  <motion.span
57
  layoutId="pill-tab"
58
  transition={{ type: 'spring', duration: 0.5 }}
59
+ className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
60
  ></motion.span>
61
  )}
62
  </button>
packages/bolt/app/components/ui/ThemeSwitch.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useStore } from '@nanostores/react';
2
+ import { memo, useEffect, useState } from 'react';
3
+ import { themeStore, toggleTheme } from '~/lib/stores/theme';
4
+ import { IconButton } from './IconButton';
5
+
6
+ interface ThemeSwitchProps {
7
+ className?: string;
8
+ }
9
+
10
+ export const ThemeSwitch = memo(({ className }: ThemeSwitchProps) => {
11
+ const theme = useStore(themeStore);
12
+ const [domLoaded, setDomLoaded] = useState(false);
13
+
14
+ useEffect(() => {
15
+ setDomLoaded(true);
16
+ }, []);
17
+
18
+ return (
19
+ domLoaded && (
20
+ <IconButton
21
+ className={className}
22
+ icon={theme === 'dark' ? 'i-ph-sun-dim-duotone' : 'i-ph-moon-stars-duotone'}
23
+ size="xl"
24
+ title="Toggle Theme"
25
+ onClick={toggleTheme}
26
+ />
27
+ )
28
+ );
29
+ });
packages/bolt/app/components/workbench/EditorPanel.tsx CHANGED
@@ -125,7 +125,7 @@ export const EditorPanel = memo(
125
  <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
126
  <PanelGroup direction="horizontal">
127
  <Panel defaultSize={25} minSize={10} collapsible>
128
- <div className="flex flex-col border-r h-full">
129
  <PanelHeader>
130
  <div className="i-ph:tree-structure-duotone shrink-0" />
131
  Files
@@ -143,6 +143,7 @@ export const EditorPanel = memo(
143
  <PanelHeader>
144
  {activeFile && (
145
  <div className="flex items-center flex-1 text-sm">
 
146
  {activeFile} {isStreaming && <span className="text-xs ml-1 font-semibold">(read-only)</span>}
147
  {activeFileUnsaved && (
148
  <div className="flex gap-1 ml-auto -mr-1.5">
@@ -191,50 +192,58 @@ export const EditorPanel = memo(
191
  }
192
  }}
193
  >
194
- <div className="border-t h-full flex flex-col">
195
- <div className="flex items-center bg-gray-50 min-h-[34px]">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  {Array.from({ length: terminalCount }, (_, index) => {
197
  const isActive = activeTerminal === index;
198
 
199
  return (
200
- <button
201
  key={index}
202
- className={classNames(
203
- 'flex items-center text-sm bg-transparent cursor-pointer gap-1.5 px-3.5 h-full whitespace-nowrap',
204
- {
205
- 'bg-white': isActive,
206
- 'hover:bg-gray-100': !isActive,
207
- },
208
- )}
209
- onClick={() => setActiveTerminal(index)}
210
- >
211
- <div className="i-ph:terminal-window-duotone text-md" />
212
- Terminal {terminalCount > 1 && index + 1}
213
- </button>
214
  );
215
  })}
216
- {terminalCount < MAX_TERMINALS && (
217
- <IconButton className="ml-2" icon="i-ph:plus" size="md" onClick={addTerminal} />
218
- )}
219
  </div>
220
- {Array.from({ length: terminalCount }, (_, index) => {
221
- const isActive = activeTerminal === index;
222
-
223
- return (
224
- <Terminal
225
- key={index}
226
- className={classNames('h-full overflow-hidden', {
227
- hidden: !isActive,
228
- })}
229
- ref={(ref) => {
230
- terminalRefs.current.push(ref);
231
- }}
232
- onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
233
- onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
234
- theme={theme}
235
- />
236
- );
237
- })}
238
  </div>
239
  </Panel>
240
  </PanelGroup>
 
125
  <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
126
  <PanelGroup direction="horizontal">
127
  <Panel defaultSize={25} minSize={10} collapsible>
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
 
143
  <PanelHeader>
144
  {activeFile && (
145
  <div className="flex items-center flex-1 text-sm">
146
+ <div className="i-ph:file-duotone mr-2" />
147
  {activeFile} {isStreaming && <span className="text-xs ml-1 font-semibold">(read-only)</span>}
148
  {activeFileUnsaved && (
149
  <div className="flex gap-1 ml-auto -mr-1.5">
 
192
  }
193
  }}
194
  >
195
+ <div className="h-full">
196
+ <div className="bg-bolt-elements-terminals-background h-full flex flex-col">
197
+ <div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
198
+ {Array.from({ length: terminalCount }, (_, index) => {
199
+ const isActive = activeTerminal === index;
200
+
201
+ return (
202
+ <button
203
+ key={index}
204
+ className={classNames(
205
+ 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
206
+ {
207
+ 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
208
+ 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
209
+ !isActive,
210
+ },
211
+ )}
212
+ onClick={() => setActiveTerminal(index)}
213
+ >
214
+ <div className="i-ph:terminal-window-duotone text-lg" />
215
+ Terminal {terminalCount > 1 && index + 1}
216
+ </button>
217
+ );
218
+ })}
219
+ {terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
220
+ <IconButton
221
+ className="ml-auto"
222
+ icon="i-ph:caret-down"
223
+ title="Close"
224
+ size="md"
225
+ onClick={() => workbenchStore.toggleTerminal(false)}
226
+ />
227
+ </div>
228
  {Array.from({ length: terminalCount }, (_, index) => {
229
  const isActive = activeTerminal === index;
230
 
231
  return (
232
+ <Terminal
233
  key={index}
234
+ className={classNames('h-full overflow-hidden', {
235
+ hidden: !isActive,
236
+ })}
237
+ ref={(ref) => {
238
+ terminalRefs.current.push(ref);
239
+ }}
240
+ onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
241
+ onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
242
+ theme={theme}
243
+ />
 
 
244
  );
245
  })}
 
 
 
246
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  </div>
248
  </Panel>
249
  </PanelGroup>
packages/bolt/app/components/workbench/FileTree.tsx CHANGED
@@ -3,7 +3,7 @@ import type { FileMap } from '~/lib/stores/files';
3
  import { classNames } from '~/utils/classNames';
4
  import { renderLogger } from '~/utils/logger';
5
 
6
- const NODE_PADDING_LEFT = 12;
7
  const DEFAULT_HIDDEN_FILES = [/\/node_modules\//, /\/\.next/, /\/\.astro/];
8
 
9
  interface Props {
@@ -86,7 +86,7 @@ export const FileTree = memo(
86
  };
87
 
88
  return (
89
- <div className={className}>
90
  {filteredFileList.map((fileOrFolder) => {
91
  switch (fileOrFolder.kind) {
92
  case 'file': {
@@ -135,7 +135,7 @@ interface FolderProps {
135
  function Folder({ folder: { depth, name }, collapsed, onClick }: FolderProps) {
136
  return (
137
  <NodeButton
138
- className="group bg-white hover:bg-gray-50 text-md"
139
  depth={depth}
140
  iconClasses={classNames({
141
  'i-ph:caret-right scale-98': collapsed,
@@ -159,18 +159,22 @@ function File({ file: { depth, name }, onClick, selected, unsavedChanges = false
159
  return (
160
  <NodeButton
161
  className={classNames('group', {
162
- 'bg-white hover:bg-gray-50': !selected,
163
- 'bg-gray-100': selected,
164
  })}
165
  depth={depth}
166
  iconClasses={classNames('i-ph:file-duotone scale-98', {
167
- 'text-gray-600': !selected,
168
  })}
169
  onClick={onClick}
170
  >
171
- <div className="flex items-center">
 
 
 
 
172
  <div className="flex-1 truncate pr-2">{name}</div>
173
- {unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-warning-400" />}
174
  </div>
175
  </NodeButton>
176
  );
@@ -187,8 +191,11 @@ interface ButtonProps {
187
  function NodeButton({ depth, iconClasses, onClick, className, children }: ButtonProps) {
188
  return (
189
  <button
190
- className={`flex items-center gap-1.5 w-full pr-2 border-2 border-transparent text-faded ${className ?? ''}`}
191
- style={{ paddingLeft: `${12 + depth * NODE_PADDING_LEFT}px` }}
 
 
 
192
  onClick={() => onClick?.()}
193
  >
194
  <div className={classNames('scale-120 shrink-0', iconClasses)}></div>
 
3
  import { classNames } from '~/utils/classNames';
4
  import { renderLogger } from '~/utils/logger';
5
 
6
+ const NODE_PADDING_LEFT = 8;
7
  const DEFAULT_HIDDEN_FILES = [/\/node_modules\//, /\/\.next/, /\/\.astro/];
8
 
9
  interface Props {
 
86
  };
87
 
88
  return (
89
+ <div className={classNames('text-sm', className)}>
90
  {filteredFileList.map((fileOrFolder) => {
91
  switch (fileOrFolder.kind) {
92
  case 'file': {
 
135
  function Folder({ folder: { depth, name }, collapsed, onClick }: FolderProps) {
136
  return (
137
  <NodeButton
138
+ className="group bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive"
139
  depth={depth}
140
  iconClasses={classNames({
141
  'i-ph:caret-right scale-98': collapsed,
 
159
  return (
160
  <NodeButton
161
  className={classNames('group', {
162
+ 'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault': !selected,
163
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
164
  })}
165
  depth={depth}
166
  iconClasses={classNames('i-ph:file-duotone scale-98', {
167
+ 'group-hover:text-bolt-elements-item-contentActive': !selected,
168
  })}
169
  onClick={onClick}
170
  >
171
+ <div
172
+ className={classNames('flex items-center', {
173
+ 'group-hover:text-bolt-elements-item-contentActive': !selected,
174
+ })}
175
+ >
176
  <div className="flex-1 truncate pr-2">{name}</div>
177
+ {unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
178
  </div>
179
  </NodeButton>
180
  );
 
191
  function NodeButton({ depth, iconClasses, onClick, className, children }: ButtonProps) {
192
  return (
193
  <button
194
+ className={classNames(
195
+ 'flex items-center gap-1.5 w-full pr-2 border-2 border-transparent text-faded py-0.5',
196
+ className,
197
+ )}
198
+ style={{ paddingLeft: `${6 + depth * NODE_PADDING_LEFT}px` }}
199
  onClick={() => onClick?.()}
200
  >
201
  <div className={classNames('scale-120 shrink-0', iconClasses)}></div>
packages/bolt/app/components/workbench/Preview.tsx CHANGED
@@ -56,12 +56,12 @@ export const Preview = memo(() => {
56
 
57
  return (
58
  <div className="w-full h-full flex flex-col">
59
- <div className="bg-white p-2 flex items-center gap-1.5">
60
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
61
- <div className="flex items-center gap-1 flex-grow bg-gray-100 rounded-full px-3 py-1 text-sm text-gray-600 hover:bg-gray-200 hover:focus-within:bg-white focus-within:bg-white focus-within:ring-2 focus-within:ring-accent">
62
- <div className="bg-white rounded-full p-[2px] -ml-1">
63
- <div className="i-ph:info-bold text-lg" />
64
- </div>
65
  <input
66
  ref={inputRef}
67
  className="w-full bg-transparent outline-none"
 
56
 
57
  return (
58
  <div className="w-full h-full flex flex-col">
59
+ <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
60
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
61
+ <div
62
+ className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
63
+ focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
64
+ >
65
  <input
66
  ref={inputRef}
67
  className="w-full bg-transparent outline-none"
packages/bolt/app/components/workbench/Workbench.client.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
  type OnScrollCallback as OnEditorScroll,
9
  } from '~/components/editor/codemirror/CodeMirrorEditor';
10
  import { IconButton } from '~/components/ui/IconButton';
 
11
  import { Slider, type SliderOptions } from '~/components/ui/Slider';
12
  import { workbenchStore } from '~/lib/stores/workbench';
13
  import { cubicEasingFn } from '~/utils/easings';
@@ -100,13 +101,22 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
100
  chatStarted && (
101
  <motion.div initial="closed" animate={showWorkbench ? 'open' : 'closed'} variants={workbenchVariants}>
102
  <div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0">
103
- <div className="flex flex-col bg-white border border-gray-200 shadow-sm rounded-lg overflow-hidden absolute inset-0 right-8">
104
- <div className="flex items-center px-3 py-2 border-b border-gray-200">
105
  <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
 
 
 
 
 
 
 
 
 
106
  <IconButton
107
  icon="i-ph:x-circle"
108
- className="ml-auto -mr-1"
109
- size="xxl"
110
  onClick={() => {
111
  workbenchStore.showWorkbench.set(false);
112
  }}
 
8
  type OnScrollCallback as OnEditorScroll,
9
  } from '~/components/editor/codemirror/CodeMirrorEditor';
10
  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 } from '~/lib/stores/workbench';
14
  import { cubicEasingFn } from '~/utils/easings';
 
101
  chatStarted && (
102
  <motion.div initial="closed" animate={showWorkbench ? 'open' : 'closed'} variants={workbenchVariants}>
103
  <div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0">
104
+ <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">
105
+ <div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
106
  <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
107
+ <PanelHeaderButton
108
+ className="ml-auto mr-1 text-sm"
109
+ onClick={() => {
110
+ workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
111
+ }}
112
+ >
113
+ <div className="i-ph:terminal" />
114
+ Toggle Terminal
115
+ </PanelHeaderButton>
116
  <IconButton
117
  icon="i-ph:x-circle"
118
+ className="-mr-1"
119
+ size="xl"
120
  onClick={() => {
121
  workbenchStore.showWorkbench.set(false);
122
  }}
packages/bolt/app/components/workbench/terminal/Terminal.tsx CHANGED
@@ -36,7 +36,7 @@ export const Terminal = memo(
36
  convertEol: true,
37
  disableStdin: readonly,
38
  theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
39
- fontSize: 13,
40
  fontFamily: 'Menlo, courier-new, courier, monospace',
41
  });
42
 
 
36
  convertEol: true,
37
  disableStdin: readonly,
38
  theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
39
+ fontSize: 12,
40
  fontFamily: 'Menlo, courier-new, courier, monospace',
41
  });
42
 
packages/bolt/app/root.tsx CHANGED
@@ -17,9 +17,9 @@ export const links: LinksFunction = () => [
17
  href: '/favicon.svg',
18
  type: 'image/svg+xml',
19
  },
 
20
  { rel: 'stylesheet', href: tailwindReset },
21
  { rel: 'stylesheet', href: globalStyles },
22
- { rel: 'stylesheet', href: reactToastifyStyles },
23
  { rel: 'stylesheet', href: xtermStyles },
24
  {
25
  rel: 'preconnect',
 
17
  href: '/favicon.svg',
18
  type: 'image/svg+xml',
19
  },
20
+ { rel: 'stylesheet', href: reactToastifyStyles },
21
  { rel: 'stylesheet', href: tailwindReset },
22
  { rel: 'stylesheet', href: globalStyles },
 
23
  { rel: 'stylesheet', href: xtermStyles },
24
  {
25
  rel: 'preconnect',
packages/bolt/app/styles/components/code.scss ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ .actions .shiki {
2
+ background-color: var(--bolt-elements-actions-code-background) !important;
3
+ }
4
+
5
+ .shiki {
6
+ &:not(:has(.actions), .actions *) {
7
+ background-color: var(--bolt-elements-messages-code-background) !important;
8
+ }
9
+ }
packages/bolt/app/{components/editor/codemirror/styles.css → styles/components/editor.scss} RENAMED
@@ -1,11 +1,11 @@
1
  :root {
2
- --cm-backgroundColor: var(--bolt-elements-editor-backgroundColor, var(--bolt-elements-app-backgroundColor));
3
- --cm-textColor: var(--bolt-elements-editor-textColor, var(--bolt-text-primary));
4
 
5
  /* Gutter */
6
 
7
  --cm-gutter-backgroundColor: var(--bolt-elements-editor-gutter-backgroundColor, var(--cm-backgroundColor));
8
- --cm-gutter-textColor: var(--bolt-elements-editor-gutter-textColor, var(--bolt-text-secondary));
9
  --cm-gutter-activeLineTextColor: var(--bolt-elements-editor-gutter-activeLineTextColor, var(--cm-gutter-textColor));
10
 
11
  /* Fold Gutter */
@@ -20,7 +20,7 @@
20
  /* Cursor */
21
 
22
  --cm-cursor-width: 2px;
23
- --cm-cursor-backgroundColor: var(--bolt-elements-editor-cursorColor, var(--bolt-text-primary));
24
 
25
  /* Matching Brackets */
26
 
@@ -35,99 +35,101 @@
35
 
36
  /* Panels */
37
 
38
- --cm-panels-borderColor: var(--bolt-elements-editor-panels-borderColor, var(--bolt-elements-app-borderColor));
39
 
40
  /* Search */
41
 
42
  --cm-search-backgroundColor: var(--bolt-elements-editor-search-backgroundColor, var(--cm-backgroundColor));
43
- --cm-search-textColor: var(--bolt-elements-editor-search-textColor, var(--bolt-elements-app-textColor));
44
  --cm-search-closeButton-backgroundColor: var(--bolt-elements-editor-search-closeButton-backgroundColor, transparent);
45
 
46
  --cm-search-closeButton-backgroundColorHover: var(
47
  --bolt-elements-editor-search-closeButton-backgroundColorHover,
48
- var(--bolt-background-secondary)
49
  );
50
 
51
  --cm-search-closeButton-textColor: var(
52
  --bolt-elements-editor-search-closeButton-textColor,
53
- var(--bolt-text-secondary)
54
  );
55
 
56
  --cm-search-closeButton-textColorHover: var(
57
  --bolt-elements-editor-search-closeButton-textColorHover,
58
- var(--bolt-text-primary)
59
  );
60
 
61
  --cm-search-button-backgroundColor: var(
62
  --bolt-elements-editor-search-button-backgroundColor,
63
- var(--bolt-background-secondary)
64
  );
65
 
66
  --cm-search-button-backgroundColorHover: var(
67
  --bolt-elements-editor-search-button-backgroundColorHover,
68
- var(--bolt-background-active)
69
  );
70
 
71
- --cm-search-button-textColor: var(--bolt-elements-editor-search-button-textColor, var(--bolt-text-secondary));
72
- --cm-search-button-textColorHover: var(--bolt-elements-editor-search-button-textColorHover, var(--bolt-text-primary));
73
- --cm-search-button-borderColor: var(--bolt-elements-editor-search-button-borderColor, transparent);
74
 
75
- --cm-search-button-borderColorHover: var(
76
- --bolt-elements-editor-search-button-borderColorHover,
77
- var(--cm-search-button-borderColor)
78
  );
79
 
 
 
 
80
  --cm-search-button-borderColorFocused: var(
81
  --bolt-elements-editor-search-button-borderColorFocused,
82
- var(--bolt-border-accent)
83
  );
84
 
85
- --cm-search-input-backgroundColor: var(
86
- --bolt-elements-editor-search-input-backgroundColor,
87
- var(--bolt-background-primary)
88
- );
89
-
90
- --cm-search-input-borderColor: var(
91
- --bolt-elements-editor-search-input-borderColor,
92
- var(--bolt-elements-app-borderColor)
93
- );
94
 
95
  --cm-search-input-borderColorFocused: var(
96
  --bolt-elements-editor-search-input-borderColorFocused,
97
- var(--bolt-border-accent)
98
  );
99
 
100
  /* Tooltip */
101
 
102
- --cm-tooltip-backgroundColor: var(
103
- --bolt-elements-editor-tooltip-backgroundColor,
104
- var(--bolt-elements-app-backgroundColor)
105
- );
106
-
107
- --cm-tooltip-textColor: var(--bolt-elements-editor-tooltip-textColor, var(--bolt-text-primary));
108
 
109
  --cm-tooltip-backgroundColorSelected: var(
110
  --bolt-elements-editor-tooltip-backgroundColorSelected,
111
- var(--bolt-background-accent)
112
  );
113
 
114
  --cm-tooltip-textColorSelected: var(
115
  --bolt-elements-editor-tooltip-textColorSelected,
116
- var(--bolt-text-primary-inverted)
117
  );
118
 
119
- --cm-tooltip-borderColor: var(--bolt-elements-editor-tooltip-borderColor, var(--bolt-elements-app-borderColor));
 
 
120
  }
121
 
122
  html[data-theme='light'] {
123
  --bolt-elements-editor-gutter-textColor: #237893;
124
- --bolt-elements-editor-gutter-activeLineTextColor: var(--bolt-text-primary);
125
- --bolt-elements-editor-foldGutter-textColorHover: var(--bolt-text-primary);
 
 
 
 
126
  }
127
 
128
  html[data-theme='dark'] {
129
- --bolt-elements-editor-gutter-activeLineTextColor: var(--bolt-text-primary);
130
- --bolt-elements-editor-selection-backgroundOpacityBlured: 0.1;
 
 
131
  --bolt-elements-editor-activeLineBackgroundColor: rgb(50 53 63 / 50%);
132
- --bolt-elements-editor-foldGutter-textColorHover: var(--bolt-text-primary);
 
 
 
133
  }
 
1
  :root {
2
+ --cm-backgroundColor: var(--bolt-elements-editor-backgroundColor, var(--bolt-elements-bg-depth-1));
3
+ --cm-textColor: var(--bolt-elements-editor-textColor, var(--bolt-elements-textPrimary));
4
 
5
  /* Gutter */
6
 
7
  --cm-gutter-backgroundColor: var(--bolt-elements-editor-gutter-backgroundColor, var(--cm-backgroundColor));
8
+ --cm-gutter-textColor: var(--bolt-elements-editor-gutter-textColor, var(--bolt-elements-textSecondary));
9
  --cm-gutter-activeLineTextColor: var(--bolt-elements-editor-gutter-activeLineTextColor, var(--cm-gutter-textColor));
10
 
11
  /* Fold Gutter */
 
20
  /* Cursor */
21
 
22
  --cm-cursor-width: 2px;
23
+ --cm-cursor-backgroundColor: var(--bolt-elements-editor-cursorColor, var(--bolt-elements-textSecondary));
24
 
25
  /* Matching Brackets */
26
 
 
35
 
36
  /* Panels */
37
 
38
+ --cm-panels-borderColor: var(--bolt-elements-editor-panels-borderColor, var(--bolt-elements-borderColor));
39
 
40
  /* Search */
41
 
42
  --cm-search-backgroundColor: var(--bolt-elements-editor-search-backgroundColor, var(--cm-backgroundColor));
43
+ --cm-search-textColor: var(--bolt-elements-editor-search-textColor, var(--bolt-elements-textSecondary));
44
  --cm-search-closeButton-backgroundColor: var(--bolt-elements-editor-search-closeButton-backgroundColor, transparent);
45
 
46
  --cm-search-closeButton-backgroundColorHover: var(
47
  --bolt-elements-editor-search-closeButton-backgroundColorHover,
48
+ var(--bolt-elements-item-backgroundActive)
49
  );
50
 
51
  --cm-search-closeButton-textColor: var(
52
  --bolt-elements-editor-search-closeButton-textColor,
53
+ var(--bolt-elements-item-contentDefault)
54
  );
55
 
56
  --cm-search-closeButton-textColorHover: var(
57
  --bolt-elements-editor-search-closeButton-textColorHover,
58
+ var(--bolt-elements-item-contentActive)
59
  );
60
 
61
  --cm-search-button-backgroundColor: var(
62
  --bolt-elements-editor-search-button-backgroundColor,
63
+ var(--bolt-elements-item-backgroundDefault)
64
  );
65
 
66
  --cm-search-button-backgroundColorHover: var(
67
  --bolt-elements-editor-search-button-backgroundColorHover,
68
+ var(--bolt-elements-item-backgroundActive)
69
  );
70
 
71
+ --cm-search-button-textColor: var(--bolt-elements-editor-search-button-textColor, var(--bolt-elements-textSecondary));
 
 
72
 
73
+ --cm-search-button-textColorHover: var(
74
+ --bolt-elements-editor-search-button-textColorHover,
75
+ var(--bolt-elements-textPrimary)
76
  );
77
 
78
+ --cm-search-button-borderColor: var(--bolt-elements-editor-search-button-borderColor, transparent);
79
+ --cm-search-button-borderColorHover: var(--bolt-elements-editor-search-button-borderColorHover, transparent);
80
+
81
  --cm-search-button-borderColorFocused: var(
82
  --bolt-elements-editor-search-button-borderColorFocused,
83
+ var(--bolt-elements-borderColorActive)
84
  );
85
 
86
+ --cm-search-input-backgroundColor: var(--bolt-elements-editor-search-input-backgroundColor, transparent);
87
+ --cm-search-input-textColor: var(--bolt-elements-editor-search-input-textColor, var(--bolt-elements-textPrimary));
88
+ --cm-search-input-borderColor: var(--bolt-elements-editor-search-input-borderColor, var(--bolt-elements-borderColor));
 
 
 
 
 
 
89
 
90
  --cm-search-input-borderColorFocused: var(
91
  --bolt-elements-editor-search-input-borderColorFocused,
92
+ var(--bolt-elements-borderColorActive)
93
  );
94
 
95
  /* Tooltip */
96
 
97
+ --cm-tooltip-backgroundColor: var(--bolt-elements-editor-tooltip-backgroundColor, var(--cm-backgroundColor));
98
+ --cm-tooltip-textColor: var(--bolt-elements-editor-tooltip-textColor, var(--bolt-elements-textPrimary));
 
 
 
 
99
 
100
  --cm-tooltip-backgroundColorSelected: var(
101
  --bolt-elements-editor-tooltip-backgroundColorSelected,
102
+ theme('colors.alpha.accent.30')
103
  );
104
 
105
  --cm-tooltip-textColorSelected: var(
106
  --bolt-elements-editor-tooltip-textColorSelected,
107
+ var(--bolt-elements-textPrimary)
108
  );
109
 
110
+ --cm-tooltip-borderColor: var(--bolt-elements-editor-tooltip-borderColor, var(--bolt-elements-borderColor));
111
+
112
+ --cm-searchMatch-backgroundColor: var(--bolt-elements-editor-searchMatch-backgroundColor, rgba(234, 92, 0, 0.33));
113
  }
114
 
115
  html[data-theme='light'] {
116
  --bolt-elements-editor-gutter-textColor: #237893;
117
+ --bolt-elements-editor-gutter-activeLineTextColor: var(--bolt-elements-textPrimary);
118
+ --bolt-elements-editor-foldGutter-textColorHover: var(--bolt-elements-textPrimary);
119
+ --bolt-elements-editor-activeLineBackgroundColor: rgb(50 53 63 / 5%);
120
+ --bolt-elements-editor-tooltip-backgroundColorSelected: theme('colors.alpha.accent.20');
121
+ --bolt-elements-editor-search-button-backgroundColor: theme('colors.gray.100');
122
+ --bolt-elements-editor-search-button-backgroundColorHover: theme('colors.alpha.gray.10');
123
  }
124
 
125
  html[data-theme='dark'] {
126
+ --cm-backgroundColor: var(--bolt-elements-bg-depth-2);
127
+ --bolt-elements-editor-gutter-textColor: var(--bolt-elements-textTertiary);
128
+ --bolt-elements-editor-gutter-activeLineTextColor: var(--bolt-elements-textSecondary);
129
+ --bolt-elements-editor-selection-inactiveBackgroundOpacity: 0.3;
130
  --bolt-elements-editor-activeLineBackgroundColor: rgb(50 53 63 / 50%);
131
+ --bolt-elements-editor-foldGutter-textColorHover: var(--bolt-elements-textPrimary);
132
+ --bolt-elements-editor-matchingBracketBackgroundColor: rgba(66, 180, 255, 0.3);
133
+ --bolt-elements-editor-search-button-backgroundColor: theme('colors.gray.800');
134
+ --bolt-elements-editor-search-button-backgroundColorHover: theme('colors.alpha.white.10');
135
  }
packages/bolt/app/styles/components/toast.scss ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .Toastify__toast {
2
+ --at-apply: shadow-md;
3
+
4
+ background-color: var(--bolt-elements-bg-depth-2);
5
+ color: var(--bolt-elements-textPrimary);
6
+ border: 1px solid var(--bolt-elements-borderColor);
7
+ }
8
+
9
+ .Toastify__close-button {
10
+ color: var(--bolt-elements-item-contentDefault);
11
+ opacity: 1;
12
+ transition: none;
13
+
14
+ &:hover {
15
+ color: var(--bolt-elements-item-contentActive);
16
+ }
17
+ }
packages/bolt/app/styles/index.scss CHANGED
@@ -3,6 +3,9 @@
3
  @import './animations.scss';
4
  @import './components/terminal.scss';
5
  @import './components/resize-handle.scss';
 
 
 
6
 
7
  html,
8
  body {
 
3
  @import './animations.scss';
4
  @import './components/terminal.scss';
5
  @import './components/resize-handle.scss';
6
+ @import './components/code.scss';
7
+ @import './components/editor.scss';
8
+ @import './components/toast.scss';
9
 
10
  html,
11
  body {
packages/bolt/app/styles/variables.scss CHANGED
@@ -1,28 +1,77 @@
1
  /* Color Tokens Light Theme */
2
  :root,
3
  :root[data-theme='light'] {
4
- --bolt-background-primary: theme('colors.gray.0');
5
- --bolt-background-secondary: theme('colors.gray.50');
6
- --bolt-background-active: theme('colors.gray.200');
7
- --bolt-background-accent: theme('colors.accent.600');
8
- --bolt-background-accent-secondary: theme('colors.accent.600');
9
- --bolt-background-accent-active: theme('colors.accent.500');
10
-
11
- --bolt-text-primary: theme('colors.gray.800');
12
- --bolt-text-primary-inverted: theme('colors.gray.0');
13
- --bolt-text-secondary: theme('colors.gray.600');
14
- --bolt-text-secondary-inverted: theme('colors.gray.200');
15
- --bolt-text-disabled: theme('colors.gray.400');
16
- --bolt-text-accent: theme('colors.accent.600');
17
- --bolt-text-positive: theme('colors.positive.700');
18
- --bolt-text-warning: theme('colors.warning.600');
19
- --bolt-text-negative: theme('colors.negative.600');
20
-
21
- --bolt-border-primary: theme('colors.gray.200');
22
- --bolt-border-accent: theme('colors.accent.600');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  /* Terminal Colors */
25
- --bolt-terminal-background: var(--bolt-background-primary);
26
  --bolt-terminal-foreground: #333333;
27
  --bolt-terminal-selection-background: #00000040;
28
  --bolt-terminal-black: #000000;
@@ -46,28 +95,77 @@
46
  /* Color Tokens Dark Theme */
47
  :root,
48
  :root[data-theme='dark'] {
49
- --bolt-background-primary: theme('colors.gray.0');
50
- --bolt-background-secondary: theme('colors.gray.50');
51
- --bolt-background-active: theme('colors.gray.200');
52
- --bolt-background-accent: theme('colors.accent.600');
53
- --bolt-background-accent-secondary: theme('colors.accent.600');
54
- --bolt-background-accent-active: theme('colors.accent.500');
55
-
56
- --bolt-text-primary: theme('colors.gray.800');
57
- --bolt-text-primary-inverted: theme('colors.gray.0');
58
- --bolt-text-secondary: theme('colors.gray.600');
59
- --bolt-text-secondary-inverted: theme('colors.gray.200');
60
- --bolt-text-disabled: theme('colors.gray.400');
61
- --bolt-text-accent: theme('colors.accent.600');
62
- --bolt-text-positive: theme('colors.positive.700');
63
- --bolt-text-warning: theme('colors.warning.600');
64
- --bolt-text-negative: theme('colors.negative.600');
65
-
66
- --bolt-border-primary: theme('colors.gray.200');
67
- --bolt-border-accent: theme('colors.accent.600');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  /* Terminal Colors */
70
- --bolt-terminal-background: #16181d;
71
  --bolt-terminal-foreground: #eff0eb;
72
  --bolt-terminal-selection-background: #97979b33;
73
  --bolt-terminal-black: #000000;
@@ -94,13 +192,11 @@
94
  * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
95
  */
96
  :root {
97
- --header-height: 65px;
98
 
99
- /* App */
100
- --bolt-elements-app-backgroundColor: var(--bolt-background-primary);
101
- --bolt-elements-app-borderColor: var(--bolt-border-primary);
102
- --bolt-elements-app-textColor: var(--bolt-text-primary);
103
- --bolt-elements-app-linkColor: var(--bolt-text-accent);
104
 
105
  /* Terminal */
106
  --bolt-elements-terminal-backgroundColor: var(--bolt-terminal-background);
 
1
  /* Color Tokens Light Theme */
2
  :root,
3
  :root[data-theme='light'] {
4
+ --bolt-elements-borderColor: theme('colors.alpha.gray.10');
5
+ --bolt-elements-borderColorActive: theme('colors.accent.600');
6
+
7
+ --bolt-elements-bg-depth-1: theme('colors.white');
8
+ --bolt-elements-bg-depth-2: theme('colors.gray.50');
9
+ --bolt-elements-bg-depth-3: theme('colors.gray.200');
10
+ --bolt-elements-bg-depth-4: theme('colors.alpha.gray.5');
11
+
12
+ --bolt-elements-textPrimary: theme('colors.gray.950');
13
+ --bolt-elements-textSecondary: theme('colors.gray.600');
14
+ --bolt-elements-textTertiary: theme('colors.gray.500');
15
+
16
+ --bolt-elements-code-background: theme('colors.gray.100');
17
+ --bolt-elements-code-text: theme('colors.gray.950');
18
+
19
+ --bolt-elements-item-contentDefault: theme('colors.alpha.gray.50');
20
+ --bolt-elements-item-contentActive: theme('colors.gray.950');
21
+ --bolt-elements-item-contentAccent: theme('colors.accent.700');
22
+ --bolt-elements-item-contentDanger: theme('colors.red.500');
23
+ --bolt-elements-item-backgroundDefault: rgba(0, 0, 0, 0);
24
+ --bolt-elements-item-backgroundActive: theme('colors.alpha.gray.5');
25
+ --bolt-elements-item-backgroundAccent: theme('colors.alpha.accent.10');
26
+ --bolt-elements-item-backgroundDanger: theme('colors.alpha.red.10');
27
+
28
+ --bolt-elements-loader-background: theme('colors.alpha.gray.10');
29
+ --bolt-elements-loader-progress: theme('colors.accent.500');
30
+
31
+ --bolt-elements-artifacts-background: theme('colors.white');
32
+ --bolt-elements-artifacts-backgroundHover: theme('colors.alpha.gray.2');
33
+ --bolt-elements-artifacts-borderColor: var(--bolt-elements-borderColor);
34
+ --bolt-elements-artifacts-inlineCode-background: theme('colors.gray.100');
35
+ --bolt-elements-artifacts-inlineCode-text: var(--bolt-elements-textPrimary);
36
+
37
+ --bolt-elements-actions-background: theme('colors.white');
38
+ --bolt-elements-actions-code-background: theme('colors.gray.800');
39
+
40
+ --bolt-elements-messages-background: theme('colors.gray.100');
41
+ --bolt-elements-messages-linkColor: theme('colors.accent.500');
42
+ --bolt-elements-messages-code-background: theme('colors.gray.800');
43
+ --bolt-elements-messages-inlineCode-background: theme('colors.gray.200');
44
+ --bolt-elements-messages-inlineCode-text: theme('colors.gray.800');
45
+
46
+ --bolt-elements-icon-success: theme('colors.green.500');
47
+ --bolt-elements-icon-error: theme('colors.red.500');
48
+ --bolt-elements-icon-primary: theme('colors.gray.950');
49
+ --bolt-elements-icon-secondary: theme('colors.gray.600');
50
+ --bolt-elements-icon-tertiary: theme('colors.gray.500');
51
+
52
+ --bolt-elements-dividerColor: theme('colors.gray.100');
53
+
54
+ --bolt-elements-prompt-background: theme('colors.alpha.white.80');
55
+
56
+ --bolt-elements-sidebar-dropdownShadow: theme('colors.alpha.gray.10');
57
+ --bolt-elements-sidebar-buttonBackgroundDefault: theme('colors.alpha.accent.10');
58
+ --bolt-elements-sidebar-buttonBackgroundHover: theme('colors.alpha.accent.20');
59
+ --bolt-elements-sidebar-buttonText: theme('colors.accent.700');
60
+
61
+ --bolt-elements-preview-addressBar-background: theme('colors.gray.100');
62
+ --bolt-elements-preview-addressBar-backgroundHover: theme('colors.alpha.gray.5');
63
+ --bolt-elements-preview-addressBar-backgroundActive: theme('colors.white');
64
+ --bolt-elements-preview-addressBar-text: var(--bolt-elements-textSecondary);
65
+ --bolt-elements-preview-addressBar-textActive: var(--bolt-elements-textPrimary);
66
+
67
+ --bolt-elements-terminals-background: theme('colors.white');
68
+ --bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-4);
69
+
70
+ --bolt-elements-cta-background: theme('colors.gray.100');
71
+ --bolt-elements-cta-text: theme('colors.gray.950');
72
 
73
  /* Terminal Colors */
74
+ --bolt-terminal-background: var(--bolt-elements-terminals-background);
75
  --bolt-terminal-foreground: #333333;
76
  --bolt-terminal-selection-background: #00000040;
77
  --bolt-terminal-black: #000000;
 
95
  /* Color Tokens Dark Theme */
96
  :root,
97
  :root[data-theme='dark'] {
98
+ --bolt-elements-borderColor: theme('colors.alpha.white.10');
99
+ --bolt-elements-borderColorActive: theme('colors.accent.500');
100
+
101
+ --bolt-elements-bg-depth-1: theme('colors.gray.950');
102
+ --bolt-elements-bg-depth-2: theme('colors.gray.900');
103
+ --bolt-elements-bg-depth-3: theme('colors.gray.800');
104
+ --bolt-elements-bg-depth-4: theme('colors.alpha.white.5');
105
+
106
+ --bolt-elements-textPrimary: theme('colors.white');
107
+ --bolt-elements-textSecondary: theme('colors.gray.400');
108
+ --bolt-elements-textTertiary: theme('colors.gray.500');
109
+
110
+ --bolt-elements-code-background: theme('colors.gray.800');
111
+ --bolt-elements-code-text: theme('colors.white');
112
+
113
+ --bolt-elements-item-contentDefault: theme('colors.alpha.white.50');
114
+ --bolt-elements-item-contentActive: theme('colors.white');
115
+ --bolt-elements-item-contentAccent: theme('colors.accent.500');
116
+ --bolt-elements-item-contentDanger: theme('colors.red.500');
117
+ --bolt-elements-item-backgroundDefault: rgba(255, 255, 255, 0);
118
+ --bolt-elements-item-backgroundActive: theme('colors.alpha.white.10');
119
+ --bolt-elements-item-backgroundAccent: theme('colors.alpha.accent.10');
120
+ --bolt-elements-item-backgroundDanger: theme('colors.alpha.red.10');
121
+
122
+ --bolt-elements-loader-background: theme('colors.alpha.gray.10');
123
+ --bolt-elements-loader-progress: theme('colors.accent.500');
124
+
125
+ --bolt-elements-artifacts-background: theme('colors.gray.900');
126
+ --bolt-elements-artifacts-backgroundHover: theme('colors.alpha.white.5');
127
+ --bolt-elements-artifacts-borderColor: var(--bolt-elements-borderColor);
128
+ --bolt-elements-artifacts-inlineCode-background: theme('colors.gray.800');
129
+ --bolt-elements-artifacts-inlineCode-text: theme('colors.white');
130
+
131
+ --bolt-elements-actions-background: theme('colors.gray.900');
132
+ --bolt-elements-actions-code-background: theme('colors.gray.800');
133
+
134
+ --bolt-elements-messages-background: theme('colors.gray.800');
135
+ --bolt-elements-messages-linkColor: theme('colors.accent.500');
136
+ --bolt-elements-messages-code-background: theme('colors.gray.900');
137
+ --bolt-elements-messages-inlineCode-background: theme('colors.gray.700');
138
+ --bolt-elements-messages-inlineCode-text: var(--bolt-elements-textPrimary);
139
+
140
+ --bolt-elements-icon-success: theme('colors.green.400');
141
+ --bolt-elements-icon-error: theme('colors.red.400');
142
+ --bolt-elements-icon-primary: theme('colors.gray.950');
143
+ --bolt-elements-icon-secondary: theme('colors.gray.600');
144
+ --bolt-elements-icon-tertiary: theme('colors.gray.500');
145
+
146
+ --bolt-elements-dividerColor: theme('colors.gray.100');
147
+
148
+ --bolt-elements-prompt-background: theme('colors.alpha.gray.80');
149
+
150
+ --bolt-elements-sidebar-dropdownShadow: theme('colors.alpha.gray.30');
151
+ --bolt-elements-sidebar-buttonBackgroundDefault: theme('colors.alpha.accent.10');
152
+ --bolt-elements-sidebar-buttonBackgroundHover: theme('colors.alpha.accent.20');
153
+ --bolt-elements-sidebar-buttonText: theme('colors.accent.500');
154
+
155
+ --bolt-elements-preview-addressBar-background: var(--bolt-elements-bg-depth-1);
156
+ --bolt-elements-preview-addressBar-backgroundHover: theme('colors.alpha.white.5');
157
+ --bolt-elements-preview-addressBar-backgroundActive: var(--bolt-elements-bg-depth-1);
158
+ --bolt-elements-preview-addressBar-text: var(--bolt-elements-textSecondary);
159
+ --bolt-elements-preview-addressBar-textActive: var(--bolt-elements-textPrimary);
160
+
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.gray.100');
165
+ --bolt-elements-cta-text: theme('colors.gray.950');
166
 
167
  /* Terminal Colors */
168
+ --bolt-terminal-background: var(--bolt-elements-terminals-background);
169
  --bolt-terminal-foreground: #eff0eb;
170
  --bolt-terminal-selection-background: #97979b33;
171
  --bolt-terminal-black: #000000;
 
192
  * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
193
  */
194
  :root {
195
+ --header-height: 54px;
196
 
197
+ /* Toasts */
198
+ --toastify-color-progress-success: var(--bolt-elements-icon-success);
199
+ --toastify-color-progress-error: var(--bolt-elements-icon-error);
 
 
200
 
201
  /* Terminal */
202
  --bolt-elements-terminal-backgroundColor: var(--bolt-terminal-background);
packages/bolt/app/styles/z-index.scss CHANGED
@@ -1,5 +1,13 @@
1
  $zIndexMax: 999;
2
 
 
 
 
 
 
 
 
 
3
  .z-max {
4
  z-index: $zIndexMax;
5
  }
 
1
  $zIndexMax: 999;
2
 
3
+ .z-logo {
4
+ z-index: $zIndexMax - 1;
5
+ }
6
+
7
+ .z-sidebar {
8
+ z-index: $zIndexMax - 2;
9
+ }
10
+
11
  .z-max {
12
  z-index: $zIndexMax;
13
  }
packages/bolt/icons/logo-text.svg ADDED
packages/bolt/package.json CHANGED
@@ -40,6 +40,7 @@
40
  "@remix-run/cloudflare-pages": "^2.10.2",
41
  "@remix-run/react": "^2.10.2",
42
  "@stackblitz/sdk": "^1.11.0",
 
43
  "@unocss/reset": "^0.61.0",
44
  "@webcontainer/api": "^1.3.0-internal.2",
45
  "@xterm/addon-fit": "^0.10.0",
 
40
  "@remix-run/cloudflare-pages": "^2.10.2",
41
  "@remix-run/react": "^2.10.2",
42
  "@stackblitz/sdk": "^1.11.0",
43
+ "@uiw/codemirror-theme-vscode": "^4.23.0",
44
  "@unocss/reset": "^0.61.0",
45
  "@webcontainer/api": "^1.3.0-internal.2",
46
  "@xterm/addon-fit": "^0.10.0",
packages/bolt/public/logo_text.svg DELETED
packages/bolt/uno.config.ts CHANGED
@@ -5,7 +5,7 @@ import { defineConfig, presetIcons, presetUno, transformerDirectives } from 'uno
5
 
6
  const iconPaths = globSync('./icons/*.svg');
7
 
8
- const collectionName = 'blitz';
9
 
10
  const customIconCollection = iconPaths.reduce(
11
  (acc, iconPath) => {
@@ -19,101 +19,185 @@ const customIconCollection = iconPaths.reduce(
19
  {} as Record<string, Record<string, () => Promise<string>>>,
20
  );
21
 
22
- const COLOR_PRIMITIVES = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  accent: {
24
- DEFAULT: '#1389FD',
25
  50: '#EEF9FF',
26
  100: '#D8F1FF',
27
- 200: '#B9E7FF',
28
- 300: '#89DBFF',
29
- 400: '#52C5FF',
30
- 500: '#2AA7FF',
31
- 600: '#1389FD',
32
- 700: '#0C70E9',
33
- 800: '#115ABC',
34
- 900: '#144D94',
35
- 950: '#11305A',
36
  },
37
- gray: {
38
- 0: '#FFFFFF',
39
- 50: '#F6F8F9',
40
- 100: '#EEF0F1',
41
- 200: '#E4E6E9',
42
- 300: '#D2D5D9',
43
- 400: '#AAAFB6',
44
- 500: '#7C8085',
45
- 600: '#565A64',
46
- 700: '#414349',
47
- 800: '#31343B',
48
- 900: '#2B2D35',
49
- 950: '#232429',
50
- 1000: '#000000',
51
  },
52
- positive: {
53
- 50: '#EDFCF6',
54
- 100: '#CEFDEB',
55
- 200: '#A1F9DC',
56
- 300: '#64F1CB',
57
- 400: '#24E0B3',
58
- 500: '#02C79F',
59
- 600: '#00A282',
60
- 700: '#00826B',
61
- 800: '#006656',
62
- 900: '#005449',
63
- 950: '#223533',
64
  },
65
- negative: {
66
- 50: '#FEF2F3',
67
- 100: '#FDE6E7',
68
- 200: '#FBD0D4',
69
- 300: '#F7AAB1',
70
- 400: '#F06A78',
71
- 500: '#E84B60',
72
- 600: '#D42A48',
73
- 700: '#B21E3C',
74
- 800: '#951C38',
75
- 900: '#801B36',
76
- 950: '#45212A',
77
  },
78
- info: {
79
- 50: '#EFF9FF',
80
- 100: '#E5F6FF',
81
- 200: '#B6E9FF',
82
- 300: '#75DAFF',
83
- 400: '#2CC8FF',
84
- 500: '#00AEF2',
85
- 600: '#008ED4',
86
- 700: '#0071AB',
87
- 800: '#005F8D',
88
- 900: '#064F74',
89
- 950: '#17374A',
90
- },
91
- warning: {
92
- 50: '#FEFAEC',
93
- 100: '#FCF4D9',
94
- 200: '#F9E08E',
95
- 300: '#F6CA53',
96
- 400: '#ED9413',
97
- 500: '#D2700D',
98
- 600: '#AE4E0F',
99
- 700: '#AE4E0F',
100
- 800: '#8E3D12',
101
- 900: '#753212',
102
- 950: '#402C22',
103
  },
104
  };
105
 
106
  export default defineConfig({
 
 
 
 
 
107
  theme: {
108
  colors: {
109
  ...COLOR_PRIMITIVES,
110
  bolt: {
111
  elements: {
112
- app: {
113
- backgroundColor: 'var(--bolt-elements-app-backgroundColor)',
114
- borderColor: 'var(--bolt-elements-app-borderColor)',
115
- textColor: 'var(--bolt-elements-app-textColor)',
116
- linkColor: 'var(--bolt-elements-app-linkColor)',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  },
118
  },
119
  },
@@ -135,3 +219,34 @@ export default defineConfig({
135
  }),
136
  ],
137
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  const iconPaths = globSync('./icons/*.svg');
7
 
8
+ const collectionName = 'bolt';
9
 
10
  const customIconCollection = iconPaths.reduce(
11
  (acc, iconPath) => {
 
19
  {} as Record<string, Record<string, () => Promise<string>>>,
20
  );
21
 
22
+ const BASE_COLORS = {
23
+ white: '#FFFFFF',
24
+ gray: {
25
+ 50: '#FAFAFA',
26
+ 100: '#F5F5F5',
27
+ 200: '#E5E5E5',
28
+ 300: '#D4D4D4',
29
+ 400: '#A3A3A3',
30
+ 500: '#737373',
31
+ 600: '#525252',
32
+ 700: '#404040',
33
+ 800: '#262626',
34
+ 900: '#171717',
35
+ 950: '#0A0A0A',
36
+ },
37
  accent: {
 
38
  50: '#EEF9FF',
39
  100: '#D8F1FF',
40
+ 200: '#BAE7FF',
41
+ 300: '#8ADAFF',
42
+ 400: '#53C4FF',
43
+ 500: '#2BA6FF',
44
+ 600: '#1488FC',
45
+ 700: '#0D6FE8',
46
+ 800: '#1259BB',
47
+ 900: '#154E93',
48
+ 950: '#122F59',
49
  },
50
+ green: {
51
+ 50: '#F0FDF4',
52
+ 100: '#DCFCE7',
53
+ 200: '#BBF7D0',
54
+ 300: '#86EFAC',
55
+ 400: '#4ADE80',
56
+ 500: '#22C55E',
57
+ 600: '#16A34A',
58
+ 700: '#15803D',
59
+ 800: '#166534',
60
+ 900: '#14532D',
61
+ 950: '#052E16',
 
 
62
  },
63
+ orange: {
64
+ 50: '#FFFAEB',
65
+ 100: '#FEEFC7',
66
+ 200: '#FEDF89',
67
+ 300: '#FEC84B',
68
+ 400: '#FDB022',
69
+ 500: '#F79009',
70
+ 600: '#DC6803',
71
+ 700: '#B54708',
72
+ 800: '#93370D',
73
+ 900: '#792E0D',
 
74
  },
75
+ red: {
76
+ 50: '#FEF2F2',
77
+ 100: '#FEE2E2',
78
+ 200: '#FECACA',
79
+ 300: '#FCA5A5',
80
+ 400: '#F87171',
81
+ 500: '#EF4444',
82
+ 600: '#DC2626',
83
+ 700: '#B91C1C',
84
+ 800: '#991B1B',
85
+ 900: '#7F1D1D',
86
+ 950: '#450A0A',
87
  },
88
+ };
89
+
90
+ const COLOR_PRIMITIVES = {
91
+ ...BASE_COLORS,
92
+ alpha: {
93
+ white: generateAlphaPalette(BASE_COLORS.white),
94
+ gray: generateAlphaPalette(BASE_COLORS.gray[900]),
95
+ red: generateAlphaPalette(BASE_COLORS.red[500]),
96
+ accent: generateAlphaPalette(BASE_COLORS.accent[500]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  },
98
  };
99
 
100
  export default defineConfig({
101
+ shortcuts: {
102
+ 'transition-theme':
103
+ 'transition-[background-color,border-color,color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]',
104
+ kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
105
+ },
106
  theme: {
107
  colors: {
108
  ...COLOR_PRIMITIVES,
109
  bolt: {
110
  elements: {
111
+ borderColor: 'var(--bolt-elements-borderColor)',
112
+ borderColorActive: 'var(--bolt-elements-borderColorActive)',
113
+ background: {
114
+ depth: {
115
+ 1: 'var(--bolt-elements-bg-depth-1)',
116
+ 2: 'var(--bolt-elements-bg-depth-2)',
117
+ 3: 'var(--bolt-elements-bg-depth-3)',
118
+ 4: 'var(--bolt-elements-bg-depth-4)',
119
+ },
120
+ },
121
+ textPrimary: 'var(--bolt-elements-textPrimary)',
122
+ textSecondary: 'var(--bolt-elements-textSecondary)',
123
+ textTertiary: 'var(--bolt-elements-textTertiary)',
124
+ code: {
125
+ background: 'var(--bolt-elements-code-background)',
126
+ text: 'var(--bolt-elements-code-text)',
127
+ },
128
+ item: {
129
+ contentDefault: 'var(--bolt-elements-item-contentDefault)',
130
+ contentActive: 'var(--bolt-elements-item-contentActive)',
131
+ contentAccent: 'var(--bolt-elements-item-contentAccent)',
132
+ contentDanger: 'var(--bolt-elements-item-contentDanger)',
133
+ backgroundDefault: 'var(--bolt-elements-item-backgroundDefault)',
134
+ backgroundActive: 'var(--bolt-elements-item-backgroundActive)',
135
+ backgroundAccent: 'var(--bolt-elements-item-backgroundAccent)',
136
+ backgroundDanger: 'var(--bolt-elements-item-backgroundDanger)',
137
+ },
138
+ actions: {
139
+ background: 'var(--bolt-elements-actions-background)',
140
+ code: {
141
+ background: 'var(--bolt-elements-actions-code-background)',
142
+ },
143
+ },
144
+ artifacts: {
145
+ background: 'var(--bolt-elements-artifacts-background)',
146
+ backgroundHover: 'var(--bolt-elements-artifacts-backgroundHover)',
147
+ borderColor: 'var(--bolt-elements-artifacts-borderColor)',
148
+ inlineCode: {
149
+ background: 'var(--bolt-elements-artifacts-inlineCode-background)',
150
+ text: 'var(--bolt-elements-artifacts-inlineCode-text)',
151
+ },
152
+ },
153
+ messages: {
154
+ background: 'var(--bolt-elements-messages-background)',
155
+ linkColor: 'var(--bolt-elements-messages-linkColor)',
156
+ code: {
157
+ background: 'var(--bolt-elements-messages-code-background)',
158
+ },
159
+ inlineCode: {
160
+ background: 'var(--bolt-elements-messages-inlineCode-background)',
161
+ text: 'var(--bolt-elements-messages-inlineCode-text)',
162
+ },
163
+ },
164
+ icon: {
165
+ success: 'var(--bolt-elements-icon-success)',
166
+ error: 'var(--bolt-elements-icon-error)',
167
+ primary: 'var(--bolt-elements-icon-primary)',
168
+ secondary: 'var(--bolt-elements-icon-secondary)',
169
+ tertiary: 'var(--bolt-elements-icon-tertiary)',
170
+ },
171
+ preview: {
172
+ addressBar: {
173
+ background: 'var(--bolt-elements-preview-addressBar-background)',
174
+ backgroundHover: 'var(--bolt-elements-preview-addressBar-backgroundHover)',
175
+ backgroundActive: 'var(--bolt-elements-preview-addressBar-backgroundActive)',
176
+ text: 'var(--bolt-elements-preview-addressBar-text)',
177
+ textActive: 'var(--bolt-elements-preview-addressBar-textActive)',
178
+ },
179
+ },
180
+ terminals: {
181
+ background: 'var(--bolt-elements-terminals-background)',
182
+ buttonBackground: 'var(--bolt-elements-terminals-buttonBackground)',
183
+ },
184
+ dividerColor: 'var(--bolt-elements-dividerColor)',
185
+ loader: {
186
+ background: 'var(--bolt-elements-loader-background)',
187
+ progress: 'var(--bolt-elements-loader-progress)',
188
+ },
189
+ prompt: {
190
+ background: 'var(--bolt-elements-prompt-background)',
191
+ },
192
+ sidebar: {
193
+ dropdownShadow: 'var(--bolt-elements-sidebar-dropdownShadow)',
194
+ buttonBackgroundDefault: 'var(--bolt-elements-sidebar-buttonBackgroundDefault)',
195
+ buttonBackgroundHover: 'var(--bolt-elements-sidebar-buttonBackgroundHover)',
196
+ buttonText: 'var(--bolt-elements-sidebar-buttonText)',
197
+ },
198
+ cta: {
199
+ background: 'var(--bolt-elements-cta-background)',
200
+ text: 'var(--bolt-elements-cta-text)',
201
  },
202
  },
203
  },
 
219
  }),
220
  ],
221
  });
222
+
223
+ /**
224
+ * Generates an alpha palette for a given hex color.
225
+ *
226
+ * @param hex - The hex color code (without alpha) to generate the palette from.
227
+ * @returns An object where keys are opacity percentages and values are hex colors with alpha.
228
+ *
229
+ * Example:
230
+ *
231
+ * ```
232
+ * {
233
+ * '1': '#FFFFFF03',
234
+ * '2': '#FFFFFF05',
235
+ * '3': '#FFFFFF08',
236
+ * }
237
+ * ```
238
+ */
239
+ function generateAlphaPalette(hex: string) {
240
+ return [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].reduce(
241
+ (acc, opacity) => {
242
+ const alpha = Math.round((opacity / 100) * 255)
243
+ .toString(16)
244
+ .padStart(2, '0');
245
+
246
+ acc[opacity] = `${hex}${alpha}`;
247
+
248
+ return acc;
249
+ },
250
+ {} as Record<number, string>,
251
+ );
252
+ }
pnpm-lock.yaml CHANGED
@@ -107,6 +107,9 @@ importers:
107
  '@stackblitz/sdk':
108
  specifier: ^1.11.0
109
  version: 1.11.0
 
 
 
110
  '@unocss/reset':
111
  specifier: ^0.61.0
112
  version: 0.61.0
@@ -1601,6 +1604,16 @@ packages:
1601
  resolution: {integrity: sha512-r0WTMpzFo6owx48/DgBVi9pKqNdGQhJcUAvO4JOn4V9MSeEd8UTFXmMt+Kgeqfai1NpGvRH3b+jJlr3WVZAChw==}
1602
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1603
 
 
 
 
 
 
 
 
 
 
 
1604
  '@ungap/[email protected]':
1605
  resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
1606
 
@@ -6609,6 +6622,20 @@ snapshots:
6609
  '@typescript-eslint/types': 8.0.0-alpha.33
6610
  eslint-visitor-keys: 3.4.3
6611
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6612
  '@ungap/[email protected]': {}
6613
 
6614
 
107
  '@stackblitz/sdk':
108
  specifier: ^1.11.0
109
  version: 1.11.0
110
+ '@uiw/codemirror-theme-vscode':
111
+ specifier: ^4.23.0
112
+ version: 4.23.0(@codemirror/[email protected])(@codemirror/[email protected])(@codemirror/[email protected])
113
  '@unocss/reset':
114
  specifier: ^0.61.0
115
  version: 0.61.0
 
1604
  resolution: {integrity: sha512-r0WTMpzFo6owx48/DgBVi9pKqNdGQhJcUAvO4JOn4V9MSeEd8UTFXmMt+Kgeqfai1NpGvRH3b+jJlr3WVZAChw==}
1605
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
1606
 
1607
+ '@uiw/[email protected]':
1608
+ resolution: {integrity: sha512-zl1FD7U1b58tqlF216jYv2okvVkTe+FP1ztqO/DF129bcH99QjszkakshyfxQEvvF4ys3zyzqZ7vU3VYBir8tg==}
1609
+
1610
+ '@uiw/[email protected]':
1611
+ resolution: {integrity: sha512-9fiji9xooZyBQozR1i6iTr56YP7j/Dr/VgsNWbqf5Szv+g+4WM1iZuiDGwNXmFMWX8gbkDzp6ASE21VCPSofWw==}
1612
+ peerDependencies:
1613
+ '@codemirror/language': '>=6.0.0'
1614
+ '@codemirror/state': '>=6.0.0'
1615
+ '@codemirror/view': '>=6.0.0'
1616
+
1617
  '@ungap/[email protected]':
1618
  resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
1619
 
 
6622
  '@typescript-eslint/types': 8.0.0-alpha.33
6623
  eslint-visitor-keys: 3.4.3
6624
 
6625
+ '@uiw/[email protected](@codemirror/[email protected])(@codemirror/[email protected])(@codemirror/[email protected])':
6626
+ dependencies:
6627
+ '@uiw/codemirror-themes': 4.23.0(@codemirror/[email protected])(@codemirror/[email protected])(@codemirror/[email protected])
6628
+ transitivePeerDependencies:
6629
+ - '@codemirror/language'
6630
+ - '@codemirror/state'
6631
+ - '@codemirror/view'
6632
+
6633
+ '@uiw/[email protected](@codemirror/[email protected])(@codemirror/[email protected])(@codemirror/[email protected])':
6634
+ dependencies:
6635
+ '@codemirror/language': 6.10.2
6636
+ '@codemirror/state': 6.4.1
6637
+ '@codemirror/view': 6.28.4
6638
+
6639
  '@ungap/[email protected]': {}
6640
 
6641