pm-canvas-viewer
v0.7.0
Published
Local viewer + doc editor for HTML prototypes & pm-canvas boards (Markdown / Excalidraw / draw.io)
Maintainers
Readme
pm-canvas-viewer
本地 HTML 原型 + 配套文档 查看编辑器。一键启动,浏览器操作。
打开任意"含 HTML 文件 + 可选 docs/ 文档"的目录,左侧选项 / 切页,中间预览,右侧 Markdown / Excalidraw / draw.io 三种文档编辑器。完整兼容 pm-canvas skill 生成的画板(含锚点系统)。
安装
npm i -g pm-canvas-viewer装好后全局命令 canvas 即可用(包内已含预构建前端,无需自行构建)。
使用
打开一个文件夹(viewer 不接受单文件路径——它的定位是"HTML + 配套文档"组合):
canvas open ./我的原型/
canvas open # 等价于 canvas open .
canvas # 从历史记录里检索
canvas list # 查看运行中的实例
canvas stop # 停止实例浏览器自动打开。viewer 会扫目录识别"展示项",然后按情况渲染界面。
viewer 识别哪些目录
每个目录独立判断:
| 类型 | 判定 | viewer 内表现 |
|---|---|---|
| 画板项 | 含 canvas.json 且 rows[] 是数组 | 完整画板能力 + 锚点系统 |
| html 项 | 直接含 ≥1 个 .html 且无 canvas.json | 普通预览 + iframe 跳转拦截 |
| 容器目录 | 自身非项,子孙含项 | 在左栏作为分组节点展开 |
| 其他 | 无 html 无 canvas.json | 完全忽略 |
过滤名单:node_modules / .git / dist / docs / .DS_Store / assets。深度上限 8 层。
界面布局
┌────────────┬──────────────────┬────────────┐
│ 左栏 │ 中间画框 │ 右栏 │
│ │ (iframe) │ │
│ 项列表 ▾ │ │ 文档面板 │
│ ──────── │ │ + 编辑器 │
│ 页列表 │ │ │
└────────────┴──────────────────┴────────────┘- 左栏自适应显隐:项数 ≥2 显示项列表(树状);当前 html 项 ≥2 页时显示页列表;两者都无显示时左栏整体隐藏(沉浸预览)
- 页序:
index.html > home.html > A-Z,支持拖动调整并按项记忆 - html 项的跳转策略:同源同项放行 + 同步页列表选中态;外链或跨项拦截 + toast 提示
几种典型场景
# 1. 标准画板(向后兼容,体验和老版本完全一致)
canvas open ./画板目录/
# 2. 多版本画板管理
canvas open ./我的产品/
# ↑ 目录里有 v1/canvas.json、v2/canvas.json,左栏显示项树
# 3. 多页 HTML 原型
canvas open ./用户中心原型/
# ↑ 目录直接含 home.html / profile.html / settings.html,左下显示页列表
# 4. 混合(画板 + 普通 HTML 共存)
canvas open ./prototypes/
# ↑ 含画板子目录 + 散户 html 目录,左栏按物理目录树状展开功能
中间画框
- 画板项:保留 pm-canvas shell 的无限画布、缩放、平移;可挂载锚点
- html 项:iframe 加载选中页,
sandbox="allow-scripts allow-same-origin",onload 注入跳转拦截脚本(同源放行 + URL 变化同步左下页列表;外链拦截 toast)
右栏:文档面板
支持三种文档(都按"当前选中项的 docs/"动态绑定):
Markdown 编辑器
- Tiptap 3 WYSIWYG,工具栏:标题、加粗、斜体、列表、代码、表格
⌘B加粗、⌘I斜体、⌘S保存
Excalidraw 编辑器
- 完整 Excalidraw(流程图、架构图等)
⌘S保存为 .excalidraw
draw.io 编辑器
- 集成 diagrams.net iframe,
⌘S保存为 .drawio
文件管理
- 标签页切换、拖动排序(按项独立记忆)
- 空状态点 + 新建文件,触发
docs/按需创建(不会无 docs 文件就生成空docs/目录)
锚点系统(仅画板项)
- 双击画板创建锚点 pin(自定义颜色 + 标签)
- 锚点可关联到文档文件、或文档内的标题(marker)
- pin ↔ Tiptap chip 双向跳转
- 数据存
docs/anchors.json,按项隔离 - 切到 html 项时彻底卸载(无 iframe DOM 监听残留)
文案就地编辑(v0.5+)
无需翻代码即可改原型/画板里的可见文字,写回源 HTML/JS 文件。
5 类可编辑范围:
| 类别 | 例 | 引入 |
|------|---|------|
| HTML 静态文本 | <p>文字</p> 里的"文字" | v1.0 |
| HTML mixed content | <div>市值<b>14.5万</b>仓位</div> 里的"市值"或"仓位"(每段独立可改) | v1.5 |
| HTML 属性白名单 | <input placeholder=>、<img alt=>、title / aria-label / aria-description | v1.5 |
| JS 字符串字面量 | const STOCK = 'AI芯片'; 里的 'AI芯片'(含单/双引号 + 纯反引号字符串) | v1.5 |
| 模板字符串静态片段 | `hello ${name}` 里的 hello | v1.5 |
入口(任一):按 E 键 / canvas 项 iframe toolbar 「开启文案编辑」 / html 项右上角浮层 toggle。
不可编辑提示:灰色高亮 + hover 200ms tooltip,10 类原因(no-locator / pm-id-not-found / text-mismatch / no-text-child / whitespace-only / computed-text / template-expression / attribute-not-editable / script-or-style-internal / source-position-ambiguous)。
实现要点:
- 后端 Patcher Strategy(HtmlText / HtmlAttr / JsLiteral),用 parse5 + acorn 解析定位 + 字符 offset 切片重写
- JS 字面量首次保存自动插 sentinel 注释(
/*pm-js-id:lit-xxx*/)便于下次精确反查;多处同字面量返source-position-ambiguous - 保存走 fileHash 校验 + 冲突弹窗(覆盖 / 取消)
- 详见
ai/specs/2026-06-03-inline-text-editing-v1.5-design.md
已知限制(v1.5 → 后续):
- JS 字面量改完后 DOM 不自动重算(DOM 是 JS innerHTML 拼出)→ toast 提示「刷新页面看效果」;根治需 skill 模板侧为动态节点注入
data-pm-id - 模板字符串 quasi 不插 sentinel(会破坏模板字符串)→ 同 stringValue quasi 多处时
source-position-ambiguous - 见
ai/ROADMAP.md技术债清单
命令行
canvas open [目录] -p <端口> # 默认 4800
canvas # 历史检索
canvas list # 列运行实例
canvas stop [port|--all] # 停止实例
canvas --version / --help技术栈
| 层 | 技术 | |---|---| | CLI | Commander.js | | 服务端 | Express(ItemRegistry + routeId 协议 + 路径穿越防御) | | 前端框架 | React 18 + Vite | | MD 编辑器 | Tiptap 3 WYSIWYG(@tiptap/markdown) | | 流程图编辑 | @excalidraw/excalidraw + diagrams.net embed | | 构建 | Vite 5 |
安全
- 服务端只接受 scanner 产出的 routeId(sha1 前 8 位),不接受任意路径
- 路径穿越二段防御:filename 层拒绝
\0///../.开头;resolve 后强制落在项目录内 - iframe sandbox(html 项)+ 跳转拦截脚本(拦
<a>/<form>/window.open/history.pushState/ 越界 location)
开发
# 启动开发服务器(前后端同时启动)
npm run dev
# 仅构建前端
npm run build
# 用开发版直接测试
node bin/cli.js open /path/to/dir --port 4800项目结构
pm-canvas-viewer/
├── bin/cli.js ← CLI 入口
├── server/
│ ├── index.js ← Express 服务器 + /item/:routeId/* 静态路由
│ ├── itemRegistry.js ← 单例 routeId → Item 映射
│ └── api/
│ ├── files.js ← docs/ CRUD(routeId 协议 + 按需 mkdir)
│ └── anchors.js ← anchors.json CRUD(仅画板项)
├── lib/
│ ├── scanner.js ← 展示项扫描(画板 / html / 容器 / 过滤)
│ ├── history.js ← 启动过的根目录历史
│ ├── port.js ← 端口管理
│ └── prompts.js ← 终端 TUI(fuzzy 选择)
├── frontend/src/
│ ├── App.jsx ← 主布局 + 导航状态机 + 切项清理
│ ├── apiClient.js ← 统一注入 routeId 的 fetch 封装
│ ├── Sidebar/ ← 左栏(项树 + 页列表)
│ ├── CanvasView/
│ │ ├── index.jsx ← iframe 双模式渲染
│ │ └── iframeInterceptor.js ← html 项跳转拦截脚本生成器
│ ├── DocPanel/ ← 右栏 + 新建文件
│ ├── MdEditor/ ← Tiptap MD
│ ├── ExcalidrawEditor/ ← Excalidraw
│ ├── DrawioEditor/ ← draw.io iframe
│ ├── AnchorSystem/ ← 锚点 overlay + store + dialog
│ └── Toast.jsx ← 拦截反馈
├── dist/ ← 前端构建产物(gitignored,发包时打入)
└── package.json