diff --git a/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx b/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx
index 575684c50..e56c4fff7 100644
--- a/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx
+++ b/apps/desktop/src/features/workspace/RoleSwitcher.test.tsx
@@ -3,23 +3,22 @@ import { describe, expect, it, vi } from "vitest";
import { RoleSwitcher, tabValueToRoleId } from "./RoleSwitcher";
vi.mock("../../i18n", () => ({
- createTranslator: () => (key: string) =>
+ createTranslator: () => (translationKey: string) =>
({
allRoles: "All Roles",
roleSwitcherTitle: "Role-specific View"
- })[key] ?? key,
+ })[translationKey] ?? translationKey,
detectPreferredLocale: () => "en"
}));
describe("RoleSwitcher", () => {
-
it("renders the title and role options", () => {
- const roles = [
- { id: "bass-guitar", name: "Bass Guitar" },
- { id: "lead-vocal", name: "Lead Vocal" }
+ const roleOptions = [
+ { roleId: "bass-guitar", roleName: "Bass Guitar" },
+ { roleId: "lead-vocal", roleName: "Lead Vocal" }
];
- render();
+ render();
expect(screen.getByText("Role-specific View")).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "All Roles" })).toBeInTheDocument();
@@ -28,48 +27,63 @@ describe("RoleSwitcher", () => {
});
it("keeps the all-roles control distinct from a real role whose id is all", () => {
- const onRoleChange = vi.fn();
+ const roleChangeHandler = vi.fn();
render(
);
fireEvent.click(screen.getByRole("tab", { name: "Alloy Synth" }));
- expect(onRoleChange).toHaveBeenLastCalledWith("all");
+ expect(roleChangeHandler).toHaveBeenLastCalledWith("all");
fireEvent.click(screen.getByRole("tab", { name: "All Roles" }));
- expect(onRoleChange).toHaveBeenLastCalledWith(null);
+ expect(roleChangeHandler).toHaveBeenLastCalledWith(null);
});
it("uses the project-standard active tab data selector", () => {
render(
);
- const allRolesTrigger = screen.getByRole("tab", { name: "All Roles" });
- expect(allRolesTrigger.className).toContain("data-active:bg-cyan-300");
- expect(allRolesTrigger.className).not.toContain("data-[state=active]:");
+ const allRolesTab = screen.getByRole("tab", { name: "All Roles" });
+ expect(allRolesTab.className).toContain("data-active:bg-cyan-300");
+ expect(allRolesTab.className).not.toContain("data-[state=active]:");
});
it("ignores tab values that are not in the rendered role allowlist", () => {
- const roles = [
- { id: "bass-guitar", name: "Bass Guitar" },
- { id: "lead-vocal", name: "Lead Vocal" }
+ const roleOptions = [
+ { roleId: "bass-guitar", roleName: "Bass Guitar" },
+ { roleId: "lead-vocal", roleName: "Lead Vocal" }
];
- expect(tabValueToRoleId("role:bass-guitar", roles)).toBe("bass-guitar");
- expect(tabValueToRoleId("role:unknown-role", roles)).toBeNull();
- expect(tabValueToRoleId("raw-unknown-role", roles)).toBeNull();
+ expect(tabValueToRoleId("role:bass-guitar", roleOptions)).toBe("bass-guitar");
+ expect(tabValueToRoleId("role:unknown-role", roleOptions)).toBeNull();
+ expect(tabValueToRoleId("raw-unknown-role", roleOptions)).toBeNull();
+ });
+
+ it("keeps the previous role projection behind the compatibility boundary", () => {
+ const roleChangeHandler = vi.fn();
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole("tab", { name: "Legacy Bass" }));
+ expect(roleChangeHandler).toHaveBeenLastCalledWith("legacy-bass");
});
});
diff --git a/apps/desktop/src/features/workspace/RoleSwitcher.tsx b/apps/desktop/src/features/workspace/RoleSwitcher.tsx
index f5275964d..e35f57c27 100644
--- a/apps/desktop/src/features/workspace/RoleSwitcher.tsx
+++ b/apps/desktop/src/features/workspace/RoleSwitcher.tsx
@@ -2,53 +2,95 @@ import { createTranslator, detectPreferredLocale } from "../../i18n";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Users } from "lucide-react";
-/** Renderable role option accepted by the role tab allowlist. */
+/** A selectable rehearsal-role projection owned by the workspace role switcher. */
export interface RehearsalRoleOption {
+ /** Stable role identity used by selection and tab-value mapping. */
+ roleId: string;
+ /** Human-readable role name rendered in the role switcher. */
+ roleName: string;
+}
+
+/** Compatibility-only projection for the pre-naming-contract component API. */
+interface LegacyRehearsalRoleOption {
id: string;
name: string;
}
-interface RoleSwitcherProps {
- roles: RehearsalRoleOption[];
+interface RoleSwitcherSharedProps {
activeRole: string | null;
onRoleChange: (roleId: string | null) => void;
}
+type RoleSwitcherProps = RoleSwitcherSharedProps &
+ (
+ | {
+ roleOptions: RehearsalRoleOption[];
+ roles?: never;
+ }
+ | {
+ roleOptions?: never;
+ /** @deprecated Use roleOptions with roleId/roleName. */
+ roles: LegacyRehearsalRoleOption[];
+ }
+ );
+
const ALL_ROLES_VALUE = "__bandscope_all_roles__";
const ROLE_VALUE_PREFIX = "role:";
+/** Translate the legacy component projection at one compatibility boundary. */
+function normalizeLegacyRoleOptions(
+ legacyRoleOptions: LegacyRehearsalRoleOption[]
+): RehearsalRoleOption[] {
+ return legacyRoleOptions.map((legacyRoleOption) => ({
+ roleId: legacyRoleOption.id,
+ roleName: legacyRoleOption.name
+ }));
+}
+
/** Documented. */
function roleTabValue(roleId: string): string {
return `${ROLE_VALUE_PREFIX}${roleId}`;
}
/** Documented. */
-export function tabValueToRoleId(value: string, roles: RehearsalRoleOption[]): string | null {
- if (value === ALL_ROLES_VALUE) {
+export function tabValueToRoleId(
+ tabValue: string,
+ roleOptions: RehearsalRoleOption[]
+): string | null {
+ if (tabValue === ALL_ROLES_VALUE) {
return null;
}
- if (!value.startsWith(ROLE_VALUE_PREFIX)) {
+ if (!tabValue.startsWith(ROLE_VALUE_PREFIX)) {
return null;
}
- const roleId = value.slice(ROLE_VALUE_PREFIX.length);
- return roles.some((role) => role.id === roleId) ? roleId : null;
+ const roleId = tabValue.slice(ROLE_VALUE_PREFIX.length);
+ return roleOptions.some((roleOption) => roleOption.roleId === roleId) ? roleId : null;
}
/** Documented. */
-export function RoleSwitcher({ roles, activeRole, onRoleChange }: RoleSwitcherProps) {
- const t = createTranslator(detectPreferredLocale());
+export function RoleSwitcher({
+ roleOptions,
+ roles: legacyRoleOptions,
+ activeRole,
+ onRoleChange
+}: RoleSwitcherProps) {
+ const resolvedRoleOptions =
+ roleOptions ?? normalizeLegacyRoleOptions(legacyRoleOptions ?? []);
+ const translatedText = createTranslator(detectPreferredLocale());
return (
- {t("roleSwitcherTitle")}
+ {translatedText("roleSwitcherTitle")}
onRoleChange(tabValueToRoleId(val, roles))}
+ onValueChange={(tabValue) =>
+ onRoleChange(tabValueToRoleId(tabValue, resolvedRoleOptions))
+ }
className="w-full sm:w-auto"
>
@@ -56,15 +98,15 @@ export function RoleSwitcher({ roles, activeRole, onRoleChange }: RoleSwitcherPr
value={ALL_ROLES_VALUE}
className="rounded-md px-4 text-slate-300 data-active:bg-cyan-300 data-active:text-slate-950 data-active:shadow-[0_8px_24px_rgba(34,211,238,0.24)]"
>
- {t("allRoles")}
+ {translatedText("allRoles")}
- {roles.map((role) => (
+ {resolvedRoleOptions.map((roleOption) => (
- {role.name}
+ {roleOption.roleName}
))}