refactor(dify-ui): finish primitive migration from web/base/ui to @langgenius/dify-ui (#35349)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh
2026-04-17 16:46:11 +08:00
committed by GitHub
parent f56ce9d3b1
commit dfcc0f8863
1056 changed files with 2547 additions and 2069 deletions

View File

@@ -89,3 +89,34 @@ jobs:
flags: web
env:
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
dify-ui-test:
name: dify-ui Tests
runs-on: ubuntu-latest
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
defaults:
run:
shell: bash
working-directory: ./packages/dify-ui
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup web environment
uses: ./.github/actions/setup-web
- name: Run dify-ui tests
run: vp test run --coverage --silent=passed-only
- name: Report coverage
if: ${{ env.CODECOV_TOKEN != '' }}
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
directory: packages/dify-ui/coverage
flags: dify-ui
env:
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}

View File

@@ -3,6 +3,10 @@ coverage:
project:
default:
target: auto
# Absorb sub-percent coverage noise (rounding, trivial added lines, CVA variants, etc).
threshold: 1%
# Deleting covered code during refactors/migrations must not regress base.
removed_code_behavior: adjust_base
flags:
web:
@@ -10,6 +14,11 @@ flags:
- "web/"
carryforward: true
dify-ui:
paths:
- "packages/dify-ui/"
carryforward: true
api:
paths:
- "api/"

View File

@@ -2113,11 +2113,6 @@
"count": 1
}
},
"web/app/components/base/switch/index.stories.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/base/tab-slider/index.tsx": {
"react/set-state-in-effect": {
"count": 2

3
packages/dify-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/coverage
/dist
/storybook-static

View File

@@ -0,0 +1,27 @@
import type { StorybookConfig } from '@storybook/react-vite'
import tailwindcss from '@tailwindcss/vite'
import { mergeConfig } from 'vite'
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-docs',
'@storybook/addon-themes',
'@chromatic-com/storybook',
],
framework: '@storybook/react-vite',
core: {
disableWhatsNewNotifications: true,
},
docs: {
defaultName: 'Documentation',
},
async viteFinal(config) {
return mergeConfig(config, {
plugins: [tailwindcss()],
})
},
}
export default config

View File

@@ -0,0 +1,31 @@
import type { Preview } from '@storybook/react-vite'
import { withThemeByDataAttribute } from '@storybook/addon-themes'
import './storybook.css'
export const decorators = [
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
attributeName: 'data-theme',
}),
]
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
toc: true,
},
},
tags: ['autodocs'],
}
export default preview

View File

@@ -0,0 +1,19 @@
@import 'tailwindcss';
@config '../tailwind.config.ts';
@import '../src/styles/styles.css';
html {
color-scheme: light;
}
html[data-theme='dark'] {
color-scheme: dark;
}
body {
background: var(--color-components-panel-bg);
color: var(--color-text-primary, #101828);
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
}

View File

@@ -1,6 +1,15 @@
# @langgenius/dify-ui
This package provides shared design tokens (colors, shadows, typography), the `cn()` utility, and a Tailwind CSS preset consumed by `web/`.
Shared design tokens, the `cn()` utility, a Tailwind CSS preset, and headless primitive components consumed by `web/`.
## Component Authoring Rules
- Use `@base-ui/react` primitives + `cva` + `cn`.
- Inside dify-ui, cross-component imports use relative paths (`../button`). External consumers use subpath exports (`@langgenius/dify-ui/button`).
- No imports from `web/`. No dependencies on next / i18next / ky / jotai / zustand.
- One component per folder: `src/<name>/index.tsx`, optional `index.stories.tsx` and `__tests__/index.spec.tsx`. Add a matching `./<name>` subpath to `package.json#exports`.
- Props pattern: `Omit<BaseXxx.Root.Props, 'className' | ...> & VariantProps<typeof xxxVariants> & { /* custom */ }`.
- When a component accepts a prop typed from a shared internal module, `export type` it from that component so consumers import it from the component subpath.
## Border Radius: Figma Token → Tailwind Class Mapping

View File

@@ -12,18 +12,109 @@
"./cn": {
"types": "./src/cn.ts",
"import": "./src/cn.ts"
},
"./alert-dialog": {
"types": "./src/alert-dialog/index.tsx",
"import": "./src/alert-dialog/index.tsx"
},
"./avatar": {
"types": "./src/avatar/index.tsx",
"import": "./src/avatar/index.tsx"
},
"./button": {
"types": "./src/button/index.tsx",
"import": "./src/button/index.tsx"
},
"./context-menu": {
"types": "./src/context-menu/index.tsx",
"import": "./src/context-menu/index.tsx"
},
"./dialog": {
"types": "./src/dialog/index.tsx",
"import": "./src/dialog/index.tsx"
},
"./dropdown-menu": {
"types": "./src/dropdown-menu/index.tsx",
"import": "./src/dropdown-menu/index.tsx"
},
"./number-field": {
"types": "./src/number-field/index.tsx",
"import": "./src/number-field/index.tsx"
},
"./popover": {
"types": "./src/popover/index.tsx",
"import": "./src/popover/index.tsx"
},
"./scroll-area": {
"types": "./src/scroll-area/index.tsx",
"import": "./src/scroll-area/index.tsx"
},
"./select": {
"types": "./src/select/index.tsx",
"import": "./src/select/index.tsx"
},
"./slider": {
"types": "./src/slider/index.tsx",
"import": "./src/slider/index.tsx"
},
"./switch": {
"types": "./src/switch/index.tsx",
"import": "./src/switch/index.tsx"
},
"./toast": {
"types": "./src/toast/index.tsx",
"import": "./src/toast/index.tsx"
},
"./tooltip": {
"types": "./src/tooltip/index.tsx",
"import": "./src/tooltip/index.tsx"
}
},
"scripts": {
"storybook": "storybook dev",
"storybook:build": "storybook build",
"test": "vp test",
"test:watch": "vp test --watch",
"type-check": "tsc"
},
"peerDependencies": {
"@base-ui/react": "catalog:",
"class-variance-authority": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"tailwindcss": "catalog:"
},
"dependencies": {
"clsx": "catalog:",
"tailwind-merge": "catalog:"
},
"devDependencies": {
"@base-ui/react": "catalog:",
"@chromatic-com/storybook": "catalog:",
"@dify/tsconfig": "workspace:*",
"@egoist/tailwindcss-icons": "catalog:",
"@iconify-json/ri": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/addon-links": "catalog:",
"@storybook/addon-themes": "catalog:",
"@storybook/react-vite": "catalog:",
"@tailwindcss/vite": "catalog:",
"@testing-library/jest-dom": "catalog:",
"@testing-library/react": "catalog:",
"@testing-library/user-event": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"@vitest/coverage-v8": "catalog:",
"class-variance-authority": "catalog:",
"happy-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"storybook": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
}
}

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import {
AlertDialog,

View File

@@ -1,10 +1,10 @@
'use client'
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
import type { ButtonProps } from '@/app/components/base/ui/button'
import type { ButtonProps } from '../button'
import { AlertDialog as BaseAlertDialog } from '@base-ui/react/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import { Button } from '@/app/components/base/ui/button'
import { Button } from '../button'
import { cn } from '../cn'
export const AlertDialog = BaseAlertDialog.Root
export const AlertDialogTrigger = BaseAlertDialog.Trigger

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { Avatar, AvatarFallback, AvatarRoot } from '.'
const meta = {

View File

@@ -1,6 +1,6 @@
import type { ImageLoadingStatus } from '@base-ui/react/avatar'
import { Avatar as BaseAvatar } from '@base-ui/react/avatar'
import { cn } from '@langgenius/dify-ui/cn'
import { cn } from '../cn'
const avatarSizeClasses = {
'xxs': { root: 'size-4', text: 'text-[7px]' },

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { Button } from '.'

View File

@@ -1,8 +1,8 @@
import type { Button as BaseButtonNS } from '@base-ui/react/button'
import type { VariantProps } from 'class-variance-authority'
import { Button as BaseButton } from '@base-ui/react/button'
import { cn } from '@langgenius/dify-ui/cn'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
const buttonVariants = cva(
'inline-flex cursor-pointer items-center justify-center whitespace-nowrap outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid data-[disabled]:cursor-not-allowed',

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import {
ContextMenu,

View File

@@ -1,10 +1,10 @@
'use client'
import type { ReactNode } from 'react'
import type { OverlayItemVariant } from '@/app/components/base/ui/overlay-shared'
import type { Placement } from '@/app/components/base/ui/placement'
import type { OverlayItemVariant } from '../overlay-shared'
import type { Placement } from '../placement'
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu'
import { cn } from '@langgenius/dify-ui/cn'
import { cn } from '../cn'
import {
overlayBackdropClassName,
overlayDestructiveClassName,
@@ -14,8 +14,11 @@ import {
overlayPopupBaseClassName,
overlayRowClassName,
overlaySeparatorClassName,
} from '@/app/components/base/ui/overlay-shared'
import { parsePlacement } from '@/app/components/base/ui/placement'
} from '../overlay-shared'
import { parsePlacement } from '../placement'
/** @public */
export type { Placement }
export const ContextMenu = BaseContextMenu.Root
export const ContextMenuTrigger = BaseContextMenu.Trigger

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import {
Dialog,

View File

@@ -9,7 +9,7 @@
import type { ReactNode } from 'react'
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import { cn } from '@langgenius/dify-ui/cn'
import { cn } from '../cn'
export const Dialog = BaseDialog.Root
/** @public */

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import {
DropdownMenu,

View File

@@ -1,10 +1,10 @@
'use client'
import type { ReactNode } from 'react'
import type { OverlayItemVariant } from '@/app/components/base/ui/overlay-shared'
import type { Placement } from '@/app/components/base/ui/placement'
import type { OverlayItemVariant } from '../overlay-shared'
import type { Placement } from '../placement'
import { Menu } from '@base-ui/react/menu'
import { cn } from '@langgenius/dify-ui/cn'
import { cn } from '../cn'
import {
overlayDestructiveClassName,
overlayIndicatorClassName,
@@ -13,8 +13,10 @@ import {
overlayPopupBaseClassName,
overlayRowClassName,
overlaySeparatorClassName,
} from '@/app/components/base/ui/overlay-shared'
import { parsePlacement } from '@/app/components/base/ui/placement'
} from '../overlay-shared'
import { parsePlacement } from '../placement'
export type { Placement }
export const DropdownMenu = Menu.Root
export const DropdownMenuTrigger = Menu.Trigger

View File

@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { cn } from '@langgenius/dify-ui/cn'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useId, useState } from 'react'
import {
NumberField,
@@ -10,6 +9,7 @@ import {
NumberFieldInput,
NumberFieldUnit,
} from '.'
import { cn } from '../cn'
type DemoFieldProps = {
label: string

View File

@@ -3,8 +3,8 @@
import type { VariantProps } from 'class-variance-authority'
import type { HTMLAttributes } from 'react'
import { NumberField as BaseNumberField } from '@base-ui/react/number-field'
import { cn } from '@langgenius/dify-ui/cn'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
export const NumberField = BaseNumberField.Root
export type NumberFieldRootProps = BaseNumberField.Root.Props

View File

@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Placement } from '../placement'
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Placement } from '.'
import { Button } from '@langgenius/dify-ui/button'
import { useState } from 'react'
import {
Popover,
@@ -9,7 +10,6 @@ import {
PopoverTitle,
PopoverTrigger,
} from '.'
import { Button } from '../button'
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 hover:bg-state-base-hover'

View File

@@ -1,10 +1,12 @@
'use client'
import type { ReactNode } from 'react'
import type { Placement } from '@/app/components/base/ui/placement'
import type { Placement } from '../placement'
import { Popover as BasePopover } from '@base-ui/react/popover'
import { cn } from '@langgenius/dify-ui/cn'
import { parsePlacement } from '@/app/components/base/ui/placement'
import { cn } from '../cn'
import { parsePlacement } from '../placement'
export type { Placement }
export const Popover = BasePopover.Root
export const PopoverTrigger = BasePopover.Trigger

View File

@@ -1,8 +1,6 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import AppIcon from '@/app/components/base/app-icon'
import {
ScrollAreaContent,
ScrollAreaCorner,
@@ -11,6 +9,7 @@ import {
ScrollAreaThumb,
ScrollAreaViewport,
} from '.'
import { cn } from '../cn'
const meta = {
title: 'Base/UI/ScrollArea',
@@ -490,12 +489,13 @@ const ExploreSidebarWebAppsPane = () => {
)}
>
<div className="flex min-w-0 grow items-center gap-2">
<AppIcon
size="tiny"
iconType="emoji"
icon={item.icon}
background={item.iconBackground}
/>
<div
aria-hidden
className="flex size-6 shrink-0 items-center justify-center rounded-md text-xs"
style={{ background: item.iconBackground }}
>
{item.icon}
</div>
<span className="min-w-0 truncate system-sm-regular">
{item.name}
</span>

View File

@@ -2,7 +2,7 @@
import type { ReactNode } from 'react'
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
import { cn } from '@langgenius/dify-ui/cn'
import { cn } from '../cn'
export const ScrollAreaRoot = BaseScrollArea.Root
type ScrollAreaRootProps = BaseScrollArea.Root.Props

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import {
Select,

View File

@@ -2,15 +2,18 @@
import type { VariantProps } from 'class-variance-authority'
import type { ReactNode } from 'react'
import type { Placement } from '@/app/components/base/ui/placement'
import type { Placement } from '../placement'
import { Select as BaseSelect } from '@base-ui/react/select'
import { cn } from '@langgenius/dify-ui/cn'
import { cva } from 'class-variance-authority'
import { cn } from '../cn'
import {
overlayLabelClassName,
overlaySeparatorClassName,
} from '@/app/components/base/ui/overlay-shared'
import { parsePlacement } from '@/app/components/base/ui/placement'
} from '../overlay-shared'
import { parsePlacement } from '../placement'
/** @public */
export type { Placement }
export const Select = BaseSelect.Root
export const SelectValue = BaseSelect.Value

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import type * as React from 'react'
import { useState } from 'react'
import { Slider } from '.'

View File

@@ -1,7 +1,7 @@
'use client'
import { Slider as BaseSlider } from '@base-ui/react/slider'
import { cn } from '@langgenius/dify-ui/cn'
import { cn } from '../cn'
/** @public */
export const SliderRoot = BaseSlider.Root

View File

@@ -1,8 +1,7 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import Switch from '../index'
import { SwitchSkeleton } from '../skeleton'
import { Switch, SwitchSkeleton } from '../index'
const getThumb = (switchElement: HTMLElement) => switchElement.querySelector('span')

View File

@@ -1,16 +1,16 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { ComponentProps } from 'react'
import { useState, useTransition } from 'react'
import Switch from '.'
import { SwitchSkeleton } from './skeleton'
import { Switch, SwitchSkeleton } from '.'
const meta = {
title: 'Base/Data Entry/Switch',
title: 'Base/UI/Switch',
component: Switch,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Toggle switch built on Base UI with CVA variants, Figma-aligned design tokens, loading spinner, and skeleton placeholder. Import `Switch` for the toggle and `SwitchSkeleton` from `./skeleton` for loading placeholders.',
component: 'Toggle switch built on Base UI with CVA variants, Figma-aligned design tokens, loading spinner, and skeleton placeholder. Import `Switch` and `SwitchSkeleton` from `@langgenius/dify-ui/switch`.',
},
},
},
@@ -42,7 +42,7 @@ const meta = {
export default meta
type Story = StoryObj<typeof meta>
const SwitchDemo = (args: any) => {
const SwitchDemo = (args: Partial<ComponentProps<typeof Switch>>) => {
const [enabled, setEnabled] = useState(args.checked ?? false)
return (
@@ -338,7 +338,7 @@ export const Skeleton: Story = {
parameters: {
docs: {
description: {
story: '`SwitchSkeleton` renders a non-interactive placeholder with `bg-text-quaternary opacity-20`. Imported separately from `./skeleton`.',
story: '`SwitchSkeleton` renders a non-interactive placeholder with `bg-text-quaternary opacity-20`. Exported from `@langgenius/dify-ui/switch` alongside `Switch`.',
},
},
},

View File

@@ -1,10 +1,11 @@
'use client'
import type { Switch as BaseSwitchNS } from '@base-ui/react/switch'
import type { VariantProps } from 'class-variance-authority'
import type { HTMLAttributes } from 'react'
import { Switch as BaseSwitch } from '@base-ui/react/switch'
import { cn } from '@langgenius/dify-ui/cn'
import { cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '../cn'
const switchRootStateClassName = 'bg-components-toggle-bg-unchecked hover:bg-components-toggle-bg-unchecked-hover data-checked:bg-components-toggle-bg data-checked:hover:bg-components-toggle-bg-hover data-disabled:cursor-not-allowed data-disabled:bg-components-toggle-bg-unchecked-disabled data-disabled:hover:bg-components-toggle-bg-unchecked-disabled data-disabled:data-checked:bg-components-toggle-bg-disabled data-disabled:data-checked:hover:bg-components-toggle-bg-disabled'
@@ -61,46 +62,35 @@ const spinnerSizeConfig: Partial<Record<SwitchSize, {
},
}
type SwitchProps = {
'checked': boolean
'onCheckedChange'?: (checked: boolean) => void
'size'?: SwitchSize
'disabled'?: boolean
'loading'?: boolean
'className'?: string
'aria-label'?: string
'aria-labelledby'?: string
'data-testid'?: string
}
export type SwitchProps
= Omit<BaseSwitchNS.Root.Props, 'className' | 'size' | 'onCheckedChange'>
& VariantProps<typeof switchRootVariants>
& {
onCheckedChange?: (checked: boolean) => void
loading?: boolean
className?: string
}
const Switch = ({
ref,
export function Switch({
checked,
onCheckedChange,
size = 'md',
disabled = false,
disabled,
loading = false,
className,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
'data-testid': dataTestid,
}: SwitchProps & {
ref?: React.Ref<HTMLElement>
}) => {
onCheckedChange,
...props
}: SwitchProps) {
const isDisabled = disabled || loading
const spinner = loading ? spinnerSizeConfig[size] : undefined
const spinner = loading && size ? spinnerSizeConfig[size] : undefined
return (
<BaseSwitch.Root
ref={ref}
checked={checked}
onCheckedChange={value => onCheckedChange?.(value)}
disabled={isDisabled}
aria-busy={loading || undefined}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
className={cn(switchRootVariants({ size }), className)}
data-testid={dataTestid}
onCheckedChange={value => onCheckedChange?.(value)}
{...props}
>
<BaseSwitch.Thumb
className={switchThumbVariants({ size })}
@@ -123,6 +113,39 @@ const Switch = ({
)
}
Switch.displayName = 'Switch'
const switchSkeletonVariants = cva(
'bg-text-quaternary opacity-20',
{
variants: {
size: {
xs: 'h-2.5 w-3.5 rounded-xs',
sm: 'h-3 w-5 rounded-[3.5px]',
md: 'h-4 w-7 rounded-[5px]',
lg: 'h-5 w-9 rounded-md',
},
},
defaultVariants: {
size: 'md',
},
},
)
export default React.memo(Switch)
export type SwitchSkeletonProps
= Omit<HTMLAttributes<HTMLDivElement>, 'className'>
& VariantProps<typeof switchSkeletonVariants>
& {
className?: string
}
export function SwitchSkeleton({
size = 'md',
className,
...props
}: SwitchSkeletonProps) {
return (
<div
className={cn(switchSkeletonVariants({ size }), className)}
{...props}
/>
)
}

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { ReactNode } from 'react'
import { toast } from '.'

View File

@@ -7,7 +7,7 @@ import type {
} from '@base-ui/react/toast'
import type { ReactNode } from 'react'
import { Toast as BaseToast } from '@base-ui/react/toast'
import { cn } from '@langgenius/dify-ui/cn'
import { cn } from '../cn'
type ToastData = Record<string, never>
type ToastToneStyle = {

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import type { Placement } from '../placement'
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Placement } from '.'
import { useState } from 'react'
import {
Tooltip,

View File

@@ -1,10 +1,12 @@
'use client'
import type { ReactNode } from 'react'
import type { Placement } from '@/app/components/base/ui/placement'
import type { Placement } from '../placement'
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
import { cn } from '@langgenius/dify-ui/cn'
import { parsePlacement } from '@/app/components/base/ui/placement'
import { cn } from '../cn'
import { parsePlacement } from '../placement'
export type { Placement }
type TooltipContentVariant = 'default' | 'plain'

View File

@@ -0,0 +1,23 @@
import type { Config } from 'tailwindcss'
import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons'
import difyUIPreset from './src/tailwind-preset'
const config: Config = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
'./.storybook/**/*.{js,ts,jsx,tsx,mdx}',
],
presets: [difyUIPreset],
plugins: [
iconsPlugin({
collections: getIconCollections(['ri']),
extraProperties: {
width: '1rem',
height: '1rem',
display: 'block',
},
}),
],
}
export default config

View File

@@ -0,0 +1,44 @@
import { act, cleanup } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
if (typeof Element !== 'undefined' && !Element.prototype.getAnimations)
Element.prototype.getAnimations = () => []
if (typeof document !== 'undefined' && !document.getAnimations)
document.getAnimations = () => []
if (typeof globalThis.ResizeObserver === 'undefined') {
globalThis.ResizeObserver = class {
observe() {
return undefined
}
unobserve() {
return undefined
}
disconnect() {
return undefined
}
}
}
if (typeof globalThis.IntersectionObserver === 'undefined') {
globalThis.IntersectionObserver = class {
readonly root: Element | Document | null = null
readonly rootMargin: string = ''
readonly scrollMargin: string = ''
readonly thresholds: ReadonlyArray<number> = []
constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) { /* noop */ }
observe(_target: Element) { /* noop */ }
unobserve(_target: Element) { /* noop */ }
disconnect() { /* noop */ }
takeRecords(): IntersectionObserverEntry[] { return [] }
}
}
afterEach(async () => {
await act(async () => {
cleanup()
})
})

View File

@@ -1,14 +1,10 @@
{
"extends": "@dify/tsconfig/base.json",
"extends": "@dify/tsconfig/react.json",
"compilerOptions": {
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"noEmit": false,
"outDir": "dist",
"sourceMap": true,
"rootDir": ".",
"types": ["vitest/globals", "@testing-library/jest-dom"],
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["src"]
"include": ["src", "tests", ".storybook", "tailwind.config.ts", "vite.config.ts"]
}

View File

@@ -0,0 +1,27 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite-plus'
const isCI = !!process.env.CI
export default defineConfig({
plugins: [react()],
resolve: {
tsconfigPaths: true,
},
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.stories.{ts,tsx}',
'src/**/__tests__/**',
'src/themes/**',
'src/styles/**',
],
reporter: isCI ? ['json', 'json-summary'] : ['text', 'json', 'json-summary'],
},
},
})

View File

@@ -1,5 +1,5 @@
{
"extends": "./web.json",
"extends": "./react.json",
"compilerOptions": {
"plugins": [
{

View File

@@ -6,6 +6,6 @@
"./base.json": "./base.json",
"./nextjs.json": "./nextjs.json",
"./node.json": "./node.json",
"./web.json": "./web.json"
"./react.json": "./react.json"
}
}

179
pnpm-lock.yaml generated
View File

@@ -135,6 +135,9 @@ catalogs:
'@storybook/react':
specifier: 10.3.5
version: 10.3.5
'@storybook/react-vite':
specifier: 10.3.5
version: 10.3.5
'@streamdown/math':
specifier: 1.0.2
version: 1.0.2
@@ -632,15 +635,87 @@ importers:
specifier: 'catalog:'
version: 3.5.0
devDependencies:
'@base-ui/react':
specifier: 'catalog:'
version: 1.4.0(@date-fns/tz@1.4.1)(@types/react@19.2.14)(date-fns@4.1.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@chromatic-com/storybook':
specifier: 'catalog:'
version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
'@dify/tsconfig':
specifier: workspace:*
version: link:../tsconfig
'@egoist/tailwindcss-icons':
specifier: 'catalog:'
version: 1.9.2(tailwindcss@4.2.2)
'@iconify-json/ri':
specifier: 'catalog:'
version: 1.2.10
'@storybook/addon-docs':
specifier: 'catalog:'
version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2))
'@storybook/addon-links':
specifier: 'catalog:'
version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
'@storybook/addon-themes':
specifier: 'catalog:'
version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
'@storybook/react-vite':
specifier: 'catalog:'
version: 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(esbuild@0.27.2))
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))
'@testing-library/jest-dom':
specifier: 'catalog:'
version: 6.9.1
'@testing-library/react':
specifier: 'catalog:'
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@testing-library/user-event':
specifier: 'catalog:'
version: 14.6.1(@testing-library/dom@10.4.1)
'@types/react':
specifier: 'catalog:'
version: 19.2.14
'@types/react-dom':
specifier: 'catalog:'
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.4(@voidzero-dev/vite-plus-test@0.1.18)
class-variance-authority:
specifier: 'catalog:'
version: 0.7.1
happy-dom:
specifier: 'catalog:'
version: 20.9.0
react:
specifier: 'catalog:'
version: 19.2.5
react-dom:
specifier: 'catalog:'
version: 19.2.5(react@19.2.5)
storybook:
specifier: 'catalog:'
version: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
tailwindcss:
specifier: 'catalog:'
version: 4.2.2
typescript:
specifier: 'catalog:'
version: 6.0.2
vite:
specifier: npm:@voidzero-dev/vite-plus-core@0.1.18
version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)'
vite-plus:
specifier: 'catalog:'
version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)
vitest:
specifier: npm:@voidzero-dev/vite-plus-test@0.1.18
version: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)'
packages/iconify-collections:
devDependencies:
@@ -11202,6 +11277,23 @@ snapshots:
- vite
- webpack
'@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2))':
dependencies:
'@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5)
'@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2))
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
ts-dedent: 2.2.0
transitivePeerDependencies:
- '@types/react'
- esbuild
- rollup
- vite
- webpack
'@storybook/addon-links@10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
dependencies:
'@storybook/global': 5.0.0
@@ -11229,6 +11321,17 @@ snapshots:
- rollup
- webpack
'@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2))':
dependencies:
'@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2))
storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
ts-dedent: 2.2.0
vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)'
transitivePeerDependencies:
- esbuild
- rollup
- webpack
'@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))':
dependencies:
storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -11239,6 +11342,16 @@ snapshots:
vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)'
webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)
'@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2))':
dependencies:
storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.27.2
rollup: 4.59.0
vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)'
webpack: 5.105.4(esbuild@0.27.2)
'@storybook/global@5.0.0': {}
'@storybook/icons@2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
@@ -11296,6 +11409,28 @@ snapshots:
- typescript
- webpack
'@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(esbuild@0.27.2))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
'@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2))
'@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)
empathic: 2.0.0
magic-string: 0.30.21
react: 19.2.5
react-docgen: 8.0.3
react-dom: 19.2.5(react@19.2.5)
resolve: 1.22.11
storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
tsconfig-paths: 4.2.0
vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)'
transitivePeerDependencies:
- esbuild
- rollup
- supports-color
- typescript
- webpack
'@storybook/react@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)':
dependencies:
'@storybook/global': 5.0.0
@@ -16589,6 +16724,17 @@ snapshots:
esbuild: 0.27.2
uglify-js: 3.19.3
terser-webpack-plugin@5.4.0(esbuild@0.27.2)(webpack@5.105.4(esbuild@0.27.2)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
terser: 5.46.1
webpack: 5.105.4(esbuild@0.27.2)
optionalDependencies:
esbuild: 0.27.2
optional: true
terser@5.46.1:
dependencies:
'@jridgewell/source-map': 0.3.11
@@ -17118,6 +17264,39 @@ snapshots:
webpack-virtual-modules@0.6.2: {}
webpack@5.105.4(esbuild@0.27.2):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
'@types/json-schema': 7.0.15
'@webassemblyjs/ast': 1.14.1
'@webassemblyjs/wasm-edit': 1.14.1
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.16.0
acorn-import-phases: 1.0.4(acorn@8.16.0)
browserslist: 4.28.1
chrome-trace-event: 1.0.4
enhanced-resolve: 5.20.1
es-module-lexer: 2.0.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
loader-runner: 4.3.1
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 4.3.3
tapable: 2.3.2
terser-webpack-plugin: 5.4.0(esbuild@0.27.2)(webpack@5.105.4(esbuild@0.27.2))
watchpack: 2.5.1
webpack-sources: 3.3.4
transitivePeerDependencies:
- '@swc/core'
- esbuild
- uglify-js
optional: true
webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3):
dependencies:
'@types/eslint-scope': 3.7.7

View File

@@ -55,6 +55,7 @@ catalog:
'@storybook/addon-themes': 10.3.5
'@storybook/nextjs-vite': 10.3.5
'@storybook/react': 10.3.5
'@storybook/react-vite': 10.3.5
'@streamdown/math': 1.0.2
'@svgdotjs/svg.js': 3.2.5
'@t3-oss/env-nextjs': 0.13.11

View File

@@ -1,8 +1,8 @@
import type { Preview } from '@storybook/react'
import type { Resource } from 'i18next'
import { ToastHost } from '@langgenius/dify-ui/toast'
import { withThemeByDataAttribute } from '@storybook/addon-themes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ToastHost } from '../app/components/base/ui/toast'
import { I18nClientProvider as I18N } from '../app/components/provider/i18n'
import commonEnUS from '../i18n/en-US/common.json'

View File

@@ -106,7 +106,7 @@ vi.mock('@/service/apps', () => ({
fetchAppDetailDirect: (...args: unknown[]) => mockFetchAppDetailDirect(...args),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: (...args: unknown[]) => mockToastError(...args),
},

View File

@@ -31,7 +31,7 @@ const toastMocks = vi.hoisted(() => ({
}))
const mockRouterPush = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
success: (message: string, options?: Record<string, unknown>) => toastMocks.mockNotify({ type: 'success', message, ...options }),
error: (message: string, options?: Record<string, unknown>) => toastMocks.mockNotify({ type: 'error', message, ...options }),

View File

@@ -8,10 +8,10 @@
* and workspace manager permission enforcement.
*/
import type { BasicPlan } from '@/app/components/billing/type'
import { toast, ToastHost } from '@langgenius/dify-ui/toast'
import { cleanup, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { toast, ToastHost } from '@/app/components/base/ui/toast'
import { ALL_PLANS } from '@/app/components/billing/config'
import { PlanRange } from '@/app/components/billing/pricing/plan-switcher/plan-range-switcher'
import CloudPlanItem from '@/app/components/billing/pricing/plans/cloud-plan-item'

View File

@@ -1,3 +1,4 @@
import { toast, ToastHost } from '@langgenius/dify-ui/toast'
/**
* Integration test: Self-Hosted Plan Flow
*
@@ -10,7 +11,6 @@
import { cleanup, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { toast, ToastHost } from '@/app/components/base/ui/toast'
import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '@/app/components/billing/config'
import SelfHostedPlanItem from '@/app/components/billing/pricing/plans/self-hosted-plan-item'
import { SelfHostedPlan } from '@/app/components/billing/type'

View File

@@ -33,7 +33,7 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
useInvalidDatasetList: () => vi.fn(),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
vi.mock('@langgenius/dify-ui/toast', () => ({
default: { notify: vi.fn() },
toast: {
success: vi.fn(),

View File

@@ -59,7 +59,7 @@ vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({
isReRankModelSelected: () => true,
}))
vi.mock('@/app/components/base/ui/toast', () => ({
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: mockToastError,
success: vi.fn(),
@@ -318,7 +318,7 @@ describe('Dataset Settings Flow - Cross-Module Configuration Cascade', () => {
describe('Form Submission Validation → All Fields Together', () => {
it('should reject empty name on save', async () => {
const { toast } = await import('@/app/components/base/ui/toast')
const { toast } = await import('@langgenius/dify-ui/toast')
const { result } = renderHook(() => useFormState())
act(() => {

View File

@@ -53,8 +53,8 @@ vi.mock('@/service/use-explore', () => ({
}),
}))
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => {
const actual = await importOriginal<typeof import('@langgenius/dify-ui/toast')>()
return {
...actual,
toast: {

View File

@@ -10,7 +10,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { checkForUpdates, fetchReleases, handleUpload } from '@/app/components/plugins/install-plugin/hooks'
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),

View File

@@ -20,7 +20,7 @@ const mockToast = {
promise: vi.fn(),
}
vi.mock('@/app/components/base/ui/toast', () => ({
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: mockToast,
}))
const mockEventEmitter = { emit: vi.fn() }

View File

@@ -133,7 +133,7 @@ vi.mock('@/app/components/base/drawer', () => ({
),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
vi.mock('@langgenius/dify-ui/toast', () => ({
default: { notify: vi.fn() },
toast: {
success: vi.fn(),

View File

@@ -5,6 +5,7 @@ import type { BlockEnum } from '@/app/components/workflow/types'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import type { App } from '@/types/app'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -12,7 +13,6 @@ import AppCard from '@/app/components/app/overview/app-card'
import TriggerCard from '@/app/components/app/overview/trigger-card'
import { useStore as useAppStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading'
import { toast } from '@/app/components/base/ui/toast'
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'

View File

@@ -2,12 +2,12 @@
import type { FC, JSX } from 'react'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import { cn } from '@langgenius/dify-ui/cn'
import { Switch } from '@langgenius/dify-ui/switch'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import ProviderConfigModal from './provider-config-modal'

View File

@@ -3,6 +3,7 @@ import type { FC } from 'react'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import type { TracingStatus } from '@/models/app'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiArrowDownDoubleLine,
RiEqualizer2Line,
@@ -14,7 +15,6 @@ import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
import Loading from '@/app/components/base/loading'
import { toast } from '@/app/components/base/ui/toast'
import Indicator from '@/app/components/header/indicator'
import { useAppContext } from '@/context/app-context'
import { usePathname } from '@/next/navigation'

View File

@@ -1,6 +1,17 @@
'use client'
import type { FC } from 'react'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useState } from 'react'
@@ -12,17 +23,6 @@ import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@/app/components/base/ui/alert-dialog'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
import { docURL } from './config'
import Field from './field'

View File

@@ -1,8 +1,9 @@
'use client'
import type { ButtonProps } from '@/app/components/base/ui/button'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { SiteInfo } from '@/models/share'
import type { HumanInputFormError } from '@/service/use-share'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiCheckboxCircleFill,
@@ -19,7 +20,6 @@ import ExpirationTime from '@/app/components/base/chat/chat/answer/human-input-c
import { getButtonStyle } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
import Loading from '@/app/components/base/loading'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { Button } from '@/app/components/base/ui/button'
import useDocumentTitle from '@/hooks/use-document-title'
import { useParams } from '@/next/navigation'
import { useGetHumanInputForm, useSubmitHumanInputForm } from '@/service/use-share'

View File

@@ -1,10 +1,10 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import Countdown from '@/app/components/signin/countdown'
import { useLocale } from '@/context/i18n'

View File

@@ -1,11 +1,11 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'

View File

@@ -1,12 +1,12 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCheckboxCircleFill } from '@remixicon/react'
import { useCountDown } from 'ahooks'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { validPassword } from '@/config'
import { useRouter, useSearchParams } from '@/next/navigation'
import { changeWebAppPasswordWithToken } from '@/service/common'

View File

@@ -1,11 +1,11 @@
'use client'
import type { FormEvent } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import Countdown from '@/app/components/signin/countdown'
import { useLocale } from '@/context/i18n'
import { useWebAppStore } from '@/context/web-app-context'

View File

@@ -1,9 +1,9 @@
'use client'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useEffect } from 'react'
import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading'
import { toast } from '@/app/components/base/ui/toast'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useRouter, useSearchParams } from '@/next/navigation'
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'

View File

@@ -1,9 +1,9 @@
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'

View File

@@ -1,10 +1,10 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'
import { useWebAppStore } from '@/context/web-app-context'

View File

@@ -1,10 +1,10 @@
'use client'
import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { useRouter, useSearchParams } from '@/next/navigation'
import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share'
import { SSOProtocol } from '@/types/feature'

View File

@@ -1,9 +1,13 @@
'use client'
import type { AvatarProps } from '@langgenius/dify-ui/avatar'
import type { Area } from 'react-easy-crop'
import type { OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
import type { AvatarProps } from '@/app/components/base/ui/avatar'
import type { ImageFile } from '@/types/app'
import { Avatar } from '@langgenius/dify-ui/avatar'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useState } from 'react'
@@ -12,10 +16,6 @@ import ImageInput from '@/app/components/base/app-icon-picker/ImageInput'
import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
import Divider from '@/app/components/base/divider'
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
import { Avatar } from '@/app/components/base/ui/avatar'
import { Button } from '@/app/components/base/ui/button'
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
import { toast } from '@/app/components/base/ui/toast'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
import { updateUserProfile } from '@/service/common'

View File

@@ -1,12 +1,12 @@
import type { ResponseError } from '@/service/fetch'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
import { toast } from '@/app/components/base/ui/toast'
import { useRouter } from '@/next/navigation'
import {
checkEmailExisted,

View File

@@ -1,6 +1,9 @@
'use client'
import type { IItem } from '@/app/components/header/account-setting/collapse'
import type { App } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiGraduationCapFill,
} from '@remixicon/react'
@@ -10,9 +13,6 @@ import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Input from '@/app/components/base/input'
import PremiumBadge from '@/app/components/base/premium-badge'
import { Button } from '@/app/components/base/ui/button'
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
import { toast } from '@/app/components/base/ui/toast'
import Collapse from '@/app/components/header/account-setting/collapse'
import { IS_CE_EDITION, validPassword } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'

View File

@@ -1,5 +1,6 @@
'use client'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import { Avatar } from '@langgenius/dify-ui/avatar'
import {
RiGraduationCapFill,
} from '@remixicon/react'
@@ -8,7 +9,6 @@ import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
import PremiumBadge from '@/app/components/base/premium-badge'
import { Avatar } from '@/app/components/base/ui/avatar'
import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'
import { useLogout, useUserProfile } from '@/service/use-common'

View File

@@ -1,8 +1,8 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import { useAppContext } from '@/context/app-context'
import Link from '@/next/link'
import { useSendDeleteAccountEmail } from '../state'

View File

@@ -1,10 +1,10 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CustomDialog from '@/app/components/base/dialog'
import Textarea from '@/app/components/base/textarea'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { useAppContext } from '@/context/app-context'
import { useRouter } from '@/next/navigation'
import { useLogout } from '@/service/use-common'

View File

@@ -1,8 +1,8 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Button } from '@/app/components/base/ui/button'
import Countdown from '@/app/components/signin/countdown'
import Link from '@/next/link'
import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state'

View File

@@ -1,9 +1,9 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { Button } from '@/app/components/base/ui/button'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useRouter } from '@/next/navigation'
import Avatar from './avatar'

View File

@@ -1,5 +1,8 @@
'use client'
import { Avatar } from '@langgenius/dify-ui/avatar'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiAccountCircleLine,
RiGlobalLine,
@@ -11,9 +14,6 @@ import * as React from 'react'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { Avatar } from '@/app/components/base/ui/avatar'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
import { useRouter, useSearchParams } from '@/next/navigation'

View File

@@ -1,9 +1,9 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { Button } from '@/app/components/base/ui/button'
import useDocumentTitle from '@/hooks/use-document-title'
import { useRouter, useSearchParams } from '@/next/navigation'

View File

@@ -19,7 +19,7 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/app/components/base/ui/dropdown-menu', () => {
vi.mock('@langgenius/dify-ui/dropdown-menu', () => {
const DropdownMenuContext = React.createContext<{ isOpen: boolean, setOpen: (open: boolean) => void } | null>(null)
const useDropdownMenuContext = () => {

View File

@@ -21,7 +21,7 @@ vi.mock('@/hooks/use-knowledge', () => ({
}),
}))
vi.mock('@/app/components/base/ui/dropdown-menu', () => {
vi.mock('@langgenius/dify-ui/dropdown-menu', () => {
const DropdownMenuContext = React.createContext<{ isOpen: boolean, setOpen: (open: boolean) => void } | null>(null)
const useDropdownMenuContext = () => {

View File

@@ -35,7 +35,7 @@ vi.mock('@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view',
),
}))
vi.mock('@/app/components/base/ui/button', () => ({
vi.mock('@langgenius/dify-ui/button', () => ({
Button: ({ children, onClick, className, size, variant }: {
children: React.ReactNode
onClick?: () => void

View File

@@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'
import * as React from 'react'
import AppOperations from '../app-operations'
vi.mock('../../../base/ui/button', () => ({
vi.mock('@langgenius/dify-ui/button', () => ({
Button: ({ children, onClick, className, size, variant, id, tabIndex, ...rest }: {
'children': React.ReactNode
'onClick'?: () => void
@@ -30,7 +30,7 @@ vi.mock('../../../base/ui/button', () => ({
),
}))
vi.mock('../../../base/ui/dropdown-menu', () => {
vi.mock('@langgenius/dify-ui/dropdown-menu', () => {
const DropdownMenuContext = React.createContext<{ isOpen: boolean, setOpen: (open: boolean) => void } | null>(null)
const useDropdownMenuContext = () => {

Some files were not shown because too many files have changed in this diff Show More