feat: added rules row under the main grid bar number row
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user