diff --git a/CHANGELOG.md b/CHANGELOG.md index a37fde6..4c68e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ easy to spot when bumping. ## [Unreleased] +## [14.0.0] - 2026-10-26 + +### Added + +- `GenreSearch` / `GenreTreeView`: optional `onQueryChange` / `onSearchQueryChange` and `extraResults` / `searchExtraResults` let the host add a second result group (e.g. artists) to the search dropdown, shown under "Genres" and the group's label. Unset, the dropdown is unchanged. +- `GenreTreeView` (wheel, pop-core): genre-tree-view's "Previous" button now sits inside the search field, left of the genre name, and fades in sliding from the left; the field's left padding widens to make room. +- `GenreActionBand`: Google Maps-style band under the detail panel header (icon over label) holding Play/Pause, Focus, Add, Rename, Parent and the `additionalActions`; genre-tree-view's own toolbar and its ⋮ menu are hidden. + +### Breaking + +- `GenreSearch`: `onZoomIn`, `onPlayPause` and `isPlaying` are removed; those actions moved to the detail panel's action band. The search icon now sits right of the field's text, left of the clear button. + ## [13.5.0] - 2026-10-26 ### Added diff --git a/packages/app-kit-ui/package.json b/packages/app-kit-ui/package.json index 1db2f50..95834fd 100644 --- a/packages/app-kit-ui/package.json +++ b/packages/app-kit-ui/package.json @@ -1,6 +1,6 @@ { "name": "@behindthemusictree/app-kit-ui", - "version": "13.5.0", + "version": "14.0.0", "description": "Styled BehindTheMusicTree React components (popups, auth callback, genre tree) built on the headless @behindthemusictree/app-kit", "repository": { "type": "git", @@ -46,7 +46,7 @@ "test": "vitest run --coverage" }, "peerDependencies": { - "@behindthemusictree/app-kit": "^13.5.0", + "@behindthemusictree/app-kit": "^14.0.0", "@behindthemusictree/ui": "^0.2.0", "@tanstack/react-query": ">=5.0.0", "next": ">=14.0.0", diff --git a/packages/app-kit-ui/src/genre-tree/GenreActionBand.test.tsx b/packages/app-kit-ui/src/genre-tree/GenreActionBand.test.tsx new file mode 100644 index 0000000..28ed2ae --- /dev/null +++ b/packages/app-kit-ui/src/genre-tree/GenreActionBand.test.tsx @@ -0,0 +1,51 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import type { GenreTreeAction, GenreTreeNode } from "@behindthemusictree/genre-tree-view"; +import GenreActionBand from "./GenreActionBand"; + +const node = { id: "gp1" } as GenreTreeNode; + +describe("GenreActionBand", () => { + it("renders nothing when no action applies", () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it("renders each provided action and wires its click", () => { + const handlers = { + onPlayPause: vi.fn(), + onZoomIn: vi.fn(), + onAddChild: vi.fn(), + onRename: vi.fn(), + onChangeParent: vi.fn(), + }; + render(); + + for (const name of ["Play", "Focus", "Add", "Rename", "Parent"]) { + fireEvent.click(screen.getByRole("button", { name })); + } + + for (const handler of Object.values(handlers)) expect(handler).toHaveBeenCalledOnce(); + }); + + it("labels the play action Pause while playing", () => { + render(); + + expect(screen.getByRole("button", { name: "Pause" })).toBeInTheDocument(); + }); + + it("renders additional actions and disables those that are not enabled", () => { + const onClick = vi.fn(); + const actions: GenreTreeAction[] = [ + { key: "a", label: () => "Merge", icon: () => , onClick }, + { key: "b", label: () => "Locked", icon: () => , onClick, enabled: () => false }, + ]; + render( actions} />); + + fireEvent.click(screen.getByRole("button", { name: "Merge" })); + + expect(onClick).toHaveBeenCalledWith(expect.anything(), node); + expect(screen.getByRole("button", { name: "Locked" })).toBeDisabled(); + }); +}); diff --git a/packages/app-kit-ui/src/genre-tree/GenreActionBand.tsx b/packages/app-kit-ui/src/genre-tree/GenreActionBand.tsx new file mode 100644 index 0000000..2572586 --- /dev/null +++ b/packages/app-kit-ui/src/genre-tree/GenreActionBand.tsx @@ -0,0 +1,103 @@ +"use client"; + +import type { ReactNode } from "react"; +import { MoveUpRight, Pause, Pencil, Play, Plus, ZoomIn } from "lucide-react"; +import type { + GenreTreeAction, + GenreTreeNode, +} from "@behindthemusictree/genre-tree-view"; + +export type GenreActionBandProps = { + node: GenreTreeNode; + /** When set, shows the Play/Pause action. */ + onPlayPause?: () => void; + isPlaying?: boolean; + /** When set, shows the "Zoom in" action. */ + onZoomIn?: () => void; + /** When set, shows the "Add" (sub-genre) action. */ + onAddChild?: () => void; + /** When set, shows the "Rename" action. */ + onRename?: () => void; + /** When set, shows the "Parent" (change parent) action. */ + onChangeParent?: () => void; + additionalActions?: (node: GenreTreeNode) => GenreTreeAction[]; +}; + +type BandItem = { + key: string; + label: string; + icon: ReactNode; + onClick: (event: React.MouseEvent) => void; + disabled?: boolean; +}; + +export default function GenreActionBand({ + node, + onPlayPause, + isPlaying = false, + onZoomIn, + onAddChild, + onRename, + onChangeParent, + additionalActions, +}: GenreActionBandProps) { + const items: BandItem[] = []; + if (onPlayPause) { + items.push({ + key: "play-pause", + label: isPlaying ? "Pause" : "Play", + icon: isPlaying ? ( +