studio: align composer/code, unify fonts, and remove tool collapse jitter (#4763)

- Add min-w-0 guards to thread/message/markdown containers to prevent
  content overflow past the composer width
- Unify chat typography from Hellix/Space Grotesk to the sans stack,
  keeping monospace for code blocks and inline code
- Restructure desktop navbar right-side controls with shrink-0 wrappers
  for consistent spacing across HoverCard roots
- Soften tool-call label styling (font-medium + text-foreground/85
  instead of bold)
- Add responsive code block sizing via @container queries
- Add horizontal scrolling for wide code blocks within the thread column
- Scope list-item code block alignment CSS to .aui-thread-root
- Preserve useScrollLock in tool-fallback and tool-group collapsibles
- Fall back to bg-background on ViewportFooter when hideComposer is true
- Widen inline code monospace selector to cover th, blockquote, and
  heading elements
- Remove unused @fontsource-variable/space-grotesk import
This commit is contained in:
Lee Jackson 2026-04-02 03:57:10 +01:00 committed by GitHub
commit ca4ea8b9fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 175 additions and 111 deletions

View file

@ -403,7 +403,7 @@ const MarkdownTextImpl = () => {
}
return (
<div data-status={status.type}>
<div data-status={status.type} className="min-w-0 max-w-full">
<Streamdown
mode="streaming"
isAnimating={status.type === "running"}

View file

@ -73,7 +73,7 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({
}}
>
<ThreadPrimitive.Viewport
className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
className="aui-thread-viewport relative flex min-w-0 flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
>
{!hideWelcome && (
<AuiIf condition={({ thread }) => thread.isEmpty}>
@ -89,7 +89,13 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({
}}
/>
<ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 z-20 mt-auto flex w-full flex-col gap-4 overflow-visible bg-background pb-4 md:pb-4">
<ThreadPrimitive.ViewportFooter className={cn(
"aui-thread-viewport-footer sticky bottom-0 z-20 mt-auto flex w-full flex-col gap-4 overflow-visible pb-4 md:pb-4",
hideComposer ? "bg-background" : "relative bg-transparent",
)}>
{!hideComposer && (
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-4 bg-background" aria-hidden />
)}
<ThreadScrollToBottom />
<AuiIf condition={({ thread }) => !thread.isEmpty}>
{!hideComposer && <ComposerAnimated />}
@ -220,14 +226,20 @@ const GeneratingSpinner: FC = () => {
const ComposerAnimated: FC = () => {
return (
<motion.div
layout={true}
layoutId="composer"
transition={{ type: "spring", bounce: 0.15, duration: 0.5 }}
className="mx-auto w-full max-w-(--thread-max-width)"
>
<Composer />
</motion.div>
<div className="relative mx-auto min-w-0 w-full max-w-(--thread-max-width)">
<div
className="pointer-events-none absolute inset-x-0 top-1/2 bottom-0 z-0 bg-background"
aria-hidden
/>
<motion.div
layout={true}
layoutId="composer"
transition={{ type: "spring", bounce: 0.15, duration: 0.5 }}
className="relative z-10 w-full"
>
<Composer />
</motion.div>
</div>
);
};
@ -262,7 +274,7 @@ const Composer: FC = () => {
<ToolStatusDisplay />
<ComposerPrimitive.Input
placeholder="Send a message..."
className="aui-composer-input mb-1 max-h-32 min-h-12 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0"
className="aui-composer-input mb-1 max-h-32 min-h-12 w-full resize-none bg-transparent pl-5 pr-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0"
rows={1}
autoFocus={true}
aria-label="Message input"
@ -574,10 +586,10 @@ const GeneratingIndicator: FC = () => {
const AssistantMessage: FC = () => {
return (
<MessagePrimitive.Root
className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto min-w-0 w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
data-role="assistant"
>
<div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
<div className="aui-assistant-message-content wrap-break-word min-w-0 text-foreground leading-relaxed">
<GeneratingIndicator />
<MessagePrimitive.Parts
components={{
@ -600,7 +612,7 @@ const AssistantMessage: FC = () => {
<MessageError />
</div>
<div className="aui-assistant-message-footer mt-1 ml-2 flex">
<div className="aui-assistant-message-footer mt-1 flex">
<BranchPicker />
<AssistantActionBar />
</div>

View file

@ -156,12 +156,12 @@ function ToolFallbackTrigger({
<span
data-slot="tool-fallback-trigger-label"
className={cn(
"aui-tool-fallback-trigger-label-wrapper relative inline-block grow text-left leading-none",
"aui-tool-fallback-trigger-label-wrapper relative inline-block grow text-left leading-none text-muted-foreground",
isCancelled && "text-muted-foreground line-through",
)}
>
<span>
{label}: <b>{toolName}</b>
{label}: <span className="font-medium text-foreground/85">{toolName}</span>
</span>
{isRunning && (
<span
@ -169,7 +169,7 @@ function ToolFallbackTrigger({
data-slot="tool-fallback-trigger-shimmer"
className="aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
>
{label}: <b>{toolName}</b>
{label}: <span className="font-medium text-foreground/85">{toolName}</span>
</span>
)}
</span>

View file

@ -372,90 +372,94 @@ export function Navbar() {
})}
</nav>
{/* Right: docs/tour desktop */}
<div className="hidden items-center justify-self-end gap-2 md:flex">
<AnimatedThemeToggler
className="flex h-9 w-9 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"
/>
<HoverCard openDelay={200} closeDelay={100}>
<HoverCardTrigger asChild={true}>
<a
href="https://unsloth.ai/docs"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm font-medium text-emerald-600 hover:text-emerald-700 transition-colors"
>
<HugeiconsIcon icon={Book03Icon} className="size-4" />
Learn more
</a>
</HoverCardTrigger>
<HoverCardContent align="end" className="w-80 p-0">
<a
href="https://unsloth.ai/docs"
target="_blank"
rel="noopener noreferrer"
className="group/card flex flex-col gap-1 p-4 no-underline"
>
<p className="text-sm font-semibold font-heading">
Unsloth Documentation
</p>
<p className="text-xs text-muted-foreground leading-relaxed">
Guides on fine-tuning LLMs 2x faster with 70% less memory.
Covers LoRA, QLoRA, data formatting, and deployment.
</p>
<span className="mt-1 flex items-center gap-1 text-xs font-medium text-emerald-600 group-hover/card:underline">
Visit docs
<HugeiconsIcon icon={ArrowRight01Icon} className="size-3" />
</span>
</a>
</HoverCardContent>
</HoverCard>
<button
type="button"
onClick={tourId ? openTour : undefined}
className={cn(
"flex h-9 items-center gap-1.5 rounded-md px-3 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
!tourId && "invisible pointer-events-none",
)}
title="Tour"
aria-hidden={!tourId}
tabIndex={tourId ? 0 : -1}
>
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
<span className="text-sm font-medium">Tour</span>
</button>
<HoverCard openDelay={200} closeDelay={100}>
<HoverCardTrigger asChild={true}>
{/* Right: docs/tour desktop — one wrapper per control so flex gap is even (HoverCard roots can confuse flex spacing). */}
<div className="hidden items-center justify-self-end gap-0 md:flex">
<div className="flex shrink-0 items-center">
<AnimatedThemeToggler
className="flex h-9 w-9 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 className="flex shrink-0 items-center">
<HoverCard openDelay={200} closeDelay={100}>
<HoverCardTrigger asChild={true}>
<a
href="https://unsloth.ai/docs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-9 items-center gap-1.5 rounded-md px-3 text-sm font-medium text-emerald-600 transition-colors hover:bg-accent hover:text-emerald-700 dark:hover:text-emerald-400"
>
<HugeiconsIcon icon={Book03Icon} className="size-4" />
Learn more
</a>
</HoverCardTrigger>
<HoverCardContent align="end" className="w-80 p-0">
<a
href="https://unsloth.ai/docs"
target="_blank"
rel="noopener noreferrer"
className="group/card flex flex-col gap-1 p-4 no-underline"
>
<p className="text-sm font-semibold font-heading">
Unsloth Documentation
</p>
<p className="text-xs text-muted-foreground leading-relaxed">
Guides on fine-tuning LLMs 2x faster with 70% less memory.
Covers LoRA, QLoRA, data formatting, and deployment.
</p>
<span className="mt-1 flex items-center gap-1 text-xs font-medium text-emerald-600 group-hover/card:underline">
Visit docs
<HugeiconsIcon icon={ArrowRight01Icon} className="size-3" />
</span>
</a>
</HoverCardContent>
</HoverCard>
</div>
{tourId ? (
<div className="flex shrink-0 items-center">
<button
type="button"
className="flex h-9 items-center gap-1.5 rounded-md px-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label="How to update Unsloth Studio"
onClick={openTour}
className="flex h-9 items-center gap-1.5 rounded-md px-3 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Tour"
>
<HugeiconsIcon icon={ArrowReloadHorizontalIcon} className="size-4" />
Update
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
<span className="text-sm font-medium">Tour</span>
</button>
</HoverCardTrigger>
<HoverCardContent align="end" className="w-[22.5rem] p-0">
<UpdateStudioInstructions
className="p-4"
defaultShell={defaultUpdateShell}
/>
</HoverCardContent>
</HoverCard>
<button
type="button"
onClick={() => setShutdownOpen(true)}
className="-mr-1.5 flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Shut down Unsloth Studio server"
aria-label="Shut down Unsloth Studio server"
>
<HugeiconsIcon icon={Cancel01Icon} className="size-5" />
</button>
</div>
) : null}
<div className="flex shrink-0 items-center">
<HoverCard openDelay={200} closeDelay={100}>
<HoverCardTrigger asChild={true}>
<button
type="button"
className="flex h-9 items-center gap-1.5 rounded-md px-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label="How to update Unsloth Studio"
>
<HugeiconsIcon icon={ArrowReloadHorizontalIcon} className="size-4" />
Update
</button>
</HoverCardTrigger>
<HoverCardContent align="end" className="w-[22.5rem] p-0">
<UpdateStudioInstructions
className="p-4"
defaultShell={defaultUpdateShell}
/>
</HoverCardContent>
</HoverCard>
</div>
<div className="flex shrink-0 items-center">
<button
type="button"
onClick={() => setShutdownOpen(true)}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Shut down Unsloth Studio server"
aria-label="Shut down Unsloth Studio server"
>
<HugeiconsIcon icon={Cancel01Icon} className="size-5" />
</button>
</div>
</div>
{/* Right: mobile */}

View file

@ -473,7 +473,7 @@ export function SharedComposer({
onChange={(e) => setText(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Send to both models..."
className="mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground"
className="mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent pl-5 pr-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground"
rows={1}
/>
<div className="relative mx-2 mb-2 flex items-center justify-between">

View file

@ -6,21 +6,11 @@
@import "shadcn/tailwind.css";
@import "streamdown/styles.css";
@import "@fontsource-variable/figtree";
@import "@fontsource-variable/space-grotesk";
@import "@fontsource-variable/inter";
@import "tw-shimmer";
@plugin "@toolwind/corner-shape";
@source "../node_modules/streamdown/dist/*.js";
@font-face {
font-family: "Hellix";
src: url("/fonts/Hellix-SemiBold.woff2") format("woff2"),
url("/fonts/Hellix-SemiBold.woff") format("woff");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@custom-variant dark (&:is(.dark *));
:root {
@ -67,7 +57,7 @@
--sidebar-ring: oklch(0.6929 0.1396 166.5513);
--destructive-foreground: oklch(1 0 0);
--font-sans: "Inter Variable", ui-sans-serif, sans-serif, system-ui;
--font-heading: "Hellix", "Space Grotesk Variable", ui-sans-serif, sans-serif;
--font-heading: var(--font-sans);
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--shadow-color: hsl(0 0% 0%);
@ -159,7 +149,7 @@
@theme inline {
--font-sans: "Inter Variable", ui-sans-serif, sans-serif, system-ui;
--font-heading: "Hellix", "Space Grotesk Variable", ui-sans-serif, sans-serif;
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@ -343,10 +333,68 @@
[data-streamdown="code-block"] {
gap: 0;
padding: 0.5rem;
/* Wide lines must scroll inside the thread column, not widen past the composer (flex min-width:auto). */
max-width: 100%;
min-width: 0;
overflow-x: auto;
}
[data-streamdown="code-block-header"] {
padding-left: 0.75rem;
}
/* Chat thread: code slightly smaller by default; step up when the thread column is wide. */
.aui-thread-root [data-streamdown="code-block"] {
font-size: 0.8125rem;
line-height: 1.55;
}
.aui-thread-root [data-streamdown="code-block-header"] {
font-size: 0.6875rem;
}
@container (min-width: 36rem) {
.aui-thread-root [data-streamdown="code-block"] {
font-size: 0.875rem;
}
.aui-thread-root [data-streamdown="code-block-header"] {
font-size: 0.75rem;
}
}
/* Chat: use the app sans stack for UI + prose. */
.aui-thread-root {
font-family: var(--font-sans);
}
.aui-thread-root .font-medium,
.aui-thread-root .font-semibold,
.aui-thread-root .font-bold,
.aui-thread-root .font-heading {
font-family: var(--font-sans);
}
.aui-thread-root :where(h1, h2, h3, h4, h5, h6) {
font-family: var(--font-sans);
}
/* Keep monospace for code fences and inline code (not KaTeX). */
.aui-thread-root [data-streamdown="code-block"] pre,
.aui-thread-root [data-streamdown="code-block"] code {
font-family: var(--font-mono), ui-monospace, monospace;
}
.aui-thread-root :where(p, li, td, th, blockquote, h1, h2, h3, h4, h5, h6) code {
font-family: var(--font-mono), ui-monospace, monospace;
}
/* Align fenced code blocks with the main chat column even when nested in lists. */
.aui-thread-root [data-streamdown="list-item"] > [data-streamdown="code-block"],
.aui-thread-root [data-streamdown="list-item"] [data-streamdown="code-block"] {
margin-left: -1.25rem;
width: calc(100% + 1.25rem);
max-width: calc(100% + 1.25rem);
}
}
/* Minimal scrollbar — thumb only, no track */