fix: misc UI updates and fixes

This commit is contained in:
Xiaohan-Tian
2026-05-07 23:54:08 -07:00
parent 1e33caafc5
commit eead27dbd8
6 changed files with 150 additions and 109 deletions
+8 -2
View File
@@ -48,7 +48,7 @@ const Toolbar: React.FC = () => {
barWidthMultiplier, setBarWidthMultiplier, barWidthMultiplier, setBarWidthMultiplier,
isLooping, toggleLoop, isLooping, toggleLoop,
canUndo, canRedo, undoDescription, redoDescription, undo, redo, canUndo, canRedo, undoDescription, redoDescription, undo, redo,
toggleChatBox, toggleSettings, toggleKGOnePanel, toggleListEventPanel, showKGOnePanel, showListEventPanel, showChatBox, cleanupProjectState, toggleMetronome, isMetronomeEnabled, toggleChatBox, toggleSettings, toggleKGOnePanel, toggleListEventPanel, showKGOnePanel, showListEventPanel, showChatBox, showSettings, cleanupProjectState, toggleMetronome, isMetronomeEnabled,
isRecording, startRecording, stopRecording, isRecording, startRecording, stopRecording,
// Piano roll state/actions // Piano roll state/actions
showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId, showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId,
@@ -1065,7 +1065,13 @@ const Toolbar: React.FC = () => {
</div> </div>
</div> </div>
<button title="Import" onClick={handleImportProject}><FaUpload /></button> <button title="Import" onClick={handleImportProject}><FaUpload /></button>
<button title="Settings" onClick={handleSettingsClick}><FaCog /></button> <button
title="Settings"
className={`tool-button ${showSettings ? 'active' : ''}`}
onClick={handleSettingsClick}
>
<FaCog />
</button>
<div className="toolbar-separator"></div> <div className="toolbar-separator"></div>
<button title="Undo" onClick={handleUndoClick}><FaUndo /></button> <button title="Undo" onClick={handleUndoClick}><FaUndo /></button>
<button title="Redo" onClick={handleRedoClick}><FaRedo /></button> <button title="Redo" onClick={handleRedoClick}><FaRedo /></button>
+24 -27
View File
@@ -180,6 +180,14 @@
--toolbar-button-bg: #d6d6d6; --toolbar-button-bg: #d6d6d6;
} }
.piano-roll-toolbar button:focus,
.piano-roll-toolbar button:focus-visible,
.piano-roll-toolbar .tool-button:focus,
.piano-roll-toolbar .tool-button:focus-visible {
outline: none;
box-shadow: none;
}
/* Override pointer-events for piano roll toolbar sections */ /* Override pointer-events for piano roll toolbar sections */
.piano-roll-toolbar .toolbar-left, .piano-roll-toolbar .toolbar-left,
.piano-roll-toolbar .toolbar-right { .piano-roll-toolbar .toolbar-right {
@@ -310,8 +318,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow-x: auto; overflow: hidden;
overflow-y: hidden;
} }
.piano-roll-main-section { .piano-roll-main-section {
@@ -319,7 +326,7 @@
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
min-width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width)); min-width: 0;
} }
.piano-roll-main-section.with-automation { .piano-roll-main-section.with-automation {
@@ -329,34 +336,12 @@
.piano-roll-note-scroll { .piano-roll-note-scroll {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; overflow: auto;
overflow-x: visible;
}
.piano-roll-body-shell {
display: flex;
min-height: 0;
}
.piano-roll-keys-viewport {
position: sticky;
left: 0;
z-index: 15;
width: var(--region-piano-key-width);
flex: 0 0 var(--region-piano-key-width);
overflow: hidden;
border-right: 1px solid #3a3a3a;
background-color: #252525;
}
.piano-roll-keys-offset {
will-change: transform;
} }
.piano-roll-automation-section { .piano-roll-automation-section {
flex: 0 0 50%; flex: 0 0 50%;
min-height: 0; min-height: 0;
min-width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width));
border-top: 1px solid #3a3a3a; border-top: 1px solid #3a3a3a;
overflow: hidden; overflow: hidden;
} }
@@ -416,7 +401,11 @@
width: var(--region-piano-key-width); width: var(--region-piano-key-width);
flex-shrink: 0; flex-shrink: 0;
overflow: hidden; overflow: hidden;
border-right: 1px solid #3a3a3a;
background-color: #252525; background-color: #252525;
position: sticky;
left: 0;
z-index: 15;
} }
.piano-grid-container { .piano-grid-container {
@@ -523,6 +512,13 @@
min-width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width)); min-width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width));
} }
.piano-roll-automation-scroll-layer {
position: absolute;
inset: 0;
will-change: transform;
z-index: 0;
}
.piano-roll-automation-track::before { .piano-roll-automation-track::before {
content: ''; content: '';
position: absolute; position: absolute;
@@ -540,6 +536,7 @@
left: 0; left: 0;
width: var(--region-piano-key-width); width: var(--region-piano-key-width);
background: linear-gradient(180deg, #343434 0%, #2e2e2e 100%); background: linear-gradient(180deg, #343434 0%, #2e2e2e 100%);
z-index: 2;
} }
.piano-roll-automation-grid { .piano-roll-automation-grid {
@@ -561,7 +558,7 @@
position: absolute; position: absolute;
top: 8px; top: 8px;
left: 10px; left: 10px;
z-index: 1; z-index: 3;
font-size: 10px; font-size: 10px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.04em; letter-spacing: 0.04em;
+7 -7
View File
@@ -671,7 +671,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const handleZoomChange = useCallback((nextZoom: number) => { const handleZoomChange = useCallback((nextZoom: number) => {
if (nextZoom === pianoRollZoom) return; if (nextZoom === pianoRollZoom) return;
const container = pianoRollContentRef.current; const container = pianoRollNoteScrollRef.current;
pendingZoomAnchorBeatRef.current = null; pendingZoomAnchorBeatRef.current = null;
if (container) { if (container) {
const keysWidth = parseInt( const keysWidth = parseInt(
@@ -742,7 +742,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// Detect manual horizontal scroll during playback // Detect manual horizontal scroll during playback
useEffect(() => { useEffect(() => {
const container = pianoRollContentRef.current; const container = pianoRollNoteScrollRef.current;
if (!container) return; if (!container) return;
const handleScroll = () => { const handleScroll = () => {
@@ -761,7 +761,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
useEffect(() => { useEffect(() => {
if (!isPlaying || !autoScrollEnabled) return; if (!isPlaying || !autoScrollEnabled) return;
const container = pianoRollContentRef.current; const container = pianoRollNoteScrollRef.current;
if (!container) return; if (!container) return;
const beatWidth = parseInt( const beatWidth = parseInt(
@@ -787,7 +787,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
useEffect(() => { useEffect(() => {
if (pianoRollScrollRequest === null) return; if (pianoRollScrollRequest === null) return;
const container = pianoRollContentRef.current; const container = pianoRollNoteScrollRef.current;
if (!container) return; if (!container) return;
const beatWidth = parseInt( const beatWidth = parseInt(
@@ -820,7 +820,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
} }
const anchorBeat = pendingZoomAnchorBeatRef.current; const anchorBeat = pendingZoomAnchorBeatRef.current;
const container = pianoRollContentRef.current; const container = pianoRollNoteScrollRef.current;
if (anchorBeat !== null && container) { if (anchorBeat !== null && container) {
const keysWidth = parseInt( const keysWidth = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width') getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
@@ -845,7 +845,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// Scroll horizontally to the active region's starting bar // Scroll horizontally to the active region's starting bar
useEffect(() => { useEffect(() => {
if (pianoRollContentRef.current && activeRegion) { if (pianoRollNoteScrollRef.current && activeRegion) {
// Get the starting beat of the region // Get the starting beat of the region
const startBeat = activeRegion.getStartFromBeat(); const startBeat = activeRegion.getStartFromBeat();
@@ -866,7 +866,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const scrollPosition = barNumber * barWidth; const scrollPosition = barNumber * barWidth;
// Scroll to the calculated position // Scroll to the calculated position
pianoRollContentRef.current.scrollLeft = Math.max(0, scrollPosition); pianoRollNoteScrollRef.current.scrollLeft = Math.max(0, scrollPosition);
} }
}, [activeRegion, timeSignature]); }, [activeRegion, timeSignature]);
@@ -26,6 +26,8 @@ interface PianoRollAutomationLaneProps {
timeSignature: { numerator: number; denominator: number }; timeSignature: { numerator: number; denominator: number };
bpm?: number; bpm?: number;
redrawVersion?: number; redrawVersion?: number;
horizontalScrollLeft?: number;
onHorizontalWheel?: (delta: number) => void;
} }
const AUTOMATION_COLOR = '#87CEFA'; const AUTOMATION_COLOR = '#87CEFA';
@@ -38,6 +40,8 @@ const PianoRollAutomationLane: React.FC<PianoRollAutomationLaneProps> = ({
maxBars, maxBars,
timeSignature, timeSignature,
redrawVersion = 0, redrawVersion = 0,
horizontalScrollLeft = 0,
onHorizontalWheel,
}) => { }) => {
const laneRef = useRef<HTMLDivElement | null>(null); const laneRef = useRef<HTMLDivElement | null>(null);
const [laneHeight, setLaneHeight] = useState(MIN_LANE_HEIGHT); const [laneHeight, setLaneHeight] = useState(MIN_LANE_HEIGHT);
@@ -66,6 +70,28 @@ const PianoRollAutomationLane: React.FC<PianoRollAutomationLaneProps> = ({
}; };
}, []); }, []);
useEffect(() => {
const element = laneRef.current;
if (!element) {
return;
}
const handleWheel = (event: WheelEvent) => {
const horizontalDelta = Math.abs(event.deltaX) > 0 ? event.deltaX : (event.shiftKey ? event.deltaY : 0);
if (horizontalDelta === 0) {
return;
}
event.preventDefault();
onHorizontalWheel?.(horizontalDelta);
};
element.addEventListener('wheel', handleWheel, { passive: false });
return () => {
element.removeEventListener('wheel', handleWheel);
};
}, [onHorizontalWheel]);
const points = useMemo<AutomationPoint[]>(() => { const points = useMemo<AutomationPoint[]>(() => {
if (!activeRegion) { if (!activeRegion) {
return []; return [];
@@ -160,62 +186,67 @@ const PianoRollAutomationLane: React.FC<PianoRollAutomationLaneProps> = ({
> >
<div className="piano-roll-automation-track" style={{ width: totalWidth }}> <div className="piano-roll-automation-track" style={{ width: totalWidth }}>
<div className="piano-roll-automation-gutter" /> <div className="piano-roll-automation-gutter" />
<div className="piano-roll-automation-grid" />
<div className="piano-roll-automation-lane-label">{laneLabel}</div> <div className="piano-roll-automation-lane-label">{laneLabel}</div>
<svg <div
className="piano-roll-automation-svg" className="piano-roll-automation-scroll-layer"
width="100%" style={{ transform: `translateX(-${horizontalScrollLeft}px)` }}
height={laneHeight}
viewBox={`0 0 ${beatWidth * totalBeats + keyWidth} ${laneHeight}`}
preserveAspectRatio="none"
> >
{interpolationMode === 'linear' && points.length > 0 && ( <div className="piano-roll-automation-grid" />
<polyline <svg
className="piano-roll-automation-line" className="piano-roll-automation-svg"
fill="none" width="100%"
stroke={AUTOMATION_COLOR} height={laneHeight}
strokeWidth="2" viewBox={`0 0 ${beatWidth * totalBeats + keyWidth} ${laneHeight}`}
points={polylinePoints} preserveAspectRatio="none"
/> >
)} {interpolationMode === 'linear' && points.length > 0 && (
{interpolationMode === 'step' && stepSegments.map(segment => ( <polyline
<line className="piano-roll-automation-line"
key={segment.id} fill="none"
className="piano-roll-automation-line" stroke={AUTOMATION_COLOR}
x1={segment.x1} strokeWidth="2"
y1={segment.y1} points={polylinePoints}
x2={segment.x2} />
y2={segment.y1} )}
stroke={AUTOMATION_COLOR} {interpolationMode === 'step' && stepSegments.map(segment => (
strokeWidth="2" <line
/> key={segment.id}
))} className="piano-roll-automation-line"
{svgPoints.map(point => { x1={segment.x1}
const labelY = Math.max(14, Math.min(laneHeight - 6, point.y - 10)); y1={segment.y1}
x2={segment.x2}
y2={segment.y1}
stroke={AUTOMATION_COLOR}
strokeWidth="2"
/>
))}
{svgPoints.map(point => {
const labelY = Math.max(14, Math.min(laneHeight - 6, point.y - 10));
return ( return (
<g key={point.id}> <g key={point.id}>
<circle <circle
className="piano-roll-automation-point" className="piano-roll-automation-point"
cx={point.x} cx={point.x}
cy={point.y} cy={point.y}
r="5" r="5"
fill={AUTOMATION_COLOR} fill={AUTOMATION_COLOR}
stroke="#1d2428" stroke="#1d2428"
strokeWidth="2" strokeWidth="2"
/> />
<text <text
className="piano-roll-automation-value" className="piano-roll-automation-value"
x={point.x + 6} x={point.x + 6}
y={labelY} y={labelY}
fill={AUTOMATION_COLOR} fill={AUTOMATION_COLOR}
> >
{point.label} {point.label}
</text> </text>
</g> </g>
); );
})} })}
</svg> </svg>
</div>
{points.length === 0 && ( {points.length === 0 && (
<div className="piano-roll-automation-empty-state"> <div className="piano-roll-automation-empty-state">
No {laneLabel} events in this region No {laneLabel} events in this region
+20 -19
View File
@@ -76,7 +76,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
const isSpectrogram = mode === 'spectrogram'; const isSpectrogram = mode === 'spectrogram';
const showAutomationLane = automationEnabled && !isSpectrogram; const showAutomationLane = automationEnabled && !isSpectrogram;
const [spectrogramLoading, setSpectrogramLoading] = useState(false); const [spectrogramLoading, setSpectrogramLoading] = useState(false);
const [noteScrollTop, setNoteScrollTop] = useState(0); const [noteScrollLeft, setNoteScrollLeft] = useState(0);
const handleSpectrogramLoadingChange = useCallback((loading: boolean) => { const handleSpectrogramLoadingChange = useCallback((loading: boolean) => {
setSpectrogramLoading(loading); setSpectrogramLoading(loading);
}, []); }, []);
@@ -87,6 +87,15 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
// Recording state // Recording state
const isRecording = useProjectStore(s => s.isRecording); const isRecording = useProjectStore(s => s.isRecording);
const recordingNotes = useProjectStore(s => s.recordingNotes); const recordingNotes = useProjectStore(s => s.recordingNotes);
const forwardAutomationHorizontalWheel = useCallback((delta: number) => {
const container = noteScrollRef.current;
if (!container) {
return;
}
container.scrollLeft += delta;
setNoteScrollLeft(container.scrollLeft);
}, [noteScrollRef]);
// Use the note operations hook for resize and drag functionality // Use the note operations hook for resize and drag functionality
const { const {
@@ -290,23 +299,14 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
data-testid={showAutomationLane ? 'piano-roll-content-split' : 'piano-roll-content-single'} data-testid={showAutomationLane ? 'piano-roll-content-split' : 'piano-roll-content-single'}
> >
<div className={`piano-roll-main-section ${showAutomationLane ? 'with-automation' : ''}`}> <div className={`piano-roll-main-section ${showAutomationLane ? 'with-automation' : ''}`}>
<PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} /> <div
className="piano-roll-note-scroll"
<div className="piano-roll-body-shell"> ref={noteScrollRef}
<div className="piano-roll-keys-viewport"> onScroll={(event) => setNoteScrollLeft(event.currentTarget.scrollLeft)}
<div >
className="piano-roll-keys-offset" <PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} />
style={{ transform: `translateY(-${noteScrollTop}px)` }} <div className="piano-roll-body">
> <PianoKeys activeRegion={activeRegion} />
<PianoKeys activeRegion={activeRegion} />
</div>
</div>
<div
className="piano-roll-note-scroll"
ref={noteScrollRef}
onScroll={(event) => setNoteScrollTop(event.currentTarget.scrollTop)}
>
<div className="piano-roll-body">
<PianoGrid <PianoGrid
gridRef={pianoGridRef} gridRef={pianoGridRef}
onDoubleClick={isSpectrogram ? () => {} : handleGridDoubleClick} onDoubleClick={isSpectrogram ? () => {} : handleGridDoubleClick}
@@ -331,7 +331,6 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
{memoizedNotes} {memoizedNotes}
{!isSpectrogram && recordingNoteOverlays} {!isSpectrogram && recordingNoteOverlays}
</PianoGrid> </PianoGrid>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -344,6 +343,8 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
timeSignature={timeSignature} timeSignature={timeSignature}
bpm={bpm} bpm={bpm}
redrawVersion={automationRedrawVersion} redrawVersion={automationRedrawVersion}
horizontalScrollLeft={noteScrollLeft}
onHorizontalWheel={forwardAutomationHorizontalWheel}
/> />
</div> </div>
)} )}
+7 -1
View File
@@ -50,6 +50,12 @@ button {
transition: border-color 0.25s; transition: border-color 0.25s;
} }
button:focus,
button:focus-visible {
outline: none;
box-shadow: none;
}
textarea { textarea {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 12px; font-size: 12px;
@@ -73,4 +79,4 @@ input[type="range"]::-webkit-slider-thumb {
background: #e0e0e0; background: #e0e0e0;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
} }