Skip to content
Open
19 changes: 12 additions & 7 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, createEvent } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { App } from "./App";

Expand Down Expand Up @@ -203,8 +203,8 @@ describe("App", () => {
expect(screen.getByText(/SYNCED β€’ LOCAL/i)).toBeTruthy();
expect(screen.getByText(/Turn a song into a practical rehearsal view\./i)).toBeTruthy();
expect(screen.getByRole("button", { name: /^Workspace$/i })).toBeTruthy();
expect(screen.getByRole("button", { name: /^Import$/i })).toBeTruthy();
expect(screen.getByRole("button", { name: /^Export$/i })).toBeTruthy();
expect(screen.getAllByTitle("Coming soon").length).toBeGreaterThan(0);

expect(screen.getByText(/^Tempo$/i)).toBeTruthy();
expect(screen.getByText(/^Key$/i)).toBeTruthy();
expect(screen.getByText(/Local-first/i)).toBeTruthy();
Expand Down Expand Up @@ -1427,10 +1427,15 @@ describe("App", () => {
});


it("renders disabled Settings and Help buttons as focusable spans for accessibility", () => {
it("renders disabled Settings and Help buttons using aria-disabled for accessibility", () => {
render(<App />);
const settingsSpan = screen.getByTitle("Settings coming soon");
expect(settingsSpan).toHaveAttribute("tabIndex", "0");
expect(settingsSpan).toHaveAttribute("role", "button");
const settingsButton = screen.getByTitle("Settings coming soon");
expect(settingsButton).toHaveAttribute("aria-disabled", "true");
expect(settingsButton.tagName).toBe("BUTTON");

// Simulate click and ensure it prevents default
const clickEvent = createEvent.click(settingsButton);
fireEvent(settingsButton, clickEvent);
expect(clickEvent.defaultPrevented).toBe(true);
});
});
139 changes: 83 additions & 56 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,24 +492,32 @@ export function App() {
</div>

<nav aria-label="Primary rehearsal views" className="space-y-2">
{NAV_ITEMS.map(({ label, icon: Icon, active }) => (
<button
key={label}
type="button"
aria-current={active ? "page" : undefined}
aria-disabled={active ? undefined : true}
disabled={!active}
title={active ? undefined : "Coming soon"}
className={`flex min-h-11 w-full items-center gap-3 rounded-xl px-3 text-left text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
active
? "bg-blue-600/70 text-white shadow-[0_12px_30px_rgba(37,99,235,0.32)]"
: "cursor-not-allowed text-slate-500 opacity-70"
}`}
>
<Icon className="size-5" aria-hidden="true" />
{label}
</button>
))}
{NAV_ITEMS.map(({ label, icon: Icon, active }) =>
active ? (
<button
key={label}
type="button"
aria-current="page"
className="flex min-h-11 w-full items-center gap-3 rounded-xl px-3 text-left text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 bg-blue-600/70 text-white shadow-[0_12px_30px_rgba(37,99,235,0.32)]"
>
<Icon className="size-5" aria-hidden="true" />
{label}
</button>
) : (
<button
key={label}
type="button"
aria-disabled="true"
title="Coming soon"
onClick={(e) => e.preventDefault()}
className="flex min-h-11 w-full cursor-not-allowed items-center gap-3 rounded-xl px-3 text-left text-sm font-semibold text-slate-500 opacity-70 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">Coming soon</span>
<Icon className="size-5" aria-hidden="true" />
{label}
</button>
)
)}
</nav>

<div className="mt-auto space-y-5">
Expand All @@ -535,41 +543,59 @@ export function App() {
</div>

<div className="flex items-center justify-between text-slate-400">
<span tabIndex={0} role="button" aria-disabled="true" title="Settings coming soon" className="inline-block cursor-not-allowed rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300">
<button
type="button"
aria-disabled="true"
title="Settings coming soon"
onClick={(e) => e.preventDefault()}
className="inline-block cursor-not-allowed rounded-xl p-2 text-slate-600 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">Settings coming soon</span>
<button type="button" disabled aria-hidden="true" className="pointer-events-none rounded-xl p-2 text-slate-600 transition">
<Settings className="size-5" aria-hidden="true" />
</button>
</span>
<span tabIndex={0} role="button" aria-disabled="true" title="Help coming soon" className="inline-block cursor-not-allowed rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300">
<Settings className="size-5" aria-hidden="true" />
</button>
<button
type="button"
aria-disabled="true"
title="Help coming soon"
onClick={(e) => e.preventDefault()}
className="inline-block cursor-not-allowed rounded-xl p-2 text-slate-600 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">Help coming soon</span>
<button type="button" disabled aria-hidden="true" className="pointer-events-none rounded-xl p-2 text-slate-600 transition">
<CircleHelp className="size-5" aria-hidden="true" />
</button>
</span>
<CircleHelp className="size-5" aria-hidden="true" />
</button>
</div>
</div>
</aside>

<main id="main-content" className="max-h-screen min-w-0 flex-1 overflow-y-auto px-4 py-4 sm:px-6 lg:px-8">
<nav aria-label="Compact rehearsal views" className="mb-4 flex gap-2 overflow-x-auto rounded-2xl border border-white/10 bg-slate-950/72 p-2 backdrop-blur-xl lg:hidden">
{NAV_ITEMS.map(({ label, icon: Icon, active }) => (
<button
key={label}
type="button"
aria-current={active ? "page" : undefined}
aria-label={`${label} compact view`}
aria-disabled={active ? undefined : true}
disabled={!active}
title={active ? undefined : "Coming soon"}
className={`inline-flex min-h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
active ? "bg-blue-600/70 text-white" : "cursor-not-allowed text-slate-500 opacity-70"
}`}
>
<Icon className="size-4" aria-hidden="true" />
{label}
</button>
))}
{NAV_ITEMS.map(({ label, icon: Icon, active }) =>
active ? (
<button
key={label}
type="button"
aria-current="page"
aria-label={`${label} compact view`}
className="inline-flex min-h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 bg-blue-600/70 text-white"
>
<Icon className="size-4" aria-hidden="true" />
{label}
</button>
) : (
<button
key={label}
type="button"
aria-disabled="true"
title="Coming soon"
aria-label={`${label} compact view`}
onClick={(e) => e.preventDefault()}
className="inline-flex min-h-10 shrink-0 cursor-not-allowed items-center gap-2 rounded-xl px-3 text-sm font-semibold text-slate-500 opacity-70 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<Icon className="size-4" aria-hidden="true" />
{label}
</button>
)
)}
</nav>

<section aria-label="Source controls" className="mb-4 rounded-3xl border border-white/10 bg-slate-950/72 p-4 shadow-[0_18px_60px_rgba(0,0,0,0.25)] backdrop-blur-xl">
Expand Down Expand Up @@ -646,17 +672,18 @@ export function App() {
Save Project
</Button>
) : (
<span tabIndex={0} role="button" aria-disabled="true" title="Analyze a song to enable saving" className="inline-block cursor-not-allowed rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300">
<Button
disabled
variant="outline"
className="min-h-11 border-white/10 bg-white/5 font-semibold text-slate-100"
aria-label="Save Project"
>
<Save className="mr-2 size-4" aria-hidden="true" />
Save Project
</Button>
</span>
<Button
type="button"
aria-disabled="true"
title="Analyze a song to enable saving"
onClick={(e) => e.preventDefault()}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 font-semibold text-slate-100 opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">Analyze a song to enable saving</span>
<Save className="mr-2 size-4" aria-hidden="true" />
Save Project
</Button>
)}
<Button
onClick={handleStartAnalysis}
Expand Down
63 changes: 44 additions & 19 deletions apps/desktop/src/features/workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,15 +311,39 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
<p className="text-xs font-black uppercase tracking-[0.24em] text-emerald-200">Stem Player</p>
<p className="mt-1 text-sm font-semibold text-slate-100">{activeRoleDetails?.name ?? activeRole}</p>
<div className="mt-3 flex flex-wrap gap-2">
<span tabIndex={0} role="button" aria-disabled="true" title="Coming soon" className="inline-block cursor-not-allowed rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300">
<Button type="button" disabled variant="outline" className="min-h-11 border-white/10 bg-white/5 text-slate-400">Play stem</Button>
</span>
<span tabIndex={0} role="button" aria-disabled="true" title="Coming soon" className="inline-block cursor-not-allowed rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300">
<Button type="button" disabled variant="outline" className="min-h-11 border-white/10 bg-white/5 text-slate-400">Loop section</Button>
</span>
<span tabIndex={0} role="button" aria-disabled="true" title="Coming soon" className="inline-block cursor-not-allowed rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300">
<Button type="button" disabled variant="outline" className="min-h-11 border-white/10 bg-white/5 text-slate-400">Solo / mute others</Button>
</span>
<Button
type="button"
aria-disabled="true"
title="Coming soon"
onClick={(e) => e.preventDefault()}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">Coming soon</span>
Play stem
</Button>
<Button
type="button"
aria-disabled="true"
title="Coming soon"
onClick={(e) => e.preventDefault()}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">Coming soon</span>
Loop section
</Button>
<Button
type="button"
aria-disabled="true"
title="Coming soon"
onClick={(e) => e.preventDefault()}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">Coming soon</span>
Solo / mute others
</Button>
{canTranscribeBass ? (
<Button
type="button"
Expand All @@ -330,16 +354,17 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
Transcribe Bass
</Button>
) : (
<span tabIndex={0} role="button" aria-disabled="true" title={`${activeRoleDetails?.name ?? "This role"} transcription is coming soon. Bass is ready first.`} className="inline-block cursor-not-allowed rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300">
<Button
type="button"
disabled
variant="outline"
className="min-h-11 border-emerald-300/20 bg-emerald-300/10 font-semibold text-emerald-100 disabled:border-white/10 disabled:bg-white/5 disabled:text-slate-500"
>
Transcribe Bass
</Button>
</span>
<Button
type="button"
aria-disabled="true"
title={`${activeRoleDetails?.name ?? "This role"} transcription is coming soon. Bass is ready first.`}
onClick={(e) => e.preventDefault()}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 font-semibold text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<span className="sr-only">{`${activeRoleDetails?.name ?? "This role"} transcription is coming soon. Bass is ready first.`}</span>
Transcribe Bass
</Button>
)}
</div>
<div className="mt-4 grid gap-3 lg:grid-cols-2">
Expand Down
Loading