mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-02 23:11:14 +08:00
feat(plugin-sdk): add TypeScript SDK for Infinite Canvas plugins with automatic JSX and build support
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# 插件模板(TypeScript + SDK)
|
||||
|
||||
复制本目录即可开始写一个新的画布节点插件。
|
||||
|
||||
## 上手
|
||||
|
||||
```bash
|
||||
cp -r plugins/canvas/template plugins/canvas/my-plugin
|
||||
cd plugins/canvas/my-plugin
|
||||
# 改 package.json 的 name;改 src/index.tsx 里的 id / name / type
|
||||
npm install
|
||||
npm run dev # watch 构建,产物同步到 web/public/plugins/my-plugin.js
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
然后启动画布 `web`,在「节点插件」管理器里启用(自动发现)即可看到你的节点。发布时把 `dist/<name>.js` 托管到任意静态地址,用户填 URL 安装。
|
||||
|
||||
> 产物名取**目录名**(`my-plugin/` → `my-plugin.js`),所以复制后记得把目录改成你的插件名。
|
||||
|
||||
## 你只需要关心
|
||||
|
||||
- `src/index.tsx` 里 `Content({ ctx })` 的节点 UI 与逻辑;
|
||||
- `definePlugin({...})` 里的 `id` / `nodes[].type` / `defaultSize` 等元信息。
|
||||
|
||||
类型、JSX、宿主 React、构建都由 `@infinite-canvas/plugin-sdk` 提供,写 TSX 全程有补全。`ctx` 的完整能力见 `plugins/canvas/README.md` 与 SDK 的类型定义。
|
||||
@@ -0,0 +1,3 @@
|
||||
import { buildPlugin } from "@infinite-canvas/plugin-sdk/build";
|
||||
|
||||
await buildPlugin(import.meta.url);
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "canvas-plugin-template",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "canvas-plugin-template",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@infinite-canvas/plugin-sdk": "file:../sdk",
|
||||
"@types/react": "19.1.12",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"name": "@infinite-canvas/plugin-sdk",
|
||||
"version": "0.1.0",
|
||||
"dev": true,
|
||||
"devDependencies": {
|
||||
"@types/react": "19.1.12",
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@infinite-canvas/plugin-sdk": {
|
||||
"resolved": "../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
||||
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "canvas-plugin-template",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Infinite Canvas 插件起步模板(TypeScript + SDK)",
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
"dev": "node build.mjs --watch",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@infinite-canvas/plugin-sdk": "file:../sdk",
|
||||
"@types/react": "19.1.12",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// 插件模板:复制本目录 → 改 id/name/type → 写你的节点。
|
||||
// 这是一个演示节点:编辑文本、跟随主题、读取上游节点、用画布指令衍生新节点。
|
||||
import { definePlugin, useState } from "@infinite-canvas/plugin-sdk";
|
||||
import type { CanvasNodeContentProps } from "@infinite-canvas/plugin-sdk";
|
||||
|
||||
function TemplateContent({ ctx }: CanvasNodeContentProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const content = ctx.node.metadata?.content || "";
|
||||
|
||||
// 读取上游相连节点的文本内容(演示 ctx.getUpstream)
|
||||
const upstreamText = ctx
|
||||
.getUpstream()
|
||||
.map((node) => node.metadata?.content)
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
// 用画布指令集衍生一个文本节点并连线(演示 ctx.applyOps)
|
||||
const spawnBelow = () => {
|
||||
const id = `template-${ctx.node.id}-${ctx.getNodes().length}`;
|
||||
ctx.applyOps([
|
||||
{ type: "add_node", id, nodeType: "text", title: "衍生节点", x: ctx.node.position.x, y: ctx.node.position.y + ctx.node.height + 40, metadata: { content, status: "success" } },
|
||||
{ type: "connect_nodes", fromNodeId: ctx.node.id, toNodeId: id },
|
||||
]);
|
||||
};
|
||||
|
||||
const btn = { padding: "4px 10px", borderRadius: 8, border: `1px solid ${ctx.theme.node.stroke}`, background: ctx.theme.toolbar.panel, color: ctx.theme.node.text, cursor: "pointer", fontSize: 12 } as const;
|
||||
|
||||
return (
|
||||
// data-canvas-no-zoom + stopPropagation:交互控件避免触发画布拖拽/缩放
|
||||
<div data-canvas-no-zoom onMouseDown={(e) => e.stopPropagation()} style={{ height: "100%", width: "100%", display: "flex", flexDirection: "column", gap: 8, padding: 12, boxSizing: "border-box", color: ctx.theme.node.text }}>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button type="button" style={btn} onClick={() => setEditing((v) => !v)}>
|
||||
{editing ? "完成" : "编辑"}
|
||||
</button>
|
||||
<button type="button" style={btn} onClick={spawnBelow}>
|
||||
衍生文本
|
||||
</button>
|
||||
</div>
|
||||
{editing ? (
|
||||
<textarea autoFocus value={content} placeholder="输入内容…" onChange={(e) => ctx.updateMetadata({ content: e.target.value })} onWheel={(e) => e.stopPropagation()} style={{ flex: 1, resize: "none", border: "none", outline: "none", background: "transparent", color: ctx.theme.node.text, fontSize: 14, lineHeight: 1.5 }} />
|
||||
) : (
|
||||
<div onWheel={(e) => e.stopPropagation()} style={{ flex: 1, overflow: "auto", whiteSpace: "pre-wrap", fontSize: 14, lineHeight: 1.5 }}>
|
||||
{content || upstreamText || <span style={{ color: ctx.theme.node.placeholder }}>点“编辑”写点什么,或连一个上游文本节点</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
id: "template", // ← 改成你的唯一 id(kebab-case)
|
||||
name: "模板节点",
|
||||
version: "1.0.0",
|
||||
description: "插件起步模板:改我。",
|
||||
nodes: [
|
||||
{
|
||||
type: "template:node", // ← 建议 "<id>:<name>",全局唯一
|
||||
title: "模板",
|
||||
icon: "✨",
|
||||
description: "起步示例节点",
|
||||
defaultSize: { width: 280, height: 200 },
|
||||
defaultMetadata: { content: "" },
|
||||
minimapColor: "#8b5cf6",
|
||||
// 作为上游输入被消费时输出文本(可连给生成/其它节点);不需要可删
|
||||
resource: (node) => ({ kind: "text", text: node.metadata?.content }),
|
||||
Content: TemplateContent,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@infinite-canvas/plugin-sdk",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user