npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@holo-max-lab/excalidraw-ai

v0.18.0-ai.9

Published

Excalidraw as a React component

Readme

@holo-max-lab/excalidraw-ai

Excalidraw(上游 master,@excalidraw/excalidraw 0.18.0 基线)的单包化 fork,叠加教学实施平台内部补丁,并内置「白板适配层 + 持久化契约」。 产物为 ESM + 类型声明 + 样式 + 全部手绘字体;React 17 / 18 / 19 均可作 peer。

版本语义

0.18.0-ai.<N>:0.18.0 = 上游基线版本,ai.<N> = 内部补丁轮次。 prerelease 的 caret 语义反直觉,消费方请钉死精确版本("@holo-max-lab/excalidraw-ai": "0.18.0-ai.9"),不要用 ^。 补丁清单与 rebase 流程见仓库根 PATCHES.md。

安装

npm i @holo-max-lab/excalidraw-ai --save-exact

运行时依赖(roughjs / jotai / codemirror / radix-ui / perfect-freehand / pica / mermaid-to-excalidraw 等 23 个)随包自动安装; react / react-dom 为 peerDependencies,由宿主提供(务必保证全应用单实例)。

快速开始(裸 Excalidraw)

import { Excalidraw } from "@holo-max-lab/excalidraw-ai";
import "@holo-max-lab/excalidraw-ai/index.css";

export const Board = () => <Excalidraw langCode="zh-CN" />;

字体(必做,否则中文手写体 404 降级为系统字体)

按需字体(Xiaolai / Excalifont 等)以相对路径 fonts/… 引用;未设置基址时上游会回落到 esm.sh CDN(本包未在该 CDN 存在,必 404)。 设置基址并把字体拷到该路径下:

// 模块顶层,先于任何 Excalidraw 渲染
window.EXCALIDRAW_ASSET_PATH = "/excalidraw-assets/";

Vite 宿主示例(幂等拷贝):

// vite.config.ts
import fs from "fs";
import path from "path";
{
  name: "copy-excalidraw-fonts",
  buildStart() {
    const src = path.resolve(__dirname, "node_modules/@holo-max-lab/excalidraw-ai/dist/prod/fonts");
    const dest = path.resolve(__dirname, "public/excalidraw-assets/fonts");
    if (fs.existsSync(dest)) return;
    fs.mkdirSync(dest, { recursive: true });
    fs.cpSync(src, dest, { recursive: true });
  },
}

webpack 宿主用 copy-webpack-plugin 做等价事;public/excalidraw-assets/ 应进 .gitignore。

白板适配层(推荐入口)

Whiteboard 组件 = Excalidraw + 去品牌主菜单 + 持久化接线;BoardStorage 是持久化契约接口(包内不含任何实现与兜底)。 你只需实现一个 BoardStorage,或直接用下面的 createHttpBoardStorage 工厂。

import { Whiteboard, type BoardStorage, BoardVersionConflictError } from "@holo-max-lab/excalidraw-ai";

const storage: BoardStorage = {
  async load(bizType, bizId) { /* GET 你的后端 → BoardDocumentVO | null */ },
  async save(bizType, bizId, payload) { /* PUT;版本冲突请抛 BoardVersionConflictError */ },
  async freeze(bizType, bizId) { /* 业务提交/冻结时调用 */ },
  async unfreeze(bizType, bizId) { /* 业务退回/返修时调用 */ },
  async listHistory(bizType, bizId) { /* 可选 */ },
  async uploadImage(blob, fileName) { /* 上传对象存储,返回可同源加载的 url */ },
};

<Whiteboard
  storage={storage}
  bizType="teaching_whiteboard"
  bizId={id}
  onPersistState={setState}      // idle/loading/saving/saved/conflict/error
  onNotify={(kind, text) => toast[kind](text)}   // 包内不依赖任何 UI 库
/>

关键 Props 与 ref

| prop | 说明 | |---|---| | storage / bizType / bizId | 三者齐备时开启持久化(initialData 改从 storage.load 读,onChange 防抖写回) | | viewMode | 只读模式(查看 / 组员 / 冻结态):透传 Excalidraw viewModeEnabled 且关闭持久化 | | onPersistState | 持久化状态回调,供宿主展示「已保存 / 保存中 / 版本冲突…」 | | onNotify | error / warning / info 通知,宿主决定用 toast 还是别的 | | mainMenu | 自定义主菜单;不传则用去品牌默认菜单 | | formFactor | 强制 "phone" / "tablet" / "desktop"。Excalidraw 默认按容器尺寸判定版式(宽 ≤599,或 宽<1000 且高<500 → phone);白板嵌在弹窗等小容器里会切成移动端版式(工具栏置底、主菜单弹层形态变化),传 "desktop" 可强制桌面版式 | | initialData / onChange / theme | 透传 Excalidraw 同名语义(未开持久化时有效) |

ref 暴露 WhiteboardHandle:

const ref = useRef<WhiteboardHandle>(null);
ref.current?.getApi();     // 原生 ExcalidrawImperativeAPI 逃生舱口(updateScene / addFiles / zoomToFit …)
ref.current?.saveNow();    // 立即落库(跳过 800ms 防抖),用于「提交前先保存」

createHttpBoardStorage:HTTP 后端一行装配(推荐)

包内已内置一个通用 HTTP 实现工厂,把「409 → BoardVersionConflictError、上传、代理改写」收敛一处,宿主只注入 transport 与地址。

import {
  createHttpBoardStorage,
  type BoardHttpTransport,
} from "@holo-max-lab/excalidraw-ai";

// 1) 把你自己的 request 封装适配成 BoardHttpTransport(axios 示例)
const transport: BoardHttpTransport = {
  async json(method, url, body) {
    try {
      const res = await axios.request({ method, url, data: body });
      return { code: res.data.code, msg: res.data.msg, data: res.data.data };
    } catch (e) {
      return { code: e?.response?.status === 409 ? 409 : 1, msg: e?.message, data: undefined };
    }
  },
  async upload(url, blob, fileName) {
    const fd = new FormData();
    fd.append("file", blob, fileName || `board-${Date.now()}.png`);
    const res = await axios.post(url, fd); // 注意自己设置 multipart 头
    return { code: res.data.code, msg: res.data.msg, data: res.data.data };
  },
};

// 2) 一行产出 BoardStorage
export const boardStorage = createHttpBoardStorage({
  transport,
  baseUrl: "/teaching/board/documents",   // 文档接口前缀
  uploadUrl: "/admin/sys-file/upload",    // 图片上传接口
  rewriteUrl: (url) => toOssProxyUrl(url), // OSS 无 CORS 时改写为同源代理地址(如 /ossimg)
});

createHttpBoardStorage 会自动组装以下后端契约(与文档接口 {bizType}/{bizId} 对齐):

| 方法 | HTTP | |---|---| | load | GET {baseUrl}/{bizType}/{bizId} → BoardDocumentVO \| null | | save | PUT {baseUrl}/{bizType}/{bizId}(body {docJson, version})→ {version};响应 code===409 抛 BoardVersionConflictError | | freeze | POST {baseUrl}/{bizType}/{bizId}/freeze | | unfreeze | POST {baseUrl}/{bizType}/{bizId}/unfreeze | | listHistory | GET {baseUrl}/{bizType}/{bizId}/history → BoardHistoryVO[] | | uploadImage | POST {uploadUrl}(multipart)→ {url},再经 rewriteUrl 改写 |

transport 的 json/upload 返回宿主统一响应体 {code,msg,data}(code===0 成功,code===409 版本冲突)。

renderThumbnailBlob:提交/冻结时派生缩略图

场景 JSON 是唯一真源,不存储整图预览;仅在「提交 / 冻结」那一刻按需派生一张缩略图(列表降级展示用)。

import { renderThumbnailBlob } from "@holo-max-lab/excalidraw-ai";

// scene = { elements, files, appState }(files 的 dataURL 必须是可同源加载的代理地址)
const blob = await renderThumbnailBlob(scene);   // PNG Blob | null
const url = await storage.uploadImage(blob!);    // 再上传回对象存储

注意:files 的 dataURL 必须可同源加载(OSS 无 CORS 时用代理地址),否则 canvas 被污染、exportToBlob 抛 SecurityError。

存储契约要点

  • 画布上的一切 = 一份 JSON 真源:{ elements, files, appState(白名单) };读取后必须过 restoreElements 归一化
  • 图片像素不进 JSON:保存前把内存 base64 经 uploadImage 换 url,files 只留 {id,mimeType,size,url};url 必须可同源加载(OSS 无 CORS 时走反代),否则 canvas 被污染、导出失败
  • 保存策略已内置:onChange 防抖 800ms;客户端内容哈希短路(瞬态 appState 不产生请求);409 自动 rebase(拉最新版 updateScene + addFiles 收敛)
  • 不派生预览图:消费方直接渲染场景;如需光栅用 exportToBlob / renderThumbnailBlob 自行导出
  • 共享文档(多人写同一 bizType+bizId)请让后端做乐观锁并在冲突时返回可识别信号,宿主实现里抛 BoardVersionConflictError

已包含的内部补丁(详见 PATCHES.md)

  1. 单包化构建(内部 workspace 包内联)
  2. 包名/版本/依赖收敛
  3. 工具栏提示统一为自定义 Tooltip(移除原生 title 白框)
  4. 字体 esm.sh 回退条件化(设了 EXCALIDRAW_ASSET_PATH 就不再回落到 CDN)
  5. adapter/ 适配层与持久化契约(Whiteboard / BoardStorage / createHttpBoardStorage / renderThumbnailBlob)

去品牌:默认主菜单不含上游 Socials 品牌块;如需完全自定义传 mainMenu prop。

已知约束

  • React Native 原生渲染不可用(DOM/canvas 库);Taro/uni 等请走 H5 目标或 WebView 承载
  • webpack 宿主需确认 woff2 / css 的 loader 配置(Vite 开箱即用)
  • 以 file:/junction 方式本地引入时:需 preserveSymlinks(tsconfig 与 bundler 双处),否则解析到 fork 仓库自己的 react/@types 造成双实例;改 fork 后需清 bundler 预打包缓存
  • 包体约 32MB(含 dev+prod 双份产物与 468 个字体子集),请路由级懒加载,勿进首屏

本地开发(fork 仓库)

corepack yarn install        # fork 用 Yarn 1 workspaces,corepack 取 yarn 1.22.22
corepack yarn build:excalidraw   # 只重建主包
corepack yarn build:packages     # 全链重建

发布 runbook(维护者)

  • scope 必须是已存在的 org:向不存在的 scope 发新包,npm 返回误导性的 404(探针包同因)
  • 用 Classic Automation token + npm publish --userconfig <含 token 的 rc>;网页登录会话 token 发布需 TOTP,recovery code 不能当 OTP(报 EOTP)
  • 连续失败发布/无效 OTP 会触发 npm 账号风控冻结,失败后停手排查,勿重试轰炸