Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4fe4950
feat(workspace): name tonight's first transposition plan on the map
seonghobae Aug 23, 2026
dcb3e9f
docs(agents): remove duplicate code-style rule
seonghobae Aug 23, 2026
3a7d4c5
test(workspace): reject non-contract transpose labels
seonghobae Aug 23, 2026
4d0a661
fix(workspace): reject non-contract transpose labels
seonghobae Aug 23, 2026
bafce64
test(workspace): require unique transposition callout ids
seonghobae Aug 24, 2026
f879f6b
fix(workspace): give transposition callouts unique ids
seonghobae Aug 24, 2026
65a024a
test(workspace): preserve transpose action across section reorder
seonghobae Aug 24, 2026
23207b5
fix(workspace): keep transpose action semantic across reorder
seonghobae Aug 24, 2026
39d0c4b
test(workspace): align transpose callout label
seonghobae Aug 24, 2026
e68a90a
test: keep transpose plan region name stable
seonghobae Aug 24, 2026
e244fa3
fix: keep transpose plan region name stable
seonghobae Aug 24, 2026
cb2dbaa
test(workspace): require localized structure labels
seonghobae Aug 24, 2026
f1e86e9
fix(workspace): localize structure labels consistently
seonghobae Aug 24, 2026
a4368ba
Merge branch 'develop' into feat/workspace-first-transposition-plan
seonghobae Aug 25, 2026
5ce6b95
fix(review): localize section roadmap card labels to match the timeline
seonghobae Aug 26, 2026
3043d4d
fix(review): memoize transposition callout locale snapshot and transl…
seonghobae Aug 26, 2026
0d5dba7
fix(review): localize the rehearsal priorities focus line
seonghobae Aug 26, 2026
de6c250
fix(review): localize focus prefix, clarify transpose empty copy, anc…
seonghobae Aug 26, 2026
6a51ace
test(workspace): isolate transposition navigation fixtures
seonghobae Aug 26, 2026
19dbaa7
test(workspace): pin valid transposition label baseline
seonghobae Aug 26, 2026
688bbe9
test(workspace): reuse shared transposition contracts
seonghobae Aug 26, 2026
54db8d4
test(workspace): verify moved transposition target
seonghobae Aug 26, 2026
de0098b
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 26, 2026
d8f17f1
fix(i18n): localize section labels in range next-action copy
seonghobae Aug 26, 2026
6f351b0
fix(i18n): localize focus-summary fallback copy
seonghobae Aug 26, 2026
c4d5032
fix(workspace): validate range section labels
seonghobae Aug 28, 2026
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
39 changes: 0 additions & 39 deletions .Jules/palette.md

This file was deleted.

40 changes: 40 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
## 2024-05-19 - Replace HTML disabled with aria-disabled="true" for Accessible Tooltips
**Learning:** Native HTML `disabled` attributes completely hide elements from screen readers and block all pointer/hover events, preventing tooltips from functioning for disabled elements.
**Action:** Replace `disabled` with `aria-disabled="true"`, enforce block click handlers via `e.preventDefault()`, and add a title tooltip directly to the element to maintain full tooltip accessibility and keyboard focus support for visually impaired and mouse users.

## 2024-05-18 - Added focus visible styles for keyboard navigation
**Learning:** Interactive inline buttons (like the chord editor) and scrollable regions with `tabIndex={0}` do not automatically get focus visible styles, meaning keyboard users tabbing through won't know they are focused on them. Unlike central `<Button />` components which bake focus states in, these custom inline interactive elements need explicit focus styling.
**Action:** Always add explicit focus visible styles (e.g., `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300`) to custom interactive elements and scrollable regions with `tabIndex={0}` for proper keyboard accessibility.

## 2024-05-24 - Visual tooltips for disabled icon-only buttons
**Learning:** Icon-only buttons with `aria-label` are accessible to screen readers, but sighted users relying on mouse hover don't get context if the `title` attribute is missing, especially when the button is disabled and its purpose is unclear (e.g. "coming soon").
**Action:** Always add a `title` attribute mirroring the `aria-label` (or providing a specific disabled reason) to icon-only buttons so sighted users also receive explanatory tooltips on hover.

## 2026-06-13 - Added screen reader text for tooltip divs
**Learning:** When using `title` attributes on non-interactive elements like icon-only `div`s for tooltips, screen readers might not announce them properly because they aren't focusable. The visual tooltip is not enough for accessibility.
**Action:** Always add a visually hidden `<span className="sr-only">[Tooltip Text]</span>` inside non-interactive elements that rely on a `title` attribute so that screen readers have text content to announce.

## 2026-06-18 - Added keyboard accessibility to scrollable regions
**Learning:** Horizontally scrollable regions (like the `SectionRoadmap` component) are not accessible to keyboard-only users unless they can receive focus. Keyboard users must be able to focus the container to scroll its content using arrow keys.
**Action:** For proper keyboard accessibility in custom scrollable regions, always include `tabIndex={0}`, an appropriate `aria-label`, `role="region"`, and explicit focus visible styling (e.g., `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300`).

## 2026-06-19 - Internationalization
**Learning:** The desktop app uses i18n via json files located in `apps/desktop/src/locales/`
**Action:** When adding new text strings, make sure to add it to all locale files.

## 2026-06-25 - Native tooltips on disabled elements
**Learning:** Standard HTML `title` attributes used as tooltips do not render on elements that use Tailwind's `pointer-events-none` class, which is often applied to `disabled:` variants in Base UI and styled components.
**Action:** Do not rely on native `title` attributes for explaining disabled states on buttons with `pointer-events-none`. Instead, either use a custom tooltip component or ensure focus/interactive styles are preserved if an explanation is strictly required.

## 2024-06-29 - 비활성화된 네이티브 버튼의 툴팁 차단
**Learning:** 네이티브 `<button>` 요소에 `disabled` 속성을 사용하면 마우스 호버 이벤트를 포함한 포인터 이벤트가 완전히 차단되어 표준 HTML `title` 속성이 툴팁으로 표시되지 않으며, 키보드 탭 순서(tab order)에서도 제외됩니다.
**Action:** "출시 예정" 등 설명 툴팁이 필요한 비활성화된 액션 버튼의 경우, `title`을 버튼에 직접 붙이는 대신 포커스 가능한 `span` (`<span tabIndex={0} title={...} role="button" aria-disabled="true">`)으로 버튼을 감싸서 시각적 및 스크린 리더 접근성을 모두 보장해야 합니다.

## 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.

## 2026-07-02 - Inline clear buttons preserve focus
**Learning:** Inline clear buttons often unmount immediately after clearing state, which can drop keyboard focus to the document body.
**Action:** Move focus back to the owning input before clearing state, and cover the behavior with a DOM focus test.
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- Keep UI and analysis engine decoupled through shared contracts.
- Prefer minimal, test-first changes for production code.
- Prefer practical, friendly, rehearsal-first wording over academic or authority-heavy language.
- Name tonight's first transposition plan with the owning part when an active role is corroborated, the owned `transpositionPlan` copy, the labeled section, and the time so the next action is obvious. Do not invent that copy from groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, confirmed overrides, harmonic explanations, or confidence notes.
- Do not reduce the product to a chord analyzer when form, timing, player coordination, playable ranges, simplification, and setup cues are the real rehearsal blockers.
- Do not frame usability as a reason to accept weak analysis quality; BandScope should aim for both easy use and high accuracy.

Expand Down
3 changes: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# ARCHITECTURE.md

Last updated: 2026-03-11
Last updated: 2026-08-24

## Brand source

- Product identity, UX tone, copy rules, and prioritization tie-breakers live in `docs/brand-story.md`.
- The mounted workspace copy for tonight's first transposition plan must name the owning part when corroborated, the owned `transpositionPlan` text, the labeled section, and the time so the next action is obvious. Open moves to the matching rendered map section. Do not invent that copy from groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, confirmed overrides, harmonic explanations, or confidence notes. Distinct from the first-entrance Tempo/Key/Transpose cockpit metrics.
- Future PRDs, TRDs, onboarding copy, empty states, error messages, and marketing copy should use that document as the single brand source of truth.

## Security source
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Name tonight's first transposition plan in the mounted rehearsal workspace so the room can lock the owned part transpose before rehearsal; the Open action moves to the matching rendered map section, while inherited or accessor-backed runtime metadata remains guidance-only instead of becoming navigation authority.
- Name tonight's first playable range on the ready rehearsal map and tell the player to check that span on their instrument before the section.
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into

Three layers, decoupled through shared contracts:

- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The ready workspace names tonight's first playable range and the next instrument check. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The mounted workspace names tonight's first transposition plan and opens the matching rendered map section. The ready workspace names tonight's first playable range and the next instrument check. Do not invent that copy from groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, confirmed overrides, harmonic explanations, or confidence notes. Distinct from the first-entrance Tempo/Key/Transpose cockpit metrics. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
- `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis.
- `services/analysis-engine` — Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { render, screen } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { describe, expect, it } from "vitest";
import { FirstTranspositionPlanCallout } from "./FirstTranspositionPlanCallout";

describe("FirstTranspositionPlanCallout accessibility", () => {
it("keeps the complementary region name stable when the plan is unavailable", () => {
const song = createDemoRehearsalSong();
for (const role of song.sections[0]!.roles) {
role.transpositionPlan = "";
}

render(<FirstTranspositionPlanCallout song={song} />);

expect(
screen.getByRole("complementary", {
name: "Tonight's first transpose plan"
})
).toBeTruthy();
expect(
screen.getByText(
"No part has a transpose plan yet. Keep working from tonight's map until one appears."
)
).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FirstTranspositionPlanCallout } from "./FirstTranspositionPlanCallout";

describe("FirstTranspositionPlanCallout Korean role copy", () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it("keeps vowel-ending dynamic role names particle-safe before and after the transpose action", () => {
vi.stubGlobal("navigator", { language: "ko-KR" });
const song = createDemoRehearsalSong();
const seed = song.sections[0]!;
seed.roles = [
{
...seed.roles[0]!,
id: "piano",
name: "피아노",
rehearsalPriority: "high",
transpositionPlan: "If the singer drops to B minor, keep the shape a whole step lower."
}
];
seed.partGraph = [{ role_id: "piano", is_active: true, handoff_to: [], handoff_from: [] }];

const grid = document.createElement("div");
grid.dataset.testid = "song-structure-grid";
grid.setAttribute("role", "region");
grid.setAttribute("aria-label", "Scrollable song structure timeline");
const target = document.createElement("div");
target.dataset.sectionIndex = "0";
Object.defineProperty(target, "scrollIntoView", {
configurable: true,
value: vi.fn()
});
grid.appendChild(target);
document.body.appendChild(grid);

render(<FirstTranspositionPlanCallout song={song} />);

expect(screen.getByText("0:10 벌스에서 피아노 파트의 이조 계획이 있습니다.")).toBeTruthy();
expect(screen.queryByText(/피아노이/)).toBeNull();
expect(screen.queryByText(/피아노가/)).toBeNull();

fireEvent.click(screen.getByRole("button", { name: "0:10 피아노 이조 위치 열기" }));

expect(screen.getByText("0:10에서 피아노 파트의 이조를 맞춘 다음 합주를 시작하세요.")).toBeTruthy();
expect(screen.queryByText(/피아노과/)).toBeNull();

grid.remove();
});
});
Loading
Loading