Skip to content
Open
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
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@
## 2024-07-01 - Testing components with focusable disabled button wrappers
**Learning:** When native disabled buttons are wrapped in a focusable `span` to provide accessible tooltips, tests that previously found and clicked the `button` (by temporarily removing the `disabled` attribute) may fail or become overly complex. It is cleaner and more accurate to query the wrapper element (e.g. via its `title`) and fire events on it, reflecting the actual accessible DOM structure.
**Action:** When testing UI components that wrap disabled buttons in a focusable span for accessibility (e.g., using a tooltip/title), use `screen.getByTitle(...)` to query the wrapper element for interactions like `fireEvent.click` rather than `screen.getByRole('button')`.

## 2024-05-24 - Avoid nesting native buttons with ARIA role button on wrappers
**Learning:** Adding `role="button"` to a `span` or `div` wrapper that contains a native `<button>` element inside violates ARIA specifications. Interactive roles (like `button`) must not contain other interactive elements (even if the inner element is disabled or has `aria-hidden`), as this causes invalid/redundant accessibility trees and screen reader confusion.
**Action:** Always verify wrappers used to implement tooltips for disabled buttons are standard elements (e.g., `<span tabIndex={0} title="...">`) but *do not* assign `role="button"` to the wrapper itself.
4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/osv-scanner.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ reason = "glib 0.18.5 VariantStrIter advisory inherited through Tauri/wry/webkit

[[IgnoredVulns]]
id = "RUSTSEC-2026-0194"
reason = "quick-xml 0.39.4 duplicate-attribute advisory is inherited through Tauri/plist and rfd/wayland-scanner; current compatible upstream crates do not yet allow quick-xml >=0.41.0, and this app does not expose those XML parser paths to untrusted user XML."
reason = "quick-xml 0.39.4 duplicate-attribute advisory is inherited through Tauri/plist runtime metadata handling and rfd/wayland-scanner build-time bindings; BandScope's user audio, YouTube, project, export, and rehearsal data paths do not parse attacker-supplied XML through these owners. Current compatible upstream crates do not yet allow quick-xml >=0.41.0; scripts/checks/verify_supply_chain.py keeps this owner-chain exception narrow until GitHub issue #542 tracks the upstream refresh and removal."

[[IgnoredVulns]]
id = "RUSTSEC-2026-0195"
reason = "quick-xml 0.39.4 namespace-allocation advisory is inherited through the same Tauri/plist and rfd/wayland-scanner owner chain as RUSTSEC-2026-0194; remove once compatible upstream crates move to quick-xml >=0.41.0."
reason = "quick-xml 0.39.4 namespace-allocation advisory is inherited through the same Tauri/plist runtime metadata handling and rfd/wayland-scanner build-time owner chain as RUSTSEC-2026-0194; GitHub issue #542 tracks removal once compatible upstream crates move to quick-xml >=0.41.0."
13 changes: 9 additions & 4 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ describe("App", () => {
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(fireEvent.click(screen.getByRole("button", { name: /settings coming soon/i }))).toBe(false);
expect(fireEvent.click(screen.getByRole("button", { name: /help coming soon/i }))).toBe(false);
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 +1429,13 @@ describe("App", () => {
});


it("renders disabled Settings and Help buttons as focusable spans for accessibility", () => {
it("renders Settings and Help as focusable aria-disabled controls", () => {
render(<App />);
const settingsSpan = screen.getByTitle("Settings coming soon");
expect(settingsSpan).toHaveAttribute("tabIndex", "0");
expect(settingsSpan).toHaveAttribute("role", "button");
const settingsButton = screen.getByRole("button", { name: "Settings coming soon" });
const helpButton = screen.getByRole("button", { name: "Help coming soon" });
expect(settingsButton).toHaveAttribute("aria-disabled", "true");
expect(settingsButton).not.toHaveAttribute("disabled");
expect(helpButton).toHaveAttribute("aria-disabled", "true");
expect(helpButton).not.toHaveAttribute("disabled");
});
});
41 changes: 27 additions & 14 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent, type ReactNode } from "react";
import {
AudioWaveform,
CircleHelp,
Expand Down Expand Up @@ -67,6 +67,11 @@ const NAV_ITEMS = [

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

/** Documented. */
function preventUnavailableAction(event: MouseEvent<HTMLButtonElement>): void {
event.preventDefault();
}

/** Documented. */
function progressMessage(
t: ReturnType<typeof createTranslator>,
Expand Down Expand Up @@ -535,18 +540,26 @@ 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">
<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">
<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>
<button
type="button"
aria-disabled={true}
aria-label="Settings coming soon"
title="Settings coming soon"
onClick={preventUnavailableAction}
className="inline-flex cursor-not-allowed items-center justify-center rounded-xl p-2 text-slate-600 transition 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}
aria-label="Help coming soon"
title="Help coming soon"
onClick={preventUnavailableAction}
className="inline-flex cursor-not-allowed items-center justify-center rounded-xl p-2 text-slate-600 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
>
<CircleHelp className="size-5" aria-hidden="true" />
</button>
</div>
</div>
</aside>
Expand Down Expand Up @@ -646,7 +659,7 @@ 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">
<span tabIndex={0} 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"
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/features/workspace/RoleSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function RoleSwitcher({ roles, activeRole, onRoleChange }: RoleSwitcherPr
return (
<div className="flex flex-col gap-4 py-2 sm:flex-row sm:items-center">
<div className="flex whitespace-nowrap text-sm font-semibold text-slate-200">
<Users className="mr-2 size-4 text-cyan-300" />
<Users className="mr-2 size-4 text-cyan-300" aria-hidden="true" />
{t("roleSwitcherTitle")}
</div>
<Tabs
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/features/workspace/SectionRoadmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,14 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma

{role.setupNote && (
<div className="flex items-start gap-2 rounded-md border border-amber-300/20 bg-amber-300/[0.08] p-2 text-xs font-medium text-amber-100">
<Lightbulb className="mt-0.5 size-3.5 shrink-0" />
<Lightbulb className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
<span className="leading-snug">{role.setupNote}</span>
</div>
)}

{role.simplification && (
<div className="flex items-start gap-2 rounded-md border border-indigo-300/20 bg-indigo-300/[0.08] p-2 text-xs font-medium text-indigo-100">
<Wand2 className="mt-0.5 size-3.5 shrink-0" />
<Wand2 className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
<span className="leading-snug">{role.simplification}</span>
</div>
)}
Expand All @@ -200,7 +200,7 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
<div className="mt-2 space-y-1.5">
{role.overlapWarnings.map((warning, wIdx) => (
<div key={wIdx} className="flex items-start gap-2 rounded-md border border-rose-300/20 bg-rose-300/[0.08] p-2 text-xs font-medium text-rose-100">
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
<AlertCircle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
<span className="leading-snug">{warning}</span>
</div>
))}
Expand Down
75 changes: 52 additions & 23 deletions apps/desktop/src/features/workspace/Workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useMemo, memo } from "react";
import { useState, useMemo, memo, type MouseEvent } from "react";
import { parseProjectBootstrapSummary, type ProjectBootstrapSummary, type RehearsalSong, type RehearsalRole } from "@bandscope/shared-types";
import { RoleSwitcher } from "./RoleSwitcher";
import { SectionRoadmap } from "./SectionRoadmap";
Expand Down Expand Up @@ -40,6 +40,11 @@ function downloadTextFile(contents: string, type: string, filename: string): voi

type Translator = ReturnType<typeof createTranslator>;

/** Documented. */
function preventUnavailableAction(event: MouseEvent<HTMLButtonElement>): void {
event.preventDefault();
}

/** Documented. */
function formatStatusLabel(status: string): string {
return status.replaceAll("_", " ");
Expand Down Expand Up @@ -222,7 +227,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
onClick={handleExportCueSheet}
className="min-h-10 border-cyan-300/30 bg-cyan-300/10 font-semibold text-cyan-50 shadow-[0_10px_30px_rgba(34,211,238,0.16)] hover:bg-cyan-300/20 hover:text-white"
>
<Download className="mr-2 size-4 text-cyan-200" />
<Download className="mr-2 size-4 text-cyan-200" aria-hidden="true" />
Export Cue Sheet (CSV)
</Button>
<Button
Expand All @@ -231,7 +236,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
onClick={handleExportChart}
className="min-h-10 border-white/10 bg-white/5 font-semibold text-slate-100 shadow-sm hover:bg-white/10 hover:text-white"
>
<Download className="mr-2 size-4 text-slate-300" />
<Download className="mr-2 size-4 text-slate-300" aria-hidden="true" />
Export Chart (JSON)
</Button>
<Button
Expand All @@ -240,7 +245,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
onClick={handleExportHandoff}
className="min-h-10 border-teal-300/25 bg-teal-300/10 font-semibold text-teal-50 shadow-sm hover:bg-teal-300/20 hover:text-white"
>
<Download className="mr-2 size-4 text-teal-200" />
<Download className="mr-2 size-4 text-teal-200" aria-hidden="true" />
Export Handoff (JSON)
</Button>
</div>
Expand Down Expand Up @@ -311,15 +316,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}
aria-label="Play stem coming soon"
title="Play stem coming soon"
onClick={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-70"
>
Play stem
</Button>
<Button
type="button"
aria-disabled={true}
aria-label="Loop section coming soon"
title="Loop section coming soon"
onClick={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-70"
>
Loop section
</Button>
<Button
type="button"
aria-disabled={true}
aria-label="Solo / mute others coming soon"
title="Solo / mute others coming soon"
onClick={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 text-slate-400 opacity-70"
>
Solo / mute others
</Button>
{canTranscribeBass ? (
<Button
type="button"
Expand All @@ -330,16 +359,16 @@ 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={preventUnavailableAction}
variant="outline"
className="min-h-11 cursor-not-allowed border-white/10 bg-white/5 font-semibold text-slate-500 opacity-70"
>
Transcribe Bass
</Button>
)}
</div>
<div className="mt-4 grid gap-3 lg:grid-cols-2">
Expand Down