Files
SonicForgeStudio/.kilo/plans/1784817475131-track-ui-fixes.md
T
2026-07-24 20:33:01 +07:00

8.0 KiB

Plan: TCP Resizable Width + Instrument Search Dropdown + Section Save Fix

Task 1: User-Resizable TCP Width

Files: app/static/js/app.jsx

Root Cause

TCP containers are hardcoded w-[320px] (lines 14253, 14809). Components like Synth button, FX button, volume/pan sliders, input select overflow when content is wide.

Implementation Steps

1a — Add TCP width state Add near line 6394 (near existing rightSidebarWidth state):

const [tcpWidth, setTcpWidth] = useState(320);

1b — Add TCP resize handler Add near line 6365 (near startColResize):

const startTcpResize = e => {
    e.preventDefault();
    const startX = e.clientX;
    const startW = tcpWidth;
    const onMove = ev => {
        const deltaX = ev.clientX - startX;
        const newWidth = Math.max(280, Math.min(600, startW + deltaX));
        setTcpWidth(newWidth);
    };
    const onUp = () => {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
};

1c — Replace w-[320px] with dynamic width in main TCP container (line 14253) Change className: "w-[320px] shrink-0 ..." to style: { width: tcpWidth + 'px', ... }.

1d — Replace w-[320px] with dynamic width in sub-tab TCP container (line 14809) Same pattern as 1c.

1e — Add resize handle (right edge of TCP) Add a vertical resize handle bar on the right edge of both TCP containers. Pattern:

React.createElement("div", {
    onMouseDown: startTcpResize,
    className: "absolute right-0 top-0 bottom-0 w-1 cursor-col-resize z-40 hover:bg-cyan-500/50 transition-colors",
    style: { right: 0 }
})

1f — Ensure the main layout accommodates variable TCP width The main timeline area should use flex-1 so it fills remaining space. Verify existing layout handles this.

Verification

  • Drag TCP right edge → width changes between 280px and 600px
  • Components fit properly at various widths
  • Timeline area fills remaining space
  • Works in section-tab view too

Task 2: Instrument Search Dropdown in TCP

Files: app/static/js/app.jsx

Root Cause

Current instrument selector is a modal overlay (lines 15661-15726) with no search/filter. Requires clicking Synth button → modal → scroll to find instrument.

Implementation Steps

2a — Add per-track dropdown open/close state Add state:

const [instrumentDropdownTrackId, setInstrumentDropdownTrackId] = useState(null);

This tracks which track's dropdown is open (null = all closed).

2b — Add search query state

const [instrumentSearchQuery, setInstrumentSearchQuery] = useState('');

2c — Replace Synth button (top toolbar, line 14444-14448) with dropdown toggle Convert the icon-only <button> into a container that:

  1. Shows current instrument name (truncated) + chevron-down icon when assigned
  2. Shows "Synth" + chevron-down icon when no instrument
  3. Click toggles instrumentDropdownTrackId for this track

2d — Render the dropdown panel (conditional, below the button) When instrumentDropdownTrackId === track.id, render a dropdown panel:

React.createElement("div", {
    className: "absolute left-0 top-full mt-0.5 z-50 bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",
    onClick: e => e.stopPropagation()
},
    // Search input
    React.createElement("input", {
        type: "text",
        placeholder: "Tìm nhạc cụ...",
        value: instrumentSearchQuery,
        onChange: e => setInstrumentSearchQuery(e.target.value),
        className: "w-full bg-black border-b border-zinc-700 px-2 py-1.5 text-xs text-zinc-200 outline-none"
    }),
    // Filtered list
    React.createElement("div", {
        className: "flex-1 overflow-y-auto"
    },
        // Filtered items from instrumentSelectorData
        // "None (Default Synth)" always shown first
        // Then filtered soundfonts
        // Then filtered VST instruments
    )
)

2e — Filter logic

const filteredInstruments = useMemo(() => {
    if (!instrumentSelectorData) return { soundfonts: [], vst: [] };
    const q = instrumentSearchQuery.toLowerCase();
    return {
        soundfonts: (instrumentSelectorData.soundfonts || []).filter(sf =>
            (sf.display || sf.name || sf.id).toLowerCase().includes(q)
        ),
        vst: (instrumentSelectorData.vst_instruments || []).filter(v =>
            (v.name || v.id).toLowerCase().includes(q)
        )
    };
}, [instrumentSearchQuery, instrumentSelectorData]);

2f — Click outside to close Add a global click handler that closes the dropdown when clicking outside.

2g — Preload instrumentSelectorData on first TCP mount Instead of only loading on openInstrumentSelector, preload listPlugins() when the first track renders (or on app mount).

2h — Apply selection On click of a dropdown item, call existing setTrackInstrumentWithProgram or setTrackInstrument. Close dropdown.

Verification

  • Click Synth button → dropdown opens with search input focused
  • Type instrument name → list filters in real-time
  • Click instrument → dropdown closes, track assigned, Synth button shows name
  • Click outside → dropdown closes

Task 3: Section-Tab Save Fix — Replace Instead of Draw On Top

Files: app/static/js/app.jsx

Root Cause

handleEditSectionInTab (line 7178) clones ALL main-session tracks (empty) into the session-tab when section.tracks doesn't exist. handleSaveSectionTab (line 7156) saves ALL those empty tracks + edited ones into s.tracks. The rendering code (lines 696-791) draws ALL stored tracks inside the section box, creating a cluttered preview with empty/minimal tracks.

Implementation Steps

3a — Fix handleEditSectionInTab (line 7169) to only initialize relevant track Change the fallback cloning (line 7178) from cloning ALL main tracks to creating a minimal set of tracks based on the section's parent track:

const clonedTracks = section.tracks ? section.tracks : [{
    ...tracks.find(tr => tr.id === trackId),
    clips: [],
    sections: [],
    midiItems: [],
    markers: [],
    isArmed: false,
    monitoringEnabled: true,
    instrumentId: null,
    instrumentProgram: undefined,
    instrumentName: null
}];

This only clones the track that owns the section, not ALL main tracks.

3b — Fix handleSaveSectionTab (line 7138) to filter non-empty tracks After building the updated section, filter tab.tracks to only include tracks that have actual content:

const contentTracks = tab.tracks.filter(t =>
    (t.clips && t.clips.length > 0) ||
    (t.midiItems && t.midiItems.length > 0)
);

Store tracks: contentTracks instead of tracks: tab.tracks.

3c — Improve section-item rendering (lines 694-791) The rendering already draws waveform from clips and MIDI notes from midiItems. Ensure:

  • Waveform rendering for clips with clip.buffer is correct (already done at lines 720-739)
  • MIDI note colors are per-track-index (already done at line 783: noteColors[trackIdx % noteColors.length])
  • Add a subtle track label inside each sub-track row so users can identify which track is which

3d — Ensure waveform preview is properly sized The section preview currently allocates subTrackHeight = (height - 24) / maxSubTracks for each sub-track (line 698). Verify this is sufficient for waveform + MIDI note rendering when there are 1-2 tracks (typical case).

Verification

  • Open a section for editing → session-tab shows only the relevant track(s), not all main tracks
  • Add MIDI items, sound clips, soundfonts, FX to tracks
  • Save section → section-item shows waveform preview + MIDI note preview (replacing previous content, not appending)
  • Open section again → previous edits are loaded correctly
  • Multiple save cycles → no doubling of content
  • Waveform rendered as background, MIDI notes in distinct colors per track