feat: added mock up global tracks
This commit is contained in:
@@ -180,6 +180,113 @@
|
|||||||
min-height: fit-content;
|
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 {
|
.info-container {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -301,4 +301,52 @@ describe('MainContent', () => {
|
|||||||
fireEvent.click(globalTracksButton);
|
fireEvent.click(globalTracksButton);
|
||||||
expect(globalTracksButton.className).not.toContain('active');
|
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;
|
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> = ({
|
const MainContent: React.FC<MainContentProps> = ({
|
||||||
onTrackClick = () => { } // Default to empty function if not provided
|
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 [selectedRegionId, setSelectedRegionId] = useState<string | null>(null);
|
||||||
const [showCreateTrackModal, setShowCreateTrackModal] = useState(false);
|
const [showCreateTrackModal, setShowCreateTrackModal] = useState(false);
|
||||||
const [showGlobalTracksMock, setShowGlobalTracksMock] = useState(false);
|
const [showGlobalTracksMock, setShowGlobalTracksMock] = useState(false);
|
||||||
|
const [renderGlobalTracksMock, setRenderGlobalTracksMock] = useState(false);
|
||||||
|
const [animateGlobalTracksMock, setAnimateGlobalTracksMock] = useState(false);
|
||||||
|
|
||||||
// Use the region operations hook
|
// Use the region operations hook
|
||||||
const { deleteSelectedRegions } = useRegionOperations({
|
const { deleteSelectedRegions } = useRegionOperations({
|
||||||
@@ -1004,6 +1018,29 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore();
|
const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore();
|
||||||
const beatTicksPerBar = Math.max(0, timeSignature.numerator - 1);
|
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
|
// Helper function to check if a bar (0-indexed) is in the loop range
|
||||||
const isBarInLoopRange = (barIndex: number): boolean => {
|
const isBarInLoopRange = (barIndex: number): boolean => {
|
||||||
if (!isLooping) return false;
|
if (!isLooping) return false;
|
||||||
@@ -1036,10 +1073,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
className={`track-header-button${showGlobalTracksMock ? ' active' : ''}`}
|
className={`track-header-button${showGlobalTracksMock ? ' active' : ''}`}
|
||||||
aria-label="Show global tracks"
|
aria-label="Show global tracks"
|
||||||
title="Show global tracks"
|
title="Show global tracks"
|
||||||
onClick={(e) => {
|
onClick={handleToggleGlobalTracksMock}
|
||||||
e.stopPropagation();
|
|
||||||
setShowGlobalTracksMock(prev => !prev);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<FaSquareArrowUpRight />
|
<FaSquareArrowUpRight />
|
||||||
</button>
|
</button>
|
||||||
@@ -1073,6 +1107,54 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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}>
|
<div className="main-content-body" onClick={handleEmptyMainContentClick}>
|
||||||
{/* Fixed left panel with track info */}
|
{/* Fixed left panel with track info */}
|
||||||
<TrackInfoPanel
|
<TrackInfoPanel
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const Playhead: React.FC<PlayheadProps> = ({ context, pixelPositionOverride }) =
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: '2px',
|
width: '2px',
|
||||||
backgroundColor: '#4ECDC4', // Blue-green color similar to the reference image
|
backgroundColor: '#4ECDC4', // Blue-green color similar to the reference image
|
||||||
zIndex: 1000,
|
zIndex: 150,
|
||||||
pointerEvents: 'none', // Allow clicks to pass through
|
pointerEvents: 'none', // Allow clicks to pass through
|
||||||
boxShadow: '0 0 4px rgba(78, 205, 196, 0.5)', // Subtle glow effect
|
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',
|
borderLeft: '6px solid transparent',
|
||||||
borderRight: '6px solid transparent',
|
borderRight: '6px solid transparent',
|
||||||
borderTop: '8px solid #4ECDC4',
|
borderTop: '8px solid #4ECDC4',
|
||||||
zIndex: 1001,
|
zIndex: 151,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
--max-number-of-bars: 32;
|
--max-number-of-bars: 32;
|
||||||
--track-grid-bar-width: 40px;
|
--track-grid-bar-width: 40px;
|
||||||
--track-info-panel-width: 200px;
|
--track-info-panel-width: 200px;
|
||||||
|
--global-track-height: 20px;
|
||||||
--region-piano-key-width: 60px;
|
--region-piano-key-width: 60px;
|
||||||
--region-piano-key-height: 20px;
|
--region-piano-key-height: 20px;
|
||||||
--region-grid-beat-width: 40px;
|
--region-grid-beat-width: 40px;
|
||||||
|
|||||||
Reference in New Issue
Block a user