From 4f03b7193e5936fd12f3cfdb86326759b2ca7aa2 Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Mon, 20 Apr 2026 15:54:49 +0800
Subject: [PATCH] docs(frontend): align docs and comments (#35364)
---
.agents/skills/frontend-testing/SKILL.md | 6 +-
.../frontend-testing/references/checklist.md | 4 +-
.../frontend-testing/references/mocking.md | 26 ++---
packages/dify-ui/README.md | 105 ++++++++++++++++++
packages/dify-ui/src/dialog/index.tsx | 2 +-
.../dify-ui/src/scroll-area/index.stories.tsx | 2 +-
.../src/toast/__tests__/index.spec.tsx | 2 +-
web/AGENTS.md | 5 +-
web/README.md | 2 +-
.../base/portal-to-follow-elem/index.tsx | 2 +-
.../__tests__/index.spec.tsx | 2 +-
.../workflow-onboarding-modal/index.tsx | 2 +-
web/app/components/workflow/run/node.tsx | 36 ++----
web/docs/overlay-migration.md | 34 +++---
web/docs/test.md | 6 +-
web/eslint.config.mjs | 7 --
web/eslint.constants.mjs | 2 +-
web/scripts/analyze-component.js | 6 +-
18 files changed, 166 insertions(+), 85 deletions(-)
create mode 100644 packages/dify-ui/README.md
diff --git a/.agents/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md
index 4da070bdbf..105c979c58 100644
--- a/.agents/skills/frontend-testing/SKILL.md
+++ b/.agents/skills/frontend-testing/SKILL.md
@@ -200,7 +200,7 @@ When assigned to test a directory/path, test **ALL content** within that path:
- ✅ **Import real project components** directly (including base components and siblings)
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
-- ❌ **DO NOT mock** base components (`@/app/components/base/*`)
+- ❌ **DO NOT mock** base components (`@/app/components/base/*`) or dify-ui primitives (`@langgenius/dify-ui/*`)
- ❌ **DO NOT mock** sibling/child components in the same directory
> See [Test Structure Template](#test-structure-template) for correct import/mock patterns.
@@ -325,12 +325,12 @@ For more detailed information, refer to:
### Reference Examples in Codebase
- `web/utils/classnames.spec.ts` - Utility function tests
-- `web/app/components/base/button/index.spec.tsx` - Component tests
+- `web/app/components/base/radio/__tests__/index.spec.tsx` - Component tests
- `web/__mocks__/provider-context.ts` - Mock factory example
### Project Configuration
-- `web/vitest.config.ts` - Vitest configuration
+- `web/vite.config.ts` - Vite/Vitest configuration
- `web/vitest.setup.ts` - Test environment setup
- `web/scripts/analyze-component.js` - Component analysis tool
- Modules are not mocked automatically. Global mocks live in `web/vitest.setup.ts` (for example `react-i18next`, `next/image`); mock other modules like `ky` or `mime` locally in test files.
diff --git a/.agents/skills/frontend-testing/references/checklist.md b/.agents/skills/frontend-testing/references/checklist.md
index 10b8fb66f9..99258498dd 100644
--- a/.agents/skills/frontend-testing/references/checklist.md
+++ b/.agents/skills/frontend-testing/references/checklist.md
@@ -36,7 +36,7 @@ Use this checklist when generating or reviewing tests for Dify frontend componen
### Integration vs Mocking
-- [ ] **DO NOT mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
+- [ ] **DO NOT mock base components or dify-ui primitives** (base `Loading`, `Input`, `Badge`; dify-ui `Button`, `Tooltip`, `Dialog`, etc.)
- [ ] Import real project components instead of mocking
- [ ] Only mock: API calls, complex context providers, third-party libs with side effects
- [ ] Prefer integration testing when using single spec file
@@ -73,7 +73,7 @@ Use this checklist when generating or reviewing tests for Dify frontend componen
### Mocks
-- [ ] **DO NOT mock base components** (`@/app/components/base/*`)
+- [ ] **DO NOT mock base components or dify-ui primitives** (`@/app/components/base/*` or `@langgenius/dify-ui/*`)
- [ ] `vi.clearAllMocks()` in `beforeEach` (not `afterEach`)
- [ ] Shared mock state reset in `beforeEach`
- [ ] i18n uses global mock (auto-loaded in `web/vitest.setup.ts`); only override locally for custom translations
diff --git a/.agents/skills/frontend-testing/references/mocking.md b/.agents/skills/frontend-testing/references/mocking.md
index f58377c4a5..8c2f1c0c58 100644
--- a/.agents/skills/frontend-testing/references/mocking.md
+++ b/.agents/skills/frontend-testing/references/mocking.md
@@ -2,29 +2,27 @@
## ⚠️ Important: What NOT to Mock
-### DO NOT Mock Base Components
+### DO NOT Mock Base Components or dify-ui Primitives
-**Never mock components from `@/app/components/base/`** such as:
+**Never mock components from `@/app/components/base/` or from `@langgenius/dify-ui/*`** such as:
-- `Loading`, `Spinner`
-- `Button`, `Input`, `Select`
-- `Tooltip`, `Modal`, `Dropdown`
-- `Icon`, `Badge`, `Tag`
+- Legacy base (`@/app/components/base/*`): `Loading`, `Spinner`, `Input`, `Badge`, `Tag`
+- dify-ui primitives (`@langgenius/dify-ui/*`): `Button`, `Tooltip`, `Dialog`, `Popover`, `DropdownMenu`, `ContextMenu`, `Select`, `AlertDialog`, `Toast`
**Why?**
-- Base components will have their own dedicated tests
+- These components have their own dedicated tests
- Mocking them creates false positives (tests pass but real integration fails)
- Using real components tests actual integration behavior
```typescript
-// ❌ WRONG: Don't mock base components
+// ❌ WRONG: Don't mock base components or dify-ui primitives
vi.mock('@/app/components/base/loading', () => () =>
Loading
)
-vi.mock('@/app/components/base/button', () => ({ children }: any) => )
+vi.mock('@langgenius/dify-ui/button', () => ({ Button: ({ children }: any) => }))
-// ✅ CORRECT: Import and use real base components
+// ✅ CORRECT: Import and use the real components
import Loading from '@/app/components/base/loading'
-import Button from '@/app/components/base/button'
+import { Button } from '@langgenius/dify-ui/button'
// They will render normally in tests
```
@@ -319,7 +317,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
### ✅ DO
-1. **Use real base components** - Import from `@/app/components/base/` directly
+1. **Use real base components and dify-ui primitives** - Import from `@/app/components/base/` or `@langgenius/dify-ui/*` directly
1. **Use real project components** - Prefer importing over mocking
1. **Use real Zustand stores** - Set test state via `store.setState()`
1. **Reset mocks in `beforeEach`**, not `afterEach`
@@ -330,7 +328,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
### ❌ DON'T
-1. **Don't mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
+1. **Don't mock base components or dify-ui primitives** (`Loading`, `Input`, `Button`, `Tooltip`, `Dialog`, etc.)
1. **Don't mock Zustand store modules** - Use real stores with `setState()`
1. Don't mock components you can import directly
1. Don't create overly simplified mocks that miss conditional logic
@@ -342,7 +340,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
```
Need to use a component in test?
│
-├─ Is it from @/app/components/base/*?
+├─ Is it from @/app/components/base/* or @langgenius/dify-ui/*?
│ └─ YES → Import real component, DO NOT mock
│
├─ Is it a project component?
diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md
new file mode 100644
index 0000000000..5e4e439e5f
--- /dev/null
+++ b/packages/dify-ui/README.md
@@ -0,0 +1,105 @@
+# @langgenius/dify-ui
+
+Shared UI primitives, design tokens, Tailwind preset, and the `cn()` utility consumed by Dify's `web/` app.
+
+The primitives are thin, opinionated wrappers around [Base UI] headless components, styled with `cva` + `cn` and Dify design tokens.
+
+> `private: true` — this package is consumed by `web/` via the pnpm workspace and is not published to npm. Treat the API as internal to Dify, but stable within the workspace.
+
+## Installation
+
+Already wired as a workspace dependency in `web/package.json`. Nothing to install.
+
+For a new workspace consumer, add:
+
+```jsonc
+{
+ "dependencies": {
+ "@langgenius/dify-ui": "workspace:*"
+ }
+}
+```
+
+## Imports
+
+Always import from a **subpath export** — there is no barrel:
+
+```ts
+import { Button } from '@langgenius/dify-ui/button'
+import { cn } from '@langgenius/dify-ui/cn'
+import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog'
+import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
+import '@langgenius/dify-ui/styles.css' // once, in the app root
+```
+
+Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported — it keeps tree-shaking trivial and makes Storybook / test coverage attribution per-primitive.
+
+## Primitives
+
+| Category | Subpath | Notes |
+| -------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
+| Overlay | `./alert-dialog`, `./context-menu`, `./dialog`, `./dropdown-menu`, `./popover`, `./select`, `./toast`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
+| Form | `./number-field`, `./slider`, `./switch` | Controlled / uncontrolled per Base UI defaults. |
+| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
+| Media | `./avatar`, `./button` | Button exposes `cva` variants. |
+
+Utilities:
+
+- `./cn` — `clsx` + `tailwind-merge` wrapper. Use this for conditional class composition.
+- `./tailwind-preset` — Tailwind v4 preset with Dify tokens. Apps extend it from their own `tailwind.config.ts`.
+- `./styles.css` — the one CSS entry that ships the design tokens, theme variables, and base reset. Import it once from the app root.
+
+## Overlay & portal contract
+
+All overlay primitives (`dialog`, `alert-dialog`, `popover`, `dropdown-menu`, `context-menu`, `select`, `tooltip`, `toast`) render their content inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Consumers **do not** need to wrap anything in a portal manually.
+
+### Root isolation requirement
+
+The host app **must** establish an isolated stacking context at its root so the portalled overlay layer is not clipped or re-ordered by ancestor `transform` / `filter` / `contain` styles. In the Dify web app this is done in `web/app/layout.tsx`:
+
+```tsx
+
+
{children}
+
+```
+
+Equivalent: any root element with `isolation: isolate` in CSS. Without it, overlays can be visually clipped on Safari when a descendant creates a new stacking context.
+
+### z-index layering
+
+Every overlay primitive uses a single, shared z-index. Do **not** override it at call sites.
+
+| Layer | z-index | Where |
+| ----------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
+| Overlays (Dialog, AlertDialog, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | `z-1002` | Positioner / Backdrop |
+| Toast viewport | `z-1003` | One layer above overlays so notifications are never hidden under a dialog. |
+
+Rationale: during Dify's migration from legacy `portal-to-follow-elem` / `base/modal` / `base/dialog` overlays to this package, new and old overlays coexist in the DOM. `z-1002` sits above any common legacy layer, eliminating per-call-site z-index hacks. Among themselves, new primitives share the same z-index and **rely on DOM order** for stacking — the portal mounted later wins.
+
+See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for the Dify-web migration history and the remaining legacy allowlist. Once the legacy overlays are gone, the values in this table can drop back to `z-50` / `z-51`.
+
+### Rules
+
+- Never add `z-1003` / `z-9999` / etc. overrides on primitives from this package. If something is getting clipped, the **parent** overlay (typically a legacy one) is the problem and should be migrated.
+- Never portal an overlay manually on top of our primitives — use `DialogTrigger`, `PopoverTrigger`, etc. Base UI handles focus management, scroll-locking, and dismissal.
+- When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it **inside** the exported component, not at call sites.
+
+## Development
+
+- `pnpm -C packages/dify-ui test` — Vitest unit tests for primitives.
+- `pnpm -C packages/dify-ui storybook` — Storybook on the default port. Each primitive has `index.stories.tsx`.
+- `pnpm -C packages/dify-ui type-check` — `tsc --noEmit` for this package only.
+
+See `[AGENTS.md](./AGENTS.md)` for:
+
+- Component authoring rules (one-component-per-folder, `cva` + `cn`, relative imports inside the package, subpath imports from consumers).
+- Figma `--radius/`* token → Tailwind `rounded-*` class mapping.
+
+## Not part of this package
+
+- Application state (`jotai`, `zustand`), data fetching (`ky`, `@tanstack/react-query`, `@orpc/*`), i18n (`next-i18next` / `react-i18next`), and routing (`next`) all live in `web/`. This package has zero dependencies on them and must stay that way so it can eventually be consumed by other apps or extracted.
+- Business components (chat, workflow, dataset views, etc.). Those belong in `web/app/components/...`.
+
+[Base UI Portal]: https://base-ui.com/react/overview/quick-start#portals
+[Base UI]: https://base-ui.com/react
+[Overlay & portal contract]: #overlay--portal-contract
diff --git a/packages/dify-ui/src/dialog/index.tsx b/packages/dify-ui/src/dialog/index.tsx
index 517d6cc9fc..24c5bcc463 100644
--- a/packages/dify-ui/src/dialog/index.tsx
+++ b/packages/dify-ui/src/dialog/index.tsx
@@ -1,7 +1,7 @@
'use client'
// z-index strategy (relies on root `isolation: isolate` in layout.tsx):
-// All base/ui/* overlay primitives — z-1002
+// All @langgenius/dify-ui/* overlay primitives — z-1002
// Toast stays one layer above overlays at z-1003.
// Overlays share the same z-index; DOM order handles stacking when multiple are open.
// This ensures overlays inside a Dialog (e.g. a Tooltip on a dialog button) render
diff --git a/packages/dify-ui/src/scroll-area/index.stories.tsx b/packages/dify-ui/src/scroll-area/index.stories.tsx
index e1f8f9cfb5..433817948f 100644
--- a/packages/dify-ui/src/scroll-area/index.stories.tsx
+++ b/packages/dify-ui/src/scroll-area/index.stories.tsx
@@ -174,7 +174,7 @@ const StickyListPane = () => (
Operational queue
-
The scrollbar is still the shared base/ui primitive, while the pane adds sticky structure and a viewport mask.
+
The scrollbar is still the shared dify-ui primitive, while the pane adds sticky structure and a viewport mask.
24 items
diff --git a/packages/dify-ui/src/toast/__tests__/index.spec.tsx b/packages/dify-ui/src/toast/__tests__/index.spec.tsx
index 8bdf4417e0..edbdacd203 100644
--- a/packages/dify-ui/src/toast/__tests__/index.spec.tsx
+++ b/packages/dify-ui/src/toast/__tests__/index.spec.tsx
@@ -8,7 +8,7 @@ declare global {
var BASE_UI_ANIMATIONS_DISABLED: boolean | undefined
}
-describe('base/ui/toast', () => {
+describe('@langgenius/dify-ui/toast', () => {
beforeAll(() => {
// Base UI waits for `requestAnimationFrame` + `getAnimations().finished`
// before unmounting a toast. Fake timers can't reliably drive that path,
diff --git a/web/AGENTS.md b/web/AGENTS.md
index 4a705bf4b8..5e9f7ed11c 100644
--- a/web/AGENTS.md
+++ b/web/AGENTS.md
@@ -4,8 +4,9 @@
## Overlay Components (Mandatory)
-- `./docs/overlay-migration.md` is the source of truth for overlay-related work.
-- In new or modified code, use only overlay primitives from `@/app/components/base/ui/*`.
+- `../packages/dify-ui/README.md` is the permanent contract for overlay primitives, portals, root `isolation: isolate`, and the `z-1002` / `z-1003` layering.
+- `./docs/overlay-migration.md` is the source of truth for the ongoing migration (deprecated import paths, allowlist, coexistence rules).
+- In new or modified code, use only overlay primitives from `@langgenius/dify-ui/*`.
- Do not introduce deprecated overlay imports from `@/app/components/base/*`; when touching legacy callers, prefer migrating them and keep the allowlist shrinking (never expanding).
## Query & Mutation (Mandatory)
diff --git a/web/README.md b/web/README.md
index 683a18c769..eb964b01e3 100644
--- a/web/README.md
+++ b/web/README.md
@@ -165,7 +165,7 @@ The Dify community can be found on [Discord community], where you can ask questi
[Storybook]: https://storybook.js.org
[Vite+]: https://viteplus.dev
[Vitest]: https://vitest.dev
-[index.spec.tsx]: ./app/components/base/button/index.spec.tsx
+[index.spec.tsx]: ./app/components/base/radio/__tests__/index.spec.tsx
[pnpm]: https://pnpm.io
[vinext]: https://github.com/cloudflare/vinext
[web/docs/test.md]: ./docs/test.md
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 48099c7020..8b531be309 100644
--- a/web/app/components/base/portal-to-follow-elem/index.tsx
+++ b/web/app/components/base/portal-to-follow-elem/index.tsx
@@ -1,6 +1,6 @@
'use client'
/**
- * @deprecated Use semantic overlay primitives from `@/app/components/base/ui/` instead.
+ * @deprecated Use semantic overlay primitives from `@langgenius/dify-ui/*` instead.
* This component will be removed after migration is complete.
* See: https://github.com/langgenius/dify/issues/32767
*
diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx
index e2248a44c3..206a7c0148 100644
--- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx
+++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/__tests__/index.spec.tsx
@@ -54,7 +54,7 @@ describe('EditWorkspaceModal', () => {
expect(await screen.findByDisplayValue('Test Workspace')).toBeInTheDocument()
})
- it('should render on the base/ui overlay layer', async () => {
+ it('should render on the dify-ui overlay layer', async () => {
renderModal()
expect(await screen.findByRole('dialog')).toHaveClass('z-1002')
diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
index cbb875a758..c72a515925 100644
--- a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
+++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
@@ -45,7 +45,7 @@ const WorkflowOnboardingModal: FC = ({
- {/* TODO: reduce z-1002 to match base/ui primitives after legacy overlay migration completes */}
+ {/* TODO: reduce z-1002 to match @langgenius/dify-ui primitives after legacy overlay migration completes */}
{t('onboarding.escTip.press', { ns: 'workflow' })}
diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx
index b683467d5f..3f7e9e9999 100644
--- a/web/app/components/workflow/run/node.tsx
+++ b/web/app/components/workflow/run/node.tsx
@@ -69,7 +69,15 @@ const NodePanel: FC = ({
doSetCollapseState(state)
}, [hideProcessDetail])
const titleRef = useRef(null)
- const [isTitleTruncated, setIsTitleTruncated] = useState(false)
+ const [isTooltipOpen, setIsTooltipOpen] = useState(false)
+ const handleTooltipOpenChange = useCallback((open: boolean) => {
+ if (open) {
+ const el = titleRef.current
+ if (!el || el.scrollWidth <= el.clientWidth)
+ return
+ }
+ setIsTooltipOpen(open)
+ }, [])
const { t } = useTranslation()
const docLink = useDocLink()
@@ -94,27 +102,6 @@ const NodePanel: FC = ({
setCollapseState(!nodeInfo.expand)
}, [nodeInfo.expand, setCollapseState])
- useEffect(() => {
- const titleElement = titleRef.current
- if (!titleElement)
- return
-
- let frameId = 0
- const updateIsTitleTruncated = () => {
- setIsTitleTruncated(titleElement.scrollWidth > titleElement.clientWidth)
- }
-
- frameId = requestAnimationFrame(updateIsTitleTruncated)
-
- const resizeObserver = new ResizeObserver(updateIsTitleTruncated)
- resizeObserver.observe(titleElement)
-
- return () => {
- cancelAnimationFrame(frameId)
- resizeObserver.disconnect()
- }
- }, [nodeInfo.title])
-
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!nodeInfo.details?.length
const isLoopNode = nodeInfo.node_type === BlockEnum.Loop && !!nodeInfo.details?.length
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
@@ -155,14 +142,13 @@ const NodePanel: FC = ({
/>
)}
-
+
diff --git a/web/docs/overlay-migration.md b/web/docs/overlay-migration.md
index 0016f34e12..0fa584ecfc 100644
--- a/web/docs/overlay-migration.md
+++ b/web/docs/overlay-migration.md
@@ -1,6 +1,8 @@
# Overlay Migration Guide
-This document tracks the migration away from legacy overlay APIs.
+This document tracks the Dify-web migration away from legacy overlay APIs.
+
+> **See also:** [`packages/dify-ui/README.md`] for the permanent overlay / portal / z-index contract of the replacement primitives. This document covers the one-off migration mechanics (allowlist, deprecated import paths, coexistence z-index strategy) and is expected to shrink and eventually be removed once the legacy overlays are gone.
## Scope
@@ -31,7 +33,7 @@ This document tracks the migration away from legacy overlay APIs.
## Migration phases
1. Business/UI features outside `app/components/base/**`
- - Migrate old calls to semantic primitives from `@/app/components/base/ui/**`.
+ - Migrate old calls to semantic primitives from `@langgenius/dify-ui/*`.
- Keep deprecated imports out of newly touched files.
1. Legacy base components in allowlist
- Migrate allowlisted base callers gradually.
@@ -53,7 +55,7 @@ pnpm -C web lint:fix --prune-suppressions
## z-index strategy
-All new overlay primitives in `base/ui/` share a single z-index value:
+All new overlay primitives in `@langgenius/dify-ui` share a single z-index value:
**`z-1002`**, except Toast which stays one layer above at **`z-1003`**.
### Why z-[1002]?
@@ -61,13 +63,13 @@ All new overlay primitives in `base/ui/` share a single z-index value:
During the migration period, legacy and new overlays coexist. Legacy overlays
portal to `document.body` with explicit z-index values:
-| Layer | z-index | Components |
-| --------------------------------- | -------------- | -------------------------------------------- |
-| Legacy Drawer | `z-30` | `base/drawer` |
-| Legacy Modal | `z-60` | `base/modal` (default) |
-| Legacy PortalToFollowElem callers | up to `z-1001` | various business components |
-| **New UI primitives** | **`z-1002`** | `base/ui/*` (Popover, Dialog, Tooltip, etc.) |
-| Toast | `z-1003` | `base/ui/toast` |
+| Layer | z-index | Components |
+| --------------------------------- | -------------- | -------------------------------------------------------- |
+| Legacy Drawer | `z-30` | `base/drawer` |
+| Legacy Modal | `z-60` | `base/modal` (default) |
+| Legacy PortalToFollowElem callers | up to `z-1001` | various business components |
+| **New UI primitives** | **`z-1002`** | `@langgenius/dify-ui/*` (Popover, Dialog, Tooltip, etc.) |
+| Toast | `z-1003` | `@langgenius/dify-ui/toast` |
`z-1002` sits above all common legacy overlays, so new primitives always
render on top without needing per-call-site z-index hacks. Among themselves,
@@ -81,8 +83,8 @@ back to `z-9999`.
### Rules
- **Do NOT add z-index overrides** (e.g. `className="z-1003"`) on new
- `base/ui/*` components. If you find yourself needing one, the parent legacy
- overlay should be migrated instead.
+ `@langgenius/dify-ui/*` components. If you find yourself needing one, the
+ parent legacy overlay should be migrated instead.
- When migrating a legacy overlay that has a high z-index, remove the z-index
entirely — the new primitive's default `z-1002` handles it.
- `portalToFollowElemContentClassName` with z-index values (e.g. `z-1000`)
@@ -92,12 +94,8 @@ back to `z-9999`.
Once all legacy overlays are removed:
-1. Reduce `z-1002` back to `z-50` across all `base/ui/` primitives.
+1. Reduce `z-1002` back to `z-50` across all `@langgenius/dify-ui` primitives.
1. Reduce Toast from `z-1003` to `z-51`.
1. Remove this section from the migration guide.
-## React Refresh policy for base UI primitives
-
-- We keep primitive aliases (for example `DropdownMenu = Menu.Root`) in the same module.
-- For `app/components/base/ui/**/*.tsx`, `react-refresh/only-export-components` is currently set to `off` in ESLint to avoid false positives and IDE noise during migration.
-- Do not use file-level `eslint-disable` comments for this policy; keep control in the scoped ESLint override.
+[`packages/dify-ui/README.md`]: ../../packages/dify-ui/README.md
diff --git a/web/docs/test.md b/web/docs/test.md
index 58acccec2a..836ab7ad56 100644
--- a/web/docs/test.md
+++ b/web/docs/test.md
@@ -95,7 +95,7 @@ Use `pnpm analyze-component ` to analyze component complexity and adopt di
- Testing time-based behavior (delays, animations)
- If you mock all time-dependent functions, fake timers are unnecessary
1. **Prefer importing over mocking project components**: When tests need other components from the project, import them directly instead of mocking them. Only mock external dependencies, APIs, or complex context providers that are difficult to set up.
-1. **DO NOT mock base components**: Never mock components from `@/app/components/base/` (e.g., `Loading`, `Button`, `Tooltip`, `Modal`). Base components will have their own dedicated tests. Use real components to test actual integration behavior.
+1. **DO NOT mock base components or dify-ui primitives**: Never mock components from `@/app/components/base/` (e.g., `Loading`, `Input`, `Badge`, `Tag`) or from `@langgenius/dify-ui/*` (e.g., `Button`, `Tooltip`, `Dialog`, `Select`, `Popover`). They have their own dedicated tests. Use real components to test actual integration behavior.
**Why this matters**: Mocks that don't match actual behavior can lead to:
@@ -134,7 +134,7 @@ When using a single spec file:
- ✅ **Import real project components** directly (including base components and siblings)
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
-- ❌ **DO NOT mock** base components (`@/app/components/base/*`)
+- ❌ **DO NOT mock** base components (`@/app/components/base/*`) or dify-ui primitives (`@langgenius/dify-ui/*`)
- ❌ **DO NOT mock** sibling/child components in the same directory
> See [Example Structure] for correct import/mock patterns.
@@ -539,4 +539,4 @@ Test examples in the project:
[Testing Library Best Practices]: https://kentcdodds.com/blog/common-mistakes-with-react-testing-library
[Vitest Documentation]: https://vitest.dev/guide
[Vitest Mocking Guide]: https://vitest.dev/guide/mocking.html
-[index.spec.tsx]: ../app/components/base/button/index.spec.tsx
+[index.spec.tsx]: ../app/components/base/radio/__tests__/index.spec.tsx
diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs
index eab02ec664..62cc63536f 100644
--- a/web/eslint.config.mjs
+++ b/web/eslint.config.mjs
@@ -160,13 +160,6 @@ export default antfu(
'hyoban/no-dependency-version-prefix': 'error',
},
},
- {
- name: 'dify/base-ui-primitives',
- files: ['app/components/base/ui/**/*.tsx'],
- rules: {
- 'react-refresh/only-export-components': 'off',
- },
- },
{
name: 'dify/no-direct-next-imports',
files: [GLOB_TS, GLOB_TSX],
diff --git a/web/eslint.constants.mjs b/web/eslint.constants.mjs
index 46690035fb..5a2330c00e 100644
--- a/web/eslint.constants.mjs
+++ b/web/eslint.constants.mjs
@@ -26,7 +26,7 @@ export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
'**/portal-to-follow-elem',
'**/portal-to-follow-elem/index',
],
- message: 'Deprecated: use semantic overlay primitives from @/app/components/base/ui/ instead. See issue #32767.',
+ message: 'Deprecated: use semantic overlay primitives from @langgenius/dify-ui (popover / dropdown-menu / tooltip / context-menu) instead. See issue #32767.',
},
{
group: [
diff --git a/web/scripts/analyze-component.js b/web/scripts/analyze-component.js
index 04bf0d8f71..36f31f3d3d 100755
--- a/web/scripts/analyze-component.js
+++ b/web/scripts/analyze-component.js
@@ -374,13 +374,13 @@ Options:
Examples:
# Analyze a component and generate test prompt
- pnpm analyze-component app/components/base/button/index.tsx
+ pnpm analyze-component app/components/base/radio/index.tsx
# Output as JSON
- pnpm analyze-component app/components/base/button/index.tsx --json
+ pnpm analyze-component app/components/base/radio/index.tsx --json
# Review existing test
- pnpm analyze-component app/components/base/button/index.tsx --review
+ pnpm analyze-component app/components/base/radio/index.tsx --review
For complete testing guidelines, see: web/docs/test.md
`)