Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a61093b
feat(workspace): add Storybook coverage and tokens for rehearsal comp…
seonghobae Aug 17, 2026
dec4e17
fix(storybook): provide required progress story args
seonghobae Aug 17, 2026
958c6b7
fix(storybook): provide required role switcher args
seonghobae Aug 17, 2026
87ed620
test(workspace): reject inert rehearsal callout actions
seonghobae Aug 17, 2026
dfea7b7
fix(workspace): disable unwired rehearsal callout actions
seonghobae Aug 17, 2026
6f56d9d
test(workspace): require localized confidence tooltip
seonghobae Aug 18, 2026
f54dd21
fix(workspace): localize confidence badge tooltip
seonghobae Aug 18, 2026
e68d37d
docs(workspace): explain overlap warning component
seonghobae Aug 18, 2026
05ec060
docs(workspace): explain rehearsal callout behavior
seonghobae Aug 18, 2026
e79f1b9
docs(workspace): explain roadmap helpers
seonghobae Aug 18, 2026
6a6c085
docs(ranges): explain range summary component
seonghobae Aug 18, 2026
edf525f
test(design): expose missing confidence badge size variants
seonghobae Aug 18, 2026
33c64c9
feat(design): support Figma confidence badge sizes
seonghobae Aug 18, 2026
248eafc
feat(tokens): encode Figma confidence badge heights
seonghobae Aug 18, 2026
785c84e
docs(storybook): inventory both confidence badge sizes
seonghobae Aug 18, 2026
f7f0c9e
docs(design): bind confidence size variants to Figma
seonghobae Aug 18, 2026
65e6236
docs(changelog): record Figma confidence size parity
seonghobae Aug 18, 2026
7db55a8
fix(design): keep confidence badge width semantics stable
seonghobae Aug 18, 2026
1f4cb0a
fix(quality): document confidence badge size contract
seonghobae Aug 18, 2026
f579995
feat(storybook): cover OverlapWarningList and enable the first practi…
seonghobae Aug 18, 2026
86be298
fix(storybook): apply current review feedback
seonghobae Aug 18, 2026
abf28f3
test(workspace): keep chord label placeholders literal
seonghobae Aug 18, 2026
d05f9d8
fix(workspace): interpolate chord edit labels once
seonghobae Aug 18, 2026
6f5cf97
test(design): lock confidence badge to live Figma semantic tokens
seonghobae Aug 18, 2026
7b38eaa
fix(design): align confidence tokens with live Figma semantics
seonghobae Aug 18, 2026
bc409e6
test(design): resolve Figma token source from workspace root
seonghobae Aug 19, 2026
bf87847
test(design): pin Figma confidence typography
seonghobae Aug 20, 2026
24026fd
fix(design): align confidence badge typography
seonghobae Aug 20, 2026
691061a
test(workspace): cover repeated overlap warnings
seonghobae Aug 21, 2026
d31a6ce
fix(workspace): keep repeated overlap warnings distinct
seonghobae Aug 21, 2026
58a4341
Merge remote-tracking branch 'origin/develop' into feat/workspace-sto…
seonghobae Aug 26, 2026
1de3d90
merge(develop): keep playable-range copy with Storybook tokens
seonghobae Aug 26, 2026
0309be8
fix(design): restore component contract governance
seonghobae Aug 27, 2026
9da6487
test(workspace): require readable light clash tokens
seonghobae Aug 27, 2026
9484322
fix(workspace): support light clash surfaces
seonghobae Aug 27, 2026
d678f37
fix(ranges): use readable clash surface
seonghobae Aug 27, 2026
e040c2d
fix(tokens): add light clash palette
seonghobae Aug 27, 2026
b172221
test(storybook): cover light clash surface
seonghobae Aug 27, 2026
6238356
docs(a11y): trace overlap contrast repair
seonghobae Aug 27, 2026
4f4c200
docs(design): bind light warning and live Figma drift
seonghobae Aug 27, 2026
c39e51f
docs(changelog): record clash contrast repair
seonghobae Aug 27, 2026
30d7e69
docs(design): record restored warning authority
seonghobae Aug 27, 2026
0b81117
docs(a11y): record restored Figma warning states
seonghobae Aug 27, 2026
027bd34
docs(design): bind current Figma warning authority
seonghobae Aug 27, 2026
1c58613
probe: write-scope retry
seonghobae Aug 28, 2026
163c2e7
chore: remove write-access probe from Storybook tokens branch
seonghobae Aug 28, 2026
192f188
docs(design): bind live overlap-warning authority
seonghobae Aug 30, 2026
d436820
docs(a11y): align warning provenance and security notes
seonghobae Aug 30, 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
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.
- Workspace components stay on `docs/design-system/component-contract.md` and Figma node 19-239. Storybook covers ConfidenceBadge, PracticeProgress, RehearsalCallout, RoleSwitcher, WorkspaceStates, SectionRoadmap, and the extracted OverlapWarningList.
- 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
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Last updated: 2026-03-11

- Product identity, UX tone, copy rules, and prioritization tie-breakers live in `docs/brand-story.md`.
- Future PRDs, TRDs, onboarding copy, empty states, error messages, and marketing copy should use that document as the single brand source of truth.
- Workspace Storybook stories cite Figma 19-239 and `docs/design-system/component-contract.md`. OverlapWarningList is the shared clash list for Section Roadmap and Ranges.

## Security source

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Storybook stories and workspace tokens for ConfidenceBadge (Figma 19-239), PracticeProgress, RehearsalCallout, RoleSwitcher, WorkspaceStates, SectionRoadmap, and the extracted OverlapWarningList. ConfidenceBadge now exposes the Figma Compact and Default size states for every confidence level while preserving Compact for existing call sites. OverlapWarningList is the shared clash list for Roadmap and Ranges. The RehearsalCallout story keeps the first practice action enabled.
- 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 All @@ -14,6 +15,7 @@

### Fixed

- Keep shared overlap warnings readable on both dark rehearsal surfaces and light Ranges cards by selecting surface-specific shared tokens and exposing both contexts in Storybook.
- Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance.

## [0.1.3] - 2026-04-29
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 ready workspace names tonight's first playable range and the next instrument check. Workspace Storybook stories cite Figma 19-239 and `docs/design-system/component-contract.md`; OverlapWarningList is the shared clash list for Section Roadmap and Ranges. `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
2 changes: 1 addition & 1 deletion apps/desktop/src/features/ranges/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe("RangesFeature", () => {

it("renders overlap warnings", () => {
render(<RangesFeature title="Ranges" song={mockSong} />);
expect(screen.getByText("⚠️ Clashing notes with Role 2")).toBeInTheDocument();
expect(screen.getByText("Clashing notes with Role 2")).toBeInTheDocument();
});

it("renders transcription count when transcription exists", () => {
Expand Down
25 changes: 4 additions & 21 deletions apps/desktop/src/features/ranges/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OverlapWarningList } from "../workspace/OverlapWarningList";
import type { RehearsalSong } from "@bandscope/shared-types";

/** Documented. */
/** Render per-role range, overlap, and transcription summaries for the loaded rehearsal song. */
export function RangesFeature(props: { title: string; song?: RehearsalSong | null }) {
Comment thread
seonghobae marked this conversation as resolved.
const { title, song } = props;

Expand Down Expand Up @@ -37,25 +38,7 @@ export function RangesFeature(props: { title: string; song?: RehearsalSong | nul
<div style={{ fontSize: "0.85em", color: "#333" }}>
🎵 {role.range.lowestNote} — {role.range.highestNote}
</div>
{role.overlapWarnings.length > 0 && (
<div style={{ marginTop: "8px" }}>
{role.overlapWarnings.map((warning, wIndex) => (
<div
key={wIndex}
style={{
fontSize: "0.8em",
color: "#fa8c16",
marginTop: "4px",
padding: "4px 6px",
backgroundColor: "#fff7e6",
borderRadius: "4px",
}}
>
⚠️ {warning}
</div>
))}
</div>
)}
<OverlapWarningList warnings={role.overlapWarnings} surface="light" />
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
{role.transcription && role.transcription.length > 0 && (
<div style={{ marginTop: "8px", fontSize: "0.8em", color: "#08979c", backgroundColor: "#e6fffb", padding: "4px 6px", borderRadius: "4px" }}>
<strong>Transcription available:</strong> {role.transcription.length} notes
Expand All @@ -68,4 +51,4 @@ export function RangesFeature(props: { title: string; song?: RehearsalSong | nul
))}
</section>
);
}
}
39 changes: 39 additions & 0 deletions apps/desktop/src/features/workspace/ConfidenceBadge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ConfidenceBadge } from "./ConfidenceBadge";

/** Storybook metadata for the Figma 19-239 confidence-badge level and size matrix. */
const meta = {
title: "Workspace/ConfidenceBadge",
component: ConfidenceBadge,
parameters: {
layout: "centered",
docs: {
description: {
component:
"Figma 19-239 Confidence Badge. Mirrors Level=Low/Medium/High and Size=Compact/Default with workspace tokens; existing call sites remain Compact by default.",
},
},
},
argTypes: {
size: {
control: "select",
options: ["compact", "default"],
},
},
} satisfies Meta<typeof ConfidenceBadge>;

export default meta;
type Story = StoryObj<typeof meta>;

/** Low-confidence compact badge, matching the backward-compatible Figma compact state. */
export const Low: Story = { args: { level: "low" } };
/** Low-confidence default-size badge from the Figma level/size matrix. */
export const LowDefault: Story = { args: { level: "low", size: "default" } };
/** Medium-confidence compact badge from the Figma level/size matrix. */
export const Medium: Story = { args: { level: "medium" } };
/** Medium-confidence default-size badge from the Figma level/size matrix. */
export const MediumDefault: Story = { args: { level: "medium", size: "default" } };
/** High-confidence compact badge from the Figma level/size matrix. */
export const High: Story = { args: { level: "high" } };
/** High-confidence default-size badge from the Figma level/size matrix. */
export const HighDefault: Story = { args: { level: "high", size: "default" } };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
95 changes: 95 additions & 0 deletions apps/desktop/src/features/workspace/ConfidenceBadge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { ConfidenceBadge } from "./ConfidenceBadge";

vi.mock("../../i18n", () => ({
createTranslator: () => (key: string) =>
({
confidenceLevelLow: "확신이 낮음",
confidenceLevelMedium: "귀로 한 번 더 확인",
confidenceLevelHigh: "믿고 가져가도 됨"
})[key] ?? key,
detectPreferredLocale: () => "ko"
}));

describe("ConfidenceBadge", () => {
it("keeps the pointer tooltip on the localized confidence contract", () => {
render(<ConfidenceBadge level="high" />);

const badge = screen.getByText("믿고 가져가도 됨");
expect(badge).toHaveAttribute("title", "믿고 가져가도 됨");
expect(badge).not.toHaveAttribute("title", "Confidence: high");
});

it("mirrors the Figma compact and default size variants without changing existing call sites", () => {
const { rerender } = render(<ConfidenceBadge level="low" />);

expect(screen.getByText("확신이 낮음")).toHaveClass(
"h-[var(--bandscope-confidence-compact-height)]"
);

rerender(<ConfidenceBadge level="low" size="default" />);

expect(screen.getByText("확신이 낮음")).toHaveClass(
"h-[var(--bandscope-confidence-default-height)]"
);
});

it("matches the live Figma Caption/Status typography contract", () => {
render(<ConfidenceBadge level="low" />);

expect(screen.getByText("확신이 낮음")).toHaveClass(
"text-[11px]",
"font-medium",
"leading-[16px]",
"tracking-normal"
);
});

it("keeps confidence colors mapped to the live Figma 19:239 semantic status variables", () => {
const tokenSource = readFileSync(
resolve(process.cwd(), "src/styles/workspace-tokens.css"),
"utf8"
).replace(/\s+/g, " ");

expect(tokenSource).toContain("--bandscope-status-danger: #fda4af;");
expect(tokenSource).toContain("--bandscope-status-danger-bg: #fda4af14;");
expect(tokenSource).toContain("--bandscope-status-danger-border: #fda4af4d;");
expect(tokenSource).toContain("--bandscope-status-warning: #fcd34d;");
expect(tokenSource).toContain("--bandscope-status-warning-bg: #fcd34d12;");
expect(tokenSource).toContain("--bandscope-status-warning-border: #fcd34d33;");
expect(tokenSource).toContain("--bandscope-status-success: #6ee7b7;");
expect(tokenSource).toContain("--bandscope-status-success-bg: #6ee7b712;");
expect(tokenSource).toContain("--bandscope-status-success-border: #6ee7b733;");

expect(tokenSource).toContain(
"--bandscope-confidence-low-fg: var(--bandscope-status-danger);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-low-bg: var(--bandscope-status-danger-bg);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-low-border: var(--bandscope-status-danger-border);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-medium-fg: var(--bandscope-status-warning);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-medium-bg: var(--bandscope-status-warning-bg);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-medium-border: var(--bandscope-status-warning-border);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-high-fg: var(--bandscope-status-success);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-high-bg: var(--bandscope-status-success-bg);"
);
expect(tokenSource).toContain(
"--bandscope-confidence-high-border: var(--bandscope-status-success-border);"
);
});
});
30 changes: 21 additions & 9 deletions apps/desktop/src/features/workspace/ConfidenceBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,49 @@ import type { ConfidenceLevel } from "@bandscope/shared-types";
import { createTranslator, detectPreferredLocale } from "../../i18n";
import { Badge } from "@/components/ui/badge";

/** Supported visual sizes from the Figma confidence-badge component contract. */
export type ConfidenceBadgeSize = "compact" | "default";

interface ConfidenceBadgeProps {
level: ConfidenceLevel;
size?: ConfidenceBadgeSize;
}

/** Documented. */
export function ConfidenceBadge({ level }: ConfidenceBadgeProps) {
/** Render a localized confidence state using the Figma-backed workspace confidence variants. */
export function ConfidenceBadge({ level, size = "compact" }: ConfidenceBadgeProps) {
const t = createTranslator(detectPreferredLocale());

let label = "";
let colorClass = "";

switch (level) {
case "low":
label = t("confidenceLevelLow");
colorClass = "bg-rose-100 text-rose-700 hover:bg-rose-100 border-rose-200";
colorClass =
"border-[color:var(--bandscope-confidence-low-border)] bg-[var(--bandscope-confidence-low-bg)] text-[color:var(--bandscope-confidence-low-fg)] hover:bg-[var(--bandscope-confidence-low-bg)]";
break;
case "medium":
label = t("confidenceLevelMedium");
colorClass = "bg-amber-100 text-amber-700 hover:bg-amber-100 border-amber-200";
colorClass =
"border-[color:var(--bandscope-confidence-medium-border)] bg-[var(--bandscope-confidence-medium-bg)] text-[color:var(--bandscope-confidence-medium-fg)] hover:bg-[var(--bandscope-confidence-medium-bg)]";
break;
case "high":
label = t("confidenceLevelHigh");
colorClass = "bg-emerald-100 text-emerald-700 hover:bg-emerald-100 border-emerald-200";
colorClass =
"border-[color:var(--bandscope-confidence-high-border)] bg-[var(--bandscope-confidence-high-bg)] text-[color:var(--bandscope-confidence-high-fg)] hover:bg-[var(--bandscope-confidence-high-bg)]";
break;
}

const heightClass =
size === "default"
? "h-[var(--bandscope-confidence-default-height)]"
: "h-[var(--bandscope-confidence-compact-height)]";

return (
<Badge
variant="outline"
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
className={`px-1.5 py-0 h-5 text-[0.65rem] font-bold uppercase tracking-wider ${colorClass}`}
title={`Confidence: ${level}`}
className={`${heightClass} px-1.5 py-0 text-[11px] font-medium leading-[16px] uppercase tracking-normal ${colorClass}`}
title={label}
>
{label}
</Badge>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { OverlapWarningList } from "./OverlapWarningList";

const meta = {
title: "Workspace/OverlapWarningList",
component: OverlapWarningList,
parameters: {
layout: "padded",
docs: {
description: {
component:
"Shared clash list extracted from Section Roadmap and Ranges. Select the dark or light surface contract so the same next-player warning remains readable in both contexts.",
},
},
},
} satisfies Meta<typeof OverlapWarningList>;

export default meta;
type Story = StoryObj<typeof meta>;

export const SharedClashes: Story = {
args: {
warnings: ["Bass and vocal share C3", "Keys cover the guitar hook"],
},
};

export const LightCard: Story = {
args: {
warnings: ["Bass and vocal share C3", "Keys cover the guitar hook"],
surface: "light",
},
render: (args) => (
<div style={{ background: "#fff", padding: "12px", maxWidth: "360px" }}>
<OverlapWarningList {...args} />
</div>
),
};

export const NoClashes: Story = {
args: {
warnings: [],
},
};
42 changes: 42 additions & 0 deletions apps/desktop/src/features/workspace/OverlapWarningList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { OverlapWarningList } from "./OverlapWarningList";

describe("OverlapWarningList", () => {
it("renders nothing when there are no clashes", () => {
const { container } = render(<OverlapWarningList warnings={[]} />);
expect(container).toBeEmptyDOMElement();
});

it("lists each clash so the next player action is visible", () => {
render(<OverlapWarningList warnings={["Bass and vocal share C3", "Keys cover the guitar hook"]} />);
expect(screen.getByRole("list", { name: "Clash warning" })).toBeTruthy();
expect(screen.getByText("Bass and vocal share C3")).toBeTruthy();
expect(screen.getByText("Keys cover the guitar hook")).toBeTruthy();
});

it("uses dedicated readable tokens when rendered on a light card", () => {
render(<OverlapWarningList warnings={["Bass and vocal share C3"]} surface="light" />);

const warning = screen.getByText("Bass and vocal share C3").closest("li");
expect(warning).not.toBeNull();
expect(warning?.className).toContain("border-[color:var(--bandscope-overlap-light-border)]");
expect(warning?.className).toContain("bg-[var(--bandscope-overlap-light-bg)]");
expect(warning?.className).toContain("text-[color:var(--bandscope-overlap-light-fg)]");
});

it("keeps repeated clash evidence distinct without duplicate React keys", () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);

try {
render(<OverlapWarningList warnings={["Bass and vocal share C3", "Bass and vocal share C3"]} />);

expect(screen.getAllByText("Bass and vocal share C3")).toHaveLength(2);
expect(
consoleError.mock.calls.some((call) => call.map(String).join(" ").includes("same key")),
).toBe(false);
} finally {
consoleError.mockRestore();
}
});
});
Loading
Loading