CODEX - Added extra columns and settings again

This commit is contained in:
2026-02-22 21:37:58 +02:00
parent 5a0faad429
commit bffcdc19f7
4 changed files with 148 additions and 24 deletions

View File

@@ -345,9 +345,19 @@ export default function MelodyComposer() {
Clear Clear
</button> </button>
<div className="ml-auto text-xs" style={{ color: "var(--text-muted)" }}> <div className="ml-auto flex items-center gap-3">
<div className="text-xs" style={{ color: "var(--text-muted)" }}>
{steps.length} steps, {noteCount} notes {steps.length} steps, {noteCount} notes
</div> </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>
</section> </section>
@@ -429,17 +439,6 @@ export default function MelodyComposer() {
</div> </div>
</div> </div>
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
<div className="ml-1">
<button
type="button"
onClick={openDeployModal}
className="px-4 py-2 rounded-md text-sm whitespace-nowrap"
style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}
>
Deploy Archetype
</button>
</div>
</div> </div>
{currentStep >= 0 && ( {currentStep >= 0 && (

View File

@@ -31,6 +31,22 @@ import {
formatDuration, formatDuration,
} from "./melodyUtils"; } from "./melodyUtils";
function formatBpm(ms) {
const value = Number(ms);
if (!value || value <= 0) return null;
return Math.round(60000 / value);
}
function mapPercentageToStepDelay(percent, minSpeed, maxSpeed) {
if (minSpeed == null || maxSpeed == null) return null;
const p = Math.max(0, Math.min(100, Number(percent || 0)));
const t = p / 100;
const a = Number(minSpeed);
const b = Number(maxSpeed);
if (a <= 0 || b <= 0) return Math.round(a + (b - a) * t);
return Math.round(a * Math.pow(b / a, t));
}
function Field({ label, children }) { function Field({ label, children }) {
return ( return (
<div> <div>
@@ -193,6 +209,10 @@ export default function MelodyDetail() {
const info = melody.information || {}; const info = melody.information || {};
const settings = melody.default_settings || {}; const settings = melody.default_settings || {};
const speedMs = mapPercentageToStepDelay(settings.speed, info.minSpeed, info.maxSpeed);
const speedBpm = formatBpm(speedMs);
const minBpm = formatBpm(info.minSpeed);
const maxBpm = formatBpm(info.maxSpeed);
const languages = melodySettings?.available_languages || ["en"]; const languages = melodySettings?.available_languages || ["en"];
const displayName = getLocalizedValue(info.name, displayLang, "Untitled Melody"); const displayName = getLocalizedValue(info.name, displayLang, "Untitled Melody");
@@ -313,8 +333,22 @@ export default function MelodyDetail() {
<Field label="Steps">{info.steps}</Field> <Field label="Steps">{info.steps}</Field>
<Field label="Total Archetype Notes">{info.totalNotes}</Field> <Field label="Total Archetype Notes">{info.totalNotes}</Field>
<Field label="Total Active Bells">{info.totalActiveBells ?? "-"}</Field> <Field label="Total Active Bells">{info.totalActiveBells ?? "-"}</Field>
<Field label="Min Speed">{info.minSpeed}</Field> <Field label="Min Speed">
<Field label="Max Speed">{info.maxSpeed}</Field> {info.minSpeed ? (
<div>
<div>{minBpm} BPM</div>
<div className="text-xs" style={{ color: "var(--text-muted)" }}>{info.minSpeed} ms</div>
</div>
) : "-"}
</Field>
<Field label="Max Speed">
{info.maxSpeed ? (
<div>
<div>{maxBpm} BPM</div>
<div className="text-xs" style={{ color: "var(--text-muted)" }}>{info.maxSpeed} ms</div>
</div>
) : "-"}
</Field>
<Field label="Color"> <Field label="Color">
{info.color ? ( {info.color ? (
<span className="inline-flex items-center gap-2"> <span className="inline-flex items-center gap-2">
@@ -391,7 +425,13 @@ export default function MelodyDetail() {
Default Settings Default Settings
</h2> </h2>
<dl className="grid grid-cols-2 md:grid-cols-3 gap-4"> <dl className="grid grid-cols-2 md:grid-cols-3 gap-4">
<Field label="Speed">{settings.speed}%</Field> <Field label="Speed">
{settings.speed != null ? (
<span>
{settings.speed}%{speedBpm ? ` · ${speedBpm} BPM` : ""}{speedMs ? ` · ${speedMs} ms` : ""}
</span>
) : "-"}
</Field>
<Field label="Duration">{formatDuration(settings.duration)}</Field> <Field label="Duration">{formatDuration(settings.duration)}</Field>
<Field label="Total Run Duration">{settings.totalRunDuration}</Field> <Field label="Total Run Duration">{settings.totalRunDuration}</Field>
<Field label="Pause Duration">{settings.pauseDuration}</Field> <Field label="Pause Duration">{settings.pauseDuration}</Field>

View File

@@ -53,6 +53,22 @@ const headingStyle = { color: "var(--text-heading)" };
const labelStyle = { color: "var(--text-secondary)" }; const labelStyle = { color: "var(--text-secondary)" };
const mutedStyle = { color: "var(--text-muted)" }; const mutedStyle = { color: "var(--text-muted)" };
function formatBpm(ms) {
const value = Number(ms);
if (!value || value <= 0) return null;
return Math.round(60000 / value);
}
function mapPercentageToStepDelay(percent, minSpeed, maxSpeed) {
if (minSpeed == null || maxSpeed == null) return null;
const p = Math.max(0, Math.min(100, Number(percent || 0)));
const t = p / 100;
const a = Number(minSpeed);
const b = Number(maxSpeed);
if (a <= 0 || b <= 0) return Math.round(a + (b - a) * t);
return Math.round(a * Math.pow(b / a, t));
}
export default function MelodyForm() { export default function MelodyForm() {
const { id } = useParams(); const { id } = useParams();
const isEdit = Boolean(id); const isEdit = Boolean(id);
@@ -308,6 +324,10 @@ export default function MelodyForm() {
const durationIndex = durationValues.indexOf(settings.duration); const durationIndex = durationValues.indexOf(settings.duration);
const currentDurationIdx = durationIndex >= 0 ? durationIndex : 0; const currentDurationIdx = durationIndex >= 0 ? durationIndex : 0;
const speedMs = mapPercentageToStepDelay(settings.speed, information.minSpeed, information.maxSpeed);
const speedBpm = formatBpm(speedMs);
const minBpm = formatBpm(information.minSpeed);
const maxBpm = formatBpm(information.maxSpeed);
const inputClass = "w-full px-3 py-2 rounded-md text-sm border"; const inputClass = "w-full px-3 py-2 rounded-md text-sm border";
@@ -508,11 +528,17 @@ export default function MelodyForm() {
<div> <div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Min Speed</label> <label className="block text-sm font-medium mb-1" style={labelStyle}>Min Speed</label>
<input type="number" min="0" value={information.minSpeed} onChange={(e) => updateInfo("minSpeed", parseInt(e.target.value, 10) || 0)} className={inputClass} /> <input type="number" min="0" value={information.minSpeed} onChange={(e) => updateInfo("minSpeed", parseInt(e.target.value, 10) || 0)} className={inputClass} />
{information.minSpeed > 0 && (
<p className="text-xs mt-1" style={mutedStyle}>{minBpm} BPM · {information.minSpeed} ms</p>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Max Speed</label> <label className="block text-sm font-medium mb-1" style={labelStyle}>Max Speed</label>
<input type="number" min="0" value={information.maxSpeed} onChange={(e) => updateInfo("maxSpeed", parseInt(e.target.value, 10) || 0)} className={inputClass} /> <input type="number" min="0" value={information.maxSpeed} onChange={(e) => updateInfo("maxSpeed", parseInt(e.target.value, 10) || 0)} className={inputClass} />
{information.maxSpeed > 0 && (
<p className="text-xs mt-1" style={mutedStyle}>{maxBpm} BPM · {information.maxSpeed} ms</p>
)}
</div> </div>
{/* Color */} {/* Color */}
@@ -594,6 +620,9 @@ export default function MelodyForm() {
<input type="range" min="1" max="100" value={settings.speed} onChange={(e) => updateSettings("speed", parseInt(e.target.value, 10))} className="flex-1 h-2 rounded-lg appearance-none cursor-pointer" /> <input type="range" min="1" max="100" value={settings.speed} onChange={(e) => updateSettings("speed", parseInt(e.target.value, 10))} className="flex-1 h-2 rounded-lg appearance-none cursor-pointer" />
<span className="text-sm font-medium w-12 text-right" style={labelStyle}>{settings.speed}%</span> <span className="text-sm font-medium w-12 text-right" style={labelStyle}>{settings.speed}%</span>
</div> </div>
<div className="text-xs mt-1" style={mutedStyle}>
{speedBpm && speedMs ? `${speedBpm} BPM · ${speedMs} ms` : "Set MIN/MAX speed to compute BPM"}
</div>
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">

View File

@@ -64,9 +64,37 @@ function getDefaultVisibleColumns() {
function speedBarColor(speedPercent) { function speedBarColor(speedPercent) {
const v = Math.max(0, Math.min(100, Number(speedPercent || 0))); const v = Math.max(0, Math.min(100, Number(speedPercent || 0)));
const hue = (v / 100) * 120; if (v <= 50) {
const t = v / 50;
const hue = 120 + (210 - 120) * t; // green -> blue
return `hsl(${hue}, 85%, 46%)`; return `hsl(${hue}, 85%, 46%)`;
} }
const t = (v - 50) / 50;
const hue = 210 + (0 - 210) * t; // blue -> red
return `hsl(${hue}, 85%, 46%)`;
}
function durationBarColor(percent) {
const v = Math.max(0, Math.min(100, Number(percent || 0)));
const hue = 220 + (0 - 220) * (v / 100); // blue -> red
return `hsl(${hue}, 85%, 46%)`;
}
function formatBpm(ms) {
const value = Number(ms);
if (!value || value <= 0) return null;
return Math.round(60000 / value);
}
function mapPercentageToStepDelay(percent, minSpeed, maxSpeed) {
if (minSpeed == null || maxSpeed == null) return null;
const p = Math.max(0, Math.min(100, Number(percent || 0)));
const t = p / 100;
const a = Number(minSpeed);
const b = Number(maxSpeed);
if (a <= 0 || b <= 0) return Math.round(a + (b - a) * t);
return Math.round(a * Math.pow(b / a, t));
}
function parseDateValue(isoValue) { function parseDateValue(isoValue) {
if (!isoValue) return 0; if (!isoValue) return 0;
@@ -373,7 +401,19 @@ export default function MelodyList() {
); );
} }
case "type": case "type":
return <span className="capitalize">{row.type}</span>; {
const typeStyles = {
orthodox: { color: "#7dd3fc", backgroundColor: "rgba(14,165,233,0.15)" },
catholic: { color: "#fda4af", backgroundColor: "rgba(244,63,94,0.15)" },
all: { color: "#86efac", backgroundColor: "rgba(34,197,94,0.14)" },
};
const style = typeStyles[row.type] || { color: "var(--text-secondary)", backgroundColor: "var(--bg-card-hover)" };
return (
<span className="px-2 py-0.5 text-xs rounded-full capitalize" style={style}>
{row.type || "-"}
</span>
);
}
case "tone": case "tone":
return <span className="capitalize">{info.melodyTone || "-"}</span>; return <span className="capitalize">{info.melodyTone || "-"}</span>;
case "totalNotes": case "totalNotes":
@@ -381,9 +421,21 @@ export default function MelodyList() {
case "totalActiveBells": case "totalActiveBells":
return info.totalActiveBells ?? "-"; return info.totalActiveBells ?? "-";
case "minSpeed": case "minSpeed":
return info.minSpeed ?? "-"; if (!info.minSpeed) return "-";
return (
<div>
<div className="text-sm font-medium">{formatBpm(info.minSpeed)} BPM</div>
<div className="text-xs" style={{ color: "var(--text-muted)" }}>{info.minSpeed} ms</div>
</div>
);
case "maxSpeed": case "maxSpeed":
return info.maxSpeed ?? "-"; if (!info.maxSpeed) return "-";
return (
<div>
<div className="text-sm font-medium">{formatBpm(info.maxSpeed)} BPM</div>
<div className="text-xs" style={{ color: "var(--text-muted)" }}>{info.maxSpeed} ms</div>
</div>
);
case "tags": case "tags":
return info.customTags?.length > 0 ? ( return info.customTags?.length > 0 ? (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
@@ -402,10 +454,13 @@ export default function MelodyList() {
); );
case "speed": case "speed":
if (ds.speed == null) return "-"; if (ds.speed == null) return "-";
{
const speedMs = mapPercentageToStepDelay(ds.speed, info.minSpeed, info.maxSpeed);
const bpm = formatBpm(speedMs);
return ( return (
<div className="min-w-28"> <div className="min-w-28">
<div className="text-xs mb-1" style={{ color: "var(--text-secondary)" }}> <div className="text-xs mb-1" style={{ color: "var(--text-secondary)" }}>
{ds.speed}% {ds.speed}%{bpm ? ` · ${bpm} BPM` : ""}{speedMs ? ` · ${speedMs} ms` : ""}
</div> </div>
<div <div
className="w-full h-2 rounded-full" className="w-full h-2 rounded-full"
@@ -421,6 +476,7 @@ export default function MelodyList() {
</div> </div>
</div> </div>
); );
}
case "duration": case "duration":
if (ds.duration == null) return "-"; if (ds.duration == null) return "-";
if (Number(ds.duration) === 0) { if (Number(ds.duration) === 0) {
@@ -453,7 +509,7 @@ export default function MelodyList() {
className="h-full rounded-full transition-all" className="h-full rounded-full transition-all"
style={{ style={{
width: `${Math.max(0, Math.min(100, percent))}%`, width: `${Math.max(0, Math.min(100, percent))}%`,
backgroundColor: speedBarColor(percent), backgroundColor: durationBarColor(percent),
}} }}
/> />
</div> </div>
@@ -766,7 +822,7 @@ export default function MelodyList() {
<th <th
key={col.key} key={col.key}
className={`px-4 py-3 text-left font-medium ${ className={`px-4 py-3 text-left font-medium ${
col.key === "color" ? "w-8 px-2" : col.key === "noteAssignments" ? "min-w-[420px]" : "" col.key === "color" ? "w-8 px-2" : ""
}`} }`}
style={{ color: "var(--text-muted)" }} style={{ color: "var(--text-muted)" }}
> >
@@ -813,7 +869,7 @@ export default function MelodyList() {
<td <td
key={col.key} key={col.key}
className={`px-4 py-3 ${ className={`px-4 py-3 ${
col.key === "color" ? "w-8 px-2" : col.key === "noteAssignments" ? "min-w-[420px]" : "" col.key === "color" ? "w-8 px-2" : ""
}`} }`}
style={{ color: "var(--text-primary)" }} style={{ color: "var(--text-primary)" }}
> >