feat: refactor the add track behavior to use a pop-up instead
This commit is contained in:
@@ -3,14 +3,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto; /* Main scrollable container */
|
overflow: auto;
|
||||||
|
/* Main scrollable container */
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content-wrapper {
|
.main-content-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: calc(var(--track-info-panel-width) + var(--max-number-of-bars) * var(--track-grid-bar-width)); /* info width + grid width */
|
min-width: calc(var(--track-info-panel-width) + var(--max-number-of-bars) * var(--track-grid-bar-width));
|
||||||
|
/* info width + grid width */
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -24,12 +26,75 @@
|
|||||||
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;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-header-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button {
|
||||||
|
--toolbar-button-bg: transparent;
|
||||||
|
--toolbar-button-fg: #e0e0e0;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: var(--toolbar-button-bg);
|
||||||
|
color: var(--toolbar-button-fg);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.18s ease,
|
||||||
|
color 0.18s ease,
|
||||||
|
transform 0.12s ease,
|
||||||
|
box-shadow 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button svg {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button:hover {
|
||||||
|
--toolbar-button-bg: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button.active {
|
||||||
|
--toolbar-button-bg: #e0e0e0;
|
||||||
|
--toolbar-button-fg: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button.active:hover {
|
||||||
|
--toolbar-button-bg: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button:active {
|
||||||
|
--toolbar-button-bg: #5a5a5a;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button.active:active {
|
||||||
|
--toolbar-button-bg: #d6d6d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-header-button:focus,
|
||||||
|
.track-header-button:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Offset spacer when instrument selection panel is visible on the left */
|
/* Offset spacer when instrument selection panel is visible on the left */
|
||||||
.main-content.has-left-instrument .top-left-spacer {
|
.main-content.has-left-instrument .top-left-spacer {
|
||||||
left: 300px;
|
left: 300px;
|
||||||
@@ -43,9 +108,12 @@
|
|||||||
border-bottom: 1px solid #3a3a3a;
|
border-bottom: 1px solid #3a3a3a;
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
margin-left: var(--track-info-panel-width); /* Offset for info-container */
|
margin-left: var(--track-info-panel-width);
|
||||||
width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width)); /* Exact width for 32 bars */
|
/* Offset for info-container */
|
||||||
cursor: pointer; /* Show pointer cursor on hover to indicate interactivity */
|
width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width));
|
||||||
|
/* Exact width for 32 bars */
|
||||||
|
cursor: pointer;
|
||||||
|
/* Show pointer cursor on hover to indicate interactivity */
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-numbers:hover {
|
.bar-numbers:hover {
|
||||||
@@ -135,8 +203,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid-container {
|
.grid-container {
|
||||||
margin-left: 0; /* No need for margin since we're using flex */
|
margin-left: 0;
|
||||||
min-width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width)); /* Ensure minimum width */
|
/* No need for margin since we're using flex */
|
||||||
|
min-width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width));
|
||||||
|
/* Ensure minimum width */
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
position: relative; /* Required for absolute positioned playhead */
|
position: relative;
|
||||||
|
/* Required for absolute positioned playhead */
|
||||||
}
|
}
|
||||||
@@ -69,6 +69,13 @@ type StoreSelector = (...args: [typeof storeState]) => unknown;
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
type RegionClickHandler = (...args: [string, { shiftKey: boolean }]) => void;
|
type RegionClickHandler = (...args: [string, { shiftKey: boolean }]) => void;
|
||||||
|
|
||||||
|
const finishTrackCreateDialogClose = () => {
|
||||||
|
const overlay = document.querySelector('.dialog-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
fireEvent.animationEnd(overlay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
vi.mock('../stores/projectStore', () => ({
|
vi.mock('../stores/projectStore', () => ({
|
||||||
useProjectStore: (selector?: StoreSelector) => (
|
useProjectStore: (selector?: StoreSelector) => (
|
||||||
selector ? selector(storeState) : storeState
|
selector ? selector(storeState) : storeState
|
||||||
@@ -138,6 +145,8 @@ describe('MainContent', () => {
|
|||||||
storeState.setActiveRegionId.mockClear();
|
storeState.setActiveRegionId.mockClear();
|
||||||
storeState.openMidiPianoRoll.mockClear();
|
storeState.openMidiPianoRoll.mockClear();
|
||||||
storeState.openSpectrogramViewer.mockClear();
|
storeState.openSpectrogramViewer.mockClear();
|
||||||
|
storeState.addTrack.mockClear();
|
||||||
|
storeState.addAudioTrack.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates activeRegionId when selecting a region with piano roll closed', () => {
|
it('updates activeRegionId when selecting a region with piano roll closed', () => {
|
||||||
@@ -205,11 +214,11 @@ describe('MainContent', () => {
|
|||||||
expect(storeState.setActiveRegionId).toHaveBeenCalledWith(null);
|
expect(storeState.setActiveRegionId).toHaveBeenCalledWith(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the two-row timeline ruler with add-track buttons and beat markers', () => {
|
it('renders the two-row timeline ruler with track controls and beat markers', () => {
|
||||||
const { container } = render(<MainContent />);
|
const { container } = render(<MainContent />);
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: '+ MIDI' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Create track' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: '+ Audio' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Show global tracks' })).toBeInTheDocument();
|
||||||
|
|
||||||
const barCells = container.querySelectorAll('[data-testid="bar-number-cell"]');
|
const barCells = container.querySelectorAll('[data-testid="bar-number-cell"]');
|
||||||
expect(barCells).toHaveLength(storeState.maxBars);
|
expect(barCells).toHaveLength(storeState.maxBars);
|
||||||
@@ -230,4 +239,66 @@ describe('MainContent', () => {
|
|||||||
|
|
||||||
expect(firstBeatRow?.querySelectorAll('.beat-marker')).toHaveLength(2);
|
expect(firstBeatRow?.querySelectorAll('.beat-marker')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens the track creation modal with MIDI selected by default', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Create track' }));
|
||||||
|
|
||||||
|
expect(screen.getByRole('dialog', { name: 'Create New Track' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'MIDI' }).className).toContain('selected');
|
||||||
|
expect(screen.getByRole('button', { name: 'Audio' }).className).not.toContain('selected');
|
||||||
|
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Create' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an audio track when audio is selected in the modal', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Create track' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Audio' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Create' }));
|
||||||
|
finishTrackCreateDialogClose();
|
||||||
|
|
||||||
|
expect(storeState.addAudioTrack).toHaveBeenCalledTimes(1);
|
||||||
|
expect(storeState.addTrack).not.toHaveBeenCalled();
|
||||||
|
expect(screen.queryByRole('dialog', { name: 'Create New Track' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a MIDI track by default when confirming the modal', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Create track' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Create' }));
|
||||||
|
finishTrackCreateDialogClose();
|
||||||
|
|
||||||
|
expect(storeState.addTrack).toHaveBeenCalledTimes(1);
|
||||||
|
expect(storeState.addAudioTrack).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the track creation modal without creating a track when canceled', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Create track' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||||
|
finishTrackCreateDialogClose();
|
||||||
|
|
||||||
|
expect(storeState.addTrack).not.toHaveBeenCalled();
|
||||||
|
expect(storeState.addAudioTrack).not.toHaveBeenCalled();
|
||||||
|
expect(screen.queryByRole('dialog', { name: 'Create New Track' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles the mock global tracks button active state locally', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
const globalTracksButton = screen.getByRole('button', { name: 'Show global tracks' });
|
||||||
|
|
||||||
|
expect(globalTracksButton.className).not.toContain('active');
|
||||||
|
|
||||||
|
fireEvent.click(globalTracksButton);
|
||||||
|
expect(globalTracksButton.className).toContain('active');
|
||||||
|
|
||||||
|
fireEvent.click(globalTracksButton);
|
||||||
|
expect(globalTracksButton.className).not.toContain('active');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { KGAudioRegion } from '../core/region/KGAudioRegion';
|
|||||||
import TrackInfoPanel from './track/TrackInfoPanel';
|
import TrackInfoPanel from './track/TrackInfoPanel';
|
||||||
import TrackGridPanel from './track/TrackGridPanel';
|
import TrackGridPanel from './track/TrackGridPanel';
|
||||||
import PianoRoll from './piano-roll/PianoRoll';
|
import PianoRoll from './piano-roll/PianoRoll';
|
||||||
|
import { TrackCreateDialog } from './common';
|
||||||
import type { RegionClickOptions, RegionUI } from './interfaces';
|
import type { RegionClickOptions, RegionUI } from './interfaces';
|
||||||
import { DEBUG_MODE, BAR_NUMBERS_CONSTANTS, TOOLBAR_CONSTANTS } from '../constants';
|
import { DEBUG_MODE, BAR_NUMBERS_CONSTANTS, TOOLBAR_CONSTANTS } from '../constants';
|
||||||
import { useRegionOperations } from '../hooks/useRegionOperations';
|
import { useRegionOperations } from '../hooks/useRegionOperations';
|
||||||
@@ -17,6 +18,8 @@ import { regionDeleteManager } from '../util/regionDeleteUtil';
|
|||||||
import { KGMainContentState } from '../core/state/KGMainContentState';
|
import { KGMainContentState } from '../core/state/KGMainContentState';
|
||||||
import { ChangeLoopSettingsCommand } from '../core/commands';
|
import { ChangeLoopSettingsCommand } from '../core/commands';
|
||||||
import { DeleteTrackAutomationPointsCommand } from '../core/commands';
|
import { DeleteTrackAutomationPointsCommand } from '../core/commands';
|
||||||
|
import { FaPlus } from 'react-icons/fa';
|
||||||
|
import { FaSquareArrowUpRight } from 'react-icons/fa6';
|
||||||
|
|
||||||
interface MainContentProps {
|
interface MainContentProps {
|
||||||
onTrackClick?: () => void;
|
onTrackClick?: () => void;
|
||||||
@@ -76,6 +79,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
// Region selection state
|
// Region selection state
|
||||||
const [selectedRegionId, setSelectedRegionId] = useState<string | null>(null);
|
const [selectedRegionId, setSelectedRegionId] = useState<string | null>(null);
|
||||||
|
const [showCreateTrackModal, setShowCreateTrackModal] = useState(false);
|
||||||
|
const [showGlobalTracksMock, setShowGlobalTracksMock] = useState(false);
|
||||||
|
|
||||||
// Use the region operations hook
|
// Use the region operations hook
|
||||||
const { deleteSelectedRegions } = useRegionOperations({
|
const { deleteSelectedRegions } = useRegionOperations({
|
||||||
@@ -763,6 +768,11 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
setActiveRegionId(null);
|
setActiveRegionId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCreateTrackModal = useCallback((e?: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
setShowCreateTrackModal(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add keyboard event listener for region deletion
|
* Add keyboard event listener for region deletion
|
||||||
* Handles Backspace (Windows) and Delete (Mac) keys to delete selected regions
|
* Handles Backspace (Windows) and Delete (Mac) keys to delete selected regions
|
||||||
@@ -1011,8 +1021,29 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
<div className="main-content-wrapper" onClick={handleEmptyMainContentClick}>
|
<div className="main-content-wrapper" onClick={handleEmptyMainContentClick}>
|
||||||
{/* Top-left spacer */}
|
{/* Top-left spacer */}
|
||||||
<div className="top-left-spacer">
|
<div className="top-left-spacer">
|
||||||
<button className="add-track-btn" onClick={() => addTrack()}>+ MIDI</button>
|
<div className="track-header-controls">
|
||||||
<button className="add-track-btn" onClick={() => addAudioTrack()}>+ Audio</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="track-header-button"
|
||||||
|
aria-label="Create track"
|
||||||
|
title="Create track"
|
||||||
|
onClick={openCreateTrackModal}
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`track-header-button${showGlobalTracksMock ? ' active' : ''}`}
|
||||||
|
aria-label="Show global tracks"
|
||||||
|
title="Show global tracks"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowGlobalTracksMock(prev => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaSquareArrowUpRight />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bar numbers at the top */}
|
{/* Bar numbers at the top */}
|
||||||
@@ -1077,6 +1108,18 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Piano Roll / Spectrogram Viewer - render using portal */}
|
{/* Piano Roll / Spectrogram Viewer - render using portal */}
|
||||||
|
{showCreateTrackModal && (
|
||||||
|
<TrackCreateDialog
|
||||||
|
onResolve={(result) => {
|
||||||
|
setShowCreateTrackModal(false);
|
||||||
|
if (result === 'audio') {
|
||||||
|
addAudioTrack();
|
||||||
|
} else if (result === 'midi') {
|
||||||
|
addTrack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showPianoRoll && createPortal(
|
{showPianoRoll && createPortal(
|
||||||
<PianoRoll
|
<PianoRoll
|
||||||
onClose={handlePianoRollClose}
|
onClose={handlePianoRollClose}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
.track-create-dialog-modal {
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-dialog-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-option {
|
||||||
|
border: 1px solid #4a4a4a;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #323232 0%, #282828 100%);
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 28px 18px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.18s ease,
|
||||||
|
background-color 0.18s ease,
|
||||||
|
box-shadow 0.18s ease,
|
||||||
|
transform 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-option:hover {
|
||||||
|
border-color: #6a6a6a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-option.selected {
|
||||||
|
border-color: #e1ae01;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(225, 174, 1, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-option-icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-option-icon.midi {
|
||||||
|
background-color: #159947;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-option-icon.audio {
|
||||||
|
background-color: #1f78d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-create-option-label {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { FaMusic, FaTimes } from 'react-icons/fa';
|
||||||
|
import { FaWaveSquare } from 'react-icons/fa6';
|
||||||
|
import './DialogProvider.css';
|
||||||
|
import './TrackCreateDialog.css';
|
||||||
|
|
||||||
|
type TrackCreateDialogResult = 'cancel' | 'midi' | 'audio';
|
||||||
|
|
||||||
|
interface TrackCreateDialogProps {
|
||||||
|
onResolve: (result: TrackCreateDialogResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrackCreateDialog: React.FC<TrackCreateDialogProps> = ({ onResolve }) => {
|
||||||
|
const [selectedTrackType, setSelectedTrackType] = useState<'midi' | 'audio'>('midi');
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const pendingResultRef = useRef<TrackCreateDialogResult>('cancel');
|
||||||
|
const mouseDownOnOverlay = useRef(false);
|
||||||
|
|
||||||
|
const close = useCallback((result: TrackCreateDialogResult) => {
|
||||||
|
pendingResultRef.current = result;
|
||||||
|
setIsClosing(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
close('cancel');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [close]);
|
||||||
|
|
||||||
|
const handleAnimationEnd = useCallback((e: React.AnimationEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target !== e.currentTarget) return;
|
||||||
|
if (!isClosing) return;
|
||||||
|
onResolve(pendingResultRef.current);
|
||||||
|
}, [isClosing, onResolve]);
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className={`dialog-overlay${isClosing ? ' dialog-overlay-closing' : ''}`}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
mouseDownOnOverlay.current = e.target === e.currentTarget;
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget && mouseDownOnOverlay.current) {
|
||||||
|
close('cancel');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onAnimationEnd={handleAnimationEnd}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`dialog-modal track-create-dialog-modal${isClosing ? ' dialog-modal-closing' : ''}`}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="track-create-title"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="dialog-header">
|
||||||
|
<h3 id="track-create-title" className="dialog-title">Create New Track</h3>
|
||||||
|
<button
|
||||||
|
className="dialog-close-btn"
|
||||||
|
onClick={() => close('cancel')}
|
||||||
|
aria-label="Close dialog"
|
||||||
|
>
|
||||||
|
<FaTimes />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-body track-create-dialog-body">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`track-create-option${selectedTrackType === 'midi' ? ' selected' : ''}`}
|
||||||
|
onClick={() => setSelectedTrackType('midi')}
|
||||||
|
>
|
||||||
|
<div className="track-create-option-icon midi">
|
||||||
|
<FaMusic />
|
||||||
|
</div>
|
||||||
|
<span className="track-create-option-label">MIDI</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`track-create-option${selectedTrackType === 'audio' ? ' selected' : ''}`}
|
||||||
|
onClick={() => setSelectedTrackType('audio')}
|
||||||
|
>
|
||||||
|
<div className="track-create-option-icon audio">
|
||||||
|
<FaWaveSquare />
|
||||||
|
</div>
|
||||||
|
<span className="track-create-option-label">Audio</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="dialog-btn dialog-btn-cancel"
|
||||||
|
onClick={() => close('cancel')}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="dialog-btn dialog-btn-primary"
|
||||||
|
onClick={() => close(selectedTrackType)}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrackCreateDialog;
|
||||||
@@ -4,5 +4,6 @@ export { default as FileImportModal } from './FileImportModal';
|
|||||||
export { default as LoadingOverlay } from './LoadingOverlay';
|
export { default as LoadingOverlay } from './LoadingOverlay';
|
||||||
export { default as OpenProjectModal } from './OpenProjectModal';
|
export { default as OpenProjectModal } from './OpenProjectModal';
|
||||||
export { default as DialogProvider } from './DialogProvider';
|
export { default as DialogProvider } from './DialogProvider';
|
||||||
|
export { default as TrackCreateDialog } from './TrackCreateDialog';
|
||||||
export { showAlert, showConfirm, showPrompt, showTimeSigPrompt } from '../../util/dialogUtil';
|
export { showAlert, showConfirm, showPrompt, showTimeSigPrompt } from '../../util/dialogUtil';
|
||||||
export type { ConfirmOptions, PromptOptions, TimeSigResult } from '../../util/dialogUtil';
|
export type { ConfirmOptions, PromptOptions, TimeSigResult } from '../../util/dialogUtil';
|
||||||
Reference in New Issue
Block a user