Rename fold/unfold to collapse/expand across panel code

Aligns terminology with standard UI conventions. Renames props,
state, handlers, aria-labels, test descriptions, and the test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-14 12:01:13 +01:00
parent aa806d4fb9
commit 930301de71
4 changed files with 74 additions and 74 deletions

View File

@@ -336,8 +336,8 @@ export function App() {
uploadAndCacheSource={uploadAndCacheSource} uploadAndCacheSource={uploadAndCacheSource}
refreshCache={refreshCache} refreshCache={refreshCache}
panelRole="pinned" panelRole="pinned"
isFolded={false} isCollapsed={false}
onToggleFold={() => {}} onToggleCollapse={() => {}}
onPin={() => {}} onPin={() => {}}
onUnpin={sidePanel.unpin} onUnpin={sidePanel.unpin}
showPinButton={false} showPinButton={false}
@@ -355,8 +355,8 @@ export function App() {
uploadAndCacheSource={uploadAndCacheSource} uploadAndCacheSource={uploadAndCacheSource}
refreshCache={refreshCache} refreshCache={refreshCache}
panelRole="browse" panelRole="browse"
isFolded={sidePanel.isRightPanelFolded} isCollapsed={sidePanel.isRightPanelCollapsed}
onToggleFold={sidePanel.toggleFold} onToggleCollapse={sidePanel.toggleCollapse}
onPin={sidePanel.togglePin} onPin={sidePanel.togglePin}
onUnpin={() => {}} onUnpin={() => {}}
showPinButton={sidePanel.isWideDesktop && !!selectedCreature} showPinButton={sidePanel.isWideDesktop && !!selectedCreature}
@@ -371,7 +371,7 @@ export function App() {
<BulkImportToasts <BulkImportToasts
state={bulkImport.state} state={bulkImport.state}
visible={!sidePanel.bulkImportMode || sidePanel.isRightPanelFolded} visible={!sidePanel.bulkImportMode || sidePanel.isRightPanelCollapsed}
onReset={bulkImport.reset} onReset={bulkImport.reset}
/> />

View File

@@ -45,8 +45,8 @@ interface PanelProps {
creatureId?: CreatureId | null; creatureId?: CreatureId | null;
creature?: Creature | null; creature?: Creature | null;
panelRole?: "browse" | "pinned"; panelRole?: "browse" | "pinned";
isFolded?: boolean; isCollapsed?: boolean;
onToggleFold?: () => void; onToggleCollapse?: () => void;
onPin?: () => void; onPin?: () => void;
onUnpin?: () => void; onUnpin?: () => void;
showPinButton?: boolean; showPinButton?: boolean;
@@ -64,8 +64,8 @@ function renderPanel(overrides: PanelProps = {}) {
uploadAndCacheSource: vi.fn(), uploadAndCacheSource: vi.fn(),
refreshCache: vi.fn(), refreshCache: vi.fn(),
panelRole: "browse" as const, panelRole: "browse" as const,
isFolded: false, isCollapsed: false,
onToggleFold: vi.fn(), onToggleCollapse: vi.fn(),
onPin: vi.fn(), onPin: vi.fn(),
onUnpin: vi.fn(), onUnpin: vi.fn(),
showPinButton: false, showPinButton: false,
@@ -78,18 +78,18 @@ function renderPanel(overrides: PanelProps = {}) {
return props; return props;
} }
describe("Stat Block Panel Fold/Unfold and Pin", () => { describe("Stat Block Panel Collapse/Expand and Pin", () => {
beforeEach(() => { beforeEach(() => {
mockMatchMedia(true); // desktop by default mockMatchMedia(true); // desktop by default
}); });
afterEach(cleanup); afterEach(cleanup);
describe("US1: Fold and Unfold", () => { describe("US1: Collapse and Expand", () => {
it("shows fold button instead of close button on desktop", () => { it("shows collapse button instead of close button on desktop", () => {
renderPanel(); renderPanel();
expect( expect(
screen.getByRole("button", { name: "Fold stat block panel" }), screen.getByRole("button", { name: "Collapse stat block panel" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.queryByRole("button", { name: /close/i }), screen.queryByRole("button", { name: /close/i }),
@@ -101,42 +101,42 @@ describe("Stat Block Panel Fold/Unfold and Pin", () => {
expect(screen.queryByText("Stat Block")).not.toBeInTheDocument(); expect(screen.queryByText("Stat Block")).not.toBeInTheDocument();
}); });
it("renders folded tab with creature name when isFolded is true", () => { it("renders collapsed tab with creature name when isCollapsed is true", () => {
renderPanel({ isFolded: true }); renderPanel({ isCollapsed: true });
expect(screen.getByText("Goblin")).toBeInTheDocument(); expect(screen.getByText("Goblin")).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "Unfold stat block panel" }), screen.getByRole("button", { name: "Expand stat block panel" }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("calls onToggleFold when fold button is clicked", () => { it("calls onToggleCollapse when collapse button is clicked", () => {
const props = renderPanel(); const props = renderPanel();
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "Fold stat block panel" }), screen.getByRole("button", { name: "Collapse stat block panel" }),
); );
expect(props.onToggleFold).toHaveBeenCalledTimes(1); expect(props.onToggleCollapse).toHaveBeenCalledTimes(1);
}); });
it("calls onToggleFold when folded tab is clicked", () => { it("calls onToggleCollapse when collapsed tab is clicked", () => {
const props = renderPanel({ isFolded: true }); const props = renderPanel({ isCollapsed: true });
fireEvent.click( fireEvent.click(
screen.getByRole("button", { name: "Unfold stat block panel" }), screen.getByRole("button", { name: "Expand stat block panel" }),
); );
expect(props.onToggleFold).toHaveBeenCalledTimes(1); expect(props.onToggleCollapse).toHaveBeenCalledTimes(1);
}); });
it("applies translate-x class when folded (right side)", () => { it("applies translate-x class when collapsed (right side)", () => {
renderPanel({ isFolded: true, side: "right" }); renderPanel({ isCollapsed: true, side: "right" });
const panel = screen const panel = screen
.getByRole("button", { name: "Unfold stat block panel" }) .getByRole("button", { name: "Expand stat block panel" })
.closest("div"); .closest("div");
expect(panel?.className).toContain("translate-x-[calc(100%-40px)]"); expect(panel?.className).toContain("translate-x-[calc(100%-40px)]");
}); });
it("applies translate-x-0 when expanded", () => { it("applies translate-x-0 when expanded", () => {
renderPanel({ isFolded: false }); renderPanel({ isCollapsed: false });
const foldBtn = screen.getByRole("button", { const foldBtn = screen.getByRole("button", {
name: "Fold stat block panel", name: "Collapse stat block panel",
}); });
const panel = foldBtn.closest("div.fixed") as HTMLElement; const panel = foldBtn.closest("div.fixed") as HTMLElement;
expect(panel?.className).toContain("translate-x-0"); expect(panel?.className).toContain("translate-x-0");
@@ -148,12 +148,12 @@ describe("Stat Block Panel Fold/Unfold and Pin", () => {
mockMatchMedia(false); // mobile mockMatchMedia(false); // mobile
}); });
it("shows fold button instead of X close button on mobile drawer", () => { it("shows collapse button instead of X close button on mobile drawer", () => {
renderPanel(); renderPanel();
expect( expect(
screen.getByRole("button", { name: "Fold stat block panel" }), screen.getByRole("button", { name: "Collapse stat block panel" }),
).toBeInTheDocument(); ).toBeInTheDocument();
// No X close icon button — only backdrop dismiss and fold toggle // No X close icon button — only backdrop dismiss and collapse toggle
const buttons = screen.getAllByRole("button"); const buttons = screen.getAllByRole("button");
const buttonLabels = buttons.map((b) => b.getAttribute("aria-label")); const buttonLabels = buttons.map((b) => b.getAttribute("aria-label"));
expect(buttonLabels).not.toContain("Close"); expect(buttonLabels).not.toContain("Close");
@@ -175,8 +175,8 @@ describe("Stat Block Panel Fold/Unfold and Pin", () => {
uploadAndCacheSource={vi.fn()} uploadAndCacheSource={vi.fn()}
refreshCache={vi.fn()} refreshCache={vi.fn()}
panelRole="pinned" panelRole="pinned"
isFolded={false} isCollapsed={false}
onToggleFold={vi.fn()} onToggleCollapse={vi.fn()}
onPin={vi.fn()} onPin={vi.fn()}
onUnpin={vi.fn()} onUnpin={vi.fn()}
showPinButton={false} showPinButton={false}
@@ -235,7 +235,7 @@ describe("Stat Block Panel Fold/Unfold and Pin", () => {
it("positions browse panel on the right side", () => { it("positions browse panel on the right side", () => {
renderPanel({ panelRole: "browse", side: "right" }); renderPanel({ panelRole: "browse", side: "right" });
const foldBtn = screen.getByRole("button", { const foldBtn = screen.getByRole("button", {
name: "Fold stat block panel", name: "Collapse stat block panel",
}); });
const panel = foldBtn.closest("div.fixed") as HTMLElement; const panel = foldBtn.closest("div.fixed") as HTMLElement;
expect(panel?.className).toContain("right-0"); expect(panel?.className).toContain("right-0");
@@ -243,16 +243,16 @@ describe("Stat Block Panel Fold/Unfold and Pin", () => {
}); });
}); });
describe("US3: Fold independence with pinned panel", () => { describe("US3: Collapse independence with pinned panel", () => {
it("pinned panel has no fold button", () => { it("pinned panel has no collapse button", () => {
renderPanel({ panelRole: "pinned", side: "left" }); renderPanel({ panelRole: "pinned", side: "left" });
expect( expect(
screen.queryByRole("button", { name: /fold/i }), screen.queryByRole("button", { name: /collapse/i }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("pinned panel is always expanded (no translate offset)", () => { it("pinned panel is always expanded (no translate offset)", () => {
renderPanel({ panelRole: "pinned", side: "left", isFolded: false }); renderPanel({ panelRole: "pinned", side: "left", isCollapsed: false });
const unpinBtn = screen.getByRole("button", { const unpinBtn = screen.getByRole("button", {
name: "Unpin creature", name: "Unpin creature",
}); });

View File

@@ -22,8 +22,8 @@ interface StatBlockPanelProps {
) => Promise<void>; ) => Promise<void>;
refreshCache: () => Promise<void>; refreshCache: () => Promise<void>;
panelRole: "browse" | "pinned"; panelRole: "browse" | "pinned";
isFolded: boolean; isCollapsed: boolean;
onToggleFold: () => void; onToggleCollapse: () => void;
onPin: () => void; onPin: () => void;
onUnpin: () => void; onUnpin: () => void;
showPinButton: boolean; showPinButton: boolean;
@@ -42,23 +42,23 @@ function extractSourceCode(cId: CreatureId): string {
return cId.slice(0, colonIndex).toUpperCase(); return cId.slice(0, colonIndex).toUpperCase();
} }
function FoldedTab({ function CollapsedTab({
creatureName, creatureName,
side, side,
onToggleFold, onToggleCollapse,
}: { }: {
creatureName: string; creatureName: string;
side: "left" | "right"; side: "left" | "right";
onToggleFold: () => void; onToggleCollapse: () => void;
}) { }) {
return ( return (
<button <button
type="button" type="button"
onClick={onToggleFold} onClick={onToggleCollapse}
className={`flex h-full w-[40px] cursor-pointer items-center justify-center text-muted-foreground hover:text-hover-neutral ${ className={`flex h-full w-[40px] cursor-pointer items-center justify-center text-muted-foreground hover:text-hover-neutral ${
side === "right" ? "self-start" : "self-end" side === "right" ? "self-start" : "self-end"
}`} }`}
aria-label="Unfold stat block panel" aria-label="Expand stat block panel"
> >
<span className="writing-vertical-rl text-sm font-medium"> <span className="writing-vertical-rl text-sm font-medium">
{creatureName} {creatureName}
@@ -70,13 +70,13 @@ function FoldedTab({
function PanelHeader({ function PanelHeader({
panelRole, panelRole,
showPinButton, showPinButton,
onToggleFold, onToggleCollapse,
onPin, onPin,
onUnpin, onUnpin,
}: { }: {
panelRole: "browse" | "pinned"; panelRole: "browse" | "pinned";
showPinButton: boolean; showPinButton: boolean;
onToggleFold: () => void; onToggleCollapse: () => void;
onPin: () => void; onPin: () => void;
onUnpin: () => void; onUnpin: () => void;
}) { }) {
@@ -87,9 +87,9 @@ function PanelHeader({
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
onClick={onToggleFold} onClick={onToggleCollapse}
className="text-muted-foreground" className="text-muted-foreground"
aria-label="Fold stat block panel" aria-label="Collapse stat block panel"
> >
<PanelRightClose className="h-4 w-4" /> <PanelRightClose className="h-4 w-4" />
</Button> </Button>
@@ -124,48 +124,48 @@ function PanelHeader({
} }
function DesktopPanel({ function DesktopPanel({
isFolded, isCollapsed,
side, side,
creatureName, creatureName,
panelRole, panelRole,
showPinButton, showPinButton,
onToggleFold, onToggleCollapse,
onPin, onPin,
onUnpin, onUnpin,
children, children,
}: { }: {
isFolded: boolean; isCollapsed: boolean;
side: "left" | "right"; side: "left" | "right";
creatureName: string; creatureName: string;
panelRole: "browse" | "pinned"; panelRole: "browse" | "pinned";
showPinButton: boolean; showPinButton: boolean;
onToggleFold: () => void; onToggleCollapse: () => void;
onPin: () => void; onPin: () => void;
onUnpin: () => void; onUnpin: () => void;
children: ReactNode; children: ReactNode;
}) { }) {
const sideClasses = side === "left" ? "left-0 border-r" : "right-0 border-l"; const sideClasses = side === "left" ? "left-0 border-r" : "right-0 border-l";
const foldedTranslate = const collapsedTranslate =
side === "right" side === "right"
? "translate-x-[calc(100%-40px)]" ? "translate-x-[calc(100%-40px)]"
: "translate-x-[calc(-100%+40px)]"; : "translate-x-[calc(-100%+40px)]";
return ( return (
<div <div
className={`fixed top-0 bottom-0 flex w-[400px] flex-col border-border bg-card transition-slide-panel ${sideClasses} ${isFolded ? foldedTranslate : "translate-x-0"}`} className={`fixed top-0 bottom-0 flex w-[400px] flex-col border-border bg-card transition-slide-panel ${sideClasses} ${isCollapsed ? collapsedTranslate : "translate-x-0"}`}
> >
{isFolded ? ( {isCollapsed ? (
<FoldedTab <CollapsedTab
creatureName={creatureName} creatureName={creatureName}
side={side} side={side}
onToggleFold={onToggleFold} onToggleCollapse={onToggleCollapse}
/> />
) : ( ) : (
<> <>
<PanelHeader <PanelHeader
panelRole={panelRole} panelRole={panelRole}
showPinButton={showPinButton} showPinButton={showPinButton}
onToggleFold={onToggleFold} onToggleCollapse={onToggleCollapse}
onPin={onPin} onPin={onPin}
onUnpin={onUnpin} onUnpin={onUnpin}
/> />
@@ -206,7 +206,7 @@ function MobileDrawer({
size="icon-sm" size="icon-sm"
onClick={onDismiss} onClick={onDismiss}
className="text-muted-foreground" className="text-muted-foreground"
aria-label="Fold stat block panel" aria-label="Collapse stat block panel"
> >
<PanelRightClose className="h-4 w-4" /> <PanelRightClose className="h-4 w-4" />
</Button> </Button>
@@ -227,8 +227,8 @@ export function StatBlockPanel({
uploadAndCacheSource, uploadAndCacheSource,
refreshCache, refreshCache,
panelRole, panelRole,
isFolded, isCollapsed,
onToggleFold, onToggleCollapse,
onPin, onPin,
onUnpin, onUnpin,
showPinButton, showPinButton,
@@ -341,12 +341,12 @@ export function StatBlockPanel({
if (isDesktop) { if (isDesktop) {
return ( return (
<DesktopPanel <DesktopPanel
isFolded={isFolded} isCollapsed={isCollapsed}
side={side} side={side}
creatureName={creatureName} creatureName={creatureName}
panelRole={panelRole} panelRole={panelRole}
showPinButton={showPinButton} showPinButton={showPinButton}
onToggleFold={onToggleFold} onToggleCollapse={onToggleCollapse}
onPin={onPin} onPin={onPin}
onUnpin={onUnpin} onUnpin={onUnpin}
> >

View File

@@ -12,7 +12,7 @@ interface SidePanelState {
selectedCreatureId: CreatureId | null; selectedCreatureId: CreatureId | null;
bulkImportMode: boolean; bulkImportMode: boolean;
sourceManagerMode: boolean; sourceManagerMode: boolean;
isRightPanelFolded: boolean; isRightPanelCollapsed: boolean;
pinnedCreatureId: CreatureId | null; pinnedCreatureId: CreatureId | null;
isWideDesktop: boolean; isWideDesktop: boolean;
} }
@@ -22,14 +22,14 @@ interface SidePanelActions {
showBulkImport: () => void; showBulkImport: () => void;
showSourceManager: () => void; showSourceManager: () => void;
dismissPanel: () => void; dismissPanel: () => void;
toggleFold: () => void; toggleCollapse: () => void;
togglePin: () => void; togglePin: () => void;
unpin: () => void; unpin: () => void;
} }
export function useSidePanelState(): SidePanelState & SidePanelActions { export function useSidePanelState(): SidePanelState & SidePanelActions {
const [panelView, setPanelView] = useState<PanelView>({ mode: "closed" }); const [panelView, setPanelView] = useState<PanelView>({ mode: "closed" });
const [isRightPanelFolded, setIsRightPanelFolded] = useState(false); const [isRightPanelCollapsed, setIsRightPanelCollapsed] = useState(false);
const [pinnedCreatureId, setPinnedCreatureId] = useState<CreatureId | null>( const [pinnedCreatureId, setPinnedCreatureId] = useState<CreatureId | null>(
null, null,
); );
@@ -49,25 +49,25 @@ export function useSidePanelState(): SidePanelState & SidePanelActions {
const showCreature = useCallback((creatureId: CreatureId) => { const showCreature = useCallback((creatureId: CreatureId) => {
setPanelView({ mode: "creature", creatureId }); setPanelView({ mode: "creature", creatureId });
setIsRightPanelFolded(false); setIsRightPanelCollapsed(false);
}, []); }, []);
const showBulkImport = useCallback(() => { const showBulkImport = useCallback(() => {
setPanelView({ mode: "bulk-import" }); setPanelView({ mode: "bulk-import" });
setIsRightPanelFolded(false); setIsRightPanelCollapsed(false);
}, []); }, []);
const showSourceManager = useCallback(() => { const showSourceManager = useCallback(() => {
setPanelView({ mode: "source-manager" }); setPanelView({ mode: "source-manager" });
setIsRightPanelFolded(false); setIsRightPanelCollapsed(false);
}, []); }, []);
const dismissPanel = useCallback(() => { const dismissPanel = useCallback(() => {
setPanelView({ mode: "closed" }); setPanelView({ mode: "closed" });
}, []); }, []);
const toggleFold = useCallback(() => { const toggleCollapse = useCallback(() => {
setIsRightPanelFolded((f) => !f); setIsRightPanelCollapsed((f) => !f);
}, []); }, []);
const togglePin = useCallback(() => { const togglePin = useCallback(() => {
@@ -87,14 +87,14 @@ export function useSidePanelState(): SidePanelState & SidePanelActions {
selectedCreatureId, selectedCreatureId,
bulkImportMode: panelView.mode === "bulk-import", bulkImportMode: panelView.mode === "bulk-import",
sourceManagerMode: panelView.mode === "source-manager", sourceManagerMode: panelView.mode === "source-manager",
isRightPanelFolded, isRightPanelCollapsed,
pinnedCreatureId, pinnedCreatureId,
isWideDesktop, isWideDesktop,
showCreature, showCreature,
showBulkImport, showBulkImport,
showSourceManager, showSourceManager,
dismissPanel, dismissPanel,
toggleFold, toggleCollapse,
togglePin, togglePin,
unpin, unpin,
}; };