This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Lexical-based rich editor ecosystem. Modular packages: core editor, UI primitives, content renderers, plugins, extensions, and integration bundles.
Build: Vite 8 + Vanilla Extract CSS-in-TS. ESM only (.mjs). TypeScript 5.9. Lexical 0.47. pnpm 11 (pinned via packageManager). Node >= 20.
pnpm 11 config note: pnpm 11 no longer reads the
pnpmfield inpackage.jsonorpublic-hoist-patternfrom.npmrc. Hoist patterns and workspace settings live inpnpm-workspace.yaml(publicHoistPattern,allowBuilds).react/react-domare public-hoisted there so transitive deps (e.g.tunnel-ratvia@excalidraw/excalidraw) resolve them whenrich-composebundles the standalonelitexml-html-preview-client.
pnpm dev # demo/ dev playground (all features)
pnpm --filter @haklex/rich-editor dev:build # watch mode for core
pnpm build # turbo build (all)
pnpm build:packages # workspace packages under packages/
pnpm --filter @haklex/rich-editor build # single package
pnpm lint # eslint . --fix (repo root)
pnpm test # vitest run (all suites)
pnpm typecheck # tsc --noEmit in packages/*
npx eslint path/to/file.ts # lint single file
npx prettier --write path/to/file.ts # format single file
npx vitest run packages/rich-renderer-katex/tests/ # run specific tests
pnpm release:rich # bump + build + publish @haklex/*- ESLint:
@lobehub/eslint-config(rooteslint.config.mjs). Key rule:react/no-use-context: 'error'— must use React 19use(Context)notuseContext(). - Versioning: All
@haklex/*share one version (frompackages/rich-editor/package.json), managed bybumpp. - Pre-commit:
simple-git-hooks+lint-staged(prettier + eslint on staged files). - Build: Shared Vite config in
vite.shared.ts(createViteConfig()). Auto-externalizes deps/peerDeps. Output: ESM.mjs+.d.ts. Vanilla Extract for CSS-in-TS (zero runtime).
The ecosystem splits nodes and renderers into static (read-only) and edit (editor-only) variants for tree-shaking.
- Static Node/Renderer: Display-only. No
@haklex/rich-editor-uiorlucide-reactimports. Used byRichRenderer. - Edit Node/Renderer: Extends static variant. Adds heavy deps (Popover, DropdownMenu, NestedComposer). Used by
RichEditor.
Split criteria: Only split when the edit path introduces heavy imports absent from the static path.
Registration:
@haklex/rich-editorsrc/config.ts→ static nodes →RichRenderer@haklex/rich-editorsrc/config-edit.ts→ edit nodes →RichEditor@haklex/rich-composesrc/modules/<name>/module.ts→ per-featureRichRendererModule(recommended composition API)- Downstream apps compose their own
RendererConfigfromcompose/modules/*(or inline a per-app preset). The legacyenhancedRendererConfigshipped by@haklex/rich-kit-shirois gone.
Adding a new node with edit UI: Create FooNode with RendererWrapper in decorate(), then FooEditNode extends FooNode overriding decorate() with edit decorator. Register base in config.ts, edit in config-edit.ts. Create FooRenderer (static) + FooEditRenderer (edit). Add a module under rich-compose/src/modules/<name>/.
@haklex/rich-editor (core: nodes + plugins + styles + contexts)
├── @haklex/rich-editor-ui (Dialog, Dropdown, Popover via @base-ui/react)
├── @haklex/rich-headless (server-side node registry, zero React)
└── @haklex/rich-style-token (theme tokens, CSS variables, variant presets)
@haklex/rich-compose (modular composition + SSR engine: composeRenderer, RichRenderer, per-feature modules)
@haklex/rich-ext-{chat,code-snippet,embed,excalidraw,gallery,nested-doc} (heavy extensions)
@haklex/rich-plugin-{block-handle,floating-toolbar,link-edit,mention,slash-menu,table,toolbar}
@haklex/rich-renderer-{alert,banner,codeblock,image,linkcard,mention,mermaid,ruby,video,katex}
@haklex/rich-diff → @haklex/rich-diff-core (standalone diff viewer)
@haklex/rich-agent-core → @haklex/rich-litexml (headless agent protocol; used by rich-ext-ai-agent)
@haklex/rich-agent-chat → @haklex/rich-agent-core, @haklex/rich-diff-core (chat UI)
@haklex/rich-editor-demo (dev playground, workspace at demo/)
- Context Provider value stability: Never pass object literals as context
value. Primitives OK directly; objects must beuseMemo'd. - Neutral colors: Use
#737373/#a3a3a3(neutral-500/400) for muted text/borders. No tinted grays (slate, zinc). - Styling: Vanilla Extract (
*.css.ts) throughout. No Tailwind in editor packages. - Variant system:
article(sans-serif, 16px),note(CJK serif, 16px),comment(sans-serif, 14px). Controlled byvariantprop +ColorSchemeContext.
Shiroi / Yohaku (Next.js frontends)
└── @haklex/{rich-compose, rich-editor, rich-ext-*, rich-renderer-*, rich-style-token}
Compose modules + a per-app ShiroEditor/ShiroRenderer wrapper
mx-core/apps/admin (React dashboard, ../mx-core/apps/admin)
└── @haklex/rich-editor + @haklex/rich-renderer-* as ordinary React components
mx-core (NestJS backend, ../mx-core)
└── @haklex/rich-headless
Server-side: createHeadlessEditor() + allHeadlessNodes + $toMarkdown() for Lexical JSON → Markdown
Release flow: pnpm release:rich → bump + build + publish to npm. Then update pinned versions in mx-core/apps/admin/package.json (dashboard) and mx-core/apps/core/package.json (server).
When creating a new Lexical node type:
- Node class in
packages/rich-editor/src/nodes/ - Static renderer in
packages/rich-renderers/ - Edit renderer in
packages/rich-renderers-edit/(if edit UI needed) - Register in
config.ts(static) andconfig-edit.ts(edit) - XML writer in
packages/rich-litexml/src/writers/(required for AI agent) - XML reader in
packages/rich-litexml/src/readers/(required for AI agent) - Register in
packages/rich-litexml/src/default-registry.ts - Update
packages/rich-ext-ai-agentsystem prompt if the node is agent-creatable - For a new
rich-ext-*/rich-renderer-*package with styles: add it toMODULE_TO_PACKAGEinpackages/rich-compose/scripts/build-css.ts(otherwiserich-compose/style/<module>.cssdoes not exist for downstream) - Downstream wiring needs four parts: node registration, renderer config entry, plugin mount, and the package's
style.cssimport