fix(studio): mobile navbar layout and chat settings sheet (#4458)

* fix(studio): mobile navbar layout and chat settings sheet

* fix(studio): portal select dropdowns inside sheet modal subtree
This commit is contained in:
Wasim Yousef Said 2026-03-19 10:04:53 +01:00 committed by GitHub
commit 6c2bfebb20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 73 additions and 24 deletions

View file

@ -209,7 +209,7 @@ export function Navbar() {
</div>
{/* Right: mobile */}
<div className="flex items-center gap-2 md:hidden">
<div className="col-start-3 flex items-center gap-2 justify-self-end md:hidden">
{tourId ? (
<button
type="button"
@ -245,8 +245,9 @@ export function Navbar() {
return (
<span
key={item.href}
className="rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground/40 cursor-not-allowed"
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground/40 cursor-not-allowed"
>
<HugeiconsIcon icon={item.icon} className="size-4" />
{item.label}
</span>
);
@ -257,12 +258,13 @@ export function Navbar() {
to={item.href}
onClick={() => setMobileOpen(false)}
className={cn(
"rounded-md border px-3 py-2 text-sm font-medium",
"flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium",
active
? "border-foreground bg-foreground text-background"
: "border-border text-foreground hover:bg-accent",
)}
>
<HugeiconsIcon icon={item.icon} className="size-4" />
{item.label}
</Link>
);
@ -271,23 +273,33 @@ export function Navbar() {
href="https://unsloth.ai/docs"
target="_blank"
rel="noopener noreferrer"
className="mt-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-accent"
className="mt-2 flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-accent"
onClick={() => setMobileOpen(false)}
>
<HugeiconsIcon icon={Book03Icon} className="size-4" />
Learn more (Docs)
</a>
{tourId ? (
<button
type="button"
className="rounded-md border border-border px-3 py-2 text-left text-sm font-medium text-foreground hover:bg-accent"
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-left text-sm font-medium text-foreground hover:bg-accent"
onClick={() => {
openTour();
setMobileOpen(false);
}}
>
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
Start tour
</button>
) : null}
<div className="mt-2 flex items-center justify-between rounded-md border border-border px-3 py-2">
<span className="text-sm font-medium text-foreground">Theme</span>
<AnimatedThemeToggler
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground [&_svg]:size-4"
title="Toggle theme"
aria-label="Toggle theme"
/>
</div>
</div>
</SheetContent>
</Sheet>

View file

@ -189,6 +189,7 @@ export {
DialogHeader,
DialogOverlay,
DialogPortal,
DialogPortalContainerContext,
DialogTitle,
DialogTrigger,
};

View file

@ -3,8 +3,10 @@
import { Dialog as SheetPrimitive } from "radix-ui";
import type * as React from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { DialogPortalContainerContext } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@ -69,6 +71,7 @@ function SheetContent({
overlayClassName?: string;
overlayPosition?: "fixed" | "absolute";
}) {
const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null);
return (
<SheetPortal container={container ?? undefined}>
<SheetOverlay
@ -76,6 +79,7 @@ function SheetContent({
position={overlayPosition ?? position}
/>
<SheetPrimitive.Content
ref={setContentEl}
data-slot="sheet-content"
data-side={side}
className={cn(
@ -85,7 +89,9 @@ function SheetContent({
)}
{...props}
>
{children}
<DialogPortalContainerContext.Provider value={contentEl}>
{children}
</DialogPortalContainerContext.Provider>
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" asChild>
<Button

View file

@ -768,6 +768,7 @@ export function ChatPage(): ReactElement {
<ChatSettingsPanel
open={settingsOpen}
onOpenChange={setSettingsOpen}
params={inferenceParams}
onParamsChange={setInferenceParams}
autoTitle={autoTitle}

View file

@ -21,6 +21,14 @@ import {
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { AnimatePresence, motion } from "motion/react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { useIsMobile } from "@/hooks/use-mobile";
import type { ReactNode } from "react";
import { useState } from "react";
import {
@ -151,6 +159,7 @@ function CollapsibleSection({
interface ChatSettingsPanelProps {
open: boolean;
onOpenChange?: (open: boolean) => void;
params: InferenceParams;
onParamsChange: (params: InferenceParams) => void;
autoTitle: boolean;
@ -160,12 +169,14 @@ interface ChatSettingsPanelProps {
export function ChatSettingsPanel({
open,
onOpenChange,
params,
onParamsChange,
autoTitle,
onAutoTitleChange,
onReloadModel,
}: ChatSettingsPanelProps) {
const isMobile = useIsMobile();
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
const ggufContextLength = useChatRuntimeStore((s) => s.ggufContextLength);
const kvCacheDtype = useChatRuntimeStore((s) => s.kvCacheDtype);
@ -214,24 +225,21 @@ export function ChatSettingsPanel({
}
}
return (
<aside
className={`shrink-0 self-start h-[calc(100%-0.875rem)] overflow-hidden bg-muted/70 rounded-2xl corner-squircle transition-[width] duration-200 ease-linear ${open ? "w-[17rem] border-l border-sidebar-border/70" : "w-0"}`}
>
<div className="flex h-full w-[17rem] flex-col">
<div className="flex items-center gap-2 px-4 py-3">
<HugeiconsIcon
icon={PencilEdit01Icon}
className="size-4 text-muted-foreground/70"
/>
<span className="flex-1 text-base font-semibold tracking-tight">
Configuration
</span>
</div>
const settingsContent = (
<>
<div className="flex items-center gap-2 px-4 py-3">
<HugeiconsIcon
icon={PencilEdit01Icon}
className="size-4 text-muted-foreground/70"
/>
<span className="flex-1 text-base font-semibold tracking-tight">
Configuration
</span>
</div>
<div className="flex-1 overflow-y-auto px-1.5">
{/* mt-4 matches the Playground sidebar gap (SidebarHeader py-3 + SidebarGroup pt-1) */}
<div className="mt-4 px-2 pb-3">
<div className="flex-1 overflow-y-auto px-1.5">
{/* mt-4 matches the Playground sidebar gap (SidebarHeader py-3 + SidebarGroup pt-1) */}
<div className="mt-4 px-2 pb-3">
<div className="flex items-center gap-2">
<Select value={activePreset} onValueChange={applyPreset}>
<SelectTrigger className="h-8 flex-1 corner-squircle text-xs">
@ -434,7 +442,28 @@ export function ChatSettingsPanel({
<ChatTemplateSection onReloadModel={onReloadModel} />
</div>
</div>
</>
);
if (isMobile) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-[18rem] p-0">
<SheetHeader className="sr-only">
<SheetTitle>Configuration</SheetTitle>
<SheetDescription>Chat inference settings</SheetDescription>
</SheetHeader>
<div className="flex h-full flex-col">{settingsContent}</div>
</SheetContent>
</Sheet>
);
}
return (
<aside
className={`shrink-0 self-start h-[calc(100%-0.875rem)] overflow-hidden bg-muted/70 rounded-2xl corner-squircle transition-[width] duration-200 ease-linear ${open ? "w-[17rem] border-l border-sidebar-border/70" : "w-0"}`}
>
<div className="flex h-full w-[17rem] flex-col">{settingsContent}</div>
</aside>
);
}