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:
parent
71b934ef9d
commit
ca4ea8b9fb
6 changed files with 175 additions and 111 deletions
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue