diff --git a/src/components/plugins/PluginToolbar.tsx b/src/components/plugins/PluginToolbar.tsx index 679c436..996ca4c 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 ( @@ -94,7 +95,7 @@ function PluginFiltersPopover({ type="button" variant="ghost" size="sm" - className="w-fit gap-2 self-center text-xs text-secondary-foreground" + className="w-fit shrink-0 gap-2 text-xs text-secondary-foreground" aria-label={intl.formatMessage( { id: "plugins.catalog.filtersActive" }, { count: activeFilterCount }, @@ -109,8 +110,14 @@ function PluginFiltersPopover({ )} - -
+ {/* End-aligned because the trigger sits at the toolbar's right edge, where a + start-aligned panel would expand past the viewport. */} + +

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

@@ -121,7 +128,7 @@ function PluginFiltersPopover({ )}
-
+
@@ -148,28 +155,46 @@ 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. + + Container queries measure the panel's content box, which is 2rem + of padding and 2px of border narrower than the widths set above: + 18/24/32rem of panel leave 15.875/21.875/29.875rem to query. The + thresholds have to sit inside those, so they read a step lower + than the panel width that triggers them. */} +
{availableTags.map((tag, index) => { const checkboxId = `${id}-tag-${index}`; return ( -
+
onToggleTag(tag, checked === true)} /> -
); })}
-
+
)}
@@ -189,14 +214,14 @@ export function PluginToolbar({
-
+
diff --git a/src/components/server-catalog/CatalogToolbar.tsx b/src/components/server-catalog/CatalogToolbar.tsx index b7acf77..d7f4fad 100644 --- a/src/components/server-catalog/CatalogToolbar.tsx +++ b/src/components/server-catalog/CatalogToolbar.tsx @@ -229,7 +229,7 @@ function CatalogFiltersDialog({ type="button" variant="ghost" size="sm" - className="w-fit gap-2 self-center text-xs text-secondary-foreground" + className="w-fit shrink-0 gap-2 text-xs text-secondary-foreground" aria-label={intl.formatMessage( { id: "mcpServer.catalog.filtersActive" }, { count: activeFilterCount }, @@ -324,14 +324,14 @@ export function CatalogToolbar({
-
+
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index 663d965..1984348 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. + "scrollbar-thin 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/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 271ece8..8e41718 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 (