refactor(ui): shared component re-skin + legacy-css-var governance

Signed-off-by: Siin Xu <31815270+SiinXu@users.noreply.github.com>
This commit is contained in:
Siin Xu
2026-07-02 20:39:44 -07:00
parent 8276fb8494
commit ad1e23c660
49 changed files with 247 additions and 214 deletions

View File

@@ -206,6 +206,9 @@ jobs:
- name: Provider Registry Package Test
run: pnpm test:provider-registry
- name: UI Package Test
run: pnpm test:pkg:ui
- name: Scripts Test
run: pnpm test:scripts

View File

@@ -22,7 +22,6 @@ const LEGACY_RENDERER_CSS_VARS = [
'--color-border-soft',
'--color-border-mute',
'--color-error',
'--color-link',
'--color-primary-bg',
'--color-fill-secondary',
'--color-fill-2',
@@ -33,7 +32,6 @@ const LEGACY_RENDERER_CSS_VARS = [
'--color-inline-code-text',
'--color-hover',
'--color-active',
'--color-frame-border',
'--color-group-background',
'--color-reference',
'--color-reference-text',

View File

@@ -147,7 +147,6 @@ describe('check-legacy-css-vars', () => {
it('auto-fixes mapped legacy variables in code lines only', () => {
const content = [
'const className = "text-(--color-text-2) bg-(--color-background-soft)"',
'const linkStyle = { color: "var(--color-link)" }',
'// var(--color-text-1)',
':root {',
' --color-text-1: var(--color-foreground);',
@@ -156,9 +155,8 @@ describe('check-legacy-css-vars', () => {
const result = fixLegacyVarsInContent(content)
expect(result.replacements).toBe(3)
expect(result.replacements).toBe(2)
expect(result.content).toContain('text-(--color-foreground-secondary) bg-(--color-muted)')
expect(result.content).toContain('var(--color-primary)')
expect(result.content).toContain('// var(--color-text-1)')
expect(result.content).toContain('--color-text-1: var(--color-foreground);')
})

View File

@@ -26,7 +26,6 @@ export const LEGACY_VARS = [
'--color-border-soft',
'--color-border-mute',
'--color-error',
'--color-link',
'--color-primary-bg',
'--color-fill-secondary',
'--color-fill-2',
@@ -37,7 +36,6 @@ export const LEGACY_VARS = [
'--color-inline-code-text',
'--color-hover',
'--color-active',
'--color-frame-border',
'--color-group-background',
'--color-reference',
'--color-reference-text',
@@ -82,7 +80,6 @@ const AUTO_FIX_REPLACEMENTS: Partial<Record<(typeof LEGACY_VARS)[number], string
'--color-border-soft': '--color-border',
'--color-border-mute': '--color-border',
'--color-error': '--color-error-base',
'--color-link': '--color-primary',
'--color-primary-bg': '--color-primary-soft',
'--color-fill-secondary': '--color-muted',
'--color-fill-2': '--color-muted',
@@ -90,7 +87,6 @@ const AUTO_FIX_REPLACEMENTS: Partial<Record<(typeof LEGACY_VARS)[number], string
'--color-bg-1': '--color-muted',
'--color-hover': '--color-accent',
'--color-active': '--color-muted',
'--color-frame-border': '--color-border',
'--color-group-background': '--color-muted',
'--color-reference': '--color-primary-soft',
'--color-reference-text': '--color-primary',

View File

@@ -85,13 +85,13 @@ const ErrorDetailItem = ({ className, ...props }: React.HTMLAttributes<HTMLDivEl
)
const ErrorDetailLabel = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('font-semibold text-[14px] text-foreground', className)} {...props} />
<div className={cn('text-(length:--font-size-body-sm) font-semibold text-foreground', className)} {...props} />
)
const ErrorDetailValue = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'rounded-[4px] border border-[var(--color-border)] bg-background-subtle p-2 font-[var(--code-font-family)] text-[12px] text-foreground [word-break:break-word]',
'text-(length:--font-size-body-xs) rounded-[4px] border border-[var(--color-border)] bg-background-subtle p-2 font-[var(--code-font-family)] text-foreground [word-break:break-word]',
className
)}
{...props}
@@ -101,7 +101,7 @@ const ErrorDetailValue = ({ className, ...props }: React.HTMLAttributes<HTMLDivE
const StackTrace = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'rounded-[6px] border border-error-base bg-background-subtle p-3 [&_pre]:m-0 [&_pre]:whitespace-pre-wrap [&_pre]:font-[var(--code-font-family)] [&_pre]:text-[12px] [&_pre]:text-error-base [&_pre]:leading-[1.4] [&_pre]:[word-break:break-word]',
'[&_pre]:text-(length:--font-size-body-xs) rounded-[6px] border border-error-base bg-background-subtle p-3 [&_pre]:m-0 [&_pre]:whitespace-pre-wrap [&_pre]:font-[var(--code-font-family)] [&_pre]:text-error-base [&_pre]:leading-[1.4] [&_pre]:[word-break:break-word]',
className
)}
{...props}
@@ -110,7 +110,10 @@ const StackTrace = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement
const TruncatedBadge = ({ className, style, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
<span
className={cn('ml-2 rounded-[4px] px-1.5 py-0.5 font-normal text-[10px] text-[var(--color-warning)]', className)}
className={cn(
'text-(length:--font-size-body-2xs) ml-2 rounded-[4px] px-1.5 py-0.5 font-normal text-[var(--color-warning)]',
className
)}
style={{
background: 'var(--color-warning-bg, rgba(250, 173, 20, 0.1))',
...style

View File

@@ -83,7 +83,7 @@ export function FileTreeRow(props: FileTreeRowProps) {
title={node.name}
style={indent}
className={cn(
'group relative flex select-none items-center gap-1.5 rounded-3xs py-1 pr-2 text-left text-sm',
'group relative flex select-none items-center gap-1.5 rounded-md py-1 pr-2 text-left text-sm',
'transition-colors',
isFolder
? 'text-foreground/75 hover:bg-accent/50 hover:text-foreground'

View File

@@ -18,8 +18,8 @@ const NavbarIcon = ({ active, className, tone = 'default', type = 'button', ...p
data-active={active || undefined}
className={cn(
conversation
? 'text-foreground/70! duration-150 ease-in-out [-webkit-app-region:none] hover:bg-accent/60 hover:text-foreground! data-[active=true]:bg-secondary data-[state=open]:bg-secondary data-[active=true]:text-secondary-foreground! data-[state=open]:text-secondary-foreground! [&_.lucide:not(.lucide-custom)]:text-current!'
: 'text-foreground/70! duration-200 ease-in-out [-webkit-app-region:none] hover:bg-muted hover:text-foreground',
? 'text-foreground/80! duration-150 ease-in-out [-webkit-app-region:none] hover:bg-accent/60 hover:text-foreground! data-[active=true]:bg-secondary data-[state=open]:bg-secondary data-[active=true]:text-secondary-foreground! data-[state=open]:text-secondary-foreground! [&_.lucide:not(.lucide-custom)]:text-current! [&_svg]:[stroke-width:1.6]'
: 'text-foreground/80! duration-200 ease-in-out [-webkit-app-region:none] hover:bg-muted hover:text-foreground [&_svg]:[stroke-width:1.6]',
conversation && active && 'bg-secondary text-secondary-foreground!',
className
)}

View File

@@ -62,10 +62,10 @@ export const LanDeviceCard: FC<LanDeviceCardProps> = ({
// Hover state
'hover:-translate-y-px hover:border-[var(--color-primary-hover)] hover:shadow-md',
// Focus state
'focus-visible:border-[var(--color-primary)] focus-visible:shadow-[0_0_0_2px_rgba(24,144,255,0.2)]',
'focus-visible:border-[var(--color-primary)] focus-visible:shadow-[0_0_0_2px_color-mix(in_srgb,var(--color-primary)_20%,transparent)]',
// Connected state
isConnected
? 'border-[var(--color-primary)] bg-[rgba(24,144,255,0.04)]'
? 'border-[var(--color-primary)] bg-primary/5'
: 'border-[var(--color-border)] bg-[var(--color-background)]',
// Disabled state
isDisabled && 'pointer-events-none translate-y-0 opacity-70 shadow-none'

View File

@@ -8,7 +8,7 @@ import {
DialogHeader,
DialogTitle,
Flex,
HelpTooltip,
InfoTooltip,
Label
} from '@cherrystudio/ui'
import { loggerService } from '@logger'
@@ -474,7 +474,7 @@ const PopupContainer: React.FC<Props> = ({ source, title, resolve }) => {
{option.count}
</CustomTag>
<span>{option.label}</span>
<HelpTooltip content={option.description} />
<InfoTooltip content={option.description} />
</Flex>
{selectedTypes.includes(option.type) && <Check size={16} color={TAG_COLORS.SELECTED} />}
</button>

View File

@@ -9,7 +9,7 @@ export const PreviewError = ({ className, ref, ...props }: DivProps) =>
React.createElement('div', {
ref,
className: cn(
'overflow-auto whitespace-pre-wrap break-words rounded-[4px] border border-[#ff4d4f] p-4 text-[#ff4d4f]',
'overflow-auto whitespace-pre-wrap break-words rounded-[4px] border border-destructive p-4 text-destructive',
className
),
...props

View File

@@ -209,7 +209,7 @@ const CommandListPopover = ({
return (
<div ref={listRef} style={style} className="command-list-popover">
{items.length === 0 ? (
<div style={{ padding: '12px', color: '#999', textAlign: 'center', fontSize: '14px' }}>
<div style={{ padding: '12px', color: 'var(--color-foreground-muted)', textAlign: 'center', fontSize: '14px' }}>
{t('richEditor.commands.noCommandsFound')}
</div>
) : (

View File

@@ -36,7 +36,7 @@ const ImagePlaceholderNodeView: React.FC<ImagePlaceholderNodeViewProps> = ({ del
return (
<NodeViewWrapper className="image-placeholder-wrapper">
<PlaceholderBlock
icon={<ImageIcon size={20} style={{ color: '#656d76' }} />}
icon={<ImageIcon size={20} style={{ color: 'var(--color-foreground-secondary)' }} />}
message={t('richEditor.image.placeholder')}
onClick={handleClick}
/>

View File

@@ -63,7 +63,7 @@ const MathPlaceholderNodeView: React.FC<NodeViewProps> = ({ node, deleteNode, ed
return (
<NodeViewWrapper className="math-placeholder-wrapper" ref={wrapperRef}>
<PlaceholderBlock
icon={<Calculator size={20} style={{ color: '#656d76' }} />}
icon={<Calculator size={20} style={{ color: 'var(--color-foreground-secondary)' }} />}
message={t('richEditor.math.placeholder')}
onClick={handleClick}
/>

View File

@@ -54,7 +54,7 @@ const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ icon, message, onCl
target.style.backgroundColor = colors.background
}}>
{icon}
<span style={{ color: '#656d76', fontSize: 14 }}>{message}</span>
<span style={{ color: 'var(--color-foreground-secondary)', fontSize: 14 }}>{message}</span>
</div>
)
}

View File

@@ -254,7 +254,7 @@ const STYLE_CONTENT = `
padding: 0.25rem 0.5rem;
color: var(--color-foreground-secondary);
text-decoration: none;
border-radius: 4px;
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.4;
transition: all 0.2s ease;

View File

@@ -172,7 +172,7 @@ const Selector = <V extends string | number>({
aria-selected={isSelected}
disabled={disabled || option.disabled}
className={cn(
'flex w-full items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-hidden transition-colors',
'flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-sm outline-hidden transition-colors',
'hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground',
'disabled:pointer-events-none disabled:opacity-50',
level > 0 && 'pl-4'
@@ -191,7 +191,7 @@ const Selector = <V extends string | number>({
<Popover open={open && !disabled} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
variant="secondary"
variant="ghost"
size="sm"
role="combobox"
aria-label={accessibleLabel || undefined}
@@ -199,15 +199,15 @@ const Selector = <V extends string | number>({
aria-disabled={disabled || undefined}
tabIndex={disabled ? -1 : 0}
className={cn(
'min-w-0 text-left leading-none',
open && !disabled && 'bg-secondary-active',
'min-w-0 justify-between rounded-lg bg-muted/50 text-left leading-none hover:bg-muted',
open && !disabled && 'bg-muted',
disabled && 'cursor-not-allowed opacity-60',
isPlaceholder && 'text-muted-foreground'
)}
onKeyDown={handleTriggerKeyDown}
style={{ fontSize: size, ...style }}>
<span className="min-w-0 truncate">{label}</span>
<ChevronDown aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground" />
<ChevronDown aria-hidden="true" className="lucide-custom size-3.5 shrink-0 text-muted-foreground/40" />
</Button>
</PopoverTrigger>
<PopoverContent

View File

@@ -134,6 +134,14 @@ vi.mock('@cherrystudio/ui', () => {
}: InputHTMLAttributes<HTMLInputElement> & { ref?: RefObject<HTMLInputElement | null> }) => (
<input ref={ref} {...props} />
),
InputGroup: ({ children, ...props }: { children?: ReactNode }) => <div {...props}>{children}</div>,
InputGroupAddon: ({ children, ...props }: { children?: ReactNode }) => <div {...props}>{children}</div>,
InputGroupInput: ({
ref,
...props
}: InputHTMLAttributes<HTMLInputElement> & { ref?: RefObject<HTMLInputElement | null> }) => (
<input ref={ref} {...props} />
),
Popover: ({ children, onOpenChange }: { children: ReactNode; onOpenChange?: (open: boolean) => void }) => (
<div>
<button type="button" data-testid="mock-popover-close" onClick={() => onOpenChange?.(false)} />
@@ -820,10 +828,8 @@ describe('ModelSelector', () => {
const providerName = screen.getByText('| OpenAI')
expect(modelName).toHaveClass('min-w-0', 'max-w-full', 'shrink-0', 'truncate')
expect(modelName).toHaveAttribute('title', longModelName)
expect(screen.queryByText(longIdentifier)).toBeNull()
expect(providerName).toHaveClass('min-w-0', 'flex-[1_999_0%]', 'truncate')
expect(providerName).toHaveAttribute('title', 'OpenAI')
})
it('passes the selector portal container to model detail hover cards', () => {

View File

@@ -23,6 +23,14 @@ vi.mock('@cherrystudio/ui', () => ({
Input: ({ ref, ...props }: InputHTMLAttributes<HTMLInputElement> & { ref?: RefObject<HTMLInputElement | null> }) => (
<input ref={ref} {...props} />
),
InputGroup: ({ children, ...props }: { children?: ReactNode }) => <div {...props}>{children}</div>,
InputGroupAddon: ({ children, ...props }: { children?: ReactNode }) => <div {...props}>{children}</div>,
InputGroupInput: ({
ref,
...props
}: InputHTMLAttributes<HTMLInputElement> & { ref?: RefObject<HTMLInputElement | null> }) => (
<input ref={ref} {...props} />
),
PortalContainerProvider: ({ children, container }: { children: ReactNode; container: HTMLElement | null }) => {
portalContainerMock.current = container
return <>{children}</>

View File

@@ -210,7 +210,6 @@ function ModelRow({
<ModelSelectorRow
selected={isSelected}
focused={isFocused}
showSelectedIndicator={!showCheckbox && isSelected}
checkbox={checkbox}
leading={leading}
trailing={trailing}
@@ -230,13 +229,9 @@ function ModelRow({
onSelect={() => onSelect(item)}
rootProps={{ className: 'pr-0.5' }}
optionProps={{ 'data-testid': `model-selector-item-${item.modelId}` }}>
<span className="min-w-0 max-w-full shrink-0 truncate" title={item.model.name}>
{item.model.name}
</span>
<span className="min-w-0 max-w-full shrink-0 truncate">{item.model.name}</span>
{item.isPinned && (
<span className="min-w-0 flex-[1_999_0%] truncate text-muted-foreground text-xs" title={providerName}>
| {providerName}
</span>
<span className="min-w-0 flex-[1_999_0%] truncate text-muted-foreground text-xs">| {providerName}</span>
)}
</ModelSelectorRow>
</ModelSelectorDetailCard>
@@ -609,7 +604,7 @@ export function ModelSelector(props: ModelSelectorProps) {
item.groupKind === 'pinned' ? t('models.pinned') : item.provider ? getProviderDisplayName(item.provider) : ''
return (
<div className="group flex h-7 items-center gap-1 bg-popover px-4 text-[11px] text-muted-foreground">
<div className="group text-(length:--font-size-body-xs) flex h-7 items-center gap-1 bg-popover px-4 text-muted-foreground">
<span className="truncate">{groupTitle}</span>
{item.provider && item.canNavigateToSettings && (
<Tooltip content={t('navigate.provider_settings')} delay={500}>
@@ -695,7 +690,9 @@ export function ModelSelector(props: ModelSelectorProps) {
return (
<>
<span className="mr-1 text-[10px] text-muted-foreground">{t('models.filter.by_tag')}</span>
<span className="text-(length:--font-size-body-2xs) mr-1 text-muted-foreground">
{t('models.filter.by_tag')}
</span>
{availableTags.map((tag) => (
<ModelTag
key={`filter-${tag}`}

View File

@@ -82,12 +82,20 @@ function ModelSelectorDetailCardBody({ item, providerName }: { item: ModelSelect
return (
<div className="max-h-[min(420px,70vh)] overflow-auto p-3">
<div className="min-w-0 space-y-1">
<div className="truncate font-medium text-foreground text-sm" title={model.name}>
<div className="truncate font-medium text-foreground text-xs" title={model.name}>
{model.name}
</div>
</div>
<dl className="mt-3 space-y-1.5 border-border border-t pt-3">
{tags.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-1.5">
{tags.map((tag) => (
<ModelTag key={`${item.key}-detail-${tag}`} tag={tag} size={10} showLabel showTooltip={false} />
))}
</div>
) : null}
<dl className="mt-3 space-y-1.5">
<DetailRow label={t('models.detail.provider')} value={providerName} />
<DetailRow
label={t('models.detail.model_id')}
@@ -99,16 +107,8 @@ function ModelSelectorDetailCardBody({ item, providerName }: { item: ModelSelect
/>
</dl>
{tags.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-1.5">
{tags.map((tag) => (
<ModelTag key={`${item.key}-detail-${tag}`} tag={tag} size={10} showLabel showTooltip={false} />
))}
</div>
) : null}
{hasTokenDetails ? (
<dl className="mt-3 space-y-1.5 border-border border-t pt-3">
<dl className="mt-3 space-y-1.5">
<DetailRow label={t('models.detail.context_window')} value={formatNumber(model.contextWindow)} />
<DetailRow label={t('models.detail.max_input_tokens')} value={formatNumber(model.maxInputTokens)} />
<DetailRow label={t('models.detail.max_output_tokens')} value={formatNumber(model.maxOutputTokens)} />
@@ -116,7 +116,7 @@ function ModelSelectorDetailCardBody({ item, providerName }: { item: ModelSelect
) : null}
{hasCapabilityDetails ? (
<dl className="mt-3 space-y-1.5 border-border border-t pt-3">
<dl className="mt-3 space-y-1.5">
<DetailRow label={t('assistants.settings.reasoning_effort.label')} value={reasoningEfforts} />
<DetailRow label={t('models.detail.image_modes')} value={imageModes} />
</dl>

View File

@@ -22,7 +22,6 @@ type ModelSelectorRowProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' |
selected: boolean
focused?: boolean
disabled?: boolean
showSelectedIndicator?: boolean
checkbox?: ReactNode
leading?: ReactNode
children: ReactNode
@@ -38,7 +37,6 @@ export function ModelSelectorRow({
selected,
focused = false,
disabled = false,
showSelectedIndicator = false,
checkbox,
leading,
children,
@@ -68,12 +66,6 @@ export function ModelSelectorRow({
rootClassName
)}
data-model-selector-row>
{showSelectedIndicator ? (
<span
aria-hidden="true"
className="-translate-y-1/2 absolute top-1/2 left-0 block h-[60%] w-0.75 rounded-full bg-muted-foreground/60"
/>
) : null}
<div
{...restOptionProps}
role={optionProps?.role ?? 'option'}

View File

@@ -1,4 +1,13 @@
import { Input, Popover, PopoverContent, PopoverTrigger, Switch, usePortalContainer } from '@cherrystudio/ui'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
Popover,
PopoverContent,
PopoverTrigger,
Switch,
usePortalContainer
} from '@cherrystudio/ui'
import { cn } from '@cherrystudio/ui/lib/utils'
import { Search } from 'lucide-react'
import {
@@ -408,29 +417,34 @@ export function SelectorShell({
ref={setContentElement}
data-testid={dataTestId}>
{search ? (
<div
ref={setSearchElement}
className="flex items-center gap-2 border-border border-b px-3 py-1"
data-selector-shell-chrome="search">
<Search className="pointer-events-none size-3.25 shrink-0 text-muted-foreground/50" />
<Input
ref={search.inputRef}
value={search.value}
autoFocus={search.autoFocus ?? true}
spellCheck={search.spellCheck ?? false}
placeholder={search.placeholder}
aria-activedescendant={search.activeDescendant}
aria-controls={search.ariaControls}
<div ref={setSearchElement} className="px-3 py-2" data-selector-shell-chrome="search">
<InputGroup
className={cn(
'h-[var(--cs-size-xs)] flex-1 border-0 bg-transparent p-0 shadow-none transition-none',
'text-xs md:text-xs',
'focus-visible:border-transparent focus-visible:ring-0',
'placeholder:text-muted-foreground/40'
)}
data-testid={search.dataTestId}
onChange={(event) => search.onChange(event.target.value)}
onKeyDown={search.onKeyDown}
/>
'rounded-full border-border-subtle',
'has-[[data-slot=input-group-control]:focus-visible]:border-border-subtle',
'has-[[data-slot=input-group-control]:focus-visible]:ring-0'
)}>
<InputGroupAddon>
<Search className="pointer-events-none size-3.25 shrink-0 text-muted-foreground/50" />
</InputGroupAddon>
<InputGroupInput
ref={search.inputRef}
value={search.value}
autoFocus={search.autoFocus ?? true}
spellCheck={search.spellCheck ?? false}
placeholder={search.placeholder}
aria-activedescendant={search.activeDescendant}
aria-controls={search.ariaControls}
className={cn(
'h-[var(--cs-size-xs)] transition-none',
'text-xs md:text-xs',
'placeholder:text-muted-foreground/40'
)}
data-testid={search.dataTestId}
onChange={(event) => search.onChange(event.target.value)}
onKeyDown={search.onKeyDown}
/>
</InputGroup>
</div>
) : null}
@@ -449,7 +463,7 @@ export function SelectorShell({
className="flex items-center justify-between gap-3 border-border border-b px-3 py-2"
data-selector-shell-chrome="multi-select"
data-testid={multiSelect.rowTestId}>
<div className="flex min-w-0 flex-1 items-center gap-1 text-[10px] text-muted-foreground">
<div className="text-(length:--font-size-body-2xs) flex min-w-0 flex-1 items-center gap-1 text-muted-foreground">
<span className="truncate">{multiSelect.label}</span>
{multiSelect.hint ? (
<span className="truncate text-muted-foreground/60">{multiSelect.hint}</span>

View File

@@ -8,10 +8,10 @@ interface Props {
// Define variants for the spinner animation
const spinnerVariants = {
defaultColor: {
color: '#2a2a2a'
color: 'var(--color-foreground)'
},
dimmed: {
color: '#8C9296'
color: 'var(--color-foreground-secondary)'
}
}

View File

@@ -1,7 +1,6 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from '@cherrystudio/ui'
import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference'
import EditableNumber from '@renderer/components/EditableNumber'
import { SettingGroup as PageSettingGroup, SettingTitle } from '@renderer/components/SettingsPrimitives'
import { useCodeStyle } from '@renderer/hooks/useCodeStyle'
import { useTheme } from '@renderer/hooks/useTheme'
import type { CodeStyleVarious } from '@renderer/types/app'
@@ -12,13 +11,7 @@ import type { FC, ReactNode } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
SettingDivider,
SettingGroup,
SettingRow,
SettingRowTitleSmall,
SettingSwitch
} from './settingsPanelPrimitives'
import { SettingCard, SettingRow, SettingRowTitleSmall, SettingSwitch } from './settingsPanelPrimitives'
type SelectOption<T extends string = string> = {
value: T
@@ -135,11 +128,10 @@ const ChatPreferenceSections: FC = () => {
)
const renderSection = (title: string, children: ReactNode) => (
<PageSettingGroup theme={theme}>
<SettingTitle>{title}</SettingTitle>
<SettingDivider />
<SettingGroup>{children}</SettingGroup>
</PageSettingGroup>
<div className="mt-6 first:mt-0">
<div className="flex select-none items-center justify-between font-medium text-foreground text-sm">{title}</div>
<SettingCard>{children}</SettingCard>
</div>
)
return (
@@ -153,28 +145,25 @@ const ChatPreferenceSections: FC = () => {
onCheckedChange={setShowInputEstimatedTokens}
label={t('settings.messages.input.show_estimated_tokens')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={renderInputMessageAsMarkdown}
onCheckedChange={setRenderInputMessageAsMarkdown}
label={t('settings.messages.markdown_rendering_input_message')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={confirmDeleteMessage}
onCheckedChange={setConfirmDeleteMessage}
label={t('settings.messages.input.confirm_delete_message')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
<Select value={sendMessageShortcut} onValueChange={setSendMessageShortcut}>
<SelectTrigger size="sm" className="w-[220px] text-sm">
<SelectTrigger size="sm" className="min-w-0 max-w-56 flex-1 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent className="text-sm">
@@ -193,16 +182,14 @@ const ChatPreferenceSections: FC = () => {
<>
<SettingRow>
<SettingSwitch checked={wideMode} onCheckedChange={setWideMode} label={t('settings.messages.wide_mode')} />
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={messageFont === 'serif'}
onCheckedChange={(checked) => setMessageFont(checked ? 'serif' : 'system')}
label={t('settings.messages.use_serif_font')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={thoughtAutoCollapse}
@@ -210,20 +197,18 @@ const ChatPreferenceSections: FC = () => {
label={t('chat.settings.thought_auto_collapse.label')}
hint={t('chat.settings.thought_auto_collapse.tip')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={showMessageOutline}
onCheckedChange={(checked) => setShowMessageOutline(checked)}
label={t('settings.messages.show_message_outline')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingRowTitleSmall>{t('message.message.style.label')}</SettingRowTitleSmall>
<Select value={messageStyle} onValueChange={setMessageStyle}>
<SelectTrigger size="sm" className="w-[220px] text-sm">
<SelectTrigger size="sm" className="min-w-0 max-w-56 flex-1 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent className="text-sm">
@@ -234,12 +219,11 @@ const ChatPreferenceSections: FC = () => {
))}
</SelectContent>
</Select>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingRowTitleSmall>{t('message.message.multi_model_style.label')}</SettingRowTitleSmall>
<Select value={multiModelMessageStyle} onValueChange={setMultiModelMessageStyle}>
<SelectTrigger size="sm" className="w-[220px] text-sm">
<SelectTrigger size="sm" className="min-w-0 max-w-56 flex-1 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent className="text-sm">
@@ -257,12 +241,11 @@ const ChatPreferenceSections: FC = () => {
</SelectItem>
</SelectContent>
</Select>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.navigation.label')}</SettingRowTitleSmall>
<Select value={messageNavigation} onValueChange={setMessageNavigation}>
<SelectTrigger size="sm" className="w-[220px] text-sm">
<SelectTrigger size="sm" className="min-w-0 max-w-56 flex-1 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent className="text-sm">
@@ -273,8 +256,7 @@ const ChatPreferenceSections: FC = () => {
))}
</SelectContent>
</Select>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow>
@@ -314,7 +296,7 @@ const ChatPreferenceSections: FC = () => {
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<Select value={codeStyle} onValueChange={onCodeStyleChange}>
<SelectTrigger size="sm" className="w-[220px] text-sm">
<SelectTrigger size="sm" className="min-w-0 max-w-56 flex-1 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent className="text-sm">
@@ -325,8 +307,7 @@ const ChatPreferenceSections: FC = () => {
))}
</SelectContent>
</Select>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={codeFancyBlock}
@@ -334,8 +315,7 @@ const ChatPreferenceSections: FC = () => {
label={t('chat.settings.code_fancy_block.label')}
hint={t('chat.settings.code_fancy_block.tip')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={codeExecution.enabled}
@@ -346,7 +326,7 @@ const ChatPreferenceSections: FC = () => {
</SettingRow>
{codeExecution.enabled && (
<>
<SettingDivider />
{' '}
<SettingRow className="pl-2">
<SettingRowTitleSmall hint={t('chat.settings.code_execution.timeout_minutes.tip')}>
{t('chat.settings.code_execution.timeout_minutes.label')}
@@ -362,8 +342,7 @@ const ChatPreferenceSections: FC = () => {
/>
</SettingRow>
</>
)}
<SettingDivider />
)}{' '}
<SettingRow>
<SettingSwitch
checked={codeEditor.enabled}
@@ -373,31 +352,28 @@ const ChatPreferenceSections: FC = () => {
</SettingRow>
{codeEditor.enabled && (
<>
<SettingDivider />
{' '}
<SettingRow className="pl-2">
<SettingSwitch
checked={codeEditor.highlightActiveLine}
onCheckedChange={(checked) => setCodeEditor({ highlightActiveLine: checked })}
label={t('chat.settings.code_editor.highlight_active_line')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow className="pl-2">
<SettingSwitch
checked={codeEditor.foldGutter}
onCheckedChange={(checked) => setCodeEditor({ foldGutter: checked })}
label={t('chat.settings.code_editor.fold_gutter')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow className="pl-2">
<SettingSwitch
checked={codeEditor.autocompletion}
onCheckedChange={(checked) => setCodeEditor({ autocompletion: checked })}
label={t('chat.settings.code_editor.autocompletion')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow className="pl-2">
<SettingSwitch
checked={codeEditor.keymap}
@@ -406,32 +382,28 @@ const ChatPreferenceSections: FC = () => {
/>
</SettingRow>
</>
)}
<SettingDivider />
)}{' '}
<SettingRow>
<SettingSwitch
checked={codeShowLineNumbers}
onCheckedChange={setCodeShowLineNumbers}
label={t('chat.settings.show_line_numbers')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={codeCollapsible}
onCheckedChange={setCodeCollapsible}
label={t('chat.settings.code_collapsible')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={codeWrappable}
onCheckedChange={setCodeWrappable}
label={t('chat.settings.code_wrappable')}
/>
</SettingRow>
<SettingDivider />
</SettingRow>{' '}
<SettingRow>
<SettingSwitch
checked={codeImageTools}

View File

@@ -10,7 +10,9 @@ export const SettingRowTitleSmall = ({
hint,
...rest
}: ComponentPropsWithoutRef<typeof SettingRowTitle> & { hint?: string }) => (
<SettingRowTitle className={cn('min-w-0 gap-1.5 text-foreground text-sm leading-4.5', className)} {...rest}>
<SettingRowTitle
className={cn('text-(length:--font-size-body-xs) min-w-0 gap-1.5 text-foreground leading-4.5', className)}
{...rest}>
<span className="min-w-0 truncate">{children}</span>
{hint && (
<Tooltip content={hint} placement="top" className="w-fit max-w-sm px-2.5 py-1.5 text-xs leading-relaxed">
@@ -41,4 +43,9 @@ export const SettingGroup = ({ className, ...rest }: ComponentPropsWithoutRef<'d
<div className={cn('flex w-full flex-col gap-0', className)} {...rest} />
)
// v2 settings card shell — rounded border with uniform row padding, no inter-row dividers.
export const SettingCard = ({ className, ...rest }: ComponentPropsWithoutRef<'div'>) => (
<div className={cn('mt-3 rounded-xl border border-border/60 py-1.5 *:px-4 *:py-1.5', className)} {...rest} />
)
export { SettingDivider }

View File

@@ -471,7 +471,7 @@ export const ComposerToolMenu = ({ inputAdapter }: ComposerToolMenuProps) => {
type="button"
className="flex size-[30px] shrink-0 items-center justify-center rounded-full text-foreground-secondary transition-colors hover:bg-accent hover:text-foreground"
aria-label={t('common.add')}>
<Plus size={18} />
<Plus size={18} strokeWidth={1.6} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent

View File

@@ -69,7 +69,7 @@ const useAttachmentToolController = ({ launcher, couldAddImageFile, extensions,
label: t('chat.input.upload.attachment'),
description: '',
tooltip: isDocumentOnly ? t('chat.input.upload.image_not_supported') : undefined,
icon: <Paperclip />,
icon: <Paperclip className="text-foreground" strokeWidth={1.6} />,
suffix: isDocumentOnly ? t('chat.input.upload.document_only') : undefined,
disabled,
action: () => {

View File

@@ -53,8 +53,7 @@ const useKnowledgeBaseToolController = ({
disabled,
disabledReason
}: Props) => {
const { i18n, t } = useTranslation()
const language = i18n.resolvedLanguage ?? i18n.language
const { t } = useTranslation()
const { isVisible: isQuickPanelVisible, symbol: quickPanelSymbol, updateList: updateQuickPanelList } = useQuickPanel()
const { bases: knowledgeBases } = useKnowledgeBases()
const onSelectRef = useRef(onSelect)
@@ -118,7 +117,7 @@ const useKnowledgeBaseToolController = ({
label: base.name,
description: tRef.current('library.config.knowledge.doc_count', { count: base.itemCount ?? 0 }),
filterText: [base.name, base.id].join(' '),
icon: <FileSearch />,
icon: <FileSearch className="text-foreground" strokeWidth={1.6} />,
isSelected: selectedBaseIds.has(base.id),
action: ({ context, inputAdapter, item }) => {
const nextSelectedIds = new Set(selectedBasesRef.current.map((selectedBase) => selectedBase.id))
@@ -133,7 +132,7 @@ const useKnowledgeBaseToolController = ({
closeKnowledgeBasePanelOnNextInput({ context, inputAdapter })
}
}))
}, [closeKnowledgeBasePanelOnNextInput, configuredBases, language, selectedBaseIds])
}, [closeKnowledgeBasePanelOnNextInput, configuredBases, selectedBaseIds])
const knowledgeBaseItems = useMemo(() => buildKnowledgeBaseItems(), [buildKnowledgeBaseItems])
@@ -190,7 +189,7 @@ const useKnowledgeBaseToolController = ({
label: t('chat.input.knowledge_base'),
description: resolvedDisabledReason ?? '',
disabledReason: resolvedDisabledReason,
icon: <FileSearch />,
icon: <FileSearch className="text-foreground" strokeWidth={1.6} />,
active: isEnabled,
showInActiveControls: false,
disabled: isDisabled,

View File

@@ -97,13 +97,13 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
if (isPromptsLoading && promptItems.length === 0) {
newList.push({
label: t('common.loading'),
icon: <Zap />,
icon: <Zap className="text-foreground" strokeWidth={1.6} />,
disabled: true
})
} else if (promptsError && promptItems.length === 0) {
newList.push({
label: formatErrorMessageWithPrefix(promptsError, t('settings.prompts.errors.loadFailed')),
icon: <Zap />,
icon: <Zap className="text-foreground" strokeWidth={1.6} />,
disabled: true
})
} else {
@@ -111,7 +111,7 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
...promptItems.map((item) => ({
label: item.title,
description: item.content,
icon: <Zap />,
icon: <Zap className="text-foreground" strokeWidth={1.6} />,
action: (options) => handleItemSelect(item, options)
}))
)
@@ -119,7 +119,7 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
newList.push({
label: t('settings.prompts.add') + '...',
icon: <Plus />,
icon: <Plus className="text-foreground" strokeWidth={1.6} />,
action: () => setIsAddModalOpen(true)
})
@@ -168,7 +168,7 @@ const useQuickPhrasesToolController = ({ launcher, setInputValue }: Props) => {
order: 70,
label: t('settings.prompts.title'),
description: '',
icon: <Zap />,
icon: <Zap className="text-foreground" strokeWidth={1.6} />,
action: ({ parentPanel, queryAnchor, triggerInfo }) => {
openQuickPanel(parentPanel, queryAnchor, triggerInfo)
}

View File

@@ -23,7 +23,7 @@ const useGenerateImageToolController = (context) => {
label: t('chat.input.generate_image'),
description: '',
disabledReason: t('chat.input.generate_image_not_supported'),
icon: <Image size={18} />,
icon: <Image className="text-foreground" strokeWidth={1.6} />,
active: enabled && isSupported,
disabled: !isSupported,
action: handleToggle

View File

@@ -50,7 +50,7 @@ function createEmptyMcpStatusItem(label: string): QuickPanelListItem {
return {
id: 'mcp-status-empty',
label,
icon: <Cable />,
icon: <Cable className="text-foreground" strokeWidth={1.6} />,
disabled: true
}
}
@@ -69,7 +69,7 @@ function createMcpStatusItem(
label: server?.name ?? t('settings.quickPanel.mcp.unknownServer', 'Unknown MCP server'),
description,
filterText: [server?.name, server?.description, description].filter(Boolean).join(' '),
icon: <Cable />
icon: <Cable className="text-foreground" strokeWidth={1.6} />
}
}
@@ -159,7 +159,7 @@ export function createMcpStatusLauncher(
: t('settings.quickPanel.mcp.description', 'View configured MCP server status'),
disabledReason: isDisabled ? modeLabel : undefined,
disabled: isDisabled,
icon: <Cable />,
icon: <Cable className="text-foreground" strokeWidth={1.6} />,
action: isDisabled
? undefined
: ({ inputAdapter, parentPanel, queryAnchor, quickPanel, triggerInfo }) => {

View File

@@ -11,15 +11,15 @@ import { useCallback, useEffect, useMemo } from 'react'
const getPermissionModeIcon = (mode: PermissionMode): ReactNode => {
switch (mode) {
case 'default':
return <Pointer size={18} color="#00b96b" />
return <Pointer className="text-foreground" strokeWidth={1.6} />
case 'plan':
return <Route size={18} color="#faad14" />
return <Route className="text-foreground" strokeWidth={1.6} />
case 'acceptEdits':
return <FolderPen size={18} color="#52c41a" />
return <FolderPen className="text-foreground" strokeWidth={1.6} />
case 'bypassPermissions':
return <RefreshCcw size={18} color="#722ed1" />
return <RefreshCcw className="text-foreground" strokeWidth={1.6} />
default:
return <Pointer size={18} color="#00b96b" />
return <Pointer className="text-foreground" strokeWidth={1.6} />
}
}

View File

@@ -75,7 +75,7 @@ const slashCommandsTool = defineTool({
order: 20 + (index + 1) / 100,
label: cmd.command,
description: descriptionKey ? t(descriptionKey, cmd.description || '') : cmd.description || '',
icon: <Terminal size={16} />,
icon: <Terminal size={16} className="text-foreground" strokeWidth={1.6} />,
action: ({ inputAdapter }) => {
insertSlashCommand(cmd.command, actions.onTextChange, inputAdapter)
}
@@ -91,7 +91,7 @@ const slashCommandsTool = defineTool({
order: 20,
label: t('chat.input.slash_commands.title'),
description: t('chat.input.slash_commands.description'),
icon: <Terminal size={16} />,
icon: <Terminal size={16} className="text-foreground" strokeWidth={1.6} />,
submenu: commandLaunchers,
action: ({ quickPanel, inputAdapter, parentPanel, queryAnchor, triggerInfo }) => {
const list: QuickPanelListItem[] = commandLaunchers.map((launcher) => ({

View File

@@ -345,7 +345,7 @@ export function ResourceSelectorShell<T extends ResourceSelectorShellItem>(props
nextSections.push({
key: 'pinned',
header: (
<div className="group flex h-7 items-center gap-1 bg-popover px-3 text-[11px] text-muted-foreground">
<div className="group text-(length:--font-size-body-xs) flex h-7 items-center gap-1 bg-popover px-3 text-muted-foreground">
<span className="truncate">{labels.pinnedTitle}</span>
</div>
),
@@ -566,7 +566,9 @@ export function ResourceSelectorShell<T extends ResourceSelectorShellItem>(props
const filterContent =
tagOptions.length > 0 ? (
<>
{labels.tagFilter ? <span className="mr-1 text-[10px] text-muted-foreground">{labels.tagFilter}</span> : null}
{labels.tagFilter ? (
<span className="text-(length:--font-size-body-2xs) mr-1 text-muted-foreground">{labels.tagFilter}</span>
) : null}
{tagOptions.map((tag) => {
const active = selectedTagName === tag.name
return (
@@ -616,7 +618,6 @@ export function ResourceSelectorShell<T extends ResourceSelectorShellItem>(props
selected={isSelected}
focused={isActive}
disabled={item.disabled}
showSelectedIndicator={!multiEnabled && isSelected}
checkbox={
multiEnabled ? (
<Checkbox

View File

@@ -143,7 +143,6 @@ export function WorkspaceSelector({
<div key={workspace.id} className="py-0.5">
<ModelSelectorRow
selected={selected}
showSelectedIndicator={selected}
leading={<Folder className="size-4 text-muted-foreground/70" />}
onSelect={() => void handleSelectWorkspace(workspace.id)}
rootProps={{ 'data-option-row': workspace.id }}

View File

@@ -3,6 +3,7 @@ import { usePreference } from '@data/hooks/usePreference'
import { setInlineFilePathHomePath } from '@renderer/components/chat/messages/utils/filePath'
import db from '@renderer/databases'
import { useAppUpdateHandler } from '@renderer/hooks/useAppUpdate'
import useMacTransparentWindow from '@renderer/hooks/useMacTransparentWindow'
import { useStorageMonitorNotification } from '@renderer/hooks/useStorageMonitorNotification'
import i18n, { setDayjsLocale } from '@renderer/i18n'
import { defaultLanguage } from '@shared/utils/languages'
@@ -19,6 +20,7 @@ export function useAppInit() {
const savedAvatar = useLiveQuery(() => db.settings.get('image://avatar'))
const navBackgroundColor = useNavBackgroundColor()
const isMacTransparentWindow = useMacTransparentWindow()
useEffect(() => {
document.getElementById('spinner')?.remove()
@@ -60,8 +62,11 @@ export function useAppInit() {
}, [language])
useEffect(() => {
window.root.style.background = navBackgroundColor
}, [navBackgroundColor])
// In mac transparent mode the shell owns the wash (sidebar tint while the
// window is key, opaque sidebar when blurred — see AppShell); #root stays
// transparent so the native vibrancy can show through the tint.
window.root.style.background = isMacTransparentWindow ? 'transparent' : navBackgroundColor
}, [isMacTransparentWindow, navBackgroundColor])
useEffect(() => {
// set app paths

View File

@@ -1,6 +1,14 @@
import { Button, PageSidePanelItem, PageSidePanelSection, Slider, Switch, Tooltip } from '@cherrystudio/ui'
import {
Button,
Combobox,
InfoTooltip,
PageSidePanelItem,
PageSidePanelSection,
Slider,
Switch,
Tooltip
} from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference'
import Selector from '@renderer/components/Selector'
import type { MiniAppRegionFilter } from '@shared/data/types/miniApp'
import { Undo2 } from 'lucide-react'
import type { FC } from 'react'
@@ -11,8 +19,8 @@ const DEFAULT_MAX_KEEPALIVE = 3
/**
* "Preferences" group of the display-settings drawer: region filter, open-link
* external switch, and the max keep-alive slider. Every item follows the same
* title + description + control structure.
* external switch, and the max keep-alive slider. Every item pairs a title +
* info tooltip with its control.
*/
const MiniAppDisplaySettings: FC = () => {
const { t } = useTranslation()
@@ -56,34 +64,47 @@ const MiniAppDisplaySettings: FC = () => {
{/* Roomier gap between items so each title + description block reads as its own unit. */}
<div className="flex flex-col gap-5">
<PageSidePanelItem
title={t('settings.miniApps.region.title')}
description={t('settings.miniApps.region.description')}
title={
<span className="inline-flex items-center gap-1">
{t('settings.miniApps.region.title')}
<InfoTooltip content={t('settings.miniApps.region.description')} />
</span>
}
action={
<Selector
size={14}
<Combobox
searchable={false}
value={region}
onChange={(v: MiniAppRegionFilter) => setRegion(v)}
onChange={(value) => setRegion(value as MiniAppRegionFilter)}
options={regionOptions}
width={140}
/>
}
/>
<PageSidePanelItem
title={t('settings.miniApps.open_link_external.title')}
description={t('settings.miniApps.open_link_external.description')}
title={
<span className="inline-flex items-center gap-1">
{t('settings.miniApps.open_link_external.title')}
<InfoTooltip content={t('settings.miniApps.open_link_external.description')} />
</span>
}
action={<Switch checked={openLinkExternal} onCheckedChange={(v) => setOpenLinkExternal(v)} />}
/>
<PageSidePanelItem
title={t('settings.miniApps.cache_title')}
description={t('settings.miniApps.cache_description')}
title={
<span className="inline-flex items-center gap-1">
{t('settings.miniApps.cache_title')}
<InfoTooltip content={t('settings.miniApps.cache_description')} />
</span>
}
action={
<Tooltip content={t('settings.miniApps.reset_tooltip')}>
<Button
variant="ghost"
size="icon-sm"
onClick={handleResetCacheLimit}
className="shrink-0 text-muted-foreground hover:text-foreground"
className="shrink-0 text-foreground/80 hover:text-foreground [&_svg]:[stroke-width:1.6]"
aria-label={t('settings.miniApps.reset_tooltip')}>
<Undo2 />
</Button>

View File

@@ -79,7 +79,7 @@ const MiniAppListColumn: FC<Props> = ({ title, count, apps, onToggle, onReorder,
<span
className="flex size-6 shrink-0 items-center justify-center text-muted-foreground/40"
aria-hidden="true">
<Icon className="size-3.5" />
<Icon className="size-3.5" strokeWidth={1.6} />
</span>
</div>
</Tooltip>

View File

@@ -25,11 +25,11 @@ const MiniAppListPair: FC<Props> = ({ visible, hidden, hide, show, reorderVisibl
actions={
<>
<Button variant="secondary" size="sm" onClick={swap}>
<ArrowLeftRight />
<ArrowLeftRight strokeWidth={1.6} />
{t('common.swap')}
</Button>
<Button variant="secondary" size="sm" onClick={reset}>
<RotateCcw />
<RotateCcw strokeWidth={1.6} />
{t('common.reset')}
</Button>
</>
@@ -45,7 +45,7 @@ const MiniAppListPair: FC<Props> = ({ visible, hidden, hide, show, reorderVisibl
onReorder={reorderVisible}
toggleAction="hide"
/>
<Separator orientation="vertical" />
<Separator orientation="vertical" className="bg-border-subtle" />
<MiniAppListColumn
title={t('settings.miniApps.disabled')}
count={hidden.length}

View File

@@ -59,14 +59,14 @@ const MiniAppsPage: FC = () => {
setEditingApp(null)
setNewAppOpen(true)
}}>
<Plus size={14} />
<Plus size={14} strokeWidth={1.6} />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label={t('settings.miniApps.display_title')}
onClick={() => setSettingsOpen(true)}>
<Menu size={14} />
<Menu size={14} strokeWidth={1.6} />
</Button>
</div>
</div>
@@ -103,7 +103,7 @@ const MiniAppsPage: FC = () => {
/>
</div>
) : (
<div className="grid w-full grid-cols-[repeat(auto-fill,minmax(84px,92px))] justify-center gap-x-4 gap-y-8 px-2 pt-12 pb-8 sm:gap-x-5 md:gap-x-6">
<div className="grid w-full grid-cols-[repeat(auto-fill,minmax(84px,92px))] justify-center gap-x-3 gap-y-4 px-2 pt-8 pb-8 sm:gap-x-4 md:gap-x-5">
{filteredApps.map((app) => (
<App key={app.appId} app={app} size={44} variant="launchpad" onEditCustom={setEditingApp} />
))}

View File

@@ -341,12 +341,12 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
const toolbarButtonClassName = ({ disabled = false, active = false }: { disabled?: boolean; active?: boolean } = {}) =>
cn(
'rounded shadow-none active:scale-95',
'rounded shadow-none active:scale-95 [&_svg]:[stroke-width:1.6]',
disabled
? 'cursor-default text-foreground-muted hover:bg-transparent hover:text-foreground-muted active:scale-100'
: active
? 'text-primary hover:text-primary'
: 'text-foreground-secondary hover:text-foreground'
: 'text-foreground/80 hover:text-foreground'
)
export default MinimalToolbar

View File

@@ -336,7 +336,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
onClick={goToPrevious}
disabled={disableNavigation}
aria-label={t('common.previous_match')}
className="text-foreground-secondary shadow-none hover:text-foreground">
className="text-foreground/80 shadow-none hover:text-foreground [&_svg]:[stroke-width:1.6]">
<ChevronUp size={16} />
</Button>
<Button
@@ -346,7 +346,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
onClick={goToNext}
disabled={disableNavigation}
aria-label={t('common.next_match')}
className="text-foreground-secondary shadow-none hover:text-foreground">
className="text-foreground/80 shadow-none hover:text-foreground [&_svg]:[stroke-width:1.6]">
<ChevronDown size={16} />
</Button>
<div className="h-4 w-px bg-border" />
@@ -356,7 +356,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
size="icon-sm"
onClick={closeSearch}
aria-label={t('common.close')}
className="text-foreground-secondary shadow-none hover:text-foreground">
className="text-foreground/80 shadow-none hover:text-foreground [&_svg]:[stroke-width:1.6]">
<X size={16} />
</Button>
</div>

View File

@@ -57,7 +57,7 @@ const DataSettings: FC = () => {
]
return (
<RowFlex className="flex-1">
<RowFlex className="min-w-0 flex-1">
<div
className={`flex flex-col ${settingsSubmenuScrollClassName} [&_.iconfont]:text-current [&_.iconfont]:leading-4`}>
<PageHeader title={t('settings.data.title')} />
@@ -84,7 +84,7 @@ const DataSettings: FC = () => {
</MenuList>
</Scrollbar>
</div>
<SettingsContentColumn theme={theme}>
<SettingsContentColumn theme={theme} className="min-w-0">
{menu === 'data' && <BasicDataSettings />}
{menu === 'webdav' && <WebDavSettings />}
{menu === 'nutstore' && <NutstoreSettings />}

View File

@@ -83,7 +83,8 @@ vi.mock('@cherrystudio/ui', () => {
DialogFooter: passthrough('div'),
DialogHeader: passthrough('div'),
DialogTitle: passthrough('div'),
Input: passthrough('input')
Input: passthrough('input'),
Tooltip: childrenOnly
}
})

View File

@@ -1122,7 +1122,7 @@ const TasksSettings: FC = () => {
return (
<div className="flex min-w-0 flex-1">
<div
className="flex w-full flex-1 flex-row overflow-hidden"
className="flex w-full min-w-0 flex-1 flex-row overflow-hidden"
style={{ height: 'calc(100vh - var(--navbar-height) - 6px)' }}>
{/* Left panel: task list */}
<Scrollbar
@@ -1187,10 +1187,12 @@ const TasksSettings: FC = () => {
onToggleStatus={handleToggleStatus}
/>
) : (
<div className="flex flex-1 items-center justify-center text-foreground-muted text-sm">
{tasks.length > 0
? t('settings.scheduledTasks.selectTask', 'Select a task to view details')
: t('settings.scheduledTasks.noTasks')}
<div className="flex flex-1 items-center justify-center px-6 text-center text-foreground-muted text-sm">
<span className="max-w-xs leading-relaxed">
{tasks.length > 0
? t('settings.scheduledTasks.selectTask', 'Select a task to view details')
: t('settings.scheduledTasks.noTasks')}
</span>
</div>
)}
</div>

View File

@@ -34,7 +34,7 @@ const CutoffSettings = () => {
}}
/>
</SettingRowTitle>
<div className="flex w-56 shrink-0">
<div className="flex min-w-0 max-w-56 flex-1">
<Input
placeholder={t('settings.tool.websearch.compression.cutoff.limit.placeholder')}
value={compressionConfig?.cutoffLimit === undefined ? '' : compressionConfig.cutoffLimit}

View File

@@ -9,7 +9,7 @@ import CutoffSettings from './CutoffSettings'
const settingRowClassName = 'items-center justify-between gap-6 py-1'
const settingLabelClassName = 'min-w-0 flex-1'
const selectTriggerClassName = 'h-8 w-56 text-sm'
const selectTriggerClassName = 'h-8 min-w-0 max-w-56 flex-1 text-sm'
const CompressionSettings = () => {
const { t } = useTranslation()

View File

@@ -347,6 +347,7 @@ vi.mock('@cherrystudio/ui', () => {
Textarea: {
Input: (props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => <textarea {...props} />
},
NormalTooltip: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
Tooltip: ({ children, title }: { children?: React.ReactNode; title?: React.ReactNode }) => (
<div data-testid="tooltip">
{children}

View File

@@ -464,6 +464,16 @@ vi.mock('@cherrystudio/ui', () => {
const context = React.useContext(PopoverContext)
return context.open ? React.createElement('div', { ...props, 'data-testid': 'popover-content' }, children) : null
},
ColorPicker: ({ children, value, defaultValue, onChange, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'color-picker' }, children),
ColorPickerSelection: (props) => React.createElement('div', { ...props, 'data-testid': 'color-picker-selection' }),
ColorPickerHue: (props) => React.createElement('div', { ...props, 'data-testid': 'color-picker-hue' }),
ColorPickerAlpha: (props) => React.createElement('div', { ...props, 'data-testid': 'color-picker-alpha' }),
ColorPickerEyeDropper: ({ size, ...props }) =>
React.createElement('button', { ...props, type: 'button', 'data-testid': 'color-picker-eye-dropper' }),
ColorPickerOutput: ({ size, ...props }) =>
React.createElement('button', { ...props, type: 'button', 'data-testid': 'color-picker-output' }),
ColorPickerFormat: (props) => React.createElement('div', { ...props, 'data-testid': 'color-picker-format' }),
MenuList: ({ children, ...props }) =>
React.createElement('div', { ...props, 'data-testid': 'menu-list' }, children),
MenuDivider: (props) => React.createElement('div', { ...props, 'data-testid': 'menu-divider' }),