setExpanded(e => !e)}>
+
setExpanded(v => !v)}>
+ {!expanded && !tabs && (
+
Modulos ▲
+ )}
{tabs && tabs.length > 0 && (
-
+
!expanded && setExpanded(true)}>
{tabs.map(tab => (
)}
-
- {children}
-
+ {expanded && (
+
+ {children}
+
+ )}
);
}
diff --git a/src/game/PuzzleView.jsx b/src/game/PuzzleView.jsx
index 0c32da9..8fae2c7 100644
--- a/src/game/PuzzleView.jsx
+++ b/src/game/PuzzleView.jsx
@@ -6,6 +6,7 @@ import ModuleNode from '../components/ModuleNode.jsx';
import WireLayer from '../components/WireLayer.jsx';
import BottomSheet from '../components/BottomSheet.jsx';
import { useIsMobile } from '../hooks/useIsMobile.js';
+import { usePinchZoom } from '../hooks/usePinchZoom.js';
import { playTarget, stopTarget, isTargetPlaying } from './targetAudio.js';
import LevelComplete from './LevelComplete.jsx';
import { completeLevel, saveLevelPatch, getLevelPatch, clearLevelPatch, markHintUsed, wasHintUsed } from './gameState.js';
@@ -25,6 +26,11 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
const isMobile = useIsMobile();
const [mobileTab, setMobileTab] = useState('mission');
+ // Pinch-to-zoom on mobile
+ const getZoom = useCallback(() => state.zoom, []);
+ const setZoom = useCallback((z) => { state.zoom = z; emit(); }, []);
+ usePinchZoom(containerRef, getZoom, setZoom);
+
useEffect(() => {
const unsub = subscribe(() => {
forceUpdate(n => n + 1);
diff --git a/src/hooks/usePinchZoom.js b/src/hooks/usePinchZoom.js
new file mode 100644
index 0000000..6aaa3f5
--- /dev/null
+++ b/src/hooks/usePinchZoom.js
@@ -0,0 +1,48 @@
+import { useRef, useEffect } from 'react';
+
+export function usePinchZoom(containerRef, getZoom, setZoom) {
+ const pinchRef = useRef({ active: false, startDist: 0, startZoom: 1 });
+
+ useEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+
+ const getDistance = (t1, t2) =>
+ Math.sqrt((t1.clientX - t2.clientX) ** 2 + (t1.clientY - t2.clientY) ** 2);
+
+ const onTouchStart = (e) => {
+ if (e.touches.length === 2) {
+ e.preventDefault();
+ pinchRef.current = {
+ active: true,
+ startDist: getDistance(e.touches[0], e.touches[1]),
+ startZoom: getZoom(),
+ };
+ }
+ };
+
+ const onTouchMove = (e) => {
+ if (pinchRef.current.active && e.touches.length === 2) {
+ e.preventDefault();
+ const dist = getDistance(e.touches[0], e.touches[1]);
+ const scale = dist / pinchRef.current.startDist;
+ const newZoom = Math.max(0.3, Math.min(3, pinchRef.current.startZoom * scale));
+ setZoom(newZoom);
+ }
+ };
+
+ const onTouchEnd = () => {
+ pinchRef.current.active = false;
+ };
+
+ el.addEventListener('touchstart', onTouchStart, { passive: false });
+ el.addEventListener('touchmove', onTouchMove, { passive: false });
+ el.addEventListener('touchend', onTouchEnd);
+
+ return () => {
+ el.removeEventListener('touchstart', onTouchStart);
+ el.removeEventListener('touchmove', onTouchMove);
+ el.removeEventListener('touchend', onTouchEnd);
+ };
+ }, [containerRef, getZoom, setZoom]);
+}
diff --git a/src/index.css b/src/index.css
index 0e28381..b789ff2 100644
--- a/src/index.css
+++ b/src/index.css
@@ -873,19 +873,26 @@ html, body, #root {
display: flex; flex-direction: column;
background: var(--panel); border-top: 1px solid var(--border);
border-radius: 16px 16px 0 0; flex-shrink: 0;
- max-height: 40vh; transition: max-height 0.3s ease;
+ transition: max-height 0.3s ease;
overflow: hidden;
}
- .bottom-sheet.expanded { max-height: 60vh; }
+ .bottom-sheet.collapsed { max-height: 42px; }
+ .bottom-sheet.collapsed:has(.bottom-sheet-tabs) { max-height: 76px; }
+ .bottom-sheet.expanded { max-height: 55vh; }
.bottom-sheet-handle {
- display: flex; justify-content: center; padding: 10px 0 6px;
- cursor: grab;
+ display: flex; align-items: center; justify-content: center;
+ gap: 8px; padding: 10px 0 6px; cursor: pointer; min-height: 34px;
}
.bottom-sheet-handle-bar {
width: 40px; height: 4px; background: var(--border);
border-radius: 2px;
}
+ .bottom-sheet-peek-label {
+ font-size: 10px; font-weight: 600; color: var(--text2);
+ font-family: 'JetBrains Mono', monospace; letter-spacing: 1px;
+ text-transform: uppercase;
+ }
.bottom-sheet-tabs {
display: flex; padding: 0 16px; gap: 0;