Compare commits

...

4 Commits

Author SHA1 Message Date
yyh
142f94e27a Merge remote-tracking branch 'origin/main' into codex/dify-ui-package-migration 2026-04-03 12:14:22 +08:00
yyh
a1bd929b3c remove 2026-04-02 18:35:02 +08:00
yyh
ffb9ee3e36 fix(web): support lint tooling package exports 2026-04-02 18:29:44 +08:00
yyh
485586f49a feat(web): extract dify ui package 2026-04-02 18:25:16 +08:00
50 changed files with 1236 additions and 889 deletions

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

@@ -0,0 +1,2 @@
dist/
node_modules/

View File

@@ -0,0 +1,82 @@
{
"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",
"default": "./dist/context-menu/index.js"
},
"./dropdown-menu": {
"types": "./dist/dropdown-menu/index.d.ts",
"import": "./dist/dropdown-menu/index.js",
"default": "./dist/dropdown-menu/index.js"
},
"./tailwind-preset": {
"types": "./dist/tailwind-preset.d.ts",
"import": "./dist/tailwind-preset.js",
"default": "./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",
"default": "./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:"
}
}

View File

@@ -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,
})

View File

@@ -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 }) => (
<ContextMenuTrigger
@@ -185,17 +191,17 @@ export const Complex: Story = {
<TriggerArea label="Right-click to inspect all menu capabilities" />
<ContextMenuContent>
<ContextMenuItem>
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Rename
</ContextMenuItem>
<ContextMenuItem>
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Duplicate
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>
<span aria-hidden className="i-ri-share-line size-4 shrink-0 text-text-tertiary" />
<RiShareLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Share
</ContextMenuSubTrigger>
<ContextMenuSubContent>
@@ -206,7 +212,7 @@ export const Complex: Story = {
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem destructive>
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
Delete
</ContextMenuItem>
</ContextMenuContent>

View File

@@ -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
@@ -42,11 +43,11 @@ type ContextMenuPopupRenderProps = Required<Pick<ContextMenuContentProps, 'child
placement: Placement
sideOffset: number
alignOffset: number
className?: string
popupClassName?: string
positionerProps?: ContextMenuContentProps['positionerProps']
popupProps?: ContextMenuContentProps['popupProps']
withBackdrop?: boolean
className?: string | undefined
popupClassName?: string | undefined
positionerProps?: ContextMenuContentProps['positionerProps'] | undefined
popupProps?: ContextMenuContentProps['popupProps'] | undefined
withBackdrop?: boolean | undefined
}
function renderContextMenuPopup({
@@ -173,6 +174,25 @@ export function ContextMenuCheckboxItem({
)
}
type ContextMenuIndicatorProps = Omit<React.ComponentPropsWithoutRef<'span'>, 'children'> & {
children?: React.ReactNode
}
export function ContextMenuItemIndicator({
className,
children,
...props
}: ContextMenuIndicatorProps) {
return (
<span
aria-hidden
className={cn(menuIndicatorClassName, className)}
{...props}
>
{children ?? <RiCheckLine aria-hidden className="h-4 w-4" />}
</span>
)
}
export function ContextMenuCheckboxItemIndicator({
className,
...props
@@ -182,7 +202,7 @@ export function ContextMenuCheckboxItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
<RiCheckLine aria-hidden className="h-4 w-4" />
</BaseContextMenu.CheckboxItemIndicator>
)
}
@@ -196,7 +216,7 @@ export function ContextMenuRadioItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
<RiCheckLine aria-hidden className="h-4 w-4" />
</BaseContextMenu.RadioItemIndicator>
)
}
@@ -217,20 +237,20 @@ export function ContextMenuSubTrigger({
{...props}
>
{children}
<span aria-hidden className="ml-auto i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary" />
<RiArrowRightSLine aria-hidden className="ml-auto size-4 shrink-0 text-text-tertiary" />
</BaseContextMenu.SubmenuTrigger>
)
}
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({
@@ -278,3 +298,5 @@ export function ContextMenuSeparator({
/>
)
}
export type { Placement }

View File

@@ -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<ComponentPropsWithoutRef<'a'>, 'href'>) => (
function MockLink({
href,
children,
...props
}: {
href: string
children?: ReactNode
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) {
return (
<a href={href} {...props}>
{children}
</a>
),
}))
)
}
describe('dropdown-menu wrapper', () => {
describe('DropdownMenuContent', () => {
@@ -301,7 +300,7 @@ describe('dropdown-menu wrapper', () => {
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLinkItem
render={<Link href="/account" />}
render={<MockLink href="/account" />}
aria-label="account link"
>
Account settings

View File

@@ -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 }) => (
<DropdownMenuTrigger
@@ -214,20 +225,20 @@ export const WithIcons: Story = {
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuItem>
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Edit
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
<RiArchiveLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Archive
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem destructive>
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
@@ -262,35 +273,35 @@ const ComplexDemo = () => {
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Edit</DropdownMenuGroupLabel>
<DropdownMenuItem>
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
<RiPencilLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Rename
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
<RiFileCopyLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem disabled>
<span aria-hidden className="i-ri-lock-line size-4 shrink-0 text-text-tertiary" />
<RiLockLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Move to Workspace
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span aria-hidden className="i-ri-share-line size-4 shrink-0 text-text-tertiary" />
<RiShareLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Share
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>
<span aria-hidden className="i-ri-mail-line size-4 shrink-0 text-text-tertiary" />
<RiMailLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Email
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-chat-1-line size-4 shrink-0 text-text-tertiary" />
<RiChat1Line aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Slack
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-link size-4 shrink-0 text-text-tertiary" />
<RiLink aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Copy Link
</DropdownMenuItem>
</DropdownMenuSubContent>
@@ -315,13 +326,13 @@ const ComplexDemo = () => {
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked={showArchived} onCheckedChange={setShowArchived}>
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
<RiArchiveLine aria-hidden className="size-4 shrink-0 text-text-tertiary" />
Show Archived
<DropdownMenuCheckboxItemIndicator />
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem destructive>
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
<RiDeleteBinLine aria-hidden className="size-4 shrink-0" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -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 DropdownMenuTrigger = Menu.Trigger
@@ -41,7 +42,7 @@ export function DropdownMenuRadioItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
<RiCheckLine aria-hidden className="h-4 w-4" />
</Menu.RadioItemIndicator>
)
}
@@ -67,7 +68,7 @@ export function DropdownMenuCheckboxItemIndicator({
className={cn(menuIndicatorClassName, className)}
{...props}
>
<span aria-hidden className="i-ri-check-line h-4 w-4" />
<RiCheckLine aria-hidden className="h-4 w-4" />
</Menu.CheckboxItemIndicator>
)
}
@@ -105,10 +106,10 @@ type DropdownMenuPopupRenderProps = Required<Pick<DropdownMenuContentProps, 'chi
placement: Placement
sideOffset: number
alignOffset: number
className?: string
popupClassName?: string
positionerProps?: DropdownMenuContentProps['positionerProps']
popupProps?: DropdownMenuContentProps['popupProps']
className?: string | undefined
popupClassName?: string | undefined
positionerProps?: DropdownMenuContentProps['positionerProps'] | undefined
popupProps?: DropdownMenuContentProps['popupProps'] | undefined
}
function renderDropdownMenuPopup({
@@ -186,20 +187,20 @@ export function DropdownMenuSubTrigger({
{...props}
>
{children}
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-4 shrink-0 text-text-tertiary" />
<RiArrowRightSLine aria-hidden className="ml-auto size-4 shrink-0 text-text-tertiary" />
</Menu.SubmenuTrigger>
)
}
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({
@@ -271,3 +272,5 @@ export function DropdownMenuSeparator({
/>
)
}
export type { Placement }

View File

@@ -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))
}

View File

@@ -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',
}
}

View File

@@ -0,0 +1,2 @@
@import './themes/markdown-light.css';
@import './themes/markdown-dark.css';

View File

@@ -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}';

View File

@@ -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;
}

View File

@@ -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,
},

3
packages/dify-ui/src/typography.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare const typography: (helpers: { theme: (path: string) => unknown }) => Record<string, unknown>
export default typography

View File

@@ -0,0 +1,8 @@
import difyUiTailwindPreset from './src/tailwind-preset'
const config = {
content: [],
...difyUiTailwindPreset,
}
export default config

View File

@@ -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__/**"
]
}

View File

@@ -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"
]
}

View File

@@ -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'],
},
})

View File

@@ -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<number> = []
constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {}
observe(_target: Element) {}
unobserve(_target: Element) {}
disconnect() {}
takeRecords(): IntersectionObserverEntry[] {
return []
}
}
}
afterEach(() => {
cleanup()
})

79
pnpm-lock.yaml generated
View File

@@ -632,6 +632,82 @@ importers:
specifier: 'catalog:'
version: 0.1.15(@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@6.0.2)(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:
@@ -694,6 +770,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)

View File

@@ -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',

View File

@@ -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`

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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 = {

View File

@@ -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 }) => (
<div data-testid="dropdown-menu" data-open={open}>{children}</div>
),

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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;

View File

@@ -1,5 +1,5 @@
@import '../../themes/markdown-light.css';
@import '../../themes/markdown-dark.css';
@import '@langgenius/dify-ui/markdown.css';
@reference "./globals.css";
.markdown-body {
-ms-text-size-adjust: 100%;

View File

@@ -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`

View File

@@ -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

View File

@@ -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: [

View File

@@ -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:",

View File

@@ -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