From e2d2801be270bcb65c0ce6c86f1433901772823f Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 16 Jul 2026 13:05:10 -0300 Subject: [PATCH 1/6] fix(toolbar): cap search width at 711.5px in search-switch context - Add isSearchSwitch context detection - Set fixed width of 711.5px for search box in search-switch context - Preserve flex behavior for anonimizador and set-de-datos contexts - Prevents search box from crowding the Switch+label on the right Refs: Figma node 40001478:54722 Co-Authored-By: Claude Sonnet 5 --- src/components/toolbar/Toolbar.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/toolbar/Toolbar.tsx b/src/components/toolbar/Toolbar.tsx index ef89163..0572cdd 100644 --- a/src/components/toolbar/Toolbar.tsx +++ b/src/components/toolbar/Toolbar.tsx @@ -102,7 +102,8 @@ export function Toolbar({ // set-de-datos — items: end, justify: start, gap: 24px, py: 24px // search-switch — items: center, justify: space-between, pt: 42px, pb: 24px const isSetDeDatos = context === "set-de-datos"; - const alignItems = context === "search-switch" ? "center" : "flex-end"; + const isSearchSwitch = context === "search-switch"; + const alignItems = isSearchSwitch ? "center" : "flex-end"; return (
- {/* Search zone — always present, fills available space */} + {/* Search zone. Fills available space in anonimizador/set-de-datos; + fixed at 711.5px in search-switch (Figma node 40001478:54722) so it + doesn't crowd the Switch+label on the right. */}
From df8adeba321fa4fdd7db53325798a7b569cce8d4 Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 16 Jul 2026 13:20:30 -0300 Subject: [PATCH 2/6] fix(side-panel): render merge/rename confirmations as a centered modal, not an anchored popover The rename-collision and merge-previous/next confirmations were rendered as Radix Popovers anchored to their trigger element. The real Figma design (node 40002384:38487) is a centered modal over a full-screen backdrop, not something tethered to a pill or button. Swap both call sites to a shared internal ConfirmDialog backed by this repo's Dialog primitive, and correct the visual details to match Figma: - 4px gap between title/description, 16px from text block to buttons, 12px between buttons (previously a single 12px gap throughout) - description text now text.default (previously the lighter grey text.lighter) - drop the 341px width cap (it measured the inner content, not the full 389px card) in favor of DialogContent's existing responsive sizing - add a visually-hidden DialogTitle so Radix's accessibility requirement is met without changing the visible title's look - rename-collision copy now names both identities explicitly per Figma ("Ya existe "X"." / "Al combinar, los turnos de "Y" pasan a "X".") The merge-previous/merge-next confirm keeps its existing copy unchanged; only its rendering primitive changes. --- src/components/side-panel/SidePanel.tsx | 284 ++++++++++++------------ 1 file changed, 147 insertions(+), 137 deletions(-) diff --git a/src/components/side-panel/SidePanel.tsx b/src/components/side-panel/SidePanel.tsx index 1d082e9..fe24224 100644 --- a/src/components/side-panel/SidePanel.tsx +++ b/src/components/side-panel/SidePanel.tsx @@ -8,7 +8,7 @@ import type { AvatarColor } from "../avatar"; import { Avatar } from "../avatar"; import { AvatarPill } from "../avatar-pill"; import { Button } from "../button"; -import { Popover, PopoverAnchor, PopoverContent } from "../popover"; +import { Dialog, DialogContent, DialogTitle } from "../dialog"; import { TextField } from "../text-field"; import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"; import { ArrowsMerge } from "./ArrowsMergeIcon"; @@ -18,7 +18,7 @@ import { ArrowsMerge } from "./ArrowsMergeIcon"; * AymurAI UI Library node 40002322:53113. * * Composite assembled from {@link AvatarPill}, {@link TextField}, - * {@link Button}, {@link Popover} and {@link Tooltip}. Sections: selected + * {@link Button}, {@link Dialog} and {@link Tooltip}. Sections: selected * turn card, suggested people, timestamp, and turn actions (merge * previous/next, add below, delete). * @@ -29,7 +29,7 @@ import { ArrowsMerge } from "./ArrowsMergeIcon"; * Consumers need a `TooltipProvider` somewhere up the tree for the Acciones * tooltips (see Tooltip.tsx / this component's story). * - * Merging two turns whose speakers differ shows a confirm popover (Figma + * Merging two turns whose speakers differ shows a confirm modal (Figma * "Conflicto Nombre etiqueta", node 40002384:38487) before firing * `onMergePrevious`/`onMergeNext` — pass `previousTurnName`/`nextTurnName` to * enable it; without them, merge fires immediately (previous behaviour). @@ -78,9 +78,9 @@ export type SidePanelProps = { onTimestampChange?: (value: string) => void; onMergePrevious?: () => void; onMergeNext?: () => void; - /** Speaker name of the previous turn — enables the merge confirm popover */ + /** Speaker name of the previous turn — enables the merge confirm modal */ previousTurnName?: string; - /** Speaker name of the next turn — enables the merge confirm popover */ + /** Speaker name of the next turn — enables the merge confirm modal */ nextTurnName?: string; onAddBelow?: () => void; onDelete?: () => void; @@ -142,25 +142,20 @@ const divider = css({ bg: "[#BCBAB8]", // Figma divider line (border/primary colour) }); -// Confirm popover (Figma "Conflicto Nombre etiqueta", node 40002384:38487): -// title + description + Combinar/Cancelar. Anchored to the Acciones section. -const confirmBox = css({ - ...stack.raw({ gap: "3" }), // 12px - p: "6", // 24px - maxW: "[341px]", -}); +// Confirm modal (Figma "Conflicto Nombre etiqueta", node 40002384:38487): +// title + description + Combinar/Cancelar, centered over a full-screen overlay. const confirmTitle = css({ textStyle: "subtitle.sm.strong", color: "text.default", }); const confirmDescription = css({ textStyle: "label.sm.default", - color: "text.lighter", + color: "text.default", }); const confirmButtons = css({ display: "flex", alignItems: "center", - gap: "4", // 16px + gap: "3", // 12px }); function Section({ @@ -199,6 +194,65 @@ function ActionButton({ ); } +const visuallyHidden = css({ + position: "absolute", + w: "[1px]", + h: "[1px]", + p: "0", + m: "[-1px]", + overflow: "hidden", + clip: "[rect(0,0,0,0)]", + whiteSpace: "nowrap", + borderWidth: "0", +}); + +const confirmTextBlock = css({ + ...stack.raw({ gap: "1" }), // 4px +}); + +function ConfirmDialog({ + open, + onOpenChange, + title, + description, + onConfirm, + onCancel, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + onConfirm: () => void; + onCancel: () => void; +}) { + return ( + + + + {title} + +
+

{title}

+

{description}

+
+
+ + +
+
+
+ ); +} + export function SidePanel({ turn, people, @@ -256,11 +310,8 @@ export function SidePanel({ ); if (targetIndex >= 0) { - // Drop out of typing/input mode — the confirm popover is the only - // active affordance now. Leaving the input mounted here would sit - // inside PopoverAnchor (outside PopoverContent), so a click into it - // to keep editing registers as a Radix pointer-down-outside and - // cancels the whole edit instead. + // Drop out of typing/input mode — the confirm dialog is the only + // active affordance now. setEditingIndex(null); setEditValue(""); setRenameConflict({ @@ -329,77 +380,38 @@ export function SidePanel({
{people.map((person, index) => { const isEditing = editingIndex === index; - const hasRenameConflict = renameConflict?.sourceIndex === index; - const conflictTarget = hasRenameConflict - ? people[renameConflict.targetIndex] - : undefined; const canRename = person.renamable && onRenamePerson && onMergePeople; return ( - { - if (!open) finishEditing(); - }} + 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}`} - /> - - - {hasRenameConflict && conflictTarget && ( - -

- Ya existe una persona llamada "{conflictTarget.name}". -

-

- Si continuás, ambas identidades se combinarán en una sola. -

-
- - -
-
- )} -
+ 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}`} + /> + ); })}
+ { + if (!open) finishEditing(); + }} + title={ + renameConflict + ? `Ya existe "${people[renameConflict.targetIndex]?.name}".` + : "" + } + description={ + renameConflict + ? `Al combinar, los turnos de "${people[renameConflict.sourceIndex]?.name}" pasan a "${people[renameConflict.targetIndex]?.name}".` + : "" + } + onConfirm={confirmPeopleMerge} + onCancel={finishEditing} + />
@@ -423,63 +453,43 @@ export function SidePanel({ {/* Actions */}
- + + + Unir con el anterior + + + + Unir con el siguiente + + + + Agregar debajo + + + + Eliminar + +
+ { if (!open) setConfirm(null); }} - > - -
- - - Unir con el anterior - - - - Unir con el siguiente - - - - Agregar debajo - - - - Eliminar - -
-
- {confirm && ( - -

{confirm.title}

-

{confirm.description}

-
- - -
-
- )} - + title={confirm?.title ?? ""} + description={confirm?.description ?? ""} + onConfirm={() => confirm?.onConfirm()} + onCancel={() => setConfirm(null)} + />
); From 079e889883a03b6bec9e6ca7d6d2fea5721bbccf Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 16 Jul 2026 13:34:43 -0300 Subject: [PATCH 3/6] fix(side-panel): constrain confirm modal width, fix a11y and shadow issues Follow-up to df8adeb's Popover->Dialog migration for ConfirmDialog: - Cap DialogContent at maxW 389px (Figma node 40002384:38487 card width) instead of falling through to the 700px default from Dialog.tsx. - Drop the custom boxShadow override, which competed non-deterministically with DialogContent's own `dialog` shadow token. - Wrap the description in DialogDescription (asChild) to satisfy Radix's aria-describedby requirement and silence its dev warning. - Make the visible title itself the DialogTitle (asChild) instead of rendering a separate visually-hidden duplicate, so screen readers announce it once; removes the now-unused visuallyHidden class. --- src/components/side-panel/SidePanel.tsx | 32 ++++++++++--------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/components/side-panel/SidePanel.tsx b/src/components/side-panel/SidePanel.tsx index fe24224..7dc5f56 100644 --- a/src/components/side-panel/SidePanel.tsx +++ b/src/components/side-panel/SidePanel.tsx @@ -8,7 +8,12 @@ import type { AvatarColor } from "../avatar"; import { Avatar } from "../avatar"; import { AvatarPill } from "../avatar-pill"; import { Button } from "../button"; -import { Dialog, DialogContent, DialogTitle } from "../dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "../dialog"; import { TextField } from "../text-field"; import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"; import { ArrowsMerge } from "./ArrowsMergeIcon"; @@ -194,18 +199,6 @@ function ActionButton({ ); } -const visuallyHidden = css({ - position: "absolute", - w: "[1px]", - h: "[1px]", - p: "0", - m: "[-1px]", - overflow: "hidden", - clip: "[rect(0,0,0,0)]", - whiteSpace: "nowrap", - borderWidth: "0", -}); - const confirmTextBlock = css({ ...stack.raw({ gap: "1" }), // 4px }); @@ -230,15 +223,16 @@ function ConfirmDialog({ - - {title} -
-

{title}

-

{description}

+ +

{title}

+
+ +

{description}

+
+ +
+
+ + ); +} +``` + +- [ ] **Step 4: Simplify the pill loop — drop the per-pill `Popover`/`PopoverAnchor`** + +Replace the `people.map((person, index) => { ... })` body (currently wraps each `AvatarPill` in a `Popover`/`PopoverAnchor`/conditional `PopoverContent`) with a plain wrapper — the confirm dialog moves out of the loop entirely (Step 5): + +```tsx + {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 + } + onEditCancel={isEditing ? finishEditing : undefined} + renameInputLabel={`Editar nombre de ${person.name}`} + /> + + ); + })} +``` + +- [ ] **Step 5: Render the rename-collision `ConfirmDialog` once, outside the loop** + +Immediately after the `pills` div's closing `` (still inside the `Section heading="Personas sugeridas"`), add: + +```tsx + { + if (!open) finishEditing(); + }} + title={`Ya existe una persona llamada "${ + renameConflict ? people[renameConflict.targetIndex]?.name : "" + }".`} + description="Si continuás, ambas identidades se combinarán en una sola." + onConfirm={confirmPeopleMerge} + onCancel={finishEditing} + /> +``` + +- [ ] **Step 6: Drop the `Popover`/`PopoverAnchor` wrapper around the Acciones buttons** + +Replace the `Section heading="Acciones"` body — remove the `Popover`/`PopoverAnchor` wrapper, keep the plain `actions` div, and move its confirm out to a `ConfirmDialog`: + +```tsx +
+
+ + + Unir con el anterior + + + + Unir con el siguiente + + + + Agregar debajo + + + + Eliminar + +
+ { + if (!open) setConfirm(null); + }} + title={confirm?.title ?? ""} + description={confirm?.description ?? ""} + onConfirm={() => confirm?.onConfirm()} + onCancel={() => setConfirm(null)} + /> +
+``` + +- [ ] **Step 7: Typecheck and lint** + +```bash +pnpm typecheck +pnpm biome check src/components/side-panel/SidePanel.tsx +``` + +Expected: PASS. If `PopoverContent`'s `showArrow` prop or the `Popover`/`PopoverAnchor` imports are now unused anywhere else in the file, biome's `organizeImports` will flag/remove them — confirm the diff only removes what Step 2 intended. + +- [ ] **Step 8: Verify in Storybook** + +Run `pnpm storybook`, open `Components/SidePanel`: +- `RenameAndCollision`: repeat the Step 1 repro — confirm the confirmation now renders as a centered card with a dark backdrop over the whole story canvas, not anchored to the pill. Confirm "Combinar" calls `onMergePeople` (watch `lastAction` text update) and "Cancelar" (or clicking the backdrop) dismisses without changes. +- `MergeConfirmation`: click "Unir con el anterior" / "Unir con el siguiente" — confirm the same centered-modal treatment, correct copy per direction, and that dismissing via backdrop click behaves like Cancelar (no `onMergePrevious`/`onMergeNext` call). +- `Default`/`InvalidTimestamp`: confirm unaffected (no confirm dialogs involved). + +- [ ] **Step 9: Commit** + +```bash +git add src/components/side-panel/SidePanel.tsx +git commit -m "fix(side-panel): render merge/rename confirmations as a centered modal, not an anchored popover" +``` + +--- + +## Final Verification + +- [ ] `pnpm typecheck` — PASS +- [ ] `pnpm biome check` (full repo) — PASS +- [ ] `pnpm build` — PASS (confirms the library still builds/exports cleanly for consumers pinned to a git tag) +- [ ] Manual Storybook pass over `Components/Toolbar` and `Components/SidePanel`, all stories, per Steps 4 and 8 above +- [ ] Push the branch and open a PR referencing the two upstream reports (search width in `search-switch`, and the rename/merge confirmation not matching Figma node `40002384:38487`) — do not merge without review, this library is consumed by a pinned tag (`v0.4.1`) from `desktop-app` From 649c5d55edc2fd7643112550e80ef019d70df598 Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 16 Jul 2026 14:48:38 -0300 Subject: [PATCH 5/6] fix(side-panel): correct confirm dialog title/description text styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit confirmTitle used subtitle.sm.strong (14px) instead of subtitle.md.strong (20px) — Figma's real title style per node 40002384:38487's own annotations. confirmDescription used label.sm.default (12px) instead of subtitle.sm.default (14px). Pre-existing since the merge-confirmation popover shipped in v0.4.0; caught now while polishing this same dialog's Figma fidelity. Verified in Storybook: title/description now render at Figma's actual scale. --- src/components/side-panel/SidePanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/side-panel/SidePanel.tsx b/src/components/side-panel/SidePanel.tsx index 7dc5f56..7c58e4c 100644 --- a/src/components/side-panel/SidePanel.tsx +++ b/src/components/side-panel/SidePanel.tsx @@ -150,11 +150,11 @@ const divider = css({ // Confirm modal (Figma "Conflicto Nombre etiqueta", node 40002384:38487): // title + description + Combinar/Cancelar, centered over a full-screen overlay. const confirmTitle = css({ - textStyle: "subtitle.sm.strong", + textStyle: "subtitle.md.strong", color: "text.default", }); const confirmDescription = css({ - textStyle: "label.sm.default", + textStyle: "subtitle.sm.default", color: "text.default", }); const confirmButtons = css({ From 5031f4910c738e7f86b5c57bab87d05c5e769556 Mon Sep 17 00:00:00 2001 From: jansaldo Date: Thu, 16 Jul 2026 16:04:19 -0300 Subject: [PATCH 6/6] chore(gitignore): ignore docs/superpowers and remove tracked plan --- .gitignore | 3 + ...-search-switch-width-and-conflict-modal.md | 310 ------------------ 2 files changed, 3 insertions(+), 310 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-16-search-switch-width-and-conflict-modal.md diff --git a/.gitignore b/.gitignore index a84e5d3..c53e981 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ src/styled # Figma fidelity audit scratch (playwright + screenshots) .audit/ + +# Local-only superpowers specs and plans (never committed) +docs/superpowers/ diff --git a/docs/superpowers/plans/2026-07-16-search-switch-width-and-conflict-modal.md b/docs/superpowers/plans/2026-07-16-search-switch-width-and-conflict-modal.md deleted file mode 100644 index b833e0f..0000000 --- a/docs/superpowers/plans/2026-07-16-search-switch-width-and-conflict-modal.md +++ /dev/null @@ -1,310 +0,0 @@ -# Search+Switch Width & Conflict Modal Fix (ui-components) Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Fix two visual regressions found while integrating this library into `desktop-app`'s Voz a Texto (VTT) screens: (1) `Toolbar`'s `search-switch` context stretches the search box edge-to-edge instead of the fixed width Figma specifies, and (2) `SidePanel`'s three "Combinar/Cancelar" confirmations (rename collision, merge-previous, merge-next) render as small anchored popovers instead of the centered modal-with-overlay the library's own code comments cite as their Figma reference. - -**Architecture:** Two independent, surgical fixes to existing components — no new components, no API changes (both are pure internal-rendering fixes; every existing prop, callback, and Storybook story keeps working unchanged). - -**Tech Stack:** React + TypeScript, Panda CSS (`@/styled/css`), Radix UI (`@radix-ui/react-dialog` via this repo's own `Dialog` wrapper), Storybook (this repo has no unit test suite — Storybook stories are the verification convention). - -## Global Constraints - -- Package manager is pnpm. -- Panda `strictTokens: true` (see `panda.config.ts`) — arbitrary pixel values must use the `[bracket]` escape syntax. -- This repo ships no unit tests anywhere (`find src -iname "*.test.tsx"` returns nothing) — verify each fix via its existing Storybook story (`pnpm storybook`), not new test files. -- Do not change any exported prop names/types on `ToolbarProps` or `SidePanelProps` — both fixes are internal-only. -- Both fixes are scoped to the `search-switch` Toolbar context and the SidePanel confirm dialogs respectively — do not touch the `anonimizador`/`set-de-datos` Toolbar layouts, which are correct as-is (verified against their own Figma frames). -- This work is deliberately kept on its own branch, separate from any desktop-app change — do not touch anything outside `src/components/toolbar/` and `src/components/side-panel/`. - ---- - -## Task 1: Cap the search box width in the `search-switch` Toolbar context - -**Files:** -- Modify: `src/components/toolbar/Toolbar.tsx:100-145` - -**Interfaces:** None — `ToolbarProps` unchanged. - -**Context:** Per Figma node `40001478:54722` ("Search+Switch" property of the `tool_bar` component) and the standalone `Search+Switch` instance embedded in node `40002383:72559`, the search box has a **fixed width of 711.5px** inside a 1440px-wide bar (with `justify-content: space-between` already pushing the switch to the far right — that part is already correct). The current code gives both the wrapper `div` and the `Search` component `flex: "1"`, so the search box always grows to fill all remaining space, crowding the switch. This must NOT change the `anonimizador` (846/1440px "Search (wide)") or `set-de-datos` contexts, which are correct as designed. - -- [ ] **Step 1: Confirm today's broken behavior** - -Run `pnpm storybook`, open `Components/Toolbar` → `SearchSwitch`. Resize the preview frame wide — confirm the search input stretches to fill almost the entire bar, leaving the "Modo Edición" switch crammed against it with no breathing room. This is the bug being fixed. - -- [ ] **Step 2: Scope the fix to `search-switch` only** - -In `src/components/toolbar/Toolbar.tsx`, add a second boolean next to the existing `isSetDeDatos`: - -```ts - const isSetDeDatos = context === "set-de-datos"; - const isSearchSwitch = context === "search-switch"; - const alignItems = isSearchSwitch ? "center" : "flex-end"; -``` - -- [ ] **Step 3: Fix the search wrapper and `Search` width** - -Replace the search-zone block (currently lines ~123-145): - -```tsx - {/* Search zone. Fills available space in anonimizador/set-de-datos; - fixed at 711.5px in search-switch (Figma node 40001478:54722) so it - doesn't crowd the Switch+label on the right. */} -
- onSearchChange?.(e.target.value)} - placeholder={searchPlaceholder} - aria-label={searchAriaLabel} - labels={searchLabels} - resultCount={searchResultCount} - onPrev={onSearchPrev} - onNext={onSearchNext} - onClear={onSearchClear} - className={css({ - flex: isSearchSwitch ? undefined : "1", - w: isSearchSwitch ? "[711.5px]" : undefined, - })} - /> -
-``` - -- [ ] **Step 4: Verify in Storybook** - -Run `pnpm storybook`, open `Components/Toolbar`: -- `SearchSwitch` and `SearchSwitchWithResults`: confirm the search box now stops at a fixed width with visible empty space before the switch, matching the Figma frame. -- `Anonimizador` and `SetDeDatos`: confirm both are pixel-identical to before (still stretch to fill). -- `Matrix`: confirm all three rows render correctly side by side. - -- [ ] **Step 5: Typecheck and lint** - -```bash -pnpm typecheck -pnpm biome check src/components/toolbar/Toolbar.tsx -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add src/components/toolbar/Toolbar.tsx -git commit -m "fix(toolbar): cap search width at 711.5px in search-switch context" -``` - ---- - -## Task 2: Replace the anchored confirm popover with a centered modal in `SidePanel` - -**Files:** -- Modify: `src/components/side-panel/SidePanel.tsx` - -**Interfaces:** None — `SidePanelProps` unchanged; `onRenamePerson`/`onMergePeople`/`onMergePrevious`/`onMergeNext` keep the exact same call sites and semantics. Only the rendering primitive for the confirmation changes (`Popover` → `Dialog`). - -**Context:** The component's own doc comment says both confirmations follow "Figma 'Conflicto Nombre etiqueta', node 40002384:38487" — but that Figma node (confirmed via `get_design_context`) is `left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2` with a full-screen `bg-[rgba(0,0,0,0.4)]` backdrop behind it: a **centered modal with overlay**, not something anchored to a specific pill or button. The current code renders it as a Radix `Popover` anchored via `PopoverAnchor`, which visually tethers it to whichever element triggered it — that's the actual bug, not just a styling gap. This repo already has a `Dialog` primitive (`src/components/dialog/Dialog.tsx`, itself ported from `desktop-app`'s own dialog) that renders exactly the centered-overlay pattern needed — reuse it instead of building anything new. - -Both existing call sites (`renameConflict` from the pill-rename flow, `confirm` from the merge-previous/merge-next flow) render the same title+description+Combinar/Cancelar shape — this task extracts that shape into one small internal `ConfirmDialog` and backs both with `Dialog` instead of `Popover`. - -- [ ] **Step 1: Confirm today's broken behavior** - -Run `pnpm storybook`, open `Components/SidePanel` → `RenameAndCollision`. Click the pencil on "Persona 1", type "Fiscal", press Enter. Today this pops up right next to the pill, in a small anchored box. Compare against the Figma screenshot for node `40002384:38487` — same copy, but the real design is a centered card with a dark backdrop over the whole screen. - -- [ ] **Step 2: Swap the import** - -At the top of `src/components/side-panel/SidePanel.tsx`, replace: - -```ts -import { Popover, PopoverAnchor, PopoverContent } from "../popover"; -``` - -with: - -```ts -import { Dialog, DialogContent } from "../dialog"; -``` - -- [ ] **Step 3: Add a shared `ConfirmDialog` subcomponent** - -Add this right after the `ActionButton` function (before `export function SidePanel`): - -```tsx -function ConfirmDialog({ - open, - onOpenChange, - title, - description, - onConfirm, - onCancel, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - title: string; - description: string; - onConfirm: () => void; - onCancel: () => void; -}) { - return ( - - -

{title}

-

{description}

-
- - -
-
-
- ); -} -``` - -- [ ] **Step 4: Simplify the pill loop — drop the per-pill `Popover`/`PopoverAnchor`** - -Replace the `people.map((person, index) => { ... })` body (currently wraps each `AvatarPill` in a `Popover`/`PopoverAnchor`/conditional `PopoverContent`) with a plain wrapper — the confirm dialog moves out of the loop entirely (Step 5): - -```tsx - {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 - } - onEditCancel={isEditing ? finishEditing : undefined} - renameInputLabel={`Editar nombre de ${person.name}`} - /> - - ); - })} -``` - -- [ ] **Step 5: Render the rename-collision `ConfirmDialog` once, outside the loop** - -Immediately after the `pills` div's closing `` (still inside the `Section heading="Personas sugeridas"`), add: - -```tsx - { - if (!open) finishEditing(); - }} - title={`Ya existe una persona llamada "${ - renameConflict ? people[renameConflict.targetIndex]?.name : "" - }".`} - description="Si continuás, ambas identidades se combinarán en una sola." - onConfirm={confirmPeopleMerge} - onCancel={finishEditing} - /> -``` - -- [ ] **Step 6: Drop the `Popover`/`PopoverAnchor` wrapper around the Acciones buttons** - -Replace the `Section heading="Acciones"` body — remove the `Popover`/`PopoverAnchor` wrapper, keep the plain `actions` div, and move its confirm out to a `ConfirmDialog`: - -```tsx -
-
- - - Unir con el anterior - - - - Unir con el siguiente - - - - Agregar debajo - - - - Eliminar - -
- { - if (!open) setConfirm(null); - }} - title={confirm?.title ?? ""} - description={confirm?.description ?? ""} - onConfirm={() => confirm?.onConfirm()} - onCancel={() => setConfirm(null)} - /> -
-``` - -- [ ] **Step 7: Typecheck and lint** - -```bash -pnpm typecheck -pnpm biome check src/components/side-panel/SidePanel.tsx -``` - -Expected: PASS. If `PopoverContent`'s `showArrow` prop or the `Popover`/`PopoverAnchor` imports are now unused anywhere else in the file, biome's `organizeImports` will flag/remove them — confirm the diff only removes what Step 2 intended. - -- [ ] **Step 8: Verify in Storybook** - -Run `pnpm storybook`, open `Components/SidePanel`: -- `RenameAndCollision`: repeat the Step 1 repro — confirm the confirmation now renders as a centered card with a dark backdrop over the whole story canvas, not anchored to the pill. Confirm "Combinar" calls `onMergePeople` (watch `lastAction` text update) and "Cancelar" (or clicking the backdrop) dismisses without changes. -- `MergeConfirmation`: click "Unir con el anterior" / "Unir con el siguiente" — confirm the same centered-modal treatment, correct copy per direction, and that dismissing via backdrop click behaves like Cancelar (no `onMergePrevious`/`onMergeNext` call). -- `Default`/`InvalidTimestamp`: confirm unaffected (no confirm dialogs involved). - -- [ ] **Step 9: Commit** - -```bash -git add src/components/side-panel/SidePanel.tsx -git commit -m "fix(side-panel): render merge/rename confirmations as a centered modal, not an anchored popover" -``` - ---- - -## Final Verification - -- [ ] `pnpm typecheck` — PASS -- [ ] `pnpm biome check` (full repo) — PASS -- [ ] `pnpm build` — PASS (confirms the library still builds/exports cleanly for consumers pinned to a git tag) -- [ ] Manual Storybook pass over `Components/Toolbar` and `Components/SidePanel`, all stories, per Steps 4 and 8 above -- [ ] Push the branch and open a PR referencing the two upstream reports (search width in `search-switch`, and the rename/merge confirmation not matching Figma node `40002384:38487`) — do not merge without review, this library is consumed by a pinned tag (`v0.4.1`) from `desktop-app`