feat: add bar width multiplier (1x–8x horizontal zoom) for track grid
This commit is contained in:
@@ -153,3 +153,49 @@
|
||||
width: 100px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Clickable zoom styling */
|
||||
.current-zoom {
|
||||
font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.current-zoom:hover {
|
||||
background-color: #4a4a4a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.zoom-slider-popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #444;
|
||||
border-radius: 3px;
|
||||
padding: 8px 12px;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
white-space: nowrap;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.zoom-slider-popup input[type="range"] {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.zoom-slider-label {
|
||||
font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #e0e0e0;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ const Toolbar: React.FC = () => {
|
||||
isPlaying, startPlaying, stopPlaying, setPlayheadPosition,
|
||||
currentTime, setBpm, setTimeSignature, setKeySignature,
|
||||
maxBars, setMaxBars,
|
||||
barWidthMultiplier, setBarWidthMultiplier,
|
||||
isLooping, toggleLoop,
|
||||
canUndo, canRedo, undoDescription, redoDescription, undo, redo,
|
||||
toggleChatBox, toggleSettings, cleanupProjectState,
|
||||
@@ -57,9 +58,25 @@ const Toolbar: React.FC = () => {
|
||||
// State for import modal
|
||||
const [showImportModal, setShowImportModal] = React.useState(false);
|
||||
|
||||
// State for zoom slider popup
|
||||
const [showZoomSlider, setShowZoomSlider] = React.useState(false);
|
||||
const zoomSliderRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// State for open project modal
|
||||
const [showOpenProject, setShowOpenProject] = React.useState(false);
|
||||
|
||||
// Close zoom slider on click outside
|
||||
React.useEffect(() => {
|
||||
if (!showZoomSlider) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (zoomSliderRef.current && !zoomSliderRef.current.contains(e.target as Node)) {
|
||||
setShowZoomSlider(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [showZoomSlider]);
|
||||
|
||||
// Key signature options
|
||||
const keySignatureOptions = Object.keys(KEY_SIGNATURE_MAP) as KeySignature[];
|
||||
|
||||
@@ -790,6 +807,28 @@ const Toolbar: React.FC = () => {
|
||||
|
||||
<div className="toolbar-right">
|
||||
<div className="transport-control">
|
||||
<div className="transport-item" style={{ position: 'relative' }} ref={zoomSliderRef}>
|
||||
<span
|
||||
className='current-zoom'
|
||||
onClick={() => setShowZoomSlider(!showZoomSlider)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{barWidthMultiplier}x
|
||||
</span>
|
||||
{showZoomSlider && (
|
||||
<div className="zoom-slider-popup">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="8"
|
||||
step="1"
|
||||
value={barWidthMultiplier}
|
||||
onChange={(e) => setBarWidthMultiplier(parseInt(e.target.value))}
|
||||
/>
|
||||
<span className="zoom-slider-label">{barWidthMultiplier}x</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="transport-item">
|
||||
<span className='current-time' onClick={handleCurrentTimeClick} style={{ cursor: 'pointer' }}>{currentTime}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user