Add mobile responsive layout with Code/Circuit tab switcher

Co-authored-by: davidmonterocrespo24 <47928504+davidmonterocrespo24@users.noreply.github.com>
pull/10/head
copilot-swe-agent[bot] 2026-03-10 16:09:32 +00:00
parent 51bdcd24c4
commit 34ee9f8e0e
4 changed files with 138 additions and 13 deletions

View File

@ -1838,6 +1838,14 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",

View File

@ -181,11 +181,70 @@ body {
width: 100% !important;
min-width: unset;
max-width: unset;
flex: 1;
}
.resize-handle {
width: 100%;
height: 5px;
cursor: row-resize;
display: none;
}
}
/* ── Mobile tab bar ──────────────────────────────── */
.mobile-tab-bar {
display: none;
}
@media (max-width: 768px) {
.mobile-tab-bar {
display: flex;
flex-shrink: 0;
height: 54px;
background: #252526;
border-top: 1px solid #007acc;
z-index: 50;
}
.mobile-tab-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: transparent;
border: none;
color: #7a7a7a;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: color 0.15s, background 0.15s;
padding: 6px 0;
}
.mobile-tab-btn--active {
color: #007acc;
background: rgba(0, 122, 204, 0.08);
}
.mobile-tab-btn:active {
background: rgba(255, 255, 255, 0.06);
}
}
/* ── Header responsive ───────────────────────────── */
@media (max-width: 768px) {
.app-header {
padding: 0 10px;
height: 44px;
}
.examples-link span,
.header-github-text,
.header-username-text {
display: none;
}
.examples-link {
padding: 6px 8px;
}
}

View File

@ -47,7 +47,7 @@ export const AppHeader: React.FC<AppHeaderProps> = () => {
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
</svg>
Examples
<span>Examples</span>
</Link>
<a
@ -60,7 +60,7 @@ export const AppHeader: React.FC<AppHeaderProps> = () => {
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.477 2 2 6.484 2 12.021c0 4.428 2.865 8.185 6.839 9.504.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.605-3.369-1.342-3.369-1.342-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.026 2.747-1.026.546 1.378.202 2.397.1 2.65.64.7 1.028 1.595 1.028 2.688 0 3.848-2.338 4.695-4.566 4.944.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.203 22 16.447 22 12.021 22 6.484 17.523 2 12 2z" />
</svg>
GitHub
<span className="header-github-text">GitHub</span>
</a>
{/* Auth UI */}
@ -77,7 +77,7 @@ export const AppHeader: React.FC<AppHeaderProps> = () => {
{user.username[0].toUpperCase()}
</div>
)}
{user.username}
<span className="header-username-text">{user.username}</span>
</button>
{dropdownOpen && (

View File

@ -18,6 +18,8 @@ import { useAuthStore } from '../store/useAuthStore';
import type { CompilationLog } from '../utils/compilationLogger';
import '../App.css';
const MOBILE_BREAKPOINT = 768;
const BOTTOM_PANEL_MIN = 80;
const BOTTOM_PANEL_MAX = 600;
const BOTTOM_PANEL_DEFAULT = 200;
@ -47,6 +49,9 @@ export const EditorPage: React.FC = () => {
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
const [explorerOpen, setExplorerOpen] = useState(true);
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
const [isMobile, setIsMobile] = useState(() => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches);
// Default to 'circuit' on mobile — the visual simulation is the primary content
const [mobileView, setMobileView] = useState<'code' | 'circuit'>('circuit');
const user = useAuthStore((s) => s.user);
const handleSaveClick = useCallback(() => {
@ -57,6 +62,19 @@ export const EditorPage: React.FC = () => {
}
}, [user]);
// Track mobile breakpoint
useEffect(() => {
const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
const update = (e: MediaQueryListEvent | MediaQueryList) => {
const mobile = e.matches;
setIsMobile(mobile);
if (mobile) setExplorerOpen(false);
};
update(mq);
mq.addEventListener('change', update);
return () => mq.removeEventListener('change', update);
}, []);
// Ctrl+S shortcut
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@ -157,7 +175,11 @@ export const EditorPage: React.FC = () => {
{/* ── Editor side ── */}
<div
className="editor-panel"
style={{ width: `${editorWidthPct}%`, display: 'flex', flexDirection: 'row' }}
style={{
width: isMobile ? '100%' : `${editorWidthPct}%`,
display: isMobile && mobileView !== 'code' ? 'none' : 'flex',
flexDirection: 'row',
}}
>
{/* File explorer sidebar + resize handle */}
{explorerOpen && (
@ -165,7 +187,9 @@ export const EditorPage: React.FC = () => {
<div style={{ width: explorerWidth, flexShrink: 0, display: 'flex', overflow: 'hidden' }}>
<FileExplorer onSaveClick={handleSaveClick} />
</div>
<div className="explorer-resize-handle" onMouseDown={handleExplorerResizeMouseDown} />
{!isMobile && (
<div className="explorer-resize-handle" onMouseDown={handleExplorerResizeMouseDown} />
)}
</>
)}
@ -221,15 +245,21 @@ export const EditorPage: React.FC = () => {
</div>
</div>
{/* Resize handle */}
<div className="resize-handle" onMouseDown={handleResizeMouseDown}>
<div className="resize-handle-grip" />
</div>
{/* Resize handle (desktop only) */}
{!isMobile && (
<div className="resize-handle" onMouseDown={handleResizeMouseDown}>
<div className="resize-handle-grip" />
</div>
)}
{/* ── Simulator side ── */}
<div
className="simulator-panel"
style={{ width: `${100 - editorWidthPct}%`, display: 'flex', flexDirection: 'column' }}
style={{
width: isMobile ? '100%' : `${100 - editorWidthPct}%`,
display: isMobile && mobileView !== 'circuit' ? 'none' : 'flex',
flexDirection: 'column',
}}
>
<div style={{ flex: 1, overflow: 'hidden', position: 'relative', minHeight: 0 }}>
<SimulatorCanvas />
@ -249,6 +279,34 @@ export const EditorPage: React.FC = () => {
</div>
</div>
{/* ── Mobile tab bar ── */}
{isMobile && (
<nav className="mobile-tab-bar">
<button
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('code')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
<span>Code</span>
</button>
<button
className={`mobile-tab-btn${mobileView === 'circuit' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('circuit')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2" />
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
<line x1="12" y1="12" x2="12" y2="16" />
<line x1="10" y1="14" x2="14" y2="14" />
</svg>
<span>Circuit</span>
</button>
</nav>
)}
{saveModalOpen && <SaveProjectModal onClose={() => setSaveModalOpen(false)} />}
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
</div>