Compare commits
2 Commits
v2
...
fix/v2/inp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
185eff16b6 | ||
|
|
13abc2d653 |
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { CompositeInputProps, SelectGroup, SelectItem } from './input'
|
||||
import { CompositeInput } from './input'
|
||||
|
||||
export { CompositeInput, type CompositeInputProps, type SelectGroup, type SelectItem }
|
||||
@@ -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 }
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4372,7 +4372,7 @@
|
||||
"url": {
|
||||
"preview": "预览: {{url}}",
|
||||
"reset": "重置",
|
||||
"tip": "在末尾添加 # 以禁用自动附加的API版本。"
|
||||
"tip": "# 结尾强制使用输入地址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 地址",
|
||||
|
||||
@@ -4372,7 +4372,7 @@
|
||||
"url": {
|
||||
"preview": "預覽:{{url}}",
|
||||
"reset": "重設",
|
||||
"tip": "在末尾添加 # 以停用自動附加的 API 版本。"
|
||||
"tip": "# 結尾強制使用輸入位址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 主機地址",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4372,7 +4372,7 @@
|
||||
"url": {
|
||||
"preview": "Προεπισκόπηση: {{url}}",
|
||||
"reset": "Επαναφορά",
|
||||
"tip": "Προσθέστε το σύμβολο # στο τέλος για να απενεργοποιήσετε την αυτόματα προστιθέμενη έκδοση API."
|
||||
"tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
|
||||
}
|
||||
},
|
||||
"api_host": "Διεύθυνση API",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4372,7 +4372,7 @@
|
||||
"url": {
|
||||
"preview": "プレビュー: {{url}}",
|
||||
"reset": "リセット",
|
||||
"tip": "自動的に付加されるAPIバージョンを無効にするには、末尾に#を追加します。"
|
||||
"tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
|
||||
}
|
||||
},
|
||||
"api_host": "APIホスト",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4372,7 +4372,7 @@
|
||||
"url": {
|
||||
"preview": "Предпросмотр: {{url}}",
|
||||
"reset": "Сброс",
|
||||
"tip": "Добавьте # в конце, чтобы отключить автоматически добавляемую версию API."
|
||||
"tip": "заканчивая на # принудительно использует введенный адрес"
|
||||
}
|
||||
},
|
||||
"api_host": "Хост API",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))} />
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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)
|
||||
// 双向翻译设置不需要持久化,它只是界面状态
|
||||
}}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
55
yarn.lock
55
yarn.lock
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user