diff --git a/packages/dify-ui/.gitignore b/packages/dify-ui/.gitignore
new file mode 100644
index 0000000000..1eae0cf670
--- /dev/null
+++ b/packages/dify-ui/.gitignore
@@ -0,0 +1,2 @@
+dist/
+node_modules/
diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json
new file mode 100644
index 0000000000..9b2571d7d3
--- /dev/null
+++ b/packages/dify-ui/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@langgenius/dify-ui",
+ "private": true,
+ "version": "0.0.0-private",
+ "type": "module",
+ "files": [
+ "dist"
+ ],
+ "sideEffects": [
+ "**/*.css"
+ ],
+ "exports": {
+ "./context-menu": {
+ "types": "./dist/context-menu/index.d.ts",
+ "import": "./dist/context-menu/index.js"
+ },
+ "./dropdown-menu": {
+ "types": "./dist/dropdown-menu/index.d.ts",
+ "import": "./dist/dropdown-menu/index.js"
+ },
+ "./tailwind-preset": {
+ "types": "./dist/tailwind-preset.d.ts",
+ "import": "./dist/tailwind-preset.js"
+ },
+ "./styles.css": "./dist/styles.css",
+ "./markdown.css": "./dist/markdown.css",
+ "./themes/light.css": "./dist/themes/light.css",
+ "./themes/dark.css": "./dist/themes/dark.css",
+ "./themes/manual-light.css": "./dist/themes/manual-light.css",
+ "./themes/manual-dark.css": "./dist/themes/manual-dark.css",
+ "./themes/markdown-light.css": "./dist/themes/markdown-light.css",
+ "./themes/markdown-dark.css": "./dist/themes/markdown-dark.css",
+ "./tokens/tailwind-theme-var-define": {
+ "types": "./dist/tokens/tailwind-theme-var-define.d.ts",
+ "import": "./dist/tokens/tailwind-theme-var-define.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "node ./scripts/build.mjs",
+ "prepack": "pnpm build",
+ "test": "vp test",
+ "test:watch": "vp test --watch",
+ "type-check": "tsc -p tsconfig.json --noEmit"
+ },
+ "peerDependencies": {
+ "react": "catalog:",
+ "react-dom": "catalog:"
+ },
+ "dependencies": {
+ "@base-ui/react": "catalog:",
+ "@dify/iconify-collections": "workspace:*",
+ "@egoist/tailwindcss-icons": "catalog:",
+ "@iconify-json/heroicons": "catalog:",
+ "@iconify-json/ri": "catalog:",
+ "@remixicon/react": "catalog:",
+ "@tailwindcss/typography": "catalog:",
+ "clsx": "catalog:",
+ "tailwind-merge": "catalog:"
+ },
+ "devDependencies": {
+ "@storybook/react": "catalog:",
+ "@testing-library/jest-dom": "catalog:",
+ "@testing-library/react": "catalog:",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "happy-dom": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:",
+ "tailwindcss": "catalog:",
+ "typescript": "catalog:",
+ "vite": "catalog:",
+ "vite-plus": "catalog:",
+ "vitest": "catalog:"
+ }
+}
diff --git a/packages/dify-ui/scripts/build.mjs b/packages/dify-ui/scripts/build.mjs
new file mode 100644
index 0000000000..6698e754af
--- /dev/null
+++ b/packages/dify-ui/scripts/build.mjs
@@ -0,0 +1,31 @@
+import { cp, mkdir, rm } from 'node:fs/promises'
+import { spawnSync } from 'node:child_process'
+import { dirname, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
+const distDir = resolve(packageRoot, 'dist')
+
+await rm(distDir, { recursive: true, force: true })
+
+const tsc = spawnSync('pnpm', ['exec', 'tsc', '-p', 'tsconfig.build.json'], {
+ cwd: packageRoot,
+ stdio: 'inherit',
+})
+
+if (tsc.status !== 0)
+ process.exit(tsc.status ?? 1)
+
+await mkdir(distDir, { recursive: true })
+
+await cp(resolve(packageRoot, 'src/styles.css'), resolve(packageRoot, 'dist/styles.css'))
+await cp(resolve(packageRoot, 'src/markdown.css'), resolve(packageRoot, 'dist/markdown.css'))
+await cp(resolve(packageRoot, 'src/styles'), resolve(packageRoot, 'dist/styles'), {
+ force: true,
+ recursive: true,
+})
+
+await cp(resolve(packageRoot, 'src/themes'), resolve(packageRoot, 'dist/themes'), {
+ force: true,
+ recursive: true,
+})
diff --git a/web/app/components/base/ui/context-menu/__tests__/index.spec.tsx b/packages/dify-ui/src/context-menu/__tests__/index.spec.tsx
similarity index 100%
rename from web/app/components/base/ui/context-menu/__tests__/index.spec.tsx
rename to packages/dify-ui/src/context-menu/__tests__/index.spec.tsx
diff --git a/web/app/components/base/ui/context-menu/index.stories.tsx b/packages/dify-ui/src/context-menu/index.stories.tsx
similarity index 92%
rename from web/app/components/base/ui/context-menu/index.stories.tsx
rename to packages/dify-ui/src/context-menu/index.stories.tsx
index 7c57a81c65..2932a10ee3 100644
--- a/web/app/components/base/ui/context-menu/index.stories.tsx
+++ b/packages/dify-ui/src/context-menu/index.stories.tsx
@@ -1,4 +1,10 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite'
+import type { Meta, StoryObj } from '@storybook/react'
+import {
+ RiDeleteBinLine,
+ RiFileCopyLine,
+ RiPencilLine,
+ RiShareLine,
+} from '@remixicon/react'
import { useState } from 'react'
import {
ContextMenu,
@@ -17,7 +23,7 @@ import {
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
-} from '.'
+} from './index'
const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: string }) => (
-
+
Rename
-
+
Duplicate
-
+
Share
@@ -206,7 +212,7 @@ export const Complex: Story = {
-
+
Delete
diff --git a/web/app/components/base/ui/context-menu/index.tsx b/packages/dify-ui/src/context-menu/index.tsx
similarity index 85%
rename from web/app/components/base/ui/context-menu/index.tsx
rename to packages/dify-ui/src/context-menu/index.tsx
index 5a0f580ca4..19e0f5ed95 100644
--- a/web/app/components/base/ui/context-menu/index.tsx
+++ b/packages/dify-ui/src/context-menu/index.tsx
@@ -1,8 +1,10 @@
'use client'
-import type { Placement } from '@/app/components/base/ui/placement'
+import type { Placement } from '../internal/placement.js'
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu'
+import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'
import * as React from 'react'
+import { cn } from '../internal/cn.js'
import {
menuBackdropClassName,
menuGroupLabelClassName,
@@ -11,9 +13,8 @@ import {
menuPopupBaseClassName,
menuRowClassName,
menuSeparatorClassName,
-} from '@/app/components/base/ui/menu-shared'
-import { parsePlacement } from '@/app/components/base/ui/placement'
-import { cn } from '@/utils/classnames'
+} from '../internal/menu-shared.js'
+import { parsePlacement } from '../internal/placement.js'
export const ContextMenu = BaseContextMenu.Root
export const ContextMenuTrigger = BaseContextMenu.Trigger
@@ -44,11 +45,11 @@ type ContextMenuPopupRenderProps = Required
- {children ?? }
+ {children ?? }
)
}
@@ -204,7 +205,7 @@ export function ContextMenuCheckboxItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
-
+
)
}
@@ -218,7 +219,7 @@ export function ContextMenuRadioItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
-
+
)
}
@@ -239,20 +240,20 @@ export function ContextMenuSubTrigger({
{...props}
>
{children}
-
+
)
}
type ContextMenuSubContentProps = {
children: React.ReactNode
- placement?: Placement
- sideOffset?: number
- alignOffset?: number
- className?: string
- popupClassName?: string
- positionerProps?: ContextMenuContentProps['positionerProps']
- popupProps?: ContextMenuContentProps['popupProps']
+ placement?: Placement | undefined
+ sideOffset?: number | undefined
+ alignOffset?: number | undefined
+ className?: string | undefined
+ popupClassName?: string | undefined
+ positionerProps?: ContextMenuContentProps['positionerProps'] | undefined
+ popupProps?: ContextMenuContentProps['popupProps'] | undefined
}
export function ContextMenuSubContent({
@@ -300,3 +301,5 @@ export function ContextMenuSeparator({
/>
)
}
+
+export type { Placement }
diff --git a/web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx b/packages/dify-ui/src/dropdown-menu/__tests__/index.spec.tsx
similarity index 98%
rename from web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx
rename to packages/dify-ui/src/dropdown-menu/__tests__/index.spec.tsx
index b6772e5ad0..7231a51fd1 100644
--- a/web/app/components/base/ui/dropdown-menu/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/dropdown-menu/__tests__/index.spec.tsx
@@ -1,7 +1,6 @@
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
-import Link from '@/next/link'
import {
DropdownMenu,
DropdownMenuContent,
@@ -14,20 +13,20 @@ import {
DropdownMenuTrigger,
} from '../index'
-vi.mock('@/next/link', () => ({
- default: ({
- href,
- children,
- ...props
- }: {
- href: string
- children?: ReactNode
- } & Omit, 'href'>) => (
+function MockLink({
+ href,
+ children,
+ ...props
+}: {
+ href: string
+ children?: ReactNode
+} & Omit, 'href'>) {
+ return (
{children}
- ),
-}))
+ )
+}
describe('dropdown-menu wrapper', () => {
describe('DropdownMenuContent', () => {
@@ -301,7 +300,7 @@ describe('dropdown-menu wrapper', () => {
Open
}
+ render={}
aria-label="account link"
>
Account settings
diff --git a/web/app/components/base/ui/dropdown-menu/index.stories.tsx b/packages/dify-ui/src/dropdown-menu/index.stories.tsx
similarity index 87%
rename from web/app/components/base/ui/dropdown-menu/index.stories.tsx
rename to packages/dify-ui/src/dropdown-menu/index.stories.tsx
index 0e2f21dd54..8058ab1103 100644
--- a/web/app/components/base/ui/dropdown-menu/index.stories.tsx
+++ b/packages/dify-ui/src/dropdown-menu/index.stories.tsx
@@ -1,4 +1,15 @@
-import type { Meta, StoryObj } from '@storybook/nextjs-vite'
+import type { Meta, StoryObj } from '@storybook/react'
+import {
+ RiArchiveLine,
+ RiChat1Line,
+ RiDeleteBinLine,
+ RiFileCopyLine,
+ RiLink,
+ RiLockLine,
+ RiMailLine,
+ RiPencilLine,
+ RiShareLine,
+} from '@remixicon/react'
import { useState } from 'react'
import {
DropdownMenu,
@@ -17,7 +28,7 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
-} from '.'
+} from './index'
const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => (
-
+
Edit
-
+
Duplicate
-
+
Archive
-
+
Delete
@@ -262,35 +273,35 @@ const ComplexDemo = () => {
Edit
-
+
Rename
-
+
Duplicate
-
+
Move to Workspace
-
+
Share
-
+
Email
-
+
Slack
-
+
Copy Link
@@ -315,13 +326,13 @@ const ComplexDemo = () => {
-
+
Show Archived
-
+
Delete
diff --git a/web/app/components/base/ui/dropdown-menu/index.tsx b/packages/dify-ui/src/dropdown-menu/index.tsx
similarity index 84%
rename from web/app/components/base/ui/dropdown-menu/index.tsx
rename to packages/dify-ui/src/dropdown-menu/index.tsx
index 13c2dab626..8bfadb9935 100644
--- a/web/app/components/base/ui/dropdown-menu/index.tsx
+++ b/packages/dify-ui/src/dropdown-menu/index.tsx
@@ -1,8 +1,10 @@
'use client'
-import type { Placement } from '@/app/components/base/ui/placement'
+import type { Placement } from '../internal/placement.js'
import { Menu } from '@base-ui/react/menu'
+import { RiArrowRightSLine, RiCheckLine } from '@remixicon/react'
import * as React from 'react'
+import { cn } from '../internal/cn.js'
import {
menuGroupLabelClassName,
menuIndicatorClassName,
@@ -10,9 +12,8 @@ import {
menuPopupBaseClassName,
menuRowClassName,
menuSeparatorClassName,
-} from '@/app/components/base/ui/menu-shared'
-import { parsePlacement } from '@/app/components/base/ui/placement'
-import { cn } from '@/utils/classnames'
+} from '../internal/menu-shared.js'
+import { parsePlacement } from '../internal/placement.js'
export const DropdownMenu = Menu.Root
export const DropdownMenuPortal = Menu.Portal
@@ -42,7 +43,7 @@ export function DropdownMenuRadioItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
-
+
)
}
@@ -68,7 +69,7 @@ export function DropdownMenuCheckboxItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
-
+
)
}
@@ -106,10 +107,10 @@ type DropdownMenuPopupRenderProps = Required
{children}
-
+
)
}
type DropdownMenuSubContentProps = {
children: React.ReactNode
- placement?: Placement
- sideOffset?: number
- alignOffset?: number
- className?: string
- popupClassName?: string
- positionerProps?: DropdownMenuContentProps['positionerProps']
- popupProps?: DropdownMenuContentProps['popupProps']
+ placement?: Placement | undefined
+ sideOffset?: number | undefined
+ alignOffset?: number | undefined
+ className?: string | undefined
+ popupClassName?: string | undefined
+ positionerProps?: DropdownMenuContentProps['positionerProps'] | undefined
+ popupProps?: DropdownMenuContentProps['popupProps'] | undefined
}
export function DropdownMenuSubContent({
@@ -272,3 +273,5 @@ export function DropdownMenuSeparator({
/>
)
}
+
+export type { Placement }
diff --git a/packages/dify-ui/src/internal/cn.ts b/packages/dify-ui/src/internal/cn.ts
new file mode 100644
index 0000000000..abba253f04
--- /dev/null
+++ b/packages/dify-ui/src/internal/cn.ts
@@ -0,0 +1,7 @@
+import type { ClassValue } from 'clsx'
+import { clsx } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/packages/dify-ui/src/internal/menu-shared.ts b/packages/dify-ui/src/internal/menu-shared.ts
new file mode 100644
index 0000000000..b0c379dae2
--- /dev/null
+++ b/packages/dify-ui/src/internal/menu-shared.ts
@@ -0,0 +1,7 @@
+export const menuRowClassName = 'mx-1 flex h-8 cursor-pointer select-none items-center gap-1 rounded-lg px-2 outline-hidden data-highlighted:bg-state-base-hover data-disabled:cursor-not-allowed data-disabled:opacity-30'
+export const menuIndicatorClassName = 'ml-auto flex shrink-0 items-center text-text-accent'
+export const menuGroupLabelClassName = 'px-3 pb-0.5 pt-1 text-text-tertiary system-xs-medium-uppercase'
+export const menuSeparatorClassName = 'my-1 h-px bg-divider-subtle'
+export const menuPopupBaseClassName = 'max-h-(--available-height) overflow-y-auto overflow-x-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur py-1 text-sm text-text-secondary shadow-lg outline-hidden focus:outline-hidden focus-visible:outline-hidden backdrop-blur-[5px]'
+export const menuPopupAnimationClassName = 'origin-(--transform-origin) transition-[transform,scale,opacity] data-ending-style:scale-95 data-starting-style:scale-95 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none'
+export const menuBackdropClassName = 'fixed inset-0 z-1002 bg-transparent transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none'
diff --git a/packages/dify-ui/src/internal/placement.ts b/packages/dify-ui/src/internal/placement.ts
new file mode 100644
index 0000000000..fbcb6f00e3
--- /dev/null
+++ b/packages/dify-ui/src/internal/placement.ts
@@ -0,0 +1,25 @@
+type Side = 'top' | 'bottom' | 'left' | 'right'
+type Align = 'start' | 'center' | 'end'
+
+export type Placement
+ = 'top'
+ | 'top-start'
+ | 'top-end'
+ | 'right'
+ | 'right-start'
+ | 'right-end'
+ | 'bottom'
+ | 'bottom-start'
+ | 'bottom-end'
+ | 'left'
+ | 'left-start'
+ | 'left-end'
+
+export function parsePlacement(placement: Placement): { side: Side, align: Align } {
+ const [side, align] = placement.split('-') as [Side, Align | undefined]
+
+ return {
+ side,
+ align: align ?? 'center',
+ }
+}
diff --git a/packages/dify-ui/src/markdown.css b/packages/dify-ui/src/markdown.css
new file mode 100644
index 0000000000..317c13e280
--- /dev/null
+++ b/packages/dify-ui/src/markdown.css
@@ -0,0 +1,2 @@
+@import './themes/markdown-light.css';
+@import './themes/markdown-dark.css';
diff --git a/packages/dify-ui/src/styles.css b/packages/dify-ui/src/styles.css
new file mode 100644
index 0000000000..063bef1afe
--- /dev/null
+++ b/packages/dify-ui/src/styles.css
@@ -0,0 +1,7 @@
+@import './themes/light.css' layer(base);
+@import './themes/dark.css' layer(base);
+@import './themes/manual-light.css' layer(base);
+@import './themes/manual-dark.css' layer(base);
+@import './styles/tokens.css';
+
+@source './**/*.{js,mjs}';
diff --git a/packages/dify-ui/src/styles/tokens.css b/packages/dify-ui/src/styles/tokens.css
new file mode 100644
index 0000000000..c38fc81c0c
--- /dev/null
+++ b/packages/dify-ui/src/styles/tokens.css
@@ -0,0 +1,713 @@
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+@utility system-kbd {
+ /* font define start */
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-2xs-regular-uppercase {
+ font-size: 10px;
+ font-weight: 400;
+ text-transform: uppercase;
+ line-height: 12px;
+
+ /* border radius end */
+}
+
+@utility system-2xs-regular {
+ font-size: 10px;
+ font-weight: 400;
+ line-height: 12px;
+
+ /* border radius end */
+}
+
+@utility system-2xs-medium {
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 12px;
+
+ /* border radius end */
+}
+
+@utility system-2xs-medium-uppercase {
+ font-size: 10px;
+ font-weight: 500;
+ text-transform: uppercase;
+ line-height: 12px;
+
+ /* border radius end */
+}
+
+@utility system-2xs-semibold-uppercase {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ line-height: 12px;
+
+ /* border radius end */
+}
+
+@utility system-xs-regular {
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-xs-regular-uppercase {
+ font-size: 12px;
+ font-weight: 400;
+ text-transform: uppercase;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-xs-medium {
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-xs-medium-uppercase {
+ font-size: 12px;
+ font-weight: 500;
+ text-transform: uppercase;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-xs-semibold {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-xs-semibold-uppercase {
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-sm-regular {
+ font-size: 13px;
+ font-weight: 400;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-sm-medium {
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-sm-medium-uppercase {
+ font-size: 13px;
+ font-weight: 500;
+ text-transform: uppercase;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-sm-semibold {
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-sm-semibold-uppercase {
+ font-size: 13px;
+ font-weight: 600;
+ text-transform: uppercase;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility system-md-regular {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility system-md-medium {
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility system-md-semibold {
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility system-md-semibold-uppercase {
+ font-size: 14px;
+ font-weight: 600;
+ text-transform: uppercase;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility system-xl-regular {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 24px;
+
+ /* border radius end */
+}
+
+@utility system-xl-medium {
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+
+ /* border radius end */
+}
+
+@utility system-xl-semibold {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 24px;
+
+ /* border radius end */
+}
+
+@utility code-xs-regular {
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility code-xs-semibold {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility code-sm-regular {
+ font-size: 13px;
+ font-weight: 400;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility code-sm-semibold {
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility code-md-regular {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility code-md-semibold {
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility body-xs-light {
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility body-xs-regular {
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility body-xs-medium {
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility body-sm-light {
+ font-size: 13px;
+ font-weight: 300;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility body-sm-regular {
+ font-size: 13px;
+ font-weight: 400;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility body-sm-medium {
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility body-md-light {
+ font-size: 14px;
+ font-weight: 300;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility body-md-regular {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility body-md-medium {
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility body-lg-light {
+ font-size: 15px;
+ font-weight: 300;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility body-lg-regular {
+ font-size: 15px;
+ font-weight: 400;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility body-lg-medium {
+ font-size: 15px;
+ font-weight: 500;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility body-xl-regular {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 24px;
+
+ /* border radius end */
+}
+
+@utility body-xl-medium {
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+
+ /* border radius end */
+}
+
+@utility body-xl-light {
+ font-size: 16px;
+ font-weight: 300;
+ line-height: 24px;
+
+ /* border radius end */
+}
+
+@utility body-2xl-light {
+ font-size: 18px;
+ font-weight: 300;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility body-2xl-regular {
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility body-2xl-medium {
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 1.5;
+
+ /* border radius end */
+}
+
+@utility title-xs-semi-bold {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility title-xs-bold {
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility title-sm-semi-bold {
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility title-sm-bold {
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 16px;
+
+ /* border radius end */
+}
+
+@utility title-md-semi-bold {
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility title-md-bold {
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 20px;
+
+ /* border radius end */
+}
+
+@utility title-lg-semi-bold {
+ font-size: 15px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-lg-bold {
+ font-size: 15px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-xl-semi-bold {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-xl-bold {
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-2xl-semi-bold {
+ font-size: 18px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-2xl-bold {
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-3xl-semi-bold {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-3xl-bold {
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-4xl-semi-bold {
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-4xl-bold {
+ font-size: 24px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-5xl-semi-bold {
+ font-size: 30px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-5xl-bold {
+ font-size: 30px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-6xl-semi-bold {
+ font-size: 36px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-6xl-bold {
+ font-size: 36px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-7xl-semi-bold {
+ font-size: 48px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-7xl-bold {
+ font-size: 48px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-8xl-semi-bold {
+ font-size: 60px;
+ font-weight: 600;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility title-8xl-bold {
+ font-size: 60px;
+ font-weight: 700;
+ line-height: 1.2;
+
+ /* border radius end */
+}
+
+@utility radius-2xs {
+ /* font define end */
+
+ /* border radius start */
+ border-radius: 2px;
+
+ /* border radius end */
+}
+
+@utility radius-xs {
+ border-radius: 4px;
+
+ /* border radius end */
+}
+
+@utility radius-sm {
+ border-radius: 6px;
+
+ /* border radius end */
+}
+
+@utility radius-md {
+ border-radius: 8px;
+
+ /* border radius end */
+}
+
+@utility radius-lg {
+ border-radius: 10px;
+
+ /* border radius end */
+}
+
+@utility radius-xl {
+ border-radius: 12px;
+
+ /* border radius end */
+}
+
+@utility radius-2xl {
+ border-radius: 16px;
+
+ /* border radius end */
+}
+
+@utility radius-3xl {
+ border-radius: 20px;
+
+ /* border radius end */
+}
+
+@utility radius-4xl {
+ border-radius: 24px;
+
+ /* border radius end */
+}
+
+@utility radius-5xl {
+ border-radius: 24px;
+
+ /* border radius end */
+}
+
+@utility radius-6xl {
+ border-radius: 28px;
+
+ /* border radius end */
+}
+
+@utility radius-7xl {
+ border-radius: 32px;
+
+ /* border radius end */
+}
+
+@utility radius-8xl {
+ border-radius: 40px;
+
+ /* border radius end */
+}
+
+@utility radius-9xl {
+ border-radius: 48px;
+
+ /* border radius end */
+}
+
+@utility radius-full {
+ border-radius: 64px;
+
+ /* border radius end */
+}
+
+@utility no-scrollbar {
+ /* Hide scrollbar for Chrome, Safari and Opera */
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ /* Hide scrollbar for IE, Edge and Firefox */
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+@utility no-spinner {
+ /* Hide arrows from number input */
+ &::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ -moz-appearance: textfield;
+}
+
diff --git a/web/tailwind-common-config.ts b/packages/dify-ui/src/tailwind-preset.ts
similarity index 96%
rename from web/tailwind-common-config.ts
rename to packages/dify-ui/src/tailwind-preset.ts
index db50f2457b..b008051ebe 100644
--- a/web/tailwind-common-config.ts
+++ b/packages/dify-ui/src/tailwind-preset.ts
@@ -1,8 +1,10 @@
import { icons as customPublicIcons } from '@dify/iconify-collections/custom-public'
import { icons as customVenderIcons } from '@dify/iconify-collections/custom-vender'
-import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons'
+import { icons as heroicons } from '@iconify-json/heroicons'
+import { icons as remixIcons } from '@iconify-json/ri'
+import { iconsPlugin } from '@egoist/tailwindcss-icons'
import tailwindTypography from '@tailwindcss/typography'
-import tailwindThemeVarDefine from './themes/tailwind-theme-var-define'
+import tailwindThemeVarDefine from './tokens/tailwind-theme-var-define.js'
import typography from './typography.js'
const config = {
@@ -151,7 +153,8 @@ const config = {
tailwindTypography,
iconsPlugin({
collections: {
- ...getIconCollections(['heroicons', 'ri']),
+ heroicons,
+ ri: remixIcons,
'custom-public': customPublicIcons,
'custom-vender': customVenderIcons,
},
diff --git a/web/themes/dark.css b/packages/dify-ui/src/themes/dark.css
similarity index 100%
rename from web/themes/dark.css
rename to packages/dify-ui/src/themes/dark.css
diff --git a/web/themes/light.css b/packages/dify-ui/src/themes/light.css
similarity index 100%
rename from web/themes/light.css
rename to packages/dify-ui/src/themes/light.css
diff --git a/web/themes/manual-dark.css b/packages/dify-ui/src/themes/manual-dark.css
similarity index 100%
rename from web/themes/manual-dark.css
rename to packages/dify-ui/src/themes/manual-dark.css
diff --git a/web/themes/manual-light.css b/packages/dify-ui/src/themes/manual-light.css
similarity index 100%
rename from web/themes/manual-light.css
rename to packages/dify-ui/src/themes/manual-light.css
diff --git a/web/themes/markdown-dark.css b/packages/dify-ui/src/themes/markdown-dark.css
similarity index 100%
rename from web/themes/markdown-dark.css
rename to packages/dify-ui/src/themes/markdown-dark.css
diff --git a/web/themes/markdown-light.css b/packages/dify-ui/src/themes/markdown-light.css
similarity index 100%
rename from web/themes/markdown-light.css
rename to packages/dify-ui/src/themes/markdown-light.css
diff --git a/web/themes/tailwind-theme-var-define.ts b/packages/dify-ui/src/tokens/tailwind-theme-var-define.ts
similarity index 100%
rename from web/themes/tailwind-theme-var-define.ts
rename to packages/dify-ui/src/tokens/tailwind-theme-var-define.ts
diff --git a/packages/dify-ui/src/typography.d.ts b/packages/dify-ui/src/typography.d.ts
new file mode 100644
index 0000000000..db1b269684
--- /dev/null
+++ b/packages/dify-ui/src/typography.d.ts
@@ -0,0 +1,3 @@
+declare const typography: (helpers: { theme: (path: string) => unknown }) => Record
+
+export default typography
diff --git a/web/typography.js b/packages/dify-ui/src/typography.js
similarity index 100%
rename from web/typography.js
rename to packages/dify-ui/src/typography.js
diff --git a/packages/dify-ui/tailwind.config.ts b/packages/dify-ui/tailwind.config.ts
new file mode 100644
index 0000000000..8185e86928
--- /dev/null
+++ b/packages/dify-ui/tailwind.config.ts
@@ -0,0 +1,8 @@
+import difyUiTailwindPreset from './src/tailwind-preset'
+
+const config = {
+ content: [],
+ ...difyUiTailwindPreset,
+}
+
+export default config
diff --git a/packages/dify-ui/tsconfig.build.json b/packages/dify-ui/tsconfig.build.json
new file mode 100644
index 0000000000..50f5155973
--- /dev/null
+++ b/packages/dify-ui/tsconfig.build.json
@@ -0,0 +1,21 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "noEmit": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "src/**/*.js"
+ ],
+ "exclude": [
+ "src/**/*.stories.tsx",
+ "src/**/__tests__/**"
+ ]
+}
diff --git a/packages/dify-ui/tsconfig.json b/packages/dify-ui/tsconfig.json
new file mode 100644
index 0000000000..67c2d163e2
--- /dev/null
+++ b/packages/dify-ui/tsconfig.json
@@ -0,0 +1,38 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "jsx": "react-jsx",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "es2022"
+ ],
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "moduleDetection": "force",
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "skipLibCheck": true,
+ "types": [
+ "node",
+ "vitest/globals",
+ "@testing-library/jest-dom"
+ ]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "src/**/*.js",
+ "scripts/**/*.mjs",
+ "vite.config.ts",
+ "vitest.setup.ts"
+ ]
+}
diff --git a/packages/dify-ui/vite.config.ts b/packages/dify-ui/vite.config.ts
new file mode 100644
index 0000000000..d6976e74b3
--- /dev/null
+++ b/packages/dify-ui/vite.config.ts
@@ -0,0 +1,11 @@
+import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite-plus'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'happy-dom',
+ globals: true,
+ setupFiles: ['./vitest.setup.ts'],
+ },
+})
diff --git a/packages/dify-ui/vitest.setup.ts b/packages/dify-ui/vitest.setup.ts
new file mode 100644
index 0000000000..8ae19714ed
--- /dev/null
+++ b/packages/dify-ui/vitest.setup.ts
@@ -0,0 +1,39 @@
+import { cleanup } from '@testing-library/react'
+import '@testing-library/jest-dom/vitest'
+import { afterEach } from 'vitest'
+
+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 = ''
+ readonly scrollMargin = ''
+ readonly thresholds: ReadonlyArray = []
+ constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {}
+ observe(_target: Element) {}
+ unobserve(_target: Element) {}
+ disconnect() {}
+ takeRecords(): IntersectionObserverEntry[] {
+ return []
+ }
+ }
+}
+
+afterEach(() => {
+ cleanup()
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7a44b621b1..dd902d5c9f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -640,6 +640,82 @@ importers:
specifier: 'catalog:'
version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
+ packages/dify-ui:
+ dependencies:
+ '@base-ui/react':
+ specifier: 'catalog:'
+ version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@dify/iconify-collections':
+ specifier: workspace:*
+ version: link:../iconify-collections
+ '@egoist/tailwindcss-icons':
+ specifier: 'catalog:'
+ version: 1.9.2(tailwindcss@4.2.2)
+ '@iconify-json/heroicons':
+ specifier: 'catalog:'
+ version: 1.2.3
+ '@iconify-json/ri':
+ specifier: 'catalog:'
+ version: 1.2.10
+ '@remixicon/react':
+ specifier: 'catalog:'
+ version: 4.9.0(react@19.2.4)
+ '@tailwindcss/typography':
+ specifier: 'catalog:'
+ version: 0.5.19(tailwindcss@4.2.2)
+ clsx:
+ specifier: 'catalog:'
+ version: 2.1.1
+ tailwind-merge:
+ specifier: 'catalog:'
+ version: 3.5.0
+ devDependencies:
+ '@storybook/react':
+ specifier: 'catalog:'
+ version: 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.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.4(react@19.2.4))(react@19.2.4)
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.5.0
+ '@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.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))
+ happy-dom:
+ specifier: 'catalog:'
+ version: 20.8.9
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ tailwindcss:
+ specifier: 'catalog:'
+ version: 4.2.2
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+ vite:
+ specifier: npm:@voidzero-dev/vite-plus-core@0.1.14
+ version: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
+ vite-plus:
+ specifier: 'catalog:'
+ version: 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
+ vitest:
+ specifier: npm:@voidzero-dev/vite-plus-test@0.1.14
+ version: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)'
+
packages/iconify-collections:
devDependencies:
iconify-import-svg:
@@ -702,6 +778,9 @@ importers:
'@heroicons/react':
specifier: 'catalog:'
version: 2.2.0(react@19.2.4)
+ '@langgenius/dify-ui':
+ specifier: workspace:*
+ version: link:../packages/dify-ui
'@lexical/code':
specifier: npm:lexical-code-no-prism@0.41.0
version: lexical-code-no-prism@0.41.0(@lexical/utils@0.42.0)(lexical@0.42.0)
diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts
index 918860c786..f7447d797e 100644
--- a/web/.storybook/main.ts
+++ b/web/.storybook/main.ts
@@ -1,7 +1,10 @@
import type { StorybookConfig } from '@storybook/nextjs-vite'
const config: StorybookConfig = {
- stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ stories: [
+ '../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)',
+ '../../packages/dify-ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
+ ],
addons: [
// Not working with Storybook Vite framework
// '@storybook/addon-onboarding',
diff --git a/web/app/components/base/portal-to-follow-elem/index.tsx b/web/app/components/base/portal-to-follow-elem/index.tsx
index 7d4f6baa9b..357b1ca1c7 100644
--- a/web/app/components/base/portal-to-follow-elem/index.tsx
+++ b/web/app/components/base/portal-to-follow-elem/index.tsx
@@ -6,7 +6,7 @@
*
* Migration guide:
* - Tooltip → `@/app/components/base/ui/tooltip`
- * - Menu/Dropdown → `@/app/components/base/ui/dropdown-menu`
+ * - Menu/Dropdown → `@langgenius/dify-ui/dropdown-menu`
* - Popover → `@/app/components/base/ui/popover`
* - Dialog/Modal → `@/app/components/base/ui/dialog`
* - Select → `@/app/components/base/ui/select`
diff --git a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx
index 01f3460e7d..dfd35cf867 100644
--- a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx
+++ b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx
@@ -1,7 +1,7 @@
import type { ModalContextState } from '@/context/modal-context'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
import { toast } from '@/app/components/base/ui/toast'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
diff --git a/web/app/components/header/account-dropdown/__tests__/support.spec.tsx b/web/app/components/header/account-dropdown/__tests__/support.spec.tsx
index a826038ad0..38be78abf5 100644
--- a/web/app/components/header/account-dropdown/__tests__/support.spec.tsx
+++ b/web/app/components/header/account-dropdown/__tests__/support.spec.tsx
@@ -1,7 +1,7 @@
import type { AppContextValue } from '@/context/app-context'
-import { fireEvent, render, screen } from '@testing-library/react'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
+import { fireEvent, render, screen } from '@testing-library/react'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
diff --git a/web/app/components/header/account-dropdown/compliance.tsx b/web/app/components/header/account-dropdown/compliance.tsx
index 41c1e81910..c149da26e0 100644
--- a/web/app/components/header/account-dropdown/compliance.tsx
+++ b/web/app/components/header/account-dropdown/compliance.tsx
@@ -1,8 +1,8 @@
import type { ReactNode } from 'react'
+import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@langgenius/dify-ui/dropdown-menu'
import { useMutation } from '@tanstack/react-query'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
-import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
import { toast } from '@/app/components/base/ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { Plan } from '@/app/components/billing/type'
diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx
index f5b0352a40..5a800a4310 100644
--- a/web/app/components/header/account-dropdown/index.tsx
+++ b/web/app/components/header/account-dropdown/index.tsx
@@ -1,13 +1,13 @@
'use client'
import type { MouseEventHandler, ReactNode } from 'react'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
import { Avatar } from '@/app/components/base/avatar'
import PremiumBadge from '@/app/components/base/premium-badge'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
diff --git a/web/app/components/header/account-dropdown/support.tsx b/web/app/components/header/account-dropdown/support.tsx
index 97d87ac7e1..9a7023c415 100644
--- a/web/app/components/header/account-dropdown/support.tsx
+++ b/web/app/components/header/account-dropdown/support.tsx
@@ -1,5 +1,5 @@
+import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@langgenius/dify-ui/dropdown-menu'
import { useTranslation } from 'react-i18next'
-import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils'
import { Plan } from '@/app/components/billing/type'
import { SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config'
diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx
index f96e7d02a5..e1503244e3 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx
@@ -1,15 +1,10 @@
import type { ReactNode } from 'react'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Brush01 } from '@/app/components/base/icons/src/vender/solid/editor'
import { Scales02 } from '@/app/components/base/icons/src/vender/solid/FinanceAndECommerce'
import { Target04 } from '@/app/components/base/icons/src/vender/solid/general'
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@/app/components/base/ui/dropdown-menu'
import { TONE_LIST } from '@/config'
const toneI18nKeyMap = {
diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx
index b8d84763df..f2d25ea231 100644
--- a/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx
@@ -14,7 +14,7 @@ vi.mock('@/utils/classnames', () => ({
cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '),
}))
-vi.mock('@/app/components/base/ui/dropdown-menu', () => ({
+vi.mock('@langgenius/dify-ui/dropdown-menu', () => ({
DropdownMenu: ({ children, open }: { children: ReactNode, open: boolean }) => (
{children}
),
diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx
index b056f97793..0dd5cc8f0d 100644
--- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx
@@ -1,15 +1,9 @@
'use client'
+import type { Placement } from '@langgenius/dify-ui/dropdown-menu'
import type { FC } from 'react'
-import type { Placement } from '@/app/components/base/ui/placement'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from '@/app/components/base/ui/dropdown-menu'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames'
import { PluginSource } from '../types'
diff --git a/web/app/components/workflow/edge-contextmenu.tsx b/web/app/components/workflow/edge-contextmenu.tsx
index cf9e70ef81..28c514bbf6 100644
--- a/web/app/components/workflow/edge-contextmenu.tsx
+++ b/web/app/components/workflow/edge-contextmenu.tsx
@@ -1,14 +1,14 @@
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+} from '@langgenius/dify-ui/context-menu'
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useEdges } from 'reactflow'
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuItem,
-} from '@/app/components/base/ui/context-menu'
import { useEdgesInteractions, usePanelInteractions } from './hooks'
import ShortcutsName from './shortcuts-name'
import { useStore } from './store'
diff --git a/web/app/components/workflow/selection-contextmenu.tsx b/web/app/components/workflow/selection-contextmenu.tsx
index 8c6304ba95..dec808ea62 100644
--- a/web/app/components/workflow/selection-contextmenu.tsx
+++ b/web/app/components/workflow/selection-contextmenu.tsx
@@ -1,4 +1,12 @@
import type { Node } from './types'
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuGroup,
+ ContextMenuGroupLabel,
+ ContextMenuItem,
+ ContextMenuSeparator,
+} from '@langgenius/dify-ui/context-menu'
import { produce } from 'immer'
import {
memo,
@@ -8,14 +16,6 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuGroup,
- ContextMenuGroupLabel,
- ContextMenuItem,
- ContextMenuSeparator,
-} from '@/app/components/base/ui/context-menu'
import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks'
import { useSelectionInteractions } from './hooks/use-selection-interactions'
import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history'
diff --git a/web/app/styles/globals.css b/web/app/styles/globals.css
index 0d9c950dec..3462e3434a 100644
--- a/web/app/styles/globals.css
+++ b/web/app/styles/globals.css
@@ -1,9 +1,5 @@
@import './preflight.css' layer(base);
-
-@import '../../themes/light.css' layer(base);
-@import '../../themes/dark.css' layer(base);
-@import '../../themes/manual-light.css' layer(base);
-@import '../../themes/manual-dark.css' layer(base);
+@import '@langgenius/dify-ui/styles.css';
@import './monaco-sticky-fix.css' layer(base);
@import '../components/base/action-button/index.css';
@@ -17,727 +13,6 @@
@config '../../tailwind.config.ts';
-/*
- The default border color has changed to `currentcolor` in Tailwind CSS v4,
- so we've added these compatibility styles to make sure everything still
- looks the same as it did with Tailwind CSS v3.
-
- If we ever want to remove these styles, we need to add an explicit border
- color utility to any element that depends on these defaults.
-*/
-@layer base {
- *,
- ::after,
- ::before,
- ::backdrop,
- ::file-selector-button {
- border-color: var(--color-gray-200, currentcolor);
- }
-}
-
-@utility system-kbd {
- /* font define start */
- font-size: 12px;
- font-weight: 500;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-2xs-regular-uppercase {
- font-size: 10px;
- font-weight: 400;
- text-transform: uppercase;
- line-height: 12px;
-
- /* border radius end */
-}
-
-@utility system-2xs-regular {
- font-size: 10px;
- font-weight: 400;
- line-height: 12px;
-
- /* border radius end */
-}
-
-@utility system-2xs-medium {
- font-size: 10px;
- font-weight: 500;
- line-height: 12px;
-
- /* border radius end */
-}
-
-@utility system-2xs-medium-uppercase {
- font-size: 10px;
- font-weight: 500;
- text-transform: uppercase;
- line-height: 12px;
-
- /* border radius end */
-}
-
-@utility system-2xs-semibold-uppercase {
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- line-height: 12px;
-
- /* border radius end */
-}
-
-@utility system-xs-regular {
- font-size: 12px;
- font-weight: 400;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-xs-regular-uppercase {
- font-size: 12px;
- font-weight: 400;
- text-transform: uppercase;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-xs-medium {
- font-size: 12px;
- font-weight: 500;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-xs-medium-uppercase {
- font-size: 12px;
- font-weight: 500;
- text-transform: uppercase;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-xs-semibold {
- font-size: 12px;
- font-weight: 600;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-xs-semibold-uppercase {
- font-size: 12px;
- font-weight: 600;
- text-transform: uppercase;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-sm-regular {
- font-size: 13px;
- font-weight: 400;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-sm-medium {
- font-size: 13px;
- font-weight: 500;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-sm-medium-uppercase {
- font-size: 13px;
- font-weight: 500;
- text-transform: uppercase;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-sm-semibold {
- font-size: 13px;
- font-weight: 600;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-sm-semibold-uppercase {
- font-size: 13px;
- font-weight: 600;
- text-transform: uppercase;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility system-md-regular {
- font-size: 14px;
- font-weight: 400;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility system-md-medium {
- font-size: 14px;
- font-weight: 500;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility system-md-semibold {
- font-size: 14px;
- font-weight: 600;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility system-md-semibold-uppercase {
- font-size: 14px;
- font-weight: 600;
- text-transform: uppercase;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility system-xl-regular {
- font-size: 16px;
- font-weight: 400;
- line-height: 24px;
-
- /* border radius end */
-}
-
-@utility system-xl-medium {
- font-size: 16px;
- font-weight: 500;
- line-height: 24px;
-
- /* border radius end */
-}
-
-@utility system-xl-semibold {
- font-size: 16px;
- font-weight: 600;
- line-height: 24px;
-
- /* border radius end */
-}
-
-@utility code-xs-regular {
- font-size: 12px;
- font-weight: 400;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility code-xs-semibold {
- font-size: 12px;
- font-weight: 600;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility code-sm-regular {
- font-size: 13px;
- font-weight: 400;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility code-sm-semibold {
- font-size: 13px;
- font-weight: 600;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility code-md-regular {
- font-size: 14px;
- font-weight: 400;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility code-md-semibold {
- font-size: 14px;
- font-weight: 600;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility body-xs-light {
- font-size: 12px;
- font-weight: 300;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility body-xs-regular {
- font-size: 12px;
- font-weight: 400;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility body-xs-medium {
- font-size: 12px;
- font-weight: 500;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility body-sm-light {
- font-size: 13px;
- font-weight: 300;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility body-sm-regular {
- font-size: 13px;
- font-weight: 400;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility body-sm-medium {
- font-size: 13px;
- font-weight: 500;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility body-md-light {
- font-size: 14px;
- font-weight: 300;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility body-md-regular {
- font-size: 14px;
- font-weight: 400;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility body-md-medium {
- font-size: 14px;
- font-weight: 500;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility body-lg-light {
- font-size: 15px;
- font-weight: 300;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility body-lg-regular {
- font-size: 15px;
- font-weight: 400;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility body-lg-medium {
- font-size: 15px;
- font-weight: 500;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility body-xl-regular {
- font-size: 16px;
- font-weight: 400;
- line-height: 24px;
-
- /* border radius end */
-}
-
-@utility body-xl-medium {
- font-size: 16px;
- font-weight: 500;
- line-height: 24px;
-
- /* border radius end */
-}
-
-@utility body-xl-light {
- font-size: 16px;
- font-weight: 300;
- line-height: 24px;
-
- /* border radius end */
-}
-
-@utility body-2xl-light {
- font-size: 18px;
- font-weight: 300;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility body-2xl-regular {
- font-size: 18px;
- font-weight: 400;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility body-2xl-medium {
- font-size: 18px;
- font-weight: 500;
- line-height: 1.5;
-
- /* border radius end */
-}
-
-@utility title-xs-semi-bold {
- font-size: 12px;
- font-weight: 600;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility title-xs-bold {
- font-size: 12px;
- font-weight: 700;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility title-sm-semi-bold {
- font-size: 13px;
- font-weight: 600;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility title-sm-bold {
- font-size: 13px;
- font-weight: 700;
- line-height: 16px;
-
- /* border radius end */
-}
-
-@utility title-md-semi-bold {
- font-size: 14px;
- font-weight: 600;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility title-md-bold {
- font-size: 14px;
- font-weight: 700;
- line-height: 20px;
-
- /* border radius end */
-}
-
-@utility title-lg-semi-bold {
- font-size: 15px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-lg-bold {
- font-size: 15px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-xl-semi-bold {
- font-size: 16px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-xl-bold {
- font-size: 16px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-2xl-semi-bold {
- font-size: 18px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-2xl-bold {
- font-size: 18px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-3xl-semi-bold {
- font-size: 20px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-3xl-bold {
- font-size: 20px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-4xl-semi-bold {
- font-size: 24px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-4xl-bold {
- font-size: 24px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-5xl-semi-bold {
- font-size: 30px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-5xl-bold {
- font-size: 30px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-6xl-semi-bold {
- font-size: 36px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-6xl-bold {
- font-size: 36px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-7xl-semi-bold {
- font-size: 48px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-7xl-bold {
- font-size: 48px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-8xl-semi-bold {
- font-size: 60px;
- font-weight: 600;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility title-8xl-bold {
- font-size: 60px;
- font-weight: 700;
- line-height: 1.2;
-
- /* border radius end */
-}
-
-@utility radius-2xs {
- /* font define end */
-
- /* border radius start */
- border-radius: 2px;
-
- /* border radius end */
-}
-
-@utility radius-xs {
- border-radius: 4px;
-
- /* border radius end */
-}
-
-@utility radius-sm {
- border-radius: 6px;
-
- /* border radius end */
-}
-
-@utility radius-md {
- border-radius: 8px;
-
- /* border radius end */
-}
-
-@utility radius-lg {
- border-radius: 10px;
-
- /* border radius end */
-}
-
-@utility radius-xl {
- border-radius: 12px;
-
- /* border radius end */
-}
-
-@utility radius-2xl {
- border-radius: 16px;
-
- /* border radius end */
-}
-
-@utility radius-3xl {
- border-radius: 20px;
-
- /* border radius end */
-}
-
-@utility radius-4xl {
- border-radius: 24px;
-
- /* border radius end */
-}
-
-@utility radius-5xl {
- border-radius: 24px;
-
- /* border radius end */
-}
-
-@utility radius-6xl {
- border-radius: 28px;
-
- /* border radius end */
-}
-
-@utility radius-7xl {
- border-radius: 32px;
-
- /* border radius end */
-}
-
-@utility radius-8xl {
- border-radius: 40px;
-
- /* border radius end */
-}
-
-@utility radius-9xl {
- border-radius: 48px;
-
- /* border radius end */
-}
-
-@utility radius-full {
- border-radius: 64px;
-
- /* border radius end */
-}
-
-@utility no-scrollbar {
- /* Hide scrollbar for Chrome, Safari and Opera */
- &::-webkit-scrollbar {
- display: none;
- }
-
- /* Hide scrollbar for IE, Edge and Firefox */
- -ms-overflow-style: none;
- scrollbar-width: none;
-}
-
-@utility no-spinner {
- /* Hide arrows from number input */
- &::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
- }
- &::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
- }
- -moz-appearance: textfield;
-}
-
@layer components {
html {
color-scheme: light;
@@ -794,35 +69,6 @@
--card-border-rgb: 131, 134, 135;
}
- /* @media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
-
- --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
- --secondary-glow: linear-gradient(to bottom right,
- rgba(1, 65, 255, 0),
- rgba(1, 65, 255, 0),
- rgba(1, 65, 255, 0.3));
-
- --tile-start-rgb: 2, 13, 46;
- --tile-end-rgb: 2, 5, 19;
- --tile-border: conic-gradient(#ffffff80,
- #ffffff40,
- #ffffff30,
- #ffffff20,
- #ffffff10,
- #ffffff10,
- #ffffff80);
-
- --callout-rgb: 20, 20, 20;
- --callout-border-rgb: 108, 108, 108;
- --card-rgb: 100, 100, 100;
- --card-border-rgb: 200, 200, 200;
- }
-} */
-
* {
box-sizing: border-box;
padding: 0;
@@ -838,12 +84,6 @@
body {
color: rgb(var(--foreground-rgb));
user-select: none;
- /* background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb)); */
}
a {
@@ -852,13 +92,6 @@
outline: none;
}
- /* @media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
- }
-} */
-
- /* CSS Utils */
.h1 {
padding-bottom: 1.5rem;
line-height: 1.5;
@@ -880,7 +113,7 @@
@layer components {
.link {
- @apply text-blue-600 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out;
+ @apply cursor-pointer text-blue-600 transition-opacity duration-200 ease-in-out hover:opacity-80;
}
.text-gradient {
@@ -891,13 +124,11 @@
text-fill-color: transparent;
}
- /* overwrite paging active dark model style */
[class*='style_paginatio'] li .text-primary-600 {
color: rgb(28 100 242);
background-color: rgb(235 245 255);
}
- /* support safari 14 and below */
.inset-0 {
left: 0;
right: 0;
diff --git a/web/app/styles/markdown.css b/web/app/styles/markdown.css
index 1ada6e49bd..b300b85b3f 100644
--- a/web/app/styles/markdown.css
+++ b/web/app/styles/markdown.css
@@ -1,5 +1,4 @@
-@import '../../themes/markdown-light.css';
-@import '../../themes/markdown-dark.css';
+@import '@langgenius/dify-ui/markdown.css';
@reference "./globals.css";
.markdown-body {
diff --git a/web/docs/overlay-migration.md b/web/docs/overlay-migration.md
index 0609b1e325..45594e668b 100644
--- a/web/docs/overlay-migration.md
+++ b/web/docs/overlay-migration.md
@@ -16,8 +16,8 @@ This document tracks the migration away from legacy overlay APIs.
- `@/app/components/base/toast` (including `context`)
- Replacement primitives:
- `@/app/components/base/ui/tooltip`
- - `@/app/components/base/ui/dropdown-menu`
- - `@/app/components/base/ui/context-menu`
+ - `@langgenius/dify-ui/dropdown-menu`
+ - `@langgenius/dify-ui/context-menu`
- `@/app/components/base/ui/popover`
- `@/app/components/base/ui/dialog`
- `@/app/components/base/ui/alert-dialog`
diff --git a/web/docs/test.md b/web/docs/test.md
index bc1546a991..b562968d74 100644
--- a/web/docs/test.md
+++ b/web/docs/test.md
@@ -10,7 +10,7 @@ When I ask you to write/refactor/fix tests, follow these rules by default.
- **Testing Tools**: Vitest 4.0.16 + React Testing Library 16.0
- **Test Environment**: happy-dom
- **File Naming**: `ComponentName.spec.tsx` inside a same-level `__tests__/` directory
-- **Placement Rule**: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`.
+- **Placement Rule**: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`. This rule also applies to workspace packages under `packages/`.
## Running Tests
diff --git a/web/eslint.constants.mjs b/web/eslint.constants.mjs
index d449042542..828faff5f8 100644
--- a/web/eslint.constants.mjs
+++ b/web/eslint.constants.mjs
@@ -71,7 +71,7 @@ export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
'**/base/dropdown',
'**/base/dropdown/index',
],
- message: 'Deprecated: use @/app/components/base/ui/dropdown-menu instead. See issue #32767.',
+ message: 'Deprecated: use @langgenius/dify-ui/dropdown-menu instead. See issue #32767.',
},
{
group: [
diff --git a/web/package.json b/web/package.json
index d72d8b1648..591144ce92 100644
--- a/web/package.json
+++ b/web/package.json
@@ -25,6 +25,7 @@
"analyze": "next experimental-analyze",
"analyze-component": "node ./scripts/analyze-component.js",
"build": "next build",
+ "build:dify-ui": "pnpm --filter @langgenius/dify-ui build",
"build:vinext": "vinext build",
"dev": "next dev",
"dev:inspect": "next dev --inspect",
@@ -40,6 +41,16 @@
"lint:quiet": "vp run lint --quiet",
"lint:tss": "tsslint --project tsconfig.json",
"preinstall": "npx only-allow pnpm",
+ "prebuild": "pnpm run build:dify-ui",
+ "prebuild:vinext": "pnpm run build:dify-ui",
+ "predev": "pnpm run build:dify-ui",
+ "predev:vinext": "pnpm run build:dify-ui",
+ "prestorybook": "pnpm run build:dify-ui",
+ "prestorybook:build": "pnpm run build:dify-ui",
+ "pretest": "pnpm run build:dify-ui",
+ "pretest:watch": "pnpm run build:dify-ui",
+ "pretype-check": "pnpm run build:dify-ui",
+ "pretype-check:tsgo": "pnpm run build:dify-ui",
"refactor-component": "node ./scripts/refactor-component.js",
"start": "node ./scripts/copy-and-start.mjs",
"start:vinext": "vinext start",
@@ -61,6 +72,7 @@
"@formatjs/intl-localematcher": "catalog:",
"@headlessui/react": "catalog:",
"@heroicons/react": "catalog:",
+ "@langgenius/dify-ui": "workspace:*",
"@lexical/code": "catalog:",
"@lexical/link": "catalog:",
"@lexical/list": "catalog:",
diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts
index 32b889e707..513f5ec3d7 100644
--- a/web/tailwind.config.ts
+++ b/web/tailwind.config.ts
@@ -1,5 +1,5 @@
import type { Config } from 'tailwindcss'
-import commonConfig from './tailwind-common-config'
+import difyUiTailwindPreset from '@langgenius/dify-ui/tailwind-preset'
const config: Config = {
content: [
@@ -10,7 +10,7 @@ const config: Config = {
'./node_modules/@streamdown/math/dist/*.js',
'!./**/*.{spec,test}.{js,ts,jsx,tsx}',
],
- ...commonConfig,
+ ...difyUiTailwindPreset,
}
export default config