From 7ca2cb0d05cff32503c31e8245f61e0dbd5f7013 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Sat, 23 May 2026 12:29:03 -0700 Subject: [PATCH] feat: added mock up global tracks --- src/components/MainContent.css | 109 +++++++++++++++++++++++++++- src/components/MainContent.test.tsx | 48 ++++++++++++ src/components/MainContent.tsx | 90 ++++++++++++++++++++++- src/components/common/Playhead.tsx | 4 +- src/styles/variables.css | 1 + 5 files changed, 245 insertions(+), 7 deletions(-) diff --git a/src/components/MainContent.css b/src/components/MainContent.css index 24e0047..19b92f5 100644 --- a/src/components/MainContent.css +++ b/src/components/MainContent.css @@ -180,6 +180,113 @@ min-height: fit-content; } +.global-tracks-section { + display: flex; + position: relative; + z-index: 1003; +} + +.global-tracks-info-shell, +.global-tracks-grid-shell { + max-height: calc(var(--global-track-count) * var(--global-track-height)); + opacity: 1; + transform: translateY(0); + transition: + max-height 0.22s ease, + opacity 0.18s ease, + transform 0.22s ease; + will-change: max-height, opacity, transform; +} + +.global-tracks-info-shell { + position: sticky; + left: 0; + width: var(--track-info-panel-width); + z-index: 1003; + overflow: hidden; + align-self: flex-start; +} + +.global-tracks-grid-shell { + overflow: hidden; +} + +.global-tracks-info-shell.collapsed, +.global-tracks-grid-shell.collapsed { + max-height: 0; + opacity: 0; + transform: translateY(-8px); + pointer-events: none; +} + +.global-tracks-info { + width: var(--track-info-panel-width); + background-color: #2d2d2d; + border-right: 1px solid #3a3a3a; +} + +.global-track-info-row { + height: var(--global-track-height); + padding: 0 10px 0 12px; + border-bottom: 1px solid #3a3a3a; + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + color: #e0e0e0; + background-color: #4a4a4a; +} + +.global-track-name { + font-size: 11px; + font-weight: 500; + letter-spacing: 0.01em; +} + +.global-track-add-button { + width: 16px; + height: 16px; + border: 1px solid #e0e0e0; + border-radius: 999px; + background: transparent; + color: #e0e0e0; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + cursor: default; +} + +.global-track-add-button svg { + font-size: 9px; +} + +.global-track-add-button:focus, +.global-track-add-button:focus-visible { + outline: none; + box-shadow: none; +} + +.global-tracks-grid { + min-width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width)); + background-color: #2d2d2d; +} + +.global-track-grid-row { + height: var(--global-track-height); + min-width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width)); + box-sizing: border-box; + border-bottom: 1px solid #3a3a3a; + background-color: #2d2d2d; + background-size: var(--track-grid-bar-width) var(--global-track-height); + background-image: linear-gradient( + to right, + transparent calc(var(--track-grid-bar-width) - 1px), + #3a3a3a calc(var(--track-grid-bar-width) - 1px), + #3a3a3a var(--track-grid-bar-width) + ); +} + .info-container { position: sticky; left: 0; @@ -210,4 +317,4 @@ min-height: fit-content; position: relative; /* Required for absolute positioned playhead */ -} \ No newline at end of file +} diff --git a/src/components/MainContent.test.tsx b/src/components/MainContent.test.tsx index f5151ff..a48abea 100644 --- a/src/components/MainContent.test.tsx +++ b/src/components/MainContent.test.tsx @@ -301,4 +301,52 @@ describe('MainContent', () => { fireEvent.click(globalTracksButton); expect(globalTracksButton.className).not.toContain('active'); }); + + it('renders the four mock global tracks only when toggled on', () => { + render(); + + expect(screen.queryByText('Marker')).not.toBeInTheDocument(); + expect(screen.queryByText('Tempo')).not.toBeInTheDocument(); + expect(screen.queryByText('Signature')).not.toBeInTheDocument(); + expect(screen.queryByText('Chord')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' })); + + expect(screen.getByText('Marker')).toBeInTheDocument(); + expect(screen.getByText('Tempo')).toBeInTheDocument(); + expect(screen.getByText('Signature')).toBeInTheDocument(); + expect(screen.getByText('Chord')).toBeInTheDocument(); + + const globalTracksInfoShell = screen.getByRole('button', { name: 'Add Marker global track item' }).closest('.global-tracks-info-shell') as HTMLElement; + + fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' })); + fireEvent.transitionEnd(globalTracksInfoShell); + + expect(screen.queryByText('Marker')).not.toBeInTheDocument(); + expect(screen.queryByText('Tempo')).not.toBeInTheDocument(); + expect(screen.queryByText('Signature')).not.toBeInTheDocument(); + expect(screen.queryByText('Chord')).not.toBeInTheDocument(); + }); + + it('renders visual-only add buttons for mock global tracks without mutating track state', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' })); + + const addButtons = [ + screen.getByRole('button', { name: 'Add Marker global track item' }), + screen.getByRole('button', { name: 'Add Tempo global track item' }), + screen.getByRole('button', { name: 'Add Signature global track item' }), + screen.getByRole('button', { name: 'Add Chord global track item' }), + ]; + + expect(addButtons).toHaveLength(4); + + addButtons.forEach(button => { + fireEvent.click(button); + }); + + expect(storeState.addTrack).not.toHaveBeenCalled(); + expect(storeState.addAudioTrack).not.toHaveBeenCalled(); + }); }); diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx index 54942fa..32fdf09 100644 --- a/src/components/MainContent.tsx +++ b/src/components/MainContent.tsx @@ -25,6 +25,18 @@ interface MainContentProps { onTrackClick?: () => void; } +interface GlobalTrackDefinition { + id: 'marker' | 'tempo' | 'signature' | 'chord'; + label: string; +} + +const GLOBAL_TRACKS: GlobalTrackDefinition[] = [ + { id: 'marker', label: 'Marker' }, + { id: 'tempo', label: 'Tempo' }, + { id: 'signature', label: 'Signature' }, + { id: 'chord', label: 'Chord' }, +]; + const MainContent: React.FC = ({ onTrackClick = () => { } // Default to empty function if not provided }) => { @@ -81,6 +93,8 @@ const MainContent: React.FC = ({ const [selectedRegionId, setSelectedRegionId] = useState(null); const [showCreateTrackModal, setShowCreateTrackModal] = useState(false); const [showGlobalTracksMock, setShowGlobalTracksMock] = useState(false); + const [renderGlobalTracksMock, setRenderGlobalTracksMock] = useState(false); + const [animateGlobalTracksMock, setAnimateGlobalTracksMock] = useState(false); // Use the region operations hook const { deleteSelectedRegions } = useRegionOperations({ @@ -1004,6 +1018,29 @@ const MainContent: React.FC = ({ const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore(); const beatTicksPerBar = Math.max(0, timeSignature.numerator - 1); + const handleToggleGlobalTracksMock = (e: React.MouseEvent) => { + e.stopPropagation(); + + if (!showGlobalTracksMock) { + setRenderGlobalTracksMock(true); + setShowGlobalTracksMock(true); + return; + } + + setAnimateGlobalTracksMock(false); + setShowGlobalTracksMock(false); + }; + + useEffect(() => { + if (!renderGlobalTracksMock || !showGlobalTracksMock || animateGlobalTracksMock) return; + + const frame = requestAnimationFrame(() => { + setAnimateGlobalTracksMock(true); + }); + + return () => cancelAnimationFrame(frame); + }, [renderGlobalTracksMock, showGlobalTracksMock, animateGlobalTracksMock]); + // Helper function to check if a bar (0-indexed) is in the loop range const isBarInLoopRange = (barIndex: number): boolean => { if (!isLooping) return false; @@ -1036,10 +1073,7 @@ const MainContent: React.FC = ({ className={`track-header-button${showGlobalTracksMock ? ' active' : ''}`} aria-label="Show global tracks" title="Show global tracks" - onClick={(e) => { - e.stopPropagation(); - setShowGlobalTracksMock(prev => !prev); - }} + onClick={handleToggleGlobalTracksMock} > @@ -1073,6 +1107,54 @@ const MainContent: React.FC = ({ ))} + {renderGlobalTracksMock && ( +
+
{ + if (!showGlobalTracksMock) { + setRenderGlobalTracksMock(false); + } + }} + > +
+ {GLOBAL_TRACKS.map(track => ( +
+ {track.label} + +
+ ))} +
+
+
+ +
+ )} +
{/* Fixed left panel with track info */} = ({ context, pixelPositionOverride }) = bottom: 0, width: '2px', backgroundColor: '#4ECDC4', // Blue-green color similar to the reference image - zIndex: 1000, + zIndex: 150, pointerEvents: 'none', // Allow clicks to pass through boxShadow: '0 0 4px rgba(78, 205, 196, 0.5)', // Subtle glow effect }; @@ -70,7 +70,7 @@ const Playhead: React.FC = ({ context, pixelPositionOverride }) = borderLeft: '6px solid transparent', borderRight: '6px solid transparent', borderTop: '8px solid #4ECDC4', - zIndex: 1001, + zIndex: 151, pointerEvents: 'none', }; diff --git a/src/styles/variables.css b/src/styles/variables.css index 8d30d51..a53bd43 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -3,6 +3,7 @@ --max-number-of-bars: 32; --track-grid-bar-width: 40px; --track-info-panel-width: 200px; + --global-track-height: 20px; --region-piano-key-width: 60px; --region-piano-key-height: 20px; --region-grid-beat-width: 40px;