CODEX - Added extra columns and settings again
This commit is contained in:
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -64,8 +64,36 @@ 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) {
|
||||||
@@ -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)" }}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user