feat: added rules row under the main grid bar number row
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(<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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -992,6 +992,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
}, [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<MainContentProps> = ({
|
||||
<div
|
||||
key={i}
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user