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:
Jose Luis
2026-03-21 16:11:32 +01:00
parent 892195410b
commit f0e7f7f37a
4 changed files with 30 additions and 40 deletions

View File

@@ -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} />
)}
</>
);

View File

@@ -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}
/>
)}
</>

View File

@@ -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} />}

View File

@@ -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 */