feat: added mock up global tracks

This commit is contained in:
Xiaohan-Tian
2026-05-23 12:29:03 -07:00
parent 041641c101
commit 7ca2cb0d05
5 changed files with 245 additions and 7 deletions
+107
View File
@@ -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;
+48
View File
@@ -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(<MainContent />);
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(<MainContent />);
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();
});
});
+86 -4
View File
@@ -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<MainContentProps> = ({
onTrackClick = () => { } // Default to empty function if not provided
}) => {
@@ -81,6 +93,8 @@ const MainContent: React.FC<MainContentProps> = ({
const [selectedRegionId, setSelectedRegionId] = useState<string | null>(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<MainContentProps> = ({
const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore();
const beatTicksPerBar = Math.max(0, timeSignature.numerator - 1);
const handleToggleGlobalTracksMock = (e: React.MouseEvent<HTMLButtonElement>) => {
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<MainContentProps> = ({
className={`track-header-button${showGlobalTracksMock ? ' active' : ''}`}
aria-label="Show global tracks"
title="Show global tracks"
onClick={(e) => {
e.stopPropagation();
setShowGlobalTracksMock(prev => !prev);
}}
onClick={handleToggleGlobalTracksMock}
>
<FaSquareArrowUpRight />
</button>
@@ -1073,6 +1107,54 @@ const MainContent: React.FC<MainContentProps> = ({
))}
</div>
{renderGlobalTracksMock && (
<div
className="global-tracks-section"
aria-label="Global tracks"
aria-hidden={!showGlobalTracksMock}
style={{ ['--global-track-count' as string]: String(GLOBAL_TRACKS.length) }}
>
<div
className={`global-tracks-info-shell${animateGlobalTracksMock ? ' expanded' : ' collapsed'}`}
onTransitionEnd={() => {
if (!showGlobalTracksMock) {
setRenderGlobalTracksMock(false);
}
}}
>
<div className="global-tracks-info">
{GLOBAL_TRACKS.map(track => (
<div key={track.id} className="global-track-info-row">
<span className="global-track-name">{track.label}</span>
<button
type="button"
className="global-track-add-button"
aria-label={`Add ${track.label} global track item`}
title={`Add ${track.label} global track item`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<FaPlus />
</button>
</div>
))}
</div>
</div>
<div className={`global-tracks-grid-shell${animateGlobalTracksMock ? ' expanded' : ' collapsed'}`}>
<div className="global-tracks-grid" aria-hidden="true">
{GLOBAL_TRACKS.map(track => (
<div
key={track.id}
className="global-track-grid-row"
/>
))}
</div>
</div>
</div>
)}
<div className="main-content-body" onClick={handleEmptyMainContentClick}>
{/* Fixed left panel with track info */}
<TrackInfoPanel
+2 -2
View File
@@ -55,7 +55,7 @@ const Playhead: React.FC<PlayheadProps> = ({ 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<PlayheadProps> = ({ context, pixelPositionOverride }) =
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderTop: '8px solid #4ECDC4',
zIndex: 1001,
zIndex: 151,
pointerEvents: 'none',
};
+1
View File
@@ -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;