mirror of
https://mirror.skon.top/github.com/langgenius/dify.git
synced 2026-04-20 23:40:16 +08:00
docs(frontend): align docs and comments (#35364)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => () => <div>Loading</div>)
|
||||
vi.mock('@/app/components/base/button', () => ({ children }: any) => <button>{children}</button>)
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({ Button: ({ children }: any) => <button>{children}</button> }))
|
||||
|
||||
// ✅ 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?
|
||||
|
||||
105
packages/dify-ui/README.md
Normal file
105
packages/dify-ui/README.md
Normal file
@@ -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
|
||||
<body>
|
||||
<div className="isolate h-full">{children}</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -174,7 +174,7 @@ const StickyListPane = () => (
|
||||
<div className="mt-1 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="system-md-semibold text-text-primary">Operational queue</div>
|
||||
<p className="mt-1 system-xs-regular text-text-secondary">The scrollbar is still the shared base/ui primitive, while the pane adds sticky structure and a viewport mask.</p>
|
||||
<p className="mt-1 system-xs-regular text-text-secondary">The scrollbar is still the shared dify-ui primitive, while the pane adds sticky structure and a viewport mask.</p>
|
||||
</div>
|
||||
<span className="rounded-lg border border-divider-subtle bg-components-panel-bg-alt px-2.5 py-1 system-xs-medium text-text-secondary">
|
||||
24 items
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -45,7 +45,7 @@ const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
{/* 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 */}
|
||||
<DialogPortal>
|
||||
<div className="pointer-events-none fixed top-1/2 left-1/2 z-1002 flex -translate-x-1/2 translate-y-[165px] items-center gap-1 body-xs-regular text-text-quaternary">
|
||||
<span>{t('onboarding.escTip.press', { ns: 'workflow' })}</span>
|
||||
|
||||
@@ -69,7 +69,15 @@ const NodePanel: FC<Props> = ({
|
||||
doSetCollapseState(state)
|
||||
}, [hideProcessDetail])
|
||||
const titleRef = useRef<HTMLDivElement>(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<Props> = ({
|
||||
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<Props> = ({
|
||||
/>
|
||||
)}
|
||||
<BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('mr-2 shrink-0', inMessage && 'mr-1!')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
||||
<Tooltip>
|
||||
<Tooltip open={isTooltipOpen} onOpenChange={handleTooltipOpenChange}>
|
||||
<TooltipTrigger
|
||||
disabled={!isTitleTruncated}
|
||||
render={(
|
||||
<div
|
||||
ref={titleRef}
|
||||
className={cn(
|
||||
'grow truncate system-xs-semibold-uppercase text-text-secondary',
|
||||
'min-w-0 grow truncate system-xs-semibold-uppercase text-text-secondary',
|
||||
hideInfo && 'text-xs!',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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 <changed-files>
|
||||
|
||||
## 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
|
||||
|
||||
@@ -95,7 +95,7 @@ Use `pnpm analyze-component <path>` 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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
`)
|
||||
|
||||
Reference in New Issue
Block a user