From 0dfa1e89ce7c2d226177a4dfb207db2a7660872f Mon Sep 17 00:00:00 2001 From: yexiyue Date: Wed, 13 May 2026 09:24:27 +0800 Subject: [PATCH 1/2] feat(editor): wire host to plugin SDK v0.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt `@swarmnote/editor-core` v0.1 plugin SDK in the SwarmNote host: - `NoteEditor.tsx` constructs `plugins[]` from preferences snapshot (`buildEditorPlugins`); `host` object replaces deprecated top-level `imageResolver` / `uploadFile` - `refreshBlockImagesEffect` now imported from `@swarmnote/editor-core/plugins/blockImage` subpath (main entry no longer re-exports it) Preferences: - `preferencesStore` adds `enabledPlugins: EditorPluginId[]` (defaults to all 8 enabled) and `codeBlockMode`. `migrateLegacyFeatures` defensively translates v0.0.x `features.*` keys and clears them (idempotent) - New "编辑器插件" section in `routes/settings/general.tsx` lists per-plugin Switch + codeBlock mode Select i18n: 21 new English strings backfilled in `src/locales/en/messages.po`. Documentation: - `dev-notes/plans/editor-plugin-architecture.md` marked as implemented; v0.1 BREAKING change summary appended as release-notes / PR description source - `dev-notes/knowledge/editor.md` gains a Plugin SDK section with the key constraints (subpath imports, host capabilities, boot-time freeze) BREAKING CHANGE: Editor `features` 7 fields removed upstream in `@swarmnote/editor-core` v0.1; capability enablement now lives in `plugins[]`. v0.0.x preferences containing `features.*` keys are auto-migrated via `migrateLegacyFeatures`. Refs: OpenSpec change `add-editor-plugin-sdk-v01`; pairs with sibling commit 37db01c. --- dev-notes/knowledge/editor.md | 15 + dev-notes/plans/editor-core-host-boundary.md | 2 + dev-notes/plans/editor-event-model-draft.md | 5 +- .../plans/editor-extension-points-draft.md | 12 + dev-notes/plans/editor-open-source-rfc.md | 12 +- dev-notes/plans/editor-plugin-architecture.md | 280 ++++++++++++++++++ src/components/editor/NoteEditor.tsx | 68 ++++- src/locales/en/messages.po | 184 ++++++++++-- src/locales/zh/messages.po | 184 ++++++++++-- src/routes/settings/general.tsx | 133 ++++++++- src/stores/preferencesStore.ts | 113 +++++++ 11 files changed, 963 insertions(+), 45 deletions(-) create mode 100644 dev-notes/plans/editor-plugin-architecture.md diff --git a/dev-notes/knowledge/editor.md b/dev-notes/knowledge/editor.md index 4fa170f..c24258d 100644 --- a/dev-notes/knowledge/editor.md +++ b/dev-notes/knowledge/editor.md @@ -6,6 +6,21 @@ 调用链:`React (NoteEditor) → createEditor() → CM6 EditorView → ySync extension ↔ Y.Text` +### Plugin SDK (v0.1) + +`@swarmnote/editor-core` v0.1 起以 plugin SDK 形式重构。8 个功能(math / table / mermaid / admonition / codeBlock / blockImage / rawHtml / smartPaste)**默认不启用**,宿主必须通过 `createEditor(..., { plugins: [...] })` 显式传入。 + +**关键约束**: +- Plugin 工厂从 subpath import:`@swarmnote/editor-core/plugins/`,main 入口不再 re-export +- 宿主能力(resolveImage / uploadFile / openLink)通过 `host: EditorHostCapabilities` 注入;旧顶层 `imageResolver` / `uploadFile` 已 `@deprecated`(仍工作但会桥接) +- Plugin 启用状态在 `createEditor` 时 freeze,**切换 plugin 启用状态后必须新开文档 / 重启应用才能生效** +- `refreshBlockImagesEffect` 从 main 入口下架,改从 `@swarmnote/editor-core/plugins/blockImage` 拿 +- `EditorFeatureToggles` 仅保留 5 个字段:`markdownHighlight` / `markdownDecorations` / `inlineRendering` / `search` / `collaboration` + +**宿主侧 plugin 配置**:`src/stores/preferencesStore.ts::enabledPlugins` + `codeBlockMode` 持久化用户启用状态;`migrateLegacyFeatures` 防御性处理 v0.0.x 旧 `features.*` key(idempotent)。 + +**详见**:[dev-notes/plans/editor-plugin-architecture.md](../plans/editor-plugin-architecture.md)、`openspec/changes/add-editor-plugin-sdk-v01/` + - 编辑器内核:`@swarmnote/editor-core`(独立仓 [`swarm-apps/swarmnote-editor`](https://github.com/swarm-apps/swarmnote-editor),pnpm workspace monorepo),桌面端和移动端共享 - 桌面端 React 容器:`src/components/editor/NoteEditor.tsx` - 文档大纲:`src/components/editor/DocumentOutline.tsx`(基于 `extractHeadings`) diff --git a/dev-notes/plans/editor-core-host-boundary.md b/dev-notes/plans/editor-core-host-boundary.md index 4760b8a..ef965da 100644 --- a/dev-notes/plans/editor-core-host-boundary.md +++ b/dev-notes/plans/editor-core-host-boundary.md @@ -1,5 +1,7 @@ # Editor Core / Interaction / Host Boundary 清单 +> **更新(2026-05-12)**:本文档下方"interaction core"是否单独成包的问题已在 [editor-plugin-architecture.md](./editor-plugin-architecture.md) 中闭环——**interaction 作为 first-party plugin 留在 `editor-core/plugins/interactions/`**,与第三方插件共用同一 `EditorPlugin` API。本文档其余分层归类仍有效。 + ## 目的 这份清单用于回答一个非常具体的问题: diff --git a/dev-notes/plans/editor-event-model-draft.md b/dev-notes/plans/editor-event-model-draft.md index be78b2f..96a3f40 100644 --- a/dev-notes/plans/editor-event-model-draft.md +++ b/dev-notes/plans/editor-event-model-draft.md @@ -1,5 +1,7 @@ # EditorEventType 演进草案 +> **更新(2026-05-12)**:v0.1 采纳本文提出的"三层分类"路线。最终类型形态、`MermaidZoomRequest` 归类、`SlashTriggerChange` / `WikiLinkTriggerChange` / `SelectionToolbarChange` 三个 interaction event 的 `@unstable` 标记,均已在 [editor-plugin-architecture.md #editor-event-三层分类-v01](./editor-plugin-architecture.md#editorevent-三层分类v01) 中收敛。本文档保留为推导过程。 + ## 目的 这份草案回答的问题是: @@ -464,8 +466,9 @@ type SelectionPopoverAnchorEvent = { | `SearchStateChange` | Core Event | 保留 | | `CollaborationUpdate` | Core Event | 保留 | | `LinkOpen` | Core Event | 保留 | -| `Remove` | Core Event / 待确认 | 需要确认长期定位 | +| `Remove` | Platform Convenience Event | 2026-05-12 定位:作为 platform convenience | | `TableContextMenu` | Platform Convenience Event | 不建议当成核心协议代表 | +| `MermaidZoomRequest` | Platform Convenience Event | 2026-05-12 补:携带 `renderedSvg: string` HTML,Web 假设强,跨端不稳定 | --- diff --git a/dev-notes/plans/editor-extension-points-draft.md b/dev-notes/plans/editor-extension-points-draft.md index 5a54e6d..c872c69 100644 --- a/dev-notes/plans/editor-extension-points-draft.md +++ b/dev-notes/plans/editor-extension-points-draft.md @@ -1,5 +1,17 @@ # Editor Extension Points Draft +> ⚠️ **SUPERSEDED(2026-05-12)** +> +> 本文档抽象出的"六类扩展点"已被具体的 `EditorPluginContext` 接口取代。**最终架构请见 [editor-plugin-architecture.md](./editor-plugin-architecture.md)**。 +> +> 主要变化: +> +> - 六类抽象 → `registerCommands` / `registerCmExtensions` / `registerMarkdownRenderer` / `host` (stable)+ `registerSlashItems` / `registerTrigger` / `on` (@unstable)的具体接口 +> - "Phase A 应用内可扩展 → Phase B 包级扩展 → Phase C 运行时插件"路线 → 直接走 **Model B 全量**,内置 plugin 一步到位按 `EditorPlugin` 重写 +> - "EditorHostCapabilities 草案"已 finalize 并写入 plugin architecture 文档 +> +> 本文档保留为思考过程(thinking trail),不再作为最终架构来源。 + ## 目的 这份草案回答的问题是: diff --git a/dev-notes/plans/editor-open-source-rfc.md b/dev-notes/plans/editor-open-source-rfc.md index 5f27ddf..b6aeced 100644 --- a/dev-notes/plans/editor-open-source-rfc.md +++ b/dev-notes/plans/editor-open-source-rfc.md @@ -1,5 +1,7 @@ # RFC: 将 SwarmNote 编辑器重构为可开箱即用的开源 Markdown 编辑器 +> **更新(2026-05-12)**:本 RFC 仍是顶层路线说明。具体的 plugin 架构、包边界、`EditorPluginContext` 形状、内置 plugin 清单与分阶段实施细节已在 [editor-plugin-architecture.md](./editor-plugin-architecture.md) 中收敛。下方"包结构建议"与"分阶段实施建议"两节按该决定对齐。 + ## 背景 当前 SwarmNote 编辑器已经具备一套比较完整的能力: @@ -262,6 +264,8 @@ graph TD ## 包结构建议 +> **2026-05-12 收敛**:v0.1 选择**单包 + subpath export** 路线——所有内置插件(math/table/mermaid 等)以 `@swarmnote/editor-core/plugins/` 形式提供,未来抽成独立 npm 包仅需改 import 路径。`editor-react` / `editor-react-native` 仍作为后续 sibling 包推进。详见 [editor-plugin-architecture.md](./editor-plugin-architecture.md#subpath-export-策略)。 + 第一阶段不一定立即拆成独立仓库,但建议先按以下逻辑收敛目录边界。 ### 候选结构 @@ -592,6 +596,8 @@ createEditor(parent, { ## 分阶段实施建议 +> **2026-05-12 收敛**:原 Phase 1-5 描述偏边界整理路线;实际选择走 **Model B 全量 + 内置 plugin 全部重写** 的更激进路线。最终 Phase 表见 [editor-plugin-architecture.md 后续路线](./editor-plugin-architecture.md#后续路线均为无破坏性扩展)。本节保留为思考记录。 + ### Phase 1:清理边界 目标:先让 core / host / UI 的职责更干净。 @@ -674,9 +680,9 @@ createEditor(parent, { ## 开放问题 -1. `packages/editor` 是否继续沿用 submodule 形式,还是在开源前迁移为独立 monorepo/workspace? -2. interaction core 是放进 `editor-core`,还是单独拆成 `editor-interactions`? -3. React Web 默认 UI 是否要内置一套“官方工具栏/菜单”,还是只提供 hooks 与 headless 状态? +1. ~~`packages/editor` 是否继续沿用 submodule 形式,还是在开源前迁移为独立 monorepo/workspace?~~ **已闭环**:迁移为 sibling 仓 `swarmnote-editor` 的 pnpm workspace monorepo(见 [README](../../../swarmnote-editor/README.md))。 +2. ~~interaction core 是放进 `editor-core`,还是单独拆成 `editor-interactions`?~~ **已闭环(2026-05-12)**:作为 first-party plugin 留在 `editor-core/plugins/interactions/`,与第三方插件共用 `EditorPlugin` API。详见 [editor-plugin-architecture.md](./editor-plugin-architecture.md)。 +3. React Web 默认 UI 是否要内置一套"官方工具栏/菜单",还是只提供 hooks 与 headless 状态? 4. RN 端是否需要与 Web 保持相同交互形态,还是只共享语义、不共享视觉表现? 5. collaboration 是否作为第一版开源能力公开,还是放到第二阶段? 6. 是否需要在第一版就提供受控(controlled)模式与非受控(uncontrolled)模式两套 API? diff --git a/dev-notes/plans/editor-plugin-architecture.md b/dev-notes/plans/editor-plugin-architecture.md new file mode 100644 index 0000000..a42c5c6 --- /dev/null +++ b/dev-notes/plans/editor-plugin-architecture.md @@ -0,0 +1,280 @@ +# Editor Plugin Architecture (v0.1) + +> **状态**:✅ 已实施于 OpenSpec change `add-editor-plugin-sdk-v01`(2026-05-13)。本文档的设计决定已在 sibling 仓 [`swarmnote-editor`](https://github.com/swarm-apps/swarmnote-editor) 与本仓 host 中落地。本文档仍保留为权威设计说明,实施细节以 `openspec/changes/add-editor-plugin-sdk-v01/` 的 proposal / design / specs 为准。 + +## 目的 + +本文档是 2026-05-12 一次 explore 讨论后收敛出的 `@swarmnote/editor-core` v0.1 插件架构决定。 + +它是本目录里**最权威**的 plugin 架构来源,统一了原本散落在四份草案中的设计意图,并对 RFC 的若干开放问题给出了明确答案。 + +| 文档 | 与本文档的关系 | +|---|---| +| `editor-open-source-rfc.md` | 顶层 RFC;其"包结构"与"分阶段实施"两节按本文档对齐 | +| `editor-core-host-boundary.md` | 边界归类参考;interaction 归属问题在本文档闭环 | +| `editor-event-model-draft.md` | 事件演进路径;interaction events 的最终类型见本文档 | +| `editor-extension-points-draft.md` | **superseded**;六类扩展点的抽象描述被 `EditorPluginContext` 的具体接口取代,保留为 thinking trail | + +--- + +## TL;DR + +- v0.1 直接走 **Model B 全量**:编辑器同时是 plugin host,并提供完整 plugin SDK +- 内置 `math / table / mermaid / admonition / codeBlock / blockImage / rawHtml / smartPaste` 全部按 `EditorPlugin` 重写,与未来第三方插件共用同一 API +- 这些"会被拆出去"的能力以 **subpath export** 形式留在 `editor-core`(`@swarmnote/editor-core/plugins/math` 等),未来抽成独立 npm 包仅需"换 import 路径",不破坏 plugin API +- interaction core(slash / wikilink / selection toolbar)作为 **first-party plugin** 留在 `editor-core`,与第三方走同一 plugin API → RFC 开放问题 2 闭环 +- `EditorSettings.features` 仅保留**真留在 core 的能力**(`markdownHighlight / markdownDecorations / inlineRendering / search / collaboration`),其他开关删除——是否启用 = 是否在 `plugins: []` 中传入 +- Plugin metadata 只锁 `id` + 可选 `version`,其余字段留给 Model C +- `EditorCommandSpec` 精简版:`{ id, title?, description?, icon?, when?, run }` +- 冲突策略:**last-wins + dev warning** + +--- + +## 选型决策与 why + +| 决策 | 选择 | Why | +|---|---|---| +| 预留深度 | 深度② 类型 + 空架子 | 锁住 API 形状,但不写 runtime 用不到的代码 | +| 插件化档位 | Model B 全量 | A 太浅(plugin API 没被验证过),C 太重(manifest/sandbox 不是 v0.1 问题)。B 是 Tiptap / BlockNote / Lexical 的成熟形态 | +| 内置功能怎么写 | 全部按 plugin 重写 | "形状被锁定"的唯一可信信号是有真实使用者。math / table / mermaid 同时验证 7-8 类不同的扩展面,覆盖度足够 | +| math/table/mermaid 与 core 的关系 | core 中立,subpath 留位 | core 完全不知道这些能力是什么;host 显式 `plugins: [math(), table(), ...]`。未来抽出去时改 import path 不是破坏性变更 | +| interaction core 归属 | first-party plugin in editor-core | 双重背书:plugin API 既被 math/table(功能型)验证,也被 interaction(交互型)验证。避免"内置交互"与"第三方插件"双 API | +| metadata 锁多少 | 只 id + version | `dependencies` / `settings schema` / `permission` 是 Model C 才需要的字段,v0.1 锁了会形似实虚 | +| 冲突策略 | last-wins + warning | first-wins 会"后装的默默无效";throw 会卡死内置 plugin 上线流程 | +| `features` 开关 | 删除"被拆"的、保留"留在 core"的 | core 不知道 math 是什么、却仍要为它留 `mathRendering: boolean` 字段是个怪局面 | + +--- + +## Plugin SDK 形状(v0.1 锁定) + +```ts +export interface EditorPlugin { + /** 唯一标识。v0.1 唯一硬约束字段。 */ + id: string; + /** 可选版本号。仅作展示用,v0.1 不参与解析。 */ + version?: string; + /** 装载时调用。 */ + setup(ctx: EditorPluginContext): EditorPluginInstance | void; +} + +export interface EditorPluginInstance { + /** 编辑器销毁时调用。setup 内 register 返回的 Disposable 也会被自动释放。 */ + dispose?(): void; +} + +export interface EditorPluginContext { + // ─── stable surface (v0.1 锁定) ──────────────────────────── + registerCommands(specs: EditorCommandSpec[]): Disposable; + registerCmExtensions(extensions: Extension[]): Disposable; + registerMarkdownRenderer(rule: MarkdownRenderRule): Disposable; + host: EditorHostCapabilities; + + // ─── @unstable (v0.1 类型先行,runtime 后续) ───────────────── + /** @unstable v0.1:slash command 数据源;接口可能在 v0.2 调整。 */ + registerSlashItems?(provider: SlashItemProvider): Disposable; + /** @unstable v0.1:自定义 trigger 检测;接口可能在 v0.2 调整。 */ + registerTrigger?(spec: EditorTriggerSpec): Disposable; + /** @unstable v0.1:事件订阅;后续可能合并到统一 lifecycle。 */ + on?(event: EditorEventType, listener: EditorEventListener): Disposable; +} + +export interface EditorCommandSpec { + id: string; + title?: string; + description?: string; + icon?: string; + /** 该命令在当前上下文是否可用。 */ + when?: (ctx: EditorCommandContext) => boolean; + run: (ctx: EditorCommandContext) => void | Promise; +} + +export interface Disposable { + dispose(): void; +} +``` + +### EditorHostCapabilities 聚合协议 + +```ts +export interface EditorHostCapabilities { + resolveImage?: (src: string) => string | Promise; + uploadFile?: UploadFileHandler; + openLink?: (url: string) => void | Promise; + // ─── @unstable 后续 ── + searchNotes?: (query: string) => Promise; + getSlashItems?: (ctx: SlashItemsContext) => Promise; +} +``` + +`EditorProps` 上 `imageResolver` / `uploadFile` 仍保留(标 `@deprecated`),同时新增 `host?: EditorHostCapabilities`。两者并存到 v0.2。 + +--- + +## EditorEvent 三层分类(v0.1) + +```ts +/** Core event:长期稳定契约。 */ +export type EditorCoreEvent = + | EditorChangeEvent + | EditorSelectionChangeEvent + | EditorSelectionFormattingChangeEvent + | EditorFocusEvent | EditorBlurEvent + | EditorSearchStateChangeEvent + | EditorCollaborationUpdateEvent + | EditorLinkOpenEvent; + +/** @unstable Interaction event:v0.1 类型先行,trigger runtime 后续。 */ +export type EditorInteractionEvent = + | EditorSlashTriggerChangeEvent + | EditorWikiLinkTriggerChangeEvent + | EditorSelectionToolbarChangeEvent; + +/** Platform convenience event:含平台耦合字段(DOM 坐标/HTML 字符串等),不保证跨端语义稳定。 */ +export type EditorPlatformEvent = + | EditorTableContextMenuEvent + | EditorMermaidZoomRequestEvent + | EditorRemoveEvent; + +export type EditorEvent = + | EditorCoreEvent | EditorInteractionEvent | EditorPlatformEvent; +``` + +--- + +## 内置 plugin 清单 + +| Plugin | 当前所在 | v0.1 后形态 | 备注 | +|---|---|---|---| +| math | `extensions/renderBlockMath.ts` + `markdownMathExtension.ts` | `plugins/math/` first-party plugin | 默认不启用 | +| table | `extensions/renderBlockTables.ts` | `plugins/table/` first-party plugin | 默认不启用;`TableContextMenu` 事件保留为 platform event | +| mermaid | `extensions/renderBlockMermaid.ts` | `plugins/mermaid/` first-party plugin | 默认不启用;`MermaidZoomRequest` 事件保留为 platform event | +| admonition | `extensions/admonition/` | `plugins/admonition/` first-party plugin | 默认不启用 | +| codeBlock | `extensions/renderBlockCode.ts` | `plugins/codeBlock/` first-party plugin | `CodeBlockMode` 配置由 plugin 自己持有 | +| blockImage | `extensions/renderBlockImages.ts` | `plugins/blockImage/` first-party plugin | `imageResolver` 通过 `ctx.host.resolveImage` 注入 | +| rawHtml | `extensions/renderRawHtml.ts` | `plugins/rawHtml/` first-party plugin | DOMPurify 仅在该 plugin 内部依赖 | +| smartPaste | `extensions/smartPasteExtension.ts` | `plugins/smartPaste/` first-party plugin | `uploadFile` 通过 `ctx.host.uploadFile` 注入 | +| **interactions** | _尚未实现_ | `plugins/interactions/{slash,wikilink,selectionToolbar}/` | runtime 后续;v0.1 仅类型层 | + +### 留在 core 不拆的能力 + +- `markdownDecorationExtension` / `markdownHighlightExtension`:Markdown 语义本身 +- `inlineRendering/`:编辑器最基本的渲染语义 +- `links/` + `LinkOpen` event:链接打开是核心交互 +- `markdownFrontMatterExtension`:core 语法 +- `editorSettingsExtension` / `lineAwareClipboardExtension`:内部基础设施 +- `searchExtension`:搜索面板(structural feature,不是可选插件) +- `collaborationExtension`:Y.Doc / `y-codemirror.next` 绑定 + +对应 `EditorSettings.features` 保留:`markdownHighlight`、`markdownDecorations`、`inlineRendering`、`search`、`collaboration`。其余 features 字段删除。 + +--- + +## Subpath export 策略 + +```jsonc +// @swarmnote/editor-core/package.json +{ + "exports": { + ".": { /* 主入口,不含任何 plugin */ }, + "./plugins/math": { /* 仅 math plugin */ }, + "./plugins/table": { /* 仅 table plugin */ }, + "./plugins/mermaid": { /* 仅 mermaid plugin */ }, + "./plugins/admonition": { /* 仅 admonition plugin */ }, + "./plugins/codeBlock": { /* 仅 codeBlock plugin */ }, + "./plugins/blockImage": { /* 仅 blockImage plugin */ }, + "./plugins/rawHtml": { /* 仅 rawHtml plugin */ }, + "./plugins/smartPaste": { /* 仅 smartPaste plugin */ }, + "./plugins/interactions/slash": { /* @unstable */ }, + "./plugins/interactions/wikilink": { /* @unstable */ }, + "./plugins/interactions/selectionToolbar": { /* @unstable */ } + } +} +``` + +**Host 接入示例**: + +```ts +import { createEditor } from '@swarmnote/editor-core'; +import { mathPlugin } from '@swarmnote/editor-core/plugins/math'; +import { tablePlugin } from '@swarmnote/editor-core/plugins/table'; +import { mermaidPlugin } from '@swarmnote/editor-core/plugins/mermaid'; + +createEditor(parent, { + initialText, + settings, + host: { resolveImage, uploadFile }, + plugins: [ + mathPlugin(), + tablePlugin(), + mermaidPlugin({ /* options */ }), + ], +}); +``` + +未来这些 plugin 抽成独立 npm 包后,host 仅需把 import 路径从 `@swarmnote/editor-core/plugins/math` 改为 `@swarmnote/editor-plugin-math`——这是**纯文本替换**,不是破坏性变更。 + +--- + +## 冲突策略 + +- 多个 plugin 注册同 `id` 的 command:**last-wins**,前一个 spec 被悄悄替换,`console.warn` 提示。 +- 多个 plugin 注册 Markdown renderer 抢同一节点:**last-wins**,同样 warn。 +- 多个 plugin 注册 CM6 extension:按 `plugins: []` 数组顺序追加,由 CM6 自身的 facet/priority 决定最终行为。 + +未来 Model C 引入 dependency 解析时再考虑更复杂的冲突处理。 + +--- + +## v0.1 实施清单(高层) + +1. **类型层先行**:把 `EditorPlugin` / `EditorPluginContext` / `EditorCommandSpec` / `EditorHostCapabilities` 类型加入 `editor-core/src/types.ts` +2. **EditorEvent 分层**:拆 `events.ts` 为三个 sub-union,标 `MermaidZoomRequest` / `TableContextMenu` 为 platform,定义 `SlashTriggerChange` / `WikiLinkTriggerChange` / `SelectionToolbarChange` 类型并标 `@unstable` +3. **Plugin host runtime**:在 `createEditor` 内实现 plugin 加载循环、registry、Disposable 跟踪 +4. **内置功能 plugin 化**:把 `extensions/` 下 8 个功能模块按 `EditorPlugin` 接口重写为 `plugins/` 下的 first-party plugin +5. **Subpath export**:更新 `package.json` exports / `tsdown.config.ts` 多入口构建 +6. **EditorProps 调整**:新增 `host?: EditorHostCapabilities` 与 `plugins?: EditorPlugin[]`;将 `imageResolver` / `uploadFile` 标 `@deprecated`(v0.2 移除) +7. **features 字段收敛**:删除 `mathRendering` / `mermaidRendering` / `blockImageRendering` / `rawHtmlRendering` / `codeBlockMode` / `smartPaste` / `admonition` 七个字段 +8. **SwarmNote host 同步**:`src/components/editor/NoteEditor.tsx` 改用 `plugins: []` 显式启用;设置 UI 中对应 toggle 改为驱动 `plugins[]` 组合而非 `features.*` + +--- + +## 后续路线(均为无破坏性扩展) + +| 阶段 | 内容 | +|---|---| +| v0.2 | interaction core runtime:真做 slash / wikilink trigger detection,把 `@unstable` 标签去掉 | +| v0.3 | `editor-react` 包:消费 interaction events,提供桌面端默认 popover/toolbar 组件 | +| v0.4 | `editor-react-native` 包:消费同一套 event/command,提供移动端 sheet/toolbar 组件 | +| v0.5+ | `@swarmnote/editor-plugin-math` 等独立 npm 包,替代 subpath(subpath 同期标 `@deprecated`) | +| v1.0+ | Model C:runtime plugin marketplace,加 manifest / loader / sandbox / settings UI | + +每一步都不破坏前一步的 plugin API。 + +--- + +## 与原四份文档的关系(再次确认) + +- 本文档为 plugin 架构的**最终决定**来源 +- `editor-open-source-rfc.md` 的"包结构"与"分阶段实施"按本文档对齐 +- `editor-core-host-boundary.md` 的"interaction 归属"问题在本文档闭环 +- `editor-event-model-draft.md` 的三层分类已被本文档具象化 +- `editor-extension-points-draft.md` 的"六类扩展点"被本文档的 `EditorPluginContext` 取代,文档保留为思考过程 + +--- + +## v0.1 BREAKING change 摘要 + +下列改动相对 v0.0.x 是破坏性,列出供 PR description / CHANGELOG / release notes 引用。 + +### `@swarmnote/editor-core` — sibling 仓 `swarmnote-editor` + +1. **`EditorFeatureToggles` 删除 7 个字段**:`mathRendering` / `mermaidRendering` / `blockImageRendering` / `rawHtmlRendering` / `codeBlockMode` / `smartPaste` / `admonition`。这些能力迁到 plugin,通过 `createEditor(..., { plugins: [...] })` 显式启用。保留:`markdownHighlight` / `markdownDecorations` / `inlineRendering` / `search` / `collaboration`。 +2. **主入口不再 re-export 已迁 plugin 工厂**:`createBlockMermaidExtension` / `createRawHtmlExtension` / `createAdmonitionExtension` / `createSmartPasteExtension` / `setTableSourceMode` / `setCodeBlockSourceMode` / `refreshBlockImagesEffect` / `refreshRawHtmlEffect` / `clearMermaidCacheEffect` / `GFM_TYPES` / `OBSIDIAN_TYPES` / `DEFAULT_ADMONITION_TYPE` 等全部仅通过 `@swarmnote/editor-core/plugins/` subpath 暴露。 +3. **`EditorProps.imageResolver` / `EditorProps.uploadFile` 标 `@deprecated`**:仍可用并被透明桥接到 `host.resolveImage` / `host.uploadFile`;同时提供 `host.*` 时 `host.*` 优先,触发一次 `console.warn`。 +4. **`EditorEvent` 拆分三层 union**:`EditorCoreEvent` / `EditorInteractionEvent`(`@unstable`)/ `EditorPlatformEvent`。聚合 `EditorEvent` 与 v0.0.x 同 shape,向后兼容;按层 import 是新增能力。 + +### SwarmNote host — 本仓 + +1. **`preferencesStore` 新增 `enabledPlugins: EditorPluginId[]` + `codeBlockMode: 'inline' | 'auto' | 'toggle'`**:默认 8 个 plugin 全开。带 v0.0.x 旧 `features.*` 键的持久化偏好通过 `migrateLegacyFeatures` 翻译并清除旧键(idempotent)。 +2. **设置 UI**:`routes/settings/general.tsx` 新增"编辑器插件"区,含 8 个 plugin Switch + codeBlock 模式 Select。无原 7 toggle 可删(host 历史上从未暴露过)。 +3. **`NoteEditor.tsx`**:构造 `plugins[]` 数组传入 `createEditor`;`imageResolver` / `uploadFile` 改走 `host` 对象;plugin 切换需新开文档或重启应用才生效(CM6 扩展集 boot-time freeze)。 diff --git a/src/components/editor/NoteEditor.tsx b/src/components/editor/NoteEditor.tsx index 47aea5a..c5d6900 100644 --- a/src/components/editor/NoteEditor.tsx +++ b/src/components/editor/NoteEditor.tsx @@ -4,9 +4,20 @@ import { DEFAULT_SETTINGS, type EditorControl, EditorEventType, + type EditorPlugin, type EditorSettings, - refreshBlockImagesEffect, } from "@swarmnote/editor-core"; +import { admonitionPlugin } from "@swarmnote/editor-core/plugins/admonition"; +import { + blockImagePlugin, + refreshBlockImagesEffect, +} from "@swarmnote/editor-core/plugins/blockImage"; +import { codeBlockPlugin } from "@swarmnote/editor-core/plugins/codeBlock"; +import { mathPlugin } from "@swarmnote/editor-core/plugins/math"; +import { mermaidPlugin } from "@swarmnote/editor-core/plugins/mermaid"; +import { rawHtmlPlugin } from "@swarmnote/editor-core/plugins/rawHtml"; +import { smartPastePlugin } from "@swarmnote/editor-core/plugins/smartPaste"; +import { tablePlugin } from "@swarmnote/editor-core/plugins/table"; import { convertFileSrc, invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { confirm } from "@tauri-apps/plugin-dialog"; @@ -23,9 +34,34 @@ import { import { colorForDevice } from "@/lib/awareness-color"; import { TauriYjsProvider } from "@/lib/TauriYjsProvider"; import { useEditorStore } from "@/stores/editorStore"; +import { type EditorPluginId, usePreferencesStore } from "@/stores/preferencesStore"; import { useUIStore } from "@/stores/uiStore"; import { useWorkspaceStore } from "@/stores/workspaceStore"; +/** + * 根据 preferencesStore 当前快照构造 `plugins[]` 数组。 + * + * 由于 CM6 扩展集合在 `EditorState.create` 时一次性 freeze,本函数仅在 + * editor 挂载时读取 snapshot —— 用户切换 plugin 启用状态后需要新开 + * 编辑器(key remount)才能反映。 + */ +function buildEditorPlugins( + enabledPluginIds: readonly EditorPluginId[], + codeBlockMode: "inline" | "auto" | "toggle", +): EditorPlugin[] { + const enabled = new Set(enabledPluginIds); + const plugins: EditorPlugin[] = []; + if (enabled.has("math")) plugins.push(mathPlugin()); + if (enabled.has("table")) plugins.push(tablePlugin()); + if (enabled.has("mermaid")) plugins.push(mermaidPlugin()); + if (enabled.has("admonition")) plugins.push(admonitionPlugin()); + if (enabled.has("codeBlock")) plugins.push(codeBlockPlugin({ mode: codeBlockMode })); + if (enabled.has("blockImage")) plugins.push(blockImagePlugin()); + if (enabled.has("rawHtml")) plugins.push(rawHtmlPlugin()); + if (enabled.has("smartPaste")) plugins.push(smartPastePlugin()); + return plugins; +} + interface YjsContext { ydoc: Y.Doc; provider: TauriYjsProvider; @@ -182,8 +218,19 @@ function NoteEditorInner({ ydoc, provider }: { ydoc: Y.Doc; provider: TauriYjsPr [wsPath], ); + // Upload handler: save a single dropped/pasted file to workspace media, + // returning the rel-path + alt for smartPaste plugin to insert as Markdown. + const uploadFile = useCallback(async (file: File): Promise<{ url: string; alt?: string }> => { + const rel = useEditorStore.getState().relPath; + if (!rel) throw new Error("no relPath"); + const buffer = await file.arrayBuffer(); + const bytes = Array.from(new Uint8Array(buffer)); + const savedRel = await saveMedia(rel, file.name, bytes); + return { url: savedRel, alt: file.name }; + }, []); + // Mount the CM6 editor once per Y.Doc (collaboration mode). - // biome-ignore lint/correctness/useExhaustiveDependencies: ydoc drives the editor lifecycle; resolvedTheme and imageResolver are applied reactively in sibling effects + // biome-ignore lint/correctness/useExhaustiveDependencies: ydoc drives the editor lifecycle; resolvedTheme / host capabilities / plugins are read from store snapshot at mount and don't trigger remount useEffect(() => { const parent = containerRef.current; if (!parent) return; @@ -196,6 +243,12 @@ function NoteEditorInner({ ydoc, provider }: { ydoc: Y.Doc; provider: TauriYjsPr }, }; + // Read plugin enablement snapshot at mount. CM6 extensions are frozen at + // EditorState.create() time, so plugin toggles only take effect on the + // next editor mount (document switch or app reload). + const prefs = usePreferencesStore.getState(); + const plugins = buildEditorPlugins(prefs.enabledPlugins, prefs.codeBlockMode); + const control = createEditor(parent, { initialText: "", settings: initialSettings, @@ -204,7 +257,16 @@ function NoteEditorInner({ ydoc, provider }: { ydoc: Y.Doc; provider: TauriYjsPr fragmentName: "document", awareness: provider.awareness, }, - imageResolver, + host: { + resolveImage: imageResolver, + uploadFile, + openLink: (url) => { + openUrl(url).catch(() => { + // URL may be malformed or blocked — silent. + }); + }, + }, + plugins, autofocus: true, onEvent: (event) => { if (event.kind === EditorEventType.Change) { diff --git a/src/locales/en/messages.po b/src/locales/en/messages.po index a363e52..d039a5a 100644 --- a/src/locales/en/messages.po +++ b/src/locales/en/messages.po @@ -44,7 +44,7 @@ msgid "新建笔记" msgstr "New note" #. placeholder {0}: event.payload.relPath -#: src/components/editor/NoteEditor.tsx:329 +#: src/components/editor/NoteEditor.tsx:425 msgid "\"{0}\" 已被外部修改。是否重新加载?当前未保存的编辑将丢失。" msgstr "\"{0}\" was modified externally. Reload? Unsaved edits will be lost." @@ -86,6 +86,22 @@ msgstr "{peerCount} devices online" msgid "{peerCount} 台设备在线 · 已同步" msgstr "{peerCount} devices online · synced" +#: src/routes/settings/general.tsx:85 +msgid "Admonition" +msgstr "Admonition" + +#: src/routes/settings/general.tsx:92 +msgid "fenced 代码块渲染与高亮" +msgstr "Fenced code block rendering and highlighting" + +#: src/routes/settings/general.tsx:103 +msgid "HTML 渲染" +msgstr "HTML rendering" + +#: src/routes/settings/general.tsx:79 +msgid "Mermaid 图表" +msgstr "Mermaid diagrams" + #: src/components/onboarding/WelcomeStep.tsx:25 msgid "P2P 同步" msgstr "P2P Sync" @@ -116,7 +132,7 @@ msgstr "Next" msgid "下载中..." msgstr "Downloading..." -#: src/routes/settings/general.tsx:52 +#: src/routes/settings/general.tsx:136 msgid "中文" msgstr "Chinese" @@ -141,6 +157,10 @@ msgstr "Remove from list" msgid "从已配对设备同步工作区到本地。{0} 台设备在线。" msgstr "Sync workspace from paired devices to local. {0} devices online." +#: src/routes/settings/general.tsx:91 +msgid "代码块" +msgstr "Code block" + #: src/components/editor/EditorContextMenu.tsx:233 msgid "任务列表" msgstr "Task list" @@ -185,6 +205,10 @@ msgstr "Closing P2P network will disconnect all devices and stop syncing notes." msgid "关闭网络" msgstr "Stop Network" +#: src/routes/settings/general.tsx:208 +msgid "内联" +msgstr "Inline" + #. placeholder {0}: formatRelativeTime(device.lastSeen ?? null) #: src/components/pairing/PairedDeviceCard.tsx:41 msgid "最后在线 {0}" @@ -207,10 +231,26 @@ msgstr "Preparing to sync · {0} documents" msgid "准备就绪!" msgstr "Ready to Go!" +#: src/routes/settings/general.tsx:210 +msgid "切换" +msgstr "Toggle" + #: src/lib/commands.ts:60 msgid "切换侧边栏" msgstr "Toggle Sidebar" +#: src/routes/settings/general.tsx:191 +msgid "切换插件启用状态后,需要重新打开文档或重启应用以生效。" +msgstr "Plugin toggles take effect on next document open or app restart." + +#: src/components/editor/TableContextMenu.tsx:180 +msgid "切换源码" +msgstr "" + +#: src/components/editor/TableContextMenu.tsx:126 +msgid "列" +msgstr "" + #: src/routes/workspace-manager.tsx:247 msgid "创建" msgstr "Create" @@ -228,10 +268,22 @@ msgstr "Create new workspace" msgid "删除" msgstr "Delete" +#: src/components/editor/TableContextMenu.tsx:169 +msgid "删除列" +msgstr "" + #: src/components/editor/EditorContextMenu.tsx:201 msgid "删除线" msgstr "Strikethrough" +#: src/components/editor/TableContextMenu.tsx:118 +msgid "删除行" +msgstr "" + +#: src/components/editor/TableContextMenu.tsx:189 +msgid "删除表格" +msgstr "" + #: src/routes/settings/devices.tsx:254 msgid "刷新" msgstr "Refresh" @@ -248,7 +300,7 @@ msgstr "Cut" msgid "加粗" msgstr "Bold" -#: src/components/editor/NoteEditor.tsx:80 +#: src/components/editor/NoteEditor.tsx:122 msgid "加载中..." msgstr "Loading..." @@ -288,10 +340,14 @@ msgid "取消配对" msgstr "Unpair" #: src/components/editor/EditorContextMenu.tsx:284 -#: src/routes/settings/general.tsx:77 +#: src/routes/settings/general.tsx:161 msgid "可读行宽" msgstr "Readable line width" +#: src/components/editor/TableContextMenu.tsx:151 +msgid "右对齐" +msgstr "" + #: src/routes/workspace-manager.tsx:284 msgid "同步" msgstr "Sync" @@ -339,7 +395,7 @@ msgstr "Start P2P network to sync workspaces" msgid "启动中..." msgstr "Starting..." -#: src/routes/settings/general.tsx:94 +#: src/routes/settings/general.tsx:178 msgid "启动时自动打开上次使用的工作区" msgstr "Automatically open the last-used workspace on startup" @@ -347,7 +403,7 @@ msgstr "Automatically open the last-used workspace on startup" msgid "启动网络" msgstr "Start Network" -#: src/routes/settings/general.tsx:88 +#: src/routes/settings/general.tsx:172 msgid "启动行为" msgstr "Startup behavior" @@ -355,10 +411,30 @@ msgstr "Startup behavior" msgid "命令面板" msgstr "Command palette" +#: src/routes/settings/general.tsx:97 +msgid "图片渲染" +msgstr "Image rendering" + +#: src/components/editor/TableContextMenu.tsx:106 +msgid "在上方新增行" +msgstr "" + +#: src/components/editor/TableContextMenu.tsx:110 +msgid "在下方新增行" +msgstr "" + #: src/components/pairing/CodePairingCard.tsx:93 msgid "在另一台设备输入此码" msgstr "Enter this code on another device" +#: src/components/editor/TableContextMenu.tsx:134 +msgid "在右侧新增列" +msgstr "" + +#: src/components/editor/TableContextMenu.tsx:130 +msgid "在左侧新增列" +msgstr "" + #: src/routes/workspace-manager.tsx:244 msgid "在指定文件夹下创建一个新的工作区。" msgstr "Create a new workspace in the specified folder." @@ -375,12 +451,20 @@ msgstr "Add headings in your document to see the outline navigation" msgid "在线" msgstr "Online" +#: src/components/editor/TableContextMenu.tsx:100 +msgid "在表头下方新增行" +msgstr "" + #: src/components/editor/EditorContextMenu.tsx:268 #: src/components/pairing/CodePairingCard.tsx:101 #: src/components/pairing/CodePairingCard.tsx:104 msgid "复制" msgstr "Copy" +#: src/components/editor/TableContextMenu.tsx:184 +msgid "复制为 Markdown" +msgstr "" + #: src/routes/workspace-manager.tsx:59 msgid "复制路径" msgstr "Copy path" @@ -389,8 +473,8 @@ msgstr "Copy path" msgid "复制配对码" msgstr "Copy Pairing Code" -#: src/routes/settings/general.tsx:42 -#: src/routes/settings/general.tsx:59 +#: src/routes/settings/general.tsx:126 +#: src/routes/settings/general.tsx:143 msgid "外观" msgstr "Appearance" @@ -414,6 +498,10 @@ msgstr "Encrypted" msgid "完成" msgstr "Done" +#: src/routes/settings/general.tsx:98 +msgid "将 Markdown 图片渲染为内联 / 块级 widget" +msgstr "Render Markdown images as inline / block widgets" + #: src/routes/workspace-manager.tsx:255 msgid "将一个本地文件夹作为工作区打开。" msgstr "Open a local folder as a workspace." @@ -431,6 +519,10 @@ msgstr "Share this code with the other device. It expires in {0}." msgid "局域网" msgstr "LAN" +#: src/components/editor/TableContextMenu.tsx:145 +msgid "居中对齐" +msgstr "" + #: src/components/layout/TitleBar.tsx:77 msgid "展开侧边栏" msgstr "Expand Sidebar" @@ -443,6 +535,10 @@ msgstr "Workspace manager" msgid "工作区管理..." msgstr "Workspace manager..." +#: src/components/editor/TableContextMenu.tsx:139 +msgid "左对齐" +msgstr "" + #. placeholder {0}: device.name ?? device.hostname #: src/components/pairing/NearbyDeviceCard.tsx:35 msgid "已与 {0} 配对" @@ -513,7 +609,7 @@ msgstr "Version {currentVersion} is no longer supported. Please update to {lates msgid "当前设备" msgstr "Current device" -#: src/routes/settings/general.tsx:93 +#: src/routes/settings/general.tsx:177 msgid "恢复上次工作区" msgstr "Restore last workspace" @@ -571,6 +667,10 @@ msgstr "Hole-punch" msgid "找到设备" msgstr "Devices found" +#: src/routes/settings/general.tsx:80 +msgid "把 mermaid 代码块渲染为 SVG" +msgstr "Render mermaid code blocks as SVG" + #: src/components/editor/EditorContextMenu.tsx:155 msgid "插入" msgstr "Insert" @@ -611,7 +711,11 @@ msgstr "Actions" msgid "收起侧边栏" msgstr "Collapse Sidebar" -#: src/components/editor/NoteEditor.tsx:330 +#: src/routes/settings/general.tsx:67 +msgid "数学公式" +msgstr "Math formulas" + +#: src/components/editor/NoteEditor.tsx:426 msgid "文件已修改" msgstr "File Modified" @@ -664,6 +768,10 @@ msgstr "Version {latestVersion} is available. Current version: {currentVersion}" msgid "无序列表" msgstr "Bullet list" +#: src/routes/settings/general.tsx:109 +msgid "智能粘贴" +msgstr "Smart paste" + #: src/components/settings/WorkspaceSyncList.tsx:112 msgid "暂无工作区" msgstr "No workspaces" @@ -813,11 +921,11 @@ msgstr "No matching files" msgid "没有找到匹配的命令" msgstr "No matching commands found" -#: src/routes/settings/general.tsx:68 +#: src/routes/settings/general.tsx:152 msgid "浅色" msgstr "Light" -#: src/routes/settings/general.tsx:69 +#: src/routes/settings/general.tsx:153 msgid "深色" msgstr "Dark" @@ -825,6 +933,14 @@ msgstr "Dark" msgid "添加设备" msgstr "Add device" +#: src/routes/settings/general.tsx:86 +msgid "渲染 GFM / Obsidian 风格的提示块" +msgstr "Render GFM / Obsidian style admonitions" + +#: src/routes/settings/general.tsx:68 +msgid "渲染 KaTeX 数学公式 ($...$ / $$...$$)" +msgstr "Render KaTeX math ($...$ / $$...$$)" + #: src/routes/workspace-manager.tsx:235 msgid "版本" msgstr "Version" @@ -916,10 +1032,22 @@ msgstr "Waiting for connection..." msgid "等待设备连接" msgstr "Waiting for device to connect" +#: src/routes/settings/general.tsx:74 +msgid "管道表格渲染为可视化卡片" +msgstr "Render pipe tables as visual cards" + +#: src/routes/settings/general.tsx:110 +msgid "粘贴 URL 转链接,拖放 / 粘贴文件上传为图片" +msgstr "Paste URLs as links; drag/paste files upload as images" + #: src/routes/settings/devices.tsx:201 msgid "编辑名称" msgstr "Edit name" +#: src/routes/settings/general.tsx:188 +msgid "编辑器插件" +msgstr "Editor plugins" + #: src/routes/settings.tsx:27 #: src/routes/settings/network.tsx:18 msgid "网络" @@ -934,14 +1062,26 @@ msgstr "Network not started" msgid "网络设置" msgstr "Network settings" +#: src/routes/settings/general.tsx:209 +msgid "自动" +msgstr "Auto" + #: src/components/settings/NetworkStatusCard.tsx:67 msgid "节点启动失败" msgstr "Node failed to start" +#: src/components/editor/TableContextMenu.tsx:95 +msgid "行" +msgstr "" + #: src/components/editor/EditorContextMenu.tsx:218 msgid "行内代码" msgstr "Inline code" +#: src/routes/settings/general.tsx:73 +msgid "表格" +msgstr "Tables" + #: src/routes/settings.tsx:28 msgid "设备" msgstr "Device" @@ -969,7 +1109,7 @@ msgstr "Device Management" msgid "设置" msgstr "Settings" -#: src/routes/settings/general.tsx:46 +#: src/routes/settings/general.tsx:130 msgid "语言" msgstr "Language" @@ -981,7 +1121,7 @@ msgstr "Please enter a 6-digit pairing code" msgid "调整侧边栏宽度 (当前 {sidebarWidth} 像素)" msgstr "Adjust sidebar width (currently {sidebarWidth} pixels)" -#: src/routes/settings/general.tsx:70 +#: src/routes/settings/general.tsx:154 msgid "跟随系统" msgstr "System" @@ -1062,19 +1202,23 @@ msgstr "Choose sync target folder" msgid "选择新工作区目录" msgstr "Choose new workspace folder" -#: src/routes/settings/general.tsx:59 +#: src/routes/settings/general.tsx:143 msgid "选择明亮或暗色主题" msgstr "Choose light or dark theme" -#: src/routes/settings/general.tsx:46 +#: src/routes/settings/general.tsx:130 msgid "选择界面显示语言" msgstr "Choose the interface language" #: src/routes/settings.tsx:26 -#: src/routes/settings/general.tsx:34 +#: src/routes/settings/general.tsx:118 msgid "通用" msgstr "General" +#: src/routes/settings/general.tsx:104 +msgid "通过 DOMPurify 渲染嵌入的原生 HTML" +msgstr "Render embedded HTML via DOMPurify" + #: src/components/pairing/NearbyDeviceCard.tsx:59 msgid "配对" msgstr "Pair" @@ -1125,7 +1269,7 @@ msgstr "Error" msgid "附近设备" msgstr "Nearby Devices" -#: src/routes/settings/general.tsx:78 +#: src/routes/settings/general.tsx:162 msgid "限制编辑器内容宽度以提升阅读体验" msgstr "Limit editor content width for a more comfortable reading experience" @@ -1136,3 +1280,7 @@ msgstr "Update Required" #: src/components/editor/EditorContextMenu.tsx:209 msgid "高亮" msgstr "Highlight" + +#: src/components/editor/TableContextMenu.tsx:157 +msgid "默认对齐" +msgstr "" diff --git a/src/locales/zh/messages.po b/src/locales/zh/messages.po index 84ee965..7cd4770 100644 --- a/src/locales/zh/messages.po +++ b/src/locales/zh/messages.po @@ -44,7 +44,7 @@ msgid "新建笔记" msgstr "新建笔记" #. placeholder {0}: event.payload.relPath -#: src/components/editor/NoteEditor.tsx:329 +#: src/components/editor/NoteEditor.tsx:425 msgid "\"{0}\" 已被外部修改。是否重新加载?当前未保存的编辑将丢失。" msgstr "\"{0}\" 已被外部修改。是否重新加载?当前未保存的编辑将丢失。" @@ -86,6 +86,22 @@ msgstr "{peerCount} 台设备在线" msgid "{peerCount} 台设备在线 · 已同步" msgstr "{peerCount} 台设备在线 · 已同步" +#: src/routes/settings/general.tsx:85 +msgid "Admonition" +msgstr "Admonition" + +#: src/routes/settings/general.tsx:92 +msgid "fenced 代码块渲染与高亮" +msgstr "fenced 代码块渲染与高亮" + +#: src/routes/settings/general.tsx:103 +msgid "HTML 渲染" +msgstr "HTML 渲染" + +#: src/routes/settings/general.tsx:79 +msgid "Mermaid 图表" +msgstr "Mermaid 图表" + #: src/components/onboarding/WelcomeStep.tsx:25 msgid "P2P 同步" msgstr "P2P 同步" @@ -116,7 +132,7 @@ msgstr "下一步" msgid "下载中..." msgstr "下载中..." -#: src/routes/settings/general.tsx:52 +#: src/routes/settings/general.tsx:136 msgid "中文" msgstr "中文" @@ -141,6 +157,10 @@ msgstr "从列表移除" msgid "从已配对设备同步工作区到本地。{0} 台设备在线。" msgstr "从已配对设备同步工作区到本地。{0} 台设备在线。" +#: src/routes/settings/general.tsx:91 +msgid "代码块" +msgstr "代码块" + #: src/components/editor/EditorContextMenu.tsx:233 msgid "任务列表" msgstr "任务列表" @@ -185,6 +205,10 @@ msgstr "关闭 P2P 网络将断开与所有设备的连接,笔记将停止同 msgid "关闭网络" msgstr "关闭网络" +#: src/routes/settings/general.tsx:208 +msgid "内联" +msgstr "内联" + #. placeholder {0}: formatRelativeTime(device.lastSeen ?? null) #: src/components/pairing/PairedDeviceCard.tsx:41 msgid "最后在线 {0}" @@ -207,10 +231,26 @@ msgstr "准备同步 · {0} 篇文档" msgid "准备就绪!" msgstr "准备就绪!" +#: src/routes/settings/general.tsx:210 +msgid "切换" +msgstr "切换" + #: src/lib/commands.ts:60 msgid "切换侧边栏" msgstr "切换侧边栏" +#: src/routes/settings/general.tsx:191 +msgid "切换插件启用状态后,需要重新打开文档或重启应用以生效。" +msgstr "切换插件启用状态后,需要重新打开文档或重启应用以生效。" + +#: src/components/editor/TableContextMenu.tsx:180 +msgid "切换源码" +msgstr "切换源码" + +#: src/components/editor/TableContextMenu.tsx:126 +msgid "列" +msgstr "列" + #: src/routes/workspace-manager.tsx:247 msgid "创建" msgstr "创建" @@ -228,10 +268,22 @@ msgstr "创建新工作区" msgid "删除" msgstr "删除" +#: src/components/editor/TableContextMenu.tsx:169 +msgid "删除列" +msgstr "删除列" + #: src/components/editor/EditorContextMenu.tsx:201 msgid "删除线" msgstr "删除线" +#: src/components/editor/TableContextMenu.tsx:118 +msgid "删除行" +msgstr "删除行" + +#: src/components/editor/TableContextMenu.tsx:189 +msgid "删除表格" +msgstr "删除表格" + #: src/routes/settings/devices.tsx:254 msgid "刷新" msgstr "刷新" @@ -248,7 +300,7 @@ msgstr "剪切" msgid "加粗" msgstr "加粗" -#: src/components/editor/NoteEditor.tsx:80 +#: src/components/editor/NoteEditor.tsx:122 msgid "加载中..." msgstr "加载中..." @@ -288,10 +340,14 @@ msgid "取消配对" msgstr "取消配对" #: src/components/editor/EditorContextMenu.tsx:284 -#: src/routes/settings/general.tsx:77 +#: src/routes/settings/general.tsx:161 msgid "可读行宽" msgstr "可读行宽" +#: src/components/editor/TableContextMenu.tsx:151 +msgid "右对齐" +msgstr "右对齐" + #: src/routes/workspace-manager.tsx:284 msgid "同步" msgstr "同步" @@ -339,7 +395,7 @@ msgstr "启动 P2P 网络后即可同步工作区" msgid "启动中..." msgstr "启动中..." -#: src/routes/settings/general.tsx:94 +#: src/routes/settings/general.tsx:178 msgid "启动时自动打开上次使用的工作区" msgstr "启动时自动打开上次使用的工作区" @@ -347,7 +403,7 @@ msgstr "启动时自动打开上次使用的工作区" msgid "启动网络" msgstr "启动网络" -#: src/routes/settings/general.tsx:88 +#: src/routes/settings/general.tsx:172 msgid "启动行为" msgstr "启动行为" @@ -355,10 +411,30 @@ msgstr "启动行为" msgid "命令面板" msgstr "命令面板" +#: src/routes/settings/general.tsx:97 +msgid "图片渲染" +msgstr "图片渲染" + +#: src/components/editor/TableContextMenu.tsx:106 +msgid "在上方新增行" +msgstr "在上方新增行" + +#: src/components/editor/TableContextMenu.tsx:110 +msgid "在下方新增行" +msgstr "在下方新增行" + #: src/components/pairing/CodePairingCard.tsx:93 msgid "在另一台设备输入此码" msgstr "在另一台设备输入此码" +#: src/components/editor/TableContextMenu.tsx:134 +msgid "在右侧新增列" +msgstr "在右侧新增列" + +#: src/components/editor/TableContextMenu.tsx:130 +msgid "在左侧新增列" +msgstr "在左侧新增列" + #: src/routes/workspace-manager.tsx:244 msgid "在指定文件夹下创建一个新的工作区。" msgstr "在指定文件夹下创建一个新的工作区。" @@ -375,12 +451,20 @@ msgstr "在文档中添加标题即可看到大纲导航" msgid "在线" msgstr "在线" +#: src/components/editor/TableContextMenu.tsx:100 +msgid "在表头下方新增行" +msgstr "在表头下方新增行" + #: src/components/editor/EditorContextMenu.tsx:268 #: src/components/pairing/CodePairingCard.tsx:101 #: src/components/pairing/CodePairingCard.tsx:104 msgid "复制" msgstr "复制" +#: src/components/editor/TableContextMenu.tsx:184 +msgid "复制为 Markdown" +msgstr "复制为 Markdown" + #: src/routes/workspace-manager.tsx:59 msgid "复制路径" msgstr "复制路径" @@ -389,8 +473,8 @@ msgstr "复制路径" msgid "复制配对码" msgstr "复制配对码" -#: src/routes/settings/general.tsx:42 -#: src/routes/settings/general.tsx:59 +#: src/routes/settings/general.tsx:126 +#: src/routes/settings/general.tsx:143 msgid "外观" msgstr "外观" @@ -414,6 +498,10 @@ msgstr "安全加密" msgid "完成" msgstr "完成" +#: src/routes/settings/general.tsx:98 +msgid "将 Markdown 图片渲染为内联 / 块级 widget" +msgstr "将 Markdown 图片渲染为内联 / 块级 widget" + #: src/routes/workspace-manager.tsx:255 msgid "将一个本地文件夹作为工作区打开。" msgstr "将一个本地文件夹作为工作区打开。" @@ -431,6 +519,10 @@ msgstr "将此配对码告知对方设备,配对码将在 {0} 后过期" msgid "局域网" msgstr "局域网" +#: src/components/editor/TableContextMenu.tsx:145 +msgid "居中对齐" +msgstr "居中对齐" + #: src/components/layout/TitleBar.tsx:77 msgid "展开侧边栏" msgstr "展开侧边栏" @@ -443,6 +535,10 @@ msgstr "工作区管理" msgid "工作区管理..." msgstr "工作区管理..." +#: src/components/editor/TableContextMenu.tsx:139 +msgid "左对齐" +msgstr "左对齐" + #. placeholder {0}: device.name ?? device.hostname #: src/components/pairing/NearbyDeviceCard.tsx:35 msgid "已与 {0} 配对" @@ -513,7 +609,7 @@ msgstr "当前版本 {currentVersion} 已不再支持,请更新到 {latestVers msgid "当前设备" msgstr "当前设备" -#: src/routes/settings/general.tsx:93 +#: src/routes/settings/general.tsx:177 msgid "恢复上次工作区" msgstr "恢复上次工作区" @@ -571,6 +667,10 @@ msgstr "打洞" msgid "找到设备" msgstr "找到设备" +#: src/routes/settings/general.tsx:80 +msgid "把 mermaid 代码块渲染为 SVG" +msgstr "把 mermaid 代码块渲染为 SVG" + #: src/components/editor/EditorContextMenu.tsx:155 msgid "插入" msgstr "插入" @@ -611,7 +711,11 @@ msgstr "操作" msgid "收起侧边栏" msgstr "收起侧边栏" -#: src/components/editor/NoteEditor.tsx:330 +#: src/routes/settings/general.tsx:67 +msgid "数学公式" +msgstr "数学公式" + +#: src/components/editor/NoteEditor.tsx:426 msgid "文件已修改" msgstr "文件已修改" @@ -664,6 +768,10 @@ msgstr "新版本 {latestVersion} 可用,当前版本 {currentVersion}" msgid "无序列表" msgstr "无序列表" +#: src/routes/settings/general.tsx:109 +msgid "智能粘贴" +msgstr "智能粘贴" + #: src/components/settings/WorkspaceSyncList.tsx:112 msgid "暂无工作区" msgstr "暂无工作区" @@ -813,11 +921,11 @@ msgstr "没有匹配的文件" msgid "没有找到匹配的命令" msgstr "没有找到匹配的命令" -#: src/routes/settings/general.tsx:68 +#: src/routes/settings/general.tsx:152 msgid "浅色" msgstr "浅色" -#: src/routes/settings/general.tsx:69 +#: src/routes/settings/general.tsx:153 msgid "深色" msgstr "深色" @@ -825,6 +933,14 @@ msgstr "深色" msgid "添加设备" msgstr "添加设备" +#: src/routes/settings/general.tsx:86 +msgid "渲染 GFM / Obsidian 风格的提示块" +msgstr "渲染 GFM / Obsidian 风格的提示块" + +#: src/routes/settings/general.tsx:68 +msgid "渲染 KaTeX 数学公式 ($...$ / $$...$$)" +msgstr "渲染 KaTeX 数学公式 ($...$ / $$...$$)" + #: src/routes/workspace-manager.tsx:235 msgid "版本" msgstr "版本" @@ -916,10 +1032,22 @@ msgstr "等待对方连接..." msgid "等待设备连接" msgstr "等待设备连接" +#: src/routes/settings/general.tsx:74 +msgid "管道表格渲染为可视化卡片" +msgstr "管道表格渲染为可视化卡片" + +#: src/routes/settings/general.tsx:110 +msgid "粘贴 URL 转链接,拖放 / 粘贴文件上传为图片" +msgstr "粘贴 URL 转链接,拖放 / 粘贴文件上传为图片" + #: src/routes/settings/devices.tsx:201 msgid "编辑名称" msgstr "编辑名称" +#: src/routes/settings/general.tsx:188 +msgid "编辑器插件" +msgstr "编辑器插件" + #: src/routes/settings.tsx:27 #: src/routes/settings/network.tsx:18 msgid "网络" @@ -934,14 +1062,26 @@ msgstr "网络未启动" msgid "网络设置" msgstr "网络设置" +#: src/routes/settings/general.tsx:209 +msgid "自动" +msgstr "自动" + #: src/components/settings/NetworkStatusCard.tsx:67 msgid "节点启动失败" msgstr "节点启动失败" +#: src/components/editor/TableContextMenu.tsx:95 +msgid "行" +msgstr "行" + #: src/components/editor/EditorContextMenu.tsx:218 msgid "行内代码" msgstr "行内代码" +#: src/routes/settings/general.tsx:73 +msgid "表格" +msgstr "表格" + #: src/routes/settings.tsx:28 msgid "设备" msgstr "设备" @@ -969,7 +1109,7 @@ msgstr "设备管理" msgid "设置" msgstr "设置" -#: src/routes/settings/general.tsx:46 +#: src/routes/settings/general.tsx:130 msgid "语言" msgstr "语言" @@ -981,7 +1121,7 @@ msgstr "请输入6位配对码" msgid "调整侧边栏宽度 (当前 {sidebarWidth} 像素)" msgstr "调整侧边栏宽度 (当前 {sidebarWidth} 像素)" -#: src/routes/settings/general.tsx:70 +#: src/routes/settings/general.tsx:154 msgid "跟随系统" msgstr "跟随系统" @@ -1062,19 +1202,23 @@ msgstr "选择同步目标目录" msgid "选择新工作区目录" msgstr "选择新工作区目录" -#: src/routes/settings/general.tsx:59 +#: src/routes/settings/general.tsx:143 msgid "选择明亮或暗色主题" msgstr "选择明亮或暗色主题" -#: src/routes/settings/general.tsx:46 +#: src/routes/settings/general.tsx:130 msgid "选择界面显示语言" msgstr "选择界面显示语言" #: src/routes/settings.tsx:26 -#: src/routes/settings/general.tsx:34 +#: src/routes/settings/general.tsx:118 msgid "通用" msgstr "通用" +#: src/routes/settings/general.tsx:104 +msgid "通过 DOMPurify 渲染嵌入的原生 HTML" +msgstr "通过 DOMPurify 渲染嵌入的原生 HTML" + #: src/components/pairing/NearbyDeviceCard.tsx:59 msgid "配对" msgstr "配对" @@ -1125,7 +1269,7 @@ msgstr "错误" msgid "附近设备" msgstr "附近设备" -#: src/routes/settings/general.tsx:78 +#: src/routes/settings/general.tsx:162 msgid "限制编辑器内容宽度以提升阅读体验" msgstr "限制编辑器内容宽度以提升阅读体验" @@ -1136,3 +1280,7 @@ msgstr "需要更新" #: src/components/editor/EditorContextMenu.tsx:209 msgid "高亮" msgstr "高亮" + +#: src/components/editor/TableContextMenu.tsx:157 +msgid "默认对齐" +msgstr "默认对齐" diff --git a/src/routes/settings/general.tsx b/src/routes/settings/general.tsx index 7006b2b..9ac2ae1 100644 --- a/src/routes/settings/general.tsx +++ b/src/routes/settings/general.tsx @@ -1,6 +1,20 @@ import { Trans, useLingui } from "@lingui/react/macro"; import { createFileRoute } from "@tanstack/react-router"; -import { FolderOpen, Globe, Palette, WrapText } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { + AlertCircle, + Code2, + FolderOpen, + Globe, + Image as ImageIcon, + Palette, + Puzzle, + Sigma, + Sparkles, + Table as TableIcon, + Wand2, + WrapText, +} from "lucide-react"; import { SettingRow } from "@/components/settings/SettingRow"; import { Select, @@ -11,9 +25,20 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import type { Locale } from "@/i18n"; -import { usePreferencesStore } from "@/stores/preferencesStore"; +import { + type CodeBlockPluginMode, + type EditorPluginId, + usePreferencesStore, +} from "@/stores/preferencesStore"; import { useUIStore } from "@/stores/uiStore"; +interface PluginRowMeta { + id: EditorPluginId; + icon: LucideIcon; + label: string; + description: string; +} + function GeneralSettingsPage() { const { t } = useLingui(); const theme = useUIStore((s) => s.theme); @@ -27,6 +52,65 @@ function GeneralSettingsPage() { const restoreLastWorkspace = usePreferencesStore((s) => s.restoreLastWorkspace); const setRestoreLastWorkspace = usePreferencesStore((s) => s.setRestoreLastWorkspace); + const enabledPlugins = usePreferencesStore((s) => s.enabledPlugins); + const setPluginEnabled = usePreferencesStore((s) => s.setPluginEnabled); + const codeBlockMode = usePreferencesStore((s) => s.codeBlockMode); + const setCodeBlockMode = usePreferencesStore((s) => s.setCodeBlockMode); + + const isPluginEnabled = (id: EditorPluginId) => enabledPlugins.includes(id); + + // i18n-keyed plugin metadata; order drives row order. + const pluginRows: PluginRowMeta[] = [ + { + id: "math", + icon: Sigma, + label: t`数学公式`, + description: t`渲染 KaTeX 数学公式 ($...$ / $$...$$)`, + }, + { + id: "table", + icon: TableIcon, + label: t`表格`, + description: t`管道表格渲染为可视化卡片`, + }, + { + id: "mermaid", + icon: Sparkles, + label: t`Mermaid 图表`, + description: t`把 mermaid 代码块渲染为 SVG`, + }, + { + id: "admonition", + icon: AlertCircle, + label: t`Admonition`, + description: t`渲染 GFM / Obsidian 风格的提示块`, + }, + { + id: "codeBlock", + icon: Code2, + label: t`代码块`, + description: t`fenced 代码块渲染与高亮`, + }, + { + id: "blockImage", + icon: ImageIcon, + label: t`图片渲染`, + description: t`将 Markdown 图片渲染为内联 / 块级 widget`, + }, + { + id: "rawHtml", + icon: Wand2, + label: t`HTML 渲染`, + description: t`通过 DOMPurify 渲染嵌入的原生 HTML`, + }, + { + id: "smartPaste", + icon: Puzzle, + label: t`智能粘贴`, + description: t`粘贴 URL 转链接,拖放 / 粘贴文件上传为图片`, + }, + ]; + return (
@@ -97,6 +181,51 @@ function GeneralSettingsPage() {
+ + {/* Editor plugins section */} +
+

+ 编辑器插件 +

+

+ 切换插件启用状态后,需要重新打开文档或重启应用以生效。 +

+
+ {pluginRows.map((row, idx) => ( +
+ + {row.id === "codeBlock" ? ( +
+ + setPluginEnabled(row.id, v)} + /> +
+ ) : ( + setPluginEnabled(row.id, v)} + /> + )} +
+
+ ))} +
+
); diff --git a/src/stores/preferencesStore.ts b/src/stores/preferencesStore.ts index 95a9334..51e11d4 100644 --- a/src/stores/preferencesStore.ts +++ b/src/stores/preferencesStore.ts @@ -2,14 +2,99 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { createTauriStorage, waitForHydration } from "@/lib/tauriStore"; +/** + * 内置编辑器 plugin id 集合。与 `@swarmnote/editor-core/plugins/` 对应。 + * + * 顺序即为 settings UI 的展示顺序。 + */ +export const EDITOR_PLUGIN_IDS = [ + "math", + "table", + "mermaid", + "admonition", + "codeBlock", + "blockImage", + "rawHtml", + "smartPaste", +] as const; + +export type EditorPluginId = (typeof EDITOR_PLUGIN_IDS)[number]; + +/** + * codeBlock plugin 的渲染模式。`off` 通过 `enabledPlugins` 排除 codeBlock 表达, + * 故此处仅含三个真正"启用"语义的模式。 + */ +export type CodeBlockPluginMode = "inline" | "auto" | "toggle"; + interface PreferencesState { autoStartP2P: boolean; restoreLastWorkspace: boolean; + /** 启用的编辑器 plugin id 集合(与 EDITOR_PLUGIN_IDS 对应) */ + enabledPlugins: EditorPluginId[]; + /** codeBlock plugin 启用时的渲染模式 */ + codeBlockMode: CodeBlockPluginMode; } interface PreferencesActions { setAutoStartP2P: (value: boolean) => void; setRestoreLastWorkspace: (value: boolean) => void; + setPluginEnabled: (id: EditorPluginId, enabled: boolean) => void; + setCodeBlockMode: (mode: CodeBlockPluginMode) => void; +} + +const DEFAULT_ENABLED_PLUGINS: EditorPluginId[] = [...EDITOR_PLUGIN_IDS]; +const DEFAULT_CODE_BLOCK_MODE: CodeBlockPluginMode = "inline"; + +/** + * v0.0.x → v0.1 持久化迁移:把旧 `features.*` 键翻译为 `enabledPlugins` / + * `codeBlockMode`,并清掉旧键。 + * + * v0.0.x 的 SwarmNote host 并未把 editor `features` 写入 preferencesStore, + * 但 spec 要求 migration step 存在以处理理论上可能的 stale storage。 + * 这里在 hydration 时无副作用地探测旧字段——找到才翻译,找不到不修改。 + */ +function migrateLegacyFeatures(state: unknown): Partial { + if (!state || typeof state !== "object") return {}; + const raw = state as Record; + const legacy = raw.features as Record | undefined; + if (!legacy || typeof legacy !== "object") return {}; + + const enabledSet = new Set(DEFAULT_ENABLED_PLUGINS); + let codeBlockMode: CodeBlockPluginMode | undefined; + + const trySet = (key: string, id: EditorPluginId) => { + if (key in legacy) { + if (legacy[key] === false) enabledSet.delete(id); + else enabledSet.add(id); + } + }; + trySet("mathRendering", "math"); + trySet("mermaidRendering", "mermaid"); + trySet("blockImageRendering", "blockImage"); + trySet("rawHtmlRendering", "rawHtml"); + trySet("smartPaste", "smartPaste"); + trySet("admonition", "admonition"); + // table 历史上与 blockImageRendering 同生共死,这里复用 blockImageRendering 的取值 + if ("blockImageRendering" in legacy) { + if (legacy.blockImageRendering === false) enabledSet.delete("table"); + else enabledSet.add("table"); + } + if ("codeBlockMode" in legacy) { + const mode = legacy.codeBlockMode; + if (mode === "off") enabledSet.delete("codeBlock"); + else if (mode === "inline" || mode === "auto" || mode === "toggle") { + enabledSet.add("codeBlock"); + codeBlockMode = mode; + } + } + + // 删除旧 features 键,避免下次启动重复 migrate + delete raw.features; + + return { + enabledPlugins: EDITOR_PLUGIN_IDS.filter((id) => enabledSet.has(id)), + ...(codeBlockMode ? { codeBlockMode } : {}), + }; } export const usePreferencesStore = create()( @@ -17,13 +102,41 @@ export const usePreferencesStore = create (set) => ({ autoStartP2P: true, restoreLastWorkspace: true, + enabledPlugins: DEFAULT_ENABLED_PLUGINS, + codeBlockMode: DEFAULT_CODE_BLOCK_MODE, setAutoStartP2P: (value: boolean) => set({ autoStartP2P: value }), setRestoreLastWorkspace: (value: boolean) => set({ restoreLastWorkspace: value }), + setPluginEnabled: (id: EditorPluginId, enabled: boolean) => + set((state) => { + const has = state.enabledPlugins.includes(id); + if (enabled && !has) { + return { + enabledPlugins: EDITOR_PLUGIN_IDS.filter( + (pid) => pid === id || state.enabledPlugins.includes(pid), + ), + }; + } + if (!enabled && has) { + return { enabledPlugins: state.enabledPlugins.filter((pid) => pid !== id) }; + } + return state; + }), + setCodeBlockMode: (mode: CodeBlockPluginMode) => set({ codeBlockMode: mode }), }), { name: "swarmnote-preferences", storage: createTauriStorage("settings.json"), + version: 1, + // Migration runs on hydration; the returned object is shallow-merged into + // the rehydrated state. Idempotent: legacy keys are deleted after first run. + migrate: (persistedState, _version) => { + const migrated = migrateLegacyFeatures(persistedState); + return { + ...(persistedState as PreferencesState), + ...migrated, + } as PreferencesState & PreferencesActions; + }, }, ), ); From cc7bdc84242541589520f85866ee566f2cbfce48 Mon Sep 17 00:00:00 2001 From: yexiyue Date: Wed, 13 May 2026 11:59:09 +0800 Subject: [PATCH 2/2] feat(editor): wire @swarmnote/editor-react sibling package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add @swarmnote/editor-react as a linked sibling dependency (alongside existing @swarmnote/editor-core): - package.json: pnpm.overrides + dependencies entries for editor-react - src/App.css: @source directive for sibling editor-react/dist so Tailwind 4 scans className literals from cross-package built output - dev-notes/knowledge/editor.md: document the new 4-package sibling architecture (editor-core / editor-web / editor-react / editor-react-native) - dev-notes/plans/editor-core-host-boundary.md: mark §六 partially implemented by OpenSpec change split-editor-react-packages Desktop host code is unchanged. SwarmNote still uses its own EditorContextMenu / TableContextMenu — those are business-level UI orchestration (deeply coupled to host shadcn primitives + uiStore + Lingui macro) and stay in the host repo per the component-library design philosophy. editor-react built-ins (EditorView, EditorToolbar, I18nProvider) are available for opt-in use when needed. Depends on swarmnote-editor commit 7acc831 --- dev-notes/knowledge/editor.md | 17 +- dev-notes/plans/editor-core-host-boundary.md | 4 + package.json | 4 +- pnpm-lock.yaml | 602 +------------------ src/App.css | 4 + 5 files changed, 31 insertions(+), 600 deletions(-) diff --git a/dev-notes/knowledge/editor.md b/dev-notes/knowledge/editor.md index c24258d..a010160 100644 --- a/dev-notes/knowledge/editor.md +++ b/dev-notes/knowledge/editor.md @@ -25,9 +25,24 @@ - 桌面端 React 容器:`src/components/editor/NoteEditor.tsx` - 文档大纲:`src/components/editor/DocumentOutline.tsx`(基于 `extractHeadings`) +## Sibling 仓 swarmnote-editor 的四包架构(v0.2 起) + +sibling 仓 `swarm-apps/swarmnote-editor` 现在包含 4 个包,按职责分层: + +| 包 | 角色 | 桌面 host 用 | RN host 用 | +|----|------|------------|----------| +| `@swarmnote/editor-core` | 平台无关 CM6 内核 + Plugin SDK | ✅ 直接 import | ❌ 不可直接 import(dep graph 含 web-only CodeMirror)| +| `@swarmnote/editor-web` | WebView 内 runtime + Comlink endpoint + `dist/index.html` 单文件 | ❌ 不用 | ✅ 通过 `./contracts` subpath 拿类型 + `./dist/index.html` require 拿 WebView 资源 | +| `@swarmnote/editor-react` | React 组件库(EditorView / EditorToolbar / I18nProvider) | ✅ 按需 import | ❌ | +| `@swarmnote/editor-react-native` | RN 组件库(useEditorBridge / useEditorFormatting / comlink-webview-adapter / I18nProvider) | ❌ | ✅ | + +**设计哲学**:`editor-react` / `editor-react-native` 是**独立组件库**(类似 chakra-ui / radix-ui 范式),不是 SwarmNote 桌面 / RN 端现有组件的搬迁目标。host 是「用户之一」,可以选用内置组件,也可以自己实现。 + +详见 OpenSpec change [`split-editor-react-packages`](../../openspec/changes/split-editor-react-packages/design.md) D12 决策。 + ## @swarmnote/editor-core 是外部 sibling 仓 + pnpm link -`@swarmnote/editor-core` 不再是主仓的 submodule,而是独立 repo `swarm-apps/swarmnote-editor`。本地通过 `pnpm.overrides` 把包名解析到 sibling 路径 `../swarmnote-editor/packages/editor-core`,所以 sibling 必须 clone 到与 SwarmNote 同级目录。 +`@swarmnote/editor-core` 不再是主仓的 submodule,而是独立 repo `swarm-apps/swarmnote-editor`。本地通过 `pnpm.overrides` 把包名解析到 sibling 路径 `../swarmnote-editor/packages/editor-core`,所以 sibling 必须 clone 到与 SwarmNote 同级目录。v0.2 起新增 3 个 sibling 包(editor-web / editor-react / editor-react-native)同样走 `pnpm.overrides` link。 ### Local development with editor-core diff --git a/dev-notes/plans/editor-core-host-boundary.md b/dev-notes/plans/editor-core-host-boundary.md index ef965da..0f75765 100644 --- a/dev-notes/plans/editor-core-host-boundary.md +++ b/dev-notes/plans/editor-core-host-boundary.md @@ -545,6 +545,10 @@ graph TD ## 六、建议的拆分结果 +> **已落实(部分)于 OpenSpec change [`split-editor-react-packages`](../../openspec/changes/split-editor-react-packages/proposal.md)(v0.2)**:sibling 仓 swarmnote-editor 已新增 3 个包(editor-web / editor-react / editor-react-native),架子搭起。 +> +> v0.2 范围(按 design D12 修订):包内只含**通用组件示例**(EditorView / EditorToolbar 简版 / I18nProvider)+ **干净的 hooks/adapter**(useEditorBridge / useEditorFormatting / comlink-webview-adapter)。SwarmNote 桌面的 ContextMenu / TableContextMenu 和 SwarmNote-RN 的 MarkdownEditor / EditorToolbar / EditorHeadingSheet **仍保留在 host**(业务编排级耦合,按组件库哲学留 v0.2.1 重写为通用版本)。 + ### 应保留在 `editor-core` - `createEditor` diff --git a/package.json b/package.json index 09a503d..e792a91 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "esbuild" ], "overrides": { - "@swarmnote/editor-core": "link:../swarmnote-editor/packages/editor-core" + "@swarmnote/editor-core": "link:../swarmnote-editor/packages/editor-core", + "@swarmnote/editor-react": "link:../swarmnote-editor/packages/editor-react" } }, "dependencies": { @@ -31,6 +32,7 @@ "@lingui/core": "^5.9.3", "@lingui/react": "^5.9.3", "@swarmnote/editor-core": "*", + "@swarmnote/editor-react": "*", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-router": "^1.168.1", "@tauri-apps/api": "^2.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa7347e..581d93e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: '@swarmnote/editor-core': link:../swarmnote-editor/packages/editor-core + '@swarmnote/editor-react': link:../swarmnote-editor/packages/editor-react importers: @@ -29,6 +30,9 @@ importers: '@swarmnote/editor-core': specifier: link:../swarmnote-editor/packages/editor-core version: link:../swarmnote-editor/packages/editor-core + '@swarmnote/editor-react': + specifier: link:../swarmnote-editor/packages/editor-react + version: link:../swarmnote-editor/packages/editor-react '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -200,61 +204,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - packages/editor: - dependencies: - '@codemirror/autocomplete': - specifier: ^6.20.1 - version: 6.20.1 - '@codemirror/commands': - specifier: ^6.10.3 - version: 6.10.3 - '@codemirror/lang-markdown': - specifier: ^6.5.0 - version: 6.5.0 - '@codemirror/language': - specifier: ^6.12.3 - version: 6.12.3 - '@codemirror/language-data': - specifier: ^6.5.2 - version: 6.5.2 - '@codemirror/legacy-modes': - specifier: ^6.5.2 - version: 6.5.2 - '@codemirror/search': - specifier: ^6.6.0 - version: 6.6.0 - '@codemirror/state': - specifier: ^6.6.0 - version: 6.6.0 - '@codemirror/view': - specifier: ^6.41.0 - version: 6.41.0 - '@lezer/common': - specifier: ^1.2.3 - version: 1.5.2 - '@lezer/highlight': - specifier: ^1.2.3 - version: 1.2.3 - '@lezer/markdown': - specifier: ^1.6.3 - version: 1.6.3 - dompurify: - specifier: ^3.4.2 - version: 3.4.2 - katex: - specifier: ^0.16.45 - version: 0.16.45 - y-codemirror.next: - specifier: ^0.3.5 - version: 0.3.5(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)(yjs@13.6.30) - yjs: - specifier: ^13.6.20 - version: 13.6.30 - devDependencies: - typescript: - specifier: ~5.9.2 - version: 5.9.3 - packages: '@astrojs/check@0.9.9': @@ -549,96 +498,6 @@ packages: resolution: {integrity: sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==} engines: {node: '>= 20.12.0'} - '@codemirror/autocomplete@6.20.1': - resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} - - '@codemirror/commands@6.10.3': - resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} - - '@codemirror/lang-angular@0.1.4': - resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} - - '@codemirror/lang-cpp@6.0.3': - resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} - - '@codemirror/lang-css@6.3.1': - resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} - - '@codemirror/lang-go@6.0.1': - resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} - - '@codemirror/lang-html@6.4.11': - resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} - - '@codemirror/lang-java@6.0.2': - resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} - - '@codemirror/lang-javascript@6.2.5': - resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} - - '@codemirror/lang-jinja@6.0.0': - resolution: {integrity: sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==} - - '@codemirror/lang-json@6.0.2': - resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} - - '@codemirror/lang-less@6.0.2': - resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} - - '@codemirror/lang-liquid@6.3.2': - resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} - - '@codemirror/lang-markdown@6.5.0': - resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} - - '@codemirror/lang-php@6.0.2': - resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} - - '@codemirror/lang-python@6.2.1': - resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} - - '@codemirror/lang-rust@6.0.2': - resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} - - '@codemirror/lang-sass@6.0.2': - resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} - - '@codemirror/lang-sql@6.10.0': - resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} - - '@codemirror/lang-vue@0.1.3': - resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} - - '@codemirror/lang-wast@6.0.2': - resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} - - '@codemirror/lang-xml@6.1.0': - resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} - - '@codemirror/lang-yaml@6.1.3': - resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==} - - '@codemirror/language-data@6.5.2': - resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} - - '@codemirror/language@6.12.3': - resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} - - '@codemirror/legacy-modes@6.5.2': - resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==} - - '@codemirror/lint@6.9.5': - resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==} - - '@codemirror/search@6.6.0': - resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} - - '@codemirror/state@6.6.0': - resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} - - '@codemirror/view@6.41.0': - resolution: {integrity: sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==} - '@commitlint/cli@20.5.0': resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} engines: {node: '>=v18'} @@ -1468,57 +1327,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@lezer/common@1.5.2': - resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - - '@lezer/cpp@1.1.5': - resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} - - '@lezer/css@1.3.3': - resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==} - - '@lezer/go@1.0.1': - resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} - - '@lezer/highlight@1.2.3': - resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} - - '@lezer/html@1.3.13': - resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} - - '@lezer/java@1.1.3': - resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} - - '@lezer/javascript@1.5.4': - resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} - - '@lezer/json@1.0.3': - resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} - - '@lezer/lr@1.4.8': - resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} - - '@lezer/markdown@1.6.3': - resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} - - '@lezer/php@1.0.5': - resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} - - '@lezer/python@1.1.18': - resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} - - '@lezer/rust@1.0.2': - resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} - - '@lezer/sass@1.1.0': - resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} - - '@lezer/xml@1.0.6': - resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} - - '@lezer/yaml@1.0.4': - resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} - '@lingui/babel-plugin-extract-messages@5.9.3': resolution: {integrity: sha512-zm6QHDILmhj8olgLL2zHQn18yFA5mf4hX7QzCr1OOI/e815I0IkecCYue1Ych+y+B+V0eLriiW8AcfpDRCQFFw==} engines: {node: '>=20.0.0'} @@ -1580,9 +1388,6 @@ packages: peerDependencies: vite: ^3 || ^4 || ^5.0.9 || ^6 || ^7 || ^8 - '@marijn/find-cluster-break@1.0.2': - resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@messageformat/parser@5.1.1': resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==} @@ -2884,9 +2689,6 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -3213,10 +3015,6 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - common-ancestor-path@2.0.0: resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} engines: {node: '>= 18'} @@ -3296,9 +3094,6 @@ packages: typescript: optional: true - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3426,9 +3221,6 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.2: - resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} - domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -4041,10 +3833,6 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - katex@0.16.45: - resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} - hasBin: true - kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -5149,9 +4937,6 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} - style-mod@4.1.3: - resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5649,9 +5434,6 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -5697,13 +5479,6 @@ packages: xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} - y-codemirror.next@0.3.5: - resolution: {integrity: sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw==} - peerDependencies: - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 - yjs: ^13.5.6 - y-protocols@1.0.7: resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -6238,254 +6013,6 @@ snapshots: fast-wrap-ansi: 0.2.0 sisteransi: 1.0.5 - '@codemirror/autocomplete@6.20.1': - dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - - '@codemirror/commands@6.10.3': - dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - - '@codemirror/lang-angular@0.1.4': - dependencies: - '@codemirror/lang-html': 6.4.11 - '@codemirror/lang-javascript': 6.2.5 - '@codemirror/language': 6.12.3 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@codemirror/lang-cpp@6.0.3': - dependencies: - '@codemirror/language': 6.12.3 - '@lezer/cpp': 1.1.5 - - '@codemirror/lang-css@6.3.1': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@lezer/common': 1.5.2 - '@lezer/css': 1.3.3 - - '@codemirror/lang-go@6.0.1': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@lezer/common': 1.5.2 - '@lezer/go': 1.0.1 - - '@codemirror/lang-html@6.4.11': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/lang-css': 6.3.1 - '@codemirror/lang-javascript': 6.2.5 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - '@lezer/css': 1.3.3 - '@lezer/html': 1.3.13 - - '@codemirror/lang-java@6.0.2': - dependencies: - '@codemirror/language': 6.12.3 - '@lezer/java': 1.1.3 - - '@codemirror/lang-javascript@6.2.5': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/language': 6.12.3 - '@codemirror/lint': 6.9.5 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - '@lezer/javascript': 1.5.4 - - '@codemirror/lang-jinja@6.0.0': - dependencies: - '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@codemirror/lang-json@6.0.2': - dependencies: - '@codemirror/language': 6.12.3 - '@lezer/json': 1.0.3 - - '@codemirror/lang-less@6.0.2': - dependencies: - '@codemirror/lang-css': 6.3.1 - '@codemirror/language': 6.12.3 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@codemirror/lang-liquid@6.3.2': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@codemirror/lang-markdown@6.5.0': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - '@lezer/markdown': 1.6.3 - - '@codemirror/lang-php@6.0.2': - dependencies: - '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@lezer/common': 1.5.2 - '@lezer/php': 1.0.5 - - '@codemirror/lang-python@6.2.1': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@lezer/common': 1.5.2 - '@lezer/python': 1.1.18 - - '@codemirror/lang-rust@6.0.2': - dependencies: - '@codemirror/language': 6.12.3 - '@lezer/rust': 1.0.2 - - '@codemirror/lang-sass@6.0.2': - dependencies: - '@codemirror/lang-css': 6.3.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@lezer/common': 1.5.2 - '@lezer/sass': 1.1.0 - - '@codemirror/lang-sql@6.10.0': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@codemirror/lang-vue@0.1.3': - dependencies: - '@codemirror/lang-html': 6.4.11 - '@codemirror/lang-javascript': 6.2.5 - '@codemirror/language': 6.12.3 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@codemirror/lang-wast@6.0.2': - dependencies: - '@codemirror/language': 6.12.3 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@codemirror/lang-xml@6.1.0': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - '@lezer/xml': 1.0.6 - - '@codemirror/lang-yaml@6.1.3': - dependencies: - '@codemirror/autocomplete': 6.20.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - '@lezer/yaml': 1.0.4 - - '@codemirror/language-data@6.5.2': - dependencies: - '@codemirror/lang-angular': 0.1.4 - '@codemirror/lang-cpp': 6.0.3 - '@codemirror/lang-css': 6.3.1 - '@codemirror/lang-go': 6.0.1 - '@codemirror/lang-html': 6.4.11 - '@codemirror/lang-java': 6.0.2 - '@codemirror/lang-javascript': 6.2.5 - '@codemirror/lang-jinja': 6.0.0 - '@codemirror/lang-json': 6.0.2 - '@codemirror/lang-less': 6.0.2 - '@codemirror/lang-liquid': 6.3.2 - '@codemirror/lang-markdown': 6.5.0 - '@codemirror/lang-php': 6.0.2 - '@codemirror/lang-python': 6.2.1 - '@codemirror/lang-rust': 6.0.2 - '@codemirror/lang-sass': 6.0.2 - '@codemirror/lang-sql': 6.10.0 - '@codemirror/lang-vue': 0.1.3 - '@codemirror/lang-wast': 6.0.2 - '@codemirror/lang-xml': 6.1.0 - '@codemirror/lang-yaml': 6.1.3 - '@codemirror/language': 6.12.3 - '@codemirror/legacy-modes': 6.5.2 - - '@codemirror/language@6.12.3': - dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - style-mod: 4.1.3 - - '@codemirror/legacy-modes@6.5.2': - dependencies: - '@codemirror/language': 6.12.3 - - '@codemirror/lint@6.9.5': - dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - crelt: 1.0.6 - - '@codemirror/search@6.6.0': - dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - crelt: 1.0.6 - - '@codemirror/state@6.6.0': - dependencies: - '@marijn/find-cluster-break': 1.0.2 - - '@codemirror/view@6.41.0': - dependencies: - '@codemirror/state': 6.6.0 - crelt: 1.0.6 - style-mod: 4.1.3 - w3c-keyname: 2.2.8 - '@commitlint/cli@20.5.0(@types/node@25.5.0)(conventional-commits-parser@6.3.0)(typescript@5.8.3)': dependencies: '@commitlint/format': 20.5.0 @@ -7073,99 +6600,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lezer/common@1.5.2': {} - - '@lezer/cpp@1.1.5': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/css@1.3.3': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/go@1.0.1': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/highlight@1.2.3': - dependencies: - '@lezer/common': 1.5.2 - - '@lezer/html@1.3.13': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/java@1.1.3': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/javascript@1.5.4': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/json@1.0.3': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/lr@1.4.8': - dependencies: - '@lezer/common': 1.5.2 - - '@lezer/markdown@1.6.3': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - - '@lezer/php@1.0.5': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/python@1.1.18': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/rust@1.0.2': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/sass@1.1.0': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/xml@1.0.6': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - - '@lezer/yaml@1.0.4': - dependencies: - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 - '@lingui/babel-plugin-extract-messages@5.9.3': {} '@lingui/babel-plugin-lingui-macro@5.9.3(typescript@5.8.3)': @@ -7263,8 +6697,6 @@ snapshots: - supports-color - typescript - '@marijn/find-cluster-break@1.0.2': {} - '@messageformat/parser@5.1.1': dependencies: moo: 0.5.3 @@ -8562,9 +7994,6 @@ snapshots: '@types/statuses@2.0.6': {} - '@types/trusted-types@2.0.7': - optional: true - '@types/unist@3.0.3': {} '@types/validate-npm-package-name@4.0.2': {} @@ -8998,8 +8427,6 @@ snapshots: commander@14.0.3: {} - commander@8.3.0: {} - common-ancestor-path@2.0.0: {} compare-func@2.0.0: @@ -9066,8 +8493,6 @@ snapshots: optionalDependencies: typescript: 5.8.3 - crelt@1.0.6: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -9175,10 +8600,6 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.2: - optionalDependencies: - '@types/trusted-types': 2.0.7 - domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -9878,10 +9299,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - katex@0.16.45: - dependencies: - commander: 8.3.0 - kleur@3.0.3: {} kleur@4.1.5: {} @@ -11278,8 +10695,6 @@ snapshots: strip-final-newline@4.0.0: {} - style-mod@4.1.3: {} - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -11684,8 +11099,6 @@ snapshots: vscode-uri@3.1.0: {} - w3c-keyname@2.2.8: {} - wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -11727,13 +11140,6 @@ snapshots: xxhash-wasm@1.1.0: {} - y-codemirror.next@0.3.5(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)(yjs@13.6.30): - dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 - lib0: 0.2.117 - yjs: 13.6.30 - y-protocols@1.0.7(yjs@13.6.30): dependencies: lib0: 0.2.117 diff --git a/src/App.css b/src/App.css index 00603a4..ca4d6db 100644 --- a/src/App.css +++ b/src/App.css @@ -4,6 +4,10 @@ @import "tw-animate-css"; @import "shadcn/tailwind.css"; +/* Scan @swarmnote/editor-react built output so Tailwind picks up className + string literals used inside the component library (e.g. EditorToolbar). */ +@source "../../swarmnote-editor/packages/editor-react/dist/**/*.{mjs,cjs,d.mts,d.cts}"; + @plugin "tailwind-scrollbar"; @custom-variant hover-none (@media (hover: none));