Compare commits

...

3 Commits

Author SHA1 Message Date
yyh
3bee546e45 refactor(web): migrate shared icon commands
Replace ordinary ActionButton usage across shared, app, and chat surfaces with the Dify UI IconButton. Preserve legacy sizes and interaction colors, move popup styling to Base UI state attributes, and require accessible names.
2026-08-13 02:54:32 +08:00
yyh
db20f64ac4 refactor(ui): adopt icon button in close controls
Compose Dialog, Drawer, and Toast close behavior through the shared IconButton while preserving each owner's layout and visual states. Document compound-control boundaries.
2026-08-13 00:57:57 +08:00
yyh
87156c1037 feat(ui): add icon button primitive
Add a Base UI-backed icon-only command with an accessible-name contract, internal Button recipe reuse, focused behavior tests, and Storybook documentation.
2026-08-13 00:57:53 +08:00
52 changed files with 695 additions and 303 deletions

View File

@@ -424,11 +424,6 @@
"count": 1
}
},
"web/app/components/app/configuration/debug/__tests__/index.spec.tsx": {
"typescript/no-explicit-any": {
"count": 1
}
},
"web/app/components/app/configuration/debug/chat-user-input.tsx": {
"jsx_a11y/no-autofocus": {
"count": 2

View File

@@ -32,6 +32,7 @@ import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog
import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer'
import { Field, FieldControl, FieldLabel } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
@@ -63,7 +64,7 @@ Keep implementation-only render helpers, context values, styling helpers, and up
| Category | Subpath | Notes |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Actions | `./button`, `./icon-button` | Visible-label actions and icon-only commands. |
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
@@ -90,6 +91,26 @@ documented layout exception.
When `loading` is true, `Button` defaults `focusableWhenDisabled` to true. Loading represents an action that has already been triggered and is temporarily pending, so the button remains focusable while Base UI still suppresses click, pointer, keyboard activation, and submit-button activation. Pass `focusableWhenDisabled={false}` only when a loading button should use native disabled behavior.
## Icon button contract
Use `IconButton` for a command represented by one icon and no visible text. Use `Button` when the control has a visible label, including buttons with leading or trailing icons.
Pass exactly one React element as its child. React SVG components and CSS icons are both supported without an icon-specific prop or wrapper:
```tsx
<IconButton aria-label="Close">
<span aria-hidden="true" className="i-ri-close-line size-4" />
</IconButton>
```
Every icon button must have an `aria-label` or `aria-labelledby`; a tooltip is only a visual enhancement. The child chooses the glyph and its optical size. Omit `variant` for the neutral action-button appearance, or use the same appearance variants as `Button`. Use `tone="destructive"` for destructive intent. Size, radius, colors, hover, disabled, and focus-visible styles belong to `IconButton`; limit `className` to external layout.
`IconButton` preserves Base UI Button's `render`, `nativeButton`, event, and ref composition.
Do not layer `IconButton` over compound controls that already own button behavior and a specialized recipe, such as Number Field increment/decrement, Combobox clear, or Pagination navigation. Those controls keep their Base UI owner and domain-specific styling.
Dify-authored icon-only compound parts, such as dialog, drawer, and toast close controls, also keep their own Base UI semantic part. They reuse the private icon-button visual recipe directly instead of rendering the public `IconButton` component, so dependency direction and DOM ownership stay unchanged.
## Segmented control contract
`SegmentedControl` is Dify's design-system primitive for mode, filter, and view selection. It is built on Base UI `ToggleGroup` + `Toggle`, so use `Tabs` instead when the UI needs `tablist` / `tabpanel` semantics.

View File

@@ -25,6 +25,10 @@
"types": "./src/button/index.tsx",
"import": "./src/button/index.tsx"
},
"./icon-button": {
"types": "./src/icon-button/index.tsx",
"import": "./src/icon-button/index.tsx"
},
"./checkbox": {
"types": "./src/checkbox/index.tsx",
"import": "./src/checkbox/index.tsx"

View File

@@ -3,6 +3,7 @@
import type * as React from 'react'
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import { cn } from '../cn'
import { iconButtonVariants } from '../icon-button/variants'
import { modalBackdropClassName, modalPopupAnimationClassName } from '../overlay-shared'
const Dialog = BaseDialog.Root
@@ -66,11 +67,12 @@ function DialogCloseButton({
aria-label={ariaLabel}
{...props}
className={cn(
'absolute inset-e-6 top-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
iconButtonVariants({ size: 'sm' }),
'absolute inset-e-6 top-6 z-10 rounded-2xl disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
>
<span aria-hidden="true" className="i-ri-close-line h-4 w-4 text-text-tertiary" />
<span aria-hidden="true" className="i-ri-close-line size-4 text-text-tertiary" />
</BaseDialog.Close>
)
}

View File

@@ -3,6 +3,7 @@
import type * as React from 'react'
import { Drawer as BaseDrawer } from '@base-ui/react/drawer'
import { cn } from '../cn'
import { iconButtonVariants } from '../icon-button/variants'
const Drawer = BaseDrawer.Root
const DrawerProvider = BaseDrawer.Provider
@@ -119,12 +120,13 @@ function DrawerCloseButton({
type={type}
aria-label={ariaLabel}
className={cn(
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50',
iconButtonVariants({ size: 'lg' }),
'focus-visible:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50 data-disabled:text-text-tertiary',
className,
)}
{...props}
>
{children ?? <span aria-hidden="true" className="i-ri-close-line h-4 w-4" />}
{children ?? <span aria-hidden="true" className="i-ri-close-line size-4" />}
</BaseDrawer.Close>
)
}

View File

@@ -0,0 +1,73 @@
import { userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { IconButton } from '../index'
describe('IconButton', () => {
it('renders a named native button by default', async () => {
const screen = await render(
<IconButton aria-label="Close">
<span aria-hidden="true" className="i-ri-close-line size-4" />
</IconButton>,
)
await expect
.element(screen.getByRole('button', { name: 'Close' }))
.toHaveAttribute('type', 'button')
})
it('preserves Base UI render prop composition', async () => {
const onClick = vi.fn()
const onRenderedClick = vi.fn()
let renderedRef: HTMLButtonElement | null = null
let iconButtonRef: HTMLElement | null = null
const screen = await render(
<IconButton
aria-label="More actions"
onClick={onClick}
ref={(element) => {
iconButtonRef = element
}}
render={
<button
data-trigger="menu"
onClick={onRenderedClick}
ref={(element) => {
renderedRef = element
}}
/>
}
>
<span aria-hidden="true" className="i-ri-more-line size-4" />
</IconButton>,
)
const button = screen.getByRole('button', { name: 'More actions' })
await expect.element(button).toHaveAttribute('data-trigger', 'menu')
await userEvent.click(button)
expect(onClick).toHaveBeenCalledOnce()
expect(onRenderedClick).toHaveBeenCalledOnce()
expect(iconButtonRef).toBe(button.element())
expect(renderedRef).toBe(button.element())
})
it('preserves Base UI disabled behavior', async () => {
const onClick = vi.fn()
const screen = await render(
<IconButton aria-label="Delete" disabled onClick={onClick}>
<span aria-hidden="true" className="i-ri-delete-bin-line size-4" />
</IconButton>,
)
const button = screen.getByRole('button', { name: 'Delete' })
await expect.element(button).toBeDisabled()
const element = button.element()
if (!(element instanceof HTMLButtonElement))
throw new TypeError('Expected IconButton to render a button element')
element.click()
expect(onClick).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,138 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { IconButton } from '.'
const meta = {
title: 'Base/UI/IconButton',
component: IconButton,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'Icon-only command button built on Base UI Button. Provide one icon element and an accessible name.',
},
},
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: [
'default',
'primary',
'secondary',
'secondary-accent',
'tertiary',
'ghost',
'ghost-accent',
],
},
tone: {
control: 'select',
options: ['default', 'destructive'],
},
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
disabled: { control: 'boolean' },
},
args: {
'aria-label': 'Information',
children: <span aria-hidden="true" className="i-ri-information-2-line size-4" />,
},
} satisfies Meta<typeof IconButton>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const CSSIcon: Story = {
args: {
'aria-label': 'Close',
children: <span aria-hidden="true" className="i-ri-close-line size-4" />,
},
}
export const ReactSVGIcon: Story = {
args: {
'aria-label': 'Add',
children: (
<svg
aria-hidden="true"
className="size-4"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
>
<path d="M8 3v10M3 8h10" strokeLinecap="round" />
</svg>
),
},
}
export const DestructiveIntent: Story = {
args: {
'aria-label': 'Delete',
tone: 'destructive',
children: <span aria-hidden="true" className="i-ri-delete-bin-line size-4" />,
},
parameters: {
docs: {
description: {
story: 'Destructive intent appears on hover while the resting action remains neutral.',
},
},
},
}
export const Disabled: Story = {
args: {
disabled: true,
},
}
export const Sizes: Story = {
render: () => {
const sizes = [
{ button: 'xs', icon: 'size-3.5' },
{ button: 'sm', icon: 'size-4' },
{ button: 'md', icon: 'size-4' },
{ button: 'lg', icon: 'size-4' },
{ button: 'xl', icon: 'size-5' },
] as const
return (
<div className="flex items-center gap-3">
{sizes.map(({ button, icon }) => (
<IconButton key={button} aria-label={`${button} icon button`} size={button}>
<span aria-hidden="true" className={`i-ri-information-2-line ${icon}`} />
</IconButton>
))}
</div>
)
},
}
export const Appearances: Story = {
render: () => (
<div className="flex items-center gap-3">
{(
[
'default',
'primary',
'secondary',
'secondary-accent',
'tertiary',
'ghost',
'ghost-accent',
] as const
).map((variant) => (
<IconButton key={variant} aria-label={`${variant} icon button`} variant={variant}>
<span aria-hidden="true" className="i-ri-information-2-line size-4" />
</IconButton>
))}
</div>
),
}

View File

@@ -0,0 +1,52 @@
'use client'
import type { Button as BaseButtonNS } from '@base-ui/react/button'
import type { VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import { Button as BaseButton } from '@base-ui/react/button'
import { cn } from '../cn'
import { iconButtonVariants } from './variants'
type AccessibleName =
| {
'aria-label': string
'aria-labelledby'?: never
}
| {
'aria-label'?: never
'aria-labelledby': string
}
type IconButtonProps = Omit<
BaseButtonNS.Props,
'aria-label' | 'aria-labelledby' | 'children' | 'className'
> &
AccessibleName &
VariantProps<typeof iconButtonVariants> & {
children: React.ReactElement
className?: string
}
function IconButton({
className,
variant,
tone,
size,
type = 'button',
children,
...props
}: IconButtonProps) {
return (
<BaseButton
type={type}
className={cn(iconButtonVariants({ variant, tone, size }), className)}
{...props}
>
{children}
</BaseButton>
)
}
export { IconButton }
export type { IconButtonProps }

View File

@@ -0,0 +1,111 @@
import { cva } from 'class-variance-authority'
const iconButtonVariants = cva(
[
'inline-flex cursor-pointer items-center justify-center focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-disabled:cursor-not-allowed',
],
{
variants: {
variant: {
default: [
'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
'data-disabled:text-text-disabled data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
],
primary: [
'bg-components-button-primary-bg text-components-button-primary-text shadow-primary-button inset-ring-[0.5px] inset-ring-components-button-primary-border',
'hover:bg-components-button-primary-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3 hover:inset-ring-components-button-primary-border-hover',
'data-disabled:bg-components-button-primary-bg-disabled data-disabled:text-components-button-primary-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-primary-border-disabled',
],
secondary: [
'bg-components-button-secondary-bg text-components-button-secondary-text shadow-xs inset-ring-[0.5px] shadow-shadow-shadow-3 inset-ring-components-button-secondary-border backdrop-blur-[5px]',
'hover:bg-components-button-secondary-bg-hover hover:inset-ring-components-button-secondary-border-hover',
'data-disabled:bg-components-button-secondary-bg-disabled data-disabled:text-components-button-secondary-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-secondary-border-disabled data-disabled:backdrop-blur-xs',
],
'secondary-accent': [
'bg-components-button-secondary-bg text-components-button-secondary-accent-text shadow-xs inset-ring-[0.5px] shadow-shadow-shadow-3 inset-ring-components-button-secondary-border backdrop-blur-[5px]',
'hover:bg-components-button-secondary-bg-hover hover:inset-ring-components-button-secondary-border-hover',
'data-disabled:bg-components-button-secondary-bg-disabled data-disabled:text-components-button-secondary-accent-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-secondary-border-disabled data-disabled:backdrop-blur-xs',
],
tertiary: [
'bg-components-button-tertiary-bg text-components-button-tertiary-text',
'hover:bg-components-button-tertiary-bg-hover',
'data-disabled:bg-components-button-tertiary-bg-disabled data-disabled:text-components-button-tertiary-text-disabled',
],
ghost: [
'text-components-button-ghost-text',
'hover:bg-components-button-ghost-bg-hover',
'data-disabled:text-components-button-ghost-text-disabled',
],
'ghost-accent': [
'text-components-button-secondary-accent-text',
'hover:bg-state-accent-hover',
'data-disabled:text-components-button-secondary-accent-text-disabled',
],
},
tone: {
default: '',
destructive: '',
},
size: {
xs: 'size-4 rounded-sm p-0',
sm: 'size-5 rounded-md',
md: 'size-6 rounded-md p-0.5',
lg: 'size-8 rounded-lg p-1.5',
xl: 'size-9 rounded-lg p-2',
},
},
compoundVariants: [
{
variant: 'default',
tone: 'destructive',
class: [
'text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive',
'data-disabled:text-text-disabled data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
],
},
{
variant: 'primary',
tone: 'destructive',
class: [
'bg-components-button-destructive-primary-bg text-components-button-destructive-primary-text inset-ring-components-button-destructive-primary-border',
'hover:bg-components-button-destructive-primary-bg-hover hover:inset-ring-components-button-destructive-primary-border-hover',
'data-disabled:bg-components-button-destructive-primary-bg-disabled data-disabled:text-components-button-destructive-primary-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-destructive-primary-bg-disabled',
],
},
{
variant: 'secondary',
tone: 'destructive',
class: [
'bg-components-button-destructive-secondary-bg text-components-button-destructive-secondary-text inset-ring-components-button-destructive-secondary-border',
'hover:bg-components-button-destructive-secondary-bg-hover hover:inset-ring-components-button-destructive-secondary-border-hover',
'data-disabled:text-components-button-destructive-secondary-text-disabled',
],
},
{
variant: 'tertiary',
tone: 'destructive',
class: [
'bg-components-button-destructive-tertiary-bg text-components-button-destructive-tertiary-text',
'hover:bg-components-button-destructive-tertiary-bg-hover',
'data-disabled:bg-components-button-destructive-tertiary-bg-disabled data-disabled:text-components-button-destructive-tertiary-text-disabled',
],
},
{
variant: 'ghost',
tone: 'destructive',
class: [
'text-components-button-destructive-ghost-text',
'hover:bg-components-button-destructive-ghost-bg-hover',
'data-disabled:text-components-button-destructive-ghost-text-disabled',
],
},
],
defaultVariants: {
variant: 'default',
tone: 'default',
size: 'md',
},
},
)
export { iconButtonVariants }

View File

@@ -9,6 +9,7 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from '../context-menu'
import { IconButton } from '../icon-button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip'
const meta = {
@@ -169,13 +170,9 @@ export const InTooltip: Story = {
<Tooltip open>
<TooltipTrigger
render={
<button
type="button"
aria-label="Collapse sidebar"
className="inline-flex size-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<IconButton aria-label="Collapse sidebar" size="lg" variant="secondary">
<span aria-hidden className="i-ri-sidebar-fold-line size-4" />
</button>
</IconButton>
}
/>
<TooltipContent className="flex items-center gap-1">

View File

@@ -9,6 +9,7 @@ import type {
import type * as React from 'react'
import { Toast as BaseToast } from '@base-ui/react/toast'
import { cn } from '../cn'
import { iconButtonVariants } from '../icon-button/variants'
type ToastData = Record<string, never>
type ToastToneStyle = {
@@ -234,10 +235,11 @@ function ToastCard({ toast: toastItem }: { toast: ToastObject<ToastData> }) {
<BaseToast.Close
aria-label={toastCloseLabel}
className={cn(
'flex h-5 w-5 items-center justify-center rounded-md hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
iconButtonVariants({ size: 'sm' }),
'focus-visible:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50',
)}
>
<span aria-hidden="true" className="i-ri-close-line h-4 w-4 text-text-tertiary" />
<span aria-hidden="true" className="i-ri-close-line size-4 text-text-tertiary" />
</BaseToast.Close>
</div>
</BaseToast.Content>

View File

@@ -2,9 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import type { TooltipContentProps } from '.'
import * as React from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '.'
import { IconButton as DifyIconButton } from '../icon-button'
const iconButtonClassName =
'inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
const triggerButtonClassName =
'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
@@ -56,9 +55,9 @@ export const IconButton: Story = {
<Tooltip key={label}>
<TooltipTrigger
render={
<button type="button" aria-label={label} className={iconButtonClassName}>
<span aria-hidden className={`${icon} h-4 w-4`} />
</button>
<DifyIconButton aria-label={label} size="lg" variant="secondary">
<span aria-hidden className={`${icon} size-4`} />
</DifyIconButton>
}
/>
<TooltipContent>{label}</TooltipContent>
@@ -130,9 +129,9 @@ const PlacementsDemo = () => {
<Tooltip open>
<TooltipTrigger
render={
<button type="button" aria-label="Placement anchor" className={iconButtonClassName}>
<span aria-hidden className="i-ri-pushpin-line h-4 w-4" />
</button>
<DifyIconButton aria-label="Placement anchor" size="lg" variant="secondary">
<span aria-hidden className="i-ri-pushpin-line size-4" />
</DifyIconButton>
}
/>
<TooltipContent placement={placement}>{`placement="${placement}"`}</TooltipContent>
@@ -167,13 +166,9 @@ const DelayDemo = () => (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label={`${label} (${delay}ms)`}
className={iconButtonClassName}
>
<span aria-hidden className="i-ri-timer-line h-4 w-4" />
</button>
<DifyIconButton aria-label={`${label} (${delay}ms)`} size="lg" variant="secondary">
<span aria-hidden className="i-ri-timer-line size-4" />
</DifyIconButton>
}
/>
<TooltipContent>{`${label} (${delay}ms)`}</TooltipContent>

View File

@@ -14,6 +14,7 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
@@ -31,7 +32,6 @@ import { useInvalid } from '@/service/use-base'
import { useExportPipelineDSL } from '@/service/use-pipeline'
import { downloadBlob } from '@/utils/download'
import { getDatasetACLCapabilities } from '@/utils/permission'
import ActionButton from '../../base/action-button'
import RenameDatasetModal from '../../datasets/rename-modal'
import Menu from './menu'
@@ -174,15 +174,15 @@ const DropDown = ({ expand, triggerClassName }: DropDownProps) => {
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
size={expand ? 'l' : 'm'}
size={expand ? 'lg' : 'md'}
className={cn('data-popup-open:bg-state-base-hover', triggerClassName)}
/>
>
<span aria-hidden className="i-ri-more-fill size-4" />
</IconButton>
}
>
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
/>
<DropdownMenuContent
placement={expand ? 'bottom-end' : 'right-start'}
sideOffset={4}

View File

@@ -1,9 +1,10 @@
'use client'
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useTranslation } from 'react-i18next'
import CopyFeedback from '@/app/components/base/copy-feedback'
import ShareQRCode from '@/app/components/base/qrcode'
import ActionButton from '../../../base/action-button'
import { AccessPointEndpoint } from './access-point-card'
type AccessPointUrlProps = {
@@ -43,6 +44,7 @@ export function AccessPointUrl({
unavailableLabel,
value,
}: AccessPointUrlProps) {
const { t } = useTranslation()
const detailsAvailable = !loading && !unavailable
const disabledActions = (
@@ -90,9 +92,8 @@ export function AccessPointUrl({
<ShareQRCode content={value} />
))}
{showRegenerate && (
<ActionButton
className="size-6 px-0"
aria-label={regenerateLabel}
<IconButton
aria-label={regenerateLabel || t(($) => $['operation.regenerate'], { ns: 'common' })}
disabled={regenerateDisabled || regenerating}
onClick={onRegenerate}
>
@@ -100,7 +101,7 @@ export function AccessPointUrl({
aria-hidden
className={`i-ri-loop-left-line size-4 ${regenerating ? 'animate-spin' : ''}`}
/>
</ActionButton>
</IconButton>
)}
{showOpen && (
<>

View File

@@ -5,6 +5,7 @@ import type { AnnotationItem, AnnotationItemBasic } from './type'
import type { AnnotationReplyConfig } from '@/models/debug'
import type { App } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast'
@@ -13,7 +14,6 @@ import { useDebounce } from 'ahooks'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import Loading from '@/app/components/base/loading'
@@ -202,7 +202,7 @@ const Annotation: FC<Props> = (props) => {
{annotationConfig?.enabled && (
<div className="flex items-center pr-1 pl-1.5">
<div className="mr-1 h-3.5 w-px shrink-0 bg-divider-subtle"></div>
<ActionButton
<IconButton
aria-label={t(($) => $['initSetup.configTitle'], {
ns: 'appAnnotation',
})}
@@ -212,7 +212,7 @@ const Annotation: FC<Props> = (props) => {
aria-hidden="true"
className="size-4 text-text-tertiary"
/>
</ActionButton>
</IconButton>
</div>
)}
</div>

View File

@@ -2,9 +2,9 @@
import type { AnnotationItem } from './type'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import useTimestamp from '@/hooks/use-timestamp'
import BatchAction from './batch-action'
import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
@@ -54,18 +54,18 @@ function AnnotationTableRow({
<td className="p-3 pr-2">{item.hit_count}</td>
<td className="w-24 p-3 pr-2" onClick={(e) => e.stopPropagation()}>
<div className="flex space-x-1 text-text-tertiary">
<ActionButton
<IconButton
aria-label={t(($) => $['feature.annotation.edit'], { ns: 'appDebug' })}
onClick={() => onView(item)}
>
<span aria-hidden className="i-ri-edit-line size-4" />
</ActionButton>
<ActionButton
</IconButton>
<IconButton
aria-label={t(($) => $['feature.annotation.remove'], { ns: 'appDebug' })}
onClick={() => onRemoveClick(item.id)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</ActionButton>
</IconButton>
</div>
</td>
</tr>

View File

@@ -12,11 +12,11 @@ import {
DrawerPortal,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Loading from '@/app/components/base/loading'
import TabSlider from '@/app/components/base/tab-slider-plain'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
@@ -189,9 +189,12 @@ const SettingBuiltInTool: FC<Props> = ({
{/* header */}
<div className="relative border-b border-divider-subtle p-4 pb-3">
<div className="absolute top-3 right-3">
<ActionButton onClick={onHide}>
<RiCloseLine className="size-4" />
</ActionButton>
<IconButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onHide}
>
<RiCloseLine aria-hidden="true" className="size-4" />
</IconButton>
</div>
{showBackButton && (
<div

View File

@@ -194,7 +194,7 @@ describe('dataset-config/card-item', () => {
})
})
it('should call onRemove and toggle destructive state on hover', async () => {
it('should call onRemove from its named icon command', async () => {
const user = userEvent.setup()
const dataset = createDataset()
const { onRemove } = renderItem(dataset)
@@ -205,15 +205,6 @@ describe('dataset-config/card-item', () => {
hidden: true,
})
expect(deleteButton.className).not.toContain('action-btn-destructive')
fireEvent.mouseEnter(deleteButton)
expect(deleteButton.className).toContain('action-btn-destructive')
expect(card.className).toContain('border-state-destructive-border')
fireEvent.mouseLeave(deleteButton)
expect(deleteButton.className).not.toContain('action-btn-destructive')
await user.click(deleteButton)
expect(onRemove).toHaveBeenCalledWith(dataset.id)
})

View File

@@ -10,11 +10,10 @@ import {
DrawerPortal,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@@ -73,32 +72,29 @@ const Item: FC<ItemProps> = ({ config, onSave, onRemove, readonly = false, edita
</div>
<div className="ml-2 hidden shrink-0 items-center space-x-1 group-hover:flex">
{editable && !readonly && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.edit'], { ns: 'common' })}
onClick={(e) => {
e.stopPropagation()
setShowSettingsModal(true)
}}
>
<RiEditLine aria-hidden="true" className="size-4 shrink-0 text-text-tertiary" />
</ActionButton>
<span
aria-hidden="true"
className="i-ri-edit-line size-4 shrink-0 text-text-tertiary"
/>
</IconButton>
)}
{!readonly && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.remove'], { ns: 'common' })}
tone="destructive"
onClick={() => onRemove(config.id)}
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
onMouseEnter={() => setIsDeleting(true)}
onMouseLeave={() => setIsDeleting(false)}
>
<RiDeleteBinLine
aria-hidden="true"
className={cn(
'size-4 shrink-0 text-text-tertiary',
isDeleting && 'text-text-destructive',
)}
/>
</ActionButton>
<span aria-hidden="true" className="i-ri-delete-bin-line size-4 shrink-0" />
</IconButton>
)}
</div>
{!!config.indexing_technique && (

View File

@@ -1,5 +1,6 @@
import type { ComponentProps } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ConfigContext from '@/context/debug-configuration'
@@ -484,10 +485,10 @@ const renderDebug = (
}
render(
React.createElement(ConfigContext.Provider, {
value: createContextValue(options.contextValue),
children: <Debug {...props} />,
}),
// oxlint-disable-next-line eslint-react/no-context-provider -- use-context-selector contexts are not React 19 context components.
<ConfigContext.Provider value={createContextValue(options.contextValue)}>
<Debug {...props} />
</ConfigContext.Provider>,
)
return { onSetting, notify: mockState.mockToastCall, props }
@@ -854,7 +855,7 @@ describe('Debug', () => {
await waitFor(() => expect(mockState.mockSendCompletionMessage).toHaveBeenCalledTimes(1))
const [, requestData, handlers] = (mockState.mockSendCompletionMessage.mock.calls[0] ??
[]) as [unknown, any, { onNotifyError: (message: string) => void }]
[]) as [unknown, unknown, { onNotifyError: (message: string) => void }]
expect(requestData).toMatchObject({
inputs: { question: 'hello' },
model_config: {
@@ -1083,7 +1084,9 @@ describe('Debug', () => {
})
})
it('should emit restart event when refresh is clicked in multiple-model mode', () => {
it('should emit restart event when refresh is clicked in multiple-model mode', async () => {
const user = userEvent.setup()
renderDebug({
props: {
debugWithMultipleModel: true,
@@ -1093,7 +1096,7 @@ describe('Debug', () => {
},
})
fireEvent.click(screen.getAllByTestId('action-button')[0]!)
await user.click(screen.getByRole('button', { name: 'common.operation.refresh' }))
expect(mockState.mockEventEmitterEmit).toHaveBeenCalledWith({
type: APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
})

View File

@@ -7,9 +7,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useProviderContext } from '@/context/provider-context'
@@ -80,12 +80,12 @@ const DebugItem: FC<DebugItemProps> = ({ modelAndParameter, className, style })
<DropdownMenu>
<DropdownMenuTrigger
render={
<ActionButton
className="focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
className="data-popup-open:bg-state-base-hover"
>
<span aria-hidden className="i-ri-more-fill size-4 text-text-tertiary" />
</ActionButton>
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-[160px]">

View File

@@ -6,6 +6,7 @@ import type { ModelParameterModalProps } from '@/app/components/header/account-s
import type { Inputs } from '@/models/debug'
import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiAddLine, RiEqualizer2Line, RiSparklingFill } from '@remixicon/react'
import { useBoolean } from 'ahooks'
@@ -442,12 +443,12 @@ const Debug: FC<IDebug> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['operation.refresh'], { ns: 'common' })}
onClick={clearConversation}
>
<RefreshCcw01 aria-hidden="true" className="size-4" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>

View File

@@ -26,6 +26,7 @@ import {
DrawerPortal,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
@@ -43,7 +44,6 @@ import { useShallow } from 'zustand/react/shallow'
import ModelInfo from '@/app/components/app/log/model-info'
import { useStore as useAppStore } from '@/app/components/app/store'
import TextGeneration from '@/app/components/app/text-generate/item'
import ActionButton from '@/app/components/base/action-button'
import AgentLogModal from '@/app/components/base/agent-log-modal'
import Chat from '@/app/components/base/chat/chat'
import CopyIcon from '@/app/components/base/copy-icon'
@@ -475,13 +475,13 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
<div className="flex grow flex-wrap items-center justify-end gap-y-1">
{!isAdvanced && <ModelInfo model={detail.model_config.model} />}
</div>
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onClose}
>
<RiCloseLine className="size-4 text-text-tertiary" />
</ActionButton>
</IconButton>
</div>
{/* Panel Body */}
<div className="shrink-0 px-1 pt-1">

View File

@@ -1,5 +1,6 @@
import type { SiteInfo } from '@/models/share'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { act } from 'react'
@@ -40,12 +41,7 @@ const baseProps = {
className: 'custom-modal',
}
const getCopyButton = () => {
const buttons = screen.getAllByRole('button')
const actionButton = buttons.find((button) => button.className.includes('action-btn'))
expect(actionButton).toBeDefined()
return actionButton!
}
const getCopyButton = () => screen.getByRole('button', { name: /copy/i })
describe('Embedded', () => {
beforeAll(() => {
@@ -75,6 +71,8 @@ describe('Embedded', () => {
})
it('copies iframe snippet', async () => {
const user = userEvent.setup()
await act(async () => {
render(<Embedded {...baseProps} />)
})
@@ -88,11 +86,8 @@ describe('Embedded', () => {
).toBeInTheDocument()
})
const actionButton = getCopyButton()
const innerDiv = actionButton.querySelector('div')
await act(async () => {
fireEvent.click(innerDiv ?? actionButton)
})
const copyButton = getCopyButton()
await user.click(copyButton)
await waitFor(() => {
expect(mockedCopy).toHaveBeenCalledWith(expect.stringContaining('/chatbot/token'))
@@ -192,6 +187,8 @@ describe('Embedded', () => {
})
it('copies script content when scripts option is selected', async () => {
const user = userEvent.setup()
await act(async () => {
render(<Embedded {...baseProps} />)
})
@@ -207,11 +204,8 @@ describe('Embedded', () => {
expect(codeBlock?.textContent ?? '').toContain('background-color: #000000')
})
const actionButton = getCopyButton()
const innerDiv = actionButton.querySelector('div')
await act(async () => {
fireEvent.click(innerDiv ?? actionButton)
})
const copyButton = getCopyButton()
await user.click(copyButton)
await waitFor(() => {
expect(mockedCopy).toHaveBeenCalledWith(expect.stringContaining("token: 'token'"))
@@ -219,6 +213,8 @@ describe('Embedded', () => {
})
it('copies chrome plugin URL (without prefix) when chromePlugin option is selected', async () => {
const user = userEvent.setup()
await act(async () => {
render(<Embedded {...baseProps} />)
})
@@ -233,11 +229,8 @@ describe('Embedded', () => {
expect(codeBlock?.textContent ?? '').toContain('ChatBot URL:')
})
const actionButton = getCopyButton()
const innerDiv = actionButton.querySelector('div')
await act(async () => {
fireEvent.click(innerDiv ?? actionButton)
})
const copyButton = getCopyButton()
await user.click(copyButton)
await waitFor(() => {
expect(mockedCopy).toHaveBeenCalledWith(expect.stringContaining('/chatbot/token'))

View File

@@ -7,12 +7,12 @@ import type {
import type { SiteInfo } from '@/models/share'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useSuspenseQuery } from '@tanstack/react-query'
import copy from 'copy-to-clipboard'
import { Suspense, use, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { createTheme } from '@/app/components/base/chat/embedded-chatbot/theme/theme'
import { InputVarType } from '@/app/components/workflow/types'
import { userProfileQueryOptions } from '@/features/account-profile/client'
@@ -305,7 +305,7 @@ const EmbeddedContent = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={
(copiedOption === option
? t(($) => $[`${prefixEmbedded}.copied`], { ns: 'appOverview' })
@@ -313,13 +313,12 @@ const EmbeddedContent = ({
}
onClick={() => void onClickCopy()}
>
{copiedOption === option && (
{copiedOption === option ? (
<span aria-hidden="true" className="i-ri-clipboard-fill size-4" />
)}
{copiedOption !== option && (
) : (
<span aria-hidden="true" className="i-ri-clipboard-line size-4" />
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>

View File

@@ -3,6 +3,7 @@ import type { FC } from 'react'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { AppSourceType } from '@/service/share'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiBookmark3Line,
@@ -78,35 +79,30 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
appSourceType !== AppSourceTypeEnum.installedApp &&
!isResponding && (
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs">
<ActionButton
<IconButton
aria-label={t(($) => $['operation.log'], { ns: 'common' })}
disabled={isError || !messageId}
title={t(($) => $['operation.log'], { ns: 'common' })}
onClick={onOpenLogModal}
>
<RiFileList3Line className="size-4" />
</ActionButton>
<RiFileList3Line aria-hidden="true" className="size-4" />
</IconButton>
</div>
)}
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs">
{moreLikeThis && !isTryApp && (
<ActionButton
<IconButton
aria-label={t(($) => $['feature.moreLikeThis.title'], { ns: 'appDebug' })}
state={
depth === MAX_GENERATION_DEPTH
? ActionButtonState.Disabled
: ActionButtonState.Default
}
disabled={depth === MAX_GENERATION_DEPTH}
title={t(($) => $['feature.moreLikeThis.title'], { ns: 'appDebug' })}
onClick={onMoreLikeThis}
>
<RiSparklingLine className="size-4" />
</ActionButton>
<RiSparklingLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{isShowTextToSpeech && !isTryApp && <NewAudioButton id={messageId!} voice={voice} />}
{showCopyAction && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.copy'], { ns: 'common' })}
disabled={isError || !messageId}
title={t(($) => $['operation.copy'], { ns: 'common' })}
@@ -117,20 +113,20 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
toast.success(t(($) => $['actionMsg.copySuccessfully'], { ns: 'common' }))
}}
>
<RiClipboardLine className="size-4" />
</ActionButton>
<RiClipboardLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{isInWebApp && isError && (
<ActionButton
<IconButton
aria-label={t(($) => $['generation.batchFailed.retry'], { ns: 'share' })}
title={t(($) => $['generation.batchFailed.retry'], { ns: 'share' })}
onClick={onRetry}
>
<RiResetLeftLine className="size-4" />
</ActionButton>
<RiResetLeftLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{isInWebApp && !isWorkflow && !isTryApp && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.save'], { ns: 'common' })}
disabled={isError || !messageId}
title={t(($) => $['operation.save'], { ns: 'common' })}
@@ -138,8 +134,8 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
onSave?.(messageId as string)
}}
>
<RiBookmark3Line className="size-4" />
</ActionButton>
<RiBookmark3Line aria-hidden="true" className="size-4" />
</IconButton>
)}
</div>
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (

View File

@@ -2,12 +2,12 @@
import type { FC } from 'react'
import type { SavedMessage } from '@/models/debug'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiClipboardLine, RiDeleteBinLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { Markdown } from '@/app/components/base/markdown'
import NewAudioButton from '@/app/components/base/new-audio-button'
import NoData from './no-data'
@@ -48,7 +48,7 @@ const SavedItems: FC<ISavedItemsProps> = ({
<div className="absolute right-2 bottom-1">
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs">
{isShowTextToSpeech && <NewAudioButton value={answer} />}
<ActionButton
<IconButton
aria-label={t(($) => $['operation.copy'], { ns: 'common' })}
onClick={() => {
copy(answer)
@@ -56,15 +56,15 @@ const SavedItems: FC<ISavedItemsProps> = ({
}}
>
<RiClipboardLine aria-hidden="true" className="size-4" />
</ActionButton>
<ActionButton
</IconButton>
<IconButton
aria-label={t(($) => $['operation.delete'], { ns: 'common' })}
onClick={() => {
onRemove(id)
}}
>
<RiDeleteBinLine aria-hidden="true" className="size-4" />
</ActionButton>
</IconButton>
</div>
</div>
</div>

View File

@@ -8,9 +8,9 @@ import {
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
@@ -78,9 +78,14 @@ const HeaderInMobile = () => {
return (
<>
<div className="flex shrink-0 items-center gap-1 bg-mask-top2bottom-gray-50-to-transparent px-2 py-3">
<ActionButton size="l" className="shrink-0" onClick={() => setShowSidebar(true)}>
<div className="i-ri-menu-line h-4.5 w-4.5" />
</ActionButton>
<IconButton
aria-label={t(($) => $['sidebar.expandSidebar'], { ns: 'layout' })}
size="lg"
className="shrink-0"
onClick={() => setShowSidebar(true)}
>
<div aria-hidden="true" className="i-ri-menu-line h-4.5 w-4.5" />
</IconButton>
<div className="flex grow items-center justify-center">
{!currentConversationId && (
<>

View File

@@ -65,17 +65,17 @@ describe('MobileOperationDropdown Component', () => {
})
})
it('applies hover state to ActionButton when open', async () => {
it('exposes popup-open state on the trigger', async () => {
const user = userEvent.setup()
render(<MobileOperationDropdown {...defaultProps} />)
const trigger = screen.getByRole('button', { name: 'common.operation.more' })
// closed state
expect(trigger).not.toHaveClass('action-btn-hover')
expect(trigger).not.toHaveAttribute('data-popup-open')
// open state
await user.click(trigger)
expect(trigger).toHaveClass('action-btn-hover')
expect(trigger).toHaveAttribute('data-popup-open')
})
it('closes the menu after clicking an action', async () => {

View File

@@ -9,11 +9,11 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiEditBoxLine, RiLayoutRight2Line, RiResetLeftLine } from '@remixicon/react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
@@ -89,14 +89,14 @@ const Header = () => {
!isSidebarCollapsed && 'user-select-none opacity-0',
)}
>
<ActionButton
<IconButton
aria-label={t(($) => $['sidebar.expandSidebar'], { ns: 'layout' })}
className={cn(!isSidebarCollapsed && 'cursor-default')}
size="l"
size="lg"
onClick={() => handleSidebarCollapse(false)}
>
<RiLayoutRight2Line aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
</IconButton>
<div className="mr-1 shrink-0">
<AppIcon
size="large"
@@ -134,19 +134,14 @@ const Header = () => {
disabled={!!currentConversationId}
render={
<div>
<ActionButton
<IconButton
aria-label={t(($) => $['chat.newChatTip'], { ns: 'share' })}
size="l"
state={
!currentConversationId || isResponding
? ActionButtonState.Disabled
: ActionButtonState.Default
}
size="lg"
disabled={!currentConversationId || isResponding}
onClick={handleNewConversation}
>
<RiEditBoxLine aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
</IconButton>
</div>
}
/>
@@ -159,13 +154,13 @@ const Header = () => {
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
size="l"
size="lg"
onClick={handleNewConversation}
>
<RiResetLeftLine aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>

View File

@@ -4,9 +4,9 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
type Props = Readonly<{
handleResetChat: () => void
@@ -29,16 +29,15 @@ const MobileOperationDropdown = ({
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={(props, state) => (
<ActionButton
{...props}
render={
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
size="l"
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<div className="i-ri-more-fill h-4.5 w-4.5" aria-hidden="true" />
</ActionButton>
)}
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-[160px]">
<DropdownMenuItem

View File

@@ -98,16 +98,16 @@ describe('ViewFormDropdown', () => {
expect(screen.getByText('Num Form')).toBeInTheDocument()
})
it('applies correct state to ActionButton when open', async () => {
it('exposes popup-open state on the trigger', async () => {
const user = userEvent.setup()
render(<ViewFormDropdown />)
const trigger = screen.getByRole('button', { name: 'share.chat.viewChatSettings' })
// closed state
expect(trigger).not.toHaveClass('action-btn-hover')
expect(trigger).not.toHaveAttribute('data-popup-open')
// open state
await user.click(trigger)
expect(trigger).toHaveClass('action-btn-hover')
expect(trigger).toHaveAttribute('data-popup-open')
})
})

View File

@@ -1,25 +1,22 @@
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { RiChatSettingsLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
const ViewFormDropdown = () => {
const { t } = useTranslation()
return (
<Popover>
<PopoverTrigger
render={(props, state) => (
<ActionButton
{...props}
render={
<IconButton
aria-label={t(($) => $['chat.viewChatSettings'], { ns: 'share' })}
size="l"
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<RiChatSettingsLine aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
)}
<span aria-hidden="true" className="i-ri-chat-settings-line h-4.5 w-4.5" />
</IconButton>
}
/>
<PopoverContent
placement="bottom-end"
@@ -29,7 +26,7 @@ const ViewFormDropdown = () => {
>
<div className="w-100 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-xs">
<div className="flex items-center gap-3 rounded-t-2xl border-b border-divider-subtle px-6 py-4">
<Message3Fill className="size-6 shrink-0" />
<span aria-hidden className="i-custom-public-other-message-3-fill size-6 shrink-0" />
<div className="grow system-xl-semibold text-text-secondary">
{t(($) => $['chat.chatSettingsTitle'], { ns: 'share' })}
</div>

View File

@@ -10,10 +10,10 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
@@ -106,22 +106,22 @@ const Sidebar = ({ isPanel }: Props) => {
{appData?.site.title}
</div>
{!isMobile && isSidebarCollapsed && (
<ActionButton
<IconButton
aria-label={t(($) => $['sidebar.expandSidebar'], { ns: 'layout' })}
size="l"
size="lg"
onClick={() => handleSidebarCollapse(false)}
>
<span aria-hidden className="i-ri-expand-right-line h-4.5 w-4.5" />
</ActionButton>
</IconButton>
)}
{!isMobile && !isSidebarCollapsed && (
<ActionButton
<IconButton
aria-label={t(($) => $['sidebar.collapseSidebar'], { ns: 'layout' })}
size="l"
size="lg"
onClick={() => handleSidebarCollapse(true)}
>
<span aria-hidden className="i-ri-layout-left-2-line h-4.5 w-4.5" />
</ActionButton>
</IconButton>
)}
</div>
<div className="shrink-0 px-3 py-4">

View File

@@ -9,6 +9,7 @@ import {
DialogDescription,
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
@@ -398,7 +399,7 @@ function Operation({
<NewAudioButton id={id} value={content} voice={config?.text_to_speech?.voice} />
)}
{hasPublicContent && !humanInputFormDataList?.length && (
<ActionButton
<IconButton
aria-label={copyLabel}
onClick={() => {
copy(content)
@@ -406,12 +407,12 @@ function Operation({
}}
>
<span aria-hidden="true" className="i-ri-clipboard-line size-4" />
</ActionButton>
</IconButton>
)}
{(!noChatInput || showRegenerate) && (
<ActionButton aria-label={regenerateLabel} onClick={() => onRegenerate?.(item)}>
<IconButton aria-label={regenerateLabel} onClick={() => onRegenerate?.(item)}>
<span aria-hidden="true" className="i-ri-reset-left-line size-4" />
</ActionButton>
</IconButton>
)}
{shouldShowAnnotationAction && (
<AnnotationCtrlButton

View File

@@ -4,9 +4,9 @@ import type { EnableType } from '../../types'
import type { FileUpload } from '@/app/components/base/features/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
type OperationProps = {
@@ -43,15 +43,15 @@ const Operation: FC<OperationProps> = ({
<FileUploaderInChatInput readonly={readonly} fileConfig={fileConfig} />
)}
{speechToTextConfig?.enabled && onShowVoiceInput && (
<ActionButton
className="shrink-0 outline-hidden focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
size="l"
<IconButton
className="shrink-0"
size="lg"
aria-label={t(($) => $['voiceInput.start'], { ns: 'common' })}
disabled={readonly}
onClick={onShowVoiceInput}
>
<span className="i-ri-mic-line size-5" aria-hidden="true" />
</ActionButton>
</IconButton>
)}
</div>
<Button

View File

@@ -1,13 +1,15 @@
import type { FC } from 'react'
import type { IChatItem } from '@/app/components/base/chat/chat/type'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { RiFileList3Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import ActionButton from '@/app/components/base/action-button'
type LogProps = {
logItem: IChatItem
}
const Log: FC<LogProps> = ({ logItem }) => {
const { t } = useTranslation()
const setCurrentLogItem = useAppStore((s) => s.setCurrentLogItem)
const setShowPromptLogModal = useAppStore((s) => s.setShowPromptLogModal)
const setShowAgentLogModal = useAppStore((s) => s.setShowAgentLogModal)
@@ -27,9 +29,9 @@ const Log: FC<LogProps> = ({ logItem }) => {
else setShowPromptLogModal(true)
}}
>
<ActionButton>
<RiFileList3Line className="size-4" />
</ActionButton>
<IconButton aria-label={t(($) => $['operation.log'], { ns: 'common' })}>
<RiFileList3Line aria-hidden="true" className="size-4" />
</IconButton>
</div>
)
}

View File

@@ -3,6 +3,7 @@ import type { Theme } from '../embedded-chatbot/theme/theme'
import type { ChatItem } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import copy from 'copy-to-clipboard'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
@@ -11,7 +12,6 @@ import Textarea from 'react-textarea-autosize'
import { FileList } from '@/app/components/base/file-uploader'
import { User } from '@/app/components/base/icons/src/public/avatar'
import { Markdown } from '@/app/components/base/markdown'
import ActionButton from '../../action-button'
import { CssTransform } from '../embedded-chatbot/theme/utils'
import ContentSwitch from './content-switch'
import { useChatContext } from './context'
@@ -158,7 +158,7 @@ const Question: FC<QuestionProps> = ({
className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs group-hover:flex"
style={{ right: contentWidth + 8 }}
>
<ActionButton
<IconButton
aria-label={copyLabel}
onClick={() => {
copy(content)
@@ -166,11 +166,11 @@ const Question: FC<QuestionProps> = ({
}}
>
<div className="i-ri-clipboard-line size-4" aria-hidden="true" />
</ActionButton>
</IconButton>
{enableEdit && (
<ActionButton aria-label={editLabel} onClick={handleEdit}>
<IconButton aria-label={editLabel} onClick={handleEdit}>
<div className="i-ri-edit-line size-4" aria-hidden="true" />
</ActionButton>
</IconButton>
)}
</div>
</div>

View File

@@ -1,12 +1,12 @@
import type { FC } from 'react'
import type { Theme } from '../theme/theme'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import Divider from '@/app/components/base/divider'
import { DifyLogo } from '@/app/components/base/logo/dify-logo'
@@ -115,8 +115,8 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={
expanded
? t(($) => $['chat.collapse'], { ns: 'share' })
@@ -132,7 +132,7 @@ const Header: FC<IHeaderProps> = ({
) : (
<div className="i-ri-expand-diagonal-2-line h-4.5 w-4.5" aria-hidden="true" />
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>
@@ -146,13 +146,13 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
onClick={onCreateNewChat}
>
<div className="i-ri-reset-left-line h-4.5 w-4.5" aria-hidden="true" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>
@@ -185,8 +185,8 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={
expanded
? t(($) => $['chat.collapse'], { ns: 'share' })
@@ -211,7 +211,7 @@ const Header: FC<IHeaderProps> = ({
aria-hidden="true"
/>
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>
@@ -225,8 +225,8 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
onClick={onCreateNewChat}
>
@@ -234,7 +234,7 @@ const Header: FC<IHeaderProps> = ({
className={cn('i-ri-reset-left-line h-4.5 w-4.5', theme?.colorPathOnHeader)}
aria-hidden="true"
/>
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>

View File

@@ -1,8 +1,8 @@
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
type Props = Readonly<{
@@ -14,19 +14,18 @@ const ViewFormDropdown = ({ iconColor }: Props) => {
return (
<Popover>
<PopoverTrigger
render={(props, state) => (
<ActionButton
{...props}
render={
<IconButton
aria-label={t(($) => $['chat.viewChatSettings'], { ns: 'share' })}
size="l"
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<span
aria-hidden
className={cn('i-ri-chat-settings-line h-4.5 w-4.5 shrink-0', iconColor)}
/>
</ActionButton>
)}
</IconButton>
}
/>
<PopoverContent
placement="bottom-end"

View File

@@ -1,10 +1,10 @@
'use client'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiClipboardFill, RiClipboardLine } from '@remixicon/react'
import { useClipboard } from 'foxact/use-clipboard'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import copyStyle from './style.module.css'
type Props = Readonly<{
@@ -35,10 +35,13 @@ const CopyFeedback = ({ content }: Props) => {
<Tooltip>
<TooltipTrigger
render={
<ActionButton aria-label={safeText} onClick={handleCopy}>
{copied && <RiClipboardFill className="size-4" aria-hidden="true" />}
{!copied && <RiClipboardLine className="size-4" aria-hidden="true" />}
</ActionButton>
<IconButton aria-label={safeText} onClick={handleCopy}>
{copied ? (
<RiClipboardFill className="size-4" aria-hidden="true" />
) : (
<RiClipboardLine className="size-4" aria-hidden="true" />
)}
</IconButton>
}
/>
<TooltipContent>{safeText}</TooltipContent>

View File

@@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiEditLine, RiFileEditLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { addAnnotation } from '@/service/annotation'
@@ -52,12 +52,12 @@ const AnnotationCtrlButton: FC<Props> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['feature.annotation.edit'], { ns: 'appDebug' })}
onClick={onEdit}
>
<RiEditLine aria-hidden className="size-4" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>
@@ -69,12 +69,12 @@ const AnnotationCtrlButton: FC<Props> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['feature.annotation.add'], { ns: 'appDebug' })}
onClick={handleAdd}
>
<RiFileEditLine aria-hidden className="size-4" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>

View File

@@ -1,10 +1,10 @@
import type { FileEntity } from '../types'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { ProgressCircle } from '@langgenius/dify-ui/progress'
import { RiDeleteBinLine, RiDownloadLine, RiEyeLine } from '@remixicon/react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { PreviewMode } from '@/app/components/base/features/types'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
@@ -77,29 +77,41 @@ const FileInAttachmentItem = ({
/>
)}
{progress === -1 && (
<ActionButton className="mr-1" onClick={() => onReUpload?.(id)}>
<ReplayLine className="size-4 text-text-tertiary" />
</ActionButton>
<IconButton
aria-label={`${t(($) => $['operation.retry'], { ns: 'common' })} ${name}`}
className="mr-1"
onClick={() => onReUpload?.(id)}
>
<ReplayLine aria-hidden="true" className="size-4 text-text-tertiary" />
</IconButton>
)}
{showDeleteAction && (
<ActionButton onClick={() => onRemove?.(id)}>
<RiDeleteBinLine className="size-4" />
</ActionButton>
<IconButton
aria-label={`${t(($) => $['operation.remove'], { ns: 'common' })} ${name}`}
onClick={() => onRemove?.(id)}
>
<RiDeleteBinLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{canPreview && isImageFile && (
<ActionButton className="mr-1" onClick={() => setImagePreviewUrl(url || '')}>
<RiEyeLine className="size-4" />
</ActionButton>
<IconButton
aria-label={`${t(($) => $['operation.view'], { ns: 'common' })} ${name}`}
className="mr-1"
onClick={() => setImagePreviewUrl(url || '')}
>
<RiEyeLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{showDownloadAction && (
<ActionButton
<IconButton
aria-label={`${t(($) => $['operation.download'], { ns: 'common' })} ${name}`}
onClick={(e) => {
e.stopPropagation()
downloadUrl({ url: url || base64Url || '', fileName: name, target: '_blank' })
}}
>
<RiDownloadLine className="size-4" />
</ActionButton>
<RiDownloadLine aria-hidden="true" className="size-4" />
</IconButton>
)}
</div>
</div>

View File

@@ -1,10 +1,10 @@
import type { FileEntity } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { ProgressCircle } from '@langgenius/dify-ui/progress'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import AudioPreview from '@/app/components/base/file-uploader/audio-preview'
import PdfPreview from '@/app/components/base/file-uploader/dynamic-pdf-preview'
import VideoPreview from '@/app/components/base/file-uploader/video-preview'
@@ -83,9 +83,9 @@ const FileItem = ({
{!!file.size && formatFileSize(file.size)}
</div>
{showDownloadAction && download_url && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.download'], { ns: 'common' })}
size="m"
size="md"
className="absolute -top-1 -right-1 hidden group-hover/file-item:flex"
onClick={(e) => {
e.stopPropagation()
@@ -93,7 +93,7 @@ const FileItem = ({
}}
>
<span className="i-ri-download-line size-3.5 text-text-tertiary" aria-hidden="true" />
</ActionButton>
</IconButton>
)}
{progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle

View File

@@ -1,11 +1,11 @@
'use client'
import type { InputProps } from '../input'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useClipboard } from 'foxact/use-clipboard'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '../action-button'
type InputWithCopyProps = {
showCopyButton?: boolean
@@ -55,12 +55,12 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>(
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
size="xs"
variant="ghost"
aria-label={safeTooltipText}
onClick={handleCopy}
onMouseLeave={reset}
className="hover:bg-components-button-ghost-bg-hover"
>
{copied ? (
<span
@@ -73,7 +73,7 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>(
aria-hidden="true"
/>
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{safeTooltipText}</TooltipContent>

View File

@@ -5,6 +5,7 @@ import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/
import type { Type } from '@/app/components/workflow/nodes/llm/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
@@ -15,7 +16,6 @@ import {
isParagraphFormInput,
isSelectFormInput,
} from '@/app/components/workflow/nodes/human-input/types'
import ActionButton from '../../../action-button'
import { VariableX } from '../../../icons/src/vender/workflow'
import InputField from './input-field'
import VariableBlock from './variable-block'
@@ -166,18 +166,18 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
{!readonly && (
<div className="hidden h-full shrink-0 items-center space-x-1 group-hover:flex">
<div className="flex h-full items-center" ref={editBtnRef}>
<ActionButton size="s" aria-label={t(($) => $['operation.edit'], { ns: 'common' })}>
<IconButton size="sm" aria-label={t(($) => $['operation.edit'], { ns: 'common' })}>
<span className="i-ri-edit-line size-4 text-text-tertiary" aria-hidden="true" />
</ActionButton>
</IconButton>
</div>
<div className="flex h-full items-center" ref={removeBtnRef}>
<ActionButton size="s" aria-label={t(($) => $['operation.remove'], { ns: 'common' })}>
<IconButton size="sm" aria-label={t(($) => $['operation.remove'], { ns: 'common' })}>
<span
className="i-ri-delete-bin-line size-4 text-text-tertiary"
aria-hidden="true"
/>
</ActionButton>
</IconButton>
</div>
</div>
)}

View File

@@ -1,10 +1,10 @@
'use client'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { QRCodeCanvas as QRCode } from 'qrcode.react'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { downloadUrl } from '@/utils/download'
type Props = Readonly<{
@@ -52,9 +52,9 @@ const ShareQRCode = ({ content }: Props) => {
<div className="relative size-6">
<TooltipTrigger
render={
<ActionButton aria-label={safeTooltipText} onClick={toggleQRCode}>
<IconButton aria-label={safeTooltipText} onClick={toggleQRCode}>
<span className="i-ri-qr-code-line size-4" aria-hidden="true" />
</ActionButton>
</IconButton>
}
/>
{isShow && (

View File

@@ -8,9 +8,9 @@ import {
DropdownMenuRadioItemIndicator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
const THEMES = ['light', 'dark', 'system'] as const
export type Theme = (typeof THEMES)[number]
@@ -39,14 +39,15 @@ export default function ThemeSelector() {
<DropdownMenu>
<DropdownMenuTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['theme.theme'], { ns: 'common' })}
className="h-8 w-8 p-1.5 data-popup-open:bg-state-base-hover"
/>
size="lg"
className="data-popup-open:bg-state-base-hover"
>
{getCurrentIcon()}
</IconButton>
}
>
{getCurrentIcon()}
</DropdownMenuTrigger>
/>
<DropdownMenuContent placement="bottom-end" sideOffset={6} popupClassName="w-[144px]">
<DropdownMenuRadioGroup<Theme>
value={currentTheme}

View File

@@ -14,9 +14,9 @@ import {
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Loading from '@/app/components/base/loading'
import useTimestamp from '@/hooks/use-timestamp'
@@ -155,12 +155,12 @@ export function SecretKeyModalView({
<div className="flex grow space-x-2 px-3">
<CopyFeedback content={api.token} />
{canManage && (
<ActionButton
<IconButton
aria-label={`${t(($) => $['operation.delete'], { ns: 'common' })} ${generateToken(api.token)}`}
onClick={() => setDeleteKeyId(api.id)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</ActionButton>
</IconButton>
)}
</div>
</div>

View File

@@ -3,13 +3,13 @@ import type { FC } from 'react'
import type { EmbeddedChatbotContextValue } from '@/app/components/base/chat/embedded-chatbot/context'
import type { TryAppInfo } from '@/service/try-app'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiResetLeftLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Alert from '@/app/components/base/alert'
import AppIcon from '@/app/components/base/app-icon'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
@@ -79,13 +79,13 @@ const TryApp: FC<Props> = ({ appId, appDetail, className }) => {
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
onClick={handleNewConversation}
>
<RiResetLeftLine className="h-4.5 w-4.5" aria-hidden="true" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>

View File

@@ -10,10 +10,10 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { useWebAppStore } from '@/context/web-app-context'
import { AccessMode } from '@/models/access-control'
@@ -51,11 +51,14 @@ const MenuDropdown: FC<Props> = ({ data, placement, hideLogout }) => {
<DropdownMenu>
<DropdownMenuTrigger
render={
<ActionButton size="l" className="data-popup-open:bg-state-base-hover">
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<span aria-hidden className="i-ri-equalizer-2-line h-4.5 w-4.5" />
</ActionButton>
</IconButton>
}
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
/>
<DropdownMenuContent
placement={placement || 'bottom-end'}

View File

@@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { RiDownloadLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useCSVDownloader } from 'react-papaparse'
import ActionButton from '@/app/components/base/action-button'
type IResDownloadProps = {
isMobile: boolean
@@ -28,9 +28,9 @@ const ResDownload: FC<IResDownloadProps> = ({ isMobile, values }) => {
data={values}
>
{isMobile && (
<ActionButton>
<RiDownloadLine className="size-4" />
</ActionButton>
<IconButton aria-label={t(($) => $['operation.download'], { ns: 'common' })}>
<RiDownloadLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{!isMobile && (
<Button>