feat: added mock up global tracks
This commit is contained in:
@@ -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;
|
||||
@@ -210,4 +317,4 @@
|
||||
min-height: fit-content;
|
||||
position: relative;
|
||||
/* Required for absolute positioned playhead */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user