Compare commits

..

2 Commits

Author SHA1 Message Date
suyao
185eff16b6 Merge remote-tracking branch 'origin/v2' into fix/v2/inputbar-cache 2025-12-01 13:53:27 +08:00
suyao
13abc2d653 Backport: PR 11558 to v2 2025-12-01 03:23:46 +08:00
70 changed files with 425 additions and 6006 deletions

View File

@@ -143,31 +143,19 @@ export default defineConfig([
files: ['**/*.{ts,tsx,js,jsx}'],
ignores: [],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
// {
// name: 'antd',
// importNames: ['Flex', 'Switch', 'message', 'Button', 'Tooltip'],
// message:
// '❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"'
// },
{
name: 'antd',
importNames: ['Switch'],
message:
'❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"'
},
{
name: '@heroui/react',
importNames: ['Switch'],
message:
'❌ Do not import the component from heroui directly. It\'s deprecated.'
}
]
}
]
// 'no-restricted-imports': [
// 'error',
// {
// paths: [
// {
// name: 'antd',
// importNames: ['Flex', 'Switch', 'message', 'Button', 'Tooltip'],
// message:
// '❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"'
// }
// ]
// }
// ]
}
},
// Schema key naming convention (cache & preferences)

View File

@@ -179,7 +179,6 @@
"@opeoginni/github-copilot-openai-compatible": "^0.1.21",
"@playwright/test": "^1.55.1",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-switch": "^1.2.6",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.12.0",
"@swc/plugin-styled-components": "^8.0.4",

View File

@@ -1,4 +0,0 @@
import type { CompositeInputProps, SelectGroup, SelectItem } from './input'
import { CompositeInput } from './input'
export { CompositeInput, type CompositeInputProps, type SelectGroup, type SelectItem }

View File

@@ -1,371 +0,0 @@
import { cn, toUndefinedIfNull } from '@cherrystudio/ui/utils'
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
import { Edit2Icon, EyeIcon, EyeOffIcon } from 'lucide-react'
import type { ReactNode } from 'react'
import { useCallback, useMemo, useState } from 'react'
import type { InputProps } from '../../primitives/input'
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '../../primitives/input-group'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue
} from '../../primitives/select'
const inputGroupVariants = cva(
[
'h-auto',
'rounded-xs',
'has-[[data-slot=input-group-control]:focus-visible]:ring-ring/40',
'has-[[data-slot=input-group-control]:focus-visible]:border-[#3CD45A]'
],
{
variants: {
disabled: {
false: null,
true: ['bg-background-subtle', 'border-border-hover', 'cursor-not-allowed']
}
},
defaultVariants: {
disabled: false
}
}
)
const inputVariants = cva(['p-0', 'h-fit', 'min-w-0'], {
variants: {
size: {
sm: ['text-sm', 'leading-4'],
md: ['leading-4.5'],
lg: ['text-lg', 'leading-5']
},
variant: {
default: [],
button: [],
email: [],
select: []
},
disabled: {
false: null,
true: ['text-foreground/40', 'placeholder:text-foreground/40', 'disabled:opacity-100']
}
},
defaultVariants: {
size: 'md',
variant: 'default',
disabled: false
}
})
const inputWrapperVariants = cva(['flex', 'flex-1', 'items-center', 'gap-2'], {
variants: {
size: {
sm: ['p-3xs'],
// Why only the md size is fixed height???
md: ['p-3xs', 'h-5.5', 'box-content'],
lg: ['px-2xs', 'py-3xs']
},
variant: {
default: [],
button: 'border-r-[1px]',
email: [],
select: []
},
disabled: {
false: null,
true: 'border-background-subtle'
}
},
defaultVariants: {
disabled: false
}
})
const iconVariants = cva([], {
variants: {
size: {
sm: 'size-4.5',
md: 'size-5',
lg: 'size-6'
},
disabled: {
false: null,
true: 'text-foreground/40'
}
},
defaultVariants: {
size: 'md',
disabled: false
}
})
const iconButtonVariants = cva(['text-foreground/60 cursor-pointer transition-colors', 'hover:shadow-none'], {
variants: {
disabled: {
false: null,
true: []
}
},
defaultVariants: {
disabled: false
}
})
const buttonVariants = cva(
['py-3xs', 'flex flex-col', 'text-foreground/60 cursor-pointer transition-colors', 'hover:shadow-none'],
{
variants: {
size: {
sm: 'px-3xs',
md: 'px-3xs',
lg: 'px-2xs'
},
disabled: {
false: null,
true: ['pointer-events-none']
}
},
defaultVariants: {
size: 'md',
disabled: false
}
}
)
const buttonLabelVariants = cva([], {
variants: {
size: {
// TODO: p/font-family, p/letter-spacing ... p?
sm: 'text-sm leading-4',
md: 'leading-4.5',
lg: 'text-lg leading-5 tracking-normal'
},
disabled: {
false: null,
true: ['text-foreground/40']
}
},
defaultVariants: {
size: 'md',
disabled: false
}
})
const prefixVariants = cva(['font-medium', 'border-r-[1px]', 'text-foreground/60'], {
variants: {
size: {
// TODO: semantic letter-spacing
sm: ['text-sm leading-4', 'p-3xs'],
md: ['leading-4.5', 'p-3xs'],
lg: ['leading-5 tracking-normal', 'px-2xs py-3xs']
},
disabled: {
false: null,
true: 'text-foreground/40'
}
},
defaultVariants: {
size: 'md',
disabled: false
}
})
const selectPrefixVariants = cva(['font-medium', 'border-r-[1px]', 'text-foreground/60', 'p-0'], {
variants: {
size: {
// TODO: semantic letter-spacing
sm: 'text-sm leading-4',
md: 'leading-4.5',
lg: 'leading-5 tracking-normal'
},
disabled: {
false: null,
true: 'text-foreground/40'
}
},
defaultVariants: {
size: 'md',
disabled: false
}
})
const selectTriggerVariants = cva(
[
'border-none box-content pl-3 aria-expanded:border-none aria-expanded:ring-0 bg-transparent',
'*:data-[slot=select-value]:text-foreground',
'[&_svg]:text-secondary-foreground!'
],
{
variants: {
size: {
sm: ['h-5', 'pl-6 pr-3xs py-3', '*:data-[slot=select-value]:text-sm'],
md: ['h-5', 'pl-6 pr-3xs py-[13px]'],
lg: ['h-6', 'pl-7 pr-2xs py-3', '*:data-[slot=select-value]:text-lg']
}
}
}
)
const selectTriggerLabelVariants = cva([], {
variants: {
size: {
// TODO: p/font-family, p/letter-spacing ... p?
sm: 'text-sm leading-4',
md: 'leading-4.5',
lg: 'text-lg leading-5 tracking-normal'
}
}
})
function ShowPasswordButton({
type,
setType,
size = 'md',
disabled = false
}: {
type: 'text' | 'password'
setType: React.Dispatch<React.SetStateAction<'text' | 'password'>>
size: VariantProps<typeof inputVariants>['size']
disabled: boolean
}) {
const togglePassword = useCallback(() => {
if (disabled) return
if (type === 'password') {
setType('text')
} else if (type === 'text') {
setType('password')
}
}, [disabled, setType, type])
const iconClassName = iconVariants({ size, disabled })
return (
<InputGroupButton onClick={togglePassword} disabled={disabled} className={iconButtonVariants({ disabled })}>
{type === 'text' && <EyeIcon className={iconClassName} />}
{type === 'password' && <EyeOffIcon className={iconClassName} />}
</InputGroupButton>
)
}
interface SelectItem {
label: ReactNode
value: string
}
interface SelectGroup {
label: ReactNode
items: SelectItem[]
}
interface CompositeInputProps
extends Omit<InputProps, 'size' | 'disabled' | 'prefix'>,
VariantProps<typeof inputVariants> {
buttonProps?: {
label?: ReactNode
onClick: React.DOMAttributes<HTMLButtonElement>['onClick']
}
prefix?: ReactNode
selectProps?: {
groups: SelectGroup[]
placeholder?: string
}
}
function CompositeInput({
type = 'text',
size = 'md',
variant = 'default',
disabled = false,
buttonProps,
prefix,
selectProps,
className,
...rest
}: CompositeInputProps) {
const isPassword = type === 'password'
const [htmlType, setHtmlType] = useState<'text' | 'password'>('password')
const buttonContent = useMemo(() => {
if (buttonProps === undefined) {
console.warn("CustomizedInput: 'button' variant requires a 'button' prop to be provided.")
return null
} else {
return (
<InputGroupButton className={buttonVariants({ size, disabled })} onClick={buttonProps.onClick}>
<div className={buttonLabelVariants({ size, disabled })}>{buttonProps.label}</div>
</InputGroupButton>
)
}
}, [buttonProps, disabled, size])
const emailContent = useMemo(() => {
if (!prefix) {
console.warn('CompositeInput: "email" variant requires a "prefix" prop to be provided.')
return null
} else {
return <div className={prefixVariants({ size, disabled })}>{prefix}</div>
}
}, [disabled, prefix, size])
const selectContent = useMemo(() => {
if (!selectProps) {
console.warn('CompositeInput: "select" variant requires a "selectProps" prop to be provided.')
return null
} else {
return (
<div className={selectPrefixVariants({ size, disabled })}>
<Select>
<SelectTrigger className={selectTriggerVariants({ size })}>
<SelectValue placeholder={selectProps.placeholder} className={selectTriggerLabelVariants({ size })} />
</SelectTrigger>
<SelectContent>
{selectProps.groups.map((group, index) => (
<SelectGroup key={index}>
<SelectLabel>{group.label}</SelectLabel>
{group.items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
</div>
)
}
}, [disabled, selectProps, size])
return (
<InputGroup className={inputGroupVariants({ disabled })}>
{variant === 'email' && emailContent}
{variant === 'select' && selectContent}
<div className={inputWrapperVariants({ size, variant, disabled })}>
<InputGroupInput
type={isPassword ? htmlType : type}
disabled={toUndefinedIfNull(disabled)}
className={cn(inputVariants({ size, variant, disabled }), className)}
{...rest}
/>
{(variant === 'default' || variant === 'button') && (
<>
<InputGroupAddon className="p-0">
<Edit2Icon className={iconVariants({ size, disabled })} />
</InputGroupAddon>
<InputGroupAddon align="inline-end" className="p-0">
<ShowPasswordButton type={htmlType} setType={setHtmlType} size={size} disabled={!!disabled} />
</InputGroupAddon>
</>
)}
</div>
{variant === 'button' && buttonContent}
</InputGroup>
)
}
export { CompositeInput, type CompositeInputProps, type SelectGroup, type SelectItem }

View File

@@ -49,12 +49,6 @@ export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './
// ImageToolButton
export { default as ImageToolButton } from './composites/ImageToolButton'
// Sortable
export {
CompositeInput,
type CompositeInputProps,
type SelectGroup as CompositeInputSelectGroup,
type SelectItem as CompositeInputSelectItem
} from './composites/Input'
export { Sortable } from './composites/Sortable'
/* Shadcn Primitive Components */
@@ -64,8 +58,6 @@ export * from './primitives/checkbox'
export * from './primitives/combobox'
export * from './primitives/command'
export * from './primitives/dialog'
export * from './primitives/input'
export * from './primitives/input-group'
export * from './primitives/kbd'
export * from './primitives/pagination'
export * from './primitives/popover'
@@ -73,4 +65,3 @@ export * from './primitives/radioGroup'
export * from './primitives/select'
export * from './primitives/shadcn-io/dropzone'
export * from './primitives/tabs'
export * from './primitives/textarea'

View File

@@ -1,147 +0,0 @@
import { Button } from '@cherrystudio/ui/components/primitives/button'
import type { InputProps } from '@cherrystudio/ui/components/primitives/input'
import { Input } from '@cherrystudio/ui/components/primitives/input'
import { Textarea } from '@cherrystudio/ui/components/primitives/textarea'
import { cn } from '@cherrystudio/ui/utils/index'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
'group/input-group border-input bg-background relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
'h-9 min-w-0 has-[>textarea]:h-auto',
// Variants based on alignment.
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
// Focus state.
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
// Error state.
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
'block-start':
'order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5',
'block-end': 'order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5'
}
},
defaultVariants: {
align: 'inline-start'
}
}
)
function InputGroupAddon({
className,
align = 'inline-start',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button')) {
return
}
e.currentTarget.parentElement?.querySelector('input')?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva('text-sm shadow-none flex gap-2 items-center', {
variants: {
size: {
xs: "h-6 gap-1 px-2 [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: 'h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5',
'icon-xs': 'size-6 p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0'
}
},
defaultVariants: {
size: 'xs'
}
})
function InputGroupButton({
className,
type = 'button',
variant = 'ghost',
size = 'xs',
...props
}: Omit<React.ComponentProps<typeof Button>, 'size'> & VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({ className, ...props }: InputProps) {
return (
<Input
data-slot="input-group-control"
className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
className
)}
{...props}
/>
)
}
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
className
)}
{...props}
/>
)
}
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea }

View File

@@ -1,23 +0,0 @@
import { cn } from '@cherrystudio/ui/utils'
import * as React from 'react'
interface InputProps extends React.ComponentProps<'input'> {}
function Input({ className, type, ...props }: InputProps) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'disabled:opacity-50',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
{...props}
/>
)
}
export { Input, type InputProps }

View File

@@ -1,178 +1,54 @@
import { cn } from '@cherrystudio/ui/utils'
import * as SwitchPrimitive from '@radix-ui/react-switch'
import { cva } from 'class-variance-authority'
import * as React from 'react'
import { useId } from 'react'
const switchRootVariants = cva(
[
'cs-switch cs-switch-root',
'group relative cursor-pointer peer inline-flex shrink-0 items-center rounded-full shadow-xs outline-none transition-all',
'data-[state=unchecked]:bg-gray-500/20 data-[state=checked]:bg-primary',
'disabled:cursor-not-allowed disabled:opacity-40',
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50'
],
{
variants: {
size: {
sm: ['w-9 h-5'],
md: ['w-11 h-5.5'],
lg: ['w-11 h-6']
},
loading: {
false: null,
true: ['bg-primary-hover!']
}
},
defaultVariants: {
size: 'md',
loading: false
}
}
)
const switchThumbVariants = cva(
[
'cs-switch cs-switch-thumb',
'pointer-events-none block rounded-full ring-0 transition-all data-[state=unchecked]:translate-x-0'
],
{
variants: {
size: {
sm: ['size-4.5 ml-[1px] data-[state=checked]:translate-x-4'],
md: ['size-[19px] ml-0.5 data-[state=checked]:translate-x-[21px]'],
lg: ['size-5 ml-[3px] data-[state=checked]:translate-x-4.5']
},
loading: {
false: null,
true: ['bg-primary-hover!']
}
},
compoundVariants: [
{
size: 'sm',
loading: true,
className: 'size-3.5 ml-0.5 data-[state=checked]:translate-x-4.5'
},
{
size: 'md',
loading: true,
className: 'size-4 ml-1 data-[state=checked]:translate-x-5'
},
{
size: 'lg',
loading: true,
className: 'size-4.5 ml-1 data-[state=checked]:translate-x-4.5'
}
]
}
)
const switchThumbSvgVariants = cva(['transition-all'], {
variants: {
loading: {
false: null,
true: ['animate-spin']
}
},
defaultVariants: {
loading: false
}
})
import type { SwitchProps } from '@heroui/react'
import { cn, Spinner, Switch } from '@heroui/react'
// Enhanced Switch component with loading state support
interface SwitchProps extends Omit<React.ComponentProps<typeof SwitchPrimitive.Root>, 'children'> {
/** When true, displays a loading animation in the switch thumb. Defaults to false when undefined. */
loading?: boolean
size?: 'sm' | 'md' | 'lg'
classNames?: {
root?: string
thumb?: string
thumbSvg?: string
}
interface CustomSwitchProps extends SwitchProps {
isLoading?: boolean
}
function Switch({ loading = false, size = 'md', className, classNames, ...props }: SwitchProps) {
/**
* A customized Switch component based on HeroUI Switch
* @see https://www.heroui.com/docs/components/switch#api
* @param isLoading When true, displays a loading spinner in the switch thumb
*/
const CustomizedSwitch = ({ isLoading, children, ref, thumbIcon, ...props }: CustomSwitchProps) => {
const finalThumbIcon = isLoading ? <Spinner size="sm" /> : thumbIcon
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(switchRootVariants({ size, loading }), className, classNames?.root)}
<Switch ref={ref} {...props} thumbIcon={finalThumbIcon}>
{children}
</Switch>
)
}
const DescriptionSwitch = ({ children, ...props }: CustomSwitchProps) => {
return (
<CustomizedSwitch
size="sm"
classNames={{
base: cn(
'inline-flex w-full max-w-md flex-row-reverse items-center hover:bg-content2',
'cursor-pointer justify-between gap-2 rounded-lg border-2 border-transparent py-2 pr-1',
'data-[selected=true]:border-primary'
),
wrapper: 'p-0 h-4 overflow-visible',
thumb: cn(
'h-6 w-6 border-2 shadow-lg',
'group-data-[hover=true]:border-primary',
//selected
'group-data-[selected=true]:ms-6',
// pressed
'group-data-[pressed=true]:w-7',
'group-data-pressed:group-data-selected:ms-4'
)
}}
{...props}>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(switchThumbVariants({ size, loading }), classNames?.thumb)}>
<svg
width="inherit"
height="inherit"
viewBox="0 0 19 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn(switchThumbSvgVariants({ loading }), classNames?.thumbSvg)}>
<path
d="M9.5 0C14.7467 0 19 4.25329 19 9.5C19 14.7467 14.7467 19 9.5 19C4.25329 19 0 14.7467 0 9.5C0 4.25329 4.25329 0 9.5 0ZM9.5 6.33301C8.91711 6.33301 8.44445 6.8058 8.44434 7.38867V11.6113C8.44445 12.1942 8.91711 12.667 9.5 12.667C10.0829 12.667 10.5555 12.1942 10.5557 11.6113V7.38867C10.5555 6.8058 10.0829 6.33301 9.5 6.33301Z"
fill="white"
/>
</svg>
</SwitchPrimitive.Thumb>
</SwitchPrimitive.Root>
{children}
</CustomizedSwitch>
)
}
interface DescriptionSwitchProps extends SwitchProps {
/** Text label displayed next to the switch. */
label: string
/** Optional helper text shown below the label. */
description?: string
/** Switch position relative to label. Defaults to 'right'. */
position?: 'left' | 'right'
}
CustomizedSwitch.displayName = 'Switch'
// TODO: It's not finished. We need to use Typography components instead of native html element.
const DescriptionSwitch = ({
label,
description,
position = 'right',
size = 'md',
...props
}: DescriptionSwitchProps) => {
const isLeftSide = position === 'left'
const id = useId()
return (
<div className={cn('flex w-full gap-3 justify-between p-4xs', isLeftSide && 'flex-row-reverse')}>
<label className={cn('flex flex-col gap-5xs cursor-pointer')} htmlFor={id}>
{/* TODO: use standard typography component */}
<p
className={cn(
'font-medium tracking-normal',
{
'text-sm leading-4': size === 'sm',
'text-md leading-4.5': size === 'md',
'text-lg leading-5.5': size === 'lg'
},
isLeftSide && 'text-right'
)}>
{label}
</p>
{/* TODO: use standard typography component */}
{description && (
<span
className={cn('text-foreground-secondary', {
'text-[10px] leading-3': size === 'sm',
'text-xs leading-3.5': size === 'md',
'text-sm leading-4': size === 'lg'
})}>
{description}
</span>
)}
</label>
<div className="flex justify-center items-center">
<Switch id={id} size={size} {...props} />
</div>
</div>
)
}
Switch.displayName = 'Switch'
export { DescriptionSwitch, Switch }
export type { SwitchProps }
export { DescriptionSwitch, CustomizedSwitch as Switch }
export type { CustomSwitchProps as SwitchProps }

View File

@@ -1,17 +0,0 @@
import { cn } from '@cherrystudio/ui/utils'
import * as React from 'react'
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot="textarea"
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -8,25 +8,3 @@ import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Converts `null` to `undefined`, otherwise returns the input value.
* Useful when interfacing with APIs or libraries that treat `null` and `undefined` differently.
* @param data - The value that might be `null`
* @returns `undefined` if `data` is `null`, otherwise the original value
*/
export const toUndefinedIfNull = <T>(data: T | null): T | undefined => {
if (data === null) return undefined
else return data
}
/**
* Converts `undefined` to `null`, otherwise returns the input value.
* Handy for ensuring consistent representation of absent values.
* @param data - The value that might be `undefined`
* @returns `null` if `data` is `undefined`, otherwise the original value
*/
export const toNullIfUndefined = <T>(data: T | undefined): T | null => {
if (data === undefined) return null
else return data
}

View File

@@ -1,823 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Bell, Eye, Lock, Moon, Shield, Wifi, Zap } from 'lucide-react'
import { useState } from 'react'
import { DescriptionSwitch } from '../../../src/components/primitives/switch'
const meta: Meta<typeof DescriptionSwitch> = {
title: 'Components/Primitives/DescriptionSwitch',
component: DescriptionSwitch,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'An enhanced Switch component with integrated label and optional description text. Perfect for settings panels and preference forms where context is important. Built on top of the Radix UI Switch primitive with support for multiple sizes, loading states, and flexible positioning.'
}
}
},
tags: ['autodocs'],
argTypes: {
label: {
control: { type: 'text' },
description: 'Text label displayed next to the switch (required)'
},
description: {
control: { type: 'text' },
description: 'Optional helper text shown below the label'
},
position: {
control: { type: 'select' },
options: ['left', 'right'],
description: 'Switch position relative to label'
},
disabled: {
control: { type: 'boolean' },
description: 'Whether the switch is disabled'
},
loading: {
control: { type: 'boolean' },
description: 'When true, displays a loading animation in the switch thumb'
},
size: {
control: { type: 'select' },
options: ['sm', 'md', 'lg'],
description: 'The size of the switch'
},
defaultChecked: {
control: { type: 'boolean' },
description: 'Default checked state'
},
checked: {
control: { type: 'boolean' },
description: 'Checked state in controlled mode'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default
export const Default: Story = {
render: () => (
<div className="w-[400px]">
<DescriptionSwitch label="Enable notifications" description="Receive alerts for important updates" />
</div>
)
}
// Without Description
export const WithoutDescription: Story = {
render: () => (
<div className="flex w-[400px] flex-col gap-4">
<DescriptionSwitch label="Enable notifications" />
<DescriptionSwitch label="Auto-save changes" defaultChecked />
<DescriptionSwitch label="Dark mode" />
</div>
)
}
// With Description
export const WithDescription: Story = {
render: () => (
<div className="flex w-[400px] flex-col gap-4">
<DescriptionSwitch label="Enable notifications" description="Receive alerts for important updates" />
<DescriptionSwitch
label="Auto-save changes"
description="Automatically save your work as you type"
defaultChecked
/>
<DescriptionSwitch label="Dark mode" description="Use dark theme for better visibility at night" />
</div>
)
}
// Positions
export const Positions: Story = {
render: () => (
<div className="flex w-[500px] flex-col gap-8">
<div>
<h3 className="mb-4 text-sm font-semibold">Switch on Right (Default)</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Email notifications"
description="Get notified about new messages and updates"
position="right"
/>
<DescriptionSwitch
label="Push notifications"
description="Receive instant alerts on your device"
position="right"
defaultChecked
/>
<DescriptionSwitch
label="Marketing emails"
description="Stay informed about new features and offers"
position="right"
/>
</div>
</div>
<div>
<h3 className="mb-4 text-sm font-semibold">Switch on Left</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Email notifications"
description="Get notified about new messages and updates"
position="left"
/>
<DescriptionSwitch
label="Push notifications"
description="Receive instant alerts on your device"
position="left"
defaultChecked
/>
<DescriptionSwitch
label="Marketing emails"
description="Stay informed about new features and offers"
position="left"
/>
</div>
</div>
</div>
)
}
// Sizes
export const Sizes: Story = {
render: () => (
<div className="flex w-[400px] flex-col gap-6">
<div>
<p className="mb-3 text-sm text-muted-foreground">Small (sm)</p>
<DescriptionSwitch
label="Small switch"
description="Compact size for dense layouts and space-constrained interfaces"
size="sm"
/>
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Medium (md) - Default</p>
<DescriptionSwitch
label="Medium switch"
description="Default size that works well in most situations"
size="md"
defaultChecked
/>
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Large (lg)</p>
<DescriptionSwitch
label="Large switch"
description="Larger size for emphasis and improved touch targets"
size="lg"
/>
</div>
</div>
)
}
// States
export const States: Story = {
render: () => (
<div className="flex w-[400px] flex-col gap-4">
<div>
<p className="mb-3 text-sm text-muted-foreground">Normal (Unchecked)</p>
<DescriptionSwitch label="Normal state" description="Default interactive state, ready to be toggled" />
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Checked</p>
<DescriptionSwitch label="Checked state" description="Currently enabled and active" defaultChecked />
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Disabled (Unchecked)</p>
<DescriptionSwitch label="Disabled state" description="Cannot be toggled, currently inactive" disabled />
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Disabled (Checked)</p>
<DescriptionSwitch
label="Disabled state"
description="Enabled but locked, cannot be changed"
disabled
defaultChecked
/>
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Loading</p>
<DescriptionSwitch
label="Loading state"
description="Processing your request, please wait"
loading
defaultChecked
/>
</div>
</div>
)
}
// Controlled
export const Controlled: Story = {
render: function ControlledExample() {
const [checked, setChecked] = useState(false)
return (
<div className="flex flex-col gap-6">
<div className="w-[400px]">
<DescriptionSwitch
label="Controlled switch"
description="This switch is controlled by React state"
checked={checked}
onCheckedChange={setChecked}
/>
</div>
<div className="flex items-center gap-4">
<div className="text-sm text-muted-foreground">Current state: {checked ? 'On' : 'Off'}</div>
<button
type="button"
onClick={() => setChecked(!checked)}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
Toggle State
</button>
</div>
</div>
)
}
}
// Long Text
export const LongText: Story = {
render: () => (
<div className="flex w-[500px] flex-col gap-4">
<DescriptionSwitch
label="Enable comprehensive analytics and tracking"
description="When enabled, this feature will collect and analyze detailed usage statistics, user behavior patterns, interaction data, and performance metrics to help improve the application experience and provide personalized recommendations."
/>
<DescriptionSwitch
label="Short label"
description="This is a very long description that explains in great detail what this particular setting does, why it might be useful, what the implications are of enabling or disabling it, and any other relevant information that users should know before making a decision."
defaultChecked
/>
</div>
)
}
// Notification Settings Example
export const NotificationSettings: Story = {
render: function NotificationSettingsExample() {
const [notifications, setNotifications] = useState({
email: true,
push: false,
sms: false,
desktop: true,
mobile: false,
weekly: true
})
return (
<div className="w-[500px] space-y-6">
<div>
<h3 className="mb-4 text-base font-semibold">Notification Preferences</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Email notifications"
description="Receive updates and alerts via email"
checked={notifications.email}
onCheckedChange={(checked) => setNotifications({ ...notifications, email: !!checked })}
/>
<DescriptionSwitch
label="Push notifications"
description="Get instant notifications on this device"
checked={notifications.push}
onCheckedChange={(checked) => setNotifications({ ...notifications, push: !!checked })}
/>
<DescriptionSwitch
label="SMS notifications"
description="Receive text message alerts for critical updates"
checked={notifications.sms}
onCheckedChange={(checked) => setNotifications({ ...notifications, sms: !!checked })}
/>
<DescriptionSwitch
label="Desktop notifications"
description="Show notifications on your desktop"
checked={notifications.desktop}
onCheckedChange={(checked) => setNotifications({ ...notifications, desktop: !!checked })}
/>
<DescriptionSwitch
label="Mobile notifications"
description="Receive alerts on your mobile device"
checked={notifications.mobile}
onCheckedChange={(checked) => setNotifications({ ...notifications, mobile: !!checked })}
/>
<DescriptionSwitch
label="Weekly digest"
description="Get a summary of activity every week"
checked={notifications.weekly}
onCheckedChange={(checked) => setNotifications({ ...notifications, weekly: !!checked })}
/>
</div>
</div>
</div>
)
}
}
// Privacy Settings Example
export const PrivacySettings: Story = {
render: function PrivacySettingsExample() {
const [privacy, setPrivacy] = useState({
profileVisible: true,
activityTracking: false,
dataSharing: false,
personalization: true,
thirdParty: false
})
return (
<div className="w-[500px] space-y-6">
<div>
<h3 className="mb-4 text-base font-semibold">Privacy & Data</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Public profile"
description="Make your profile visible to other users"
checked={privacy.profileVisible}
onCheckedChange={(checked) => setPrivacy({ ...privacy, profileVisible: !!checked })}
/>
<DescriptionSwitch
label="Activity tracking"
description="Allow us to track your activity to improve services"
checked={privacy.activityTracking}
onCheckedChange={(checked) => setPrivacy({ ...privacy, activityTracking: !!checked })}
/>
<DescriptionSwitch
label="Data sharing"
description="Share anonymous usage data with partners"
checked={privacy.dataSharing}
onCheckedChange={(checked) => setPrivacy({ ...privacy, dataSharing: !!checked })}
/>
<DescriptionSwitch
label="Personalization"
description="Use your data to personalize your experience"
checked={privacy.personalization}
onCheckedChange={(checked) => setPrivacy({ ...privacy, personalization: !!checked })}
/>
<DescriptionSwitch
label="Third-party cookies"
description="Allow third-party cookies for enhanced features"
checked={privacy.thirdParty}
onCheckedChange={(checked) => setPrivacy({ ...privacy, thirdParty: !!checked })}
/>
</div>
</div>
</div>
)
}
}
// Application Settings Example
export const ApplicationSettings: Story = {
render: function ApplicationSettingsExample() {
const [settings, setSettings] = useState({
autoSave: true,
spellCheck: true,
darkMode: false,
compactMode: false,
animations: true,
sound: false,
offlineMode: false
})
return (
<div className="w-[500px] space-y-6">
<div>
<h3 className="mb-4 text-base font-semibold">Application Settings</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Auto-save"
description="Automatically save your work every few minutes"
checked={settings.autoSave}
onCheckedChange={(checked) => setSettings({ ...settings, autoSave: !!checked })}
/>
<DescriptionSwitch
label="Spell check"
description="Check spelling as you type"
checked={settings.spellCheck}
onCheckedChange={(checked) => setSettings({ ...settings, spellCheck: !!checked })}
/>
<DescriptionSwitch
label="Dark mode"
description="Use dark theme throughout the application"
checked={settings.darkMode}
onCheckedChange={(checked) => setSettings({ ...settings, darkMode: !!checked })}
/>
<DescriptionSwitch
label="Compact mode"
description="Reduce spacing for a more dense layout"
checked={settings.compactMode}
onCheckedChange={(checked) => setSettings({ ...settings, compactMode: !!checked })}
/>
<DescriptionSwitch
label="Animations"
description="Enable smooth transitions and animations"
checked={settings.animations}
onCheckedChange={(checked) => setSettings({ ...settings, animations: !!checked })}
/>
<DescriptionSwitch
label="Sound effects"
description="Play sounds for notifications and actions"
checked={settings.sound}
onCheckedChange={(checked) => setSettings({ ...settings, sound: !!checked })}
/>
<DescriptionSwitch
label="Offline mode"
description="Enable working without internet connection"
checked={settings.offlineMode}
onCheckedChange={(checked) => setSettings({ ...settings, offlineMode: !!checked })}
/>
</div>
</div>
</div>
)
}
}
// With Icons
export const WithIcons: Story = {
render: () => (
<div className="flex w-[500px] flex-col gap-4">
<div className="flex items-start gap-3">
<Bell className="mt-1 size-5 text-muted-foreground" />
<div className="flex-1">
<DescriptionSwitch label="Notifications" description="Receive alerts for important updates" defaultChecked />
</div>
</div>
<div className="flex items-start gap-3">
<Moon className="mt-1 size-5 text-muted-foreground" />
<div className="flex-1">
<DescriptionSwitch label="Dark mode" description="Use dark theme for better visibility at night" />
</div>
</div>
<div className="flex items-start gap-3">
<Shield className="mt-1 size-5 text-muted-foreground" />
<div className="flex-1">
<DescriptionSwitch
label="Two-factor authentication"
description="Add an extra layer of security to your account"
/>
</div>
</div>
<div className="flex items-start gap-3">
<Wifi className="mt-1 size-5 text-muted-foreground" />
<div className="flex-1">
<DescriptionSwitch label="Offline mode" description="Work without internet connection" defaultChecked />
</div>
</div>
<div className="flex items-start gap-3">
<Zap className="mt-1 size-5 text-muted-foreground" />
<div className="flex-1">
<DescriptionSwitch
label="Performance mode"
description="Optimize for speed and responsiveness"
defaultChecked
/>
</div>
</div>
</div>
)
}
// Loading Simulation
export const LoadingSimulation: Story = {
render: function LoadingSimulationExample() {
const [states, setStates] = useState({
wifi: { enabled: false, loading: false },
bluetooth: { enabled: false, loading: false },
location: { enabled: false, loading: false }
})
const handleToggle = async (setting: keyof typeof states, checked: boolean) => {
setStates((prev) => ({
...prev,
[setting]: { ...prev[setting], loading: true }
}))
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500))
setStates((prev) => ({
...prev,
[setting]: { enabled: checked, loading: false }
}))
}
return (
<div className="w-[500px] space-y-6">
<div>
<h3 className="mb-4 text-base font-semibold">System Settings</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Wi-Fi"
description="Connect to wireless networks"
checked={states.wifi.enabled}
onCheckedChange={(checked) => handleToggle('wifi', !!checked)}
loading={states.wifi.loading}
disabled={states.wifi.loading}
/>
<DescriptionSwitch
label="Bluetooth"
description="Connect to Bluetooth devices"
checked={states.bluetooth.enabled}
onCheckedChange={(checked) => handleToggle('bluetooth', !!checked)}
loading={states.bluetooth.loading}
disabled={states.bluetooth.loading}
/>
<DescriptionSwitch
label="Location services"
description="Allow apps to use your location"
checked={states.location.enabled}
onCheckedChange={(checked) => handleToggle('location', !!checked)}
loading={states.location.loading}
disabled={states.location.loading}
/>
</div>
</div>
<p className="text-xs text-muted-foreground">Toggle switches to see a simulated 1.5-second loading state</p>
</div>
)
}
}
// Complex Settings Panel
export const ComplexSettingsPanel: Story = {
render: function ComplexSettingsPanelExample() {
const [settings, setSettings] = useState({
notifications: {
email: true,
push: false,
desktop: true
},
privacy: {
profile: true,
activity: false,
analytics: true
},
features: {
autoSave: true,
darkMode: false,
compactView: false
},
security: {
twoFactor: false,
biometric: true,
sessionTimeout: false
}
})
return (
<div className="w-[600px] space-y-8">
{/* Notifications Section */}
<div>
<div className="mb-4 flex items-center gap-2">
<Bell className="size-5" />
<h3 className="text-base font-semibold">Notifications</h3>
</div>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Email notifications"
description="Receive updates and alerts via email"
checked={settings.notifications.email}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: { ...settings.notifications, email: !!checked }
})
}
/>
<DescriptionSwitch
label="Push notifications"
description="Get instant notifications on this device"
checked={settings.notifications.push}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: { ...settings.notifications, push: !!checked }
})
}
/>
<DescriptionSwitch
label="Desktop notifications"
description="Show notifications on your desktop"
checked={settings.notifications.desktop}
onCheckedChange={(checked) =>
setSettings({
...settings,
notifications: { ...settings.notifications, desktop: !!checked }
})
}
/>
</div>
</div>
{/* Privacy Section */}
<div>
<div className="mb-4 flex items-center gap-2">
<Eye className="size-5" />
<h3 className="text-base font-semibold">Privacy</h3>
</div>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Public profile"
description="Make your profile visible to other users"
checked={settings.privacy.profile}
onCheckedChange={(checked) =>
setSettings({
...settings,
privacy: { ...settings.privacy, profile: !!checked }
})
}
/>
<DescriptionSwitch
label="Activity tracking"
description="Allow us to track your activity"
checked={settings.privacy.activity}
onCheckedChange={(checked) =>
setSettings({
...settings,
privacy: { ...settings.privacy, activity: !!checked }
})
}
/>
<DescriptionSwitch
label="Analytics"
description="Help improve the app by sharing usage data"
checked={settings.privacy.analytics}
onCheckedChange={(checked) =>
setSettings({
...settings,
privacy: { ...settings.privacy, analytics: !!checked }
})
}
/>
</div>
</div>
{/* Features Section */}
<div>
<div className="mb-4 flex items-center gap-2">
<Zap className="size-5" />
<h3 className="text-base font-semibold">Features</h3>
</div>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Auto-save"
description="Automatically save your work"
checked={settings.features.autoSave}
onCheckedChange={(checked) =>
setSettings({
...settings,
features: { ...settings.features, autoSave: !!checked }
})
}
/>
<DescriptionSwitch
label="Dark mode"
description="Use dark theme throughout the app"
checked={settings.features.darkMode}
onCheckedChange={(checked) =>
setSettings({
...settings,
features: { ...settings.features, darkMode: !!checked }
})
}
/>
<DescriptionSwitch
label="Compact view"
description="Reduce spacing for more content"
checked={settings.features.compactView}
onCheckedChange={(checked) =>
setSettings({
...settings,
features: { ...settings.features, compactView: !!checked }
})
}
/>
</div>
</div>
{/* Security Section */}
<div>
<div className="mb-4 flex items-center gap-2">
<Lock className="size-5" />
<h3 className="text-base font-semibold">Security</h3>
</div>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Two-factor authentication"
description="Require a second verification step when signing in"
checked={settings.security.twoFactor}
onCheckedChange={(checked) =>
setSettings({
...settings,
security: { ...settings.security, twoFactor: !!checked }
})
}
/>
<DescriptionSwitch
label="Biometric authentication"
description="Use fingerprint or face recognition"
checked={settings.security.biometric}
onCheckedChange={(checked) =>
setSettings({
...settings,
security: { ...settings.security, biometric: !!checked }
})
}
/>
<DescriptionSwitch
label="Auto session timeout"
description="Automatically sign out after inactivity"
checked={settings.security.sessionTimeout}
onCheckedChange={(checked) =>
setSettings({
...settings,
security: { ...settings.security, sessionTimeout: !!checked }
})
}
/>
</div>
</div>
</div>
)
}
}
// Accessibility Features
export const AccessibilityFeatures: Story = {
render: () => (
<div className="w-[500px] space-y-6">
<div>
<h3 className="mb-4 text-base font-semibold">Keyboard Navigation</h3>
<p className="mb-4 text-sm text-muted-foreground">
Use Tab to navigate between switches and Space/Enter to toggle them. Each switch has a proper label for screen
readers.
</p>
<div className="flex flex-col gap-4">
<DescriptionSwitch label="High contrast mode" description="Increase contrast for better visibility" />
<DescriptionSwitch label="Reduce motion" description="Minimize animations and transitions" />
<DescriptionSwitch
label="Screen reader optimization"
description="Optimize interface for screen readers"
defaultChecked
/>
<DescriptionSwitch label="Large text" description="Increase font size throughout the app" />
</div>
</div>
</div>
)
}
// Responsive Layout
export const ResponsiveLayout: Story = {
render: () => (
<div className="space-y-6">
<div className="w-[300px]">
<h3 className="mb-4 text-sm font-semibold">Narrow Layout (300px)</h3>
<div className="flex flex-col gap-3">
<DescriptionSwitch label="Notifications" description="Receive important alerts" size="sm" />
<DescriptionSwitch label="Auto-save" description="Save automatically" size="sm" defaultChecked />
</div>
</div>
<div className="w-[500px]">
<h3 className="mb-4 text-sm font-semibold">Standard Layout (500px)</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch label="Notifications" description="Receive alerts for important updates and messages" />
<DescriptionSwitch label="Auto-save" description="Automatically save your work as you type" defaultChecked />
</div>
</div>
<div className="w-[700px]">
<h3 className="mb-4 text-sm font-semibold">Wide Layout (700px)</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Notifications"
description="Receive alerts for important updates, messages, and system notifications to stay informed"
size="lg"
/>
<DescriptionSwitch
label="Auto-save"
description="Automatically save your work as you type to prevent data loss and ensure your progress is always preserved"
size="lg"
defaultChecked
/>
</div>
</div>
</div>
)
}

View File

@@ -1,628 +0,0 @@
import { Input } from '@cherrystudio/ui'
import type { Meta, StoryObj } from '@storybook/react'
import { Mail, Search, User } from 'lucide-react'
import { useState } from 'react'
const meta: Meta<typeof Input> = {
title: 'Components/Primitives/Input',
component: Input,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'A basic text input component with focus states, error handling, and file upload support. Built with accessibility in mind and styled with Tailwind CSS.'
}
}
},
tags: ['autodocs'],
argTypes: {
type: {
control: { type: 'select' },
options: ['text', 'email', 'password', 'number', 'search', 'tel', 'url', 'date', 'time', 'file'],
description: 'The type of the input'
},
placeholder: {
control: { type: 'text' },
description: 'Placeholder text'
},
disabled: {
control: { type: 'boolean' },
description: 'Whether the input is disabled'
},
className: {
control: { type: 'text' },
description: 'Additional CSS classes'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default
export const Default: Story = {
args: {
placeholder: 'Enter text...'
}
}
// With Value
export const WithValue: Story = {
args: {
defaultValue: 'Hello World',
placeholder: 'Enter text...'
}
}
// Types
export const TextType: Story = {
args: {
type: 'text',
placeholder: 'Enter text...'
}
}
export const EmailType: Story = {
args: {
type: 'email',
placeholder: 'Enter email...'
}
}
export const PasswordType: Story = {
args: {
type: 'password',
placeholder: 'Enter password...'
}
}
export const NumberType: Story = {
args: {
type: 'number',
placeholder: 'Enter number...'
}
}
export const SearchType: Story = {
args: {
type: 'search',
placeholder: 'Search...'
}
}
// All Input Types
export const AllInputTypes: Story = {
render: () => (
<div className="flex w-80 flex-col gap-4">
<div>
<label className="mb-1 block text-sm font-medium">Text</label>
<Input type="text" placeholder="Enter text..." />
</div>
<div>
<label className="mb-1 block text-sm font-medium">Email</label>
<Input type="email" placeholder="email@example.com" />
</div>
<div>
<label className="mb-1 block text-sm font-medium">Password</label>
<Input type="password" placeholder="Enter password..." />
</div>
<div>
<label className="mb-1 block text-sm font-medium">Number</label>
<Input type="number" placeholder="0" />
</div>
<div>
<label className="mb-1 block text-sm font-medium">Search</label>
<Input type="search" placeholder="Search..." />
</div>
<div>
<label className="mb-1 block text-sm font-medium">URL</label>
<Input type="url" placeholder="https://example.com" />
</div>
<div>
<label className="mb-1 block text-sm font-medium">Tel</label>
<Input type="tel" placeholder="+1 (555) 000-0000" />
</div>
<div>
<label className="mb-1 block text-sm font-medium">Date</label>
<Input type="date" />
</div>
<div>
<label className="mb-1 block text-sm font-medium">Time</label>
<Input type="time" />
</div>
</div>
)
}
// States
export const Disabled: Story = {
args: {
disabled: true,
placeholder: 'Disabled input',
defaultValue: 'Cannot edit this'
}
}
export const ReadOnly: Story = {
args: {
readOnly: true,
defaultValue: 'Read-only value'
}
}
export const ErrorState: Story = {
render: () => (
<div className="w-80">
<Input placeholder="Invalid input..." aria-invalid />
</div>
)
}
// All States
export const AllStates: Story = {
render: () => (
<div className="flex w-80 flex-col gap-4">
<div>
<p className="mb-2 text-sm text-muted-foreground">Normal</p>
<Input placeholder="Normal input" />
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">With Value</p>
<Input defaultValue="Input with value" />
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">Disabled</p>
<Input disabled placeholder="Disabled input" />
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">Read-only</p>
<Input readOnly defaultValue="Read-only value" />
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">Error State</p>
<Input placeholder="Invalid input" aria-invalid />
</div>
</div>
)
}
// Controlled
export const Controlled: Story = {
render: function ControlledExample() {
const [value, setValue] = useState('')
return (
<div className="flex w-80 flex-col gap-4">
<Input placeholder="Type something..." value={value} onChange={(e) => setValue(e.target.value)} />
<div className="text-sm text-muted-foreground">
Current value: <span className="font-mono">{value || '(empty)'}</span>
</div>
<div className="text-sm text-muted-foreground">Length: {value.length}</div>
</div>
)
}
}
// With Labels
export const WithLabels: Story = {
render: () => (
<div className="flex w-80 flex-col gap-4">
<div>
<label htmlFor="username" className="mb-1 block text-sm font-medium">
Username
</label>
<Input id="username" placeholder="Enter username..." />
</div>
<div>
<label htmlFor="email" className="mb-1 block text-sm font-medium">
Email
</label>
<Input id="email" type="email" placeholder="email@example.com" />
</div>
<div>
<label htmlFor="password" className="mb-1 block text-sm font-medium">
Password
</label>
<Input id="password" type="password" placeholder="Enter password..." />
</div>
</div>
)
}
// With Helper Text
export const WithHelperText: Story = {
render: () => (
<div className="flex w-80 flex-col gap-4">
<div>
<label htmlFor="email-helper" className="mb-1 block text-sm font-medium">
Email
</label>
<Input id="email-helper" type="email" placeholder="email@example.com" />
<p className="mt-1 text-xs text-muted-foreground">We'll never share your email with anyone else.</p>
</div>
<div>
<label htmlFor="password-helper" className="mb-1 block text-sm font-medium">
Password
</label>
<Input id="password-helper" type="password" placeholder="Enter password..." />
<p className="mt-1 text-xs text-muted-foreground">Must be at least 8 characters long.</p>
</div>
</div>
)
}
// With Error Message
export const WithErrorMessage: Story = {
render: () => (
<div className="flex w-80 flex-col gap-4">
<div>
<label htmlFor="email-error" className="mb-1 block text-sm font-medium">
Email
</label>
<Input id="email-error" type="email" placeholder="email@example.com" aria-invalid />
<p className="mt-1 text-xs text-destructive">Please enter a valid email address.</p>
</div>
<div>
<label htmlFor="password-error" className="mb-1 block text-sm font-medium">
Password
</label>
<Input id="password-error" type="password" placeholder="Enter password..." aria-invalid />
<p className="mt-1 text-xs text-destructive">Password must be at least 8 characters.</p>
</div>
</div>
)
}
// Validation States
export const ValidationStates: Story = {
render: () => (
<div className="flex w-80 flex-col gap-6">
<div>
<p className="mb-2 text-sm font-medium">Valid Input</p>
<Input type="email" placeholder="email@example.com" defaultValue="user@example.com" />
<p className="mt-1 text-xs text-green-600">✓ Email is valid</p>
</div>
<div>
<p className="mb-2 text-sm font-medium">Invalid Email Format</p>
<Input type="email" placeholder="email@example.com" defaultValue="invalid-email" aria-invalid />
<p className="mt-1 text-xs text-destructive">✗ Please enter a valid email address</p>
</div>
<div>
<p className="mb-2 text-sm font-medium">Required Field Empty</p>
<Input placeholder="Required field" aria-invalid aria-required />
<p className="mt-1 text-xs text-destructive">✗ This field is required</p>
</div>
<div>
<p className="mb-2 text-sm font-medium">Password Too Short</p>
<Input type="password" placeholder="Enter password..." defaultValue="123" aria-invalid />
<p className="mt-1 text-xs text-destructive">✗ Password must be at least 8 characters</p>
</div>
<div>
<p className="mb-2 text-sm font-medium">Number Out of Range</p>
<Input type="number" placeholder="1-100" defaultValue="150" min="1" max="100" aria-invalid />
<p className="mt-1 text-xs text-destructive">✗ Value must be between 1 and 100</p>
</div>
</div>
)
}
// Real-time Validation
export const RealTimeValidation: Story = {
render: function RealTimeValidationExample() {
const [email, setEmail] = useState('')
const [emailError, setEmailError] = useState('')
const validateEmail = (value: string) => {
if (!value) {
setEmailError('Email is required')
return false
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
setEmailError('Please enter a valid email address')
return false
}
setEmailError('')
return true
}
return (
<div className="w-80 space-y-4">
<h3 className="text-base font-semibold">Real-time Email Validation</h3>
<div>
<label htmlFor="realtime-email" className="mb-1 block text-sm font-medium">
Email Address
</label>
<Input
id="realtime-email"
type="email"
placeholder="email@example.com"
value={email}
onChange={(e) => {
setEmail(e.target.value)
validateEmail(e.target.value)
}}
aria-invalid={!!emailError}
/>
{emailError ? (
<p className="mt-1 text-xs text-destructive">{emailError}</p>
) : email ? (
<p className="mt-1 text-xs text-green-600">✓ Email is valid</p>
) : (
<p className="mt-1 text-xs text-muted-foreground">Enter your email address</p>
)}
</div>
</div>
)
}
}
// File Input
export const FileInput: Story = {
render: () => (
<div className="w-80">
<label htmlFor="file" className="mb-1 block text-sm font-medium">
Upload File
</label>
<Input id="file" type="file" />
</div>
)
}
// Multiple Files
export const MultipleFiles: Story = {
render: () => (
<div className="w-80">
<label htmlFor="files" className="mb-1 block text-sm font-medium">
Upload Multiple Files
</label>
<Input id="files" type="file" multiple />
</div>
)
}
// Form Example
export const FormExample: Story = {
render: function FormExample() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const [errors, setErrors] = useState<Record<string, string>>({})
const [submitted, setSubmitted] = useState(false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const newErrors: Record<string, string> = {}
if (!formData.username) newErrors.username = 'Username is required'
if (!formData.email) newErrors.email = 'Email is required'
if (!formData.password) newErrors.password = 'Password is required'
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'Passwords do not match'
setErrors(newErrors)
if (Object.keys(newErrors).length === 0) {
setSubmitted(true)
setTimeout(() => setSubmitted(false), 3000)
}
}
return (
<form onSubmit={handleSubmit} className="w-80 space-y-4">
<h3 className="text-base font-semibold">Sign Up Form</h3>
<div>
<label htmlFor="form-username" className="mb-1 block text-sm font-medium">
Username
</label>
<Input
id="form-username"
placeholder="Enter username..."
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
aria-invalid={!!errors.username}
/>
{errors.username && <p className="mt-1 text-xs text-destructive">{errors.username}</p>}
</div>
<div>
<label htmlFor="form-email" className="mb-1 block text-sm font-medium">
Email
</label>
<Input
id="form-email"
type="email"
placeholder="email@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
aria-invalid={!!errors.email}
/>
{errors.email && <p className="mt-1 text-xs text-destructive">{errors.email}</p>}
</div>
<div>
<label htmlFor="form-password" className="mb-1 block text-sm font-medium">
Password
</label>
<Input
id="form-password"
type="password"
placeholder="Enter password..."
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
aria-invalid={!!errors.password}
/>
{errors.password && <p className="mt-1 text-xs text-destructive">{errors.password}</p>}
</div>
<div>
<label htmlFor="form-confirm" className="mb-1 block text-sm font-medium">
Confirm Password
</label>
<Input
id="form-confirm"
type="password"
placeholder="Confirm password..."
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
aria-invalid={!!errors.confirmPassword}
/>
{errors.confirmPassword && <p className="mt-1 text-xs text-destructive">{errors.confirmPassword}</p>}
</div>
<button
type="submit"
className="w-full rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
Sign Up
</button>
{submitted && <p className="text-center text-sm text-green-600">Form submitted successfully!</p>}
</form>
)
}
}
// Search Example
export const SearchExample: Story = {
render: function SearchExample() {
const [query, setQuery] = useState('')
const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape']
const filtered = items.filter((item) => item.toLowerCase().includes(query.toLowerCase()))
return (
<div className="w-80 space-y-4">
<div>
<label htmlFor="search" className="mb-1 flex items-center gap-2 text-sm font-medium">
<Search className="size-4" />
Search Fruits
</label>
<Input
id="search"
type="search"
placeholder="Type to search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div className="rounded-md border p-3">
<p className="mb-2 text-sm font-medium">Results ({filtered.length})</p>
{filtered.length > 0 ? (
<ul className="space-y-1">
{filtered.map((item) => (
<li key={item} className="text-sm text-muted-foreground">
{item}
</li>
))}
</ul>
) : (
<p className="text-sm text-muted-foreground">No results found</p>
)}
</div>
</div>
)
}
}
// Real World Examples
export const RealWorldExamples: Story = {
render: () => (
<div className="flex flex-col gap-8">
{/* Login Form */}
<div className="w-80">
<h3 className="mb-4 text-base font-semibold">Login Form</h3>
<div className="space-y-3">
<div>
<label htmlFor="login-email" className="mb-1 flex items-center gap-2 text-sm font-medium">
<Mail className="size-4" />
Email
</label>
<Input id="login-email" type="email" placeholder="email@example.com" />
</div>
<div>
<label htmlFor="login-password" className="mb-1 block text-sm font-medium">
Password
</label>
<Input id="login-password" type="password" placeholder="Enter password..." />
</div>
<button
type="button"
className="w-full rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
Sign In
</button>
</div>
</div>
{/* Profile Form */}
<div className="w-80">
<h3 className="mb-4 text-base font-semibold">Profile Information</h3>
<div className="space-y-3">
<div>
<label htmlFor="profile-name" className="mb-1 flex items-center gap-2 text-sm font-medium">
<User className="size-4" />
Full Name
</label>
<Input id="profile-name" placeholder="John Doe" />
</div>
<div>
<label htmlFor="profile-email" className="mb-1 flex items-center gap-2 text-sm font-medium">
<Mail className="size-4" />
Email
</label>
<Input id="profile-email" type="email" placeholder="john@example.com" />
</div>
<div>
<label htmlFor="profile-phone" className="mb-1 block text-sm font-medium">
Phone
</label>
<Input id="profile-phone" type="tel" placeholder="+1 (555) 000-0000" />
</div>
<button
type="button"
className="w-full rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
Save Changes
</button>
</div>
</div>
</div>
)
}
// Accessibility
export const Accessibility: Story = {
render: () => (
<div className="w-80 space-y-6">
<div>
<h3 className="mb-4 text-base font-semibold">Keyboard Navigation</h3>
<p className="mb-4 text-sm text-muted-foreground">Use Tab to navigate between inputs.</p>
<div className="space-y-3">
<Input placeholder="First input" />
<Input placeholder="Second input" />
<Input placeholder="Third input" />
</div>
</div>
<div>
<h3 className="mb-4 text-base font-semibold">ARIA Labels</h3>
<p className="mb-4 text-sm text-muted-foreground">
Inputs include proper ARIA attributes for screen reader support.
</p>
<div className="space-y-3">
<Input placeholder="Input with aria-label" aria-label="Username input" />
<Input placeholder="Invalid input" aria-invalid aria-describedby="error-message" />
<p id="error-message" className="text-xs text-destructive">
This input has an error
</p>
</div>
</div>
</div>
)
}

View File

@@ -1,666 +0,0 @@
import { DescriptionSwitch, Switch } from '@cherrystudio/ui'
import type { Meta, StoryObj } from '@storybook/react'
import { Bell, Moon, Shield, Wifi, Zap } from 'lucide-react'
import { useState } from 'react'
const meta: Meta<typeof Switch> = {
title: 'Components/Primitives/Switch',
component: Switch,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'A switch component based on Radix UI Switch, allowing users to toggle between on/off states. Supports three sizes (sm, md, lg), loading states, and an enhanced DescriptionSwitch variant with label and description. Built with accessibility in mind.'
}
}
},
tags: ['autodocs'],
argTypes: {
disabled: {
control: { type: 'boolean' },
description: 'Whether the switch is disabled'
},
loading: {
control: { type: 'boolean' },
description: 'When true, displays a loading animation in the switch thumb'
},
size: {
control: { type: 'select' },
options: ['sm', 'md', 'lg'],
description: 'The size of the switch'
},
defaultChecked: {
control: { type: 'boolean' },
description: 'Default checked state'
},
checked: {
control: { type: 'boolean' },
description: 'Checked state in controlled mode'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default
export const Default: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Switch id="default1" />
<label htmlFor="default1" className="cursor-pointer text-sm">
Enable notifications
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="default2" />
<label htmlFor="default2" className="cursor-pointer text-sm">
Auto-save changes
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="default3" />
<label htmlFor="default3" className="cursor-pointer text-sm">
Dark mode
</label>
</div>
</div>
)
}
// With Default Checked
export const WithDefaultChecked: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Switch id="checked1" defaultChecked />
<label htmlFor="checked1" className="cursor-pointer text-sm">
Option 1 (Default On)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="checked2" />
<label htmlFor="checked2" className="cursor-pointer text-sm">
Option 2
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="checked3" defaultChecked />
<label htmlFor="checked3" className="cursor-pointer text-sm">
Option 3 (Default On)
</label>
</div>
</div>
)
}
// Disabled
export const Disabled: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Switch id="disabled1" disabled />
<label htmlFor="disabled1" className="cursor-not-allowed text-sm opacity-50">
Disabled (Off)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="disabled2" disabled defaultChecked />
<label htmlFor="disabled2" className="cursor-not-allowed text-sm opacity-50">
Disabled (On)
</label>
</div>
</div>
)
}
// Loading State
export const Loading: Story = {
render: () => (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Switch id="loading1" loading />
<label htmlFor="loading1" className="cursor-pointer text-sm">
Loading state (Off)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="loading2" loading defaultChecked />
<label htmlFor="loading2" className="cursor-pointer text-sm">
Loading state (On)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="loading3" loading disabled defaultChecked />
<label htmlFor="loading3" className="cursor-not-allowed text-sm opacity-50">
Loading + Disabled
</label>
</div>
</div>
)
}
// Controlled
export const Controlled: Story = {
render: function ControlledExample() {
const [checked, setChecked] = useState(false)
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Switch id="controlled" checked={checked} onCheckedChange={setChecked} />
<label htmlFor="controlled" className="cursor-pointer text-sm">
Controlled switch
</label>
</div>
<div className="text-sm text-muted-foreground">Current state: {checked ? 'On' : 'Off'}</div>
<button
type="button"
onClick={() => setChecked(!checked)}
className="w-fit rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
Toggle State
</button>
</div>
)
}
}
// Sizes
export const Sizes: Story = {
render: () => (
<div className="flex flex-col gap-6">
<div>
<p className="mb-3 text-sm text-muted-foreground">Small (sm)</p>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Switch id="size-sm-1" size="sm" />
<label htmlFor="size-sm-1" className="cursor-pointer text-sm">
Small switch
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="size-sm-2" size="sm" defaultChecked />
<label htmlFor="size-sm-2" className="cursor-pointer text-sm">
Small switch (on)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="size-sm-3" size="sm" loading defaultChecked />
<label htmlFor="size-sm-3" className="cursor-pointer text-sm">
Small switch (loading)
</label>
</div>
</div>
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Medium (md) - Default</p>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Switch id="size-md-1" size="md" />
<label htmlFor="size-md-1" className="cursor-pointer text-sm">
Medium switch
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="size-md-2" size="md" defaultChecked />
<label htmlFor="size-md-2" className="cursor-pointer text-sm">
Medium switch (on)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="size-md-3" size="md" loading defaultChecked />
<label htmlFor="size-md-3" className="cursor-pointer text-sm">
Medium switch (loading)
</label>
</div>
</div>
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Large (lg)</p>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Switch id="size-lg-1" size="lg" />
<label htmlFor="size-lg-1" className="cursor-pointer text-sm">
Large switch
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="size-lg-2" size="lg" defaultChecked />
<label htmlFor="size-lg-2" className="cursor-pointer text-sm">
Large switch (on)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="size-lg-3" size="lg" loading defaultChecked />
<label htmlFor="size-lg-3" className="cursor-pointer text-sm">
Large switch (loading)
</label>
</div>
</div>
</div>
</div>
)
}
// Description Switch - Basic
export const DescriptionSwitchBasic: Story = {
render: () => (
<div className="flex w-96 flex-col gap-4">
<DescriptionSwitch label="Enable notifications" description="Receive alerts for important updates" />
<DescriptionSwitch label="Auto-save" description="Automatically save changes as you work" defaultChecked />
<DescriptionSwitch label="Dark mode" description="Use dark theme for better visibility at night" />
</div>
)
}
// Description Switch - Positions
export const DescriptionSwitchPositions: Story = {
render: () => (
<div className="flex w-96 flex-col gap-6">
<div>
<p className="mb-3 text-sm text-muted-foreground">Switch on Right (Default)</p>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Email notifications"
description="Get notified about new messages"
position="right"
/>
<DescriptionSwitch label="Marketing emails" description="Receive promotional content" position="right" />
</div>
</div>
<div>
<p className="mb-3 text-sm text-muted-foreground">Switch on Left</p>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Email notifications"
description="Get notified about new messages"
position="left"
/>
<DescriptionSwitch label="Marketing emails" description="Receive promotional content" position="left" />
</div>
</div>
</div>
)
}
// Description Switch - Sizes
export const DescriptionSwitchSizes: Story = {
render: () => (
<div className="flex w-96 flex-col gap-6">
<DescriptionSwitch label="Small switch" description="Compact size for dense layouts" size="sm" />
<DescriptionSwitch label="Medium switch" description="Default size for most use cases" size="md" defaultChecked />
<DescriptionSwitch label="Large switch" description="Larger size for emphasis" size="lg" />
</div>
)
}
// Description Switch - States
export const DescriptionSwitchStates: Story = {
render: () => (
<div className="flex w-96 flex-col gap-4">
<DescriptionSwitch label="Normal state" description="Default interactive state" />
<DescriptionSwitch label="Checked state" description="Currently enabled" defaultChecked />
<DescriptionSwitch label="Disabled state" description="Cannot be toggled" disabled />
<DescriptionSwitch label="Disabled + Checked" description="Enabled but locked" disabled defaultChecked />
<DescriptionSwitch label="Loading state" description="Processing your request" loading defaultChecked />
</div>
)
}
// Size Comparison
export const SizeComparison: Story = {
render: () => (
<div className="flex items-center gap-6">
<div className="flex flex-col gap-4">
<p className="text-xs font-medium text-muted-foreground">Off</p>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Switch id="compare-sm-1" size="sm" />
<span className="text-xs text-muted-foreground">sm</span>
</div>
<div className="flex flex-col items-center gap-2">
<Switch id="compare-md-1" size="md" />
<span className="text-xs text-muted-foreground">md</span>
</div>
<div className="flex flex-col items-center gap-2">
<Switch id="compare-lg-1" size="lg" />
<span className="text-xs text-muted-foreground">lg</span>
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<p className="text-xs font-medium text-muted-foreground">On</p>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Switch id="compare-sm-2" size="sm" defaultChecked />
<span className="text-xs text-muted-foreground">sm</span>
</div>
<div className="flex flex-col items-center gap-2">
<Switch id="compare-md-2" size="md" defaultChecked />
<span className="text-xs text-muted-foreground">md</span>
</div>
<div className="flex flex-col items-center gap-2">
<Switch id="compare-lg-2" size="lg" defaultChecked />
<span className="text-xs text-muted-foreground">lg</span>
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<p className="text-xs font-medium text-muted-foreground">Loading</p>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Switch id="compare-sm-3" size="sm" loading defaultChecked />
<span className="text-xs text-muted-foreground">sm</span>
</div>
<div className="flex flex-col items-center gap-2">
<Switch id="compare-md-3" size="md" loading defaultChecked />
<span className="text-xs text-muted-foreground">md</span>
</div>
<div className="flex flex-col items-center gap-2">
<Switch id="compare-lg-3" size="lg" loading defaultChecked />
<span className="text-xs text-muted-foreground">lg</span>
</div>
</div>
</div>
</div>
)
}
// Real World Examples
export const RealWorldExamples: Story = {
render: function RealWorldExample() {
const [settings, setSettings] = useState({
notifications: true,
autoSave: true,
darkMode: false,
analytics: true
})
const [privacy, setPrivacy] = useState({
shareData: false,
allowCookies: true,
trackLocation: false,
personalizedAds: false
})
return (
<div className="flex w-[500px] flex-col gap-8">
{/* General Settings */}
<div>
<h3 className="mb-4 text-base font-semibold">General Settings</h3>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<Switch
id="settings-notifications"
checked={settings.notifications}
onCheckedChange={(checked) => setSettings({ ...settings, notifications: !!checked })}
/>
<label htmlFor="settings-notifications" className="flex cursor-pointer items-center gap-2 text-sm">
<Bell className="size-4" />
Push Notifications
</label>
</div>
<div className="flex items-center gap-3">
<Switch
id="settings-autosave"
checked={settings.autoSave}
onCheckedChange={(checked) => setSettings({ ...settings, autoSave: !!checked })}
/>
<label htmlFor="settings-autosave" className="flex cursor-pointer items-center gap-2 text-sm">
<Zap className="size-4" />
Auto-save Changes
</label>
</div>
<div className="flex items-center gap-3">
<Switch
id="settings-darkmode"
checked={settings.darkMode}
onCheckedChange={(checked) => setSettings({ ...settings, darkMode: !!checked })}
/>
<label htmlFor="settings-darkmode" className="flex cursor-pointer items-center gap-2 text-sm">
<Moon className="size-4" />
Dark Mode
</label>
</div>
<div className="flex items-center gap-3">
<Switch
id="settings-analytics"
checked={settings.analytics}
onCheckedChange={(checked) => setSettings({ ...settings, analytics: !!checked })}
/>
<label htmlFor="settings-analytics" className="flex cursor-pointer items-center gap-2 text-sm">
<Shield className="size-4" />
Usage Analytics
</label>
</div>
</div>
</div>
{/* Privacy Settings with DescriptionSwitch */}
<div>
<h3 className="mb-4 text-base font-semibold">Privacy Settings</h3>
<div className="flex flex-col gap-4">
<DescriptionSwitch
label="Share usage data"
description="Help us improve by sharing anonymous usage statistics"
checked={privacy.shareData}
onCheckedChange={(checked) => setPrivacy({ ...privacy, shareData: !!checked })}
/>
<DescriptionSwitch
label="Allow cookies"
description="Enable cookies for better user experience"
checked={privacy.allowCookies}
onCheckedChange={(checked) => setPrivacy({ ...privacy, allowCookies: !!checked })}
/>
<DescriptionSwitch
label="Track location"
description="Use your location for personalized content"
checked={privacy.trackLocation}
onCheckedChange={(checked) => setPrivacy({ ...privacy, trackLocation: !!checked })}
/>
<DescriptionSwitch
label="Personalized ads"
description="Show ads based on your interests"
checked={privacy.personalizedAds}
onCheckedChange={(checked) => setPrivacy({ ...privacy, personalizedAds: !!checked })}
/>
</div>
</div>
</div>
)
}
}
// Interactive Loading Example
export const InteractiveLoading: Story = {
render: function InteractiveLoadingExample() {
const [isLoading, setIsLoading] = useState(false)
const [isEnabled, setIsEnabled] = useState(false)
const handleToggle = async (checked: boolean) => {
setIsLoading(true)
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000))
setIsEnabled(checked)
setIsLoading(false)
}
return (
<div className="flex flex-col gap-6">
<div className="flex w-96 flex-col gap-4">
<DescriptionSwitch
label="Wi-Fi Connection"
description="Connect to wireless networks"
checked={isEnabled}
onCheckedChange={handleToggle}
loading={isLoading}
disabled={isLoading}
/>
<div className="rounded-md bg-muted p-4">
<div className="flex items-center gap-2 text-sm">
<Wifi className="size-4" />
<span className="font-medium">Status:</span>
<span className="text-muted-foreground">
{isLoading ? 'Connecting...' : isEnabled ? 'Connected' : 'Disconnected'}
</span>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground">Click the switch to see a simulated 2-second loading state</p>
</div>
)
}
}
// Form Example
export const FormExample: Story = {
render: function FormExample() {
const [formData, setFormData] = useState({
emailNotifications: true,
pushNotifications: false,
smsNotifications: false,
newsletter: true,
twoFactorAuth: false,
biometricAuth: true
})
const [isSaving, setIsSaving] = useState(false)
const [saved, setSaved] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSaving(true)
setSaved(false)
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500))
setIsSaving(false)
setSaved(true)
setTimeout(() => setSaved(false), 3000)
}
return (
<form onSubmit={handleSubmit} className="w-[500px] space-y-6">
<h3 className="text-base font-semibold">Account Preferences</h3>
<div className="space-y-4">
<div>
<h4 className="mb-3 text-sm font-medium">Notifications</h4>
<div className="space-y-3">
<DescriptionSwitch
label="Email notifications"
description="Receive updates via email"
checked={formData.emailNotifications}
onCheckedChange={(checked) => setFormData({ ...formData, emailNotifications: !!checked })}
disabled={isSaving}
/>
<DescriptionSwitch
label="Push notifications"
description="Get instant alerts on your device"
checked={formData.pushNotifications}
onCheckedChange={(checked) => setFormData({ ...formData, pushNotifications: !!checked })}
disabled={isSaving}
/>
<DescriptionSwitch
label="SMS notifications"
description="Receive text message alerts"
checked={formData.smsNotifications}
onCheckedChange={(checked) => setFormData({ ...formData, smsNotifications: !!checked })}
disabled={isSaving}
/>
<DescriptionSwitch
label="Newsletter subscription"
description="Stay updated with our latest news"
checked={formData.newsletter}
onCheckedChange={(checked) => setFormData({ ...formData, newsletter: !!checked })}
disabled={isSaving}
/>
</div>
</div>
<div>
<h4 className="mb-3 text-sm font-medium">Security</h4>
<div className="space-y-3">
<DescriptionSwitch
label="Two-factor authentication"
description="Add an extra layer of security"
checked={formData.twoFactorAuth}
onCheckedChange={(checked) => setFormData({ ...formData, twoFactorAuth: !!checked })}
disabled={isSaving}
/>
<DescriptionSwitch
label="Biometric authentication"
description="Use fingerprint or face recognition"
checked={formData.biometricAuth}
onCheckedChange={(checked) => setFormData({ ...formData, biometricAuth: !!checked })}
disabled={isSaving}
/>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button
type="submit"
disabled={isSaving}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50">
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
{saved && <p className="text-sm text-green-600">Settings saved successfully!</p>}
</div>
</form>
)
}
}
// Accessibility Example
export const Accessibility: Story = {
render: () => (
<div className="flex w-96 flex-col gap-6">
<div>
<h3 className="mb-4 text-base font-semibold">Keyboard Navigation</h3>
<p className="mb-4 text-sm text-muted-foreground">
Use Tab to navigate between switches and Space/Enter to toggle them.
</p>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Switch id="a11y-1" />
<label htmlFor="a11y-1" className="cursor-pointer text-sm">
First switch
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="a11y-2" />
<label htmlFor="a11y-2" className="cursor-pointer text-sm">
Second switch
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="a11y-3" />
<label htmlFor="a11y-3" className="cursor-pointer text-sm">
Third switch
</label>
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-base font-semibold">ARIA Labels</h3>
<p className="mb-4 text-sm text-muted-foreground">
Switches include proper ARIA attributes for screen reader support.
</p>
<DescriptionSwitch
label="Accessibility features"
description="Enable enhanced accessibility options for better usability"
defaultChecked
/>
</div>
</div>
)
}

View File

@@ -27,7 +27,6 @@ import { buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder'
import { buildPlugins } from './plugins/PluginBuilder'
import { createAiSdkProvider } from './provider/factory'
import {
adaptProvider,
getActualProvider,
isModernSdkSupported,
prepareSpecialProviderConfig,
@@ -65,11 +64,12 @@ export default class ModernAiProvider {
* - URL will be automatically formatted via `formatProviderApiHost`, adding version suffixes like `/v1`
*
* 2. When called with `(model, provider)`:
* - The provided provider will be adapted via `adaptProvider`
* - URL formatting behavior depends on the adapted result
* - **Directly uses the provided provider WITHOUT going through `getActualProvider`**
* - **URL will NOT be automatically formatted, `/v1` suffix will NOT be added**
* - This is legacy behavior kept for backward compatibility
*
* 3. When called with `(provider)`:
* - The provider will be adapted via `adaptProvider`
* - Directly uses the provider without requiring a model
* - Used for operations that don't need a model (e.g., fetchModels)
*
* @example
@@ -77,7 +77,7 @@ export default class ModernAiProvider {
* // Recommended: Auto-format URL
* const ai = new ModernAiProvider(model)
*
* // Provider will be adapted
* // Not recommended: Skip URL formatting (only for special cases)
* const ai = new ModernAiProvider(model, customProvider)
*
* // For operations that don't need a model
@@ -91,12 +91,12 @@ export default class ModernAiProvider {
if (this.isModel(modelOrProvider)) {
// 传入的是 Model
this.model = modelOrProvider
this.actualProvider = provider ? adaptProvider({ provider }) : getActualProvider(modelOrProvider)
this.actualProvider = provider || getActualProvider(modelOrProvider)
// 只保存配置不预先创建executor
this.config = providerToAiSdkConfig(this.actualProvider, modelOrProvider)
} else {
// 传入的是 Provider
this.actualProvider = adaptProvider({ provider: modelOrProvider })
this.actualProvider = modelOrProvider
// model为可选某些操作如fetchModels不需要model
}

View File

@@ -79,13 +79,11 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
}
/**
* Format and normalize the API host URL for a provider.
* Handles provider-specific URL formatting rules (e.g., appending version paths, Azure formatting).
*
* @param provider - The provider whose API host is to be formatted.
* @returns A new provider instance with the formatted API host.
* 主要用来对齐AISdk的BaseURL格式
* @param provider
* @returns
*/
export function formatProviderApiHost(provider: Provider): Provider {
function formatProviderApiHost(provider: Provider): Provider {
const formatted = { ...provider }
if (formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
@@ -117,38 +115,18 @@ export function formatProviderApiHost(provider: Provider): Provider {
}
/**
* Retrieve the effective Provider configuration for the given model.
* Applies all necessary transformations (special-provider handling, URL formatting, etc.).
*
* @param model - The model whose provider is to be resolved.
* @returns A new Provider instance with all adaptations applied.
* 获取实际的Provider配置
* 简化版:将逻辑分解为小函数
*/
export function getActualProvider(model: Model): Provider {
const baseProvider = getProviderByModel(model)
return adaptProvider({ provider: baseProvider, model })
}
// 按顺序处理各种转换
let actualProvider = cloneDeep(baseProvider)
actualProvider = handleSpecialProviders(model, actualProvider)
actualProvider = formatProviderApiHost(actualProvider)
/**
* Transforms a provider configuration by applying model-specific adaptations and normalizing its API host.
* The transformations are applied in the following order:
* 1. Model-specific provider handling (e.g., New-API, system providers, Azure OpenAI)
* 2. API host formatting (provider-specific URL normalization)
*
* @param provider - The base provider configuration to transform.
* @param model - The model associated with the provider; optional but required for special-provider handling.
* @returns A new Provider instance with all transformations applied.
*/
export function adaptProvider({ provider, model }: { provider: Provider; model?: Model }): Provider {
let adaptedProvider = cloneDeep(provider)
// Apply transformations in order
if (model) {
adaptedProvider = handleSpecialProviders(model, adaptedProvider)
}
adaptedProvider = formatProviderApiHost(adaptedProvider)
return adaptedProvider
return actualProvider
}
/**

View File

@@ -415,7 +415,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
</Form.Item>
{!rawContent && (
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
<Switch checked={exportReasoning} onCheckedChange={setExportReasoning} />
<Switch isSelected={exportReasoning} onValueChange={setExportReasoning} />
</Form.Item>
)}
</Form>

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "Preview: {{url}}",
"reset": "Reset",
"tip": "Add # at the end to disable the automatically appended API version."
"tip": "ending with # forces use of input address"
}
},
"api_host": "API Host",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "预览: {{url}}",
"reset": "重置",
"tip": "在末尾添加 # 以禁用自动附加的API版本。"
"tip": "# 结尾强制使用输入地址"
}
},
"api_host": "API 地址",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "預覽:{{url}}",
"reset": "重設",
"tip": "在末尾添加 # 以停用自動附加的 API 版本。"
"tip": "# 結尾強制使用輸入位址"
}
},
"api_host": "API 主機地址",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "Vorschau: {{url}}",
"reset": "Zurücksetzen",
"tip": "Fügen Sie am Ende ein # hinzu, um die automatisch angehängte API-Version zu deaktivieren."
"tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
}
},
"api_host": "API-Adresse",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "Προεπισκόπηση: {{url}}",
"reset": "Επαναφορά",
"tip": "Προσθέστε το σύμβολο # στο τέλος για να απενεργοποιήσετε την αυτόματα προστιθέμενη έκδοση API."
"tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
}
},
"api_host": "Διεύθυνση API",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "Vista previa: {{url}}",
"reset": "Restablecer",
"tip": "Añada # al final para deshabilitar la versión de la API que se añade automáticamente."
"tip": "forzar uso de dirección de entrada con # al final"
}
},
"api_host": "Dirección API",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "Aperçu : {{url}}",
"reset": "Réinitialiser",
"tip": "Ajoutez # à la fin pour désactiver la version d'API ajoutée automatiquement."
"tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #"
}
},
"api_host": "Adresse API",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "プレビュー: {{url}}",
"reset": "リセット",
"tip": "自動的に付加されるAPIバージョンを無効にするには、末尾に#を追加します"
"tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
}
},
"api_host": "APIホスト",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "Pré-visualização: {{url}}",
"reset": "Redefinir",
"tip": "Adicione # no final para desativar a versão da API adicionada automaticamente."
"tip": "e forçar o uso do endereço original quando terminar com '#'"
}
},
"api_host": "Endereço API",

View File

@@ -4372,7 +4372,7 @@
"url": {
"preview": "Предпросмотр: {{url}}",
"reset": "Сброс",
"tip": "Добавьте # в конце, чтобы отключить автоматически добавляемую версию API."
"tip": "заканчивая на # принудительно использует введенный адрес"
}
},
"api_host": "Хост API",

View File

@@ -45,7 +45,7 @@ const logger = loggerService.withContext('AgentSessionInputbar')
const DRAFT_CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours
const getAgentDraftCacheKey = (agentId: string) => `agent-session-draft-${agentId}`
const getAgentDraftCacheKey = (agentId: string) => `agent.session.draft.${agentId}`
type Props = {
agentId: string

View File

@@ -51,10 +51,10 @@ import TokenCount from './TokenCount'
const logger = loggerService.withContext('Inputbar')
const INPUTBAR_DRAFT_CACHE_KEY = 'inputbar-draft'
const INPUTBAR_DRAFT_CACHE_KEY = 'inputbar.draft.text'
const DRAFT_CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours
const getMentionedModelsCacheKey = (assistantId: string) => `inputbar-mentioned-models-${assistantId}`
const getMentionedModelsCacheKey = (assistantId: string) => `inputbar.mentioned.models.${assistantId}`
const getValidatedCachedModels = (assistantId: string): Model[] => {
const cached = cacheService.get<Model[]>(getMentionedModelsCacheKey(assistantId))
@@ -134,6 +134,10 @@ const InputbarInner: FC<InputbarInnerProps> = ({ assistant: initialAssistant, se
const { setFiles, setMentionedModels, setSelectedKnowledgeBases } = useInputbarToolsDispatch()
const { setCouldAddImageFile } = useInputbarToolsInternalDispatch()
const { text, setText } = useInputText({
initialValue: cacheService.get<string>(INPUTBAR_DRAFT_CACHE_KEY) ?? '',
onChange: (value) => cacheService.set(INPUTBAR_DRAFT_CACHE_KEY, value, DRAFT_CACHE_TTL)
})
const { text, setText } = useInputText({
initialValue: cacheService.get<string>(INPUTBAR_DRAFT_CACHE_KEY) ?? '',
onChange: (value) => cacheService.set(INPUTBAR_DRAFT_CACHE_KEY, value, DRAFT_CACHE_TTL)
@@ -213,8 +217,7 @@ const InputbarInner: FC<InputbarInnerProps> = ({ assistant: initialAssistant, se
useEffect(() => {
return () => onUnmount(assistant.id)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assistant.id])
}, [assistant.id, onUnmount])
const placeholderText = enableQuickPanelTriggers
? t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })

View File

@@ -115,7 +115,6 @@ const SettingsTab: FC<Props> = (props) => {
const { theme } = useTheme()
const { themeNames } = useCodeStyle()
// FIXME: We should use useMemo to calculate these states instead of using useEffect to sync
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? true)
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
@@ -271,9 +270,10 @@ const SettingsTab: FC<Props> = (props) => {
<HelpTooltip title={t('chat.settings.temperature.tip')} />
</SettingRowTitleSmall>
<Switch
size="sm"
style={{ marginLeft: 'auto' }}
checked={enableTemperature}
onCheckedChange={(enabled) => {
isSelected={enableTemperature}
onValueChange={(enabled) => {
setEnableTemperature(enabled)
onUpdateAssistantSettings({ enableTemperature: enabled })
}}
@@ -340,8 +340,9 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRow>
<SettingRowTitleSmall>{t('models.stream_output')}</SettingRowTitleSmall>
<Switch
checked={streamOutput}
onCheckedChange={(checked) => {
size="sm"
isSelected={streamOutput}
onValueChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
@@ -356,8 +357,9 @@ const SettingsTab: FC<Props> = (props) => {
</SettingRowTitleSmall>
</Row>
<Switch
checked={enableMaxTokens}
onCheckedChange={async (enabled) => {
size="sm"
isSelected={enableMaxTokens}
onValueChange={async (enabled) => {
if (enabled) {
const confirmed = await modalConfirm({
title: t('chat.settings.max_tokens.confirm'),
@@ -408,36 +410,38 @@ const SettingsTab: FC<Props> = (props) => {
<CollapsibleSettingGroup title={t('settings.messages.title')} defaultExpanded={true}>
<SettingGroup>
<SettingRow>
<DescriptionSwitch
checked={showPrompt}
onCheckedChange={setShowPrompt}
label={t('settings.messages.prompt')}
/>
<DescriptionSwitch size="sm" isSelected={showPrompt} onValueChange={setShowPrompt}>
<SettingRowTitleSmall>{t('settings.messages.prompt')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
{/* <SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall> */}
<DescriptionSwitch
checked={messageFont === 'serif'}
onCheckedChange={(checked) => setMessageFont(checked ? 'serif' : 'system')}
label={t('settings.messages.use_serif_font')}
/>
size="sm"
isSelected={messageFont === 'serif'}
onValueChange={(checked) => setMessageFont(checked ? 'serif' : 'system')}>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={thoughtAutoCollapse}
onCheckedChange={setThoughtAutoCollapse}
label={t('chat.settings.thought_auto_collapse.label')}
description={t('chat.settings.thought_auto_collapse.tip')}
/>
{/* <SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse.label')}
<HelpTooltip title={t('chat.settings.thought_auto_collapse.tip')} />
</SettingRowTitleSmall> */}
<DescriptionSwitch isSelected={thoughtAutoCollapse} onValueChange={setThoughtAutoCollapse}>
<SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse.label')}
<HelpTooltip content={t('chat.settings.thought_auto_collapse.tip')} />
</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={showMessageOutline}
onCheckedChange={setShowMessageOutline}
label={t('settings.messages.show_message_outline')}
/>
<DescriptionSwitch isSelected={showMessageOutline} onValueChange={setShowMessageOutline}>
<SettingRowTitleSmall>{t('settings.messages.show_message_outline')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
@@ -530,12 +534,16 @@ const SettingsTab: FC<Props> = (props) => {
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={mathEnableSingleDollar}
onCheckedChange={setMathEnableSingleDollar}
label={t('settings.math.single_dollar.label')}
description={t('settings.math.single_dollar.tip')}
/>
{/* <SettingRowTitleSmall>
{t('settings.math.single_dollar.label')}
<HelpTooltip title={t('settings.math.single_dollar.tip')} />
</SettingRowTitleSmall> */}
<DescriptionSwitch size="sm" isSelected={mathEnableSingleDollar} onValueChange={setMathEnableSingleDollar}>
<SettingRowTitleSmall>
{t('settings.math.single_dollar.label')}
<HelpTooltip content={t('settings.math.single_dollar.tip')} />
</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
</SettingGroup>
@@ -559,21 +567,32 @@ const SettingsTab: FC<Props> = (props) => {
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={codeFancyBlock}
onCheckedChange={setCodeFancyBlock}
label={t('chat.settings.code_fancy_block.label')}
description={t('chat.settings.code_fancy_block.tip')}
/>
{/* <SettingRowTitleSmall>
{t('chat.settings.code_fancy_block.label')}
<HelpTooltip title={t('chat.settings.code_fancy_block.tip')} />
</SettingRowTitleSmall> */}
<DescriptionSwitch size="sm" isSelected={codeFancyBlock} onValueChange={setCodeFancyBlock}>
<SettingRowTitleSmall>
{t('chat.settings.code_fancy_block.label')}
<HelpTooltip content={t('chat.settings.code_fancy_block.tip')} />
</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
{/* <SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<HelpTooltip title={t('chat.settings.code_execution.tip')} />
</SettingRowTitleSmall> */}
<DescriptionSwitch
checked={codeExecution.enabled}
onCheckedChange={(checked) => setCodeExecution({ enabled: checked })}
label={t('chat.settings.code_execution.title')}
description={t('chat.settings.code_execution.tip')}
/>
size="sm"
isSelected={codeExecution.enabled}
onValueChange={(checked) => setCodeExecution({ enabled: checked })}>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<HelpTooltip content={t('chat.settings.code_execution.tip')} />
</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
{codeExecution.enabled && (
<>
@@ -597,80 +616,90 @@ const SettingsTab: FC<Props> = (props) => {
)}
<SettingDivider />
<SettingRow>
{/* <SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall> */}
<DescriptionSwitch
checked={codeEditor.enabled}
onCheckedChange={(checked) => setCodeEditor({ enabled: checked })}
label={t('chat.settings.code_editor.title')}
/>
size="sm"
isSelected={codeEditor.enabled}
onValueChange={(checked) => setCodeEditor({ enabled: checked })}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
{codeEditor.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
{/* <SettingRowTitleSmall>
{t('chat.settings.code_editor.highlight_active_line')}
<HelpTooltip title={t('chat.settings.code_editor.highlight_active_line.tip')} />
</SettingRowTitleSmall> */}
<DescriptionSwitch
checked={codeEditor.highlightActiveLine}
onCheckedChange={(checked) => setCodeEditor({ highlightActiveLine: checked })}
label={t('chat.settings.code_editor.highlight_active_line')}
/>
size="sm"
isSelected={codeEditor.highlightActiveLine}
onValueChange={(checked) => setCodeEditor({ highlightActiveLine: checked })}>
<SettingRowTitleSmall>
{t('chat.settings.code_editor.highlight_active_line')}
<HelpTooltip content={t('chat.settings.code_editor.highlight_active_line.tip')} />
</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
{/* <SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall> */}
<DescriptionSwitch
checked={codeEditor.foldGutter}
onCheckedChange={(checked) => setCodeEditor({ foldGutter: checked })}
label={t('chat.settings.code_editor.fold_gutter')}
/>
size="sm"
isSelected={codeEditor.foldGutter}
onValueChange={(checked) => setCodeEditor({ foldGutter: checked })}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
{/* <SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall> */}
<DescriptionSwitch
checked={codeEditor.autocompletion}
onCheckedChange={(checked) => setCodeEditor({ autocompletion: checked })}
label={t('chat.settings.code_editor.autocompletion')}
/>
size="sm"
isSelected={codeEditor.autocompletion}
onValueChange={(checked) => setCodeEditor({ autocompletion: checked })}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
{/* <SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall> */}
<DescriptionSwitch
checked={codeEditor.keymap}
onCheckedChange={(checked) => setCodeEditor({ keymap: checked })}
label={t('chat.settings.code_editor.keymap')}
/>
size="sm"
isSelected={codeEditor.keymap}
onValueChange={(checked) => setCodeEditor({ keymap: checked })}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={codeShowLineNumbers}
onCheckedChange={setCodeShowLineNumbers}
label={t('chat.settings.show_line_numbers')}
/>
<DescriptionSwitch size="sm" isSelected={codeShowLineNumbers} onValueChange={setCodeShowLineNumbers}>
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={codeCollapsible}
onCheckedChange={setCodeCollapsible}
label={t('chat.settings.code_collapsible')}
/>
<DescriptionSwitch size="sm" isSelected={codeCollapsible} onValueChange={setCodeCollapsible}>
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={codeWrappable}
onCheckedChange={setCodeWrappable}
label={t('chat.settings.code_wrappable')}
/>
<DescriptionSwitch size="sm" isSelected={codeWrappable} onValueChange={setCodeWrappable}>
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={codeImageTools}
onCheckedChange={setCodeImageTools}
label={t('chat.settings.code_image_tools.label')}
description={t('chat.settings.code_image_tools.tip')}
/>
<DescriptionSwitch size="sm" isSelected={codeImageTools} onValueChange={setCodeImageTools}>
<SettingRowTitleSmall>
{t('chat.settings.code_image_tools.label')}
<HelpTooltip content={t('chat.settings.code_image_tools.tip')} />
</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
</SettingGroup>
<SettingDivider />
@@ -679,18 +708,17 @@ const SettingsTab: FC<Props> = (props) => {
<SettingGroup>
<SettingRow>
<DescriptionSwitch
checked={showInputEstimatedTokens}
onCheckedChange={setShowInputEstimatedTokens}
label={t('settings.messages.input.show_estimated_tokens')}
/>
size="sm"
isSelected={showInputEstimatedTokens}
onValueChange={setShowInputEstimatedTokens}>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={pasteLongTextAsFile}
onCheckedChange={setPasteLongTextAsFile}
label={t('settings.messages.input.paste_long_text_as_file')}
/>
<DescriptionSwitch size="sm" isSelected={pasteLongTextAsFile} onValueChange={setPasteLongTextAsFile}>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
{pasteLongTextAsFile && (
<>
@@ -712,54 +740,54 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={renderInputMessageAsMarkdown}
onCheckedChange={setRenderInputMessageAsMarkdown}
label={t('settings.messages.markdown_rendering_input_message')}
/>
size="sm"
isSelected={renderInputMessageAsMarkdown}
onValueChange={setRenderInputMessageAsMarkdown}>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
{!(language || navigator.language).startsWith('en') && (
<>
<SettingRow>
<DescriptionSwitch
checked={autoTranslateWithSpace}
onCheckedChange={setAutoTranslateWithSpace}
label={t('settings.input.auto_translate_with_space')}
/>
size="sm"
isSelected={autoTranslateWithSpace}
onValueChange={setAutoTranslateWithSpace}>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
</>
)}
<SettingRow>
<DescriptionSwitch
checked={showTranslateConfirm}
onCheckedChange={setShowTranslateConfirm}
label={t('settings.input.show_translate_confirm')}
/>
<DescriptionSwitch size="sm" isSelected={showTranslateConfirm} onValueChange={setShowTranslateConfirm}>
<SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={enableQuickPanelTriggers}
onCheckedChange={setEnableQuickPanelTriggers}
label={t('settings.messages.input.enable_quick_triggers')}
/>
size="sm"
isSelected={enableQuickPanelTriggers}
onValueChange={setEnableQuickPanelTriggers}>
<SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch size="sm" isSelected={confirmDeleteMessage} onValueChange={setConfirmDeleteMessage}>
<SettingRowTitleSmall>{t('settings.messages.input.confirm_delete_message')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={confirmDeleteMessage}
onCheckedChange={setConfirmDeleteMessage}
label={t('settings.messages.input.confirm_delete_message')}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<DescriptionSwitch
checked={confirmRegenerateMessage}
onCheckedChange={setConfirmRegenerateMessage}
label={t('settings.messages.input.confirm_regenerate_message')}
/>
size="sm"
isSelected={confirmRegenerateMessage}
onValueChange={setConfirmRegenerateMessage}>
<SettingRowTitleSmall>{t('settings.messages.input.confirm_regenerate_message')}</SettingRowTitleSmall>
</DescriptionSwitch>
</SettingRow>
<SettingDivider />
<SettingRow>

View File

@@ -96,7 +96,7 @@ const MiniAppSettings: FC = () => {
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
</SettingLabelGroup>
<Switch checked={minappsOpenLinkExternal} onCheckedChange={(checked) => setMinappsOpenLinkExternal(checked)} />
<Switch isSelected={minappsOpenLinkExternal} onValueChange={(checked) => setMinappsOpenLinkExternal(checked)} />
</SettingRow>
<SettingDivider />
{/* 缓存小程序数量设置 */}
@@ -134,8 +134,8 @@ const MiniAppSettings: FC = () => {
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
</SettingLabelGroup>
<Switch
checked={showOpenedMinappsInSidebar}
onCheckedChange={(checked) => setShowOpenedMinappsInSidebar(checked)}
isSelected={showOpenedMinappsInSidebar}
onValueChange={(checked) => setShowOpenedMinappsInSidebar(checked)}
/>
</SettingRow>
</Container>

View File

@@ -800,8 +800,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<RowFlex>
<Switch
checked={(painting[item.key!] || item.initialValue) as boolean}
onCheckedChange={(checked) => updatePaintingState({ [item.key!]: checked })}
isSelected={(painting[item.key!] || item.initialValue) as boolean}
onValueChange={(checked) => updatePaintingState({ [item.key!]: checked })}
/>
</RowFlex>
)

View File

@@ -938,7 +938,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
<InfoTooltip content={t('paintings.auto_create_paint_tip')} />
</SettingTitle>
<RowFlex>
<Switch checked={painting.autoCreate} onCheckedChange={(checked) => onChangeAutoCreate(checked)} />
<Switch isSelected={painting.autoCreate} onValueChange={(checked) => onChangeAutoCreate(checked)} />
</RowFlex>
</LeftContainer>
<MainContainer>

View File

@@ -464,8 +464,8 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
</SettingTitle>
<RowFlex>
<Switch
checked={painting.promptEnhancement}
onCheckedChange={(checked) => updatePaintingState({ promptEnhancement: checked })}
isSelected={painting.promptEnhancement}
onValueChange={(checked) => updatePaintingState({ promptEnhancement: checked })}
/>
</RowFlex>
</LeftContainer>

View File

@@ -198,8 +198,8 @@ export const DynamicFormRender: React.FC<DynamicFormRenderProps> = ({
if (type === 'boolean') {
return (
<Switch
checked={value !== undefined ? value : defaultValue}
onCheckedChange={(checked) => onChange(propertyName, checked)}
isSelected={value !== undefined ? value : defaultValue}
onValueChange={(checked) => onChange(propertyName, checked)}
style={{ width: '2px' }}
/>
)

View File

@@ -228,13 +228,13 @@ const AboutSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.auto_check_update.title')}</SettingRowTitle>
<Switch checked={autoCheckUpdate} onCheckedChange={(v) => setAutoCheckUpdate(v)} />
<Switch isSelected={autoCheckUpdate} onValueChange={(v) => setAutoCheckUpdate(v)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.test_plan.title')}</SettingRowTitle>
<Tooltip content={t('settings.general.test_plan.tooltip')}>
<Switch checked={testPlan} onCheckedChange={(v) => handleSetTestPlan(v)} />
<Switch isSelected={testPlan} onValueChange={(v) => handleSetTestPlan(v)} />
</Tooltip>
</SettingRow>
{testPlan && (

View File

@@ -1,4 +1,3 @@
import { Switch } from '@cherrystudio/ui'
import { permissionModeCards } from '@renderer/config/agent'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
@@ -14,7 +13,7 @@ import type {
} from '@renderer/types'
import { AgentConfigurationSchema } from '@renderer/types'
import { Modal, Tag } from 'antd'
import { Alert, Card, Input } from 'antd'
import { Alert, Card, Input, Switch } from 'antd'
import { ShieldAlert, Wrench } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useMemo, useState } from 'react'
@@ -402,8 +401,8 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
})}
checked={isApproved}
disabled={isAuto || isUpdatingTools}
size="sm"
onCheckedChange={(checked) => handleToggleTool(tool.id, checked)}
size="small"
onChange={(checked) => handleToggleTool(tool.id, checked)}
/>
</div>
}
@@ -484,9 +483,9 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
name: server.name
})}
checked={isSelected}
size="sm"
size="small"
disabled={!server.isActive || isUpdatingMcp}
onCheckedChange={(checked) => handleToggleMcp(server.id, checked)}
onChange={(checked) => handleToggleMcp(server.id, checked)}
/>
</div>
}

View File

@@ -87,9 +87,10 @@ const AssistantMCPSettings: React.FC<Props> = ({ assistant, updateAssistant }) =
: undefined
}>
<Switch
checked={isEnabled}
isSelected={isEnabled}
disabled={!server.isActive}
onCheckedChange={() => handleServerToggle(server.id)}
onValueChange={() => handleServerToggle(server.id)}
size="sm"
/>
</Tooltip>
</ServerItem>

View File

@@ -94,8 +94,8 @@ const AssistantMemorySettings: React.FC<Props> = ({ assistant, updateAssistant,
: ''
}>
<Switch
checked={assistant.enableMemory || false}
onCheckedChange={handleMemoryToggle}
isSelected={assistant.enableMemory || false}
onValueChange={handleMemoryToggle}
disabled={!isMemoryEnabled}
/>
</Tooltip>

View File

@@ -246,8 +246,8 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
</Label>
</RowFlex>
<Switch
checked={enableTemperature}
onCheckedChange={(enabled) => {
isSelected={enableTemperature}
onValueChange={(enabled) => {
setEnableTemperature(enabled)
updateAssistantSettings({ enableTemperature: enabled })
}}
@@ -295,8 +295,8 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
/>
</RowFlex>
<Switch
checked={enableTopP}
onCheckedChange={(enabled) => {
isSelected={enableTopP}
onValueChange={(enabled) => {
setEnableTopP(enabled)
updateAssistantSettings({ enableTopP: enabled })
}}
@@ -387,8 +387,8 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
/>
</RowFlex>
<Switch
checked={enableMaxTokens}
onCheckedChange={async (enabled) => {
isSelected={enableMaxTokens}
onValueChange={async (enabled) => {
if (enabled) {
const confirmed = await modalConfirm({
title: t('chat.settings.max_tokens.confirm'),
@@ -430,8 +430,8 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
<SettingRow style={{ minHeight: 30 }}>
<Label>{t('models.stream_output')}</Label>
<Switch
checked={streamOutput}
onCheckedChange={(checked) => {
isSelected={streamOutput}
onValueChange={(checked) => {
setStreamOutput(checked)
updateAssistantSettings({ streamOutput: checked })
}}

View File

@@ -6,7 +6,7 @@ import {
WifiOutlined,
YuqueOutlined
} from '@ant-design/icons'
import { Button, RowFlex, Switch } from '@cherrystudio/ui'
import { Button, RowFlex } from '@cherrystudio/ui'
import { usePreference } from '@data/hooks/usePreference'
import DividerWithText from '@renderer/components/DividerWithText'
import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons'
@@ -22,7 +22,7 @@ import { reset } from '@renderer/services/BackupService'
import type { AppInfo } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { occupiedDirs } from '@shared/config/constant'
import { Progress, Typography } from 'antd'
import { Progress, Switch, Typography } from 'antd'
import { FileText, FolderCog, FolderInput, FolderOpen, SaveIcon } from 'lucide-react'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
@@ -291,7 +291,7 @@ const DataSettings: FC = () => {
<MigrationPathRow style={{ marginTop: '20px', flexDirection: 'row', alignItems: 'center' }}>
<Switch
defaultChecked={shouldCopyData}
onCheckedChange={(checked) => (shouldCopyData = checked)}
onChange={(checked) => (shouldCopyData = checked)}
style={{ marginRight: '8px' }}
title={t('settings.data.app_data.copy_data_option')}
/>
@@ -616,7 +616,7 @@ const DataSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={skipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
<Switch checked={skipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>

View File

@@ -35,15 +35,18 @@ const ExportMenuOptions: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.image')}</SettingRowTitle>
<Switch checked={exportMenuOptions.image} onCheckedChange={(checked) => handleToggleOption('image', checked)} />
<Switch
isSelected={exportMenuOptions.image}
onValueChange={(checked) => handleToggleOption('image', checked)}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.markdown')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.markdown}
onCheckedChange={(checked) => handleToggleOption('markdown', checked)}
isSelected={exportMenuOptions.markdown}
onValueChange={(checked) => handleToggleOption('markdown', checked)}
/>
</SettingRow>
<SettingDivider />
@@ -51,8 +54,8 @@ const ExportMenuOptions: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.markdown_reason')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.markdown_reason}
onCheckedChange={(checked) => handleToggleOption('markdown_reason', checked)}
isSelected={exportMenuOptions.markdown_reason}
onValueChange={(checked) => handleToggleOption('markdown_reason', checked)}
/>
</SettingRow>
<SettingDivider />
@@ -60,23 +63,26 @@ const ExportMenuOptions: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.notion')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.notion}
onCheckedChange={(checked) => handleToggleOption('notion', checked)}
isSelected={exportMenuOptions.notion}
onValueChange={(checked) => handleToggleOption('notion', checked)}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.yuque')}</SettingRowTitle>
<Switch checked={exportMenuOptions.yuque} onCheckedChange={(checked) => handleToggleOption('yuque', checked)} />
<Switch
isSelected={exportMenuOptions.yuque}
onValueChange={(checked) => handleToggleOption('yuque', checked)}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.joplin')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.joplin}
onCheckedChange={(checked) => handleToggleOption('joplin', checked)}
isSelected={exportMenuOptions.joplin}
onValueChange={(checked) => handleToggleOption('joplin', checked)}
/>
</SettingRow>
<SettingDivider />
@@ -84,8 +90,8 @@ const ExportMenuOptions: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.obsidian')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.obsidian}
onCheckedChange={(checked) => handleToggleOption('obsidian', checked)}
isSelected={exportMenuOptions.obsidian}
onValueChange={(checked) => handleToggleOption('obsidian', checked)}
/>
</SettingRow>
<SettingDivider />
@@ -93,23 +99,23 @@ const ExportMenuOptions: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.siyuan')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.siyuan}
onCheckedChange={(checked) => handleToggleOption('siyuan', checked)}
isSelected={exportMenuOptions.siyuan}
onValueChange={(checked) => handleToggleOption('siyuan', checked)}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.docx')}</SettingRowTitle>
<Switch checked={exportMenuOptions.docx} onCheckedChange={(checked) => handleToggleOption('docx', checked)} />
<Switch isSelected={exportMenuOptions.docx} onValueChange={(checked) => handleToggleOption('docx', checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.plain_text')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.plain_text}
onCheckedChange={(checked) => handleToggleOption('plain_text', checked)}
isSelected={exportMenuOptions.plain_text}
onValueChange={(checked) => handleToggleOption('plain_text', checked)}
/>
</SettingRow>
</SettingGroup>

View File

@@ -122,7 +122,7 @@ const JoplinSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.joplin.export_reasoning.title')}</SettingRowTitle>
<Switch checked={joplinExportReasoning} onCheckedChange={handleToggleJoplinExportReasoning} />
<Switch isSelected={joplinExportReasoning} onValueChange={handleToggleJoplinExportReasoning} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.joplin.export_reasoning.help')}</SettingHelpText>

View File

@@ -261,7 +261,7 @@ const LocalBackupSettings: React.FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={localBackupSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
<Switch isSelected={localBackupSkipBackupFile} onValueChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>

View File

@@ -98,7 +98,7 @@ const MarkdownExportSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.markdown_export.force_dollar_math.title')}</SettingRowTitle>
<Switch checked={forceDollarMathInMarkdown} onCheckedChange={handleToggleForceDollarMath} />
<Switch isSelected={forceDollarMathInMarkdown} onValueChange={handleToggleForceDollarMath} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.markdown_export.force_dollar_math.help')}</SettingHelpText>
@@ -106,7 +106,7 @@ const MarkdownExportSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.message_title.use_topic_naming.title')}</SettingRowTitle>
<Switch checked={useTopicNamingForMessageTitle} onCheckedChange={handleToggleTopicNaming} />
<Switch isSelected={useTopicNamingForMessageTitle} onValueChange={handleToggleTopicNaming} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.message_title.use_topic_naming.help')}</SettingHelpText>
@@ -114,7 +114,7 @@ const MarkdownExportSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.markdown_export.show_model_name.title')}</SettingRowTitle>
<Switch checked={showModelNameInExport} onCheckedChange={handleToggleShowModelName} />
<Switch isSelected={showModelNameInExport} onValueChange={handleToggleShowModelName} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.markdown_export.show_model_name.help')}</SettingHelpText>
@@ -122,7 +122,7 @@ const MarkdownExportSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.markdown_export.show_model_provider.title')}</SettingRowTitle>
<Switch checked={showModelProviderInMarkdown} onCheckedChange={handleToggleShowModelProvider} />
<Switch isSelected={showModelProviderInMarkdown} onValueChange={handleToggleShowModelProvider} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.markdown_export.show_model_provider.help')}</SettingHelpText>
@@ -130,7 +130,7 @@ const MarkdownExportSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.markdown_export.exclude_citations.title')}</SettingRowTitle>
<Switch checked={excludeCitationsInExport} onCheckedChange={handleToggleExcludeCitations} />
<Switch isSelected={excludeCitationsInExport} onValueChange={handleToggleExcludeCitations} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.markdown_export.exclude_citations.help')}</SettingHelpText>
@@ -138,7 +138,7 @@ const MarkdownExportSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.markdown_export.standardize_citations.title')}</SettingRowTitle>
<Switch checked={standardizeCitationsInExport} onCheckedChange={handleToggleStandardizeCitations} />
<Switch isSelected={standardizeCitationsInExport} onValueChange={handleToggleStandardizeCitations} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.markdown_export.standardize_citations.help')}</SettingHelpText>

View File

@@ -128,7 +128,7 @@ const NotionSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.notion.export_reasoning.title')}</SettingRowTitle>
<Switch checked={notionExportReasoning} onCheckedChange={handleNotionExportReasoningChange} />
<Switch isSelected={notionExportReasoning} onValueChange={handleNotionExportReasoningChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.notion.export_reasoning.help')}</SettingHelpText>

View File

@@ -319,7 +319,7 @@ const NutstoreSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={nutstoreSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
<Switch isSelected={nutstoreSkipBackupFile} onValueChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>

View File

@@ -243,7 +243,7 @@ const S3Settings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.s3.skipBackupFile.label')}</SettingRowTitle>
<Switch checked={s3SkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
<Switch isSelected={s3SkipBackupFile} onValueChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.s3.skipBackupFile.help')}</SettingHelpText>

View File

@@ -201,7 +201,7 @@ const WebDavSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={webdavSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
<Switch isSelected={webdavSkipBackupFile} onValueChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
@@ -209,7 +209,7 @@ const WebDavSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.disableStream.title')}</SettingRowTitle>
<Switch checked={webdavDisableStream} onCheckedChange={onDisableStreamChange} />
<Switch isSelected={webdavDisableStream} onValueChange={onDisableStreamChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.webdav.disableStream.help')}</SettingHelpText>

View File

@@ -231,7 +231,7 @@ const DisplaySettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.theme.window.style.transparent')}</SettingRowTitle>
<Switch checked={windowStyle === 'transparent'} onCheckedChange={handleWindowStyleChange} />
<Switch isSelected={windowStyle === 'transparent'} onValueChange={handleWindowStyleChange} />
</SettingRow>
</>
)}
@@ -355,8 +355,8 @@ const DisplaySettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.advanced.auto_switch_to_topics')}</SettingRowTitle>
<Switch
checked={clickAssistantToShowTopic}
onCheckedChange={(checked) => setClickAssistantToShowTopic(checked)}
isSelected={clickAssistantToShowTopic}
onValueChange={(checked) => setClickAssistantToShowTopic(checked)}
/>
</SettingRow>
<SettingDivider />
@@ -364,12 +364,12 @@ const DisplaySettings: FC = () => {
)}
<SettingRow>
<SettingRowTitle>{t('settings.topic.show.time')}</SettingRowTitle>
<Switch checked={showTopicTime} onCheckedChange={(checked) => setShowTopicTime(checked)} />
<Switch isSelected={showTopicTime} onValueChange={(checked) => setShowTopicTime(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.topic.pin_to_top')}</SettingRowTitle>
<Switch checked={pinTopicsToTop} onCheckedChange={(checked) => setPinTopicsToTop(checked)} />
<Switch isSelected={pinTopicsToTop} onValueChange={(checked) => setPinTopicsToTop(checked)} />
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>

View File

@@ -268,12 +268,12 @@ const GeneralSettings: FC = () => {
/>
)}
</RowFlex>
<Switch checked={enableSpellCheck} onCheckedChange={handleSpellCheckChange} />
<Switch isSelected={enableSpellCheck} onValueChange={handleSpellCheckChange} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.hardware_acceleration.title')}</SettingRowTitle>
<Switch checked={disableHardwareAcceleration} onCheckedChange={handleHardwareAccelerationChange} />
<Switch isSelected={disableHardwareAcceleration} onValueChange={handleHardwareAccelerationChange} />
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>
@@ -289,24 +289,24 @@ const GeneralSettings: FC = () => {
/>
</SettingRowTitle>
<Switch
checked={notificationSettings.assistant}
onCheckedChange={(v) => handleNotificationChange('assistant', v)}
isSelected={notificationSettings.assistant}
onValueChange={(v) => handleNotificationChange('assistant', v)}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.notification.backup')}</SettingRowTitle>
<Switch
checked={notificationSettings.backup}
onCheckedChange={(v) => handleNotificationChange('backup', v)}
isSelected={notificationSettings.backup}
onValueChange={(v) => handleNotificationChange('backup', v)}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.notification.knowledge_embed')}</SettingRowTitle>
<Switch
checked={notificationSettings.knowledge}
onCheckedChange={(v) => handleNotificationChange('knowledge', v)}
isSelected={notificationSettings.knowledge}
onValueChange={(v) => handleNotificationChange('knowledge', v)}
/>
</SettingRow>
</SettingGroup>
@@ -315,12 +315,12 @@ const GeneralSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.launch.onboot')}</SettingRowTitle>
<Switch checked={launchOnBoot} onCheckedChange={(checked) => updateLaunchOnBoot(checked)} />
<Switch isSelected={launchOnBoot} onValueChange={(checked) => updateLaunchOnBoot(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.launch.totray')}</SettingRowTitle>
<Switch checked={launchToTray} onCheckedChange={(checked) => updateLaunchToTray(checked)} />
<Switch isSelected={launchToTray} onValueChange={(checked) => updateLaunchToTray(checked)} />
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>
@@ -328,12 +328,12 @@ const GeneralSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tray.show')}</SettingRowTitle>
<Switch checked={tray} onCheckedChange={(checked) => updateTray(checked)} />
<Switch isSelected={tray} onValueChange={(checked) => updateTray(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tray.onclose')}</SettingRowTitle>
<Switch checked={trayOnClose} onCheckedChange={(checked) => updateTrayOnClose(checked)} />
<Switch isSelected={trayOnClose} onValueChange={(checked) => updateTrayOnClose(checked)} />
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>
@@ -342,8 +342,8 @@ const GeneralSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.privacy.enable_privacy_mode')}</SettingRowTitle>
<Switch
checked={enableDataCollection}
onCheckedChange={(v) => {
isSelected={enableDataCollection}
onValueChange={(v) => {
setEnableDataCollection(v)
window.api.config.set('enableDataCollection', v)
}}
@@ -358,7 +358,7 @@ const GeneralSettings: FC = () => {
<SettingRowTitle>{t('settings.developer.enable_developer_mode')}</SettingRowTitle>
<InfoTooltip content={t('settings.developer.help')} />
</Flex>
<Switch checked={enableDeveloperMode} onCheckedChange={setEnableDeveloperMode} />
<Switch isSelected={enableDeveloperMode} onValueChange={setEnableDeveloperMode} />
</SettingRow>
</SettingGroup>
</SettingContainer>

View File

@@ -116,10 +116,11 @@ const McpServerCard: FC<McpServerCardProps> = ({
</ServerNameWrapper>
<ToolbarWrapper onClick={(e) => e.stopPropagation()}>
<Switch
checked={server.isActive}
isSelected={server.isActive}
key={server.id}
disabled={isLoading}
onCheckedChange={onToggle}
onValueChange={onToggle}
size="sm"
data-no-dnd
/>
<Button size="sm" variant="destructive" className="rounded-full" onClick={onDelete}>

View File

@@ -653,7 +653,7 @@ const McpSettings: React.FC = () => {
tooltip={t('settings.mcp.longRunningTooltip')}
layout="horizontal"
valuePropName="checked">
<Switch className="ml-2.5" />
<Switch size="sm" className="ml-2.5" />
</Form.Item>
<Form.Item
name="timeout"
@@ -758,10 +758,10 @@ const McpSettings: React.FC = () => {
</Flex>
<Flex className="items-center gap-4">
<Switch
checked={server.isActive}
isSelected={server.isActive}
key={server.id}
loading={loadingServer === server.id}
onCheckedChange={onToggleActive}
isLoading={loadingServer === server.id}
onValueChange={onToggleActive}
/>
<Button
size="sm"

View File

@@ -146,7 +146,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
width: 150, // Fixed width might be good for alignment
align: 'center',
render: (_, tool) => (
<Switch checked={isToolEnabled(tool)} onCheckedChange={(checked) => handleToggle(tool, checked)} />
<Switch isSelected={isToolEnabled(tool)} onValueChange={(checked) => handleToggle(tool, checked)} size="sm" />
)
},
{
@@ -169,9 +169,10 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
: t('settings.mcp.tools.autoApprove.tooltip.disabled')
}>
<Switch
checked={isToolAutoApproved(tool, server)}
isSelected={isToolAutoApproved(tool, server)}
disabled={!isToolEnabled(tool)}
onCheckedChange={(checked) => handleAutoApproveToggle(tool, checked)}
onValueChange={(checked) => handleAutoApproveToggle(tool, checked)}
size="sm"
/>
</Tooltip>
)

View File

@@ -585,7 +585,7 @@ const MemorySettings = () => {
<TextBadge text="Beta" />
</RowFlex>
<RowFlex className="items-center gap-2.5">
<Switch checked={globalMemoryEnabled} onCheckedChange={handleGlobalMemoryToggle} />
<Switch isSelected={globalMemoryEnabled} onValueChange={handleGlobalMemoryToggle} />
<Button variant="ghost" onClick={() => setSettingsModalVisible(true)} size="icon">
<Settings2 size={16} />
</Button>

View File

@@ -173,8 +173,8 @@ const AssistantSettings: FC = () => {
</RowFlex>
<Switch
style={{ marginLeft: 10 }}
checked={enableTemperature}
onCheckedChange={(enabled) => {
isSelected={enableTemperature}
onValueChange={(enabled) => {
setEnableTemperature(enabled)
onUpdateAssistantSettings({ enableTemperature: enabled })
}}
@@ -215,8 +215,8 @@ const AssistantSettings: FC = () => {
</RowFlex>
<Switch
style={{ marginLeft: 10 }}
checked={enableTopP}
onCheckedChange={(enabled) => {
isSelected={enableTopP}
onValueChange={(enabled) => {
setEnableTopP(enabled)
onUpdateAssistantSettings({ enableTopP: enabled })
}}
@@ -280,8 +280,8 @@ const AssistantSettings: FC = () => {
</RowFlex>
<Switch
style={{ marginLeft: 10 }}
checked={enableMaxTokens}
onCheckedChange={async (enabled) => {
isSelected={enableMaxTokens}
onValueChange={async (enabled) => {
if (enabled) {
const confirmed = await modalConfirm({
title: t('chat.settings.max_tokens.confirm'),

View File

@@ -59,7 +59,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
<ColFlex className="items-stretch gap-2">
<RowFlex className="items-center gap-4">
<div>{t('settings.models.topic_naming.auto')}</div>
<Switch checked={enableTopicNaming} onCheckedChange={setEnableTopicNaming} />
<Switch isSelected={enableTopicNaming} onValueChange={setEnableTopicNaming} />
</RowFlex>
<Divider style={{ margin: 0 }} />
<div>

View File

@@ -164,8 +164,8 @@ const NotesSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('notes.settings.display.compress_content')}</SettingRowTitle>
<Switch
checked={!settings.isFullWidth}
onCheckedChange={(checked) => updateSettings({ isFullWidth: !checked })}
isSelected={!settings.isFullWidth}
onValueChange={(checked) => updateSettings({ isFullWidth: !checked })}
/>
</SettingRow>
<SettingHelpText>{t('notes.settings.display.compress_content_description')}</SettingHelpText>
@@ -188,8 +188,8 @@ const NotesSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('notes.settings.display.show_table_of_contents')}</SettingRowTitle>
<Switch
checked={settings.showTableOfContents}
onCheckedChange={(checked) => updateSettings({ showTableOfContents: checked })}
isSelected={settings.showTableOfContents}
onValueChange={(checked) => updateSettings({ showTableOfContents: checked })}
/>
</SettingRow>
<SettingHelpText>{t('notes.settings.display.show_table_of_contents_description')}</SettingHelpText>

View File

@@ -124,7 +124,7 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
</label>
<InfoTooltip content={item.tip}></InfoTooltip>
</RowFlex>
<Switch id={item.key} checked={item.checked} onCheckedChange={item.onChange} />
<Switch id={item.key} isSelected={item.checked} onValueChange={item.onChange} />
</RowFlex>
))}
</ColFlex>

View File

@@ -343,9 +343,10 @@ const ModelEditContent: FC<ModelEditContentProps & ModalProps> = ({ provider, mo
label={t('settings.models.add.supported_text_delta.label')}
tooltip={t('settings.models.add.supported_text_delta.tooltip')}>
<Switch
checked={supportedTextDelta}
isSelected={supportedTextDelta}
className="ml-auto"
onCheckedChange={(checked) => {
size="sm"
onValueChange={(checked) => {
setSupportedTextDelta(checked)
// 直接传递新值给autoSave
autoSave({ supported_text_delta: checked })

View File

@@ -1,6 +1,4 @@
import { Button, Flex, RowFlex, Switch, Tooltip, WarnTooltip } from '@cherrystudio/ui'
import { HelpTooltip } from '@cherrystudio/ui'
import { adaptProvider } from '@renderer/aiCore/provider/providerConfig'
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
import { LoadingIcon } from '@renderer/components/Icons'
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
@@ -21,7 +19,14 @@ import type { SystemProviderId } from '@renderer/types'
import { isSystemProvider, isSystemProviderId, SystemProviderIds } from '@renderer/types'
import type { ApiKeyConnectivity } from '@renderer/types/healthCheck'
import { HealthStatus } from '@renderer/types/healthCheck'
import { formatApiHost, formatApiKeys, getFancyProviderName, validateApiHost } from '@renderer/utils'
import {
formatApiHost,
formatApiKeys,
formatAzureOpenAIApiHost,
formatVertexApiHost,
getFancyProviderName,
validateApiHost
} from '@renderer/utils'
import { formatErrorMessage } from '@renderer/utils/error'
import {
isAIGatewayProvider,
@@ -31,6 +36,7 @@ import {
isNewApiProvider,
isOpenAICompatibleProvider,
isOpenAIProvider,
isSupportAPIVersionProvider,
isVertexProvider
} from '@renderer/utils/provider'
import { Divider, Input, Select, Space } from 'antd'
@@ -275,10 +281,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
}, [configuredApiHost, apiHost])
const hostPreview = () => {
const formattedApiHost = adaptProvider({ provider: { ...provider, apiHost } }).apiHost
if (apiHost.endsWith('#')) {
return apiHost.replace('#', '')
}
if (isOpenAICompatibleProvider(provider)) {
return formattedApiHost + '/chat/completions'
return formatApiHost(apiHost, isSupportAPIVersionProvider(provider)) + '/chat/completions'
}
if (isAzureOpenAIProvider(provider)) {
@@ -286,26 +294,29 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const path = !['preview', 'v1'].includes(apiVersion)
? `/v1/chat/completion?apiVersion=v1`
: `/v1/responses?apiVersion=v1`
return formattedApiHost + path
return formatAzureOpenAIApiHost(apiHost) + path
}
if (isAnthropicProvider(provider)) {
return formattedApiHost + '/messages'
// AI SDK uses the baseURL with /v1, then appends /messages
// formatApiHost adds /v1 automatically if not present
const normalizedHost = formatApiHost(apiHost)
return normalizedHost + '/messages'
}
if (isGeminiProvider(provider)) {
return formattedApiHost + '/models'
return formatApiHost(apiHost, true, 'v1beta') + '/models'
}
if (isOpenAIProvider(provider)) {
return formattedApiHost + '/responses'
return formatApiHost(apiHost) + '/responses'
}
if (isVertexProvider(provider)) {
return formattedApiHost + '/publishers/google'
return formatVertexApiHost(provider) + '/publishers/google'
}
if (isAIGatewayProvider(provider)) {
return formattedApiHost + '/language-model'
return formatApiHost(apiHost) + '/language-model'
}
return formattedApiHost
return formatApiHost(apiHost)
}
// API key 连通性检查状态指示器,目前仅在失败时显示
@@ -399,9 +410,9 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
)}
</Flex>
<Switch
checked={provider.enabled}
isSelected={provider.enabled}
key={provider.id}
onCheckedChange={(enabled) => {
onValueChange={(enabled) => {
updateProvider({ apiHost, enabled })
if (enabled) {
moveProviderToTop(provider.id)
@@ -487,21 +498,16 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
{!isDmxapi && (
<>
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div className="flex items-center gap-1">
<Tooltip title={hostSelectorTooltip} delay={300}>
<div>
<Selector
size={14}
value={activeHostField}
onChange={(value) => setActiveHostField(value as HostField)}
options={hostSelectorOptions}
style={{ paddingLeft: 1, fontWeight: 'bold' }}
placement="bottomLeft"
/>
</div>
</Tooltip>
<HelpTooltip title={t('settings.provider.api.url.tip')}></HelpTooltip>
</div>
<Tooltip title={hostSelectorTooltip} delay={300}>
<Selector
size={14}
value={activeHostField}
onChange={(value) => setActiveHostField(value as HostField)}
options={hostSelectorOptions}
style={{ paddingLeft: 1, fontWeight: 'bold' }}
placement="bottomLeft"
/>
</Tooltip>
<Button variant="ghost" onClick={() => CustomHeaderPopup.show({ provider })} size="icon">
<Settings2 size={16} />
</Button>

View File

@@ -82,14 +82,14 @@ const QuickAssistantSettings: FC = () => {
iconProps={{ className: 'cursor-pointer' }}
/>
</SettingRowTitle>
<Switch checked={enableQuickAssistant} onCheckedChange={handleEnableQuickAssistant} />
<Switch isSelected={enableQuickAssistant} onValueChange={handleEnableQuickAssistant} />
</SettingRow>
{enableQuickAssistant && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.quickAssistant.click_tray_to_show')}</SettingRowTitle>
<Switch checked={clickTrayToShowQuickAssistant} onCheckedChange={handleClickTrayToShowQuickAssistant} />
<Switch isSelected={clickTrayToShowQuickAssistant} onValueChange={handleClickTrayToShowQuickAssistant} />
</SettingRow>
</>
)}
@@ -98,7 +98,7 @@ const QuickAssistantSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.quickAssistant.read_clipboard_at_startup')}</SettingRowTitle>
<Switch checked={readClipboardAtStartup} onCheckedChange={handleClickReadClipboardAtStartup} />
<Switch isSelected={readClipboardAtStartup} onValueChange={handleClickReadClipboardAtStartup} />
</SettingRow>
</>
)}

View File

@@ -101,8 +101,8 @@ const SelectionAssistantSettings: FC = () => {
{!isSupportedOS && <SettingDescription>{t('selection.settings.enable.description')}</SettingDescription>}
</SettingLabel>
<Switch
checked={isSupportedOS && selectionEnabled}
onCheckedChange={handleEnableCheckboxChange}
isSelected={isSupportedOS && selectionEnabled}
onValueChange={handleEnableCheckboxChange}
disabled={!isSupportedOS}
/>
</SettingRow>
@@ -162,7 +162,7 @@ const SelectionAssistantSettings: FC = () => {
<SettingRowTitle>{t('selection.settings.toolbar.compact_mode.title')}</SettingRowTitle>
<SettingDescription>{t('selection.settings.toolbar.compact_mode.description')}</SettingDescription>
</SettingLabel>
<Switch checked={isCompact} onCheckedChange={setIsCompact} />
<Switch isSelected={isCompact} onValueChange={setIsCompact} />
</SettingRow>
</SettingGroup>
@@ -174,7 +174,7 @@ const SelectionAssistantSettings: FC = () => {
<SettingRowTitle>{t('selection.settings.window.follow_toolbar.title')}</SettingRowTitle>
<SettingDescription>{t('selection.settings.window.follow_toolbar.description')}</SettingDescription>
</SettingLabel>
<Switch checked={isFollowToolbar} onCheckedChange={setIsFollowToolbar} />
<Switch isSelected={isFollowToolbar} onValueChange={setIsFollowToolbar} />
</SettingRow>
<SettingDivider />
<SettingRow>
@@ -182,7 +182,7 @@ const SelectionAssistantSettings: FC = () => {
<SettingRowTitle>{t('selection.settings.window.remember_size.title')}</SettingRowTitle>
<SettingDescription>{t('selection.settings.window.remember_size.description')}</SettingDescription>
</SettingLabel>
<Switch checked={isRemeberWinSize} onCheckedChange={setIsRemeberWinSize} />
<Switch isSelected={isRemeberWinSize} onValueChange={setIsRemeberWinSize} />
</SettingRow>
<SettingDivider />
<SettingRow>
@@ -190,7 +190,7 @@ const SelectionAssistantSettings: FC = () => {
<SettingRowTitle>{t('selection.settings.window.auto_close.title')}</SettingRowTitle>
<SettingDescription>{t('selection.settings.window.auto_close.description')}</SettingDescription>
</SettingLabel>
<Switch checked={isAutoClose} onCheckedChange={setIsAutoClose} />
<Switch isSelected={isAutoClose} onValueChange={setIsAutoClose} />
</SettingRow>
<SettingDivider />
<SettingRow>
@@ -198,7 +198,7 @@ const SelectionAssistantSettings: FC = () => {
<SettingRowTitle>{t('selection.settings.window.auto_pin.title')}</SettingRowTitle>
<SettingDescription>{t('selection.settings.window.auto_pin.description')}</SettingDescription>
</SettingLabel>
<Switch checked={isAutoPin} onCheckedChange={setIsAutoPin} />
<Switch isSelected={isAutoPin} onValueChange={setIsAutoPin} />
</SettingRow>
<SettingDivider />
<SettingRow>

View File

@@ -392,7 +392,7 @@ const ShortcutSettings: FC = () => {
align: 'right',
width: '50px',
render: (record: Shortcut) => (
<Switch checked={record.enabled} onCheckedChange={() => dispatch(toggleShortcut(record.key))} />
<Switch size="sm" isSelected={record.enabled} onValueChange={() => dispatch(toggleShortcut(record.key))} />
)
}
]

View File

@@ -23,7 +23,7 @@ const BasicSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tool.websearch.search_with_time')}</SettingRowTitle>
<Switch checked={searchWithTime} onCheckedChange={(checked) => dispatch(setSearchWithTime(checked))} />
<Switch isSelected={searchWithTime} onValueChange={(checked) => dispatch(setSearchWithTime(checked))} />
</SettingRow>
<SettingDivider style={{ marginTop: 15, marginBottom: 10 }} />
<SettingRow style={{ height: 40 }}>

View File

@@ -67,8 +67,8 @@ const TranslateSettings: FC<{
<Flex className="items-center justify-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.preview')}</div>
<Switch
checked={enableMarkdown}
onCheckedChange={(checked) => {
isSelected={enableMarkdown}
onValueChange={(checked) => {
setEnableMarkdown(checked)
db.settings.put({ id: 'translate:markdown:enabled', value: checked })
}}
@@ -80,9 +80,9 @@ const TranslateSettings: FC<{
<RowFlex className="items-center justify-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.autoCopy')}</div>
<Switch
checked={autoCopy}
isSelected={autoCopy}
color="primary"
onCheckedChange={(isSelected) => {
onValueChange={(isSelected) => {
updateSettings({ autoCopy: isSelected })
}}
/>
@@ -93,9 +93,9 @@ const TranslateSettings: FC<{
<Flex className="items-center justify-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.scroll_sync')}</div>
<Switch
checked={isScrollSyncEnabled}
isSelected={isScrollSyncEnabled}
color="primary"
onCheckedChange={(isSelected) => {
onValueChange={(isSelected) => {
setIsScrollSyncEnabled(isSelected)
db.settings.put({ id: 'translate:scroll:sync', value: isSelected })
}}
@@ -145,9 +145,9 @@ const TranslateSettings: FC<{
</RowFlex>
</div>
<Switch
checked={isBidirectional}
isSelected={isBidirectional}
color="primary"
onCheckedChange={(isSelected) => {
onValueChange={(isSelected) => {
setIsBidirectional(isSelected)
// 双向翻译设置不需要持久化,它只是界面状态
}}

View File

@@ -13,8 +13,7 @@ import {
routeToEndpoint,
splitApiKeyString,
validateApiHost,
withoutTrailingApiVersion,
withoutTrailingSharp
withoutTrailingApiVersion
} from '../api'
vi.mock('@renderer/store', () => {
@@ -82,27 +81,6 @@ describe('api', () => {
it('keeps host untouched when api version unsupported', () => {
expect(formatApiHost('https://api.example.com', false)).toBe('https://api.example.com')
})
it('removes trailing # and does not append api version when host ends with #', () => {
expect(formatApiHost('https://api.example.com#')).toBe('https://api.example.com')
expect(formatApiHost('http://localhost:5173/#')).toBe('http://localhost:5173/')
expect(formatApiHost(' https://api.openai.com/# ')).toBe('https://api.openai.com/')
})
it('handles trailing # with custom api version settings', () => {
expect(formatApiHost('https://api.example.com#', true, 'v2')).toBe('https://api.example.com')
expect(formatApiHost('https://api.example.com#', false, 'v2')).toBe('https://api.example.com')
})
it('handles host with both trailing # and existing api version', () => {
expect(formatApiHost('https://api.example.com/v2#')).toBe('https://api.example.com/v2')
expect(formatApiHost('https://api.example.com/v3beta#')).toBe('https://api.example.com/v3beta')
})
it('trims whitespace before processing trailing #', () => {
expect(formatApiHost(' https://api.example.com# ')).toBe('https://api.example.com')
expect(formatApiHost('\thttps://api.example.com#\n')).toBe('https://api.example.com')
})
})
describe('hasAPIVersion', () => {
@@ -426,56 +404,4 @@ describe('api', () => {
expect(withoutTrailingApiVersion('')).toBe('')
})
})
describe('withoutTrailingSharp', () => {
it('removes trailing # from URL', () => {
expect(withoutTrailingSharp('https://api.example.com#')).toBe('https://api.example.com')
expect(withoutTrailingSharp('http://localhost:3000#')).toBe('http://localhost:3000')
})
it('returns URL unchanged when no trailing #', () => {
expect(withoutTrailingSharp('https://api.example.com')).toBe('https://api.example.com')
expect(withoutTrailingSharp('http://localhost:3000')).toBe('http://localhost:3000')
})
it('handles URLs with multiple # characters but only removes trailing one', () => {
expect(withoutTrailingSharp('https://api.example.com#path#')).toBe('https://api.example.com#path')
})
it('handles URLs with # in the middle (not trailing)', () => {
expect(withoutTrailingSharp('https://api.example.com#section/path')).toBe('https://api.example.com#section/path')
expect(withoutTrailingSharp('https://api.example.com/v1/chat/completions#')).toBe(
'https://api.example.com/v1/chat/completions'
)
})
it('handles empty string', () => {
expect(withoutTrailingSharp('')).toBe('')
})
it('handles single character #', () => {
expect(withoutTrailingSharp('#')).toBe('')
})
it('preserves whitespace around the URL (pure function)', () => {
expect(withoutTrailingSharp(' https://api.example.com# ')).toBe(' https://api.example.com# ')
expect(withoutTrailingSharp('\thttps://api.example.com#\n')).toBe('\thttps://api.example.com#\n')
})
it('only removes exact trailing # character', () => {
expect(withoutTrailingSharp('https://api.example.com# ')).toBe('https://api.example.com# ')
expect(withoutTrailingSharp(' https://api.example.com#')).toBe(' https://api.example.com')
expect(withoutTrailingSharp('https://api.example.com#\t')).toBe('https://api.example.com#\t')
})
it('handles URLs ending with multiple # characters', () => {
expect(withoutTrailingSharp('https://api.example.com##')).toBe('https://api.example.com#')
expect(withoutTrailingSharp('https://api.example.com###')).toBe('https://api.example.com##')
})
it('preserves URL with trailing # and other content', () => {
expect(withoutTrailingSharp('https://api.example.com/v1#')).toBe('https://api.example.com/v1')
expect(withoutTrailingSharp('https://api.example.com/v2beta#')).toBe('https://api.example.com/v2beta')
})
})
})

View File

@@ -62,23 +62,6 @@ export function withoutTrailingSlash<T extends string>(url: T): T {
return url.replace(/\/$/, '') as T
}
/**
* Removes the trailing '#' from a URL string if it exists.
*
* @template T - The string type to preserve type safety
* @param {T} url - The URL string to process
* @returns {T} The URL string without a trailing '#'
*
* @example
* ```ts
* withoutTrailingSharp('https://example.com#') // 'https://example.com'
* withoutTrailingSharp('https://example.com') // 'https://example.com'
* ```
*/
export function withoutTrailingSharp<T extends string>(url: T): T {
return url.replace(/#$/, '') as T
}
/**
* Formats an API host URL by normalizing it and optionally appending an API version.
*
@@ -87,12 +70,12 @@ export function withoutTrailingSharp<T extends string>(url: T): T {
* @param apiVersion - The API version to append if needed. Defaults to `'v1'`.
*
* @returns The formatted API host URL. If the host is empty after normalization, returns an empty string.
* If the host ends with '#', API version is not supported, or the host already contains a version, returns the normalized host with trailing '#' removed.
* If the host ends with '#', API version is not supported, or the host already contains a version, returns the normalized host as-is.
* Otherwise, returns the host with the API version appended.
*
* @example
* formatApiHost('https://api.example.com/') // Returns 'https://api.example.com/v1'
* formatApiHost('https://api.example.com#') // Returns 'https://api.example.com'
* formatApiHost('https://api.example.com#') // Returns 'https://api.example.com#'
* formatApiHost('https://api.example.com/v2', true, 'v1') // Returns 'https://api.example.com/v2'
*/
export function formatApiHost(host?: string, supportApiVersion: boolean = true, apiVersion: string = 'v1'): string {
@@ -101,13 +84,10 @@ export function formatApiHost(host?: string, supportApiVersion: boolean = true,
return ''
}
const shouldAppendApiVersion = !(normalizedHost.endsWith('#') || !supportApiVersion || hasAPIVersion(normalizedHost))
if (shouldAppendApiVersion) {
return `${normalizedHost}/${apiVersion}`
} else {
return withoutTrailingSharp(normalizedHost)
if (normalizedHost.endsWith('#') || !supportApiVersion || hasAPIVersion(normalizedHost)) {
return normalizedHost
}
return `${normalizedHost}/${apiVersion}`
}
/**

View File

@@ -231,7 +231,7 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@npm:1.0.27":
"@ai-sdk/openai-compatible@npm:1.0.27, @ai-sdk/openai-compatible@npm:^1.0.19":
version: 1.0.27
resolution: "@ai-sdk/openai-compatible@npm:1.0.27"
dependencies:
@@ -243,18 +243,6 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@npm:^1.0.19":
version: 1.0.19
resolution: "@ai-sdk/openai-compatible@npm:1.0.19"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/5b7b21fb515e829c3d8a499a5760ffc035d9b8220695996110e361bd79e9928859da4ecf1ea072735bcbe4977c6dd0661f543871921692e86f8b5bfef14fe0e5
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch":
version: 1.0.27
resolution: "@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch::version=1.0.27&hash=c44b76"
@@ -303,19 +291,6 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.10":
version: 3.0.10
resolution: "@ai-sdk/provider-utils@npm:3.0.10"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/d2c16abdb84ba4ef48c9f56190b5ffde224b9e6ae5147c5c713d2623627732d34b96aa9aef2a2ea4b0c49e1b863cc963c7d7ff964a1dc95f0f036097aaaaaa98
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.17, @ai-sdk/provider-utils@npm:^3.0.10, @ai-sdk/provider-utils@npm:^3.0.17":
version: 3.0.17
resolution: "@ai-sdk/provider-utils@npm:3.0.17"
@@ -7755,31 +7730,6 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-switch@npm:^1.2.6":
version: 1.2.6
resolution: "@radix-ui/react-switch@npm:1.2.6"
dependencies:
"@radix-ui/primitive": "npm:1.1.3"
"@radix-ui/react-compose-refs": "npm:1.1.2"
"@radix-ui/react-context": "npm:1.1.2"
"@radix-ui/react-primitive": "npm:2.1.3"
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
"@radix-ui/react-use-previous": "npm:1.1.1"
"@radix-ui/react-use-size": "npm:1.1.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10c0/888303cbeb0e69ebba5676b225f9ea0f00f61453c6b8a6b66384b5c5c4c7fb0ccc53493c1eb14ec6d436e5b867b302aadd6af51a1f2e6c04581c583fd9be65be
languageName: node
linkType: hard
"@radix-ui/react-tabs@npm:^1.1.13":
version: 1.1.13
resolution: "@radix-ui/react-tabs@npm:1.1.13"
@@ -13872,7 +13822,6 @@ __metadata:
"@paymoapp/electron-shutdown-handler": "npm:^1.1.2"
"@playwright/test": "npm:^1.55.1"
"@radix-ui/react-context-menu": "npm:^2.2.16"
"@radix-ui/react-switch": "npm:^1.2.6"
"@reduxjs/toolkit": "npm:^2.2.5"
"@shikijs/markdown-it": "npm:^3.12.0"
"@strongtz/win32-arm64-msvc": "npm:^0.4.7"
@@ -18570,7 +18519,7 @@ __metadata:
languageName: node
linkType: hard
"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.5":
"eventsource-parser@npm:^3.0.0":
version: 3.0.5
resolution: "eventsource-parser@npm:3.0.5"
checksum: 10c0/5cb75e3f84ff1cfa1cee6199d4fd430c4544855ab03e953ddbe5927e7b31bc2af3933ab8aba6440ba160ed2c48972b6c317f27b8a1d0764c7b12e34e249de631