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:
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user