diff --git a/src/components/MainContent.css b/src/components/MainContent.css index 2357b90..5f35e2e 100644 --- a/src/components/MainContent.css +++ b/src/components/MainContent.css @@ -20,13 +20,14 @@ top: 50px; left: 0; width: var(--track-info-panel-width); - height: 20px; + height: 40px; background-color: #2d2d2d; border-bottom: 1px solid #3a3a3a; border-right: 1px solid #3a3a3a; z-index: 1002; /* Higher than other elements to ensure it's always visible */ display: flex; flex-direction: row; + align-items: stretch; } /* Offset spacer when instrument selection panel is visible on the left */ @@ -37,7 +38,7 @@ .bar-numbers { position: sticky; top: 0; - height: 20px; + height: 40px; display: flex; border-bottom: 1px solid #3a3a3a; background-color: #2d2d2d; @@ -56,17 +57,51 @@ width: var(--track-grid-bar-width); flex-shrink: 0; flex-grow: 0; - height: 20px; - text-align: center; - font-size: 12px; + height: 40px; border-right: 1px solid #3a3a3a; color: #999; display: flex; + flex-direction: column; + box-sizing: border-box; + position: relative; +} + +.bar-number-label { + height: 20px; + text-align: center; + font-size: 12px; + display: flex; align-items: center; justify-content: center; +} + +.bar-beat-markers { + position: relative; + height: 20px; + border-top: 1px solid #3a3a3a; + border-bottom: 1px solid #3a3a3a; + background-color: #1e1e1e; box-sizing: border-box; } +.bar-boundary-marker { + position: absolute; + left: -1px; + bottom: 0; + width: 1px; + height: 100%; + background-color: #3a3a3a; +} + +.beat-marker { + position: absolute; + bottom: 0; + width: 1px; + height: 50%; + background-color: #3a3a3a; + transform: translateX(-50%); +} + .bar-number-cell.looped { background-color: #e1ae01; color: #1e1e1e; @@ -91,7 +126,7 @@ Only shift the fixed spacer when the left panel is present. */ .track-top-spacer { - height: 20px; + height: 40px; border-bottom: 1px solid #3a3a3a; background-color: #2d2d2d; position: sticky; diff --git a/src/components/MainContent.test.tsx b/src/components/MainContent.test.tsx index 5d24208..fef3c13 100644 --- a/src/components/MainContent.test.tsx +++ b/src/components/MainContent.test.tsx @@ -131,6 +131,7 @@ describe('MainContent', () => { storeState.selectedRegionIds = []; storeState.activeRegionId = null; storeState.showPianoRoll = false; + storeState.timeSignature = { numerator: 4, denominator: 4 }; storeState.clearAllSelections.mockClear(); storeState.setSelectedTrack.mockClear(); storeState.setShowPianoRoll.mockClear(); @@ -203,4 +204,30 @@ describe('MainContent', () => { expect(storeState.setShowPianoRoll).toHaveBeenCalledWith(false); expect(storeState.setActiveRegionId).toHaveBeenCalledWith(null); }); + + it('renders the two-row timeline ruler with add-track buttons and beat markers', () => { + const { container } = render(); + + expect(screen.getByRole('button', { name: '+ MIDI' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '+ Audio' })).toBeInTheDocument(); + + const barCells = container.querySelectorAll('[data-testid="bar-number-cell"]'); + expect(barCells).toHaveLength(storeState.maxBars); + expect(barCells[0]).toHaveTextContent('1'); + expect(barCells[storeState.maxBars - 1]).toHaveTextContent(String(storeState.maxBars)); + + const beatRows = container.querySelectorAll('[data-testid="bar-beat-markers"]'); + expect(beatRows).toHaveLength(storeState.maxBars); + expect(beatRows[0].querySelectorAll('.beat-marker')).toHaveLength(storeState.timeSignature.numerator - 1); + expect(beatRows[0].querySelector('.bar-boundary-marker')).not.toBeNull(); + }); + + it('updates lower-row beat ticks when the time signature numerator changes', () => { + storeState.timeSignature = { numerator: 3, denominator: 4 }; + + const { container } = render(); + const firstBeatRow = container.querySelector('[data-testid="bar-beat-markers"]'); + + expect(firstBeatRow?.querySelectorAll('.beat-marker')).toHaveLength(2); + }); }); diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx index ab677ec..f2e385c 100644 --- a/src/components/MainContent.tsx +++ b/src/components/MainContent.tsx @@ -992,6 +992,7 @@ const MainContent: React.FC = ({ }, [calculateBarIndexFromMouse, calculatePlayheadFromMouse, setPlayheadPosition, requestPianoRollScroll]); const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore(); + const beatTicksPerBar = Math.max(0, timeSignature.numerator - 1); // Helper function to check if a bar (0-indexed) is in the loop range const isBarInLoopRange = (barIndex: number): boolean => { @@ -1024,8 +1025,19 @@ const MainContent: React.FC = ({
- {i + 1} +
{i + 1}
+
+
+ {Array.from({ length: beatTicksPerBar }, (_, beatIndex) => ( +
+ ))} +
))}