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 { state } from '../engine/state.js';
|
||||
import { useIsMobile } from '../hooks/useIsMobile.js';
|
||||
|
||||
// 4x4 pad layout — each pad maps to a MIDI note
|
||||
const PAD_NOTES = [
|
||||
@@ -66,11 +64,8 @@ function FullscreenDrumPad({ moduleId, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function DrumPadWidget({ moduleId }) {
|
||||
const isMobile = useIsMobile();
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
export default function DrumPadWidget({ moduleId, fullscreen, onCloseFullscreen }) {
|
||||
const [activePad, setActivePad] = useState(-1);
|
||||
const lastTap = useRef(0);
|
||||
|
||||
const hitPad = useCallback((pad, idx) => {
|
||||
triggerKeyboard(moduleId, midiToFreq(pad.note), true);
|
||||
@@ -81,19 +76,9 @@ export default function DrumPadWidget({ moduleId }) {
|
||||
}, 150);
|
||||
}, [moduleId]);
|
||||
|
||||
const handleDoubleTap = useCallback((e) => {
|
||||
const now = Date.now();
|
||||
if (now - lastTap.current < 350) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setFullscreen(true);
|
||||
}
|
||||
lastTap.current = now;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onPointerDown={handleDoubleTap}>
|
||||
<div>
|
||||
<div className="drumpad-grid">
|
||||
{PAD_NOTES.map((pad, i) => (
|
||||
<div
|
||||
@@ -110,12 +95,12 @@ export default function DrumPadWidget({ moduleId }) {
|
||||
))}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{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 { triggerKeyboard } from '../engine/audioEngine.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'];
|
||||
|
||||
@@ -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 octave = mod?.params?.octave ?? 4;
|
||||
const activeKeys = useRef(new Set());
|
||||
const isMobile = useIsMobile();
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const lastTap = useRef(0);
|
||||
|
||||
const playNote = useCallback((semitone) => {
|
||||
const midi = (octave + 1) * 12 + semitone;
|
||||
@@ -138,23 +134,13 @@ export default function KeyboardWidget({ moduleId }) {
|
||||
};
|
||||
}, [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)
|
||||
const whites = [0, 2, 4, 5, 7, 9, 11];
|
||||
const blacks = [1, 3, -1, 6, 8, 10];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: '2px 0' }} onPointerDown={handleDoubleTap}>
|
||||
<div style={{ padding: '2px 0' }}>
|
||||
<svg viewBox="0 0 140 30" style={{ width: '100%', height: 30 }}>
|
||||
{whites.map((note, i) => (
|
||||
<rect key={`w${i}`} x={i * 20} y={0} width={19} height={28}
|
||||
@@ -177,7 +163,7 @@ export default function KeyboardWidget({ moduleId }) {
|
||||
})}
|
||||
</svg>
|
||||
<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>
|
||||
|
||||
@@ -185,7 +171,7 @@ export default function KeyboardWidget({ moduleId }) {
|
||||
<FullscreenPiano
|
||||
moduleId={moduleId}
|
||||
initialOctave={octave}
|
||||
onClose={() => setFullscreen(false)}
|
||||
onClose={onCloseFullscreen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -46,6 +46,7 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
||||
if (!def) return null;
|
||||
|
||||
const isSelected = state.selectedModuleId === mod.id;
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
|
||||
// Merge default 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}>
|
||||
<span className="type-icon">{def.icon}</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>
|
||||
</div>
|
||||
|
||||
@@ -247,10 +255,10 @@ export default function ModuleNode({ mod, zoom, onStartConnect, onPortPosition }
|
||||
{mod.type === 'scope' && <ScopeDisplay moduleId={mod.id} />}
|
||||
|
||||
{/* Keyboard widget */}
|
||||
{mod.type === 'keyboard' && <KeyboardWidget moduleId={mod.id} />}
|
||||
{mod.type === 'keyboard' && <KeyboardWidget moduleId={mod.id} fullscreen={fullscreen} onCloseFullscreen={() => setFullscreen(false)} />}
|
||||
|
||||
{/* Drum Pad widget */}
|
||||
{mod.type === 'drumpad' && <DrumPadWidget moduleId={mod.id} />}
|
||||
{mod.type === 'drumpad' && <DrumPadWidget moduleId={mod.id} fullscreen={fullscreen} onCloseFullscreen={() => setFullscreen(false)} />}
|
||||
|
||||
{/* Sequencer widget */}
|
||||
{mod.type === 'sequencer' && <SequencerWidget moduleId={mod.id} />}
|
||||
|
||||
@@ -35,6 +35,9 @@ html, body, #root {
|
||||
}
|
||||
/* Block native browser zoom gestures globally */
|
||||
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 ===== */
|
||||
.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 .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; }
|
||||
|
||||
/* Ports */
|
||||
|
||||
Reference in New Issue
Block a user