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

@xnary/iris-common

v0.1.0

Published

Iris 系列(IrisEditor/IrisBoard/IrisExcel)共享内核:文档内核、空间索引、协同 op、纯工具与设计令牌。直出 TS 源码,无构建。

Readme

IrisCommon

Iris 系列(IrisEditor / IrisBoard / 未来 IrisExcel)共享的统一内核与纯工具库。直出 TS 源码,无构建。

内容

  • core/ 统一文档内核:通用 Record(recordId/type/attrs/inline/children/parentId/rev)、CommonDoc(不可变 + 结构共享)、Transaction(变更唯一入口,产出 ChangeSet + 细粒度 Op)、History(before/after 双快照)、ops(Path 统一寻址 + Op 编码 + applyOps 有序回放/幂等/pending 补序)
  • spatial/ 空间索引(均匀网格:queryRect 视口剔除 / queryPoint 命中粗查)+ TileCache(世界磁贴离屏缓存,画布工厂注入)
  • collab/ awareness 光标协议(CursorState 编解码 + CursorStore 聚合)
  • elements/ scene/ 引擎层(自 IrisBoard 下沉):元素体系(spec/registry/shape*/connector/mindmap…)+ 场景模型与渲染核心(renderer/overlays/hit-test/text-edit…)
  • ui/ 通用 DOM 部件(视觉样式由组件自注入,宿主只负责排布):HistoryPanel / AnimPanel / ShapePicker / Dialog

数学公式内核 math/(M8/M9)

KaTeX 是唯一的公式实现(依赖装在 IrisCommon),五个应用共用同一份内核,不存在「预览一种、导出另一种」:

  • render.ts 出口:mathHtmlOf(DOM 出 KaTeX HTML,未加载时先给降级文本 + ensureMathReady 回调重渲)、 measureMath / mathNaturalSize(排版测量,DOM 不可用时按字符宽度估算)、mathImageOf / requestMathImage / ensureMathImage(位图:KaTeX HTML → SVG foreignObject → <img> → PNG, 字体用 font-inline.ts 内联 woff2 base64,避免离屏渲染缺字形)、mathScaleFor(按 zoom×DPR 量化档位, 档位变化才重新栅格化)、mathSvgFragment / mathFallbackText / mathDrawSize;
  • spec.ts:元素类型 MATH_TYPE='math',MATH_ATTRS(tex/display/fontSize/textColor/align/background)、 mathAttrsOf(脏数据收敛)、newMathAttrs / mathDefaultSize(插入时的自然盒)、 mathPlacement(盒内 contain + 对齐)、createMathSpec()(画布 spec,未就绪先画降级文本并请求重绘)、 mathElementSvg(record, zoom)(局部坐标 SVG 片段,Board/PPT 的 SVG 导出共用);
  • atom.ts(scene/atom.ts 的 math 分支):行内公式复用 atom token 的可选载荷 InlineAtomPayload (token.ts),不新增 token 种类——Board 脑图的 atom 行为不受影响;
  • 缓存 cache.ts 按「tex + 显示模式 + 字号 + 颜色 + 缩放档」键,font-inline.ts 只在内联渲染时按需读取。

统一图表内核 charts/(M9)

一份数据契约同时产出画布与 SVG,五个应用(Excel 单元格 / Board 元素 / PPT 元素 / Editor 块 / Base 仪表盘) 外观一致:

  • types.ts:ChartKind = stat | bar | barH | line | area | pie | scatter | radar | map + CHART_KINDS / CHART_KIND_TITLES / CHART_AXIS_KINDS;ChartView(完整绘图视图,含 colorBy: 'series' | 'data' 与 map?: MapAttrs)、ChartSeries、MapAttrs + DEFAULT_MAP_ATTRS、emptyChartView();
  • scene.ts:图元中间层。buildChartScene({view,w,h,measure,palette}) → ChartPrim[] (rect / poly / circle / sector / text / path),paintChartScene(g, prims) 落画布、 chartSceneSvg(prims) 出 SVG——「画布上有的导出里也有」是结构保证。地图走 path 图元 (Path2D / <path>,evenodd 表达内环挖洞);
  • palette.ts / measure.ts:调色板与色阶、默认文本测量(chartMeasurer);
  • geo.ts:GeoJSON 宽容解析(Polygon / MultiPolygon / FeatureCollection)、区域名候选匹配 (REGION_KEY_CANDIDATES)、外接盒自适应、等距圆柱 / 墨卡托投影、按屏幕像素 RDP 抽稀、按尺寸缓存;
  • spec.ts:元素类型 CHART_TYPE='chart'、CHART_ATTRS(含 MAP_KEYS 六个地图键)、 chartViewOfElement / chartAttrsOf(视图 ⇄ attrs 双向收敛)、chartSceneOfElement / chartElementSvg、createChartSpec({paletteOf, defaultKind, atomic})(各应用注入自己的调色板与默认图型);
  • sample-geo.ts:虚构 9 区域示例 GeoJSON(≈1.8KB,含内环演示),供 demo 与单测兜底。

共享图表配置面板 ui/chart-panel(M9)

new ChartPanel({view, onChange, hint?, editableData?}):图型下拉 + 标题 + 数据(类目 textarea、 系列名/数值/系列色/增删)+ 地图区(createMapSection(doc, map, onPatch, onChanged):GeoJSON 导入/清空、 投影、色阶档数、未匹配区底色、区域标签),样式自注入一次(CHART_PANEL_STYLE_ID)。 面板只改本地视图并回调宿主,落库由宿主决定(Board 写元素 attrs、Excel 写单元格值、Editor 写行 attrs); update(view) 供外部同步(宿主比对后再调,避免「面板自己提交 → 重建 → 输入框掉焦点」)。

提及内核 mention/ + 画布胶囊(M11)

统一口径:一个提及就是一段带实体的「块」,文本形态恒为「@显示名」(tokenText(mention)), 所以显示、测量、导出、比较都只有一处真相;画布/网页/导出都把这段文本画成胶囊。

  • token.ts:MentionEntity { type, id, label, meta? }、mention token、mentionDisplayText;
  • mention/style.ts:五类色板 MENTION_TYPES = user | doc | date | record | custom(未知类型落 custom 灰)、 mentionStyleOf / MENTION_TYPE_TITLES / mentionKey / filterMentions; doc 候选行图形按 meta.docType 取与宿主目录树逐一对应的图标/颜色(MENTION_DOC_GLYPHS / mentionDocGlyphOf:icon-park outline 原始 path + 目录树 docKindMeta 类型色,裸图标无底色芯片; 缺省/未知 docType 兜底目录树缺省项 doc-detail/#3370ff),createMentionGlyph 的 doc 分支消费;
  • mention/trigger.ts:mentionTriggerAt(text, caret)(@ 触发位 + 查询串)、isBareMentionTrigger;
  • mention/sample.ts:SAMPLE_MENTIONS / sampleMentionProvider(虚构示例候选,demo 共用);
  • scene/mention-draw.ts:mentionChipWidth/Height/PadX、mentionChipText、paintMentionChip (画布胶囊的唯一实现:圆角底 + 类型色文本,宽度与排版同源);
  • scene/text-draw.ts / text-layout.ts:排版与绘制都把提及当不可拆单元(宽度 = 胶囊宽);
  • scene/atom-dom.ts / text-merge.ts:浮层编辑把提及渲染成 contenteditable=false 胶囊, 提交时按段序列回写 mention token(不会被退化成纯文本);
  • scene/atom-dom.ts 的触发扫描:mentionTriggerProbe / mentionTriggerRanges 只看「光标所在的 连续文本段」(胶囊 / atom / <br> 都是边界),并把触发区间同时换算到 editorPlainText 空间 ——胶囊自己的 @ 不算触发符(否则插入胶囊后紧跟的 keyup/input 同步会把候选菜单又弹出来, 菜单随后吞掉回车,宿主再也提交不了;这是 M11 浏览器走查抓到的真 bug,五个宿主同一口径);
  • mention/caret-anchor.ts:caretLineAnchor(编辑框, 挂载容器) —— 光标行盒 → 容器坐标的唯一换算口径 (取 Range 的行矩形减容器矩形;拿不到选区/无布局环境回落编辑框),lineHeightOf 附带行高兜底; 四个宿主此前各写一套(电子表格漏减容器偏移、多维表格用输入框整框、画布浮层用行盒、演示文稿写死坐标), 口径不一正是「弹窗离光标很远 / 压住光标行」的根因;
  • ui/mention-menu.ts:MentionMenu(上下键/回车/Tab/Esc/空结果文案/边界翻转定位)+ MentionProvider 类型(契约 =「查询串 → 过滤后候选」,菜单不做二次子串过滤, 远程/模糊匹配结果才留得住;分类 tab 以 provider('') 全量归纳,输入关键字时 tab 不塌缩), 五个应用共用同一份菜单; 定位契约(M11 修正):
    • 锚点 = 光标所在行盒(x/y 左缘/上缘 + h 行高),坐标一律相对菜单挂载的那个容器;
    • 优先落在行盒下方 y + h + 4,下方空间不够才翻到上方 y - 菜单高 - 4(翻上去不再重复减行高);
    • 尺寸用实测矩形,测不到按候选条数估算(不再用固定 60 兜底);左右与上下都钳进容器可视范围;
    • 容器比菜单矮时先收住 max-height(宁可滚动,也不越出可视区被裁);
    • 传 anchorSource 后菜单自己订阅「窗口改大小 / 容器滚动 / 容器尺寸变化」并逐帧节流重定位, 宿主只提供「当前锚点怎么取」;样式自带 pointer-events: auto(宿主浮层多为 pointer-events:none)。

各应用接法

| 应用 | 注入 | 触发点 | 提交通道 | | --- | --- | --- | --- | | IrisEditor | EditorOptions.mentionProvider / editor.mentionProvider | 正文输入 @、/ 菜单 | insertTokens(trigger, [mentionToken]) | | IrisExcel | ExcelOptions.mentionProvider / setMentionProvider | 单元格编辑器输入 @、工具条「插入 → 提及…」 | setCellInline(tokens) | | IrisBoard | BoardOptions.mentionProvider / setMentionProvider | 文本浮层输入 @、右键「插入提及…」 | commitTextEditSegments | | IrisPPT | PPTOptions.mentionProvider / setMentionProvider | 文本浮层输入 @、插入菜单「提及」 | commitTextEditSegments | | IrisBase | BaseOptions.mentionProvider / setMentionProvider | 文本格编辑器输入 @ | setCellValue(tokens) |

菜单挂载容器与锚点由各宿主给出(同一契约):Editor 用 editor.renderer.root(斜杠/提及菜单是它自己的实现)、 Excel 用 .ir-excel__overlay、Base 用 .ir-base__overlay、Board/PPT 用 board.container / ppt.container; PPT 的插入菜单锚点在容器坐标系内自算(选中元素 → 幻灯片左上角),不从工具条按钮取(按钮在容器之外)。

已知限制(提及,M11)

  • 胶囊只承载「实体引用」,不支持加粗/字号等 marks 编辑;提及文本不可局部编辑(要么整体删,要么整段替换);
  • 导出分流:Markdown/HTML/CSV/SVG 是纯文本「@显示名」(HTML 与 SVG 额外带胶囊样式), PPTX 与 DOCX 一律纯文本(没有胶囊样式通道,宁可可选中可搜索);PDF/图片走整页位图,胶囊随画布;
  • 候选数据源由宿主注入,内核不做「@所有人」「按部门过滤」这类业务规则;
  • 触发扫描以「光标所在的连续文本段」为界:光标紧贴胶囊之后时不会因为胶囊的 @显示名 而弹菜单 (想再提一个实体,先敲空格或直接在胶囊后面输入 @);
  • 提及不参与协同合并的语义校验:并发改动同一段时按字符级三方合并,提及块可能被移动但仍保持实体;
  • 菜单挂载容器必须是能覆盖可视区的定位祖先:容器的 clientHeight 同时是钳制边界,挂到一条窄浮层上 (例如把菜单挂进只有几十像素高的 overlay)会被钳成很小的一条并把触发行盖住。

公式/图表内核的已知限制

  • 公式:位图是栅格化产物(按 tex + 显示模式 + 字号 + 颜色 + 缩放档缓存),画布与导出里都不可选、 不可搜索;docx 侧没有 OMML(是等宽 LaTeX 源串);行内公式在 Markdown/IR 里是 $tex$ 源串。 位图需要 canvas + 字体度量(node/jsdom 判为「不支持栅格化」,一切请求立刻走降级文本)。
  • 图表:图元层是「绘图」而非「图表对象」——没有悬浮提示、没有点击钻取、没有动画; 数值轴按 nice ticks 取整,多系列同点重叠的数据标签不做避让。
  • 地图:只画 GeoJSON 轮廓 + 分级设色(+ 可选区域标签与色阶图例);没有瓦片底图、没有投影之外的 几何纠正(跨 180° 经线不特判)、没有交互;区域数据靠「labels + 单系列 values」, 名称匹配不到就落 unmatchedFill;sample-geo.ts 是虚构区划,只为演示与测试兜底。

通用居中弹窗 ui/dialog(P8)

createDialog(opts?) → DialogHandle。零领域依赖(只操作 DOM 与宿主给的标题字符串,不引用任何编辑器概念), Board / Excel / Editor / PPT 任一消费方都能直接用,不需要 Host 或回调注入。

interface DialogOptions {
  title?: string;              // 标题栏文案(setTitle 可改)
  width?: number | string;     // 卡片宽度(数字 = px;实际取 min(该值, 100%))
  maxHeight?: number | string; // 卡片最大高度(实际取 min(100%, 该值))
  closeOnMask?: boolean;       // 点遮罩是否关闭(缺省 true)
  initialFocus?: 'first' | 'card' | 'none'; // 打开时焦点落到哪(缺省 'first')
  onClose?: () => void;        // ✕ / 遮罩 / Esc / close() 同口;先于焦点归还执行
}
interface DialogHandle {
  readonly el: HTMLElement;    // 根(遮罩 + 卡片;hidden = 关闭)
  readonly body: HTMLElement;  // 内容容器:宿主把面板挂进来
  readonly isOpen: boolean;
  setTitle(t: string): void;
  open(): void; close(): void; toggle(): void; destroy(): void;
}
  • 形态:position: fixed 遮罩(rgba(12,14,18,.38))+ 居中圆角卡片(标题栏 + ✕);padding: 24px 的 flex 居中容器 + width: min(--ir-dialog-w, 100%) / max-height: min(100%, …) 保证窗口变化不溢出, 内容超高由 body 滚动。类名 ir-dialog__*,样式由 ensureDialogStyles() 幂等自注入一次(#ir-dialog-styles)。
  • Esc:window 捕获阶段监听 + stopPropagation,只关最上层那一个(支持多弹窗堆叠), 从而抢占宿主既有的按键分层(画布 Esc 分层 / 快捷键不会被顺手触发);无弹窗在开时不占用全局按键。
  • 堆叠:z-index 按打开顺序递增(基数 100 + 栈内下标,关闭后重排),上层必然更高。
  • 焦点:打开时记录 document.activeElement,并试聚焦正文内首个可聚焦元素(浏览器会拒绝 display:none 子树里的元素,故逐个试到真拿到焦点为止),都不行才落到卡片自身(tabindex="-1"); 关闭时把焦点还给打开前的元素(已从文档移除则不动)。onClose 先于焦点归还执行,宿主在回调里同步 自身态或顺手 focus 别处都不会抢走焦点归还。initialFocus: 'none' 供「聚焦会改变既有语义」的宿主 (如 PPT 备注面板:textarea 聚焦后文档变更不再回读进输入框)。
  • [hidden] 兜底(必须):组件自身写了 display: flex,故显式声明 .ir-dialog[hidden] { display: none !important }; 漏掉会出现「已 hidden 却仍盖住整页且关不掉」(本仓历史上踩过这个坑),单测锁住这条规则的存在。
  • AnimPanel 的弹窗形态:AnimPanelOptions.dialog(true 或 DialogOptions)开时面板自带弹窗 (el 仍挂进 dialog.body,默认关闭,setVisible/el.hidden 语义不变);缺省关着,宿主自行排布 (IrisBoard 的贴边面板走这条,行为零变化)。

图片模型与形状面板(P3b)

  • image attrs 扩展(elements/image.ts):clip(归一化源坐标 {x,y,w,h} ∈ [0,1],默认整图;脏值逐字段夹取、退化回整图)/ cornerRadius / stroke + strokeWidth + dash(盒内框描边,线宽内缩一半保证不越界)。
  • imagePlacement(geom, fit, natural, clip):源矩形 → 目标矩形(画布 / SVG / PPTX 的同源出口)。映射规则 = 裁剪先发生,fit 的比例判据取「裁剪后的有效自然尺寸」(nw·clip.w, nh·clip.h):
    • fill:有效源等比拉伸铺满 geom 盒(dst = 盒),无目标裁切;
    • contain:按有效源等比缩放居中(backdrop = true,留白由调用方铺底色);
    • cover:按有效源等比放大居中 + clip = geom 盒;
    • 源是否被裁(cropped)决定 draw 用 9 参还是 5 参 drawImage(整图时与旧实现逐调用一致);
    • natural 缺失/非法 → src = null、dst = geom 盒(按 fit 兜底)。 旧函数 imageDrawBox 保留为「无源裁剪的盒折算」内部件(既有单测锚定其行为)。
  • ui/shape-picker.ts(ShapePicker):弹出式形状选择面板(分类标签 + 网格),ShapePickerHost{ kinds, onPick, onClose? } + shapeKindList(registry, {labels, categoryLabels}) 构造清单;图标零维护——格子用 kind 自身 draw 实时渲到小 canvas(drawShapeKindIcon),新增 kind 无需登记图标资源。关闭语义:选中即回调并关闭 / Esc / 点击面板外(均回调 onClose),Esc 用捕获阶段 + stopPropagation 免得穿透到画布的分层 Esc。

文本排版(P3a:段落级排版 + 自动调整 + 垂直对齐)

  • 数据表示:段落级属性(行距倍数 lineHeightMul、段前/段后 spaceBefore/spaceAfter、首行缩进 indent、 列表 listType/listLevel/orderedStart/bulletChar)以元素级 attrs 为「全框统一默认」; 单段覆盖写在该段每个 token 的 list mark 上,值 <type>[:<level>[:<start>]](scene/text-layout.ts)。 选它的理由:纯文本编辑提交走位置式 marks 继承(mergeTokensWithOldMarks),故「新增段落继承前一段落的 列表样式、删除段落不影响其余编号」无需提交路径特判,且段落样式与字符同住 inline(序列化/协同/撤销零新字段)。 编号在排版期按段序计算(paragraphNumbers:同级连续、更浅级别重置更深计数),PPTX 导出复用同一函数写 a:buAutoNum@startAt。
  • 段间距语义:段 i 与 i+1 之间 = spaceAfter + spaceBefore(相加,PowerPoint 语义);首段之前不加段前, 末段之后计入排版高度。列表段正文列左缩进 = (level+1) × 1.5 × fontSize(选定 1.5em:随字号等比), 标记悬挂在正文列左侧(软折行与首行同列 = 悬挂缩进);标记不计入 lineWidths,绘制信息在 layout.markers。
  • 一致口径:画布绘制、编辑 overlay、远程光标定位三处都用 layout.lineTops / lineHeights / layoutOffsetX/Y(不再按 i × lineHeight 推算),scene/text-draw-golden.test.ts 用录制型 ctx 固化 「默认样式下绘制调用序列」以守卫零回归。
  • 文本自动调整(scene/text-fit.ts,attrs.autoFit === 'shrink'):按 盒高 / 当前排版高 等比缩小字号后重排, 最多 3 次迭代,下限 0.4 × 原字号;未启用/无盒高(attrs.h ≤ 0)/原字号装得下 → 原样返回。 溢出策略:已到下限仍装不下时不静默截断,画布与 SVG 都在盒右下角画同色同尺寸的小三角 (drawOverflowHint / overflowHintSvg,OVERFLOW_HINT_SIZE/COLOR),提示用户改盒高或删字。
  • text 元素盒高:attrs.h > 0 = 固定盒高(valign 与 autoFit 生效,bounds 用盒高);h ≤ 0/未设置 = 自适应高度(忽略 valign,bounds = max(排版高, 20),与 P3a 之前一致)。h 未注册默认值:geomOf 对 缺省 h 兜底 100(旋转轴沿用旧口径),注册 0 会覆盖它。
  • 已知限制:编辑 overlay 是扁平 \n 文本(保证 textContent 提交与 IrisBoard 共用路径不回归), 故不镜像段落缩进/列表标记/段前段后间距/行内 atom 抬高的逐行差异——多段或列表文本在编辑态下看不到标记与缩进; 垂直口径(行高 × zoom、valign 的 paddingTop)与画布一致。
  • ui/ 通用 DOM 组件(视觉样式由组件自注入,宿主 CSS 只负责排布):
    • HistoryPanel(undo/redo 时间线面板,宿主只需 history.timeline() / jumpHistory() / on('change'))
    • AnimPanel(动画时间轴面板:按触发三段分组「单击时/与上一动画同时/上一动画之后」+ 内联编辑九项 + 效果下拉按 EffectDef.group 归入 <optgroup>、选中项呈现效果参数(EffectDef.params → spec.params) + pointer 拖拽排序(跨组改 trigger)+ 动画刷 + 预览;只依赖 host 的 getAnimations/setAnimations/animScopeRecords/selection/on/previewAnimations,排序真相始终是宿主文档;形态可选:{ dialog: true } = 自带居中弹窗,缺省 = 宿主排布)
    • Dialog(createDialog:通用居中弹窗——遮罩 + 卡片 + 标题栏 + ✕ + Esc 只关最上层 + 焦点移入/归还 + 堆叠 z-index + 尺寸自适应 + 样式自注入;零领域依赖,见上一节)
  • anim/ 动画内核(宿主无关,Board/PPT 同权复用):效果 = 绘制期变换(ElementPaint,不改 attrs)→ effects(效果库:30 个效果 = 进入 10 / 强调 8 / 退出 8 / 路径 4,含 group 分组与 params 可调参数元数据;方向语义与类别不变量见文件头注释)/ easings / timeline(buildAnimSteps 分步与 describeAnim)/ player(可注入时钟的逐帧播放器;path 类效果经 EffectContext.path 透传 AnimSpec.path)/ frame-store(帧仓 + 变更通知)/ types(AnimSpec、AnimHost)。渲染侧经 RenderState.paints 通用补丁通道生效:补丁记录的键集并入 excluded,不参与静态瓦片烘焙,由动态层实时绘制
  • 纯工具:ids(全局唯一 id)、token(字符级 InlineToken + marks)、keymap(parseKeymap/matchKey)、emitter、geometry、throttle
  • styles/tokens.css:--ir-* 设计令牌

画布字体串:必须是字面量族(DEFAULT_FONT_FAMILY)

ctx.font 是 canvas 的解析属性,不接受 CSS 变量:g.font = '400 24px var(--ir-font)' 会被浏览器 静默忽略,g.font 保持上下文残留值(典型 10px sans-serif),于是 measureText 与字形都按残留字体走。 后果是未显式设族(fontFamily: '',「插入文本」的默认值)的文本按错误的字号/字体渲染与测量,且结果取决于 「上一个元素碰巧用了什么字体」(实测:24px 四字文本,正确族墨迹 95×23px,残留 10px sans-serif 时仅 40×10px)。 同一字符串在 DOM overlay(scene/text-edit.ts 的 font:)里合法,所以只有画布路径坏——现在两条路径同源同值。

  • elements/spec.ts 导出 DEFAULT_FONT_FAMILY = -apple-system, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif, 逐字镜像 styles/tokens.css 的 --ir-font;spec.test.ts 用 ?raw 读 tokens.css 断言两者一致, 改令牌值必须同步本常量(否则测试红)。
  • textStyleOf 的空/无效族、fontOf 以及 scene/text-draw.ts 的 token 族(含 code mark 的 MONO_FONT_FAMILY, 本就是字面量)一律经 sanitizeFontFamily:空串、含 var( 的历史/外来值(旧文档里存过 var(--ir-font))、 裸 --x 变量名 → 替换为 DEFAULT_FONT_FAMILY;带引号/逗号的显式族栈原样保留(不二次加引号)。
  • 行为变化(如实):文本元素的字体族不再跟随宿主对 --ir-font 的覆盖——画布、编辑 overlay、SVG 导出三处 现在都固定为字面量栈。此前画布侧是「碰巧继承上一个元素字体」的不确定状态,overlay 侧跟随宿主;宿主(含 demo 的 demo.css)对 --ir-font 的覆盖今后只影响 DOM 文本观感。宿主若要改画布文本字体,请写入文本元素的 fontFamily attrs,或同时修改 styles/tokens.css 与本常量。

依赖接线(重要)

消费方以 link: 协议引用(本包改动即时生效):

pnpm add link:../IrisCommon

消费方 Vite 需要两处配置(否则预打包缓存陈旧源码 / dev server 拒绝 root 外文件):

optimizeDeps: { exclude: ['@xnary/iris-common'] },
server: { fs: { allow: ['.', '../IrisCommon'] } },

Vitest 无需额外配置(linked 包真实路径在 node_modules 之外,默认现场转译)。

开发

pnpm install
pnpm test        # node 环境纯逻辑单测(DOM 用例逐文件 jsdom 覆盖)
pnpm typecheck

准入纪律

只放纯函数、零领域概念的代码(elements/ scene/ ui/ 三个下沉层例外:随界面包一起搬入的引擎/组件代码原样搬移不改逻辑);迁入代码配单测守卫,需要 DOM 的用例首行标 // @vitest-environment jsdom。