From 6cabd1b3bebab97a5ffa21448b678c8d483e21de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 11:11:24 +0000 Subject: [PATCH 1/3] feat(workspace): start tonight's first part from the ready board After analysis, Roles & Harmony and empty collaboration cards now name the first extracted part and open that role board so chords, range, and simplification are one action away. --- AGENTS.md | 1 + ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + CLAUDE.md | 2 + .../src/features/workspace/Workspace.test.tsx | 44 ++++++++++ .../src/features/workspace/Workspace.tsx | 81 ++++++++++++++++++- apps/desktop/src/locales/en/common.json | 7 +- apps/desktop/src/locales/ko/common.json | 7 +- docs/design-system/component-contract.md | 2 +- 9 files changed, 139 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fca448ce9..c2734f631 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. +- After analysis, the ready workspace must name the first part to start. Do not leave Roles & Harmony or an empty collaboration card as a description that never opens tonight's chords, range, or simplification. - Do not reduce the product to a chord analyzer when form, timing, player coordination, 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. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3302a6fc3..cbe6242a2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -68,6 +68,7 @@ Last updated: 2026-03-11 - BandScope is not only a shell around chord labels, stems, and ranges. - The technical scope includes rehearsal-facing outputs for harmony, section roadmap, groove cues, role entry and dropout cues, simplification guidance, transposition or setup guidance, confidence flags, and rehearsal priority. - These outputs must stay aligned with `docs/brand-story.md` rather than drifting back to a song-summary-only analyzer. +- After analysis, the ready workspace must start the first extracted part so tonight's chords, range, and simplification are one action away. Empty collaboration copy must start that same part instead of waiting for assignments. ## Analysis target model diff --git a/CHANGELOG.md b/CHANGELOG.md index eea696893..191e47f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Ready workspace now names the first extracted part and starts that role board so tonight's chords, range, and simplification are one action away. - Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace. - 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함. diff --git a/CLAUDE.md b/CLAUDE.md index 82c2c704a..41a855adc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co `AGENTS.md` is the canonical agent operating guide — read and follow it before making changes. It defines the security workflow (`Security Notes`), supply-chain workflow, cross-platform build rules, GitHub bootstrap rules, code style, and safety guardrails. This file complements it with commands and architecture; when in doubt, `AGENTS.md` and the docs it references win. +After analysis, Roles & Harmony and empty collaboration cards must name the first part and start that role board. Do not leave those ready-state surfaces as dead-end descriptions. + Agent execution and delegation rules live in `docs/agents/README.md`. PR canonicalization rules live in `docs/workflow/pr-continuity.md`. ## Common commands diff --git a/apps/desktop/src/features/workspace/Workspace.test.tsx b/apps/desktop/src/features/workspace/Workspace.test.tsx index a3da5ffe6..b475bffce 100644 --- a/apps/desktop/src/features/workspace/Workspace.test.tsx +++ b/apps/desktop/src/features/workspace/Workspace.test.tsx @@ -269,5 +269,49 @@ describe("Workspace", () => { expect(screen.getByText("스템")).toBeTruthy(); expect(screen.getByText("합주 우선순위")).toBeTruthy(); expect(screen.getByText("역할과 화성")).toBeTruthy(); + expect(screen.getByRole("button", { name: "Bass Guitar로 시작" })).toBeTruthy(); + }); + + it("names the first part and opens that role board from the ready workspace", () => { + setNavigatorLanguage("en-US"); + const song = createDemoRehearsalSong(); + + render(); + + expect(screen.getByTestId("workspace-pick-part-callout")).toBeTruthy(); + expect(screen.getByText(/Start as Bass Guitar to see tonight's chords/i)).toBeTruthy(); + expect(screen.queryByText(/The bass holds the vi center/i)).toBeNull(); + + fireEvent.click(screen.getByRole("button", { name: "Start as Bass Guitar" })); + + expect(screen.queryByTestId("workspace-pick-part-callout")).toBeNull(); + expect(screen.getByText(/The bass holds the vi center/i)).toBeTruthy(); + expect(screen.getByRole("tab", { name: "Bass Guitar", selected: true })).toBeTruthy(); + }); + + it("starts the first part from an empty collaboration card", () => { + setNavigatorLanguage("en-US"); + const song = createDemoRehearsalSong(); + song.collaboration = undefined; + + render(); + + expect(screen.getByText(/No assignments yet. Start as Bass Guitar/i)).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: "Start the room as Bass Guitar" })); + + expect(screen.getByText(/The bass holds the vi center/i)).toBeTruthy(); + }); + + it("hides the pick-part next action when the song has no roles", () => { + setNavigatorLanguage("en-US"); + const song = createDemoRehearsalSong(); + song.sections = []; + song.collaboration = undefined; + + render(); + + expect(screen.queryByTestId("workspace-pick-part-callout")).toBeNull(); + expect(screen.queryByRole("button", { name: /Start as/i })).toBeNull(); + expect(screen.getByText(/No assignments yet. Start as your part/i)).toBeTruthy(); }); }); diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx index 71546b524..5add8baed 100644 --- a/apps/desktop/src/features/workspace/Workspace.tsx +++ b/apps/desktop/src/features/workspace/Workspace.tsx @@ -8,7 +8,7 @@ import { createTranslator, detectPreferredLocale } from "../../i18n"; import { generateCueSheetCsv, generateChartSummaryJson, generateMetadataHandoffJson, sanitizeFilename } from "../../lib/export"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardDescription } from "@/components/ui/card"; -import { Download, CheckCheck, ClipboardList, MessageSquareMore, CloudOff, Music4 } from "lucide-react"; +import { Download, CheckCheck, ClipboardList, MessageSquareMore, CloudOff, Music4, Users } from "lucide-react"; interface WorkspaceProps { song: RehearsalSong; @@ -70,6 +70,25 @@ function safeProjectBootstrapSummary(value: ProjectBootstrapSummary | null): Pro } } +/** Substitute the player-facing role name into a copy template. */ +function fillRoleCopy(template: string, roleName: string): string { + return template.replaceAll("{role}", roleName); +} + +/** Scroll the roles board into view after the player starts a part. */ +function focusWorkspaceRolesCard(): void { + const node = document.getElementById("workspace-roles-card"); + if (!(node instanceof HTMLElement)) { + return; + } + if (typeof node.scrollIntoView === "function") { + node.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + if (typeof node.focus === "function") { + node.focus(); + } +} + /** Documented. */ const SongStructure = memo(function SongStructure({ sections, t }: { sections: RehearsalSong["sections"]; t: Translator }) { return ( @@ -150,6 +169,17 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp return roleMap.get(activeRole); }, [activeRole, roleMap]); const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false; + const firstRole = allRoles[0]; + const firstRoleName = firstRole?.name.trim() || t("workspacePickPartFallback"); + + /** Start tonight's part view on the first extracted role. */ + const handlePickFirstPart = () => { + if (!firstRole) { + return; + } + setActiveRole(firstRole.id); + focusWorkspaceRolesCard(); + }; /** Handle the practice progress change internally by immutably updating the song state. */ const handlePracticeProgressChange = (newProgress: number) => { @@ -314,7 +344,24 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp ) : ( -

{t("workspaceCollaborationEmpty")}

+
+

+ {fillRoleCopy(t("workspaceCollaborationEmpty"), firstRoleName)} +

+ {firstRole ? ( + + ) : null} +
)} @@ -333,11 +380,15 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp -
+

{t("workspaceRolesHarmonyLabel")}

-

Filter the board by player or vocal role without losing the full form context.

+

{t("workspaceRolesHarmonyHint")}

+ {!activeRole && firstRole ? ( +
+

+ {fillRoleCopy(t("workspacePickPartHint"), firstRoleName)} +

+ +
+ ) : null} + {activeRole && (

Stem Player

diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json index 39f716d50..ea127d29f 100644 --- a/apps/desktop/src/locales/en/common.json +++ b/apps/desktop/src/locales/en/common.json @@ -39,7 +39,8 @@ "workspaceRehearsalTimelineLabel": "Rehearsal timeline", "workspaceSongTimelineLabel": "Song Timeline", "workspaceCollaborationLabel": "Collaboration", - "workspaceCollaborationEmpty": "Assignments, comments, and approvals will show up here as the room aligns.", + "workspaceCollaborationEmpty": "No assignments yet. Start as {role} so the room can see who locks this part tonight.", + "workspaceCollaborationPickPart": "Start the room as {role}", "workspaceSyncStatusLabel": "Sync", "workspaceAssignmentsLabel": "Assignments", "workspaceCommentsLabel": "Comments", @@ -50,6 +51,10 @@ "workspaceStemsLabel": "Stems", "workspaceRehearsalPrioritiesLabel": "Rehearsal Priorities", "workspaceRolesHarmonyLabel": "Roles & Harmony", + "workspaceRolesHarmonyHint": "Pick your part to open tonight's chords, range, and simplification.", + "workspacePickPartHint": "Start as {role} to see tonight's chords, range, and what to simplify.", + "workspacePickPartAction": "Start as {role}", + "workspacePickPartFallback": "your part", "sectionRoadmapTitle": "Section Roadmap", "sectionRoadmapScrollHint": "Scroll for more sections →", "sectionGrooveLabel": "Groove", diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json index 371884abb..3c7a23f68 100644 --- a/apps/desktop/src/locales/ko/common.json +++ b/apps/desktop/src/locales/ko/common.json @@ -39,7 +39,8 @@ "workspaceRehearsalTimelineLabel": "합주 타임라인", "workspaceSongTimelineLabel": "곡 타임라인", "workspaceCollaborationLabel": "협업", - "workspaceCollaborationEmpty": "담당, 코멘트, 승인 내역이 정리되면 이곳에 표시됩니다.", + "workspaceCollaborationEmpty": "아직 담당이 없습니다. {role}로 시작하면 오늘 이 파트를 누가 잠글지 합주실에 보입니다.", + "workspaceCollaborationPickPart": "합주실을 {role}로 시작", "workspaceSyncStatusLabel": "동기화", "workspaceAssignmentsLabel": "담당", "workspaceCommentsLabel": "코멘트", @@ -50,6 +51,10 @@ "workspaceStemsLabel": "스템", "workspaceRehearsalPrioritiesLabel": "합주 우선순위", "workspaceRolesHarmonyLabel": "역할과 화성", + "workspaceRolesHarmonyHint": "파트를 고르면 오늘 칠 화성, 음역, 단순화 힌트가 열립니다.", + "workspacePickPartHint": "오늘 코드, 음역, 단순화할 자리를 보려면 {role}로 시작하세요.", + "workspacePickPartAction": "{role}로 시작", + "workspacePickPartFallback": "내 파트", "sectionRoadmapTitle": "구간 흐름", "sectionRoadmapScrollHint": "더 많은 구간은 옆으로 스크롤하세요 →", "sectionGrooveLabel": "그루브", diff --git a/docs/design-system/component-contract.md b/docs/design-system/component-contract.md index 22602c313..205e4bed6 100644 --- a/docs/design-system/component-contract.md +++ b/docs/design-system/component-contract.md @@ -28,7 +28,7 @@ The authoritative Figma view is `31 Component Contract Catalog`. This file mirro | Metric Card | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-216 | `apps/desktop/src/App.tsx` | Feature-local `MetricCard({ icon, label, value, detail, accent? })`. Metrics follow source controls on mobile. | | Confidence Badge | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-239 | `apps/desktop/src/features/workspace/ConfidenceBadge.tsx` | Use `level: ConfidenceLevel`; no `score` or `label` prop exists in current code. | | Status Pill | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-283 | `apps/desktop/src/features/workspace/Workspace.tsx` | Design pattern only. Current code uses `formatStatusLabel(status)` inside local badge-like markup. | -| Role Switcher | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-337 | `apps/desktop/src/features/workspace/RoleSwitcher.tsx` | Use `roles`, `activeRole`, and `onRoleChange`; `null` means all roles. | +| Role Switcher | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-337 | `apps/desktop/src/features/workspace/RoleSwitcher.tsx` | Use `roles`, `activeRole`, and `onRoleChange`; `null` means all roles. When no role is selected, Workspace must name the first part and start that board. | | Section Roadmap Card | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-402 | `apps/desktop/src/features/workspace/SectionRoadmap.tsx` | Use `song`, `activeRole`, and optional `onSongUpdate`; avoid rebuilding its internal card layout. | | Song Structure Timeline | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-457 | `apps/desktop/src/features/workspace/Workspace.tsx` | Feature-local `SongStructure({ sections, t })` memo component; not exported. | | Groove Map | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-526 | `apps/desktop/src/features/workspace/GrooveMap.tsx` | Use `notes?: TranscriptionNote[]` and `isLoading?: boolean`; preserve scrollable region semantics and note labels. | From c79643e067fa47c8b9386826eac40951756e7c53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:00:03 +0900 Subject: [PATCH 2/3] test(workspace): require reduced-motion part navigation --- ...orkspace.pick-part-reduced-motion.test.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 apps/desktop/src/features/workspace/Workspace.pick-part-reduced-motion.test.tsx diff --git a/apps/desktop/src/features/workspace/Workspace.pick-part-reduced-motion.test.tsx b/apps/desktop/src/features/workspace/Workspace.pick-part-reduced-motion.test.tsx new file mode 100644 index 000000000..30586d591 --- /dev/null +++ b/apps/desktop/src/features/workspace/Workspace.pick-part-reduced-motion.test.tsx @@ -0,0 +1,60 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { createDemoRehearsalSong } from "@bandscope/shared-types"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Workspace } from "./Workspace"; + +const originalLanguage = navigator.language; +const originalMatchMedia = window.matchMedia; +const originalScrollIntoView = HTMLElement.prototype.scrollIntoView; + +function setNavigatorLanguage(language: string): void { + Object.defineProperty(navigator, "language", { + configurable: true, + value: language + }); +} + +describe("Workspace pick-part reduced-motion navigation", () => { + afterEach(() => { + setNavigatorLanguage(originalLanguage); + Object.defineProperty(window, "matchMedia", { + configurable: true, + value: originalMatchMedia + }); + Object.defineProperty(HTMLElement.prototype, "scrollIntoView", { + configurable: true, + value: originalScrollIntoView + }); + vi.restoreAllMocks(); + }); + + it("uses non-animated scrolling when reduced motion is requested", () => { + setNavigatorLanguage("en-US"); + const scrollIntoView = vi.fn(); + Object.defineProperty(HTMLElement.prototype, "scrollIntoView", { + configurable: true, + value: scrollIntoView + }); + Object.defineProperty(window, "matchMedia", { + configurable: true, + value: vi.fn((query: string) => ({ + matches: query === "(prefers-reduced-motion: reduce)", + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn() + })) + }); + + render(); + fireEvent.click(screen.getByRole("button", { name: "Start as Bass Guitar" })); + + expect(scrollIntoView).toHaveBeenCalledWith({ + behavior: "auto", + block: "nearest" + }); + }); +}); From 940ff24aaeac3261f922a8f33d47dd8abb1abb93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:04:12 +0900 Subject: [PATCH 3/3] fix(workspace): respect reduced motion when starting a part --- apps/desktop/src/features/workspace/Workspace.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx index 5add8baed..245638a13 100644 --- a/apps/desktop/src/features/workspace/Workspace.tsx +++ b/apps/desktop/src/features/workspace/Workspace.tsx @@ -81,8 +81,11 @@ function focusWorkspaceRolesCard(): void { if (!(node instanceof HTMLElement)) { return; } + const reduceMotion = + typeof window.matchMedia === "function" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (typeof node.scrollIntoView === "function") { - node.scrollIntoView({ behavior: "smooth", block: "nearest" }); + node.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "nearest" }); } if (typeof node.focus === "function") { node.focus(); @@ -563,4 +566,4 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
); -} +} \ No newline at end of file