From 98a112c8b8d4f2b18b0ce04aff76c9657b7d33b8 Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 6 Aug 2026 15:33:57 -0300 Subject: [PATCH 1/9] feat(person-menu): add PersonMenu surface for speaker/role selection Row buttons get an explicit aria-label since AvatarPill's avatar-initials span would otherwise leak into the computed accessible name. --- .../person-menu/PersonMenu.test.tsx | 109 ++++++++++++++ src/components/person-menu/PersonMenu.tsx | 138 ++++++++++++++++++ src/components/person-menu/index.ts | 5 + src/index.ts | 1 + 4 files changed, 253 insertions(+) create mode 100644 src/components/person-menu/PersonMenu.test.tsx create mode 100644 src/components/person-menu/PersonMenu.tsx create mode 100644 src/components/person-menu/index.ts diff --git a/src/components/person-menu/PersonMenu.test.tsx b/src/components/person-menu/PersonMenu.test.tsx new file mode 100644 index 0000000..ebaa3fd --- /dev/null +++ b/src/components/person-menu/PersonMenu.test.tsx @@ -0,0 +1,109 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { PersonMenu, type PersonMenuOption } from "./PersonMenu"; + +const OPTIONS: PersonMenuOption[] = [ + { id: "r1", initials: "DE", name: "Defensor/a", color: "green-light" }, + { id: "r2", initials: "FI", name: "Fiscal", color: "yellow" }, + { id: "r3", initials: "JU", name: "Juez/a", color: "violet" }, +]; + +describe("PersonMenu", () => { + it("renders one button per option, named after the person", () => { + render(); + expect( + screen.getByRole("button", { name: "Defensor/a" }), + ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Fiscal" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Juez/a" })).toBeInTheDocument(); + }); + + it("reports the index of the clicked option", () => { + const onSelectOption = vi.fn(); + render(); + screen.getByRole("button", { name: "Fiscal" }).click(); + expect(onSelectOption).toHaveBeenCalledWith(1); + }); + + it("marks the selected option with aria-current", () => { + render( + , + ); + expect(screen.getByRole("button", { name: "Juez/a" })).toHaveAttribute( + "aria-current", + "true", + ); + expect(screen.getByRole("button", { name: "Fiscal" })).not.toHaveAttribute( + "aria-current", + ); + }); + + it("renders the footer action and reports clicks on it", () => { + const onFooterAction = vi.fn(); + render( + , + ); + screen.getByRole("button", { name: "Nueva persona" }).click(); + expect(onFooterAction).toHaveBeenCalledTimes(1); + }); + + it("lets footerSlot replace the footer button", () => { + render( + } + />, + ); + expect(screen.getByLabelText("Nombre de la persona")).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Nueva persona" }), + ).not.toBeInTheDocument(); + }); + + it("renders only the footer when there are no options left", () => { + render( + , + ); + expect( + screen.getByRole("button", { name: "Nueva persona" }), + ).toBeInTheDocument(); + expect(screen.getAllByRole("button")).toHaveLength(1); + }); + + it("renders nothing when there is neither an option nor a footer", () => { + const { container } = render( + , + ); + expect(container).toBeEmptyDOMElement(); + }); + + it("labels the group so assistive tech can announce it", () => { + render( + , + ); + expect( + screen.getByRole("group", { name: "Elegir persona o rol" }), + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/person-menu/PersonMenu.tsx b/src/components/person-menu/PersonMenu.tsx new file mode 100644 index 0000000..a22e122 --- /dev/null +++ b/src/components/person-menu/PersonMenu.tsx @@ -0,0 +1,138 @@ +import { PlusIcon } from "@phosphor-icons/react"; +import type { ReactNode } from "react"; +import { css, cx } from "@/styled/css"; +import type { AvatarColor } from "../avatar"; +import { AvatarPill } from "../avatar-pill"; +import { Button } from "../button"; + +/** + * PersonMenu — lista flotante de personas/roles para elegir, con una acción + * al pie. AymurAI UI Library nodo 40002701:44844 ("single select"). + * + * Es sólo la superficie: no monta un Popover ni maneja apertura o anclaje. + * El consumidor la posiciona — SidePanel la ancla con Popover al botón + * "Nuevo"; el SpeakerPicker de desktop-app la mete en su barra flotante. + * + * Cada fila va envuelta en un + ) : null); + + if (options.length === 0 && !footer) return null; + + return ( +
+ {options.map((option, index) => ( + + ))} + {footer} +
+ ); +} + +export default PersonMenu; diff --git a/src/components/person-menu/index.ts b/src/components/person-menu/index.ts new file mode 100644 index 0000000..206165b --- /dev/null +++ b/src/components/person-menu/index.ts @@ -0,0 +1,5 @@ +export { + PersonMenu, + type PersonMenuOption, + type PersonMenuProps, +} from "./PersonMenu"; diff --git a/src/index.ts b/src/index.ts index aaadcc0..069a383 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export * from "./components/logo"; // Voz a texto (speech-to-text) export * from "./components/option"; export * from "./components/page-title"; +export * from "./components/person-menu"; export * from "./components/player"; export * from "./components/popover"; export * from "./components/radio"; From d899c7fe58e508063777b26ed2a42d451388a2a7 Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 6 Aug 2026 15:35:20 -0300 Subject: [PATCH 2/9] docs(person-menu): add stories mirroring Figma 40002701:44844 --- .../person-menu/PersonMenu.stories.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/components/person-menu/PersonMenu.stories.tsx diff --git a/src/components/person-menu/PersonMenu.stories.tsx b/src/components/person-menu/PersonMenu.stories.tsx new file mode 100644 index 0000000..cd6ddb9 --- /dev/null +++ b/src/components/person-menu/PersonMenu.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; +import { PersonMenu, type PersonMenuOption } from "./PersonMenu"; + +const ROLES: PersonMenuOption[] = [ + { id: "r1", initials: "DE", name: "Defensor/a", color: "green-light" }, + { id: "r2", initials: "FI", name: "Fiscal", color: "yellow" }, + { id: "r3", initials: "QU", name: "Querella", color: "pink" }, + { id: "r4", initials: "AC", name: "Acusado/a", color: "orange" }, +]; + +const meta = { + title: "Components/PersonMenu", + component: PersonMenu, + parameters: { + layout: "centered", + figma: { + url: "https://www.figma.com/design/2BahKpebYzaccFih0ZB79y?node-id=40002701:44844", + }, + }, + args: { + options: ROLES, + onSelectOption: () => {}, + footerLabel: "Nueva persona", + onFooterAction: () => {}, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** Réplica del nodo 40002701:44844. */ +export const Default: Story = {}; + +export const WithSelection: Story = { args: { selectedIndex: 1 } }; + +/** Todos los roles ya usados: sólo queda la acción al pie. */ +export const Empty: Story = { args: { options: [] } }; + +/** La etiqueta más larga del set real, para verificar el ancho hug. */ +export const LongNames: Story = { + args: { + options: [ + ...ROLES, + { + id: "r5", + initials: "NA", + name: "Niño/a - Adolescente", + color: "violet", + }, + ], + }, +}; + +/** Como lo usa el SpeakerPicker de desktop-app: input de nombre libre al pie. */ +export const WithFooterSlot: Story = { + render: (args) => { + const [name, setName] = useState(""); + return ( + setName(e.target.value)} + style={{ width: "100%", padding: 8 }} + /> + } + /> + ); + }, +}; From 6c01fa088b1ac8e6960fe7431d96941c703bfe99 Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 6 Aug 2026 15:37:15 -0300 Subject: [PATCH 3/9] feat(side-panel): rename section to 'Personas identificadas' and move 'Nuevo' to its own row --- src/components/side-panel/SidePanel.test.tsx | 48 ++++++++++ src/components/side-panel/SidePanel.tsx | 97 +++++++++++--------- 2 files changed, 103 insertions(+), 42 deletions(-) create mode 100644 src/components/side-panel/SidePanel.test.tsx diff --git a/src/components/side-panel/SidePanel.test.tsx b/src/components/side-panel/SidePanel.test.tsx new file mode 100644 index 0000000..a4bab92 --- /dev/null +++ b/src/components/side-panel/SidePanel.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from "@testing-library/react"; +import { beforeAll, describe, expect, it, vi } from "vitest"; +import { TooltipProvider } from "../tooltip"; +import { SidePanel, type SidePanelPerson } from "./SidePanel"; + +// Radix mide sus superficies flotantes con ResizeObserver, que jsdom no trae. +beforeAll(() => { + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); +}); + +const PEOPLE: SidePanelPerson[] = [ + { id: "s1", initials: "P1", name: "Persona 1", renamable: true }, + { id: "s2", initials: "P2", name: "Persona 2", renamable: true }, +]; + +function renderPanel( + props: Partial> = {}, +) { + return render( + + + , + ); +} + +describe("SidePanel people section", () => { + it("titles the section 'Personas identificadas'", () => { + renderPanel(); + expect(screen.getByText("Personas identificadas")).toBeInTheDocument(); + }); + + it("does not title it 'Personas sugeridas' anymore", () => { + renderPanel(); + expect(screen.queryByText("Personas sugeridas")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/side-panel/SidePanel.tsx b/src/components/side-panel/SidePanel.tsx index a1f4779..3aee3e7 100644 --- a/src/components/side-panel/SidePanel.tsx +++ b/src/components/side-panel/SidePanel.tsx @@ -30,7 +30,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"; * * Composite assembled from {@link AvatarPill}, {@link TextField}, * {@link Button}, {@link Dialog} and {@link Tooltip}. Sections: selected - * turn card, suggested people, timestamp, and turn actions (merge + * turn card, identified people, timestamp, and turn actions (merge * previous/next, add below, delete). * * Merge actions use the Phosphor "ArrowsMergeIcon" glyph: base points down @@ -160,6 +160,13 @@ const pills = css({ gap: "2", // 8px w: "full", }); +// Figma nodo 40002701:44839 ("Pills"): dos filas de 40px con 8px de gap — +// las pills arriba (envolviendo si hace falta) y el botón "Nuevo" siempre +// debajo, no pegado al final de la última fila de pills. +const peopleGroup = css({ + ...stack.raw({ gap: "2" }), // 8px + w: "full", +}); const actions = css({ ...stack.raw({ gap: "4" }), w: "full" }); // 16px const fullWidthButton = css({ w: "full" }); const divider = css({ @@ -393,49 +400,55 @@ export function SidePanel({ - {/* Suggested people */} -
-
- {people.map((person, index) => { - const isEditing = editingIndex === index; - const canRename = - person.renamable && onRenamePerson && onMergePeople; + {/* Identified people */} +
+
+
+ {people.map((person, index) => { + const isEditing = editingIndex === index; + const canRename = + person.renamable && onRenamePerson && onMergePeople; - return ( - - onSelectPerson?.(index)} - onRename={canRename ? () => startEditing(index) : undefined} - editValue={isEditing ? editValue : undefined} - onEditValueChange={isEditing ? setEditValue : undefined} - onEditCommit={ - isEditing - ? (value) => handleRenameCommit(index, value) - : undefined + return ( + - - ); - })} - + className={css({ display: "inline-flex" })} + > + onSelectPerson?.(index)} + onRename={canRename ? () => startEditing(index) : undefined} + editValue={isEditing ? editValue : undefined} + onEditValueChange={isEditing ? setEditValue : undefined} + onEditCommit={ + isEditing + ? (value) => handleRenameCommit(index, value) + : undefined + } + onEditCancel={isEditing ? finishEditing : undefined} + renameInputLabel={`Editar nombre de ${person.name}`} + /> + + ); + })} +
+
+ +
Date: Thu, 6 Aug 2026 15:39:53 -0300 Subject: [PATCH 4/9] feat(side-panel): turn 'Nuevo' into a PersonMenu trigger for role options --- src/components/side-panel/SidePanel.test.tsx | 63 +++++++++++++++++++ src/components/side-panel/SidePanel.tsx | 65 ++++++++++++++++++-- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/components/side-panel/SidePanel.test.tsx b/src/components/side-panel/SidePanel.test.tsx index a4bab92..ef660ac 100644 --- a/src/components/side-panel/SidePanel.test.tsx +++ b/src/components/side-panel/SidePanel.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { TooltipProvider } from "../tooltip"; import { SidePanel, type SidePanelPerson } from "./SidePanel"; @@ -13,6 +14,10 @@ beforeAll(() => { disconnect() {} }, ); + Element.prototype.hasPointerCapture = () => false; + Element.prototype.setPointerCapture = () => {}; + Element.prototype.releasePointerCapture = () => {}; + Element.prototype.scrollIntoView = () => {}; }); const PEOPLE: SidePanelPerson[] = [ @@ -46,3 +51,61 @@ describe("SidePanel people section", () => { expect(screen.queryByText("Personas sugeridas")).not.toBeInTheDocument(); }); }); + +const ROLES: SidePanelPerson[] = [ + { id: "r1", initials: "JU", name: "Juez/a" }, + { id: "r2", initials: "FI", name: "Fiscal" }, +]; + +describe("SidePanel new-person menu", () => { + it("calls onNewPerson directly when there are no options", async () => { + const onNewPerson = vi.fn(); + renderPanel({ onNewPerson }); + await userEvent.click(screen.getByRole("button", { name: /Nuevo/ })); + expect(onNewPerson).toHaveBeenCalledTimes(1); + expect( + screen.queryByRole("button", { name: "Fiscal" }), + ).not.toBeInTheDocument(); + }); + + it("opens a menu with the roles when options are provided", async () => { + renderPanel({ newPersonOptions: ROLES, onSelectNewPersonOption: vi.fn() }); + expect( + screen.queryByRole("button", { name: "Fiscal" }), + ).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: /Nuevo/ })); + expect(screen.getByRole("button", { name: "Juez/a" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Fiscal" })).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Nueva persona" }), + ).toBeInTheDocument(); + }); + + it("reports the chosen role index and closes the menu", async () => { + const onSelectNewPersonOption = vi.fn(); + renderPanel({ newPersonOptions: ROLES, onSelectNewPersonOption }); + await userEvent.click(screen.getByRole("button", { name: /Nuevo/ })); + await userEvent.click(screen.getByRole("button", { name: "Fiscal" })); + expect(onSelectNewPersonOption).toHaveBeenCalledWith(1); + expect( + screen.queryByRole("button", { name: "Fiscal" }), + ).not.toBeInTheDocument(); + }); + + it("routes the footer action to onNewPerson and closes the menu", async () => { + const onNewPerson = vi.fn(); + renderPanel({ + newPersonOptions: ROLES, + onSelectNewPersonOption: vi.fn(), + onNewPerson, + }); + await userEvent.click(screen.getByRole("button", { name: /Nuevo/ })); + await userEvent.click( + screen.getByRole("button", { name: "Nueva persona" }), + ); + expect(onNewPerson).toHaveBeenCalledTimes(1); + expect( + screen.queryByRole("button", { name: "Nueva persona" }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/side-panel/SidePanel.tsx b/src/components/side-panel/SidePanel.tsx index 3aee3e7..e73aad9 100644 --- a/src/components/side-panel/SidePanel.tsx +++ b/src/components/side-panel/SidePanel.tsx @@ -14,6 +14,8 @@ import { DialogDescription, DialogTitle, } from "../dialog"; +import { PersonMenu } from "../person-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "../popover"; import { TextField } from "../text-field"; import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"; @@ -78,7 +80,19 @@ export type SidePanelProps = { /** Index of the selected pill in `people` */ selectedIndex?: number; onSelectPerson?: (index: number) => void; + /** + * With `newPersonOptions`, this is the handler for the "Nueva persona" + * action in the menu's footer. Without options, it's the "Nuevo" button's + * direct click handler, as before. + */ onNewPerson?: () => void; + /** + * Opciones del desplegable de "Nuevo" (p. ej. roles sugeridos). Vacío u + * omitido → "Nuevo" llama a `onNewPerson` directo, como antes. + */ + newPersonOptions?: SidePanelPerson[]; + /** Recibe el índice en `newPersonOptions` de la opción elegida. */ + onSelectNewPersonOption?: (index: number) => void; /** Persists a non-conflicting speaker rename. Set with `onMergePeople` to enable editing. */ onRenamePerson?: (index: number, nextName: string) => void; /** Merges the edited source identity into the existing target identity. Set with `onRenamePerson`. */ @@ -167,6 +181,9 @@ const peopleGroup = css({ ...stack.raw({ gap: "2" }), // 8px w: "full", }); +// Figma nodo 40002701:45247 muestra el botón en 40px; Button no tiene ese +// tamaño (sm=32, md=48). +const newButton = css({ h: "10" }); const actions = css({ ...stack.raw({ gap: "4" }), w: "full" }); // 16px const fullWidthButton = css({ w: "full" }); const divider = css({ @@ -283,6 +300,8 @@ export function SidePanel({ selectedIndex, onSelectPerson, onNewPerson, + newPersonOptions, + onSelectNewPersonOption, onRenamePerson, onMergePeople, timestamp, @@ -297,6 +316,7 @@ export function SidePanel({ className, }: SidePanelProps) { const [confirm, setConfirm] = useState(null); + const [menuOpen, setMenuOpen] = useState(false); const [editingIndex, setEditingIndex] = useState(null); const [editValue, setEditValue] = useState(""); const [renameConflict, setRenameConflict] = @@ -444,10 +464,47 @@ export function SidePanel({ })}
- + {newPersonOptions && newPersonOptions.length > 0 ? ( + + + + + + { + setMenuOpen(false); + onSelectNewPersonOption?.(index); + }} + footerLabel="Nueva persona" + onFooterAction={() => { + setMenuOpen(false); + onNewPerson?.(); + }} + /> + + + ) : ( + + )}
Date: Thu, 6 Aug 2026 15:42:29 -0300 Subject: [PATCH 5/9] docs(side-panel): split identified people from role options in stories --- .../side-panel/SidePanel.stories.tsx | 59 +++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/src/components/side-panel/SidePanel.stories.tsx b/src/components/side-panel/SidePanel.stories.tsx index 8f5bf45..372a061 100644 --- a/src/components/side-panel/SidePanel.stories.tsx +++ b/src/components/side-panel/SidePanel.stories.tsx @@ -1,7 +1,14 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; import { TooltipProvider } from "../tooltip"; -import { SidePanel } from "./SidePanel"; +import { SidePanel, type SidePanelPerson } from "./SidePanel"; + +const ROLE_OPTIONS: SidePanelPerson[] = [ + { id: "r1", initials: "JU", name: "Juez/a", color: "violet" }, + { id: "r2", initials: "FI", name: "Fiscal", color: "green-light" }, + { id: "r3", initials: "DE", name: "Defensor/a", color: "pink" }, + { id: "r4", initials: "QU", name: "Querella", color: "yellow" }, +]; const meta = { title: "Components/SidePanel", @@ -27,11 +34,7 @@ type Story = StoryObj; const PEOPLE = [ { initials: "AB", name: "Persona 1", color: "violet" as const }, { initials: "AB", name: "Persona 2", color: "green" as const }, - { initials: "JU", name: "Jueza", color: "red" as const }, - { initials: "FI", name: "Fiscal", color: "yellow" as const }, - { initials: "DE", name: "Defensor", color: "pink" as const }, - { initials: "AB", name: "Imputado", color: "orange" as const }, - { initials: "DE", name: "Defensor 2", color: "green-light" as const }, + { initials: "AB", name: "Persona 3", color: "orange" as const }, ]; const RENAMEABLE_PEOPLE = [ @@ -75,6 +78,8 @@ export const Default: Story = { onSelectPerson={setSelected} timestamp={time} onTimestampChange={setTime} + newPersonOptions={ROLE_OPTIONS} + onSelectNewPersonOption={() => {}} /> ); }, @@ -100,6 +105,8 @@ export const Sizes: Story = { selectedIndex={selected} onSelectPerson={setSelected} timestamp="01:15" + newPersonOptions={ROLE_OPTIONS} + onSelectNewPersonOption={() => {}} /> ))} @@ -125,6 +132,8 @@ export const NarrowContainer: Story = { }} people={PEOPLE} timestamp="01:15" + newPersonOptions={ROLE_OPTIONS} + onSelectNewPersonOption={() => {}} /> ), @@ -150,10 +159,12 @@ export const MergeConfirmation: Story = { selectedIndex={selected} onSelectPerson={setSelected} timestamp="01:15" - previousTurnName="Fiscal" - nextTurnName="Defensor" - onMergePrevious={() => window.alert("Unido con Fiscal")} - onMergeNext={() => window.alert("Unido con Defensor")} + previousTurnName="Persona 2" + nextTurnName="Persona 3" + onMergePrevious={() => window.alert("Unido con Persona 2")} + onMergeNext={() => window.alert("Unido con Persona 3")} + newPersonOptions={ROLE_OPTIONS} + onSelectNewPersonOption={() => {}} /> ); }, @@ -174,6 +185,8 @@ export const InvalidTimestamp: Story = { people={PEOPLE} timestamp={time} onTimestampChange={setTime} + newPersonOptions={ROLE_OPTIONS} + onSelectNewPersonOption={() => {}} /> ); }, @@ -227,3 +240,29 @@ export const RenameAndCollision: Story = { ); }, }; + +/** Réplica del nodo 40002701:44829, con el desplegable de "Nuevo" disponible. */ +export const WithNewPersonMenu: Story = { + args: { + people: [ + { id: "s1", initials: "P1", name: "Persona 1", renamable: true }, + { id: "s2", initials: "P2", name: "Persona 2", renamable: true }, + { id: "s3", initials: "P3", name: "Persona 3", renamable: true }, + ], + selectedIndex: 0, + timestamp: "01:15", + turn: { initials: "P1", name: "Persona 1", time: "01:15", color: "violet" }, + newPersonOptions: ROLE_OPTIONS, + onSelectNewPersonOption: () => {}, + }, +}; + +/** Sin `newPersonOptions`: "Nuevo" llama a onNewPerson directo (retrocompatible). */ +export const WithoutNewPersonMenu: Story = { + args: { + people: [{ id: "s1", initials: "P1", name: "Persona 1", renamable: true }], + timestamp: "01:15", + turn: { initials: "P1", name: "Persona 1", time: "01:15", color: "violet" }, + newPersonOptions: undefined, + }, +}; From 0ea4007e50475c216d4240a26a4915571f8c9ef4 Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 6 Aug 2026 15:45:49 -0300 Subject: [PATCH 6/9] chore: release v0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45245d1..cb923a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aymurai/ui", - "version": "0.5.0", + "version": "0.6.0", "description": "AymurAI shared React component library, extracted from the Figma UI Library and authored with Panda CSS.", "license": "MIT", "type": "module", From 6a212ee8a0e6d85e34762a75e6f72fce956767fe Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 6 Aug 2026 19:17:33 -0300 Subject: [PATCH 7/9] fix: address Sourcery review feedback on PR #22 - Gate PersonMenu's footer button on both footerLabel and onFooterAction so a future consumer that passes only footerLabel doesn't get a dead button. - Wire onNewPerson in the WithoutNewPersonMenu story so it actually demonstrates the backwards-compatible fallback it documents. --- src/components/person-menu/PersonMenu.tsx | 2 +- src/components/side-panel/SidePanel.stories.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/person-menu/PersonMenu.tsx b/src/components/person-menu/PersonMenu.tsx index a22e122..364048a 100644 --- a/src/components/person-menu/PersonMenu.tsx +++ b/src/components/person-menu/PersonMenu.tsx @@ -97,7 +97,7 @@ export function PersonMenu({ }: PersonMenuProps) { const footer = footerSlot ?? - (footerLabel ? ( + (footerLabel && onFooterAction ? (