fix: expand button for fullscreen + disable text selection
- Replace double-tap trigger with ⤢ expand button in module header for keyboard and drumpad modules (more reliable, no text selection) - Disable user-select globally (except inputs/textareas) - Fullscreen state managed in ModuleNode, passed to widgets as props - Remove unused imports (useIsMobile, useRef) from widgets Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { triggerKeyboard } from '../engine/audioEngine.js';
|
import { triggerKeyboard } from '../engine/audioEngine.js';
|
||||||
import { state } from '../engine/state.js';
|
|
||||||
import { useIsMobile } from '../hooks/useIsMobile.js';
|
|
||||||
|
|
||||||
// 4x4 pad layout — each pad maps to a MIDI note
|
// 4x4 pad layout — each pad maps to a MIDI note
|
||||||
const PAD_NOTES = [
|
const PAD_NOTES = [
|
||||||
@@ -66,11 +64,8 @@ function FullscreenDrumPad({ moduleId, onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DrumPadWidget({ moduleId }) {
|
export default function DrumPadWidget({ moduleId, fullscreen, onCloseFullscreen }) {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const [fullscreen, setFullscreen] = useState(false);
|
|
||||||
const [activePad, setActivePad] = useState(-1);
|
const [activePad, setActivePad] = useState(-1);
|
||||||
const lastTap = useRef(0);
|
|
||||||
|
|
||||||
const hitPad = useCallback((pad, idx) => {
|
const hitPad = useCallback((pad, idx) => {
|
||||||
triggerKeyboard(moduleId, midiToFreq(pad.note), true);
|
triggerKeyboard(moduleId, midiToFreq(pad.note), true);
|
||||||
@@ -81,19 +76,9 @@ export default function DrumPadWidget({ moduleId }) {
|
|||||||
}, 150);
|
}, 150);
|
||||||
}, [moduleId]);
|
}, [moduleId]);
|
||||||
|
|
||||||
const handleDoubleTap = useCallback((e) => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastTap.current < 350) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setFullscreen(true);
|
|
||||||
}
|
|
||||||
lastTap.current = now;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div onPointerDown={handleDoubleTap}>
|
<div>
|
||||||
<div className="drumpad-grid">
|
<div className="drumpad-grid">
|
||||||
{PAD_NOTES.map((pad, i) => (
|
{PAD_NOTES.map((pad, i) => (
|
||||||
<div
|
<div
|
||||||
@@ -110,12 +95,12 @@ export default function DrumPadWidget({ moduleId }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 9, color: 'var(--text2)', textAlign: 'center', marginTop: 2 }}>
|
<div style={{ fontSize: 9, color: 'var(--text2)', textAlign: 'center', marginTop: 2 }}>
|
||||||
{isMobile ? 'Doble-tap: pantalla completa' : 'Tap pads to trigger'}
|
Tap pads to trigger
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fullscreen && (
|
{fullscreen && (
|
||||||
<FullscreenDrumPad moduleId={moduleId} onClose={() => setFullscreen(false)} />
|
<FullscreenDrumPad moduleId={moduleId} onClose={onCloseFullscreen} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { triggerKeyboard } from '../engine/audioEngine.js';
|
import { triggerKeyboard } from '../engine/audioEngine.js';
|
||||||
import { state } from '../engine/state.js';
|
import { state } from '../engine/state.js';
|
||||||
import { useIsMobile } from '../hooks/useIsMobile.js';
|
|
||||||
|
|
||||||
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||||
|
|
||||||
@@ -96,13 +95,10 @@ function FullscreenPiano({ moduleId, initialOctave, onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function KeyboardWidget({ moduleId }) {
|
export default function KeyboardWidget({ moduleId, fullscreen, onCloseFullscreen }) {
|
||||||
const mod = state.modules.find(m => m.id === moduleId);
|
const mod = state.modules.find(m => m.id === moduleId);
|
||||||
const octave = mod?.params?.octave ?? 4;
|
const octave = mod?.params?.octave ?? 4;
|
||||||
const activeKeys = useRef(new Set());
|
const activeKeys = useRef(new Set());
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const [fullscreen, setFullscreen] = useState(false);
|
|
||||||
const lastTap = useRef(0);
|
|
||||||
|
|
||||||
const playNote = useCallback((semitone) => {
|
const playNote = useCallback((semitone) => {
|
||||||
const midi = (octave + 1) * 12 + semitone;
|
const midi = (octave + 1) * 12 + semitone;
|
||||||
@@ -138,23 +134,13 @@ export default function KeyboardWidget({ moduleId }) {
|
|||||||
};
|
};
|
||||||
}, [playNote, stopNote]);
|
}, [playNote, stopNote]);
|
||||||
|
|
||||||
const handleDoubleTap = useCallback((e) => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastTap.current < 350) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setFullscreen(true);
|
|
||||||
}
|
|
||||||
lastTap.current = now;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Mini keyboard (1 octave)
|
// Mini keyboard (1 octave)
|
||||||
const whites = [0, 2, 4, 5, 7, 9, 11];
|
const whites = [0, 2, 4, 5, 7, 9, 11];
|
||||||
const blacks = [1, 3, -1, 6, 8, 10];
|
const blacks = [1, 3, -1, 6, 8, 10];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ padding: '2px 0' }} onPointerDown={handleDoubleTap}>
|
<div style={{ padding: '2px 0' }}>
|
||||||
<svg viewBox="0 0 140 30" style={{ width: '100%', height: 30 }}>
|
<svg viewBox="0 0 140 30" style={{ width: '100%', height: 30 }}>
|
||||||
{whites.map((note, i) => (
|
{whites.map((note, i) => (
|
||||||
<rect key={`w${i}`} x={i * 20} y={0} width={19} height={28}
|
<rect key={`w${i}`} x={i * 20} y={0} width={19} height={28}
|
||||||
@@ -177,7 +163,7 @@ export default function KeyboardWidget({ moduleId }) {
|
|||||||
})}
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
<div style={{ fontSize: 9, color: 'var(--text2)', textAlign: 'center', marginTop: 2 }}>
|
<div style={{ fontSize: 9, color: 'var(--text2)', textAlign: 'center', marginTop: 2 }}>
|
||||||
{isMobile ? 'Doble-tap: pantalla completa' : 'Z-M / Q-I keys'} · Oct {octave}
|
Z-M / Q-I keys · Oct {octave}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,7 +171,7 @@ export default function KeyboardWidget({ moduleId }) {
|
|||||||
<FullscreenPiano
|
<FullscreenPiano
|
||||||
moduleId={moduleId}
|
moduleId={moduleId}
|
||||||
initialOctave={octave}
|
initialOctave={octave}
|
||||||
onClose={() => setFullscreen(false)}
|
onClose={onCloseFullscreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
|||||||
if (!def) return null;
|
if (!def) return null;
|
||||||
|
|
||||||
const isSelected = state.selectedModuleId === mod.id;
|
const isSelected = state.selectedModuleId === mod.id;
|
||||||
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
|
||||||
// Merge default params
|
// Merge default params
|
||||||
const params = { ...Object.fromEntries(Object.entries(def.params).map(([k, v]) => [k, v.default])), ...mod.params };
|
const params = { ...Object.fromEntries(Object.entries(def.params).map(([k, v]) => [k, v.default])), ...mod.params };
|
||||||
@@ -177,6 +178,13 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
|||||||
<div className="module-header" onPointerDown={handleHeaderDown}>
|
<div className="module-header" onPointerDown={handleHeaderDown}>
|
||||||
<span className="type-icon">{def.icon}</span>
|
<span className="type-icon">{def.icon}</span>
|
||||||
<span className="type-name">{def.name}</span>
|
<span className="type-name">{def.name}</span>
|
||||||
|
{(mod.type === 'keyboard' || mod.type === 'drumpad') && (
|
||||||
|
<button
|
||||||
|
className="expand-btn"
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFullscreen(true); }}
|
||||||
|
title="Pantalla completa"
|
||||||
|
>⤢</button>
|
||||||
|
)}
|
||||||
<button className="close-btn" onClick={handleDelete}>✕</button>
|
<button className="close-btn" onClick={handleDelete}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -247,10 +255,10 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
|||||||
{mod.type === 'scope' && <ScopeDisplay moduleId={mod.id} />}
|
{mod.type === 'scope' && <ScopeDisplay moduleId={mod.id} />}
|
||||||
|
|
||||||
{/* Keyboard widget */}
|
{/* Keyboard widget */}
|
||||||
{mod.type === 'keyboard' && <KeyboardWidget moduleId={mod.id} />}
|
{mod.type === 'keyboard' && <KeyboardWidget moduleId={mod.id} fullscreen={fullscreen} onCloseFullscreen={() => setFullscreen(false)} />}
|
||||||
|
|
||||||
{/* Drum Pad widget */}
|
{/* Drum Pad widget */}
|
||||||
{mod.type === 'drumpad' && <DrumPadWidget moduleId={mod.id} />}
|
{mod.type === 'drumpad' && <DrumPadWidget moduleId={mod.id} fullscreen={fullscreen} onCloseFullscreen={() => setFullscreen(false)} />}
|
||||||
|
|
||||||
{/* Sequencer widget */}
|
{/* Sequencer widget */}
|
||||||
{mod.type === 'sequencer' && <SequencerWidget moduleId={mod.id} />}
|
{mod.type === 'sequencer' && <SequencerWidget moduleId={mod.id} />}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ html, body, #root {
|
|||||||
}
|
}
|
||||||
/* Block native browser zoom gestures globally */
|
/* Block native browser zoom gestures globally */
|
||||||
html { touch-action: manipulation; }
|
html { touch-action: manipulation; }
|
||||||
|
/* Prevent text selection globally (except inputs/textareas) */
|
||||||
|
* { -webkit-user-select: none; user-select: none; }
|
||||||
|
input, textarea, [contenteditable] { -webkit-user-select: text; user-select: text; }
|
||||||
|
|
||||||
/* ===== Layout ===== */
|
/* ===== Layout ===== */
|
||||||
.app { display: flex; flex-direction: column; height: 100vh; }
|
.app { display: flex; flex-direction: column; height: 100vh; }
|
||||||
@@ -121,6 +124,14 @@ html { touch-action: manipulation; }
|
|||||||
}
|
}
|
||||||
.module-header .close-btn:hover { background: var(--red); color: #fff; }
|
.module-header .close-btn:hover { background: var(--red); color: #fff; }
|
||||||
|
|
||||||
|
.module-header .expand-btn {
|
||||||
|
width: 18px; height: 18px; border: none; background: transparent;
|
||||||
|
color: var(--text2); cursor: pointer; font-size: 13px; border-radius: 3px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.module-header .expand-btn:hover { background: var(--accent); color: #000; }
|
||||||
|
|
||||||
.module-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
.module-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
/* Ports */
|
/* Ports */
|
||||||
|
|||||||
Reference in New Issue
Block a user