* ♻️ refactor(modal): migrate confirm modals to @lobehub/ui/base-ui Replace all `App.useApp().modal.confirm`, `Modal.confirm` and `AntModal.confirm` call sites with the headless `confirmModal` from `@lobehub/ui/base-ui`, dropping antd-only props (`centered`, `type`, `width`, `okButtonProps.type='primary'`, `okButtonProps.loading`, `classNames.root`) that the base-ui imperative API does not accept. - 82 files touched; `modal.confirm`/`Modal.confirm` call sites now zero - `PageEditor/store/action.ts`: drop `modal` arg from `handleDelete` - `ResourceManager/useUploadFolder`: replace dynamic `import('antd').Modal` - `Eval/DatasetsTab`: migrate `modal.success` to `confirmModal` Part of LOBE-9645 Phase 1. * ♻️ refactor(ui): migrate select/modal call sites to @lobehub/ui/base-ui - Convert imperative-modal factories (createXxxModal + Content split) for apikey, creds (Create/Edit/View), provider (CreateNewProvider), and messenger LinkModal. - Switch Select usages to base-ui Select (Messenger AgentSelect, provider sdkType). - Restructure CreateNewProvider form to vertical layout with manual section titles for tighter spacing; drop FormModal/Form group nesting. - Standardize small ActionIcon sizing via DESKTOP_HEADER_ICON_SMALL_SIZE (WideScreenButton, ToggleRightPanelButton, ContextDropdown, AddNewProvider). - Fix missing title on ResourceManager delete confirm modal so the header (title + close X) renders. - Update react skill and AGENTS.md to require base-ui priority over root @lobehub/ui / antd; expand component table and Common Mistakes with explicit base-ui rules. * ♻️ refactor(ui): swap antd Select to base-ui Select and migrate createStyles to createStaticStyles * ✅ test: update test mocks for base-ui confirmModal migration * ✅ test(e2e): switch delete confirm selector to base-ui dialog role
8.7 KiB
name, description, user-invocable
| name | description | user-invocable |
|---|---|---|
| react | LobeHub React component conventions — base-ui (`@lobehub/ui/base-ui`) first for headless primitives (Select, Modal, DropdownMenu, ContextMenu, Popover, ScrollArea, Switch, Toast, FloatingSheet), then `@lobehub/ui` root, antd as last resort; styling via `antd-style` `createStaticStyles` + `cssVar.*` (zero-runtime preferred over `createStyles` + `token`); routing via `react-router-dom` (not `next/link`). Use when writing or editing any `.tsx` under `src/**`. Triggers on `createStaticStyles`, `createStyles`, `cssVar`, `antd-style`, `Flexbox`, `Center`, `Select`, `Modal`, `Drawer`, `Button`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Popover`, `Switch`, `ScrollArea`, `Toast`, `FloatingSheet`, `Link`, `useNavigate`, `react-router-dom`, `next/link`, `desktopRouter`, `componentMap.desktop`, `.desktop.tsx`, `base-ui`, `@lobehub/ui/base-ui`, 'new component', 'new page', 'edit layout', 'add styles', 'zustand selector', '@lobehub/ui', 'antd import'. | false |
React Component Writing Guide
Styling
| Scenario | Approach |
|---|---|
| Most cases | createStaticStyles + cssVar.* (zero-runtime, module-level) |
| Simple one-off | Inline style attribute |
Truly dynamic (JS color fns like readableColor/chroma) |
createStyles + token — last resort |
Component Priority
src/components— project-specific reusable components@lobehub/ui/base-ui— headless primitives. If the component lives here, use it. Do NOT import the same-named root export.@lobehub/ui— higher-level / antd-wrapping components (only when no base-ui equivalent)- antd — only when neither base-ui nor
@lobehub/uiroot provides it - Custom implementation — true last resort
If unsure about available components, search existing code or check node_modules/@lobehub/ui/es/index.mjs and node_modules/@lobehub/ui/es/base-ui/.
@lobehub/ui/base-ui — always prefer for these
| Component | Import |
|---|---|
Select (+ SelectProps, SelectOption) |
import { Select } from '@lobehub/ui/base-ui'; |
Modal (imperative API) |
import { createModal, confirmModal, useModalContext, type ModalInstance } from '@lobehub/ui/base-ui'; |
DropdownMenu |
import { DropdownMenu } from '@lobehub/ui/base-ui'; |
ContextMenu |
import { ContextMenu } from '@lobehub/ui/base-ui'; |
Popover |
import { Popover } from '@lobehub/ui/base-ui'; |
ScrollArea |
import { ScrollArea } from '@lobehub/ui/base-ui'; |
Switch |
import { Switch } from '@lobehub/ui/base-ui'; |
Toast |
import { Toast } from '@lobehub/ui/base-ui'; |
FloatingSheet |
import { FloatingSheet } from '@lobehub/ui/base-ui'; |
For Modal specifically, see the dedicated modal skill — use the imperative createModal({ content: … }) pattern over the legacy <Modal open … /> declarative pattern. base-ui has its own ModalHost already mounted in SPAGlobalProvider.
Common slip:
import { Select } from '@lobehub/ui'looks fine but it's the antd-backed Select. Use base-ui Select. Same forModal,DropdownMenu, etc.
@lobehub/ui root — use when base-ui has no equivalent
| Category | Components |
|---|---|
| General | ActionIcon, ActionIconGroup, Block, Button, Icon |
| Data Display | Avatar, Collapse, Empty, Highlighter, Markdown, Tag, Tooltip |
| Data Entry | CodeEditor, CopyButton, EditableText, Form, Input, InputPassword, SearchBar, TextArea |
| Feedback | Alert, Drawer |
| Layout | Center, DraggablePanel, Flexbox, Grid, Header, MaskShadow |
| Navigation | Burger, Menu, SideNav, Tabs |
Layout
Use Flexbox and Center from @lobehub/ui. See references/layout-kit.md for full props and examples.
- Use
gapinstead ofmarginfor spacing between flex children - Use
flex={1}to fill available space - Nest Flexbox for complex layouts; set
overflow: 'auto'for scrollable regions
Navigation
For SPA pages, use react-router-dom, NOT next/link.
// ❌ Wrong
import Link from 'next/link';
// ✅ Correct
import { Link, useNavigate } from 'react-router-dom';
Access navigate from stores: useGlobalStore.getState().navigate?.('/settings');
Desktop File Sync Rule
Files with a .desktop.ts(x) variant must be edited in sync. Drift causes blank pages in Electron.
| Base file (web) | Desktop file (Electron) |
|---|---|
desktopRouter.config.tsx |
desktopRouter.config.desktop.tsx |
componentMap.ts |
componentMap.desktop.ts |
After editing any .ts/.tsx: glob for <filename>.desktop.{ts,tsx} in the same directory. If found, apply the equivalent sync-import change.
Routing Architecture
| Route Type | Use Case | Implementation |
|---|---|---|
| Next.js App Router | Auth pages | src/app/[variants]/(auth)/ |
| React Router DOM | Main SPA | desktopRouter.config.tsx + .desktop.tsx (pair) |
Router utilities:
import { dynamicElement, redirectElement, ErrorBoundary } from '@/utils/router';
element: dynamicElement(() => import('./chat'), 'Desktop > Chat');
element: redirectElement('/settings/profile');
errorElement: <ErrorBoundary />;
Common Mistakes
| Mistake | Fix |
|---|---|
Using next/link in SPA |
Use react-router-dom Link |
| Using antd directly | Use @lobehub/ui/base-ui first, then @lobehub/ui |
import { Select } from '@lobehub/ui' |
import { Select } from '@lobehub/ui/base-ui' |
import { Modal } from '@lobehub/ui' + <Modal open> declarative |
createModal / confirmModal from @lobehub/ui/base-ui (see modal skill) |
import { DropdownMenu/Popover/Switch } from '@lobehub/ui' |
Import same name from @lobehub/ui/base-ui instead |
createStyles for static styles |
Use createStaticStyles + cssVar |
Editing only desktopRouter.config.tsx |
Must edit both .tsx and .desktop.tsx |
Using margin for flex spacing |
Use gap prop on Flexbox |
| Accessing zustand store without selector | Use selectors to access store data (see zustand skill) |
Text or icon-text actions built with Flexbox/Text + onClick |
Use Button type={'text'} size={'small'} with icon when needed |