Add mobile responsive layout with Code/Circuit tab switcher
Co-authored-by: davidmonterocrespo24 <47928504+davidmonterocrespo24@users.noreply.github.com>pull/10/head
parent
51bdcd24c4
commit
34ee9f8e0e
|
|
@ -1838,6 +1838,14 @@
|
||||||
"@types/react": "^19.2.0"
|
"@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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.56.1",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -181,11 +181,70 @@ body {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
width: 100%;
|
display: none;
|
||||||
height: 5px;
|
}
|
||||||
cursor: row-resize;
|
}
|
||||||
|
|
||||||
|
/* ── 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="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" />
|
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
|
||||||
</svg>
|
</svg>
|
||||||
Examples
|
<span>Examples</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
@ -60,7 +60,7 @@ export const AppHeader: React.FC<AppHeaderProps> = () => {
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<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" />
|
<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>
|
</svg>
|
||||||
GitHub
|
<span className="header-github-text">GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Auth UI */}
|
{/* Auth UI */}
|
||||||
|
|
@ -77,7 +77,7 @@ export const AppHeader: React.FC<AppHeaderProps> = () => {
|
||||||
{user.username[0].toUpperCase()}
|
{user.username[0].toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{user.username}
|
<span className="header-username-text">{user.username}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{dropdownOpen && (
|
{dropdownOpen && (
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import { useAuthStore } from '../store/useAuthStore';
|
||||||
import type { CompilationLog } from '../utils/compilationLogger';
|
import type { CompilationLog } from '../utils/compilationLogger';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
const BOTTOM_PANEL_MIN = 80;
|
const BOTTOM_PANEL_MIN = 80;
|
||||||
const BOTTOM_PANEL_MAX = 600;
|
const BOTTOM_PANEL_MAX = 600;
|
||||||
const BOTTOM_PANEL_DEFAULT = 200;
|
const BOTTOM_PANEL_DEFAULT = 200;
|
||||||
|
|
@ -47,6 +49,9 @@ export const EditorPage: React.FC = () => {
|
||||||
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
|
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
|
||||||
const [explorerOpen, setExplorerOpen] = useState(true);
|
const [explorerOpen, setExplorerOpen] = useState(true);
|
||||||
const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT);
|
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 user = useAuthStore((s) => s.user);
|
||||||
|
|
||||||
const handleSaveClick = useCallback(() => {
|
const handleSaveClick = useCallback(() => {
|
||||||
|
|
@ -57,6 +62,19 @@ export const EditorPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [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
|
// Ctrl+S shortcut
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -157,7 +175,11 @@ export const EditorPage: React.FC = () => {
|
||||||
{/* ── Editor side ── */}
|
{/* ── Editor side ── */}
|
||||||
<div
|
<div
|
||||||
className="editor-panel"
|
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 */}
|
{/* File explorer sidebar + resize handle */}
|
||||||
{explorerOpen && (
|
{explorerOpen && (
|
||||||
|
|
@ -165,7 +187,9 @@ export const EditorPage: React.FC = () => {
|
||||||
<div style={{ width: explorerWidth, flexShrink: 0, display: 'flex', overflow: 'hidden' }}>
|
<div style={{ width: explorerWidth, flexShrink: 0, display: 'flex', overflow: 'hidden' }}>
|
||||||
<FileExplorer onSaveClick={handleSaveClick} />
|
<FileExplorer onSaveClick={handleSaveClick} />
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Resize handle */}
|
{/* Resize handle (desktop only) */}
|
||||||
<div className="resize-handle" onMouseDown={handleResizeMouseDown}>
|
{!isMobile && (
|
||||||
<div className="resize-handle-grip" />
|
<div className="resize-handle" onMouseDown={handleResizeMouseDown}>
|
||||||
</div>
|
<div className="resize-handle-grip" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Simulator side ── */}
|
{/* ── Simulator side ── */}
|
||||||
<div
|
<div
|
||||||
className="simulator-panel"
|
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 }}>
|
<div style={{ flex: 1, overflow: 'hidden', position: 'relative', minHeight: 0 }}>
|
||||||
<SimulatorCanvas />
|
<SimulatorCanvas />
|
||||||
|
|
@ -249,6 +279,34 @@ export const EditorPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</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)} />}
|
{saveModalOpen && <SaveProjectModal onClose={() => setSaveModalOpen(false)} />}
|
||||||
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
|
{loginPromptOpen && <LoginPromptModal onClose={() => setLoginPromptOpen(false)} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue