Merge pull request #10 from davidmonterocrespo24/copilot/fix-mobile-layout-issues
feat: mobile-responsive editor layout with Code/Circuit panel switcherpull/12/head
commit
8b582f966b
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{!isMobile && (
|
||||
<div className="explorer-resize-handle" onMouseDown={handleExplorerResizeMouseDown} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -221,15 +245,21 @@ export const EditorPage: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
{/* 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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue