From dbda5f02e3e60632b9f9517d64a4436dd6af42a1 Mon Sep 17 00:00:00 2001 From: Anna Effort Date: Mon, 17 Aug 2026 16:18:46 -0700 Subject: [PATCH 1/9] fix(plugins): keep the filters popover inside the viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The filters panel was a fixed w-80 with a height-capped tag list, so a catalog with many tags scrolled a 160px box while the panel itself had room to grow. Widen it in steps and let the tag list take the remaining height, then clamp the panel to what Radix measured between the trigger and the viewport edge (--radix-popover-content-available-height, plus collisionPadding) so it can never run off screen at any width. The tag list moves from a fieldset to role=group: a rendered legend is not subtracted from the height flex assigns its fieldset, so the scroll box overflowed it. It also lays out as a grid rather than CSS columns, because a height-capped multi-column box overflows sideways into new columns instead of scrolling down. Tag labels fill their row so the tap target clears 44px on touch. The popover primitive gains the clamp for every consumer, which is the right default — HeaderQuickNav, VisibilityInfoPopover, TeamsTable and TokensTable are all narrow enough to be unaffected today. scrollbar-thin gives the tag list a transparent track so the bar does not paint over the rounded corners of the box it scrolls in. Signed-off-by: Anna Effort --- src/components/plugins/PluginToolbar.tsx | 39 ++++++++++++++++++------ src/components/ui/popover.tsx | 6 +++- src/index.css | 7 +++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/plugins/PluginToolbar.tsx b/src/components/plugins/PluginToolbar.tsx index 679c436..9fab7a2 100644 --- a/src/components/plugins/PluginToolbar.tsx +++ b/src/components/plugins/PluginToolbar.tsx @@ -86,6 +86,7 @@ function PluginFiltersPopover({ const id = useId(); const filtersTitleId = `${id}-title`; const hookTriggerId = `${id}-hook`; + const tagsLabelId = `${id}-tags-label`; return ( @@ -109,8 +110,14 @@ function PluginFiltersPopover({ )} - -
+ {/* Start-aligned because the wrapped toolbar puts this trigger at the left + of the content area, where an end-aligned panel expands over the nav. */} + +

{intl.formatMessage({ id: "plugins.catalog.filters" })}

@@ -121,7 +128,7 @@ function PluginFiltersPopover({ )}
-
+
@@ -148,28 +155,40 @@ function PluginFiltersPopover({
{availableTags.length > 0 && ( -
- + // role=group rather than fieldset: a rendered legend is not subtracted + // from the height flex assigns its fieldset, so the box overflows it. +
+ {intl.formatMessage({ id: "plugins.catalog.tags" })} - -
+ + {/* A grid rather than CSS columns: a height-capped multi-column box + overflows sideways into new columns instead of scrolling down. */} +
{availableTags.map((tag, index) => { const checkboxId = `${id}-tag-${index}`; return ( -
+
onToggleTag(tag, checked === true)} /> -
); })}
-
+
)}
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index 663d965..0561d9e 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -15,6 +15,7 @@ function PopoverContent({ className, align = "center", sideOffset = 4, + collisionPadding = 16, ...props }: React.ComponentProps) { return ( @@ -23,8 +24,11 @@ function PopoverContent({ data-slot="popover-content" align={align} sideOffset={sideOffset} + collisionPadding={collisionPadding} className={cn( - "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", + // Clamped to the space Radix measured between the trigger and the + // viewport edge, so a wide or tall panel can never run off screen. + "z-50 max-h-(--radix-popover-content-available-height) w-72 max-w-[calc(100vw-2rem)] origin-(--radix-popover-content-transform-origin) overflow-y-auto rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className, )} {...props} diff --git a/src/index.css b/src/index.css index a414659..b3e4364 100644 --- a/src/index.css +++ b/src/index.css @@ -186,6 +186,13 @@ /* Status-headline icon accent (brand cyan) — bright on the dark background. */ --status-icon: #70f9ff; } +/* A transparent track so the scrollbar doesn't paint a solid bar over the + rounded corners of the box it scrolls in. */ +@utility scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: var(--muted-foreground) transparent; +} + /* ---break--- */ From 5219d9fb2b018a70de5cb1b5534199968e20bd59 Mon Sep 17 00:00:00 2001 From: Anna Effort Date: Mon, 17 Aug 2026 16:19:28 -0700 Subject: [PATCH 2/9] fix(layout): collapse the sidebar at lg and declare color-scheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docked sidebar took 16rem out of a 768px viewport, leaving list toolbars roughly 30rem to lay out in — enough that content had to wrap across the whole md range. Move the dock to lg so tablet widths get the sheet instead, which is the arrangement they had room for anyway. useIsMobile takes the breakpoint as a parameter so the sidebar can ask for 1024 without moving the 768 default every other caller relies on. SIDEBAR_COLLAPSE_BREAKPOINT has to stay in step with the lg: utilities that show the docked sidebar, or the sheet and the dock render at once. color-scheme tells the browser which palette to paint native UI with — scrollbars, form controls, the caret. Without it the .dark class swaps our custom properties but leaves that chrome light, which is most visible on the scrollbars the sidebar and page share. Signed-off-by: Anna Effort --- src/components/ui/sidebar.tsx | 11 +++++++---- src/hooks/use-mobile.ts | 10 +++++----- src/index.css | 7 +++++++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 271ece8..7a625b5 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -24,6 +24,9 @@ const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_KEYBOARD_SHORTCUT = "b"; +// Collapses to a sheet below Tailwind's `lg`. Must stay in step with the `lg:` +// utilities that show the docked sidebar, or both would render at once. +const SIDEBAR_COLLAPSE_BREAKPOINT = 1024; type SidebarContextProps = { state: "expanded" | "collapsed"; @@ -59,7 +62,7 @@ function SidebarProvider({ open?: boolean; onOpenChange?: (open: boolean) => void; }) { - const isMobile = useIsMobile(); + const isMobile = useIsMobile(SIDEBAR_COLLAPSE_BREAKPOINT); const [openMobile, setOpenMobile] = React.useState(false); // This is the internal state of the sidebar. @@ -197,7 +200,7 @@ function Sidebar({ return (