Studio: fully rounded Hub pills and refreshed menu icons (#6248)

Rounds out the Hub chrome and aligns a handful of icons with the rest
of the app. The header stat pills, HTTP/Xet toggle, HF token button,
info chips, status badges and the single-row download card are now full
pills, and the All formats style dropdown matches the composer + menu
geometry. Also widens the chat options menu so "Move to project" fits
on one line, and swaps a few icons to their HugeIcons equivalents.

Changes:
- hub.css: hub-tag-soft/hub-tag-meta pills go fully rounded; a single-
  row hub-download-card reads as a full pill via :has(), while the
  expanded card keeps its 22px radius, border and background.
- hub-option-menu.tsx: dropdown surface and items match the composer
  + menu (22px container, 10px side padding, 12px item radius).
- hf-token-indicator.tsx, transport-toggle.tsx: fully rounded with a
  fixed 26px height so they line up with the neighbouring pills.
- model-inspector.tsx, dot-tag.tsx, gguf-download-card.tsx: status and
  info chips go fully rounded.
- models-header.tsx: Eject uses RemoveCircleIcon; active model pill
  fully rounded.
- on-device-folders-dialog.tsx: folder rows use Folder02Icon.
- pickers.tsx: Recommended star uses the HugeIcons star.
- command.tsx, chat-search-dialog.tsx, prompt-storage-dialog.tsx: search
  fields use the HugeIcons Search01Icon.
- app-sidebar.tsx: chat options menu widened to w-56.

Pure styling and icon swaps, no behavior changes.
This commit is contained in:
Michael Han 2026-06-12 04:51:19 -07:00 committed by GitHub
commit 9df607c0de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 37 additions and 31 deletions

View file

@ -610,7 +610,7 @@ export function AppSidebar() {
side="bottom"
align="start"
sideOffset={0}
className="unsloth-plus-menu menu-flat-destructive w-52"
className="unsloth-plus-menu menu-flat-destructive w-56"
>
<DropdownMenuItem onSelect={() => openRenameChat(item)}>
<HugeiconsIcon icon={Edit03Icon} strokeWidth={1.75} className="size-icon" />

View file

@ -39,11 +39,11 @@ import { extractParamLabel } from "@/lib/model-size";
import { cn, formatCompact } from "@/lib/utils";
import type { VramFitStatus } from "@/lib/vram";
import { checkVramFit, estimateLoadingVram } from "@/lib/vram";
import { Add01Icon, Cancel01Icon, Download01Icon, Folder02Icon, Search01Icon } from "@hugeicons/core-free-icons";
import { Add01Icon, Cancel01Icon, Download01Icon, Folder02Icon, Search01Icon, StarIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { FolderBrowser } from "./folder-browser";
import { ModelDeleteAction } from "./model-delete-action";
import { ChevronDownIcon, ChevronRightIcon, StarIcon } from "lucide-react";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import {
type ReactNode,
useCallback,
@ -1231,7 +1231,7 @@ export function HubModelPicker({
{!showHfSection && cachedReady ? (
<>
<ListLabel
icon={<StarIcon className="size-3" />}
icon={<HugeiconsIcon icon={StarIcon} className="size-3" />}
collapsed={recommendedCollapsed}
onToggle={() => setRecommendedCollapsed((v) => !v)}
>Recommended</ListLabel>
@ -1292,7 +1292,7 @@ export function HubModelPicker({
{showHfSection && filteredRecommendedIds.length > 0 ? (
<>
<ListLabel icon={<StarIcon className="size-3" />}>Recommended</ListLabel>
<ListLabel icon={<HugeiconsIcon icon={StarIcon} className="size-3" />}>Recommended</ListLabel>
{filteredRecommendedIds.map((id) => {
const vram = recommendedVramMap.get(id);
return (

View file

@ -15,7 +15,7 @@ import {
} from "@/components/ui/dialog";
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group";
import { cn } from "@/lib/utils";
import { SearchIcon, Tick02Icon } from "@hugeicons/core-free-icons";
import { Search01Icon, Tick02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
function Command({
@ -86,7 +86,7 @@ function CommandInput({
/>
<InputGroupAddon>
<HugeiconsIcon
icon={SearchIcon}
icon={Search01Icon}
strokeWidth={2}
className="size-4 shrink-0 opacity-50"
/>

View file

@ -8,7 +8,7 @@ import {
CommandGroup,
CommandList,
} from "@/components/ui/command";
import { Cancel01Icon, Message01Icon, SearchIcon } from "@hugeicons/core-free-icons";
import { Cancel01Icon, Message01Icon, Search01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { useNavigate } from "@tanstack/react-router";
import { Command as CommandPrimitive } from "cmdk";
@ -70,7 +70,7 @@ export function ChatSearchDialog() {
<Command className="rounded-3xl p-0" filter={chatSearchFilter}>
<div className="flex items-center gap-3 border-b border-border/40 px-4 py-3">
<HugeiconsIcon
icon={SearchIcon}
icon={Search01Icon}
strokeWidth={2}
className="size-4 shrink-0 text-muted-foreground"
/>

View file

@ -11,6 +11,8 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { Search01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
BookmarkIcon,
CheckIcon,
@ -20,7 +22,6 @@ import {
PencilIcon,
PlayIcon,
PlusIcon,
SearchIcon,
Trash2Icon,
UploadIcon,
XIcon,
@ -1879,7 +1880,7 @@ export function PromptStorageDialog({
{/* */}
<div className="relative">
<SearchIcon className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/60" />
<HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/60" />
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
@ -1898,7 +1899,7 @@ export function PromptStorageDialog({
onClick={() => { setSearchQuery(name); setShowSuggestions(false); }}
className="flex w-full items-center gap-2.5 px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors text-left"
>
<SearchIcon className="size-3 shrink-0 text-muted-foreground/60" />
<HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} className="size-3 shrink-0 text-muted-foreground/60" />
<span className="truncate">{name}</span>
</button>
))}
@ -1939,7 +1940,7 @@ export function PromptStorageDialog({
{searchQuery.trim() ? (
<>
<div className="flex size-12 items-center justify-center rounded-2xl bg-muted/60">
<SearchIcon className="size-5 text-muted-foreground/40" />
<HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} className="size-5 text-muted-foreground/40" />
</div>
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-muted-foreground">
@ -2003,7 +2004,7 @@ export function PromptStorageDialog({
{searchQuery.trim() ? (
<>
<div className="flex size-12 items-center justify-center rounded-2xl bg-muted/60">
<SearchIcon className="size-5 text-muted-foreground/40" />
<HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} className="size-5 text-muted-foreground/40" />
</div>
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-muted-foreground">

View file

@ -32,7 +32,7 @@ export function DotTag({
return (
<span
className={cn(
"inline-flex h-5 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-[7px] border border-border/60 bg-transparent px-1.5 text-[11px] font-medium leading-none text-muted-foreground",
"inline-flex h-5 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full border border-border/60 bg-transparent px-2 text-[11px] font-medium leading-none text-muted-foreground",
className,
)}
>

View file

@ -110,7 +110,7 @@ const FIT_BADGE: Record<GgufFitClass, FitBadgeMeta> = {
/** Chip styling matching the on-device list's StatChip, no icon. */
const CHIP_BASE =
"inline-flex h-5 shrink-0 items-center justify-center whitespace-nowrap rounded-[7px] border px-1.5 text-[11.5px] font-medium tabular-nums leading-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]";
"inline-flex h-5 shrink-0 items-center justify-center whitespace-nowrap rounded-full border px-2 text-[11.5px] font-medium tabular-nums leading-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]";
const CHIP_DEFAULT =
"border-foreground/15 bg-muted text-foreground/85 dark:border-border/60 dark:bg-white/[0.04] dark:text-foreground/85";
const CHIP_ACTIVE =
@ -328,7 +328,7 @@ const GgufVariantMenuRow = memo(function GgufVariantMenuRow({
onClick={handleDelete}
aria-label={`Delete ${item.label}${item.partial && !item.downloaded ? " (partial)" : ""}`}
className={cn(
"absolute inset-0 inline-flex cursor-pointer items-center justify-center rounded-[7px]",
"absolute inset-0 inline-flex cursor-pointer items-center justify-center rounded-full",
"bg-popover text-foreground/70 ring-1 ring-border transition-colors",
"opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
"hover:text-destructive hover:ring-destructive/40",

View file

@ -185,7 +185,7 @@ export function HubOptionMenu<T extends string>({
collisionPadding={12}
onCloseAutoFocus={(event) => event.preventDefault()}
className={cn(
"hub-menu-instant menu-soft-surface w-max min-w-[var(--radix-popover-trigger-width)] max-w-[min(var(--radix-popover-content-available-width),calc(100vw-1rem))] rounded-[14px] p-1 ring-0",
"hub-menu-instant menu-soft-surface w-max min-w-[var(--radix-popover-trigger-width)] max-w-[min(var(--radix-popover-content-available-width),calc(100vw-1rem))] rounded-[22px] px-[10px] py-2 ring-0",
contentClassName,
)}
>
@ -214,7 +214,7 @@ export function HubOptionMenu<T extends string>({
}}
onPointerEnter={() => activateIndex(index)}
className={cn(
"relative flex w-full min-w-0 cursor-pointer select-none items-center rounded-xl corner-squircle py-2 pr-8 pl-3 text-left text-sm leading-snug outline-none transition-colors",
"relative flex w-full min-w-0 cursor-pointer select-none items-center rounded-[12px] py-2 pr-8 pl-3 text-left text-sm leading-snug outline-none transition-colors",
)}
>
<span className="flex min-w-0 flex-1 items-center gap-2.5 overflow-hidden whitespace-normal break-words">

View file

@ -200,7 +200,7 @@ function StatusChip({
return (
<span
className={cn(
"inline-flex h-5 shrink-0 items-center whitespace-nowrap rounded-[7px] border bg-transparent px-1.5 text-[11px] font-medium leading-none",
"inline-flex h-5 shrink-0 items-center whitespace-nowrap rounded-full border bg-transparent px-2 text-[11px] font-medium leading-none",
toneClass,
className,
)}

View file

@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
import {
ChipIcon,
Database02Icon,
Logout01Icon,
RemoveCircleIcon,
PackageIcon,
RamMemoryIcon,
} from "@hugeicons/core-free-icons";
@ -38,7 +38,7 @@ function StatPill({
className={cn(
"inline-flex cursor-default items-center gap-1.5 px-2.5 py-1 text-[11.5px] transition-colors duration-150",
tone === "active"
? "rounded-[11px] bg-emerald-500/10 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:text-emerald-300"
? "rounded-full bg-emerald-500/10 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:text-emerald-300"
: "hub-tag-soft text-muted-foreground hover:text-foreground/80",
)}
>
@ -125,7 +125,7 @@ export function ModelsHeader({
className="-mr-0.5 ml-0.5 inline-flex cursor-pointer items-center gap-1 rounded-md px-1.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
>
<HugeiconsIcon
icon={Logout01Icon}
icon={RemoveCircleIcon}
strokeWidth={1.75}
className="size-3"
/>

View file

@ -28,7 +28,7 @@ import {
Delete02Icon,
FileSearchIcon,
FolderAddIcon,
FolderOpenIcon,
Folder02Icon,
FolderSearchIcon,
PlusSignIcon,
RefreshIcon,
@ -294,7 +294,7 @@ export function OnDeviceFoldersDialog({
) : sortedFolders.length === 0 ? (
<div className="flex h-28 flex-col items-center justify-center gap-2 px-4 text-center text-[12px] text-muted-foreground">
<HugeiconsIcon
icon={FolderOpenIcon}
icon={Folder02Icon}
strokeWidth={1.75}
className="size-5 text-muted-foreground/60"
/>
@ -310,7 +310,7 @@ export function OnDeviceFoldersDialog({
>
<div className="flex size-8 shrink-0 items-center justify-center rounded-[9px] bg-muted text-muted-foreground">
<HugeiconsIcon
icon={FolderOpenIcon}
icon={Folder02Icon}
strokeWidth={1.75}
className="size-4"
/>

View file

@ -40,7 +40,7 @@ export function TransportToggle() {
return (
<fieldset
aria-label="Download transport"
className="hub-tag-soft m-0 inline-flex h-[26px] min-w-0 items-center gap-0.5 rounded-[11px] border-0 p-0.5 text-[11px]"
className="hub-tag-soft m-0 inline-flex h-[26px] min-w-0 items-center gap-0.5 rounded-full border-0 p-0.5 text-[11px]"
>
{OPTIONS.map((opt) => {
const active = mode === opt.value;
@ -58,7 +58,7 @@ export function TransportToggle() {
onClick={() => setMode(opt.value)}
aria-pressed={active}
className={cn(
"inline-flex h-[22px] cursor-pointer items-center justify-center rounded-[8px] px-2 font-medium tracking-tight transition-colors",
"inline-flex h-[22px] cursor-pointer items-center justify-center rounded-full px-2 font-medium tracking-tight transition-colors",
active
? "hub-tab-toggle-pill text-foreground"
: disabled

View file

@ -87,10 +87,10 @@ export function HfTokenIndicator({ showLabel = false }: HfTokenIndicatorProps =
onClick={() => openDialog("general")}
aria-label={ariaLabel}
className={cn(
"inline-flex items-center justify-center px-2.5 py-1 text-[11.5px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"inline-flex h-[26px] items-center justify-center px-2.5 text-[11.5px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
hasToken
? "hub-tag-soft text-muted-foreground hover:text-foreground/80"
: "rounded-[11px] bg-destructive text-destructive-foreground hover:bg-destructive/90",
: "rounded-full bg-destructive text-destructive-foreground hover:bg-destructive/90",
)}
>
<HugeiconsIcon

View file

@ -320,7 +320,7 @@
@layer utilities {
.hub-tag-soft,
.hub-tag-meta {
@apply min-h-[26px] rounded-[11px] bg-muted/70 ring-1 ring-inset;
@apply min-h-[26px] rounded-full bg-muted/70 ring-1 ring-inset;
--tw-ring-color: color-mix(in srgb, var(--foreground) 3.5%, transparent);
}
@ -335,6 +335,11 @@
box-shadow: none;
}
/* Single-row state (variant + actions only) reads as a full pill. */
.hub-download-card:not(:has(> :nth-child(2))) {
border-radius: 9999px;
}
.dark .hub-download-card {
background-color: color-mix(in srgb, var(--foreground) 3.5%, transparent);
border: 1px solid color-mix(in srgb, var(--foreground) 5.5%, transparent);