Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
512bf42
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 11, 2026
c7650cb
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 11, 2026
f42c696
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 11, 2026
478f97f
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 11, 2026
17b96f1
fix: retain tooltip in read-only roadmap
seonghobae Aug 11, 2026
56b4793
fix(security): require patched PDF.js
seonghobae Aug 11, 2026
0163b12
fix(security): lock patched npm dependencies
seonghobae Aug 11, 2026
f272653
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 11, 2026
53b9a14
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 11, 2026
563f445
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 14, 2026
dbfe1a8
fix(workspace): remove unrelated dependency baseline changes
seonghobae Aug 14, 2026
2b0f68c
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 14, 2026
b3d6869
fix(a11y): explain unavailable chord editing
seonghobae Aug 14, 2026
ae921ba
fix(a11y): localize readonly chord guidance
seonghobae Aug 14, 2026
793a543
fix(a11y): localize readonly chord guidance
seonghobae Aug 14, 2026
4e4043a
test(a11y): pin readonly chord guidance copy
seonghobae Aug 14, 2026
fac59c2
fix(a11y): announce readonly chord recovery
seonghobae Aug 14, 2026
dda631d
test(a11y): verify readonly chord announcement
seonghobae Aug 14, 2026
edcfa7d
docs(a11y): describe readonly chord recovery
seonghobae Aug 14, 2026
aa5ef65
docs(changelog): remove duplicated historical entry
seonghobae Aug 14, 2026
814ded1
fix(a11y): describe readonly chord recovery semantically
seonghobae Aug 14, 2026
713c8c6
test(a11y): verify readonly chord description semantics
seonghobae Aug 14, 2026
02239cc
docs(a11y): record disabled-control semantics
seonghobae Aug 14, 2026
8175cbc
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 14, 2026
55e3753
fix(a11y): restore actionable unavailable chord description
seonghobae Aug 14, 2026
d8222ee
test(a11y): restore exact unavailable chord evidence
seonghobae Aug 14, 2026
9b8ac60
fix(i18n): restore unavailable chord recovery copy
seonghobae Aug 14, 2026
a23b803
fix(i18n): restore Korean chord recovery copy
seonghobae Aug 14, 2026
b30097e
docs(a11y): restore disabled chord control rationale
seonghobae Aug 14, 2026
b9ec25d
docs(changelog): restore buyer-visible accessibility outcome
seonghobae Aug 14, 2026
21cfdda
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 14, 2026
dcd54ad
test(a11y): require recovery description for readonly chords
seonghobae Aug 14, 2026
33a3a6e
fix(a11y): describe readonly chord recovery action
seonghobae Aug 14, 2026
1cf1b1c
feat(i18n): add readonly chord recovery copy
seonghobae Aug 14, 2026
a809038
feat(i18n): add Korean readonly chord recovery copy
seonghobae Aug 14, 2026
4800dc6
docs(changelog): describe readonly chord recovery behavior
seonghobae Aug 14, 2026
acf8a57
docs(a11y): record readonly chord control contract
seonghobae Aug 14, 2026
73824f5
test(a11y): guard chord description id references
seonghobae Aug 15, 2026
4d57af1
fix(a11y): decouple ARIA ids from analysis identifiers
seonghobae Aug 15, 2026
41519ad
docs(a11y): record ARIA id reference integrity
seonghobae Aug 15, 2026
7ab2dcd
docs(changelog): record accessible ID boundary
seonghobae Aug 15, 2026
3206c25
🎨 Palette: μ½”λ“œ μˆ˜μ • λ²„νŠΌμ— μ ‘κ·Όμ„± μžˆλŠ” 툴팁 지원 μΆ”κ°€
seonghobae Aug 15, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 ν•©μ£Ό μ—­ν• (Role)별 개인 μ—°μŠ΅ 진행도λ₯Ό 0~100% λ²”μœ„λ‘œ 기둝 및 μ‹œκ°ν™”ν•  수 μžˆλŠ” μ—°μŠ΅ 진척도(`practiceProgress`) 트래컀 κΈ°λŠ₯ μΆ”κ°€. UI 컨트둀(μŠ¬λΌμ΄λ” 및 +/- λ²„νŠΌ)κ³Ό ν•œ/영 λ‹€κ΅­μ–΄ 지원 포함.

### Fixed

- ν•©μ£Ό μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ˜ 읽기 μ „μš© μ½”λ“œ μˆ˜μ • λ²„νŠΌμ΄ ν‚€λ³΄λ“œ 포컀슀λ₯Ό μœ μ§€ν•˜κ³  `aria-disabled="true"`λ₯Ό λ…ΈμΆœν•˜λ©°, ν•œΒ·μ˜ μ ‘κ·Όμ„± 이름과 μ„€λͺ…μœΌλ‘œ νŽΈμ§‘ κ°€λŠ₯ν•œ 곑을 μ—΄μ–΄μ•Ό ν•œλ‹€λŠ” 볡ꡬ 방법을 μ•ˆλ‚΄ν•˜λ„λ‘ κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€. `aria-describedby`λŠ” 뢄석 λ°μ΄ν„°μ˜ section/role μ‹λ³„μž λŒ€μ‹  λ Œλ”λŸ¬κ°€ μ†Œμœ ν•˜λŠ” κ³ μœ ν•˜κ³  곡백 μ—†λŠ” IDλ₯Ό μ‚¬μš©ν•˜μ—¬ μž„μ˜ λ¬Έμžμ—΄ μ‹λ³„μžμ—μ„œλ„ μ°Έμ‘° 무결성을 μœ μ§€ν•˜κ³ , 포인터 `title`에도 같은 볡ꡬ 문ꡬλ₯Ό μ œκ³΅ν•˜λ©° 클릭은 계속 μ‹€νŒ¨-폐쇄 λ°©μ‹μœΌλ‘œ μ°¨λ‹¨λ©λ‹ˆλ‹€.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
46 changes: 46 additions & 0 deletions apps/desktop/src/features/workspace/SectionRoadmap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("SectionRoadmap", () => {
expect(screen.getAllByText("μ½”λ“œ").length).toBeGreaterThan(0);
expect(screen.getAllByText("큐").length).toBeGreaterThan(0);
expect(screen.getAllByTitle("μš°μ„ μˆœμœ„: high").length).toBeGreaterThan(0);
expect(screen.getAllByTitle("μ½”λ“œλ₯Ό λ°”κΎΈλ €λ©΄ νŽΈμ§‘ κ°€λŠ₯ν•œ 곑을 μ—¬μ„Έμš”").length).toBeGreaterThan(0);
expect(screen.getByText("μ‚¬μš©μž")).toBeTruthy();
});

Expand Down Expand Up @@ -60,4 +61,49 @@ describe("SectionRoadmap", () => {

expect(onSongUpdate).not.toHaveBeenCalled();
});

it("applies aria-disabled when onSongUpdate is missing, describes how to recover, and prevents click", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
// Intentionally omit onSongUpdate to simulate readonly mode
render(<SectionRoadmap song={song} activeRole={null} />);

const button = screen.getByRole("button", { name: "Edit chord for Bass Guitar in verse, current C#m7" });
expect(button).toHaveAttribute("aria-disabled", "true");
expect(button).toHaveAttribute("title", "Open an editable song to change this chord");

const descriptionId = button.getAttribute("aria-describedby");
expect(descriptionId).toBeTruthy();
expect(document.getElementById(descriptionId ?? "")?.textContent).toBe(
"Open an editable song to change this chord"
);

const promptSpy = vi.spyOn(window, "prompt");
fireEvent.click(button);

// Verify click handler early returns and prevents prompt
expect(promptSpy).not.toHaveBeenCalled();
});

it("uses unique whitespace-free description IDs for arbitrary section and role IDs", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0].id = "verse 1";
song.sections[0].roles[0].id = "bass guitar";

render(<SectionRoadmap song={song} activeRole={null} />);

const buttons = screen.getAllByRole("button", { name: /^Edit chord for / });
const descriptionIds = buttons.map((button) => button.getAttribute("aria-describedby"));
expect(descriptionIds.every((id) => typeof id === "string" && id.length > 0)).toBe(true);

const ids = descriptionIds.filter((id): id is string => id !== null);
expect(ids.every((id) => !/\s/.test(id))).toBe(true);
expect(new Set(ids).size).toBe(buttons.length);
for (const id of ids) {
expect(document.getElementById(id)?.textContent).toBe(
"Open an editable song to change this chord"
);
}
});
});
36 changes: 30 additions & 6 deletions apps/desktop/src/features/workspace/SectionRoadmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
return <CheckCircle2 className="size-4 text-emerald-200" aria-hidden="true" />;
};

/** Return a renderer-owned, whitespace-free description ID for one unavailable chord action. */
const chordEditUnavailableDescriptionId = (sectionIndex: number, roleIndex: number): string => {
return `chord-edit-unavailable-${sectionIndex}-${roleIndex}`;
};

return (
<div className="mt-6 space-y-4">
<div className="flex items-center justify-between">
Expand All @@ -103,7 +108,7 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
tabIndex={0}
aria-labelledby={sectionRoadmapTitleId}
>
{song.sections.map((section) => (
{song.sections.map((section, sectionIndex) => (
<Card
key={section.id}
className={`w-80 flex-none shrink-0 snap-start overflow-hidden shadow-[0_18px_60px_rgba(0,0,0,0.22)] transition duration-300 hover:-translate-y-1 hover:shadow-[0_24px_80px_rgba(0,0,0,0.32)] ${
Expand All @@ -124,7 +129,7 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
<CardContent className="p-4 space-y-4">
{section.roles
.filter(role => !activeRole || role.id === activeRole)
.map(role => (
.map((role, roleIndex) => (
<div
key={role.id}
className={`rounded-xl border-l-4 p-4 transition-all hover:translate-x-1 ${getPriorityColor(role.rehearsalPriority)}`}
Expand Down Expand Up @@ -152,7 +157,12 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
<button
type="button"
aria-label={editChordLabel(role, section.label)}
className={`-ml-2 rounded px-2 py-0.5 text-lg font-black tracking-tight transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${
aria-describedby={
onSongUpdate
? undefined
: chordEditUnavailableDescriptionId(sectionIndex, roleIndex)
}
className={`-ml-2 rounded px-2 py-0.5 text-lg font-black tracking-tight transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 ${
onSongUpdate
? "cursor-pointer hover:bg-white/10"
: "cursor-default"
Expand All @@ -161,12 +171,26 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
? "bg-indigo-300/15 text-indigo-200"
: "text-cyan-100"
}`}
onClick={() => handleChordEdit(section.id, role)}
title={onSongUpdate ? t("chordEditTitle") : undefined}
disabled={!onSongUpdate}
onClick={(e) => {
if (!onSongUpdate) {
e.preventDefault();
return;
}
handleChordEdit(section.id, role);
}}
title={onSongUpdate ? t("chordEditTitle") : t("chordEditUnavailableTitle")}
aria-disabled={!onSongUpdate}
Comment thread
seonghobae marked this conversation as resolved.
>
{role.harmony.chord}
</button>
{!onSongUpdate && (
<span
id={chordEditUnavailableDescriptionId(sectionIndex, roleIndex)}
className="sr-only"
>
{t("chordEditUnavailableTitle")}
</span>
)}
{role.harmony.source === "user" && (
<Badge variant="secondary" className="h-4 bg-indigo-300/20 px-1 text-[0.6rem] text-indigo-100 hover:bg-indigo-300/20">
{t("harmonySourceUserBadge")}
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"chordEditAriaLabel": "Edit chord for {roleName} in {sectionLabel}, current {chord}",
"chordEditPrompt": "Enter new chord:",
"chordEditTitle": "Click to edit chord",
"chordEditUnavailableTitle": "Open an editable song to change this chord",
"harmonySourceUserBadge": "User",
"roleSwitcherTitle": "Role-specific View",
"allRoles": "All Roles",
Expand Down Expand Up @@ -149,4 +150,4 @@
"practiceProgressLabel": "Practice Progress",
"decreasePracticeProgressLabel": "Decrease progress",
"increasePracticeProgressLabel": "Increase progress"
}
}
3 changes: 2 additions & 1 deletion apps/desktop/src/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"chordEditAriaLabel": "{roleName}의 {sectionLabel} μ½”λ“œ μˆ˜μ •, ν˜„μž¬ {chord}",
"chordEditPrompt": "μƒˆ μ½”λ“œ μž…λ ₯:",
"chordEditTitle": "μ½”λ“œ μˆ˜μ •",
"chordEditUnavailableTitle": "μ½”λ“œλ₯Ό λ°”κΎΈλ €λ©΄ νŽΈμ§‘ κ°€λŠ₯ν•œ 곑을 μ—¬μ„Έμš”",
"harmonySourceUserBadge": "μ‚¬μš©μž",
"roleSwitcherTitle": "μ•…κΈ°/보컬 μ—­ν• ",
"allRoles": "전체 보기",
Expand Down Expand Up @@ -149,4 +150,4 @@
"practiceProgressLabel": "μ—°μŠ΅ 진척도",
"decreasePracticeProgressLabel": "진척도 κ°μ†Œ",
"increasePracticeProgressLabel": "진척도 증가"
}
}
42 changes: 42 additions & 0 deletions docs/doctoring/accessible-disabled-chord-controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Discoverable unavailable chord controls

## Decision

BandScope keeps the chord-edit button focusable when editing is unavailable because the action remains useful to discover during rehearsal. The control uses `aria-disabled="true"` rather than native `disabled`, blocks activation before the edit prompt, preserves its stable localized accessible name, and associates a localized recovery instruction with `aria-describedby`.

The recovery copy is action-oriented: users are told to open an editable song to change the chord. The HTML `title` mirrors that recovery copy for pointer users, but BandScope does not treat `title` alone as sufficient assistive-technology evidence. The referenced description is the semantic explanation exposed for assistive technologies.

## Conformance rationale

WAI-ARIA 1.2 defines `aria-disabled` for elements that are perceivable but not operable, while `aria-describedby` identifies content that describes an object. The W3C ARIA Authoring Practices keyboard-interface guidance notes that unavailable controls can remain in the focus order when discoverability is important, using `aria-disabled` rather than native disabling where appropriate. The APG button pattern uses `aria-disabled="true"` for unavailable buttons, and the W3C ARIA1 technique documents `aria-describedby` as a programmatic association through an ID reference list whose referenced element IDs are unique.

The HTML Standard additionally requires an element `id` to be unique in its tree, non-empty, and free of ASCII whitespace. BandScope therefore generates the description IDs from renderer-owned section and visible-role positions rather than from analysis-domain identifiers. This keeps the ARIA references unique and whitespace-free even when otherwise valid section or role IDs contain whitespace or punctuation, while leaving those domain identifiers unchanged for business logic.

BandScope therefore verifies exact rendered values rather than inferring accessibility from CSS or source intent:

- the unavailable chord control remains a button and remains keyboard-focusable;
- `aria-disabled` renders exactly as `true`;
- the accessible name remains the localized chord-edit action;
- `aria-describedby` resolves to the exact localized recovery instruction;
- description IDs remain unique and whitespace-free even when validated domain section or role IDs contain arbitrary string content;
- the pointer `title` exposes the same recovery instruction;
- activation does not open the edit prompt when the action is unavailable; and
- enabled chord editing retains the existing localized action label, tooltip, prompt, and update behavior.

## Scope boundary

This change does not claim that a native `title` attribute implements the WAI-ARIA tooltip pattern. The APG tooltip pattern describes a distinct popup that appears on hover or keyboard focus and is referenced from the triggering element. If BandScope later requires a visible keyboard-focus popup, it should use the shared tooltip primitive and validate focus, dismissal, and description behavior explicitly.

## References

World Wide Web Consortium. (2023). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria/

World Wide Web Consortium, Web Accessibility Initiative. (2026, January 12). *ARIA1: Using the aria-describedby property to provide a descriptive label for user interface controls*. https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA1

World Wide Web Consortium, Web Accessibility Initiative. (n.d.). *Button pattern*. ARIA Authoring Practices Guide. Retrieved August 15, 2026, from https://www.w3.org/WAI/ARIA/apg/patterns/button/

World Wide Web Consortium, Web Accessibility Initiative. (n.d.). *Developing a keyboard interface*. ARIA Authoring Practices Guide. Retrieved August 15, 2026, from https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/

World Wide Web Consortium, Web Accessibility Initiative. (n.d.). *Tooltip pattern*. ARIA Authoring Practices Guide. Retrieved August 15, 2026, from https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/

WHATWG. (2026). *HTML Standard: The global attributes*. Retrieved August 15, 2026, from https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
Loading