feat: added rules row under the main grid bar number row

This commit is contained in:
Xiaohan-Tian
2026-05-23 10:53:05 -07:00
parent 1964f1279f
commit a7071f318c
3 changed files with 81 additions and 7 deletions
+41 -6
View File
@@ -20,13 +20,14 @@
top: 50px; top: 50px;
left: 0; left: 0;
width: var(--track-info-panel-width); width: var(--track-info-panel-width);
height: 20px; height: 40px;
background-color: #2d2d2d; background-color: #2d2d2d;
border-bottom: 1px solid #3a3a3a; border-bottom: 1px solid #3a3a3a;
border-right: 1px solid #3a3a3a; border-right: 1px solid #3a3a3a;
z-index: 1002; /* Higher than other elements to ensure it's always visible */ z-index: 1002; /* Higher than other elements to ensure it's always visible */
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: stretch;
} }
/* Offset spacer when instrument selection panel is visible on the left */ /* Offset spacer when instrument selection panel is visible on the left */
@@ -37,7 +38,7 @@
.bar-numbers { .bar-numbers {
position: sticky; position: sticky;
top: 0; top: 0;
height: 20px; height: 40px;
display: flex; display: flex;
border-bottom: 1px solid #3a3a3a; border-bottom: 1px solid #3a3a3a;
background-color: #2d2d2d; background-color: #2d2d2d;
@@ -56,17 +57,51 @@
width: var(--track-grid-bar-width); width: var(--track-grid-bar-width);
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
height: 20px; height: 40px;
text-align: center;
font-size: 12px;
border-right: 1px solid #3a3a3a; border-right: 1px solid #3a3a3a;
color: #999; color: #999;
display: flex; 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; align-items: center;
justify-content: 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; 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 { .bar-number-cell.looped {
background-color: #e1ae01; background-color: #e1ae01;
color: #1e1e1e; color: #1e1e1e;
@@ -91,7 +126,7 @@
Only shift the fixed spacer when the left panel is present. */ Only shift the fixed spacer when the left panel is present. */
.track-top-spacer { .track-top-spacer {
height: 20px; height: 40px;
border-bottom: 1px solid #3a3a3a; border-bottom: 1px solid #3a3a3a;
background-color: #2d2d2d; background-color: #2d2d2d;
position: sticky; position: sticky;
+27
View File
@@ -131,6 +131,7 @@ describe('MainContent', () => {
storeState.selectedRegionIds = []; storeState.selectedRegionIds = [];
storeState.activeRegionId = null; storeState.activeRegionId = null;
storeState.showPianoRoll = false; storeState.showPianoRoll = false;
storeState.timeSignature = { numerator: 4, denominator: 4 };
storeState.clearAllSelections.mockClear(); storeState.clearAllSelections.mockClear();
storeState.setSelectedTrack.mockClear(); storeState.setSelectedTrack.mockClear();
storeState.setShowPianoRoll.mockClear(); storeState.setShowPianoRoll.mockClear();
@@ -203,4 +204,30 @@ describe('MainContent', () => {
expect(storeState.setShowPianoRoll).toHaveBeenCalledWith(false); expect(storeState.setShowPianoRoll).toHaveBeenCalledWith(false);
expect(storeState.setActiveRegionId).toHaveBeenCalledWith(null); expect(storeState.setActiveRegionId).toHaveBeenCalledWith(null);
}); });
it('renders the two-row timeline ruler with add-track buttons and beat markers', () => {
const { container } = render(<MainContent />);
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(<MainContent />);
const firstBeatRow = container.querySelector('[data-testid="bar-beat-markers"]');
expect(firstBeatRow?.querySelectorAll('.beat-marker')).toHaveLength(2);
});
}); });
+13 -1
View File
@@ -992,6 +992,7 @@ const MainContent: React.FC<MainContentProps> = ({
}, [calculateBarIndexFromMouse, calculatePlayheadFromMouse, setPlayheadPosition, requestPianoRollScroll]); }, [calculateBarIndexFromMouse, calculatePlayheadFromMouse, setPlayheadPosition, requestPianoRollScroll]);
const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore(); 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 // Helper function to check if a bar (0-indexed) is in the loop range
const isBarInLoopRange = (barIndex: number): boolean => { const isBarInLoopRange = (barIndex: number): boolean => {
@@ -1024,8 +1025,19 @@ const MainContent: React.FC<MainContentProps> = ({
<div <div
key={i} key={i}
className={`bar-number-cell${isBarInLoopRange(i) ? ' looped' : ''}`} className={`bar-number-cell${isBarInLoopRange(i) ? ' looped' : ''}`}
data-testid="bar-number-cell"
> >
{i + 1} <div className="bar-number-label">{i + 1}</div>
<div className="bar-beat-markers" data-testid="bar-beat-markers">
<div className="bar-boundary-marker" />
{Array.from({ length: beatTicksPerBar }, (_, beatIndex) => (
<div
key={beatIndex}
className="beat-marker"
style={{ left: `${((beatIndex + 1) / timeSignature.numerator) * 100}%` }}
/>
))}
</div>
</div> </div>
))} ))}
</div> </div>