style(desktop): add Switch xs size; move appearance controls inline-right

Add an xs size variant to the Switch primitive and use it for the provider
edit submenu toggles. In appearance settings, drop the redundant selection
Pills (the UI already shows the active choice), move the Color Mode and Tool
Call Display segmented controls into the section header's right side
(responsive: stacks under the heading on narrow widths), and shrink the
segmented control.
This commit is contained in:
Brooklyn Nicholson
2026-06-03 22:17:26 -05:00
parent 947f305f84
commit 75e29f97ee
3 changed files with 76 additions and 56 deletions
@@ -9,8 +9,7 @@ import { useTheme } from '@/themes/context'
import { BUILTIN_THEMES } from '@/themes/presets'
import { MODE_OPTIONS } from './constants'
import { prettyName } from './helpers'
import { Pill, SettingsContent } from './primitives'
import { SettingsContent } from './primitives'
function ThemePreview({ name }: { name: string }) {
const t = BUILTIN_THEMES[name]
@@ -52,16 +51,16 @@ function ThemePreview({ name }: { name: string }) {
)
}
function SectionHead({ title, description, pill }: { title: string; description: string; pill?: ReactNode }) {
function SectionHead({ title, description, control }: { title: string; description: string; control?: ReactNode }) {
return (
<div className="flex items-center justify-between gap-3">
<div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="min-w-0">
<div className="text-[length:var(--conversation-text-font-size)] font-medium">{title}</div>
<div className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
{description}
</div>
</div>
{pill}
{control && <div className="shrink-0">{control}</div>}
</div>
)
}
@@ -76,7 +75,7 @@ function SegmentedControl<T extends string>({
onChange: (id: T) => void
}) {
return (
<div className="inline-grid w-fit auto-cols-fr grid-flow-col gap-0.5 rounded-md bg-(--ui-bg-tertiary) p-0.5">
<div className="inline-grid w-fit auto-cols-fr grid-flow-col gap-0.5 rounded-[5px] bg-(--ui-bg-tertiary) p-0.5">
{options.map(({ id, label, icon: Icon }) => {
const active = value === id
@@ -84,7 +83,7 @@ function SegmentedControl<T extends string>({
<button
aria-pressed={active}
className={cn(
'flex items-center justify-center gap-1.5 rounded-[3px] px-3.5 py-1.5 text-xs font-medium transition-colors',
'flex items-center justify-center gap-1 rounded-[3px] px-2.5 py-0.5 text-[0.6875rem] font-medium transition-colors',
active
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
@@ -93,7 +92,7 @@ function SegmentedControl<T extends string>({
onClick={() => onChange(id)}
type="button"
>
{Icon && <Icon className="size-3.5" />}
{Icon && <Icon className="size-3" />}
{label}
</button>
)
@@ -105,7 +104,6 @@ function SegmentedControl<T extends string>({
export function AppearanceSettings() {
const { themeName, mode, availableThemes, setTheme, setMode } = useTheme()
const toolViewMode = useStore($toolViewMode)
const activeTheme = availableThemes.find(t => t.name === themeName)
return (
<SettingsContent>
@@ -115,49 +113,47 @@ export function AppearanceSettings() {
chat surface styling.
</p>
<section className="grid gap-3">
<section>
<SectionHead
control={
<SegmentedControl
onChange={id => {
triggerHaptic('crisp')
setMode(id)
}}
options={MODE_OPTIONS}
value={mode}
/>
}
description="Pick a fixed mode or let Hermes follow your system setting."
pill={<Pill>{prettyName(mode)}</Pill>}
title="Color Mode"
/>
<SegmentedControl
onChange={id => {
triggerHaptic('crisp')
setMode(id)
}}
options={MODE_OPTIONS}
value={mode}
/>
</section>
<section className="grid gap-3">
<section>
<SectionHead
control={
<SegmentedControl
onChange={id => {
triggerHaptic('selection')
setToolViewMode(id)
}}
options={
[
{ id: 'product', label: 'Product' },
{ id: 'technical', label: 'Technical' }
] as const
}
value={toolViewMode}
/>
}
description="Product hides raw tool payloads; Technical shows full input/output."
pill={<Pill>{toolViewMode === 'technical' ? 'Technical' : 'Product'}</Pill>}
title="Tool Call Display"
/>
<SegmentedControl
onChange={id => {
triggerHaptic('selection')
setToolViewMode(id)
}}
options={
[
{ id: 'product', label: 'Product' },
{ id: 'technical', label: 'Technical' }
] as const
}
value={toolViewMode}
/>
</section>
<section className="grid gap-3">
<SectionHead
description="Desktop palettes only. The selected mode is applied on top."
pill={activeTheme ? <Pill>{activeTheme.label}</Pill> : undefined}
title="Theme"
/>
<SectionHead description="Desktop palettes only. The selected mode is applied on top." title="Theme" />
<div className="grid gap-x-4 gap-y-5 sm:grid-cols-2 xl:grid-cols-3">
{availableThemes.map(theme => {
const active = themeName === theme.name
@@ -191,6 +191,7 @@ export function ModelEditSubmenu({
checked={thinkingOn}
className="ml-auto"
onCheckedChange={checked => void patchReasoning(checked ? effort || 'medium' : 'none', currentReasoningEffort)}
size="xs"
/>
</DropdownMenuItem>
) : null}
@@ -200,7 +201,7 @@ export function ModelEditSubmenu({
onSelect={event => event.preventDefault()}
>
Fast
<Switch checked={fastOn} className="ml-auto" onCheckedChange={toggleFast} />
<Switch checked={fastOn} className="ml-auto" onCheckedChange={toggleFast} size="xs" />
</DropdownMenuItem>
) : null}
{reasoning ? (
+38 -15
View File
@@ -1,24 +1,47 @@
import { cva, type VariantProps } from 'class-variance-authority'
import { Switch as SwitchPrimitive } from 'radix-ui'
import * as React from 'react'
import { cn } from '@/lib/utils'
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
const switchVariants = cva(
'peer inline-flex shrink-0 items-center rounded-full border border-[color-mix(in_srgb,var(--dt-foreground)_18%,transparent)] bg-[color-mix(in_srgb,var(--dt-background)_58%,var(--dt-input))] shadow-[inset_0_0_0_0.0625rem_color-mix(in_srgb,var(--dt-foreground)_8%,transparent)] transition-colors outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-transparent data-[state=checked]:bg-primary',
{
variants: {
size: {
default: 'h-5 w-9',
xs: 'h-4 w-7'
}
},
defaultVariants: {
size: 'default'
}
}
)
const switchThumbVariants = cva(
'pointer-events-none block rounded-full bg-foreground shadow-[0_0.0625rem_0.1875rem_color-mix(in_srgb,var(--dt-background)_50%,transparent)] ring-0 transition-transform data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-background',
{
variants: {
size: {
default: 'size-4 data-[state=checked]:translate-x-4',
xs: 'size-3 data-[state=checked]:translate-x-3.5'
}
},
defaultVariants: {
size: 'default'
}
}
)
function Switch({
className,
size,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & VariantProps<typeof switchVariants>) {
return (
<SwitchPrimitive.Root
className={cn(
'peer inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-[color-mix(in_srgb,var(--dt-foreground)_18%,transparent)] bg-[color-mix(in_srgb,var(--dt-background)_58%,var(--dt-input))] shadow-[inset_0_0_0_0.0625rem_color-mix(in_srgb,var(--dt-foreground)_8%,transparent)] transition-colors outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-transparent data-[state=checked]:bg-primary',
className
)}
data-slot="switch"
{...props}
>
<SwitchPrimitive.Thumb
className={cn(
'pointer-events-none block size-4 rounded-full bg-foreground shadow-[0_0.0625rem_0.1875rem_color-mix(in_srgb,var(--dt-background)_50%,transparent)] ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-background data-[state=unchecked]:translate-x-0'
)}
data-slot="switch-thumb"
/>
<SwitchPrimitive.Root className={cn(switchVariants({ size }), className)} data-slot="switch" {...props}>
<SwitchPrimitive.Thumb className={cn(switchThumbVariants({ size }))} data-slot="switch-thumb" />
</SwitchPrimitive.Root>
)
}