vzcode 1.45.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/assets/{index-C1lTrzOZ.js → index-CC0CUUDi.js} +113 -113
  2. package/dist/assets/{index-DeeOcIzw.css → index-CQyHWTWE.css} +1 -1
  3. package/dist/index.html +2 -2
  4. package/package.json +7 -3
  5. package/src/client/CodeEditor/index.tsx +7 -4
  6. package/src/client/RunCodeWidget/index.tsx +18 -15
  7. package/src/client/SplitPaneResizeContext.tsx +26 -108
  8. package/src/client/VZCodeContext.tsx +8 -1
  9. package/src/client/VZRight.tsx +32 -4
  10. package/src/client/VZSidebar/AIChat/ChatInput.tsx +72 -2
  11. package/src/client/VZSidebar/AIChat/DiffView.scss +29 -0
  12. package/src/client/VZSidebar/AIChat/DiffView.tsx +62 -1
  13. package/src/client/VZSidebar/AIChat/Message.tsx +14 -1
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -13
  15. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
  16. package/src/client/VZSidebar/AIChat/index.tsx +148 -12
  17. package/src/client/VZSidebar/AIChat/styles.scss +223 -1
  18. package/src/client/VZSidebar/aiCopyPaste.ts +6 -1
  19. package/src/client/VZSidebar/index.tsx +10 -2
  20. package/src/client/bootstrap.ts +11 -1
  21. package/src/client/useActions.ts +12 -0
  22. package/src/client/usePrettier/index.ts +1 -3
  23. package/src/client/vzReducer/aiChatReducer.ts +13 -0
  24. package/src/client/vzReducer/createInitialState.ts +1 -0
  25. package/src/client/vzReducer/index.ts +9 -0
  26. package/src/client/vzReducer/searchReducer.test.ts +44 -2
  27. package/src/runCode.ts +5 -17
  28. package/src/server/aiChatHandler/aiEditing.ts +37 -0
  29. package/src/server/aiChatHandler/chatOperations.ts +38 -1
  30. package/src/server/aiChatHandler/index.ts +20 -9
  31. package/src/server/aiChatHandler/llmStreaming.ts +101 -19
  32. package/src/server/generateAIResponse.ts +4 -0
  33. package/src/server/index.ts +52 -0
@@ -13,14 +13,21 @@ interface MessageProps {
13
13
  timestamp: number;
14
14
  isStreaming?: boolean;
15
15
  diffData?: UnifiedFilesDiff;
16
+ beforeFiles?: any;
17
+ chatId?: string;
18
+ canUndo?: boolean;
16
19
  }
17
20
 
18
21
  const MessageComponent = ({
22
+ id,
19
23
  role,
20
24
  content,
21
25
  timestamp,
22
26
  isStreaming,
23
27
  diffData,
28
+ beforeFiles,
29
+ chatId,
30
+ canUndo,
24
31
  }: MessageProps) => {
25
32
  // Memoize date formatting to avoid repeated computation
26
33
  const formattedTime = useMemo(() => {
@@ -47,7 +54,13 @@ const MessageComponent = ({
47
54
  {enableDiffView &&
48
55
  diffData &&
49
56
  Object.keys(diffData).length > 0 && (
50
- <DiffView diffData={diffData} />
57
+ <DiffView
58
+ diffData={diffData}
59
+ messageId={id}
60
+ chatId={chatId}
61
+ beforeFiles={beforeFiles}
62
+ canUndo={canUndo}
63
+ />
51
64
  )}
52
65
  </div>
53
66
  <div className="ai-chat-message-time">
@@ -3,31 +3,118 @@ import {
3
3
  useEffect,
4
4
  useCallback,
5
5
  memo,
6
+ useState,
6
7
  } from 'react';
7
8
  import { Message } from './Message';
8
9
  import { TypingIndicator } from './TypingIndicator';
10
+ import { ThinkingScratchpad } from './ThinkingScratchpad';
9
11
  import { VizChatMessage } from '@vizhub/viz-types';
10
12
 
11
13
  const MessageListComponent = ({
12
14
  messages,
13
15
  aiStatus,
14
16
  isLoading,
17
+ chatId, // Add chatId prop
18
+ aiScratchpad, // Add aiScratchpad prop
15
19
  }: {
16
20
  messages: VizChatMessage[];
17
21
  aiStatus?: string;
18
22
  isLoading: boolean;
23
+ chatId?: string; // Add chatId to the type
24
+ aiScratchpad?: string; // Add aiScratchpad to the type
19
25
  }) => {
20
26
  const messagesEndRef = useRef<HTMLDivElement>(null);
27
+ const messagesContainerRef = useRef<HTMLDivElement>(null);
28
+ const [isUserScrolled, setIsUserScrolled] =
29
+ useState(false);
30
+ const [autoScrollEnabled, setAutoScrollEnabled] =
31
+ useState(true);
32
+ const scrollTimeoutRef =
33
+ useRef<ReturnType<typeof setTimeout>>();
21
34
 
35
+ // Check if the user is scrolled to the bottom
36
+ const isScrolledToBottom = useCallback(() => {
37
+ const container = messagesContainerRef.current;
38
+ if (!container) return true;
39
+
40
+ const threshold = 50; // Allow 50px tolerance for "at bottom"
41
+ const { scrollTop, scrollHeight, clientHeight } =
42
+ container;
43
+ return (
44
+ scrollHeight - scrollTop - clientHeight < threshold
45
+ );
46
+ }, []);
47
+
48
+ // Smooth scroll to bottom with linear transition
22
49
  const scrollToBottom = useCallback(() => {
50
+ if (!autoScrollEnabled) return;
51
+
23
52
  messagesEndRef.current?.scrollIntoView({
24
53
  behavior: 'smooth',
54
+ block: 'end',
25
55
  });
26
- }, []);
56
+ }, [autoScrollEnabled]);
57
+
58
+ // Handle scroll events to detect user manual scrolling
59
+ const handleScroll = useCallback(() => {
60
+ const container = messagesContainerRef.current;
61
+ if (!container) return;
62
+
63
+ const isAtBottom = isScrolledToBottom();
64
+
65
+ // Clear any existing timeout
66
+ if (scrollTimeoutRef.current) {
67
+ clearTimeout(scrollTimeoutRef.current);
68
+ }
69
+
70
+ // If user scrolled up from bottom, disable auto-scroll
71
+ if (!isAtBottom && !isUserScrolled) {
72
+ setIsUserScrolled(true);
73
+ setAutoScrollEnabled(false);
74
+ }
75
+
76
+ // If user scrolled back to bottom, re-enable auto-scroll after a brief delay
77
+ if (isAtBottom && isUserScrolled) {
78
+ scrollTimeoutRef.current = setTimeout(() => {
79
+ setIsUserScrolled(false);
80
+ setAutoScrollEnabled(true);
81
+ }, 500); // 500ms delay to prevent flickering
82
+ }
83
+ }, [isUserScrolled, isScrolledToBottom]);
27
84
 
85
+ // Auto-scroll when messages change, but only if auto-scroll is enabled
28
86
  useEffect(() => {
29
- scrollToBottom();
30
- }, [messages]);
87
+ if (autoScrollEnabled && !isUserScrolled) {
88
+ // Use a short debounce to prevent multiple scroll calls during rapid updates
89
+ if (scrollTimeoutRef.current) {
90
+ clearTimeout(scrollTimeoutRef.current);
91
+ }
92
+
93
+ scrollTimeoutRef.current = setTimeout(() => {
94
+ scrollToBottom();
95
+ }, 100); // 100ms debounce
96
+ }
97
+
98
+ return () => {
99
+ if (scrollTimeoutRef.current) {
100
+ clearTimeout(scrollTimeoutRef.current);
101
+ }
102
+ };
103
+ }, [
104
+ messages,
105
+ autoScrollEnabled,
106
+ isUserScrolled,
107
+ scrollToBottom,
108
+ ]);
109
+
110
+ // Clean up timeout on unmount
111
+ useEffect(() => {
112
+ return () => {
113
+ if (scrollTimeoutRef.current) {
114
+ clearTimeout(scrollTimeoutRef.current);
115
+ }
116
+ };
117
+ }, []);
31
118
 
32
119
  // Check if AI generation has started (last message is from assistant)
33
120
  const lastMessage = messages[messages.length - 1];
@@ -39,18 +126,52 @@ const MessageListComponent = ({
39
126
  const showTypingIndicator =
40
127
  isLoading && !aiGenerationStarted;
41
128
 
129
+ // Show thinking scratchpad when AI is thinking (has scratchpad content)
130
+ const showThinkingScratchpad = Boolean(
131
+ aiScratchpad && aiScratchpad.trim(),
132
+ );
133
+
42
134
  return (
43
- <div className="ai-chat-messages">
44
- {messages.map((msg) => (
45
- <Message
46
- key={msg.id}
47
- id={msg.id}
48
- role={msg.role}
49
- content={msg.content}
50
- timestamp={msg.timestamp}
51
- diffData={(msg as any).diffData}
135
+ <div
136
+ className="ai-chat-messages"
137
+ ref={messagesContainerRef}
138
+ onScroll={handleScroll}
139
+ >
140
+ {messages.map((msg, index) => {
141
+ // Only the most recent assistant message with diffData can be undone
142
+ const isLastAssistantMessage =
143
+ msg.role === 'assistant' &&
144
+ index === messages.length - 1 &&
145
+ !isLoading; // Can't undo while AI is still generating
146
+
147
+ const canUndo =
148
+ isLastAssistantMessage &&
149
+ (msg as any).diffData &&
150
+ (msg as any).beforeFiles &&
151
+ Object.keys((msg as any).diffData || {}).length >
152
+ 0;
153
+
154
+ return (
155
+ <Message
156
+ key={msg.id}
157
+ id={msg.id}
158
+ role={msg.role}
159
+ content={msg.content}
160
+ timestamp={msg.timestamp}
161
+ diffData={(msg as any).diffData}
162
+ beforeFiles={(msg as any).beforeFiles}
163
+ chatId={chatId}
164
+ canUndo={canUndo}
165
+ />
166
+ );
167
+ })}
168
+
169
+ {showThinkingScratchpad && (
170
+ <ThinkingScratchpad
171
+ content={aiScratchpad || ''}
172
+ isVisible={showThinkingScratchpad}
52
173
  />
53
- ))}
174
+ )}
54
175
 
55
176
  {showTypingIndicator && <TypingIndicator />}
56
177
  <div ref={messagesEndRef} />
@@ -0,0 +1,37 @@
1
+ import { memo } from 'react';
2
+ import Markdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+
5
+ interface ThinkingScratchpadProps {
6
+ content: string;
7
+ isVisible: boolean;
8
+ }
9
+
10
+ const ThinkingScratchpadComponent = ({
11
+ content,
12
+ isVisible,
13
+ }: ThinkingScratchpadProps) => {
14
+ if (!isVisible || !content) {
15
+ return null;
16
+ }
17
+
18
+ return (
19
+ <div className="thinking-scratchpad">
20
+ <div className="thinking-scratchpad-header">
21
+ <span className="thinking-scratchpad-icon">🧠</span>
22
+ <span className="thinking-scratchpad-title">
23
+ VizBot is thinking...
24
+ </span>
25
+ </div>
26
+ <div className="thinking-scratchpad-content">
27
+ <Markdown remarkPlugins={[remarkGfm]}>
28
+ {content}
29
+ </Markdown>
30
+ </div>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export const ThinkingScratchpad = memo(
36
+ ThinkingScratchpadComponent,
37
+ );
@@ -24,12 +24,15 @@ export const AIChat = () => {
24
24
  content,
25
25
  aiChatEndpoint = defaultAIChatEndpoint,
26
26
  aiChatOptions = {},
27
+ aiChatMode,
28
+ setAIChatMode,
27
29
  } = useContext(VZCodeContext);
28
30
 
29
31
  // Get current chat data from content
30
32
  const currentChat = content?.chats?.[currentChatId];
31
33
  const rawMessages = currentChat?.messages || [];
32
34
  const aiStatus = currentChat?.aiStatus;
35
+ const aiScratchpad = currentChat?.aiScratchpad;
33
36
 
34
37
  // Transform messages to ensure they have required id field - memoized to avoid recreation
35
38
  const messages = useMemo(
@@ -41,6 +44,9 @@ export const AIChat = () => {
41
44
  [rawMessages],
42
45
  );
43
46
 
47
+ // Check if this is the first time opening the chat (no messages)
48
+ const isEmptyState = rawMessages.length === 0;
49
+
44
50
  const handleSendMessage = useCallback(async () => {
45
51
  if (!aiChatMessage.trim() || isLoading) return;
46
52
 
@@ -59,6 +65,7 @@ export const AIChat = () => {
59
65
  ...aiChatOptions,
60
66
  content: aiChatMessage.trim(),
61
67
  chatId: currentChatId,
68
+ mode: aiChatMode,
62
69
  }),
63
70
  });
64
71
 
@@ -86,18 +93,147 @@ export const AIChat = () => {
86
93
 
87
94
  return (
88
95
  <div className="ai-chat-container">
89
- <MessageList
90
- messages={messages}
91
- aiStatus={aiStatus}
92
- isLoading={isLoading}
93
- />
94
- <ChatInput
95
- aiChatMessage={aiChatMessage}
96
- setAIChatMessage={setAIChatMessage}
97
- onSendMessage={handleSendMessage}
98
- isLoading={isLoading}
99
- focused={aiChatFocused}
100
- />
96
+ <div
97
+ style={{
98
+ padding: '10px',
99
+ flex: 1,
100
+ display: 'flex',
101
+ flexDirection: 'column',
102
+ }}
103
+ >
104
+ {isEmptyState ? (
105
+ <div className="ai-chat-empty">
106
+ <div className="ai-chat-empty-icon">✨</div>
107
+ <h3 className="ai-chat-empty-title">
108
+ Hi, I'm VizBot!
109
+ </h3>
110
+ <div className="ai-chat-empty-text">
111
+ How can I help you?
112
+ </div>
113
+ <div className="ai-chat-empty-examples">
114
+ {aiChatMode === 'ask' ? (
115
+ <>
116
+ <h4>Try asking questions like:</h4>
117
+ <div className="ai-chat-suggested-prompts">
118
+ <button
119
+ className="ai-chat-suggested-prompt"
120
+ onClick={() =>
121
+ setAIChatMessage(
122
+ 'Explain how this works',
123
+ )
124
+ }
125
+ >
126
+ "Explain how this works"
127
+ </button>
128
+ <button
129
+ className="ai-chat-suggested-prompt"
130
+ onClick={() =>
131
+ setAIChatMessage(
132
+ 'How could I change it so that the circles are bigger?',
133
+ )
134
+ }
135
+ >
136
+ "How could I change it so that the
137
+ circles are bigger?"
138
+ </button>
139
+ <button
140
+ className="ai-chat-suggested-prompt"
141
+ onClick={() =>
142
+ setAIChatMessage(
143
+ 'What does this function do?',
144
+ )
145
+ }
146
+ >
147
+ "What does this function do?"
148
+ </button>
149
+ <button
150
+ className="ai-chat-suggested-prompt"
151
+ onClick={() =>
152
+ setAIChatMessage(
153
+ 'How can I make this more accessible?',
154
+ )
155
+ }
156
+ >
157
+ "How can I make this more accessible?"
158
+ </button>
159
+ </div>
160
+ </>
161
+ ) : (
162
+ <>
163
+ <h4>Try edit requests like these:</h4>
164
+ <div className="ai-chat-suggested-prompts">
165
+ <button
166
+ className="ai-chat-suggested-prompt"
167
+ onClick={() =>
168
+ setAIChatMessage(
169
+ 'Change the circles to squares',
170
+ )
171
+ }
172
+ >
173
+ "Change the circles to squares"
174
+ </button>
175
+ <button
176
+ className="ai-chat-suggested-prompt"
177
+ onClick={() =>
178
+ setAIChatMessage(
179
+ 'Add a button that toggles the animation',
180
+ )
181
+ }
182
+ >
183
+ "Add a button that toggles the
184
+ animation"
185
+ </button>
186
+ <button
187
+ className="ai-chat-suggested-prompt"
188
+ onClick={() =>
189
+ setAIChatMessage(
190
+ 'Fix the CSS so the layout is responsive',
191
+ )
192
+ }
193
+ >
194
+ "Fix the CSS so the layout is
195
+ responsive"
196
+ </button>
197
+ <button
198
+ className="ai-chat-suggested-prompt"
199
+ onClick={() =>
200
+ setAIChatMessage(
201
+ 'Refactor this function to use async/await',
202
+ )
203
+ }
204
+ >
205
+ "Refactor this function to use
206
+ async/await"
207
+ </button>
208
+ </div>
209
+ </>
210
+ )}
211
+ </div>
212
+ <div className="ai-chat-empty-text">
213
+ {aiChatMode === 'ask'
214
+ ? 'Type your question below to get started!'
215
+ : 'Type your edit request below to get started!'}
216
+ </div>
217
+ </div>
218
+ ) : (
219
+ <MessageList
220
+ messages={messages}
221
+ aiStatus={aiStatus}
222
+ isLoading={isLoading}
223
+ chatId={currentChatId}
224
+ aiScratchpad={aiScratchpad}
225
+ />
226
+ )}
227
+ <ChatInput
228
+ aiChatMessage={aiChatMessage}
229
+ setAIChatMessage={setAIChatMessage}
230
+ onSendMessage={handleSendMessage}
231
+ isLoading={isLoading}
232
+ focused={aiChatFocused}
233
+ aiChatMode={aiChatMode}
234
+ setAIChatMode={setAIChatMode}
235
+ />
236
+ </div>
101
237
  </div>
102
238
  );
103
239
  };
@@ -2,7 +2,84 @@
2
2
  height: 100%;
3
3
  display: flex;
4
4
  flex-direction: column;
5
- padding: 10px;
5
+ padding: 0;
6
+ }
7
+
8
+ .ai-chat-empty {
9
+ flex: 1;
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ text-align: center;
14
+ padding: 20px;
15
+ overflow-y: auto;
16
+
17
+ .ai-chat-empty-icon {
18
+ font-size: 42px;
19
+ margin-bottom: 15px;
20
+ }
21
+
22
+ .ai-chat-empty-title {
23
+ font-size: 18px;
24
+ font-weight: 600;
25
+ color: var(--vh-color-neutral-04);
26
+ margin-bottom: 15px;
27
+ }
28
+
29
+ .ai-chat-empty-text {
30
+ font-size: 14px;
31
+ color: var(--vh-color-neutral-04);
32
+ margin-bottom: 20px;
33
+ line-height: 1.5;
34
+ }
35
+
36
+ .ai-chat-empty-examples {
37
+ background: var(--vh-color-neutral-02);
38
+ border-radius: 8px;
39
+ padding: 15px;
40
+ margin-bottom: 20px;
41
+ text-align: left;
42
+ width: 100%;
43
+
44
+ h4 {
45
+ font-size: 15px;
46
+ font-weight: 600;
47
+ color: var(--vh-color-neutral-04);
48
+ margin-bottom: 10px;
49
+ }
50
+
51
+ .ai-chat-suggested-prompts {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 8px;
55
+ margin: 0;
56
+
57
+ .ai-chat-suggested-prompt {
58
+ background: var(--vh-color-neutral-03);
59
+ border: 1px solid var(--vh-color-neutral-04);
60
+ border-radius: 6px;
61
+ padding: 8px 12px;
62
+ font-size: 13px;
63
+ color: var(--vh-color-neutral-04);
64
+ text-align: left;
65
+ cursor: pointer;
66
+ transition: all 0.2s ease;
67
+ line-height: 1.4;
68
+
69
+ &:hover {
70
+ background: var(--vh-color-neutral-04);
71
+ color: var(--vh-color-neutral-01);
72
+ border-color: var(--vh-color-primary-01);
73
+ }
74
+
75
+ &:active {
76
+ background: var(--vh-color-primary-01);
77
+ color: white;
78
+ border-color: var(--vh-color-primary-01);
79
+ }
80
+ }
81
+ }
82
+ }
6
83
  }
7
84
 
8
85
  .ai-chat-messages {
@@ -10,6 +87,7 @@
10
87
  overflow-y: auto;
11
88
  margin-bottom: 10px;
12
89
  padding-right: 5px;
90
+ scroll-behavior: smooth;
13
91
 
14
92
  &::-webkit-scrollbar {
15
93
  width: 6px;
@@ -199,8 +277,57 @@
199
277
  padding-top: 10px;
200
278
  }
201
279
 
280
+ .ai-chat-mode-toggle {
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 4px;
284
+
285
+ .btn-group {
286
+ justify-content: center;
287
+ }
288
+
289
+ .btn-group .btn {
290
+ font-size: 12px;
291
+ font-weight: 500;
292
+ padding: 4px 12px;
293
+ border: 1px solid var(--vh-color-neutral-03);
294
+ background: var(--vh-color-neutral-02);
295
+ color: var(--vh-color-neutral-04);
296
+ transition: all 0.2s ease;
297
+
298
+ &:hover {
299
+ background: var(--vh-color-neutral-03);
300
+ border-color: var(--vh-color-neutral-04);
301
+ }
302
+
303
+ &.btn-primary {
304
+ background: var(--vh-color-primary-01);
305
+ border-color: var(--vh-color-primary-01);
306
+ color: white;
307
+
308
+ &:hover {
309
+ background: var(--vh-color-primary-02);
310
+ border-color: var(--vh-color-primary-02);
311
+ }
312
+ }
313
+
314
+ &:disabled {
315
+ opacity: 0.6;
316
+ cursor: not-allowed;
317
+ }
318
+ }
319
+
320
+ .ai-chat-mode-description {
321
+ font-size: 11px;
322
+ color: var(--vh-color-neutral-03);
323
+ text-align: center;
324
+ font-style: italic;
325
+ }
326
+ }
327
+
202
328
  .ai-chat-input-group {
203
329
  display: flex;
330
+ flex-direction: column;
204
331
  gap: 8px;
205
332
  margin-bottom: 0;
206
333
 
@@ -233,6 +360,18 @@
233
360
  }
234
361
  }
235
362
 
363
+ .ai-chat-input-info {
364
+ display: flex;
365
+ justify-content: space-between;
366
+ font-size: 12px;
367
+ color: var(--vh-color-neutral-03);
368
+ margin-bottom: 6px;
369
+
370
+ .ai-chat-hint {
371
+ font-style: italic;
372
+ }
373
+ }
374
+
236
375
  .ai-chat-send-button {
237
376
  background: var(--vh-color-primary-01);
238
377
  border: none;
@@ -243,6 +382,8 @@
243
382
  font-weight: 500;
244
383
  cursor: pointer;
245
384
  transition: background-color 0.2s ease;
385
+ align-self: flex-end;
386
+ min-width: 80px;
246
387
 
247
388
  &:hover:not(:disabled) {
248
389
  background: var(--vh-color-primary-02);
@@ -254,3 +395,84 @@
254
395
  }
255
396
  }
256
397
  }
398
+
399
+ // Thinking scratchpad styles
400
+ .thinking-scratchpad {
401
+ background: var(--vh-color-neutral-02);
402
+ border: 1px solid var(--vh-color-neutral-03);
403
+ border-radius: 8px;
404
+ margin: 10px 0;
405
+ padding: 0;
406
+ overflow: hidden;
407
+ font-size: 13px;
408
+
409
+ .thinking-scratchpad-header {
410
+ background: var(--vh-color-neutral-03);
411
+ color: var(--vh-color-neutral-04);
412
+ padding: 8px 12px;
413
+ font-weight: 600;
414
+ display: flex;
415
+ align-items: center;
416
+ gap: 8px;
417
+
418
+ .thinking-scratchpad-icon {
419
+ font-size: 16px;
420
+ }
421
+
422
+ .thinking-scratchpad-title {
423
+ font-size: 13px;
424
+ }
425
+ }
426
+
427
+ .thinking-scratchpad-content {
428
+ padding: 12px;
429
+ max-height: 200px;
430
+ overflow-y: auto;
431
+ background: var(--vh-color-neutral-02);
432
+
433
+ // Style markdown content in thinking scratchpad
434
+ p {
435
+ margin: 0 0 8px 0;
436
+ line-height: 1.4;
437
+ color: var(--vh-color-neutral-04);
438
+
439
+ &:last-child {
440
+ margin-bottom: 0;
441
+ }
442
+ }
443
+
444
+ code {
445
+ background: rgba(0, 0, 0, 0.3);
446
+ color: #e6e6e6;
447
+ padding: 1px 4px;
448
+ border-radius: 3px;
449
+ font-size: 12px;
450
+ border: 1px solid rgba(255, 255, 255, 0.1);
451
+ }
452
+
453
+ ul,
454
+ ol {
455
+ margin: 0 0 8px 0;
456
+ padding-left: 20px;
457
+
458
+ li {
459
+ margin-bottom: 4px;
460
+ color: var(--vh-color-neutral-04);
461
+ }
462
+ }
463
+
464
+ h1,
465
+ h2,
466
+ h3,
467
+ h4,
468
+ h5,
469
+ h6 {
470
+ margin: 8px 0 6px 0;
471
+ color: var(--vh-color-neutral-04);
472
+
473
+ &:first-child {
474
+ margin-top: 0;
475
+ }
476
+ }
477
+ }
478
+ }