Modernize the UI and fix the equalizer sizing bug
Bug fix
- EqualizerIcon: the SVG was rendering at the wrong size (filling
its parent) because it had no explicit width/height attributes.
Now it accepts a size prop with a 24px default and the SVG element
carries the size as width/height attributes, with the className
on top. Can't accidentally balloon.
Design
- Stripped the light-mode CSS — the app is dark-only now, and the
theme toggle was a no-op (clicking it just toggled a class that
had no matching rules). ThemeToggle.tsx and lib/theme.ts are
gone; initTheme() call removed from main.tsx.
- Reworked the input form:
• brand row is now small + refined (icon + wordmark, no huge
gradient title)
• Options is a single subtle collapsible card with a chevron and
a count badge showing how many options are non-default
• Vocals is now a segmented pill (single rounded container, two
flat segments) instead of two full-width grid buttons
• Generate button is slightly smaller (py-3.5, text-sm) and
uses a softer shadow (shadow-violet-900/20)
- Empty state in the results panel: dashed-border card, soft
gradient blur behind the music icon, cleaner copy.
- Results header: smaller heading, the 'Regenerate All' and
'Cancel' buttons are now tiny ghost-style pills in the top-right.
- Sticky ZIP bar: softer shadow, more refined copy.
- Result cards: smaller icon, single-line title, the Regenerate
button is now an inline pill that fits next to Revert + Copy.
- Home page header now has a tiny equalizer icon + 'MelodyMuse'
wordmark on the left (was just text). Settings page header has
the back arrow on the left and the title, no theme toggle.
- Global CSS: added modern focus-visible ring, custom selection
color, subtle scrollbar thumb, slightly tweaked the gradient
text to a softer violet-300 → cyan-300 instead of the loud
violet-400 → fuchsia-400 → cyan-400.
This commit is contained in:
+129
-116
@@ -1,8 +1,7 @@
|
||||
import { useState, type FormEvent, type KeyboardEvent } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ChevronRight,
|
||||
Loader2,
|
||||
Settings as SettingsIcon,
|
||||
Sparkles,
|
||||
@@ -29,8 +28,6 @@ const LANGUAGES: { value: Language; label: string }[] = [
|
||||
{ value: "Other", label: "Other" },
|
||||
];
|
||||
|
||||
// A handful of starter ideas shown as quick-fill chips. Kept short and varied
|
||||
// so the user can see the kind of input the model expects.
|
||||
const EXAMPLE_IDEAS: { text: string; mood: string; vocals: Vocals }[] = [
|
||||
{
|
||||
text: "melancholic lo-fi house beat for a rainy sunday morning",
|
||||
@@ -71,10 +68,10 @@ export function InputPanel({
|
||||
disabled,
|
||||
cancelButton,
|
||||
}: InputPanelProps) {
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
const [styleLoading, setStyleLoading] = useState<null | "normal" | "crazy">(
|
||||
null,
|
||||
);
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
const elapsed = useElapsed(loading);
|
||||
const toast = useToast();
|
||||
|
||||
@@ -88,8 +85,7 @@ export function InputPanel({
|
||||
if (canSubmit) onSubmit();
|
||||
};
|
||||
|
||||
// Submit on Cmd/Ctrl+Enter, even from inside the textarea.
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const onCmdEnter = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
@@ -107,7 +103,7 @@ export function InputPanel({
|
||||
};
|
||||
|
||||
const handleStyleRandom = async (mode: "normal" | "crazy") => {
|
||||
if (styleLoading) return; // already running, don't fire two in parallel
|
||||
if (styleLoading) return;
|
||||
const ctrl = new AbortController();
|
||||
setStyleLoading(mode);
|
||||
try {
|
||||
@@ -126,77 +122,81 @@ export function InputPanel({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Animated gradient backdrop behind the header */}
|
||||
<div className="absolute -top-10 -left-10 -right-10 h-64 animated-gradient-bg rounded-3xl -z-10 opacity-70 pointer-events-none" />
|
||||
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<EqualizerIcon className="w-8 h-8 shrink-0" />
|
||||
<h1 className="text-3xl font-bold gradient-text leading-none">
|
||||
MelodyMuse
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-sm text-fg-muted mb-6 ml-11">
|
||||
Generate your Suno song assets with AI
|
||||
</p>
|
||||
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
<form onSubmit={submit} className="space-y-6">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<EqualizerIcon size={22} className="shrink-0" />
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label htmlFor="idea" className="sr-only">
|
||||
Describe your music idea
|
||||
</label>
|
||||
<span className="text-[11px] uppercase tracking-wider text-fg-muted">
|
||||
Music idea
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fillExample}
|
||||
className="inline-flex items-center gap-1 text-[11px] text-fg-muted hover:text-fg transition-colors"
|
||||
title="Fill the input with a random example"
|
||||
>
|
||||
<Shuffle className="w-3 h-3" />
|
||||
Try an example
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="idea"
|
||||
value={values.idea}
|
||||
onChange={(e) => set("idea", e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
rows={4}
|
||||
placeholder={`Describe your music idea...\ne.g. 'melancholic house beat for a rainy night'`}
|
||||
className="textarea text-base leading-relaxed"
|
||||
required
|
||||
/>
|
||||
<p className="mt-1 text-[11px] text-fg-muted">
|
||||
<kbd className="px-1 py-0.5 rounded bg-bg-hover border border-border font-mono">
|
||||
{isMac() ? "⌘" : "Ctrl"}+Enter
|
||||
</kbd>{" "}
|
||||
to generate
|
||||
</p>
|
||||
<h1 className="text-lg font-semibold tracking-tight gradient-text leading-none">
|
||||
MelodyMuse
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main textarea */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label
|
||||
htmlFor="idea"
|
||||
className="text-[11px] font-medium uppercase tracking-wider text-fg-muted"
|
||||
>
|
||||
Music idea
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fillExample}
|
||||
className="inline-flex items-center gap-1 text-[11px] text-fg-muted hover:text-fg transition-colors"
|
||||
title="Fill the input with a random example"
|
||||
>
|
||||
<Shuffle className="w-3 h-3" />
|
||||
Try an example
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="idea"
|
||||
value={values.idea}
|
||||
onChange={(e) => set("idea", e.target.value)}
|
||||
onKeyDown={onCmdEnter}
|
||||
rows={4}
|
||||
placeholder="Describe your music idea…"
|
||||
className="textarea text-[15px] leading-relaxed"
|
||||
required
|
||||
/>
|
||||
<p className="mt-1.5 text-[11px] text-fg-muted">
|
||||
<kbd className="px-1 py-0.5 rounded bg-bg-hover border border-border font-mono">
|
||||
{isMac() ? "⌘" : "Ctrl"}+Enter
|
||||
</kbd>{" "}
|
||||
to generate
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Options — collapsible, subtle */}
|
||||
<div className="rounded-xl border border-border bg-bg-card/30">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOptionsOpen((v) => !v)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg border border-border bg-bg-card hover:bg-bg-hover transition-colors text-sm"
|
||||
className="w-full flex items-center justify-between gap-2 px-4 py-3 text-sm font-medium text-fg-muted hover:text-fg transition-colors"
|
||||
aria-expanded={optionsOpen}
|
||||
>
|
||||
<span className="font-medium text-fg">⚙ Options</span>
|
||||
{optionsOpen ? (
|
||||
<ChevronUp className="w-4 h-4 text-fg-muted" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-fg-muted" />
|
||||
)}
|
||||
<span className="flex items-center gap-2">
|
||||
<ChevronRight
|
||||
className={`w-3.5 h-3.5 transition-transform ${optionsOpen ? "rotate-90" : ""}`}
|
||||
/>
|
||||
Options
|
||||
</span>
|
||||
<span className="text-[11px] text-fg-muted">
|
||||
{summaryCount(values) > 0
|
||||
? summaryCount(values) + " set"
|
||||
: "defaults"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{optionsOpen && (
|
||||
<div className="space-y-4 p-4 rounded-lg border border-border bg-bg-card/50">
|
||||
<div className="px-4 pb-4 pt-1 space-y-4 border-t border-border">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="language"
|
||||
className="block text-xs uppercase tracking-wider text-fg-muted mb-1"
|
||||
className="block text-[11px] font-medium uppercase tracking-wider text-fg-muted mb-1.5"
|
||||
>
|
||||
Language
|
||||
</label>
|
||||
@@ -228,16 +228,18 @@ export function InputPanel({
|
||||
loading={styleLoading}
|
||||
onChange={(v) => set("style_hint", v)}
|
||||
onRandom={handleStyleRandom}
|
||||
onCmdEnter={onKeyDown}
|
||||
onCmdEnter={onCmdEnter}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="mood"
|
||||
className="block text-xs uppercase tracking-wider text-fg-muted mb-1"
|
||||
className="block text-[11px] font-medium uppercase tracking-wider text-fg-muted mb-1.5"
|
||||
>
|
||||
Mood{" "}
|
||||
<span className="text-fg-muted/70 normal-case">(optional)</span>
|
||||
<span className="text-fg-muted/70 normal-case font-normal">
|
||||
(optional)
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="mood"
|
||||
@@ -250,17 +252,17 @@ export function InputPanel({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="block text-xs uppercase tracking-wider text-fg-muted mb-1">
|
||||
<span className="block text-[11px] font-medium uppercase tracking-wider text-fg-muted mb-1.5">
|
||||
Vocals
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="inline-flex rounded-lg border border-border bg-bg-card p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => set("vocals", "vocals")}
|
||||
className={
|
||||
values.vocals === "vocals"
|
||||
? "px-3 py-2 rounded-lg font-medium bg-accent-primary text-white transition-colors"
|
||||
: "chip justify-center py-2"
|
||||
? "px-3 py-1.5 rounded-md text-sm font-medium bg-accent-primary text-white transition-colors"
|
||||
: "px-3 py-1.5 rounded-md text-sm font-medium text-fg-muted hover:text-fg transition-colors"
|
||||
}
|
||||
aria-pressed={values.vocals === "vocals"}
|
||||
>
|
||||
@@ -271,8 +273,8 @@ export function InputPanel({
|
||||
onClick={() => set("vocals", "instrumental")}
|
||||
className={
|
||||
values.vocals === "instrumental"
|
||||
? "px-3 py-2 rounded-lg font-medium bg-accent-primary text-white transition-colors"
|
||||
: "chip justify-center py-2"
|
||||
? "px-3 py-1.5 rounded-md text-sm font-medium bg-accent-primary text-white transition-colors"
|
||||
: "px-3 py-1.5 rounded-md text-sm font-medium text-fg-muted hover:text-fg transition-colors"
|
||||
}
|
||||
aria-pressed={values.vocals === "instrumental"}
|
||||
>
|
||||
@@ -282,47 +284,58 @@ export function InputPanel({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
className="w-full py-4 rounded-xl text-base font-semibold text-white shadow-lg shadow-violet-900/30 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none flex items-center justify-center gap-2 gradient-bg hover:brightness-110 active:brightness-95"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>Generating… {formatElapsed(elapsed)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-5 h-5" />
|
||||
Generate Song Assets
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{loading && cancelButton}
|
||||
</div>
|
||||
{/* Generate */}
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
className="w-full py-3.5 rounded-xl text-sm font-semibold text-white shadow-lg shadow-violet-900/20 transition-all duration-150 disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none flex items-center justify-center gap-2 gradient-bg hover:brightness-110 active:brightness-95"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Generating… {formatElapsed(elapsed)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Generate song assets
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{loading && cancelButton}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/settings"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-fg-muted hover:text-fg transition-colors"
|
||||
>
|
||||
<SettingsIcon className="w-3.5 h-3.5" />
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<Link
|
||||
to="/settings"
|
||||
className="inline-flex items-center gap-1.5 text-fg-muted hover:text-fg transition-colors"
|
||||
>
|
||||
<SettingsIcon className="w-3.5 h-3.5" />
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function summaryCount(v: InputValues): number {
|
||||
let n = 0;
|
||||
if (v.style_hint.trim()) n++;
|
||||
if (v.mood.trim()) n++;
|
||||
if (v.vocals === "instrumental") n++;
|
||||
if (v.language !== "English") n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
interface StyleFieldProps {
|
||||
value: string;
|
||||
loading: null | "normal" | "crazy";
|
||||
onChange: (v: string) => void;
|
||||
onRandom: (mode: "normal" | "crazy") => void;
|
||||
onCmdEnter: (e: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
}
|
||||
|
||||
function StyleField({
|
||||
@@ -331,17 +344,17 @@ function StyleField({
|
||||
onChange,
|
||||
onRandom,
|
||||
onCmdEnter,
|
||||
}: StyleFieldProps & {
|
||||
onCmdEnter: (e: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
}) {
|
||||
}: StyleFieldProps) {
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="style_hint"
|
||||
className="block text-xs uppercase tracking-wider text-fg-muted mb-1"
|
||||
className="block text-[11px] font-medium uppercase tracking-wider text-fg-muted mb-1.5"
|
||||
>
|
||||
Music style{" "}
|
||||
<span className="text-fg-muted/70 normal-case">(optional)</span>
|
||||
<span className="text-fg-muted/70 normal-case font-normal">
|
||||
(optional)
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="style_hint"
|
||||
@@ -357,13 +370,13 @@ function StyleField({
|
||||
type="button"
|
||||
onClick={() => onRandom("normal")}
|
||||
disabled={loading !== null}
|
||||
className="btn-secondary text-xs py-2"
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-bg-card hover:bg-bg-hover transition-colors text-xs font-medium text-fg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Generate a coherent Suno-friendly style description"
|
||||
>
|
||||
{loading === "normal" ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<Wand2 className="w-3.5 h-3.5" />
|
||||
<Wand2 className="w-3 h-3" />
|
||||
)}
|
||||
Surprise me
|
||||
</button>
|
||||
@@ -371,13 +384,13 @@ function StyleField({
|
||||
type="button"
|
||||
onClick={() => onRandom("crazy")}
|
||||
disabled={loading !== null}
|
||||
className="btn-secondary text-xs py-2"
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-bg-card hover:bg-bg-hover transition-colors text-xs font-medium text-fg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Generate an unusual genre mashup that still works in Suno"
|
||||
>
|
||||
{loading === "crazy" ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<Flame className="w-3.5 h-3.5" />
|
||||
<Flame className="w-3 h-3" />
|
||||
)}
|
||||
Go crazy
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user