Merge pull request #141 from unslothai/feature/uxui-heuristics

style: improve layout consistency and responsiveness across components
This commit is contained in:
Wasim Yousef Said 2026-02-17 12:34:37 -08:00 committed by GitHub
commit 43fac18d40
14 changed files with 163 additions and 47 deletions

View file

@ -112,7 +112,10 @@ function ModelSelectorContent({
<PopoverContent
align="start"
data-tour={dataTour}
className={cn("w-[440px] min-w-[440px] gap-0 p-2", className)}
className={cn(
"w-[min(440px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] min-w-0 gap-0 p-2",
className,
)}
>
<Tabs defaultValue="hub" className="w-full">
<TabsList className="mb-2 w-full">

View file

@ -3,6 +3,13 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
import {
AiChat02Icon,
@ -29,6 +36,7 @@ const NAV_ITEMS = [
export function Navbar() {
const pathname = useRouterState({ select: (s) => s.location.pathname });
const [logoHovered, setLogoHovered] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const tourId =
pathname === "/studio"
@ -39,9 +47,16 @@ export function Navbar() {
? "export"
: null;
const openTour = () => {
if (!tourId) return;
window.dispatchEvent(
new CustomEvent(TOUR_OPEN_EVENT, { detail: { id: tourId } }),
);
};
return (
<header className="top-0 z-40 h-16 w-full">
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-6">
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-4 sm:px-6">
{/* Left: logo */}
<div
className="relative flex items-center gap-2.5 cursor-pointer select-none"
@ -55,7 +70,7 @@ export function Navbar() {
animate={{ rotate: logoHovered ? 360 : 0 }}
transition={{ duration: 0.5, ease: [0.165, 0.84, 0.44, 1] }}
/>
<span className="text-2xl font-bold tracking-wide font-heading">
<span className="text-xl font-bold tracking-wide font-heading sm:text-2xl">
unsloth
</span>
<AnimatePresence>
@ -76,7 +91,7 @@ export function Navbar() {
{/* Center: pill nav */}
<nav
data-tour="navbar"
className="flex items-center rounded-full border border-border bg-card p-1 ring-1 ring-foreground/5"
className="hidden items-center rounded-full border border-border bg-card p-1 ring-1 ring-foreground/5 md:flex"
>
{NAV_ITEMS.map((item) => {
const active = pathname === item.href;
@ -138,8 +153,8 @@ export function Navbar() {
})}
</nav>
{/* Right: docs link */}
<div className="flex items-center gap-2">
{/* Right: docs/tour desktop */}
<div className="hidden items-center gap-2 md:flex">
<HoverCard openDelay={200} closeDelay={100}>
<HoverCardTrigger asChild={true}>
<a
@ -177,11 +192,7 @@ export function Navbar() {
{tourId ? (
<button
type="button"
onClick={() => {
window.dispatchEvent(
new CustomEvent(TOUR_OPEN_EVENT, { detail: { id: tourId } }),
);
}}
onClick={openTour}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Tour"
>
@ -189,6 +200,77 @@ export function Navbar() {
</button>
) : null}
</div>
{/* Right: mobile */}
<div className="flex items-center gap-2 md:hidden">
{tourId ? (
<button
type="button"
onClick={openTour}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Tour"
>
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
</button>
) : null}
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger asChild={true}>
<button
type="button"
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground"
aria-label="Open navigation menu"
>
Menu
</button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px] p-4">
<SheetHeader>
<SheetTitle>Navigate</SheetTitle>
</SheetHeader>
<div className="mt-6 flex flex-col gap-2">
{NAV_ITEMS.filter((item) => item.enabled).map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
to={item.href}
onClick={() => setMobileOpen(false)}
className={cn(
"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",
)}
>
{item.label}
</Link>
);
})}
<a
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"
onClick={() => setMobileOpen(false)}
>
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"
onClick={() => {
openTour();
setMobileOpen(false);
}}
>
Start tour
</button>
) : null}
</div>
</SheetContent>
</Sheet>
</div>
</div>
</header>
);

View file

@ -4,7 +4,7 @@ import { AuthForm } from "./components/auth-form";
export function LoginPage() {
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-6 py-10 md:px-10">
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-8 sm:px-6 sm:py-10 md:px-10">
<LightRays
count={6}
color="rgba(34, 197, 94, 0.25)"
@ -13,7 +13,7 @@ export function LoginPage() {
length="70vh"
style={{ opacity: 0.4 }}
/>
<Card className="relative z-10 w-full max-w-sm px-6 py-8 shadow-border ring-1 ring-border">
<Card className="relative z-10 w-full max-w-sm px-5 py-6 shadow-border ring-1 ring-border sm:px-6 sm:py-8">
<AuthForm mode="login" />
</Card>
</div>

View file

@ -4,7 +4,7 @@ import { AuthForm } from "./components/auth-form";
export function SignupPage() {
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-6 py-10 md:px-10">
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-8 sm:px-6 sm:py-10 md:px-10">
<LightRays
count={6}
color="rgba(34, 197, 94, 0.25)"
@ -13,7 +13,7 @@ export function SignupPage() {
length="70vh"
style={{ opacity: 0.4 }}
/>
<Card className="relative z-10 w-full max-w-sm px-6 py-8 shadow-border ring-1 ring-border">
<Card className="relative z-10 w-full max-w-sm px-5 py-6 shadow-border ring-1 ring-border sm:px-6 sm:py-8">
<AuthForm mode="signup" />
</Card>
</div>

View file

@ -6,6 +6,13 @@ import {
import { Thread } from "@/components/assistant-ui/thread";
import { Button } from "@/components/ui/button";
import { SidebarProvider, SidebarTrigger, useSidebar } from "@/components/ui/sidebar";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
@ -127,7 +134,10 @@ const CompareContent = memo(function CompareContent({
return (
<CompareHandlesProvider handlesRef={handlesRef}>
<div className="flex min-h-0 flex-1 flex-col">
<div data-tour="chat-compare-view" className="grid min-h-0 flex-1 grid-cols-2 px-0">
<div
data-tour="chat-compare-view"
className="grid min-h-0 flex-1 grid-cols-1 px-0 md:grid-cols-2"
>
<div className="flex min-h-0 flex-col">
<div className="px-3 py-1.5">
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
@ -145,8 +155,8 @@ const CompareContent = memo(function CompareContent({
</ChatRuntimeProvider>
</div>
</div>
<div className="flex min-h-0 flex-col">
<div className="text-end px-3 py-1.5">
<div className="flex min-h-0 flex-col border-t border-border/60 md:border-t-0 md:border-l">
<div className="px-3 py-1.5 text-start md:text-end">
<span className="text-[10px] font-semibold uppercase tracking-wider text-primary">
Fine-tuned (LoRA)
</span>
@ -178,8 +188,23 @@ function InlineSidebar({
children: ReactNode;
side?: "left" | "right";
}) {
const { state } = useSidebar();
const { state, isMobile, openMobile, setOpenMobile } = useSidebar();
const collapsed = state === "collapsed";
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
<SheetContent side={side} className="w-[18rem] p-0">
<SheetHeader className="sr-only">
<SheetTitle>Chat sidebar</SheetTitle>
<SheetDescription>Chat threads and actions</SheetDescription>
</SheetHeader>
<div className="h-full overflow-auto">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group shrink-0 h-full"
@ -447,13 +472,13 @@ export function ChatPage(): ReactElement {
}, [modelSelectorLocked, tour.open]);
return (
<div className="h-[calc(100vh-4rem)] bg-background overflow-hidden">
<div className="h-[calc(100dvh-4rem)] bg-background overflow-hidden">
<GuidedTour {...tour.tourProps} />
<SidebarProvider
defaultOpen={true}
open={sidebarOpen}
onOpenChange={setSidebarOpen}
className="!min-h-0 h-full max-w-7xl mx-auto px-4"
className="!min-h-0 h-full w-full max-w-7xl mx-auto px-2 sm:px-4"
style={
{
"--sidebar-width": "14rem",
@ -472,7 +497,7 @@ export function ChatPage(): ReactElement {
</InlineSidebar>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="flex h-11 shrink-0 items-center px-2">
<div className="flex h-11 shrink-0 items-center px-1.5 sm:px-2">
<div className="flex items-center gap-1">
<SidebarTrigger />
<TopBarActions
@ -491,6 +516,7 @@ export function ChatPage(): ReactElement {
onOpenChange={handleModelSelectorOpenChange}
triggerDataTour="chat-model-selector"
contentDataTour="chat-model-selector-popover"
className="max-w-[62vw] sm:max-w-none"
/>
</div>
{modelsError && (

View file

@ -237,7 +237,7 @@ export function ExportPage() {
// ---- Render ----
return (
<div className="min-h-screen bg-background">
<main className="mx-auto max-w-7xl px-6 py-4">
<main className="mx-auto max-w-7xl px-4 py-4 sm:px-6">
<GuidedTour {...tour.tourProps} />
<div className="mb-8 flex flex-col gap-0.5">
@ -275,7 +275,7 @@ export function ExportPage() {
{!loadingCheckpoints && !checkpointError && (
<>
{/* Top row: Dropdowns + metadata | Guide */}
<div className="grid grid-cols-2 gap-8">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8">
<div className="flex flex-col gap-4">
{/* Training run dropdown */}
<div data-tour="export-training-run" className="flex flex-col gap-2">
@ -410,7 +410,7 @@ export function ExportPage() {
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
Training Info
</span>
<div className="grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
<div className="grid grid-cols-1 gap-x-6 gap-y-1.5 text-xs sm:grid-cols-2">
<div className="flex justify-between">
<span className="text-muted-foreground">Base Model</span>
<span className="font-medium">{baseModelName}</span>

View file

@ -31,19 +31,19 @@ export function WizardContent() {
return (
<main className="flex-1 flex flex-col overflow-y-auto">
<header className="flex items-center gap-4 p-6 pb-4">
<img src={mascotSrc} alt="Unsloth mascot" className="size-14" />
<header className="flex flex-wrap items-start gap-3 p-4 pb-3 sm:p-6 sm:pb-4">
<img src={mascotSrc} alt="Unsloth mascot" className="size-12 sm:size-14" />
<div className="flex flex-col min-w-0">
<h1 className="text-xl font-semibold">{stepConfig.title}</h1>
<h1 className="text-lg font-semibold sm:text-xl">{stepConfig.title}</h1>
<p className="text-sm text-muted-foreground">
{stepConfig.description}
</p>
</div>
<p className="ml-auto shrink-0 text-xs text-muted-foreground uppercase tracking-wider">
<p className="ml-auto hidden shrink-0 text-xs text-muted-foreground uppercase tracking-wider md:block">
Step {currentStep} of {STEPS.length}
</p>
</header>
<div className="flex-1 p-6 pt-2">
<div className="flex-1 p-4 pt-1.5 sm:p-6 sm:pt-2">
<StepComponent />
</div>
</main>

View file

@ -54,7 +54,7 @@ export function WizardLayout() {
}, [isFinalStep]);
return (
<div className="relative min-h-screen flex items-center justify-center p-8 bg-gradient-to-br from-primary/5 via-background to-primary/3 overflow-hidden">
<div className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-primary/5 via-background to-primary/3 p-4 sm:p-6 md:p-8">
{showSplash && (
<SplashScreen
onStartOnboarding={() => setShowSplash(false)}
@ -81,7 +81,7 @@ export function WizardLayout() {
ease: [0.165, 0.84, 0.44, 1],
}}
>
<Card className="relative z-10 h-[660px] w-full !gap-0 !m-0 !p-0 flex flex-row overflow-hidden shadow-border ring-1 ring-border">
<Card className="relative z-10 w-full !gap-0 !m-0 !p-0 flex min-h-[560px] flex-col overflow-hidden shadow-border ring-1 ring-border md:min-h-[620px] md:flex-row lg:h-[660px]">
<WizardSidebar />
<div className="flex-1 flex flex-col">
<WizardContent />

View file

@ -8,8 +8,8 @@ export function WizardSidebar() {
const progress = ((currentStep - 1) / (STEPS.length - 1)) * 100;
return (
<aside className="w-64 flex flex-col gap-4 p-6 shrink-0 bg-muted/70">
<div className="flex items-center gap-3 py-2">
<aside className="w-full shrink-0 bg-muted/70 p-4 md:w-64 md:p-6">
<div className="flex items-center gap-3 py-1 md:py-2">
<img
src="https://unsloth.ai/cgi/image/unsloth_sticker_no_shadow_ldN4V4iydw00qSIIWDCUv.png?width=96&quality=80&format=auto"
alt="Unsloth"
@ -20,8 +20,13 @@ export function WizardSidebar() {
<span className="text-xs text-muted-foreground">Studio</span>
</div>
</div>
<Progress value={progress} className="h-1.5" />
<nav className="flex flex-col gap-1">
<div className="mt-3 md:mt-0">
<Progress value={progress} className="h-1.5" />
</div>
<p className="mt-2 text-xs text-muted-foreground md:hidden">
Step {currentStep} of {STEPS.length}
</p>
<nav className="mt-3 hidden flex-col gap-1 md:flex">
{STEPS.map((step) => (
<WizardStepItem key={step.number} step={step} />
))}

View file

@ -121,13 +121,13 @@ export function DatasetSection() {
);
return (
<div data-tour="studio-dataset" className="lg:col-span-4">
<div data-tour="studio-dataset" className="col-span-1 xl:col-span-4">
<SectionCard
icon={<HugeiconsIcon icon={Database02Icon} className="size-5" />}
title="Dataset"
description="Select or upload training data"
accent="indigo"
className="min-h-[450px]"
className="md:min-h-[450px]"
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">

View file

@ -169,7 +169,7 @@ export function ModelSection() {
);
return (
<div data-tour="studio-model" className="col-span-12">
<div data-tour="studio-model" className="col-span-1 md:col-span-2 xl:col-span-12">
<SectionCard
icon={<HugeiconsIcon icon={ChipIcon} className="size-5" />}
title="Model"
@ -179,7 +179,7 @@ export function ModelSection() {
badge="2x Faster Training"
className="shadow-border ring-1 ring-border"
>
<div className="grid gap-4 lg:grid-cols-4">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div data-tour="studio-local-model" className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Local Model

View file

@ -116,13 +116,13 @@ export function ParamsSection(): ReactElement {
const epochsSliderMax = Math.max(20, store.epochs, 1);
return (
<div data-tour="studio-params" className="lg:col-span-4">
<div data-tour="studio-params" className="col-span-1 xl:col-span-4">
<SectionCard
icon={<HugeiconsIcon icon={Settings04Icon} className="size-5" />}
title="Parameters"
description="Configure training hyperparameters"
accent="orange"
className="min-h-[450px]"
className="md:min-h-[450px]"
>
<div className="flex flex-col gap-4">
{/* Max Steps */}

View file

@ -43,13 +43,13 @@ export function TrainingSection() {
return (
<div data-tour="studio-training" className="lg:col-span-4">
<div data-tour="studio-training" className="col-span-1 xl:col-span-4">
<SectionCard
icon={<HugeiconsIcon icon={ChartAverageIcon} className="size-5" />}
title="Training"
description="Monitor and control training"
accent="blue"
className="min-h-[450px]"
className="md:min-h-[450px]"
>
<div className="flex flex-col gap-4">
{/* Loss chart */}

View file

@ -72,7 +72,7 @@ export function StudioPage(): ReactElement {
length="62vh"
style={{ opacity: 0.45 }}
/>
<main className="relative z-10 mx-auto max-w-7xl px-6 py-4">
<main className="relative z-10 mx-auto max-w-7xl px-4 py-4 sm:px-6">
<GuidedTour {...tour.tourProps} celebrate={isConfigTour} />
<DatasetPreviewDialog
@ -103,7 +103,7 @@ export function StudioPage(): ReactElement {
</Button>
)}
<div className="mb-8 flex flex-col gap-0.5">
<div className="mb-6 flex flex-col gap-0.5 sm:mb-8">
<h1 className="text-2xl font-semibold tracking-tight">
Fine-tuning Studio
</h1>
@ -121,7 +121,7 @@ export function StudioPage(): ReactElement {
) : showTrainingView ? (
<TrainingView />
) : (
<div className="grid grid-cols-12 items-start gap-6">
<div className="grid grid-cols-1 items-start gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-12">
<ModelSection />
<DatasetSection />
<ParamsSection />