Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ describe("App", () => {
it("renders the rehearsal cockpit shell before analysis starts", () => {
render(<App />);

expect(screen.getByRole("img", { name: /BandScope circular equalizer mark/i })).toBeTruthy();
expect(screen.getByText(/Your rehearsal map stays on this device/i)).toBeTruthy();
expect(screen.getByRole("navigation", { name: /primary rehearsal views/i })).toBeTruthy();
expect(screen.getByRole("heading", { name: /Workspace Home/i })).toBeTruthy();
expect(screen.getByText(/SYNCED • LOCAL/i)).toBeTruthy();
Expand All @@ -189,8 +191,8 @@ describe("App", () => {
expect(screen.getByText(/^Tempo$/i)).toBeTruthy();
expect(screen.getByText(/^Key$/i)).toBeTruthy();
expect(screen.getByText(/Local-first/i)).toBeTruthy();
expect(screen.getByText(/Local project data stays on this device/i)).toBeTruthy();
expect(screen.getByText(/YouTube import contacts the source provider/i)).toBeTruthy();
expect(screen.getByText(/Project files stay local/i)).toBeTruthy();
expect(screen.getByText(/YouTube only leaves the app when you choose import/i)).toBeTruthy();
});

it("renders the loaded song as a dark rehearsal command board", async () => {
Expand Down
44 changes: 35 additions & 9 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ const NAV_ITEMS = [
{ label: "Notes", icon: FileMusic, active: false }
] as const;

const BRAND_BAR_HEIGHTS = ["h-3", "h-5", "h-7", "h-4", "h-6"] as const;

/** Documented. */
function progressMessage(
t: ReturnType<typeof createTranslator>,
Expand All @@ -76,6 +78,27 @@ function progressMessage(
}
}

/** Documented. */
function BandScopeMark() {
return (
<span
role="img"
aria-label="BandScope circular equalizer mark"
className="relative grid size-11 shrink-0 place-items-center rounded-full border border-cyan-200/45 bg-cyan-200/10 shadow-[0_0_28px_rgba(103,232,249,0.34)]"
>
<span className="absolute inset-1 rounded-full border border-teal-200/20" aria-hidden="true" />
<span className="flex h-8 items-end gap-0.5" aria-hidden="true">
{BRAND_BAR_HEIGHTS.map((height, index) => (
<span
key={`${height}-${index}`}
className={`w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 ${height}`}
/>
))}
</span>
</span>
);
}

/** Documented. */
function MetricCard({
icon,
Expand All @@ -91,7 +114,7 @@ function MetricCard({
accent?: string;
}) {
return (
<article className="group relative overflow-hidden rounded-2xl border border-white/10 bg-slate-950/65 p-4 shadow-[0_18px_60px_rgba(0,0,0,0.24)] backdrop-blur-xl transition hover:-translate-y-0.5 hover:border-cyan-300/40">
<article className="group relative overflow-hidden rounded-lg border border-[color:var(--bandscope-border)] bg-[var(--bandscope-surface)] p-4 shadow-[0_18px_60px_rgba(0,0,0,0.24)] backdrop-blur-xl transition hover:-translate-y-0.5 hover:border-cyan-300/40">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(34,211,238,0.16),transparent_35%)] opacity-60 transition group-hover:opacity-100" />
<div className="relative flex items-start gap-3">
<div className={`rounded-xl bg-white/5 p-2 ${accent}`}>{icon}</div>
Expand Down Expand Up @@ -328,24 +351,27 @@ export function App() {
};

return (
<div className="min-h-screen overflow-x-hidden bg-[#020713] text-slate-100 selection:bg-cyan-300/30">
<div className="min-h-screen overflow-x-hidden bg-[var(--bandscope-bg)] text-slate-100 selection:bg-cyan-300/30">
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_18%_8%,rgba(15,120,255,0.22),transparent_28%),radial-gradient(circle_at_78%_0%,rgba(124,58,237,0.20),transparent_30%),linear-gradient(180deg,#07111f_0%,#020713_55%,#020611_100%)]" />
<div className="pointer-events-none fixed inset-0 opacity-[0.06] [background-image:linear-gradient(rgba(255,255,255,.8)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.8)_1px,transparent_1px)] [background-size:46px_46px]" />

<div className="relative flex min-h-screen">
<aside className="hidden w-64 shrink-0 border-r border-white/10 bg-slate-950/72 px-5 py-5 shadow-[24px_0_80px_rgba(0,0,0,0.28)] backdrop-blur-2xl lg:flex lg:flex-col">
<aside className="hidden w-64 shrink-0 border-r border-[color:var(--bandscope-border)] bg-[var(--bandscope-surface-strong)] px-5 py-5 shadow-[24px_0_80px_rgba(0,0,0,0.28)] backdrop-blur-2xl lg:flex lg:flex-col">
<div className="mb-7 flex items-center gap-2" aria-hidden="true">
<span className="size-3 rounded-full bg-red-400" />
<span className="size-3 rounded-full bg-amber-300" />
<span className="size-3 rounded-full bg-emerald-400" />
</div>

<div className="mb-9 flex items-center gap-3">
<div className="grid size-11 place-items-center rounded-2xl border border-cyan-300/50 bg-cyan-300/10 shadow-[0_0_28px_rgba(34,211,238,0.35)]">
<AudioWaveform className="size-6 text-cyan-300" aria-hidden="true" />
</div>
<div className="text-2xl font-black tracking-tight">
Band<span className="text-cyan-300">Scope</span>
<BandScopeMark />
<div>
<div className="text-2xl font-black tracking-tight">
Band<span className="text-cyan-300">Scope</span>
</div>
<div className="text-xs font-semibold uppercase text-slate-400">
Rehearsal cockpit
</div>
</div>
</div>

Expand Down Expand Up @@ -377,7 +403,7 @@ export function App() {
Local-first
</div>
<p className="mt-2 text-sm leading-6 text-slate-400">
Local project data stays on this device. YouTube import contacts the source provider only when you choose it.
Your rehearsal map stays on this device. Project files stay local. YouTube only leaves the app when you choose import.
</p>
<div className="mt-3 h-14 overflow-hidden rounded-xl bg-[linear-gradient(90deg,rgba(34,211,238,.12),rgba(124,58,237,.12))]">
<div className="flex h-full items-end gap-0.5 px-2 pb-1" aria-hidden="true">
Expand Down
44 changes: 16 additions & 28 deletions apps/desktop/src/features/workspace/GrooveMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
return (
<div
aria-live="polite"
className="mt-4 flex items-center justify-between rounded-lg border border-emerald-300/20 bg-emerald-300/[0.06] p-6"
className="mt-4 flex items-center justify-between rounded-lg border border-teal-300/20 bg-slate-950/72 p-6 shadow-inner shadow-cyan-950/40"
>
<span className="flex items-center font-medium text-emerald-100">
<span className="flex items-center font-medium text-teal-100">
<Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
Analyzing pitch... 45%
Checking the bass line... 45%
</span>
<Button variant="outline" size="sm" className="border-emerald-300/20 bg-emerald-300/10 text-emerald-100 hover:bg-emerald-300/20 hover:text-white">
<Button variant="outline" size="sm" className="border-teal-300/20 bg-teal-300/10 text-teal-100 hover:bg-teal-300/20 hover:text-white">
Cancel
</Button>
</div>
Expand All @@ -51,42 +51,34 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
if (renderedNotes.length === 0) {
return (
<div
className="mt-4 rounded-lg border border-dashed border-white/10 bg-white/[0.02] p-6 text-center text-sm italic text-slate-400"
className="mt-4 rounded-lg border border-dashed border-cyan-200/15 bg-slate-950/60 p-6 text-center text-sm text-slate-400"
>
No transcription yet. Click to analyze bass line.
No bass line transcription yet. Use it when you want to check the groove before rehearsal.
</div>
);
}

return (
<div
className="relative mt-4 overflow-x-auto rounded-lg bg-[#2c2c2c] p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300"
className="relative mt-4 overflow-x-auto rounded-lg border border-cyan-200/15 bg-slate-950/80 p-4 shadow-inner shadow-cyan-950/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
role="region"
tabIndex={0}
aria-label="Groove Map Transcription"
aria-label="Bass transcription groove map"
>
<div className="sr-only" style={{ position: "absolute", left: "-9999px" }}>
<div className="sr-only">
Transcription complete. {renderedNotes.length} notes analyzed.
</div>
<p className="mb-3 text-xs font-black uppercase tracking-[0.22em] text-cyan-200">
{renderedNotes.length} notes mapped for rehearsal
</p>

<div style={{ position: "relative", minWidth: "100%", height: `${uniquePitches.length * 40}px` }}>
{/* Render horizontal lanes for unique pitches */}
{uniquePitches.map((pitch, index) => (
<div
key={pitch}
style={{
position: "absolute",
top: `${index * 40}px`,
left: 0,
right: 0,
height: "40px",
borderBottom: "1px solid #444",
display: "flex",
alignItems: "center",
color: "#aaa",
fontSize: "12px",
paddingLeft: "8px"
}}
className="absolute inset-x-0 flex h-10 items-center border-b border-cyan-100/10 pl-2 text-xs font-semibold text-slate-400"
style={{ top: `${index * 40}px` }}
>
{pitch}
</div>
Expand All @@ -101,15 +93,11 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
return (
<div
key={index}
className="absolute h-6 rounded bg-gradient-to-r from-teal-300 via-cyan-300 to-violet-300 shadow-[0_0_18px_rgba(94,234,212,0.28)]"
style={{
position: "absolute",
top: `${pitchIndex * 40 + 8}px`,
left: `${leftPercent}%`,
width: `${widthPercent}%`,
height: "24px",
backgroundColor: "#52c41a",
borderRadius: "4px",
boxShadow: "0 1px 3px rgba(0,0,0,0.5)"
width: `${widthPercent}%`
}}
title={`${note.pitch} (${note.onset.toFixed(2)}s - ${note.offset.toFixed(2)}s)`}
/>
Expand Down
21 changes: 21 additions & 0 deletions apps/desktop/src/features/workspace/Workspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@ describe("Workspace", () => {
expect(transcribeButton.title).toBe("Transcribe part");
});

it("renders bass transcription in the dark rehearsal cockpit system", () => {
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
name: "Bass Guitar",
transcription: [
{ pitch: "E2", onset: 0, offset: 0.75, velocity: 0.74 },
{ pitch: "G2", onset: 0.9, offset: 1.25, velocity: 0.68 }
]
};

render(<Workspace song={song} />);
fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));

const grooveMap = screen.getByRole("region", { name: /bass transcription groove map/i });
expect(grooveMap.className).toContain("bg-slate-950");
expect(screen.getByText("E2")).toBeTruthy();
expect(screen.getByText("G2")).toBeTruthy();
expect(screen.getByText(/2 notes mapped for rehearsal/i)).toBeTruthy();
});

it("localizes empty and loading state titles", () => {
setNavigatorLanguage("ko-KR");
render(<EmptyState />);
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@
}

:root {
--bandscope-bg: #020713;
--bandscope-surface: rgb(8 18 35 / 78%);
--bandscope-surface-strong: rgb(2 6 23 / 88%);
--bandscope-border: rgb(255 255 255 / 10%);
--bandscope-cyan: #67e8f9;
--bandscope-teal: #5eead4;
--bandscope-violet: #a78bfa;
--bandscope-amber: #fcd34d;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
Expand Down