From 0fd95c15a1c726f9dfa15678e06df116bd535090 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Wed, 29 Jul 2026 01:05:58 +0200 Subject: [PATCH] fix(channels): stable tag column width, no popover flash, no sync wrap (R8 S1 UAT) Three UAT refinements on the tag cell + popover platform: - Fixed tag-column width (w-[15rem], was max-w): under table-layout:auto a content- driven cell re-solved every column's width on each chip added, so the table visibly shifted (and the "+N" count flickered) until the cap was hit. A fixed footprint makes adding/removing tags move nothing (verified: column x unchanged, "+N" count stable). - No popover flash: gate the floating surface's visibility on Floating UI's isPositioned so it never paints at (0,0) top-left for a frame before the transform lands. - Sync cell no longer wraps: flex-nowrap so the recent/full badges stay on one line (the column grows to fit them instead of breaking to two rows). --- frontend/src/components/AnchoredPopover.tsx | 23 ++++++++++++++++----- frontend/src/components/ChannelTags.tsx | 10 +++++---- frontend/src/components/Channels.tsx | 2 +- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/AnchoredPopover.tsx b/frontend/src/components/AnchoredPopover.tsx index 3422f9f7..8f112d63 100644 --- a/frontend/src/components/AnchoredPopover.tsx +++ b/frontend/src/components/AnchoredPopover.tsx @@ -64,7 +64,7 @@ export function useAnchoredPopover(opts: AnchoredPopoverOptions) { // null when not inside a — the hook still works, just without nested-layer awareness. const nodeId = useFloatingNodeId(); - const { refs, floatingStyles, context, middlewareData } = useFloating({ + const { refs, floatingStyles, context, middlewareData, isPositioned } = useFloating({ nodeId, open: opts.open, onOpenChange: opts.onOpenChange, @@ -108,6 +108,7 @@ export function useAnchoredPopover(opts: AnchoredPopoverOptions) { refs, floatingStyles, context, + isPositioned, referenceHidden: middlewareData.hide?.referenceHidden ?? false, ...interactions, }; @@ -134,8 +135,16 @@ export function PopoverSurface({ focusManaged?: boolean; initialFocus?: number | React.MutableRefObject; }) { - const { context, refs, floatingStyles, getFloatingProps, nodeId, referenceHidden, open } = - popover; + const { + context, + refs, + floatingStyles, + getFloatingProps, + nodeId, + referenceHidden, + isPositioned, + open, + } = popover; const mergedRef = useMergeRefs([refs.setFloating, floatingRef ?? null]); const floatingProps = getFloatingProps(); @@ -149,8 +158,12 @@ export function PopoverSurface({ style={{ ...(floatingProps.style as CSSProperties | undefined), ...floatingStyles, - // Trigger scrolled out of its clipping scroller → hide-and-follow rather than float over chrome. - ...(referenceHidden ? { visibility: "hidden", pointerEvents: "none" } : null), + // Hide until (a) the first position is measured — else it flashes at top-left (0,0) before + // the transform lands — and (b) while the trigger is scrolled out of its clipping scroller + // (hide-and-follow rather than float over the chrome). + ...(!isPositioned || referenceHidden + ? { visibility: "hidden", pointerEvents: "none" } + : null), ...style, }} > diff --git a/frontend/src/components/ChannelTags.tsx b/frontend/src/components/ChannelTags.tsx index 19d4c3d0..cade20b0 100644 --- a/frontend/src/components/ChannelTags.tsx +++ b/frontend/src/components/ChannelTags.tsx @@ -190,10 +190,12 @@ export function ChannelTagsControl({ const fade = useScrollFade(); return ( - // max-w caps the cell's MAX-CONTENT so `table-layout:auto` can't ignore the column width and let - // an over-tagged row stretch the whole table (the documented channel/About-column trap). The chip - // row then greedily fits within it and collapses the rest into "+N". -
+ // FIXED width (not just max-w): under `table-layout:auto` a content-driven cell re-solves the + // whole table's column widths every time a chip is added — the table visibly shifts until the cap + // is hit. A fixed width makes the cell's footprint constant, so adding/removing tags never moves + // any column; the chip row greedily fits within it and collapses the rest into "+N". (max-w-full + // lets a narrower card shrink it.) +
{attached.slice(0, visible).map((tg) => ( +