paintress 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -14
- package/README.zh-CN.md +29 -13
- package/dist/cli.js +467 -83
- package/dist/index.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/{render-CzpO1cOe.js → render-BSf62388.js} +268 -96
- package/dist/server-DZRNNWac.js +1599 -0
- package/package.json +1 -1
- package/skills/paintress/SKILL.md +45 -17
- package/ui/dist/assets/index-BFcvu7wr.css +1 -0
- package/ui/dist/assets/index-CEhK1JXE.js +8 -0
- package/ui/dist/assets/marked.esm-DZCVybOJ.js +72 -0
- package/ui/dist/index.html +2 -2
- package/dist/server-vPylVJZf.js +0 -854
- package/ui/dist/assets/index-Dyrrkh92.css +0 -1
- package/ui/dist/assets/index-kPX0SDa0.js +0 -8
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { It as Affine, it as BackgroundBlurEffect, bt as BlendMode, at as BlurEffect, Lt as BoolOrVar, z as BrowserSchema, Rt as Color, zt as ColorOrVar, Bt as CornerRadius, xt as CrossAxis, j as DocumentObjectSchema, M as DocumentSchema, ot as EffectEntry, st as Effects, B as EllipseSchema, w as FileChangedError, ct as FillEntry, St as FillRule, lt as Fills, V as FrameSchema, ut as GradientFill, Ct as GradientType, dt as GraphicsBlock, H as GroupSchema, U as IconFontSchema, wt as IconLibrary, W as IconSchema, ft as ImageFill, Tt as ImageFit, G as KNOWN_NODE_TYPES, K as LayoutBlock, Et as LayoutDir, Dt as MainAxis, pt as MeshGradientFill, q as NodeBase, J as NodeSchema, Y as NoteSchema, Vt as NumOrVar, N as PEN_DOC_VERSION, P as PTR_SPEC_VERSION, Ht as Padding, X as PathSchema, Z as PolygonSchema, Ot as PositionMode, F as PtrDocumentSchema, Q as RectangleSchema, $ as RefSchema, et as ScriptSchema, mt as ShaderEffect, ht as ShaderFill, gt as ShadowEffect, kt as ShadowKind, Ut as Sizing, _t as SolidFill, Wt as StringOrVar, At as StrokeAlign, vt as StrokeBlock, jt as StrokeCap, Mt as StrokeJoin, yt as StrokeWidth, Nt as TextAlign, Pt as TextAlignVertical, Ft as TextGrowth, tt as TextSchema, nt as UnknownNodeSchema, Gt as VARIABLE_NAME, I as VariableDef, Kt as VariableRef, L as XPaintress, R as X_PAINTRESS_KEY, g as batch, n as boardBoxes, _ as buildIndex, k as collectIds, c as commonToCss, r as computeCanvas, l as cornerRadiusToCss, u as effectsToCss, i as escapeAttr, a as escapeHtml, m as expandRefs, d as fillsToCss, A as genId, v as get, s as getLucideIcon, y as insert, T as loadDoc, h as makeVarResolver, b as move, x as query, S as remove, t as renderDocument, o as renderTree, E as saveDoc, rt as schemaForType, D as serializeDoc, f as strokeToCss, p as textToCss, C as update, O as validateDoc } from "./render-
|
|
1
|
+
export { It as Affine, it as BackgroundBlurEffect, bt as BlendMode, at as BlurEffect, Lt as BoolOrVar, z as BrowserSchema, Rt as Color, zt as ColorOrVar, Bt as CornerRadius, xt as CrossAxis, j as DocumentObjectSchema, M as DocumentSchema, ot as EffectEntry, st as Effects, B as EllipseSchema, w as FileChangedError, ct as FillEntry, St as FillRule, lt as Fills, V as FrameSchema, ut as GradientFill, Ct as GradientType, dt as GraphicsBlock, H as GroupSchema, U as IconFontSchema, wt as IconLibrary, W as IconSchema, ft as ImageFill, Tt as ImageFit, G as KNOWN_NODE_TYPES, K as LayoutBlock, Et as LayoutDir, Dt as MainAxis, pt as MeshGradientFill, q as NodeBase, J as NodeSchema, Y as NoteSchema, Vt as NumOrVar, N as PEN_DOC_VERSION, P as PTR_SPEC_VERSION, Ht as Padding, X as PathSchema, Z as PolygonSchema, Ot as PositionMode, F as PtrDocumentSchema, Q as RectangleSchema, $ as RefSchema, et as ScriptSchema, mt as ShaderEffect, ht as ShaderFill, gt as ShadowEffect, kt as ShadowKind, Ut as Sizing, _t as SolidFill, Wt as StringOrVar, At as StrokeAlign, vt as StrokeBlock, jt as StrokeCap, Mt as StrokeJoin, yt as StrokeWidth, Nt as TextAlign, Pt as TextAlignVertical, Ft as TextGrowth, tt as TextSchema, nt as UnknownNodeSchema, Gt as VARIABLE_NAME, I as VariableDef, Kt as VariableRef, L as XPaintress, R as X_PAINTRESS_KEY, g as batch, n as boardBoxes, _ as buildIndex, k as collectIds, c as commonToCss, r as computeCanvas, l as cornerRadiusToCss, u as effectsToCss, i as escapeAttr, a as escapeHtml, m as expandRefs, d as fillsToCss, A as genId, v as get, s as getLucideIcon, y as insert, T as loadDoc, h as makeVarResolver, b as move, x as query, S as remove, t as renderDocument, o as renderTree, E as saveDoc, rt as schemaForType, D as serializeDoc, f as strokeToCss, p as textToCss, C as update, O as validateDoc } from "./render-BSf62388.js";
|
package/dist/mcp/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as createMcpServer, n as runStdioServer } from "../server-
|
|
1
|
+
export { t as createMcpServer, n as runStdioServer } from "../server-DZRNNWac.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { copyFileSync, linkSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { z } from "zod";
|
|
5
4
|
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
// src/shared/version.ts
|
|
7
7
|
/**
|
|
8
8
|
* 包版本:运行时从 package.json 读取,避免在多处硬编码后忘记同步。
|
|
@@ -597,7 +597,7 @@ function collectIds(children, out = []) {
|
|
|
597
597
|
}
|
|
598
598
|
// src/doc/validate.ts
|
|
599
599
|
/** 结构校验(schema)+ 语义校验(重复 id、悬空 ref、未知类型、未解析变量) */
|
|
600
|
-
function validateDoc(input) {
|
|
600
|
+
function validateDoc(input, opts = {}) {
|
|
601
601
|
const errors = [];
|
|
602
602
|
const warnings = [];
|
|
603
603
|
const parsed = DocumentSchema.safeParse(input);
|
|
@@ -648,7 +648,13 @@ function validateDoc(input) {
|
|
|
648
648
|
}
|
|
649
649
|
};
|
|
650
650
|
walk(doc.children, "children");
|
|
651
|
-
const
|
|
651
|
+
const localNames = new Set(doc.variables ? Object.keys(doc.variables) : []);
|
|
652
|
+
const varNames = /* @__PURE__ */ new Set([...localNames, ...opts.knownVars ?? []]);
|
|
653
|
+
for (const name of opts.presetVars ?? []) if (localNames.has(name)) warnings.push({
|
|
654
|
+
level: "warning",
|
|
655
|
+
path: "<variables>",
|
|
656
|
+
message: `本地变量 $${name} 覆盖了项目预设(在查看器里调预设不会影响这份文件)`
|
|
657
|
+
});
|
|
652
658
|
const varRefs = /* @__PURE__ */ new Set();
|
|
653
659
|
const scan = (obj) => {
|
|
654
660
|
if (typeof obj === "string" && VARIABLE_NAME.test(obj)) varRefs.add(obj.slice(1));
|
|
@@ -933,59 +939,92 @@ function isRecord$3(v) {
|
|
|
933
939
|
function asVarValue(v) {
|
|
934
940
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
935
941
|
}
|
|
936
|
-
/**
|
|
937
|
-
|
|
942
|
+
/**
|
|
943
|
+
* themed 列表 → 按当前主题匹配。
|
|
944
|
+
* 规则:主题键全匹配的条目优先(越具体的胜出),其次无主题项,最后首项。
|
|
945
|
+
* 这样 `{value:'#202226', theme:{mode:'dark'}}` 在 dark 下胜出,
|
|
946
|
+
* 而只写了 `{density:'loose'}` 的尺寸条目在 compact 下不会误命中。
|
|
947
|
+
*/
|
|
948
|
+
function resolveThemed(v, theme) {
|
|
938
949
|
if (Array.isArray(v)) {
|
|
939
950
|
const entries = v.filter(isRecord$3);
|
|
940
|
-
|
|
941
|
-
|
|
951
|
+
if (entries.length === 0) return void 0;
|
|
952
|
+
let best = null;
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
const t = entry["theme"];
|
|
955
|
+
if (t === void 0) {
|
|
956
|
+
if (best === null) best = {
|
|
957
|
+
entry,
|
|
958
|
+
score: 0
|
|
959
|
+
};
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (!isRecord$3(t)) continue;
|
|
963
|
+
const keys = Object.keys(t);
|
|
964
|
+
if (!keys.every((k) => theme[k] === String(t[k]))) continue;
|
|
965
|
+
if (best === null || keys.length > best.score) best = {
|
|
966
|
+
entry,
|
|
967
|
+
score: keys.length
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
return best === null ? asVarValue(entries[0]?.["value"]) : asVarValue(best.entry["value"]);
|
|
942
971
|
}
|
|
943
972
|
return asVarValue(v);
|
|
944
973
|
}
|
|
945
|
-
function makeVarResolver(doc, warnings) {
|
|
974
|
+
function makeVarResolver(doc, warnings, theme = {}) {
|
|
946
975
|
const vars = doc.variables ?? {};
|
|
947
|
-
const
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (val === void 0) {
|
|
957
|
-
warnings.push(`未定义变量 ${input},按原样输出`);
|
|
958
|
-
return input;
|
|
959
|
-
}
|
|
960
|
-
return String(val);
|
|
961
|
-
},
|
|
962
|
-
resolveNum(input) {
|
|
963
|
-
if (typeof input === "number") return input;
|
|
964
|
-
if (typeof input === "string") {
|
|
965
|
-
if (!VARIABLE_NAME.test(input)) {
|
|
966
|
-
const n = Number(input);
|
|
967
|
-
return Number.isFinite(n) ? n : void 0;
|
|
968
|
-
}
|
|
969
|
-
const val = lookup(input.slice(1));
|
|
970
|
-
if (typeof val === "number") return val;
|
|
971
|
-
warnings.push(`数字变量 ${input} 解析失败,按缺省处理`);
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
},
|
|
975
|
-
resolveBool(input) {
|
|
976
|
-
if (typeof input === "boolean") return input;
|
|
977
|
-
if (typeof input === "string") {
|
|
978
|
-
if (!VARIABLE_NAME.test(input)) return input === "true";
|
|
976
|
+
const make = (scope) => {
|
|
977
|
+
const lookup = (name) => {
|
|
978
|
+
const def = vars[name];
|
|
979
|
+
if (!isRecord$3(def)) return void 0;
|
|
980
|
+
return resolveThemed(def["value"], scope);
|
|
981
|
+
};
|
|
982
|
+
return {
|
|
983
|
+
resolve(input) {
|
|
984
|
+
if (!VARIABLE_NAME.test(input)) return input;
|
|
979
985
|
const val = lookup(input.slice(1));
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
986
|
+
if (val === void 0) {
|
|
987
|
+
warnings.push(`未定义变量 ${input},按原样输出`);
|
|
988
|
+
return input;
|
|
989
|
+
}
|
|
990
|
+
return String(val);
|
|
991
|
+
},
|
|
992
|
+
resolveNum(input) {
|
|
993
|
+
if (typeof input === "number") return input;
|
|
994
|
+
if (typeof input === "string") {
|
|
995
|
+
if (!VARIABLE_NAME.test(input)) {
|
|
996
|
+
const n = Number(input);
|
|
997
|
+
return Number.isFinite(n) ? n : void 0;
|
|
998
|
+
}
|
|
999
|
+
const val = lookup(input.slice(1));
|
|
1000
|
+
if (typeof val === "number") return val;
|
|
1001
|
+
warnings.push(`数字变量 ${input} 解析失败,按缺省处理`);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
resolveBool(input) {
|
|
1006
|
+
if (typeof input === "boolean") return input;
|
|
1007
|
+
if (typeof input === "string") {
|
|
1008
|
+
if (!VARIABLE_NAME.test(input)) return input === "true";
|
|
1009
|
+
const val = lookup(input.slice(1));
|
|
1010
|
+
if (typeof val === "boolean") return val;
|
|
1011
|
+
warnings.push(`布尔变量 ${input} 解析失败,按缺省处理`);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
has(name) {
|
|
1016
|
+
return lookup(name) !== void 0;
|
|
1017
|
+
},
|
|
1018
|
+
theme: () => scope,
|
|
1019
|
+
withTheme(patch) {
|
|
1020
|
+
if (patch === void 0) return make(scope);
|
|
1021
|
+
const next = { ...scope };
|
|
1022
|
+
for (const [k, v] of Object.entries(patch)) if (typeof v === "string") next[k] = v;
|
|
1023
|
+
return make(next);
|
|
983
1024
|
}
|
|
984
|
-
}
|
|
985
|
-
has(name) {
|
|
986
|
-
return lookup(name) !== void 0;
|
|
987
|
-
}
|
|
1025
|
+
};
|
|
988
1026
|
};
|
|
1027
|
+
return make(theme);
|
|
989
1028
|
}
|
|
990
1029
|
// src/render/expand.ts
|
|
991
1030
|
function isRecord$2(v) {
|
|
@@ -1109,16 +1148,26 @@ function stopsToCssList(colors, vars) {
|
|
|
1109
1148
|
}
|
|
1110
1149
|
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
1111
1150
|
}
|
|
1112
|
-
/**
|
|
1151
|
+
/**
|
|
1152
|
+
* .pen/.ptr 渐变角度 → CSS 角(度)。
|
|
1153
|
+
*
|
|
1154
|
+
* 基准:`0` = 从左往右(渐变方向指向右),正值顺时针(Figma 那套手感)。
|
|
1155
|
+
* 所以 0 → 90deg(to right)、90 → 0deg(to top)、180 → 270deg(to left)、270 → 180deg(to bottom)。
|
|
1156
|
+
*
|
|
1157
|
+
* 为什么不用「0 指向上」:设计稿里可见的渐变只剩「流光带」(透明→近白→透明,
|
|
1158
|
+
* 从左往右飘的光带),它只有按 0=向右 才是一条竖光带;按 0=向上 会变成横着的一大块。
|
|
1159
|
+
* 深色「渐隐覆盖」类节点是「同色→自身透明」,两条约定看不出差别,不必为它们拗。
|
|
1160
|
+
*/
|
|
1113
1161
|
function penAngleToCss(deg) {
|
|
1114
|
-
return `${((
|
|
1162
|
+
return `${(((90 - deg) % 360 + 360) % 360).toFixed(1)}deg`;
|
|
1115
1163
|
}
|
|
1116
|
-
function normCenter(c) {
|
|
1164
|
+
function normCenter(c, vars) {
|
|
1117
1165
|
if (isRecord$1(c)) {
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1166
|
+
const rx = vars?.resolveNum(c["x"]) ?? (typeof c["x"] === "number" ? c["x"] : void 0);
|
|
1167
|
+
const ry = vars?.resolveNum(c["y"]) ?? (typeof c["y"] === "number" ? c["y"] : void 0);
|
|
1168
|
+
if (rx !== void 0 && ry !== void 0) return {
|
|
1169
|
+
x: `${(rx * 100).toFixed(2)}%`,
|
|
1170
|
+
y: `${(ry * 100).toFixed(2)}%`
|
|
1122
1171
|
};
|
|
1123
1172
|
}
|
|
1124
1173
|
return {
|
|
@@ -1126,6 +1175,20 @@ function normCenter(c) {
|
|
|
1126
1175
|
y: "50%"
|
|
1127
1176
|
};
|
|
1128
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* 径向渐变的椭圆直径(归一化到自身包围盒)→ CSS `ellipse <rx> <ry>`。
|
|
1180
|
+
* 缺省返回 null(用 CSS 默认的 farthest-corner);`{height:1}` 这类只写一边的,
|
|
1181
|
+
* 另一边按正方形处理(与 Pencil 的径向渐变一致)。
|
|
1182
|
+
*/
|
|
1183
|
+
function normSize(size, vars) {
|
|
1184
|
+
if (!isRecord$1(size)) return null;
|
|
1185
|
+
const rw = vars.resolveNum(size["width"]);
|
|
1186
|
+
const rh = vars.resolveNum(size["height"]);
|
|
1187
|
+
if (rw === void 0 && rh === void 0) return null;
|
|
1188
|
+
const w = rw ?? rh ?? 1;
|
|
1189
|
+
const h = rh ?? rw ?? 1;
|
|
1190
|
+
return `${(w * 100).toFixed(2)}% ${(h * 100).toFixed(2)}%`;
|
|
1191
|
+
}
|
|
1129
1192
|
/** fill 集 → background 系列声明。纯色取「最后绘制的」作 background-color(顶层语义) */
|
|
1130
1193
|
function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
1131
1194
|
const entries = Array.isArray(fillsRaw) ? fillsRaw : fillsRaw !== void 0 ? [fillsRaw] : [];
|
|
@@ -1138,6 +1201,7 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
|
1138
1201
|
continue;
|
|
1139
1202
|
}
|
|
1140
1203
|
if (!isRecord$1(entry)) continue;
|
|
1204
|
+
if (vars.resolveBool(entry["enabled"]) === false) continue;
|
|
1141
1205
|
const kind = entry["type"];
|
|
1142
1206
|
if (kind === "color") bgColor = colorFromFillEntry(entry, vars);
|
|
1143
1207
|
else if (kind === "gradient") {
|
|
@@ -1150,8 +1214,9 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
|
1150
1214
|
const rot = vars.resolveNum(entry["rotation"]) ?? 0;
|
|
1151
1215
|
if (gType === "linear") bgImages.push(`linear-gradient(${penAngleToCss(rot)}, ${stops})`);
|
|
1152
1216
|
else if (gType === "radial") {
|
|
1153
|
-
const c = normCenter(entry["center"]);
|
|
1154
|
-
|
|
1217
|
+
const c = normCenter(entry["center"], vars);
|
|
1218
|
+
const sz = normSize(entry["size"], vars);
|
|
1219
|
+
bgImages.push(sz === null ? `radial-gradient(ellipse at ${c.x} ${c.y}, ${stops})` : `radial-gradient(ellipse ${sz} at ${c.x} ${c.y}, ${stops})`);
|
|
1155
1220
|
} else if (gType === "angular") bgImages.push(`conic-gradient(from ${penAngleToCss(rot)} at 50% 50%, ${stops})`);
|
|
1156
1221
|
else bgImages.push(`linear-gradient(${penAngleToCss(rot)}, ${stops})`);
|
|
1157
1222
|
} else if (kind === "image") {
|
|
@@ -1177,16 +1242,37 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
|
1177
1242
|
if (bgColor !== void 0) decls.unshift(["background-color", bgColor]);
|
|
1178
1243
|
return decls;
|
|
1179
1244
|
}
|
|
1180
|
-
/**
|
|
1245
|
+
/**
|
|
1246
|
+
* stroke → CSS。
|
|
1247
|
+
*
|
|
1248
|
+
* 对齐方式决定用哪套 CSS:
|
|
1249
|
+
* - `inner`(默认):`border` 画在盒子内侧 —— 与 Pencil 的 inner 一致;
|
|
1250
|
+
* - `outer`:`outline` 画在外侧,且不占布局(不会把内容挤小);
|
|
1251
|
+
* - `center`:`outline` + 负 offset 半个线宽,线心落在盒子边界上。
|
|
1252
|
+
*
|
|
1253
|
+
* 旧实现一律按 border 画并只发警告,`outer` 的线会整体偏进 1–2px;
|
|
1254
|
+
* 设计稿里 outer 比 inner 还多(editor.pen 76 : 26),值得按真实语义画。
|
|
1255
|
+
*/
|
|
1181
1256
|
function strokeToCss(node, vars, warnings, what) {
|
|
1182
1257
|
const stroke = node["stroke"];
|
|
1183
1258
|
if (stroke === void 0) return [];
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1259
|
+
const enabled = (Array.isArray(stroke) ? stroke : [stroke]).filter((e) => !(isRecord$1(e) && e["enabled"] === false));
|
|
1260
|
+
if (enabled.length === 0) return [];
|
|
1261
|
+
const firstColor = enabled.map((e) => colorFromFillEntry(e, vars)).find((c) => c !== void 0);
|
|
1262
|
+
if (firstColor === void 0) return [];
|
|
1186
1263
|
const decls = [];
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1264
|
+
if (enabled.length > 1) warnings.push(`${what}: 多层描边仅渲染第一层`);
|
|
1265
|
+
const alignRaw = node["strokeAlignment"];
|
|
1266
|
+
const align = alignRaw === "outer" || alignRaw === "center" ? alignRaw : "inner";
|
|
1189
1267
|
const widthRaw = node["strokeWidth"];
|
|
1268
|
+
if (isRecord$1(widthRaw) && align !== "inner") warnings.push(`${what}: 逐边描边宽度 + strokeAlignment=${align} 暂按 inner(CSS border)渲染`);
|
|
1269
|
+
if (!isRecord$1(widthRaw) && align !== "inner") {
|
|
1270
|
+
const w = vars.resolveNum(widthRaw) ?? 1;
|
|
1271
|
+
decls.push(["outline", `${w}px solid ${firstColor}`]);
|
|
1272
|
+
decls.push(["outline-offset", align === "outer" ? "0" : `${-w / 2}px`]);
|
|
1273
|
+
return decls;
|
|
1274
|
+
}
|
|
1275
|
+
decls.push(["border-color", firstColor]);
|
|
1190
1276
|
if (typeof widthRaw === "number") {
|
|
1191
1277
|
decls.push(["border-width", `${widthRaw}px`]);
|
|
1192
1278
|
decls.push(["border-style", "solid"]);
|
|
@@ -1205,12 +1291,10 @@ function strokeToCss(node, vars, warnings, what) {
|
|
|
1205
1291
|
}
|
|
1206
1292
|
}
|
|
1207
1293
|
decls.push(...sides);
|
|
1208
|
-
} else
|
|
1294
|
+
} else {
|
|
1209
1295
|
decls.push(["border-width", "1px"]);
|
|
1210
1296
|
decls.push(["border-style", "solid"]);
|
|
1211
1297
|
}
|
|
1212
|
-
const align = node["strokeAlignment"];
|
|
1213
|
-
if (align === "outer" || align === "center") warnings.push(`${what}: strokeAlignment=${String(align)} 暂按 inner(CSS border)语义渲染`);
|
|
1214
1298
|
return decls;
|
|
1215
1299
|
}
|
|
1216
1300
|
/** effect → box-shadow / filter / backdrop-filter */
|
|
@@ -1263,10 +1347,20 @@ function textToCss(node, vars, warnings) {
|
|
|
1263
1347
|
if (vars.resolveBool(node["underline"])) deco.push("underline");
|
|
1264
1348
|
if (vars.resolveBool(node["strikethrough"])) deco.push("line-through");
|
|
1265
1349
|
if (deco.length > 0) decls.push(["text-decoration", deco.join(" ")]);
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1350
|
+
const vaRaw = node["textAlignVertical"];
|
|
1351
|
+
const va = typeof vaRaw === "string" ? vars.resolve(vaRaw) : void 0;
|
|
1352
|
+
if (va === "middle" || va === "bottom") decls.push(["display", "flex"], ["flex-direction", "column"], ["justify-content", va === "middle" ? "center" : "flex-end"]);
|
|
1353
|
+
else if (va !== void 0 && va !== "top") warnings.push(`${node["id"]}: textAlignVertical=${va} 不在 top/middle/bottom 内,按 top 渲染`);
|
|
1354
|
+
const fillDecls = fillsToCss(node["fill"], vars, warnings, "text");
|
|
1355
|
+
const image = fillDecls.find(([p]) => p === "background-image");
|
|
1356
|
+
const color = fillDecls.find(([p]) => p === "background-color")?.[1];
|
|
1357
|
+
if (image !== void 0) {
|
|
1358
|
+
decls.push(image);
|
|
1359
|
+
decls.push(["-webkit-background-clip", "text"]);
|
|
1360
|
+
decls.push(["background-clip", "text"]);
|
|
1361
|
+
decls.push(["color", "transparent"]);
|
|
1362
|
+
decls.push(["-webkit-text-fill-color", "transparent"]);
|
|
1363
|
+
} else if (color !== void 0) decls.push(["color", color]);
|
|
1270
1364
|
const growth = node["textGrowth"];
|
|
1271
1365
|
if (growth === "auto") decls.push(["white-space", "nowrap"]);
|
|
1272
1366
|
if (growth === "fixed-width-height") decls.push(["overflow", "visible"]);
|
|
@@ -1342,6 +1436,77 @@ function getLucideIcon(name, warnings) {
|
|
|
1342
1436
|
cache.set(name, result);
|
|
1343
1437
|
return result;
|
|
1344
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* 常见图标库 → lucide 的别名表。
|
|
1441
|
+
*
|
|
1442
|
+
* 设计稿里除 lucide 外最常出现的是 Material Symbols(名字是 `snake_case`),
|
|
1443
|
+
* 不映射就整块渲染成虚线占位框 —— 一个界面里几个关键操作图标全是框,
|
|
1444
|
+
* 可读性比「图标形状略有出人」差得多。
|
|
1445
|
+
*/
|
|
1446
|
+
const ALIASES = {
|
|
1447
|
+
check_circle: "circle-check",
|
|
1448
|
+
check: "check",
|
|
1449
|
+
delete: "trash-2",
|
|
1450
|
+
delete_forever: "trash-2",
|
|
1451
|
+
delete_sweep: "trash-2",
|
|
1452
|
+
do_not_disturb_on: "circle-slash",
|
|
1453
|
+
download: "download",
|
|
1454
|
+
upload: "upload",
|
|
1455
|
+
filter_alt: "filter",
|
|
1456
|
+
info: "info",
|
|
1457
|
+
schedule: "clock",
|
|
1458
|
+
history: "history",
|
|
1459
|
+
settings_backup_restore: "history",
|
|
1460
|
+
settings: "settings",
|
|
1461
|
+
zoom_in: "zoom-in",
|
|
1462
|
+
zoom_out: "zoom-out",
|
|
1463
|
+
close: "x",
|
|
1464
|
+
search: "search",
|
|
1465
|
+
visibility: "eye",
|
|
1466
|
+
visibility_off: "eye-off",
|
|
1467
|
+
favorite: "heart",
|
|
1468
|
+
star: "star",
|
|
1469
|
+
play_arrow: "play",
|
|
1470
|
+
pause: "pause",
|
|
1471
|
+
stop: "square",
|
|
1472
|
+
refresh: "rotate-cw",
|
|
1473
|
+
restore: "rotate-ccw",
|
|
1474
|
+
keyboard_arrow_down: "chevron-down",
|
|
1475
|
+
keyboard_arrow_up: "chevron-up",
|
|
1476
|
+
keyboard_arrow_left: "chevron-left",
|
|
1477
|
+
keyboard_arrow_right: "chevron-right",
|
|
1478
|
+
expand_more: "chevron-down",
|
|
1479
|
+
expand_less: "chevron-up",
|
|
1480
|
+
more_vert: "more-vertical",
|
|
1481
|
+
more_horiz: "more-horizontal",
|
|
1482
|
+
folder: "folder",
|
|
1483
|
+
folder_open: "folder-open",
|
|
1484
|
+
image: "image",
|
|
1485
|
+
photo: "image",
|
|
1486
|
+
edit: "pencil",
|
|
1487
|
+
crop: "crop",
|
|
1488
|
+
rotate_left: "rotate-ccw",
|
|
1489
|
+
rotate_right: "rotate-cw",
|
|
1490
|
+
fullscreen: "maximize",
|
|
1491
|
+
fullscreen_exit: "minimize",
|
|
1492
|
+
lock: "lock",
|
|
1493
|
+
lock_open: "unlock",
|
|
1494
|
+
warning: "triangle-alert",
|
|
1495
|
+
error: "circle-alert",
|
|
1496
|
+
help: "circle-help"
|
|
1497
|
+
};
|
|
1498
|
+
/**
|
|
1499
|
+
* 把任意图标库里的名字归一成 lucide 名。
|
|
1500
|
+
* lucide 自己的名字直接返回;Material Symbols 走别表;
|
|
1501
|
+
* 其它库(Font Awesome、自定义字体)也先试别名,再不中就由调用方降级占位。
|
|
1502
|
+
*/
|
|
1503
|
+
function resolveIconName(library, name) {
|
|
1504
|
+
if (name === "") return name;
|
|
1505
|
+
if (library === "lucide" || library === "feather") return name;
|
|
1506
|
+
const direct = ALIASES[name];
|
|
1507
|
+
if (direct !== void 0) return direct;
|
|
1508
|
+
return name;
|
|
1509
|
+
}
|
|
1345
1510
|
// src/render/html.ts
|
|
1346
1511
|
function escapeHtml(s) {
|
|
1347
1512
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1437,7 +1602,9 @@ function layoutDecls(node, vars) {
|
|
|
1437
1602
|
else if (jc === "space_between") decls.push(["justify-content", "space-between"]);
|
|
1438
1603
|
else if (jc === "space_around") decls.push(["justify-content", "space-around"]);
|
|
1439
1604
|
const ai = node["alignItems"];
|
|
1440
|
-
if (ai === "start"
|
|
1605
|
+
if (ai === "start") decls.push(["align-items", "flex-start"]);
|
|
1606
|
+
else if (ai === "center" || ai === "end") decls.push(["align-items", ai === "end" ? "flex-end" : "center"]);
|
|
1607
|
+
else decls.push(["align-items", "flex-start"]);
|
|
1441
1608
|
}
|
|
1442
1609
|
return {
|
|
1443
1610
|
decls,
|
|
@@ -1496,60 +1663,63 @@ function boardAttrs(node, vars) {
|
|
|
1496
1663
|
}
|
|
1497
1664
|
function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
|
|
1498
1665
|
const type = String(node["type"] ?? "?");
|
|
1666
|
+
const scoped = isRecord(node["theme"]) ? vars.withTheme(node["theme"]) : vars;
|
|
1667
|
+
if (scoped.resolveBool(node["enabled"]) === false) return "";
|
|
1499
1668
|
const label = typeof node["name"] === "string" && node["name"] !== "" ? `${type}: ${String(node["name"])}` : type;
|
|
1500
1669
|
const positioned = parentLayout === "none" || node["layoutPosition"] === "absolute";
|
|
1501
1670
|
if (type === "script" || type === "browser" || !KNOWN_RENDER_TYPES.has(type)) return openTag(node, "", extraAttrs) + placeholderBox(label, []) + "</div>";
|
|
1502
1671
|
const css = [];
|
|
1503
1672
|
if (positioned) {
|
|
1504
1673
|
css.push(["position", "absolute"]);
|
|
1505
|
-
const x = numericSize(node["x"],
|
|
1506
|
-
const y = numericSize(node["y"],
|
|
1674
|
+
const x = numericSize(node["x"], scoped);
|
|
1675
|
+
const y = numericSize(node["y"], scoped);
|
|
1507
1676
|
css.push(["left", `${x ?? 0}px`], ["top", `${y ?? 0}px`]);
|
|
1508
|
-
}
|
|
1509
|
-
const { decls: layoutCss, effective } = layoutDecls(node,
|
|
1677
|
+
} else css.push(["position", "relative"]);
|
|
1678
|
+
const { decls: layoutCss, effective } = layoutDecls(node, scoped);
|
|
1510
1679
|
css.push(...layoutCss);
|
|
1511
|
-
css.push(
|
|
1512
|
-
css.push(...
|
|
1513
|
-
|
|
1514
|
-
css.push(...
|
|
1515
|
-
css.push(...
|
|
1680
|
+
if (parentLayout !== "none" && !positioned) css.push(["flex-shrink", "0"]);
|
|
1681
|
+
css.push(...sizeDecls(node["width"], node["height"], positioned, parentLayout, scoped, warnings, label));
|
|
1682
|
+
css.push(...commonToCss(node, scoped));
|
|
1683
|
+
if (type !== "text") css.push(...fillsToCss(node["fill"], scoped, warnings, label));
|
|
1684
|
+
css.push(...strokeToCss(node, scoped, warnings, label));
|
|
1685
|
+
css.push(...effectsToCss(node["effect"], scoped, warnings, label));
|
|
1516
1686
|
let inner = "";
|
|
1517
1687
|
switch (type) {
|
|
1518
1688
|
case "text":
|
|
1519
|
-
css.push(...textToCss(node,
|
|
1520
|
-
inner = escapeHtml(typeof node["content"] === "string" ?
|
|
1689
|
+
css.push(...textToCss(node, scoped, warnings));
|
|
1690
|
+
inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
|
|
1521
1691
|
break;
|
|
1522
1692
|
case "icon":
|
|
1523
1693
|
case "icon_font": {
|
|
1524
|
-
const colorCss = fillsToCss(node["fill"],
|
|
1694
|
+
const colorCss = fillsToCss(node["fill"], scoped, warnings, label).find(([p]) => p === "background-color");
|
|
1525
1695
|
if (colorCss) css.push(["color", colorCss[1]], ["background", "transparent"]);
|
|
1526
1696
|
const fontVariant = type === "icon_font";
|
|
1527
1697
|
const libField = fontVariant ? node["iconFontFamily"] : node["library"];
|
|
1528
1698
|
const nameField = fontVariant ? node["iconFontName"] : node["icon"];
|
|
1529
1699
|
const lib = typeof libField === "string" && libField !== "" ? libField : "lucide";
|
|
1530
|
-
const iconName = typeof nameField === "string" ?
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1700
|
+
const iconName = typeof nameField === "string" ? scoped.resolve(nameField) : "";
|
|
1701
|
+
const lucideName = resolveIconName(lib, iconName);
|
|
1702
|
+
const svg = iconName === "" ? null : getLucideIcon(lucideName, warnings);
|
|
1703
|
+
if (svg === null && lucideName === iconName && lib !== "lucide" && lib !== "feather") warnings.push(`${label}: 图标库 "${lib}" 的 "${iconName}" 没有可用的 lucide 等价图标,渲染为占位框`);
|
|
1534
1704
|
inner = svg ? svg.replace("<svg", "<svg style=\"width:100%;height:100%;display:block\"") : placeholderBox(`${lib}:${iconName}`, []);
|
|
1535
1705
|
break;
|
|
1536
1706
|
}
|
|
1537
1707
|
case "note": {
|
|
1538
|
-
const bg = fillsToCss(node["fill"] ?? node["color"] ?? "#FEE484",
|
|
1708
|
+
const bg = fillsToCss(node["fill"] ?? node["color"] ?? "#FEE484", scoped, warnings, label).find(([p]) => p === "background-color")?.[1] ?? "#FEE484";
|
|
1539
1709
|
css.push(["background", bg], ["padding", "8px 10px"], ["color", "#292524"], ["font", "12px/1.5 system-ui,sans-serif"], ["border-radius", "4px"], ["overflow", "hidden"]);
|
|
1540
|
-
inner = escapeHtml(typeof node["content"] === "string" ?
|
|
1710
|
+
inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
|
|
1541
1711
|
break;
|
|
1542
1712
|
}
|
|
1543
1713
|
case "ellipse": {
|
|
1544
1714
|
css.push(["border-radius", "50%"]);
|
|
1545
|
-
const inner2 =
|
|
1715
|
+
const inner2 = scoped.resolveNum(node["innerRadius"]);
|
|
1546
1716
|
if (inner2 !== void 0 && inner2 > 0) warnings.push(`${label}: innerRadius 环形暂按实心椭圆渲染`);
|
|
1547
|
-
const sweep =
|
|
1717
|
+
const sweep = scoped.resolveNum(node["sweepAngle"]);
|
|
1548
1718
|
if (sweep !== void 0 && sweep !== 360) warnings.push(`${label}: 弧形(sweepAngle=${sweep})暂按整圆渲染`);
|
|
1549
1719
|
break;
|
|
1550
1720
|
}
|
|
1551
1721
|
case "polygon": {
|
|
1552
|
-
const count =
|
|
1722
|
+
const count = scoped.resolveNum(node["polygonCount"]);
|
|
1553
1723
|
const n = count ?? 3;
|
|
1554
1724
|
if (count === void 0) warnings.push(`${label}: polygonCount 缺省按 3 渲染`);
|
|
1555
1725
|
if (node["cornerRadius"] !== void 0) warnings.push(`${label}: 多边形圆角暂忽略`);
|
|
@@ -1557,9 +1727,9 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
|
|
|
1557
1727
|
break;
|
|
1558
1728
|
}
|
|
1559
1729
|
case "path": {
|
|
1560
|
-
css.push(...cornerRadiusToCss(node["cornerRadius"],
|
|
1730
|
+
css.push(...cornerRadiusToCss(node["cornerRadius"], scoped));
|
|
1561
1731
|
const d = typeof node["geometry"] === "string" ? node["geometry"] : "";
|
|
1562
|
-
const color = fillsToCss(node["fill"],
|
|
1732
|
+
const color = fillsToCss(node["fill"], scoped, warnings, label).find(([p]) => p === "background-color")?.[1] ?? "#000000";
|
|
1563
1733
|
const vbRaw = node["viewBox"];
|
|
1564
1734
|
inner = `<svg viewBox="${(Array.isArray(vbRaw) && vbRaw.length === 4 && vbRaw.every((v) => typeof v === "number") ? vbRaw : [
|
|
1565
1735
|
0,
|
|
@@ -1570,11 +1740,11 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
|
|
|
1570
1740
|
break;
|
|
1571
1741
|
}
|
|
1572
1742
|
default:
|
|
1573
|
-
css.push(...cornerRadiusToCss(node["cornerRadius"],
|
|
1743
|
+
css.push(...cornerRadiusToCss(node["cornerRadius"], scoped));
|
|
1574
1744
|
if (type === "group" && node["effect"] !== void 0) {}
|
|
1575
1745
|
}
|
|
1576
1746
|
const kidsRaw = node["children"];
|
|
1577
|
-
const childrenHtml = Array.isArray(kidsRaw) ? kidsRaw.filter(isRecord).map((k) => renderNode(k, effective,
|
|
1747
|
+
const childrenHtml = Array.isArray(kidsRaw) ? kidsRaw.filter(isRecord).map((k) => renderNode(k, effective, scoped, warnings)).join("") : "";
|
|
1578
1748
|
return openTag(node, css.map(([p, v]) => `${p}:${v}`).join(";"), extraAttrs) + inner + childrenHtml + "</div>";
|
|
1579
1749
|
}
|
|
1580
1750
|
const KNOWN_RENDER_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -1625,7 +1795,9 @@ function boardBoxes(nodes, vars) {
|
|
|
1625
1795
|
}
|
|
1626
1796
|
/** 渲染整棵(已展开的)树:顶层节点在画布上绝对定位,并带画布元数据 */
|
|
1627
1797
|
function renderTree(nodes, vars, warnings) {
|
|
1628
|
-
return nodes.filter(isRecord).map((n) =>
|
|
1798
|
+
return nodes.filter(isRecord).map((n) => {
|
|
1799
|
+
return renderNode(n, "none", vars, warnings, boardAttrs(n, isRecord(n["theme"]) ? vars.withTheme(n["theme"]) : vars));
|
|
1800
|
+
}).join("");
|
|
1629
1801
|
}
|
|
1630
1802
|
// src/render/entry.ts
|
|
1631
1803
|
/**
|
|
@@ -1650,7 +1822,7 @@ function renderDocument(doc, opts = {}) {
|
|
|
1650
1822
|
`<title>${escapeAttr(title)}</title>`,
|
|
1651
1823
|
"<style>",
|
|
1652
1824
|
"* { box-sizing: border-box; margin: 0; padding: 0; }",
|
|
1653
|
-
|
|
1825
|
+
`body { margin: 0; background: ${opts.transparent === true ? "transparent" : "#ffffff"}; }`,
|
|
1654
1826
|
".ptr-canvas { position: relative; }",
|
|
1655
1827
|
"</style>",
|
|
1656
1828
|
"</head>",
|