**Goal:** Move custom-editor dialog state and cross-node dialog events out of the editor entry component without changing menu behavior or document HTML.
**Architecture:** A feature-dialog hook owns dialog visibility and payloads. A small event bridge replaces the global `localStorage.setItem` override; custom Slate node renderers publish a typed browser event, while the editor entry subscribes through the hook. Dialog rendering remains behaviorally unchanged in this phase.
**Tech Stack:** React, wangEditor, Ant Design.
## Global Constraints
- Keep toolbar keys, menu order, node types, serialized document data, and dialog UI unchanged.
- Do not add broad test suites; verify with static checks and the admin production build.
- Do not commit changes.
### Task 1: Introduce typed feature-dialog state and event bridge
# WangEditor Vertical Feature Refactor Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Refactor `apps/admin/src/common/wangeditor-customer` into editor-scoped infrastructure plus vertically colocated business features, while preserving every toolbar key, custom node type, serialized HTML shape, modal behavior, and public consumer contract.
**Architecture:** Generic code lives in `core/` and knows nothing about concrete features. Every business feature owns its menu implementations, wangEditor node modules, dialogs, API functions, styles, and local components; a declarative feature registry is the only composition point. Dialog requests travel through the current wangEditor instance rather than `window`, so multiple editors cannot cross-open each other's dialogs.
- Work only in `apps/admin` and root `docs/`; never import another app's `src` directory.
- Preserve the existing JavaScript/JSX style used by the target files while moving them; do not combine this refactor with repository-wide formatting.
- Do not upgrade dependencies or introduce a new state-management/event-bus library.
- Preserve all public `WangEditorCustomer` props: `chapterId`, `bookId`, `contentId`, `html`, `setHtml`, `saveContent`, `gData`, `nowTitle`, `disabled`, and `isView`.
- Preserve the imperative ref contract `{ editor }`.
- Preserve every existing toolbar key, menu key, custom node type, `data-w-e-*` attribute, and node registration order unless a failing characterization test proves the old code never exposed it.
- Preserve existing document HTML byte-for-byte for the same Slate node input; security hardening of legacy HTML is a separate follow-up because it changes persisted content.
- Keep admin on React 18 and Ant Design 5; do not copy code into `apps/learning`.
- Use editor-instance events only. Do not use `window.dispatchEvent`, `window.addEventListener`, `localStorage` writes, or a global store for dialog routing.
- Do not create React roots from menu classes. All dialogs must render below the application's existing providers.
- Keep toolbar layout as shell composition; features provide menu registrations, not sidebar placement.
- Run `pnpm test:admin` and `pnpm build:admin` after every task that changes runtime code.
- The working tree is already dirty. Never reset, checkout, stash, reformat, or commit unrelated user changes. Stage only paths listed by the current task.
---
## Audit Baseline
### Repository state
- Baseline commit: `d180e97` on branch `next`.
- Target directory: 123 files and approximately 16,622 lines.
- The working tree contains an unfinished refactor: `core/`, `features/`, and `registry.js` are untracked; many target files are modified; `apps/admin/src/utils/storage.js` is deleted.
- Baseline verification on 2026-07-21: 11 Node tests pass, 2 configured Vitest tests pass, and the admin production build passes with only the pre-existing large-chunk warning.
- Keep the deletion of `apps/admin/src/utils/storage.js`. Replacing the `localStorage.setItem` monkey patch is correct.
- Replace the unfinished global `window` dialog bridge, shallow feature wrappers, `globalThis` registry flag, and registry-owned Overlay list rather than layering more code on them.
- Keep the current AI `onDestroy` changes only until Task 9 removes independent React roots; then remove `onDestroy` and `BaseModalMenu` together.
- Unrelated formatting changes in `editor-simple`, `preview.less`, and `components/editor` are not part of this refactor except for the explicit public-import edits in Task 7.
### Confirmed design defects
1.`features/` contains indexing shells while implementations remain split across `customer/`, `node/`, `components/`, `menu/`, `practice/`, and `history/`.
2.`index.jsx` still binds every feature-specific `editor.on(...)` handler, so the shell knows all business features.
3.`core/feature-dialogs.js` broadcasts through `window`; two mounted editors subscribe to the same channel and can open the same dialog.
4.`selectionSize` is set asynchronously and read from a stale effect closure. Gallery, tooltip, link, and expand-read requests can keep the initial value `16`.
5. The unfinished registry changes custom node plugin order. These plugins wrap `normalizeNode`, `isInline`, and `isVoid`, so order is an observable compatibility contract.
6.`globalThis.__wangeditor_customer_features_registered__` hides changes during HMR and can collide with another editor bundle.
7.`toolSettingReplace()` uses a document-global selector, a fixed 50 ms timeout, positional indexes, `innerHTML`, and unguarded title appends. It can mutate the wrong editor or add duplicate labels.
8.`setColor()` uses document-global selectors and can recolor another editor instance.
9. AI menus create independent React roots under `document.body`, duplicating provider/lifecycle management.
10.`AIBaiduSearchModal.jsx` and `AISearchModal.jsx` are byte-for-byte identical.
11. Feature API functions are mixed in `utils/request.js`; several are also imported outside the editor, so a deliberate public facade is required before moving them.
12. Large files (`preview.jsx` 800 lines, practice customer form 506 lines, `index.jsx` 481 lines) combine orchestration and UI, but they should be split only after ownership boundaries are stable.
### Compatibility constants
Top-level custom menu registration order must remain:
```js
exportconstexpectedMenuKeys=[
'ImageAutoOnline',
'GalleryAuto',
'GalleryAutoOnline',
'VideoAuto',
'AudioAuto',
'FormulaAuto',
'TooltipAuto',
'ChapterTitle',
'ChapterItem',
'Practice',
'ImageEditor',
'CustomerLink',
'ExpandRead',
'RemoveSpaces',
'ConvertTooltipType',
'AIChat',
'AIRewrite',
'AIExpand',
'AISummary',
'AIPolishing',
'AIPunctuation',
'AIContentInspect',
'AIQuestionSingle',
'AIQuestionMultiple',
'AIQuestionJudge',
'AIQuestionGapFill',
'AIQuestionOpenEnded',
'AIDigitalHuman',
'AITranslate',
'AISearch',
'AIWrite',
'AIImage',
'AIVideo',
'AIBaiduSearch',
'Icon',
]
```
Custom node module registration order and owned node types must remain:
```js
exportconstexpectedNodeTypes=[
'chapterSection',
'chapterHeader',
'chapterImage',
'chapterVideo',
'chapterAudio',
'chapterGallery',
'chapterGalleryInline',
'chapterFormula',
'chapterPractice',
'chapterTooltip',
'chapterLink',
'chapterExpandRead',
'chapterExpandReadSimple',
]
```
Keep the toolbar key array currently exported by `core/editor-config.js` unchanged. Note that `ImageAuto` and `imageWidthChpater100/50/30` are registered by the image node module, not by the top-level menu list.
### Explicitly deferred findings
- Several node serializers interpolate unescaped values into HTML attributes, and preview/history render persisted HTML with `dangerouslySetInnerHTML`. Audit and sanitize this in a separate compatibility/migration plan.
- Preview and practice contain additional large-component decomposition opportunities. This plan colocates them first and splits only obvious orchestration boundaries.
- Bundle-size optimization is limited to lazy dialog bodies after behavior is stable. Do not change node/menu registration loading semantics.
---
## Target Structure
```text
apps/admin/src/common/wangeditor-customer/
├── index.jsx
├── public-api.js
├── core/
│ ├── define-editor-feature.js
│ ├── create-feature-registry.js
│ ├── register-editor-features.js
│ ├── editor-dialog.js
│ ├── useEditorDialog.js
│ ├── EditorDialogHost.jsx
│ ├── create-dialog-menu.js
│ ├── editor-config.js
│ ├── selection.js
│ └── register-editor-features.js
├── shell/
│ ├── WangEditorCustomer.jsx
│ ├── EditorHeader.jsx
│ ├── EditorSidebar.jsx
│ ├── toolbar-layout.js
│ ├── useEditorLifecycle.js
│ ├── useToolbarPresentation.js
│ └── styles.less
├── features/
│ ├── index.js
│ ├── image/
│ ├── gallery/
│ ├── video/
│ ├── audio/
│ ├── chapter-structure/
│ ├── formula/
│ ├── practice/
│ ├── tooltip/
│ ├── link/
│ ├── expand-read/
│ ├── content-tools/
│ ├── icon/
│ ├── ai-text/
│ ├── ai-question/
│ ├── ai-media/
│ ├── ai-research/
│ ├── preview/
│ └── history/
└── shared/
├── editor-settings.js
├── node-path.js
├── html.js
├── request.js
├── iconfont.js
├── dialog-form.less
└── online-image/
├── OnlineImageList.jsx
└── styles.less
```
Feature-local subdirectories such as `components/` or `dialogs/` are allowed only when a feature has more than five implementation files. No root-level `customer/`, `node/`, `menu/`, `components/`, `practice/`, or `history/` directory remains at the end.
### Core interfaces
Every feature must export this shape from `feature.js`:
shell -> features registry -> feature implementation
shell -> core
feature -> core/shared
core -X-> feature
```
Preview may import the public API of practice and expand-read because it is an integration feature; no other feature may deep-import another feature's components.
Expected: FAIL because the current feature registry changes menu/node order and lacks the target exports.
-[]**Step 3: Convert every feature descriptor to explicit order metadata**
Use these node orders exactly:
```text
10 chapter-structure/chapterSection
20 chapter-structure/chapterHeader
30 image/chapterImage
40 video/chapterVideo
50 audio/chapterAudio
60 gallery/chapterGallery
70 gallery/chapterGalleryInline
80 formula/chapterFormula
90 practice/chapterPractice
100 tooltip/chapterTooltip
110 link/chapterLink
120 expand-read/chapterExpandRead
130 expand-read/chapterExpandReadSimple
```
Use the top-level menu array index multiplied by 10 as each menu order. During this task descriptors may import implementations from the legacy directories; later tasks move those files without changing descriptor metadata.
Add a comment that feature-registration HMR requires a full page reload; do not silently accept changed registrations.
-[]**Step 6: Switch `index.jsx` to the new register function**
Import `registerEditorFeatures` from `./core/register-editor-features` and `editorFeatureRegistry` from `./features`. Keep `registry.js` temporarily as a deprecated `editorOverlays` compatibility export so unmigrated dialogs continue to work. It must contain no menu or node registration logic and is deleted after the last legacy Overlay migrates.
-[]**Step 1: Add a regression test for synchronous font-size payloads**
Create a fake editor selection and DOM adapter, call `getSelectionFontSize(editor)`, and assert it returns the computed integer immediately. Use `18` as the fallback for missing selection or invalid CSS.
-[]**Step 2: Implement `getSelectionFontSize` as a pure return-value helper**
Move the existing DOM lookup from `listenNodeStyle`, but return the parsed value instead of calling React state. Never store selection font size in `WangEditorCustomer` state.
-[]**Step 3: Capture exact serializer snapshots before moving files**
Create `apps/admin/src/test/wangeditor-customer/node-serialization.test.ts`. Import the six legacy node modules, generate HTML through each `elemsToHtml` handler, assert `toMatchSnapshot()`, then parse the generated element and assert the identifying fields listed in Step 8. Run once with `-u` to create the committed baseline snapshots:
Expected: the test and a `__snapshots__/node-serialization.test.ts.snap` file are created before any node file moves. After moving files, update imports only and never update these snapshots during the refactor.
-[]**Step 4: Move files without changing their bodies first**
-[]**Step 5: Repair relative imports and feature descriptors**
Each moved implementation may import only its own feature, `../../core`, `../../shared`, or application aliases, except for temporary imports from legacy `utils/request`, `utils/setting`, and `utils/iconfont`; Tasks 5-7 remove those compatibility paths. Image and gallery import `OnlineImageList` from `../../shared/online-image/OnlineImageList`. All migrated dialogs import the unchanged shared dialog CSS from `../../shared/dialog-form.less` until Task 10 splits feature-specific rules.
-[]**Step 6: Replace menu-to-shell events with direct dialog requests**
git commit -m"refactor(admin): colocate editor media features"
```
---
### Task 5: Migrate Structural and Inline Content Features
**Files:**
- Move chapter title/item, tooltip, link, and expand-read implementations into their feature directories.
- Move `ConvertTooltipType` into tooltip.
- Create feature-local dialogs and API files.
- Extend node serialization tests.
**Interfaces:**
- Consumes: standard dialog contract and editor-scoped dialog requests.
- Produces: self-contained `chapter-structure`, `tooltip`, `link`, and `expand-read` features.
-[]**Step 1: Add the six legacy serializers to the existing snapshot test**
Before moving files, add chapter section/header, tooltip, link, and both expand-read node modules to `node-serialization.test.ts`. Run with `-u` once, inspect the new snapshots, and commit them with this task. Do not update the six Task 4 snapshots.
### Task 6: Migrate Practice as One Business Capability
**Files:**
- Move: `customer/Practice.js`, `node/practice.js`, `components/practice.jsx`, and the entire legacy `practice/` directory into `features/practice/`.
- Create: `features/practice/api.js`
- Modify: practice Redux interaction only at the dialog boundary.
- Extend: node serialization and dialog tests.
**Interfaces:**
- Consumes: standard runtime/dialog contract.
- Produces: `practice.insert` and `practice.settings` dialogs owned entirely by practice.
-[]**Step 1: Capture the legacy practice serializer snapshot**
Add `chapterPractice` to `node-serialization.test.ts`, run that file with `-u`, and inspect the new snapshot before moving the node file. Do not update earlier snapshots.
-[]**Step 2: Add a dialog-transition test**
Assert that opening `practice.settings` replaces `practice.insert` in the single dialog host and carries `{ practiceNum, title, theme }` without opening two modals.
-[]**Step 4: Consolidate chapter-practice endpoints in `api.js`**
Move `addChapterTopic`, `getChapterTopic`, and `delChapterTopic` from legacy `utils/request.js` into practice. Keep question-bank endpoints in `question-api.js` because they serve a separate backend resource.
-[]**Step 5: Remove ref coupling between insert and settings dialogs**
Do not pass `practiceRef.current.nodes`. `PracticeSettingsDialog` can derive matching nodes from `runtime.editor` and `payload.practiceNum`. Opening settings dispatches the existing Redux `setPracticeRandom` action once; closing clears it once.
-[]**Step 6: Route practice requests directly**
```text
Practice menu -> practice.insert
practice node click -> practice.settings
```
The node click request carries `{ practiceNum, title, theme }`. The insertion flow closes `practice.insert`; it does not automatically open settings unless the legacy callback did so for that exact action.
-[]**Step 7: Add `chapterPractice` serialization field assertions**
Assert round-trip preservation of `title`, `practiceNum`, `bookId`, `chapterId`, and `theme`.
They import named values from `@/common/wangeditor-customer/public-api`. Keep the default component imports in chapters and student-book unchanged until Task 10.
Preview imports practice's public `TopicItem` export and the practice/expand-read API exports. It must not deep-import private practice state or dialog components.
-[]**Step 7: Verify no external consumer reaches internal directories**
AIContentInspect, AITranslate, AIModal, AIChatDrawer and its Less
ai-question: five AIQuestion menus and AIQuestionModal
ai-media: AIDigitalHuman, AIImage, AIVideo and their dialogs/Less; digital-human-data was already moved in Task 7
ai-research: AISearch, AIBaiduSearch, AIWrite, search/write dialogs and search Less
```
Keep menu order metadata equal to the compatibility list even for registered menus absent from the visible toolbar.
-[]**Step 4: Replace `getValue()` React elements with dialog payloads**
Use ids:
```text
AIChat -> ai-text.chat
rewrite/expand/summary/polish/punctuation/inspect/translate -> ai-text.action with { action }
five question menus -> ai-question.generate with { action }
AIDigitalHuman -> ai-media.digital-human
AIImage -> ai-media.image
AIVideo -> ai-media.video
AISearch/AIBaiduSearch -> ai-research.search with { provider }
AIWrite -> ai-research.write
```
-[]**Step 5: Make every AI dialog controlled**
Replace internal initial `isModalOpen = true` state with the standard `open` prop. Every cancel/complete path calls `onClose`. Remove `afterClose={onDestroy}` and the `onDestroy` prop.
Keep one `SearchDialog.jsx`. Both search menu ids render it with different `provider` payloads; if provider is not currently used by the request hook, preserve that fact rather than inventing behavior.
-[]**Step 7: Delete the independent-root infrastructure**
git commit -m"refactor(admin): host editor AI dialogs in app tree"
```
---
### Task 10: Reduce the Editor Shell to Orchestration
**Files:**
- Move: `index.jsx` implementation to `shell/WangEditorCustomer.jsx`.
- Create: `EditorHeader.jsx`, `EditorSidebar.jsx`, `useEditorLifecycle.js`, `useToolbarPresentation.js`, and `toolbar-layout.js`.
- Move/split: `index.less` into `shell/styles.less` and feature-local styles.
- Modify: public `index.jsx` to a facade.
**Interfaces:**
- Consumes: complete feature registry and dialog host.
- Produces: a shell containing no feature-specific menu, node, API, or dialog imports.
-[]**Step 1: Add a source-boundary test**
Create `apps/admin/src/test/wangeditor-customer/source-boundary.test.ts` that reads the shell source and rejects imports containing `/features/` except `features/index`, plus the strings `ImageMenuClick`, `GalleryMenuClick`, `PracticeMenuClick`, `window.dispatchEvent`, and `document.querySelector(`.
-[]**Step 2: Extract the header**
`EditorHeader` owns title, autosave label, save, preview, and history buttons. It receives semantic props only:
```js
{
title,
autosaveTime,
isView,
canSave,
canPreview,
canViewHistory,
onSave,
onPreview,
onHistory,
}
```
-[]**Step 3: Extract the sidebar**
`EditorSidebar` owns tab state, Toolbar rendering, and style color choices. It receives `editor`, `toolbarConfig`, and `onApplyChapterColor`.
-[]**Step 4: Extract editor lifecycle**
`useEditorLifecycle` owns default marks, initial content, enable/disable behavior, and destruction. Register each emitter listener with a named function and call `editor.off` in cleanup. At this stage no feature-specific listeners should remain.
-[]**Step 5: Scope toolbar presentation to its ref**
Replace the fixed timeout/global query with `useToolbarPresentation(toolbarRef, active)`. Use `requestAnimationFrame`, `toolbarRef.current.querySelectorAll`, `textContent`, and a guard that does not append a second `.title` element.
-[]**Step 6: Scope chapter recoloring to the current editor**
-[]**Step 8: Split CSS by ownership without changing selectors**
Move generic shell/editor-canvas rules to `shell/styles.less`; dialog form primitives to `shared/dialog-form.less`; gallery/image/formula-specific rules to those features; practice/history existing styles stay feature-local. Preserve selector text and declaration order during the move.
Expected: no dialog-routing or storage-monkey-patch matches. Legitimate domain field names such as Slate node property `practiceNum` may remain; inspect and distinguish them from storage keys.
-[]**Step 4: Verify registry and toolbar contracts**
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为六个 AI 文本菜单提供可比较、可编辑、最多保留三个版本且安全替换初始选区的统一工作台。
Run: `pnpm --filter @ebook/admin exec vitest run src/test/wangeditor-customer/ai-selection-workbench.test.ts`
Expected: FAIL because `replaceCapturedSelection` does not exist.
-[]**Step 3: Write minimal implementation**
`captureTextSelection` must copy `editor.selection` before opening the modal and reject empty `editor.getSelectionText()`. `replaceCapturedSelection` must assign the saved selection only after verifying it has both `anchor` and `focus`, then use the existing `SlateTransforms.removeNodes` / paragraph insertion behavior. It must return `false` rather than operating at the current caret when validation fails.
-[]**Step 4: Run test to verify it passes**
Run: `pnpm --filter @ebook/admin exec vitest run src/test/wangeditor-customer/ai-selection-workbench.test.ts`
Run: `pnpm --filter @ebook/admin exec vitest run src/test/wangeditor-customer/ai-selection-workbench.test.ts`
Expected: FAIL because the legacy dialog has no comparison labels or replacement button text.
-[]**Step 3: Implement the minimal workbench**
Render an `antd``Modal` using `open` and `onClose`, with `width="min(1200px, 92vw)"`, `styles={{ body: { height: 'calc(80vh - 120px)' } }}`, a read-only left `TextArea`, editable right `TextArea`, result tabs, and footer buttons.
On mount, capture the selection. If absent, call `message.warning('请先选中需要处理的文本')`, call `onClose`, and do not call `post`. Start the initial AI request only after capture succeeds. As stream content changes, update only the newly-created active version. “重新生成” appends a fresh active version; retain only three. “替换选中内容” calls `replaceCapturedSelection`; on `false`, show `message.error('原始选区已失效,请重新选择文本后再试')` and retain the modal.
-[]**Step 4: Run UI tests to verify they pass**
Run: `pnpm --filter @ebook/admin exec vitest run src/test/wangeditor-customer/ai-selection-workbench.test.ts`
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make paragraph deletion and native media editing behave like a document editor without replacing wangEditor native media nodes.
**Architecture:** Keep native `video` nodes as the persisted representation for video and audio. Add feature-local media commands and hoverbar mappings; centralize empty-paragraph normalization and cursor repair in the editor shell.