feat: 收口 mindmap Phase 6 Leptos UI shell

This commit is contained in:
lix-2026
2026-05-11 12:27:40 +08:00
parent b7dddd2a66
commit b5eb27fabc
51 changed files with 8669 additions and 1313 deletions
@@ -1,6 +1,9 @@
"use client";
import "simple-mind-map/dist/simpleMindMap.esm.css";
"use client";
// 说明:本文件仅保留为 legacy/compat/reference 实现,用于独立导图页、
// 历史 BlockNote 导图块和 blob import/export 对照;3000 文档页 Phase 6
// 默认主链是 leptos-tiptap NodeView + leptos-mindmap adapter。
import "simple-mind-map/dist/simpleMindMap.esm.css";
import React, {
useCallback,
useEffect,
@@ -1,7 +1,10 @@
"use client";
import React, { useEffect, useRef, useState, useCallback } from "react";
import { createPortal } from "react-dom";
"use client";
import React, { useEffect, useRef, useState, useCallback } from "react";
// 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
import { createPortal } from "react-dom";
import type { MindMapNode } from "./mindmapTypes";
// 菜单项配置
@@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useEffect, useState } from "react";
// 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
type CountProps = {
mindmap: any;
};
@@ -3,6 +3,9 @@
/* eslint-disable @next/next/no-img-element */
import React, { useEffect, useRef, useState } from "react";
// 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
type MiniMapProps = {
mindmap: any;
show: boolean;
@@ -1,5 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useEffect } from "react";
// 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
import { Minus, Plus, Maximize, Map, MapPin, Minimize, Eye, EyeOff } from "lucide-react";
type NavigatorProps = {
@@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useEffect, useMemo, useState } from "react";
import Image from "next/image";
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useEffect, useMemo, useState } from "react";
// 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
import Image from "next/image";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Toggle } from "@/components/ui/toggle";
@@ -1,4 +1,7 @@
import React from "react";
// 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
import { ChevronRight } from "lucide-react";
import { sidebarTriggers, type SidebarPanel } from "./mindmapSidebarConfig";
@@ -1,5 +1,8 @@
import React from "react";
import {
import React from "react";
// 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
import {
fileToolbarMeta,
fileToolbarOrder,
nodeToolbarMeta,
@@ -5,6 +5,12 @@ import {
} from "@/components/editor/editor-host-config";
import { describe, expect, it } from "vitest";
const phase6MindmapSuccessSelectors = [
'[data-testid="mnote-mindmap-editor-root"]',
'[data-testid="simple-mind-map-runtime"]',
'[data-testid="mindmap-rust-shell"]',
];
describe("editor-host-config", () => {
it("保持 island 为默认正式 host", () => {
expect(DEFAULT_EDITOR_HOST_KIND).toBe("leptos_tiptap_island");
@@ -16,22 +22,34 @@ describe("editor-host-config", () => {
expect(normalizeEditorHostKind("leptos_tiptap")).toBe("leptos_tiptap_island");
});
it("保留 iframe debug 作为显式调试 host", () => {
expect(normalizeEditorHostKind("leptos_tiptap_iframe_debug")).toBe(
"leptos_tiptap_iframe_debug",
);
expect(normalizeEditorHostKind("iframe_debug")).toBe("leptos_tiptap_iframe_debug");
expect(normalizeEditorHostKind("leptos_tiptap_debug")).toBe(
"leptos_tiptap_iframe_debug",
);
});
it("优先尊重 query override 的 debug host 选择", () => {
it(" iframe debug host 配置应折返到正式 island host", () => {
expect(normalizeEditorHostKind("leptos_tiptap_iframe_debug")).toBe("leptos_tiptap_island");
expect(normalizeEditorHostKind("iframe_debug")).toBe("leptos_tiptap_island");
expect(normalizeEditorHostKind("leptos_tiptap_debug")).toBe("leptos_tiptap_island");
expect(
resolveEditorHostKind({
override: "leptos_tiptap_iframe_debug",
runtimeDefault: "leptos_tiptap_island",
}),
).toBe("leptos_tiptap_iframe_debug");
).toBe("leptos_tiptap_island");
});
it("不再允许通过 query 或 runtime 默认值切回 blocknote", () => {
expect(
resolveEditorHostKind({
override: "blocknote",
runtimeDefault: "leptos_tiptap_island",
}),
).toBe("leptos_tiptap_island");
expect(normalizeEditorHostKind("blocknote")).toBe("leptos_tiptap_island");
});
it("Phase 6 mindmap 成功条件不应依赖旧 React runtime 标记", () => {
expect(phase6MindmapSuccessSelectors).not.toContain(
".mnote-mindmap-react-mount",
);
expect(phase6MindmapSuccessSelectors).not.toContain(
"window.__mindmapInstance",
);
});
});
@@ -1,7 +1,4 @@
export type EditorHostKind =
| "blocknote"
| "leptos_tiptap_island"
| "leptos_tiptap_iframe_debug";
export type EditorHostKind = "leptos_tiptap_island";
export interface EditorHostConfig {
kind: EditorHostKind;
@@ -15,7 +12,7 @@ export function normalizeEditorHostKind(
): EditorHostKind {
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
if (normalized === "blocknote") {
return "blocknote";
return fallback;
}
if (
normalized === "leptos_tiptap_runtime" ||
@@ -24,15 +21,7 @@ export function normalizeEditorHostKind(
) {
return "leptos_tiptap_island";
}
// 说明:iframe 只保留给显式调试桥,不再混入正式 runtime 主链
if (
normalized === "leptos_tiptap_iframe_debug" ||
normalized === "leptos_tiptap_debug" ||
normalized === "iframe_debug"
) {
return "leptos_tiptap_iframe_debug";
}
// 说明:保留历史 query 值兼容;未显式声明 debug 时,一律回到正式 runtime host。
// 说明:历史 debug host 已退出文档页主路径,旧 query/runtime 值统一折返正式 island host
return fallback;
}
@@ -53,7 +42,7 @@ export function resolveEditorHostKind(input: {
}
export function isLeptosTiptapHostKind(kind: EditorHostKind): boolean {
return kind !== "blocknote";
return kind === "leptos_tiptap_island";
}
export function getEditorHostKindFromEnv(value?: unknown): EditorHostKind {
@@ -17,19 +17,6 @@ const LeptosTiptapIslandEditor = dynamic(
},
) as ComponentType<DocumentEditorHostProps>;
const LeptosTiptapIframeDebugEditor = dynamic(
() => import("@/components/editor/leptos-tiptap-editor-host").then((mod) => mod.LeptosTiptapEditorHost),
{
ssr: false,
loading: () => (
<div className="flex h-64 items-center justify-center text-sm text-gray-400">...</div>
),
},
) as ComponentType<DocumentEditorHostProps>;
export function EditorHost(props: DocumentEditorHostProps) {
if (props.hostKind === "leptos_tiptap_iframe_debug") {
return <LeptosTiptapIframeDebugEditor {...props} />;
}
return <LeptosTiptapIslandEditor {...props} />;
}
@@ -1,6 +1,7 @@
import { act, useState } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import { readFileSync } from "node:fs";
import { LeptosTiptapIslandEditorHost } from "@/components/editor/leptos-tiptap-island-editor-host";
import type { DocumentEditorHostProps } from "@/components/editor/editor-host-types";
@@ -135,6 +136,18 @@ describe("leptos-tiptap-island-editor-host", () => {
let container: HTMLDivElement;
let root: Root;
it("默认 island host 不加载旧 React mindmap 主链", () => {
const source = readFileSync("src/components/editor/leptos-tiptap-island-editor-host.tsx", "utf8");
const smokeSource = readFileSync("../scripts/task166-mindmap-phase6-block-smoke.js", "utf8");
expect(source).not.toContain("MindmapBlockView");
expect(source).not.toContain("mnote-mindmap-react-mount");
expect(source).not.toContain("mnoteMindmapData");
expect(source).not.toContain("__mindmapInstance");
expect(smokeSource).not.toContain("mnote-mindmap-react-mount");
expect(smokeSource).not.toContain("__mindmapInstance");
});
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
@@ -39,8 +39,21 @@ type IslandRuntimeModule = {
default: (input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module) => Promise<unknown>;
mount: (container: Element, options: unknown) => number;
unmount: (mountId: number) => void;
mount_mindmap_shell?: (container: Element, options: unknown) => number;
unmount_mindmap_shell?: (mountId: number) => void;
};
type MindmapRustShellBridge = {
mount: (container: Element, options: unknown) => number;
unmount: (mountId: number) => void;
};
declare global {
interface Window {
__MNOTE_MINDMAP_RUST_SHELL__?: MindmapRustShellBridge;
}
}
type RuntimeEnvelope<T = unknown> = {
protocol?: string;
runtime?: string;
@@ -251,6 +264,15 @@ async function loadIslandRuntime(): Promise<{
throw new Error("island entry 缺少 unmount 导出");
}
await runtimeModule.default(wasmAssetUrl ?? undefined);
if (
typeof runtimeModule.mount_mindmap_shell === "function" &&
typeof runtimeModule.unmount_mindmap_shell === "function"
) {
window.__MNOTE_MINDMAP_RUST_SHELL__ = {
mount: runtimeModule.mount_mindmap_shell,
unmount: runtimeModule.unmount_mindmap_shell,
};
}
return {
runtimeModule,
entryAssetUrl,
@@ -778,9 +800,6 @@ export function LeptosTiptapIslandEditorHost(props: DocumentEditorHostProps) {
);
}
},
requestFallbackToBlockNote: () => {
requestFallback("explicit_fallback", "通过页面壳显式切回 BlockNote");
},
};
useEditorBridgeStore.getState().registerBridge(editorBridge);
return () => {