CODEX - Major Composer Changes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@ dist/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
MAIN-APP-REFERENCE/
|
MAIN-APP-REFERENCE/
|
||||||
|
SecondaryApps/
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
You are working on the bellsystems-cp project at ~/bellsystems-cp/.
|
|
||||||
Read BellSystems_AdminPanel_Strategy.md for the full project strategy.
|
|
||||||
|
|
||||||
We are now building Phase X — [Phase Name].
|
|
||||||
|
|
||||||
Review the existing codebase first, then implement the following:
|
|
||||||
[list of tasks for that phase]
|
|
||||||
|
|
||||||
Ask me before making any major architectural decisions.
|
|
||||||
Commit when done.
|
|
||||||
BIN
VesperPlus.png
BIN
VesperPlus.png
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
@@ -6,7 +6,8 @@ const MAX_NOTES = 16;
|
|||||||
const NOTE_LABELS = "ABCDEFGHIJKLMNOP";
|
const NOTE_LABELS = "ABCDEFGHIJKLMNOP";
|
||||||
|
|
||||||
function bellFrequency(bellNumber) {
|
function bellFrequency(bellNumber) {
|
||||||
return 880 * Math.pow(Math.pow(2, 1 / 12), -2 * (bellNumber - 1));
|
// One octave every 8 notes.
|
||||||
|
return 880 * Math.pow(2, -((bellNumber - 1) / 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepToNotation(stepValue) {
|
function stepToNotation(stepValue) {
|
||||||
@@ -22,6 +23,14 @@ function stepToHex(stepValue) {
|
|||||||
return `0x${(stepValue >>> 0).toString(16).toUpperCase().padStart(4, "0")}`;
|
return `0x${(stepValue >>> 0).toString(16).toUpperCase().padStart(4, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function msToBpm(ms) {
|
||||||
|
return Math.max(1, Math.round(60000 / Math.max(1, Number(ms) || 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bpmToMs(bpm) {
|
||||||
|
return Math.max(20, Math.round(60000 / Math.max(1, Number(bpm) || 1)));
|
||||||
|
}
|
||||||
|
|
||||||
function interpolateHue(t) {
|
function interpolateHue(t) {
|
||||||
const stops = [
|
const stops = [
|
||||||
[0.0, 190],
|
[0.0, 190],
|
||||||
@@ -103,6 +112,7 @@ export default function MelodyComposer() {
|
|||||||
const [noteCount, setNoteCount] = useState(8);
|
const [noteCount, setNoteCount] = useState(8);
|
||||||
const [stepDelayMs, setStepDelayMs] = useState(280);
|
const [stepDelayMs, setStepDelayMs] = useState(280);
|
||||||
const [noteDurationMs, setNoteDurationMs] = useState(110);
|
const [noteDurationMs, setNoteDurationMs] = useState(110);
|
||||||
|
const [measureEvery, setMeasureEvery] = useState(0);
|
||||||
const [loopEnabled, setLoopEnabled] = useState(true);
|
const [loopEnabled, setLoopEnabled] = useState(true);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [currentStep, setCurrentStep] = useState(-1);
|
const [currentStep, setCurrentStep] = useState(-1);
|
||||||
@@ -114,6 +124,7 @@ export default function MelodyComposer() {
|
|||||||
const [deployError, setDeployError] = useState("");
|
const [deployError, setDeployError] = useState("");
|
||||||
const [deploying, setDeploying] = useState(false);
|
const [deploying, setDeploying] = useState(false);
|
||||||
const [noteColors, setNoteColors] = useState([]);
|
const [noteColors, setNoteColors] = useState([]);
|
||||||
|
const [stepMenuIndex, setStepMenuIndex] = useState(null);
|
||||||
|
|
||||||
const audioCtxRef = useRef(null);
|
const audioCtxRef = useRef(null);
|
||||||
const playbackRef = useRef(null);
|
const playbackRef = useRef(null);
|
||||||
@@ -121,6 +132,7 @@ export default function MelodyComposer() {
|
|||||||
const stepDelayRef = useRef(stepDelayMs);
|
const stepDelayRef = useRef(stepDelayMs);
|
||||||
const noteDurationRef = useRef(noteDurationMs);
|
const noteDurationRef = useRef(noteDurationMs);
|
||||||
const loopEnabledRef = useRef(loopEnabled);
|
const loopEnabledRef = useRef(loopEnabled);
|
||||||
|
const stepMenuRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stepsRef.current = steps;
|
stepsRef.current = steps;
|
||||||
@@ -137,6 +149,16 @@ export default function MelodyComposer() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loopEnabledRef.current = loopEnabled;
|
loopEnabledRef.current = loopEnabled;
|
||||||
}, [loopEnabled]);
|
}, [loopEnabled]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (stepMenuIndex == null) return undefined;
|
||||||
|
const onDocClick = (e) => {
|
||||||
|
if (stepMenuRef.current && !stepMenuRef.current.contains(e.target)) {
|
||||||
|
setStepMenuIndex(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", onDocClick);
|
||||||
|
return () => document.removeEventListener("mousedown", onDocClick);
|
||||||
|
}, [stepMenuIndex]);
|
||||||
|
|
||||||
const ensureAudioContext = useCallback(() => {
|
const ensureAudioContext = useCallback(() => {
|
||||||
if (!audioCtxRef.current || audioCtxRef.current.state === "closed") {
|
if (!audioCtxRef.current || audioCtxRef.current.state === "closed") {
|
||||||
@@ -238,6 +260,28 @@ export default function MelodyComposer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearAll = () => setSteps((prev) => prev.map(() => 0));
|
const clearAll = () => setSteps((prev) => prev.map(() => 0));
|
||||||
|
const insertStepAt = (index) => {
|
||||||
|
setSteps((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
next.splice(index, 0, 0);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setCurrentStep((prev) => (prev >= index ? prev + 1 : prev));
|
||||||
|
};
|
||||||
|
const deleteStepAt = (index) => {
|
||||||
|
setSteps((prev) => {
|
||||||
|
if (prev.length <= 1) return prev;
|
||||||
|
const next = [...prev];
|
||||||
|
next.splice(index, 1);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setCurrentStep((prev) => {
|
||||||
|
if (prev < 0) return prev;
|
||||||
|
if (prev === index) return -1;
|
||||||
|
if (prev > index) return prev - 1;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
if (!stepsRef.current.length) return;
|
if (!stepsRef.current.length) return;
|
||||||
@@ -317,6 +361,9 @@ export default function MelodyComposer() {
|
|||||||
}
|
}
|
||||||
return active;
|
return active;
|
||||||
}, [currentStep, noteCount, steps]);
|
}, [currentStep, noteCount, steps]);
|
||||||
|
const speedBpm = msToBpm(stepDelayMs);
|
||||||
|
const measureChoices = [0, 4, 8, 16, 32];
|
||||||
|
const measureSliderIdx = Math.max(0, measureChoices.indexOf(measureEvery));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -359,153 +406,80 @@ export default function MelodyComposer() {
|
|||||||
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<button
|
<button type="button" onClick={addStep} className="px-3 py-1.5 text-sm rounded-md" style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}>+ Step</button>
|
||||||
type="button"
|
<button type="button" onClick={removeStep} className="px-3 py-1.5 text-sm rounded-md" style={{ backgroundColor: "var(--btn-neutral)", color: "var(--text-white)" }}>- Step</button>
|
||||||
onClick={addStep}
|
|
||||||
className="px-3 py-1.5 text-sm rounded-md"
|
|
||||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
|
||||||
>
|
|
||||||
+ Step
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={removeStep}
|
|
||||||
className="px-3 py-1.5 text-sm rounded-md"
|
|
||||||
style={{ backgroundColor: "var(--btn-neutral)", color: "var(--text-white)" }}
|
|
||||||
>
|
|
||||||
- Step
|
|
||||||
</button>
|
|
||||||
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
|
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
|
||||||
<button
|
<button type="button" onClick={addNote} disabled={noteCount >= MAX_NOTES} className="px-3 py-1.5 text-sm rounded-md disabled:opacity-50" style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}>+ Note</button>
|
||||||
type="button"
|
<button type="button" onClick={removeNote} disabled={noteCount <= 1} className="px-3 py-1.5 text-sm rounded-md disabled:opacity-50" style={{ backgroundColor: "var(--btn-neutral)", color: "var(--text-white)" }}>- Note</button>
|
||||||
onClick={addNote}
|
|
||||||
disabled={noteCount >= MAX_NOTES}
|
|
||||||
className="px-3 py-1.5 text-sm rounded-md disabled:opacity-50"
|
|
||||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
|
||||||
>
|
|
||||||
+ Note
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={removeNote}
|
|
||||||
disabled={noteCount <= 1}
|
|
||||||
className="px-3 py-1.5 text-sm rounded-md disabled:opacity-50"
|
|
||||||
style={{ backgroundColor: "var(--btn-neutral)", color: "var(--text-white)" }}
|
|
||||||
>
|
|
||||||
- Note
|
|
||||||
</button>
|
|
||||||
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
|
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
|
||||||
<button
|
<button type="button" onClick={clearAll} className="px-3 py-1.5 text-sm rounded-md" style={{ backgroundColor: "var(--danger-btn)", color: "var(--text-white)" }}>Clear</button>
|
||||||
type="button"
|
|
||||||
onClick={clearAll}
|
|
||||||
className="px-3 py-1.5 text-sm rounded-md"
|
|
||||||
style={{ backgroundColor: "var(--danger-btn)", color: "var(--text-white)" }}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-3">
|
<div className="ml-auto flex items-center gap-3">
|
||||||
<div className="text-xs" style={{ color: "var(--text-muted)" }}>
|
<div className="text-xs" style={{ color: "var(--text-muted)" }}>{steps.length} steps, {noteCount} notes</div>
|
||||||
{steps.length} steps, {noteCount} notes
|
<button type="button" onClick={openDeployModal} className="px-3 py-1.5 rounded-md text-sm whitespace-nowrap" style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}>Deploy Archetype</button>
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={openDeployModal}
|
|
||||||
className="px-3 py-1.5 rounded-md text-sm whitespace-nowrap"
|
|
||||||
style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}
|
|
||||||
>
|
|
||||||
Deploy Archetype
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border-primary)" }}>
|
||||||
className="rounded-lg border p-4"
|
<div className="flex flex-wrap items-end gap-4">
|
||||||
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
<div className="flex items-center gap-2">
|
||||||
>
|
{!isPlaying ? (
|
||||||
<div className="flex flex-wrap items-end gap-4">
|
<button type="button" onClick={handlePlay} className="px-4 py-2 rounded-md text-sm" style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}>Play</button>
|
||||||
<div className="flex items-center gap-2">
|
) : (
|
||||||
{!isPlaying ? (
|
<button type="button" onClick={stopPlayback} className="px-4 py-2 rounded-md text-sm" style={{ backgroundColor: "var(--danger-btn)", color: "var(--text-white)" }}>Stop</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handlePlay}
|
onClick={() => setLoopEnabled((v) => !v)}
|
||||||
className="px-4 py-2 rounded-md text-sm"
|
className="px-4 py-2 rounded-md text-sm inline-flex items-center gap-2"
|
||||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
style={{
|
||||||
|
backgroundColor: loopEnabled ? "rgba(34,197,94,0.2)" : "var(--bg-card-hover)",
|
||||||
|
color: loopEnabled ? "#22c55e" : "var(--text-secondary)",
|
||||||
|
border: "1px solid var(--border-primary)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Play
|
<span>Loop</span>
|
||||||
|
<span className="inline-block w-2.5 h-2.5 rounded-full" style={{ backgroundColor: loopEnabled ? "#22c55e" : "var(--text-muted)" }} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={stopPlayback}
|
|
||||||
className="px-4 py-2 rounded-md text-sm"
|
|
||||||
style={{ backgroundColor: "var(--danger-btn)", color: "var(--text-white)" }}
|
|
||||||
>
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm" style={{ color: "var(--text-secondary)" }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={loopEnabled}
|
|
||||||
onChange={(e) => setLoopEnabled(e.target.checked)}
|
|
||||||
/>
|
|
||||||
Loop
|
|
||||||
</label>
|
|
||||||
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-end gap-5">
|
|
||||||
<div className="w-full sm:w-72">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>
|
|
||||||
Step Delay
|
|
||||||
</label>
|
|
||||||
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>
|
|
||||||
{stepDelayMs} ms
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="40"
|
|
||||||
max="2000"
|
|
||||||
step="10"
|
|
||||||
value={stepDelayMs}
|
|
||||||
onChange={(e) => setStepDelayMs(Number(e.target.value))}
|
|
||||||
className="w-full mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full sm:w-72">
|
<div className="flex flex-wrap items-end gap-5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="w-full sm:w-72">
|
||||||
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>
|
<div className="flex items-center justify-between">
|
||||||
Note Duration
|
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>Speed in BPM</label>
|
||||||
</label>
|
<span className="text-sm inline-flex items-baseline gap-2">
|
||||||
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>
|
<span style={{ color: "var(--accent)", fontWeight: 600 }}>{speedBpm} bpm</span>
|
||||||
{noteDurationMs} ms
|
<span style={{ color: "var(--text-muted)", fontSize: "11px" }}>{stepDelayMs} ms</span>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" min="30" max="1500" step="1" value={speedBpm} onChange={(e) => setStepDelayMs(bpmToMs(Number(e.target.value)))} className="w-full mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full sm:w-72">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>Note Duration</label>
|
||||||
|
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>{noteDurationMs} ms</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" min="20" max="500" step="10" value={noteDurationMs} onChange={(e) => setNoteDurationMs(Number(e.target.value))} className="w-full mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="mx-1 text-sm self-center" style={{ color: "var(--border-primary)" }}>|</span>
|
||||||
|
<div className="w-full sm:w-36">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>Measure</label>
|
||||||
|
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>{measureEvery || "Off"}</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" min="0" max="4" step="1" value={measureSliderIdx} onChange={(e) => setMeasureEvery(measureChoices[Number(e.target.value)] || 0)} className="w-full mt-2" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="20"
|
|
||||||
max="500"
|
|
||||||
step="10"
|
|
||||||
value={noteDurationMs}
|
|
||||||
onChange={(e) => setNoteDurationMs(Number(e.target.value))}
|
|
||||||
className="w-full mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{currentStep >= 0 && (
|
||||||
|
<p className="text-xs mt-3" style={{ color: "var(--text-muted)" }}>
|
||||||
|
Playing step {currentStep + 1}/{steps.length}
|
||||||
|
{activeBellsInCurrentStep.length ? ` (bells: ${activeBellsInCurrentStep.join(", ")})` : " (silence)"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentStep >= 0 && (
|
|
||||||
<p className="text-xs mt-3" style={{ color: "var(--text-muted)" }}>
|
|
||||||
Playing step {currentStep + 1}/{steps.length}
|
|
||||||
{activeBellsInCurrentStep.length ? ` (bells: ${activeBellsInCurrentStep.join(", ")})` : " (silence)"}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -524,18 +498,65 @@ export default function MelodyComposer() {
|
|||||||
</th>
|
</th>
|
||||||
{steps.map((_, stepIndex) => {
|
{steps.map((_, stepIndex) => {
|
||||||
const isCurrent = stepIndex === currentStep;
|
const isCurrent = stepIndex === currentStep;
|
||||||
|
const measureHit = measureEvery > 0 && (stepIndex + 1) % measureEvery === 0;
|
||||||
|
const measureBlockOdd = measureEvery > 0 && Math.floor(stepIndex / measureEvery) % 2 === 1;
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
key={stepIndex}
|
key={stepIndex}
|
||||||
className="sticky top-0 z-20 px-2 py-2 text-xs font-semibold border-b border-r"
|
className="sticky top-0 z-20 px-1 py-1 text-xs font-semibold border-b border-r relative"
|
||||||
style={{
|
style={{
|
||||||
minWidth: "44px",
|
minWidth: "44px",
|
||||||
backgroundColor: isCurrent ? "rgba(116,184,22,0.24)" : "var(--bg-secondary)",
|
backgroundColor: isCurrent
|
||||||
borderColor: "var(--border-primary)",
|
? "rgba(116,184,22,0.24)"
|
||||||
|
: (measureBlockOdd ? "rgba(255,255,255,0.02)" : "var(--bg-secondary)"),
|
||||||
|
borderColor: measureHit ? "rgba(255,255,255,0.38)" : "var(--border-primary)",
|
||||||
|
borderRightWidth: measureHit ? "2px" : "1px",
|
||||||
color: isCurrent ? "var(--accent)" : "var(--text-secondary)",
|
color: isCurrent ? "var(--accent)" : "var(--text-secondary)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{stepIndex + 1}
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full px-1 py-1"
|
||||||
|
style={{ background: "transparent", border: "none", color: "inherit" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setStepMenuIndex((prev) => (prev === stepIndex ? null : stepIndex));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stepIndex + 1}
|
||||||
|
</button>
|
||||||
|
{stepMenuIndex === stepIndex && (
|
||||||
|
<div
|
||||||
|
ref={stepMenuRef}
|
||||||
|
className="absolute right-0 top-full mt-1 w-28 rounded-md border shadow-lg z-50"
|
||||||
|
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full text-left px-2 py-1.5 text-xs"
|
||||||
|
style={{ color: "var(--text-primary)", background: "transparent", border: "none" }}
|
||||||
|
onClick={() => { insertStepAt(stepIndex + 1); setStepMenuIndex(null); }}
|
||||||
|
>
|
||||||
|
Add After
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full text-left px-2 py-1.5 text-xs"
|
||||||
|
style={{ color: "var(--text-primary)", background: "transparent", border: "none" }}
|
||||||
|
onClick={() => { insertStepAt(stepIndex); setStepMenuIndex(null); }}
|
||||||
|
>
|
||||||
|
Add Before
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full text-left px-2 py-1.5 text-xs"
|
||||||
|
style={{ color: "var(--danger)", background: "transparent", border: "none" }}
|
||||||
|
onClick={() => { deleteStepAt(stepIndex); setStepMenuIndex(null); }}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -557,13 +578,18 @@ export default function MelodyComposer() {
|
|||||||
{steps.map((stepValue, stepIndex) => {
|
{steps.map((stepValue, stepIndex) => {
|
||||||
const enabled = Boolean(stepValue & (1 << noteIndex));
|
const enabled = Boolean(stepValue & (1 << noteIndex));
|
||||||
const isCurrent = stepIndex === currentStep;
|
const isCurrent = stepIndex === currentStep;
|
||||||
|
const measureHit = measureEvery > 0 && (stepIndex + 1) % measureEvery === 0;
|
||||||
|
const measureBlockOdd = measureEvery > 0 && Math.floor(stepIndex / measureEvery) % 2 === 1;
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={`${noteIndex}-${stepIndex}`}
|
key={`${noteIndex}-${stepIndex}`}
|
||||||
className="border-b border-r"
|
className="border-b border-r"
|
||||||
style={{
|
style={{
|
||||||
borderColor: "var(--border-primary)",
|
borderColor: measureHit ? "rgba(255,255,255,0.34)" : "var(--border-primary)",
|
||||||
backgroundColor: isCurrent ? "rgba(116,184,22,0.08)" : "transparent",
|
borderRightWidth: measureHit ? "2px" : "1px",
|
||||||
|
backgroundColor: isCurrent
|
||||||
|
? "rgba(116,184,22,0.08)"
|
||||||
|
: (measureBlockOdd ? "rgba(255,255,255,0.015)" : "transparent"),
|
||||||
width: "44px",
|
width: "44px",
|
||||||
height: "44px",
|
height: "44px",
|
||||||
}}
|
}}
|
||||||
@@ -608,12 +634,9 @@ export default function MelodyComposer() {
|
|||||||
className="rounded-lg border p-4"
|
className="rounded-lg border p-4"
|
||||||
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
||||||
>
|
>
|
||||||
<h2 className="text-sm font-semibold mb-2" style={{ color: "var(--text-heading)" }}>
|
|
||||||
Generated Output Preview
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>CSV-like notation</p>
|
<p className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>Generated CSV notation</p>
|
||||||
<pre
|
<pre
|
||||||
className="rounded-md p-3 text-xs overflow-auto"
|
className="rounded-md p-3 text-xs overflow-auto"
|
||||||
style={{ backgroundColor: "var(--bg-primary)", border: "1px solid var(--border-primary)", color: "var(--text-primary)" }}
|
style={{ backgroundColor: "var(--bg-primary)", border: "1px solid var(--border-primary)", color: "var(--text-primary)" }}
|
||||||
@@ -622,7 +645,7 @@ export default function MelodyComposer() {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>PROGMEM values</p>
|
<p className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>Generated PROGMEM Values</p>
|
||||||
<pre
|
<pre
|
||||||
className="rounded-md p-3 text-xs overflow-auto"
|
className="rounded-md p-3 text-xs overflow-auto"
|
||||||
style={{ backgroundColor: "var(--bg-primary)", border: "1px solid var(--border-primary)", color: "var(--text-primary)" }}
|
style={{ backgroundColor: "var(--bg-primary)", border: "1px solid var(--border-primary)", color: "var(--text-primary)" }}
|
||||||
|
|||||||
@@ -1165,7 +1165,7 @@ export default function MelodyList() {
|
|||||||
<div className="flex items-center justify-end gap-3 shrink-0 flex-wrap ml-auto">
|
<div className="flex items-center justify-end gap-3 shrink-0 flex-wrap ml-auto">
|
||||||
<span className="text-sm whitespace-nowrap" style={{ color: "var(--text-muted)" }}>
|
<span className="text-sm whitespace-nowrap" style={{ color: "var(--text-muted)" }}>
|
||||||
{hasAnyFilter
|
{hasAnyFilter
|
||||||
? `Filtered - Showing ${displayRows.length} / ${allMelodyCount || melodies.length} Melodies | ${offlineTaggedCount} Melodies tagged for Offline`
|
? `Filtered - Showing ${displayRows.length} / ${allMelodyCount || melodies.length} Melodies | ${offlineTaggedCount} Offline-tagged`
|
||||||
: `Showing all (${allMelodyCount || melodies.length}) Melodies | ${offlineTaggedCount} Melodies tagged for Offline`}
|
: `Showing all (${allMelodyCount || melodies.length}) Melodies | ${offlineTaggedCount} Melodies tagged for Offline`}
|
||||||
</span>
|
</span>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
|
|||||||
Reference in New Issue
Block a user