mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 12:27:41 +08:00
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:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);')
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}</>
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
Reference in New Issue
Block a user