diff --git a/packages/bolt/app/components/ui/IconButton.tsx b/packages/bolt/app/components/ui/IconButton.tsx index 085d9cd..f22ab28 100644 --- a/packages/bolt/app/components/ui/IconButton.tsx +++ b/packages/bolt/app/components/ui/IconButton.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { classNames } from '~/utils/classNames'; -type IconSize = 'sm' | 'md' | 'xl' | 'xxl'; +type IconSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl'; interface BaseIconButtonProps { size?: IconSize; @@ -64,6 +64,8 @@ function getIconSize(size: IconSize) { return 'text-sm'; } else if (size === 'md') { return 'text-md'; + } else if (size === 'lg') { + return 'text-lg'; } else if (size === 'xl') { return 'text-xl'; } else { diff --git a/packages/bolt/app/components/workbench/EditorPanel.tsx b/packages/bolt/app/components/workbench/EditorPanel.tsx index 4851c03..4571336 100644 --- a/packages/bolt/app/components/workbench/EditorPanel.tsx +++ b/packages/bolt/app/components/workbench/EditorPanel.tsx @@ -9,12 +9,14 @@ import { type OnSaveCallback as OnEditorSave, type OnScrollCallback as OnEditorScroll, } from '~/components/editor/codemirror/CodeMirrorEditor'; +import { IconButton } from '~/components/ui/IconButton'; import { PanelHeader } from '~/components/ui/PanelHeader'; import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton'; import { shortcutEventEmitter } from '~/lib/hooks'; import type { FileMap } from '~/lib/stores/files'; import { themeStore } from '~/lib/stores/theme'; import { workbenchStore } from '~/lib/stores/workbench'; +import { classNames } from '~/utils/classNames'; import { renderLogger } from '~/utils/logger'; import { isMobile } from '~/utils/mobile'; import { FileTreePanel } from './FileTreePanel'; @@ -33,6 +35,7 @@ interface EditorPanelProps { onFileReset?: () => void; } +const MAX_TERMINALS = 3; const DEFAULT_TERMINAL_SIZE = 25; const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE; @@ -60,7 +63,8 @@ export const EditorPanel = memo( const terminalPanelRef = useRef(null); const terminalToggledByShortcut = useRef(false); - const [terminalCount] = useState(1); + const [activeTerminal, setActiveTerminal] = useState(0); + const [terminalCount, setTerminalCount] = useState(1); const activeFile = useMemo(() => { if (!editorDocument) { @@ -109,6 +113,13 @@ export const EditorPanel = memo( terminalToggledByShortcut.current = false; }, [showTerminal]); + const addTerminal = () => { + if (terminalCount < MAX_TERMINALS) { + setTerminalCount(terminalCount + 1); + setActiveTerminal(terminalCount); + } + }; + return ( @@ -180,27 +191,50 @@ export const EditorPanel = memo( } }} > -
- - Terminal - -
+
+
{Array.from({ length: terminalCount }, (_, index) => { + const isActive = activeTerminal === index; + return ( -
- { - terminalRefs.current.push(ref); - }} - onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)} - onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)} - className="h-full" - theme={theme} - /> -
+ ); })} + {terminalCount < MAX_TERMINALS && ( + + )}
+ {Array.from({ length: terminalCount }, (_, index) => { + const isActive = activeTerminal === index; + + return ( + { + terminalRefs.current.push(ref); + }} + onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)} + onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)} + theme={theme} + /> + ); + })}
diff --git a/packages/bolt/app/components/workbench/terminal/Terminal.tsx b/packages/bolt/app/components/workbench/terminal/Terminal.tsx index 2e386c4..fd9f708 100644 --- a/packages/bolt/app/components/workbench/terminal/Terminal.tsx +++ b/packages/bolt/app/components/workbench/terminal/Terminal.tsx @@ -3,9 +3,10 @@ import { WebLinksAddon } from '@xterm/addon-web-links'; import { Terminal as XTerm } from '@xterm/xterm'; import { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react'; import type { Theme } from '~/lib/stores/theme'; +import { createScopedLogger } from '~/utils/logger'; import { getTerminalTheme } from './theme'; -import '@xterm/xterm/css/xterm.css'; +const logger = createScopedLogger('Terminal'); export interface TerminalRef { reloadStyles: () => void; @@ -52,6 +53,8 @@ export const Terminal = memo( resizeObserver.observe(element); + logger.info('Attach terminal'); + onTerminalReady?.(terminal); return () => { diff --git a/packages/bolt/app/lib/hooks/useShortcuts.ts b/packages/bolt/app/lib/hooks/useShortcuts.ts index 4f982ad..531d0c0 100644 --- a/packages/bolt/app/lib/hooks/useShortcuts.ts +++ b/packages/bolt/app/lib/hooks/useShortcuts.ts @@ -41,7 +41,10 @@ export function useShortcuts(): void { ) { shortcutEventEmitter.dispatch(name as keyof Shortcuts); event.preventDefault(); + event.stopPropagation(); + shortcut.action(); + break; } } diff --git a/packages/bolt/app/root.tsx b/packages/bolt/app/root.tsx index cf1b2e1..6c01e1f 100644 --- a/packages/bolt/app/root.tsx +++ b/packages/bolt/app/root.tsx @@ -7,6 +7,7 @@ import { stripIndents } from './utils/stripIndent'; import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url'; import globalStyles from './styles/index.scss?url'; +import xtermStyles from '@xterm/xterm/css/xterm.css?url'; import 'virtual:uno.css'; @@ -19,6 +20,7 @@ export const links: LinksFunction = () => [ { rel: 'stylesheet', href: tailwindReset }, { rel: 'stylesheet', href: globalStyles }, { rel: 'stylesheet', href: reactToastifyStyles }, + { rel: 'stylesheet', href: xtermStyles }, { rel: 'preconnect', href: 'https://fonts.googleapis.com', diff --git a/packages/bolt/app/styles/components/terminal.scss b/packages/bolt/app/styles/components/terminal.scss index dc1d224..0962c7a 100644 --- a/packages/bolt/app/styles/components/terminal.scss +++ b/packages/bolt/app/styles/components/terminal.scss @@ -1,3 +1,3 @@ .xterm { - height: 100%; + padding: 1rem; }