import type { JSX } from "@char/aftercare/jsx-runtime";
import { voiceTrack } from "../project/harmony.ts";
import { canonBeat, insertLeadIn, projectKey } from "../project/model.ts";
import { project } from "../project/store.ts";
import { prettyRoman } from "../theory/analysis.ts";
import { type Chord, chordName, chordSymbol } from "../theory/chord.ts";
import { connections } from "../theory/connect.ts";
import { parallel, relative, suspend, toggleSeventh, tritoneSub } from "../theory/transform.ts";
import { invertVoicing } from "../theory/voicing.ts";
import { leadInto, revoice, transformChord, unpin } from "./actions.ts";
import { disclosure, watch } from "./dom.tsx";
import { activeTrackId, chordPanelOpen, cursor } from "./state.ts";
const button = (label: string, result: string | undefined, run: () => void, title = "") => (
);
const group = (label: string, buttons: JSX.Element[]) => (
);
const names = (chords: Chord | Chord[] | undefined) =>
chords && [chords].flat().map(chordName).join(" ");
/** tools for the chord at the cursor: its voicing, variations on it, and ways into it */
export function ChordPanel(): JSX.Element {
const panel = () as HTMLElement;
const name = () as HTMLElement;
watch(panel, [project, cursor, activeTrackId, chordPanelOpen], () => {
if (!chordPanelOpen.get()) {
name.replaceChildren();
return panel.replaceChildren();
}
const p = project.get();
const track = p.tracks.find(t => t.id === activeTrackId.get());
const key = projectKey(p);
const voiced = track?.kind === "chords" ? voiceTrack(track, key) : [];
const v = voiced.find(v => v.event.start === cursor.get());
if (!v || track?.kind !== "chords") {
name.replaceChildren();
panel.replaceChildren(select a chord to change it);
return;
}
const [trackId, id, c] = [track.id, v.event.id, v.chord];
const voicing = (label: string, change: (notes: number[]) => number[]) =>
button(label, "", () => revoice(trackId, id, change));
const octave =
(by: number) =>
([bass, ...upper]: number[]) => [bass, ...upper.map(n => n + by)];
const change = (label: string, f: (c: Chord) => Chord | undefined, title?: string) =>
button(label, names(c && f(c)), () => transformChord(trackId, id, f), title);
// the ways in from the chord before, where there's room for them before this one
const i = voiced.indexOf(v);
const prev = voiced[i - 1];
const before = prev && canonBeat(prev.event.start + prev.event.dur) === v.event.start;
const routes = c ? connections(before ? prev.chord : undefined, c) : [];
const fits = (chords: Chord[]) =>
insertLeadIn(structuredClone(track), id, chords.map(chordSymbol), p.grid);
const connect = routes
.filter(r => fits(r.chords))
.slice(0, 6)
.map(r => button(r.name, names(r.chords), () => leadInto(trackId, id, r.chords)));
const adds = c && (toggleSeventh(c, key)?.tones.length ?? 0) > c.tones.length;
const seventh = adds ? "+ 7th" : "− 7th";
name.replaceChildren(
{v.label},
{v.analysis ? prettyRoman(v.analysis.roman) : ""},
);
panel.replaceChildren(
group("voicing", [
voicing("inversion ↓", notes => invertVoicing(notes, -1)),
voicing("inversion ↑", notes => invertVoicing(notes, 1)),
voicing("octave ↓", octave(-12)),
voicing("octave ↑", octave(12)),
button(
"unpin",
v.pinned ? "" : undefined,
() => unpin(trackId, id),
"back to voicing it from its neighbours",
),
]),
...(c
? [
group("alterations", [
change(seventh, c => toggleSeventh(c, key)),
change("sus2", c => suspend(c, key, 2)),
change("sus4", c => suspend(c, key, 5)),
change("parallel", parallel, "the same chord with the other third"),
change("relative", relative, "the chord a minor third away, sharing most notes"),
change("tritone sub", tritoneSub, "a dominant a tritone away"),
]),
...(connect.length
? [group(before ? `from ${prev.label}` : "lead in", connect)]
: []),
]
: []),
);
});
return (
{disclosure(chordPanelOpen, "chord")}
{name}
{panel}
);
}