| import { useMemo, useState } from 'react' |
| import useAppStore from '../store/appStore' |
| import { useProcesses } from '../hooks/useProcesses' |
| import PreviewFrame from './PreviewFrame' |
| import LogsPanel from './LogsPanel' |
| import DbBrowserPanel from './DbBrowserPanel' |
| import TerminalPanel from './TerminalPanel' |
|
|
| export default function PreviewPanel() { |
| const { runtime, project, templates } = useAppStore() |
| const { status } = useProcesses() |
| const [tab, setTab] = useState('preview') |
|
|
| const isRunning = status === 'running' |
|
|
| |
| |
| const tplEntry = useMemo( |
| () => templates.find((t) => t.id === project.template), |
| [templates, project.template], |
| ) |
| const preview = tplEntry?.preview ?? { kind: 'frontend', path: '/' } |
|
|
| const targetPort = preview.kind === 'backend' |
| ? runtime.backendPort |
| : runtime.frontendPort |
| const iframeUrl = isRunning && targetPort |
| ? `http://localhost:${targetPort}${preview.path || '/'}` |
| : null |
|
|
| return ( |
| <div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: 'var(--bg)' }}> |
| |
| {/* ββ Header β tabs only. Run / Stop / Reload / Open-in-browser all |
| live in TopBar so they're visible from any panel. βββββββββββββββββ */} |
| <div |
| style={{ |
| display: 'flex', |
| alignItems: 'center', |
| padding: '0 10px', |
| height: 34, |
| flexShrink: 0, |
| borderBottom: '1px solid var(--border)', |
| background: 'var(--surface)', |
| }} |
| > |
| <div style={{ display: 'flex', alignItems: 'center', gap: 0 }}> |
| {['preview', 'logs', 'db', 'terminal'].map((t) => ( |
| <button |
| key={t} |
| onClick={() => setTab(t)} |
| style={{ |
| padding: '0 12px', |
| height: 34, |
| border: 'none', |
| borderBottom: tab === t ? '2px solid var(--accent)' : '2px solid transparent', |
| background: 'transparent', |
| color: tab === t ? 'var(--text)' : 'var(--text-dim)', |
| fontSize: 11, |
| fontWeight: tab === t ? 600 : 400, |
| cursor: 'pointer', |
| letterSpacing: '0.04em', |
| textTransform: 'uppercase', |
| transition: 'color 0.15s', |
| }} |
| > |
| {t} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* ββ Body βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */} |
| <div style={{ flex: 1, minHeight: 0, position: 'relative' }}> |
| {/* Preview tab β always rendered so iframe keeps its state when switching tabs */} |
| <div style={{ position: 'absolute', inset: 0, visibility: tab === 'preview' ? 'visible' : 'hidden' }}> |
| <PreviewFrame url={iframeUrl} /> |
| </div> |
| |
| {tab === 'logs' && ( |
| <div style={{ position: 'absolute', inset: 0 }}> |
| <LogsPanel /> |
| </div> |
| )} |
| |
| {tab === 'db' && ( |
| <div style={{ position: 'absolute', inset: 0 }}> |
| <DbBrowserPanel /> |
| </div> |
| )} |
| |
| {/* Terminal stays mounted once visited so the PTY session survives |
| tab switches. Rendered hidden when another tab is active. */} |
| {tab === 'terminal' && ( |
| <div style={{ position: 'absolute', inset: 0 }}> |
| <TerminalPanel /> |
| </div> |
| )} |
| </div> |
| </div> |
| ) |
| } |
|
|