From 2b70ba7a1f68d8e5d3e91000c549a97a910dab3c Mon Sep 17 00:00:00 2001 From: npeter83 Date: Wed, 29 Jul 2026 01:50:58 +0200 Subject: [PATCH] =?UTF-8?q?fix(channels):=20tag-cell=20review=20follow-ups?= =?UTF-8?q?=20=E2=80=94=20rename-aware=20fit,=20measured=20gap,=20close=20?= =?UTF-8?q?reveal-all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the /code-review findings on the tag cell: - key the overflow-fit measurement on tag id+name so a RENAME (which changes chip width) re-runs the fit, not just add/remove; - reserve the '+N' pill space using the MEASURED gap (gap-1 is rem, scales with the font slider) instead of a px constant; - force the reveal-all hover popover's state closed when nothing is collapsed, so it can't linger open against an unmounted '+N' trigger. --- frontend/src/components/ChannelTags.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ChannelTags.tsx b/frontend/src/components/ChannelTags.tsx index 5c70a382..b2a3cb97 100644 --- a/frontend/src/components/ChannelTags.tsx +++ b/frontend/src/components/ChannelTags.tsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Check, Plus } from "lucide-react"; import { useTranslation } from "react-i18next"; import { type Tag } from "../lib/api"; @@ -146,7 +146,8 @@ export function ChannelTagsControl({ const rowRef = useRef(null); const measureRef = useRef(null); const [visible, setVisible] = useState(attached.length); - const idsKey = attached.map((tg) => tg.id).join(","); + // Key on id AND name so a RENAME (which changes a chip's width) re-runs the fit, not just add/remove. + const tagsKey = attached.map((tg) => `${tg.id}:${tg.name}`).join("|"); useLayoutEffect(() => { const row = rowRef.current; @@ -158,7 +159,12 @@ export function ChannelTagsControl({ if (!pill) return; const pillW = pill.offsetWidth; const chips = nodes.slice(0, -1); - const GAP = 4; // gap-1 + // Measure the real rendered gap (gap-1 is rem — it scales with the font slider) off the box + // between the last chip and the sample pill, rather than assuming a px constant. + const lastChip = chips[chips.length - 1]; + const gap = lastChip + ? Math.max(0, pill.offsetLeft - (lastChip.offsetLeft + lastChip.offsetWidth)) + : 0; const avail = row.clientWidth; let count = chips.length; for (let i = 0; i < chips.length; i++) { @@ -166,7 +172,7 @@ export function ChannelTagsControl({ if (!chip) break; const right = chip.offsetLeft + chip.offsetWidth; // right edge within the measurer const moreAfter = i < chips.length - 1; - if (right > avail - (moreAfter ? pillW + GAP : 0)) { + if (right > avail - (moreAfter ? pillW + gap : 0)) { count = i; break; } @@ -178,7 +184,7 @@ export function ChannelTagsControl({ const ro = new ResizeObserver(compute); ro.observe(row); return () => ro.disconnect(); - }, [idsKey]); + }, [tagsKey]); const hidden = attached.length - visible; @@ -197,6 +203,11 @@ export function ChannelTagsControl({ mode: "hover", placement: "bottom", }); + // If tags change so nothing is collapsed anymore, the "+N" trigger unmounts — force the reveal-all + // popover's state closed so it can't linger open against a gone reference. + useEffect(() => { + if (hidden === 0 && overflowOpen) setOverflowOpen(false); + }, [hidden, overflowOpen]); const pillClass = "shrink-0 text-[10px] px-1.5 py-0.5 rounded-full bg-card text-muted border border-border";