paintress 0.5.1 → 0.5.2
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 +5 -3
- package/README.zh-CN.md +1 -1
- package/dist/cli.js +110 -30
- package/dist/index.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/{render-CzpO1cOe.js → render-CCxYNL6A.js} +259 -93
- package/dist/{server-vPylVJZf.js → server-ssrAxbTR.js} +158 -36
- package/package.json +1 -1
- package/skills/paintress/SKILL.md +4 -2
- package/ui/dist/assets/index-BnDj5nL8.js +8 -0
- package/ui/dist/assets/index-CP8lZ8lp.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-Dyrrkh92.css +0 -1
- package/ui/dist/assets/index-kPX0SDa0.js +0 -8
|
@@ -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 读取,避免在多处硬编码后忘记同步。
|
|
@@ -933,59 +933,92 @@ function isRecord$3(v) {
|
|
|
933
933
|
function asVarValue(v) {
|
|
934
934
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
935
935
|
}
|
|
936
|
-
/**
|
|
937
|
-
|
|
936
|
+
/**
|
|
937
|
+
* themed 列表 → 按当前主题匹配。
|
|
938
|
+
* 规则:主题键全匹配的条目优先(越具体的胜出),其次无主题项,最后首项。
|
|
939
|
+
* 这样 `{value:'#202226', theme:{mode:'dark'}}` 在 dark 下胜出,
|
|
940
|
+
* 而只写了 `{density:'loose'}` 的尺寸条目在 compact 下不会误命中。
|
|
941
|
+
*/
|
|
942
|
+
function resolveThemed(v, theme) {
|
|
938
943
|
if (Array.isArray(v)) {
|
|
939
944
|
const entries = v.filter(isRecord$3);
|
|
940
|
-
|
|
941
|
-
|
|
945
|
+
if (entries.length === 0) return void 0;
|
|
946
|
+
let best = null;
|
|
947
|
+
for (const entry of entries) {
|
|
948
|
+
const t = entry["theme"];
|
|
949
|
+
if (t === void 0) {
|
|
950
|
+
if (best === null) best = {
|
|
951
|
+
entry,
|
|
952
|
+
score: 0
|
|
953
|
+
};
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (!isRecord$3(t)) continue;
|
|
957
|
+
const keys = Object.keys(t);
|
|
958
|
+
if (!keys.every((k) => theme[k] === String(t[k]))) continue;
|
|
959
|
+
if (best === null || keys.length > best.score) best = {
|
|
960
|
+
entry,
|
|
961
|
+
score: keys.length
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return best === null ? asVarValue(entries[0]?.["value"]) : asVarValue(best.entry["value"]);
|
|
942
965
|
}
|
|
943
966
|
return asVarValue(v);
|
|
944
967
|
}
|
|
945
|
-
function makeVarResolver(doc, warnings) {
|
|
968
|
+
function makeVarResolver(doc, warnings, theme = {}) {
|
|
946
969
|
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";
|
|
970
|
+
const make = (scope) => {
|
|
971
|
+
const lookup = (name) => {
|
|
972
|
+
const def = vars[name];
|
|
973
|
+
if (!isRecord$3(def)) return void 0;
|
|
974
|
+
return resolveThemed(def["value"], scope);
|
|
975
|
+
};
|
|
976
|
+
return {
|
|
977
|
+
resolve(input) {
|
|
978
|
+
if (!VARIABLE_NAME.test(input)) return input;
|
|
979
979
|
const val = lookup(input.slice(1));
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
980
|
+
if (val === void 0) {
|
|
981
|
+
warnings.push(`未定义变量 ${input},按原样输出`);
|
|
982
|
+
return input;
|
|
983
|
+
}
|
|
984
|
+
return String(val);
|
|
985
|
+
},
|
|
986
|
+
resolveNum(input) {
|
|
987
|
+
if (typeof input === "number") return input;
|
|
988
|
+
if (typeof input === "string") {
|
|
989
|
+
if (!VARIABLE_NAME.test(input)) {
|
|
990
|
+
const n = Number(input);
|
|
991
|
+
return Number.isFinite(n) ? n : void 0;
|
|
992
|
+
}
|
|
993
|
+
const val = lookup(input.slice(1));
|
|
994
|
+
if (typeof val === "number") return val;
|
|
995
|
+
warnings.push(`数字变量 ${input} 解析失败,按缺省处理`);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
resolveBool(input) {
|
|
1000
|
+
if (typeof input === "boolean") return input;
|
|
1001
|
+
if (typeof input === "string") {
|
|
1002
|
+
if (!VARIABLE_NAME.test(input)) return input === "true";
|
|
1003
|
+
const val = lookup(input.slice(1));
|
|
1004
|
+
if (typeof val === "boolean") return val;
|
|
1005
|
+
warnings.push(`布尔变量 ${input} 解析失败,按缺省处理`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
has(name) {
|
|
1010
|
+
return lookup(name) !== void 0;
|
|
1011
|
+
},
|
|
1012
|
+
theme: () => scope,
|
|
1013
|
+
withTheme(patch) {
|
|
1014
|
+
if (patch === void 0) return make(scope);
|
|
1015
|
+
const next = { ...scope };
|
|
1016
|
+
for (const [k, v] of Object.entries(patch)) if (typeof v === "string") next[k] = v;
|
|
1017
|
+
return make(next);
|
|
983
1018
|
}
|
|
984
|
-
}
|
|
985
|
-
has(name) {
|
|
986
|
-
return lookup(name) !== void 0;
|
|
987
|
-
}
|
|
1019
|
+
};
|
|
988
1020
|
};
|
|
1021
|
+
return make(theme);
|
|
989
1022
|
}
|
|
990
1023
|
// src/render/expand.ts
|
|
991
1024
|
function isRecord$2(v) {
|
|
@@ -1109,16 +1142,26 @@ function stopsToCssList(colors, vars) {
|
|
|
1109
1142
|
}
|
|
1110
1143
|
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
1111
1144
|
}
|
|
1112
|
-
/**
|
|
1145
|
+
/**
|
|
1146
|
+
* .pen/.ptr 渐变角度 → CSS 角(度)。
|
|
1147
|
+
*
|
|
1148
|
+
* 基准:`0` = 从左往右(渐变方向指向右),正值顺时针(Figma 那套手感)。
|
|
1149
|
+
* 所以 0 → 90deg(to right)、90 → 0deg(to top)、180 → 270deg(to left)、270 → 180deg(to bottom)。
|
|
1150
|
+
*
|
|
1151
|
+
* 为什么不用「0 指向上」:设计稿里可见的渐变只剩「流光带」(透明→近白→透明,
|
|
1152
|
+
* 从左往右飘的光带),它只有按 0=向右 才是一条竖光带;按 0=向上 会变成横着的一大块。
|
|
1153
|
+
* 深色「渐隐覆盖」类节点是「同色→自身透明」,两条约定看不出差别,不必为它们拗。
|
|
1154
|
+
*/
|
|
1113
1155
|
function penAngleToCss(deg) {
|
|
1114
|
-
return `${((
|
|
1156
|
+
return `${(((90 - deg) % 360 + 360) % 360).toFixed(1)}deg`;
|
|
1115
1157
|
}
|
|
1116
|
-
function normCenter(c) {
|
|
1158
|
+
function normCenter(c, vars) {
|
|
1117
1159
|
if (isRecord$1(c)) {
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1160
|
+
const rx = vars?.resolveNum(c["x"]) ?? (typeof c["x"] === "number" ? c["x"] : void 0);
|
|
1161
|
+
const ry = vars?.resolveNum(c["y"]) ?? (typeof c["y"] === "number" ? c["y"] : void 0);
|
|
1162
|
+
if (rx !== void 0 && ry !== void 0) return {
|
|
1163
|
+
x: `${(rx * 100).toFixed(2)}%`,
|
|
1164
|
+
y: `${(ry * 100).toFixed(2)}%`
|
|
1122
1165
|
};
|
|
1123
1166
|
}
|
|
1124
1167
|
return {
|
|
@@ -1126,6 +1169,20 @@ function normCenter(c) {
|
|
|
1126
1169
|
y: "50%"
|
|
1127
1170
|
};
|
|
1128
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* 径向渐变的椭圆直径(归一化到自身包围盒)→ CSS `ellipse <rx> <ry>`。
|
|
1174
|
+
* 缺省返回 null(用 CSS 默认的 farthest-corner);`{height:1}` 这类只写一边的,
|
|
1175
|
+
* 另一边按正方形处理(与 Pencil 的径向渐变一致)。
|
|
1176
|
+
*/
|
|
1177
|
+
function normSize(size, vars) {
|
|
1178
|
+
if (!isRecord$1(size)) return null;
|
|
1179
|
+
const rw = vars.resolveNum(size["width"]);
|
|
1180
|
+
const rh = vars.resolveNum(size["height"]);
|
|
1181
|
+
if (rw === void 0 && rh === void 0) return null;
|
|
1182
|
+
const w = rw ?? rh ?? 1;
|
|
1183
|
+
const h = rh ?? rw ?? 1;
|
|
1184
|
+
return `${(w * 100).toFixed(2)}% ${(h * 100).toFixed(2)}%`;
|
|
1185
|
+
}
|
|
1129
1186
|
/** fill 集 → background 系列声明。纯色取「最后绘制的」作 background-color(顶层语义) */
|
|
1130
1187
|
function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
1131
1188
|
const entries = Array.isArray(fillsRaw) ? fillsRaw : fillsRaw !== void 0 ? [fillsRaw] : [];
|
|
@@ -1138,6 +1195,7 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
|
1138
1195
|
continue;
|
|
1139
1196
|
}
|
|
1140
1197
|
if (!isRecord$1(entry)) continue;
|
|
1198
|
+
if (vars.resolveBool(entry["enabled"]) === false) continue;
|
|
1141
1199
|
const kind = entry["type"];
|
|
1142
1200
|
if (kind === "color") bgColor = colorFromFillEntry(entry, vars);
|
|
1143
1201
|
else if (kind === "gradient") {
|
|
@@ -1150,8 +1208,9 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
|
1150
1208
|
const rot = vars.resolveNum(entry["rotation"]) ?? 0;
|
|
1151
1209
|
if (gType === "linear") bgImages.push(`linear-gradient(${penAngleToCss(rot)}, ${stops})`);
|
|
1152
1210
|
else if (gType === "radial") {
|
|
1153
|
-
const c = normCenter(entry["center"]);
|
|
1154
|
-
|
|
1211
|
+
const c = normCenter(entry["center"], vars);
|
|
1212
|
+
const sz = normSize(entry["size"], vars);
|
|
1213
|
+
bgImages.push(sz === null ? `radial-gradient(ellipse at ${c.x} ${c.y}, ${stops})` : `radial-gradient(ellipse ${sz} at ${c.x} ${c.y}, ${stops})`);
|
|
1155
1214
|
} else if (gType === "angular") bgImages.push(`conic-gradient(from ${penAngleToCss(rot)} at 50% 50%, ${stops})`);
|
|
1156
1215
|
else bgImages.push(`linear-gradient(${penAngleToCss(rot)}, ${stops})`);
|
|
1157
1216
|
} else if (kind === "image") {
|
|
@@ -1177,16 +1236,37 @@ function fillsToCss(fillsRaw, vars, warnings, what) {
|
|
|
1177
1236
|
if (bgColor !== void 0) decls.unshift(["background-color", bgColor]);
|
|
1178
1237
|
return decls;
|
|
1179
1238
|
}
|
|
1180
|
-
/**
|
|
1239
|
+
/**
|
|
1240
|
+
* stroke → CSS。
|
|
1241
|
+
*
|
|
1242
|
+
* 对齐方式决定用哪套 CSS:
|
|
1243
|
+
* - `inner`(默认):`border` 画在盒子内侧 —— 与 Pencil 的 inner 一致;
|
|
1244
|
+
* - `outer`:`outline` 画在外侧,且不占布局(不会把内容挤小);
|
|
1245
|
+
* - `center`:`outline` + 负 offset 半个线宽,线心落在盒子边界上。
|
|
1246
|
+
*
|
|
1247
|
+
* 旧实现一律按 border 画并只发警告,`outer` 的线会整体偏进 1–2px;
|
|
1248
|
+
* 设计稿里 outer 比 inner 还多(editor.pen 76 : 26),值得按真实语义画。
|
|
1249
|
+
*/
|
|
1181
1250
|
function strokeToCss(node, vars, warnings, what) {
|
|
1182
1251
|
const stroke = node["stroke"];
|
|
1183
1252
|
if (stroke === void 0) return [];
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1253
|
+
const enabled = (Array.isArray(stroke) ? stroke : [stroke]).filter((e) => !(isRecord$1(e) && e["enabled"] === false));
|
|
1254
|
+
if (enabled.length === 0) return [];
|
|
1255
|
+
const firstColor = enabled.map((e) => colorFromFillEntry(e, vars)).find((c) => c !== void 0);
|
|
1256
|
+
if (firstColor === void 0) return [];
|
|
1186
1257
|
const decls = [];
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1258
|
+
if (enabled.length > 1) warnings.push(`${what}: 多层描边仅渲染第一层`);
|
|
1259
|
+
const alignRaw = node["strokeAlignment"];
|
|
1260
|
+
const align = alignRaw === "outer" || alignRaw === "center" ? alignRaw : "inner";
|
|
1189
1261
|
const widthRaw = node["strokeWidth"];
|
|
1262
|
+
if (isRecord$1(widthRaw) && align !== "inner") warnings.push(`${what}: 逐边描边宽度 + strokeAlignment=${align} 暂按 inner(CSS border)渲染`);
|
|
1263
|
+
if (!isRecord$1(widthRaw) && align !== "inner") {
|
|
1264
|
+
const w = vars.resolveNum(widthRaw) ?? 1;
|
|
1265
|
+
decls.push(["outline", `${w}px solid ${firstColor}`]);
|
|
1266
|
+
decls.push(["outline-offset", align === "outer" ? "0" : `${-w / 2}px`]);
|
|
1267
|
+
return decls;
|
|
1268
|
+
}
|
|
1269
|
+
decls.push(["border-color", firstColor]);
|
|
1190
1270
|
if (typeof widthRaw === "number") {
|
|
1191
1271
|
decls.push(["border-width", `${widthRaw}px`]);
|
|
1192
1272
|
decls.push(["border-style", "solid"]);
|
|
@@ -1205,12 +1285,10 @@ function strokeToCss(node, vars, warnings, what) {
|
|
|
1205
1285
|
}
|
|
1206
1286
|
}
|
|
1207
1287
|
decls.push(...sides);
|
|
1208
|
-
} else
|
|
1288
|
+
} else {
|
|
1209
1289
|
decls.push(["border-width", "1px"]);
|
|
1210
1290
|
decls.push(["border-style", "solid"]);
|
|
1211
1291
|
}
|
|
1212
|
-
const align = node["strokeAlignment"];
|
|
1213
|
-
if (align === "outer" || align === "center") warnings.push(`${what}: strokeAlignment=${String(align)} 暂按 inner(CSS border)语义渲染`);
|
|
1214
1292
|
return decls;
|
|
1215
1293
|
}
|
|
1216
1294
|
/** effect → box-shadow / filter / backdrop-filter */
|
|
@@ -1263,10 +1341,20 @@ function textToCss(node, vars, warnings) {
|
|
|
1263
1341
|
if (vars.resolveBool(node["underline"])) deco.push("underline");
|
|
1264
1342
|
if (vars.resolveBool(node["strikethrough"])) deco.push("line-through");
|
|
1265
1343
|
if (deco.length > 0) decls.push(["text-decoration", deco.join(" ")]);
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1344
|
+
const vaRaw = node["textAlignVertical"];
|
|
1345
|
+
const va = typeof vaRaw === "string" ? vars.resolve(vaRaw) : void 0;
|
|
1346
|
+
if (va === "middle" || va === "bottom") decls.push(["display", "flex"], ["flex-direction", "column"], ["justify-content", va === "middle" ? "center" : "flex-end"]);
|
|
1347
|
+
else if (va !== void 0 && va !== "top") warnings.push(`${node["id"]}: textAlignVertical=${va} 不在 top/middle/bottom 内,按 top 渲染`);
|
|
1348
|
+
const fillDecls = fillsToCss(node["fill"], vars, warnings, "text");
|
|
1349
|
+
const image = fillDecls.find(([p]) => p === "background-image");
|
|
1350
|
+
const color = fillDecls.find(([p]) => p === "background-color")?.[1];
|
|
1351
|
+
if (image !== void 0) {
|
|
1352
|
+
decls.push(image);
|
|
1353
|
+
decls.push(["-webkit-background-clip", "text"]);
|
|
1354
|
+
decls.push(["background-clip", "text"]);
|
|
1355
|
+
decls.push(["color", "transparent"]);
|
|
1356
|
+
decls.push(["-webkit-text-fill-color", "transparent"]);
|
|
1357
|
+
} else if (color !== void 0) decls.push(["color", color]);
|
|
1270
1358
|
const growth = node["textGrowth"];
|
|
1271
1359
|
if (growth === "auto") decls.push(["white-space", "nowrap"]);
|
|
1272
1360
|
if (growth === "fixed-width-height") decls.push(["overflow", "visible"]);
|
|
@@ -1342,6 +1430,77 @@ function getLucideIcon(name, warnings) {
|
|
|
1342
1430
|
cache.set(name, result);
|
|
1343
1431
|
return result;
|
|
1344
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* 常见图标库 → lucide 的别名表。
|
|
1435
|
+
*
|
|
1436
|
+
* 设计稿里除 lucide 外最常出现的是 Material Symbols(名字是 `snake_case`),
|
|
1437
|
+
* 不映射就整块渲染成虚线占位框 —— 一个界面里几个关键操作图标全是框,
|
|
1438
|
+
* 可读性比「图标形状略有出人」差得多。
|
|
1439
|
+
*/
|
|
1440
|
+
const ALIASES = {
|
|
1441
|
+
check_circle: "circle-check",
|
|
1442
|
+
check: "check",
|
|
1443
|
+
delete: "trash-2",
|
|
1444
|
+
delete_forever: "trash-2",
|
|
1445
|
+
delete_sweep: "trash-2",
|
|
1446
|
+
do_not_disturb_on: "circle-slash",
|
|
1447
|
+
download: "download",
|
|
1448
|
+
upload: "upload",
|
|
1449
|
+
filter_alt: "filter",
|
|
1450
|
+
info: "info",
|
|
1451
|
+
schedule: "clock",
|
|
1452
|
+
history: "history",
|
|
1453
|
+
settings_backup_restore: "history",
|
|
1454
|
+
settings: "settings",
|
|
1455
|
+
zoom_in: "zoom-in",
|
|
1456
|
+
zoom_out: "zoom-out",
|
|
1457
|
+
close: "x",
|
|
1458
|
+
search: "search",
|
|
1459
|
+
visibility: "eye",
|
|
1460
|
+
visibility_off: "eye-off",
|
|
1461
|
+
favorite: "heart",
|
|
1462
|
+
star: "star",
|
|
1463
|
+
play_arrow: "play",
|
|
1464
|
+
pause: "pause",
|
|
1465
|
+
stop: "square",
|
|
1466
|
+
refresh: "rotate-cw",
|
|
1467
|
+
restore: "rotate-ccw",
|
|
1468
|
+
keyboard_arrow_down: "chevron-down",
|
|
1469
|
+
keyboard_arrow_up: "chevron-up",
|
|
1470
|
+
keyboard_arrow_left: "chevron-left",
|
|
1471
|
+
keyboard_arrow_right: "chevron-right",
|
|
1472
|
+
expand_more: "chevron-down",
|
|
1473
|
+
expand_less: "chevron-up",
|
|
1474
|
+
more_vert: "more-vertical",
|
|
1475
|
+
more_horiz: "more-horizontal",
|
|
1476
|
+
folder: "folder",
|
|
1477
|
+
folder_open: "folder-open",
|
|
1478
|
+
image: "image",
|
|
1479
|
+
photo: "image",
|
|
1480
|
+
edit: "pencil",
|
|
1481
|
+
crop: "crop",
|
|
1482
|
+
rotate_left: "rotate-ccw",
|
|
1483
|
+
rotate_right: "rotate-cw",
|
|
1484
|
+
fullscreen: "maximize",
|
|
1485
|
+
fullscreen_exit: "minimize",
|
|
1486
|
+
lock: "lock",
|
|
1487
|
+
lock_open: "unlock",
|
|
1488
|
+
warning: "triangle-alert",
|
|
1489
|
+
error: "circle-alert",
|
|
1490
|
+
help: "circle-help"
|
|
1491
|
+
};
|
|
1492
|
+
/**
|
|
1493
|
+
* 把任意图标库里的名字归一成 lucide 名。
|
|
1494
|
+
* lucide 自己的名字直接返回;Material Symbols 走别表;
|
|
1495
|
+
* 其它库(Font Awesome、自定义字体)也先试别名,再不中就由调用方降级占位。
|
|
1496
|
+
*/
|
|
1497
|
+
function resolveIconName(library, name) {
|
|
1498
|
+
if (name === "") return name;
|
|
1499
|
+
if (library === "lucide" || library === "feather") return name;
|
|
1500
|
+
const direct = ALIASES[name];
|
|
1501
|
+
if (direct !== void 0) return direct;
|
|
1502
|
+
return name;
|
|
1503
|
+
}
|
|
1345
1504
|
// src/render/html.ts
|
|
1346
1505
|
function escapeHtml(s) {
|
|
1347
1506
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1437,7 +1596,9 @@ function layoutDecls(node, vars) {
|
|
|
1437
1596
|
else if (jc === "space_between") decls.push(["justify-content", "space-between"]);
|
|
1438
1597
|
else if (jc === "space_around") decls.push(["justify-content", "space-around"]);
|
|
1439
1598
|
const ai = node["alignItems"];
|
|
1440
|
-
if (ai === "start"
|
|
1599
|
+
if (ai === "start") decls.push(["align-items", "flex-start"]);
|
|
1600
|
+
else if (ai === "center" || ai === "end") decls.push(["align-items", ai === "end" ? "flex-end" : "center"]);
|
|
1601
|
+
else decls.push(["align-items", "flex-start"]);
|
|
1441
1602
|
}
|
|
1442
1603
|
return {
|
|
1443
1604
|
decls,
|
|
@@ -1496,60 +1657,63 @@ function boardAttrs(node, vars) {
|
|
|
1496
1657
|
}
|
|
1497
1658
|
function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
|
|
1498
1659
|
const type = String(node["type"] ?? "?");
|
|
1660
|
+
const scoped = isRecord(node["theme"]) ? vars.withTheme(node["theme"]) : vars;
|
|
1661
|
+
if (scoped.resolveBool(node["enabled"]) === false) return "";
|
|
1499
1662
|
const label = typeof node["name"] === "string" && node["name"] !== "" ? `${type}: ${String(node["name"])}` : type;
|
|
1500
1663
|
const positioned = parentLayout === "none" || node["layoutPosition"] === "absolute";
|
|
1501
1664
|
if (type === "script" || type === "browser" || !KNOWN_RENDER_TYPES.has(type)) return openTag(node, "", extraAttrs) + placeholderBox(label, []) + "</div>";
|
|
1502
1665
|
const css = [];
|
|
1503
1666
|
if (positioned) {
|
|
1504
1667
|
css.push(["position", "absolute"]);
|
|
1505
|
-
const x = numericSize(node["x"],
|
|
1506
|
-
const y = numericSize(node["y"],
|
|
1668
|
+
const x = numericSize(node["x"], scoped);
|
|
1669
|
+
const y = numericSize(node["y"], scoped);
|
|
1507
1670
|
css.push(["left", `${x ?? 0}px`], ["top", `${y ?? 0}px`]);
|
|
1508
|
-
}
|
|
1509
|
-
const { decls: layoutCss, effective } = layoutDecls(node,
|
|
1671
|
+
} else css.push(["position", "relative"]);
|
|
1672
|
+
const { decls: layoutCss, effective } = layoutDecls(node, scoped);
|
|
1510
1673
|
css.push(...layoutCss);
|
|
1511
|
-
css.push(
|
|
1512
|
-
css.push(...
|
|
1513
|
-
|
|
1514
|
-
css.push(...
|
|
1515
|
-
css.push(...
|
|
1674
|
+
if (parentLayout !== "none" && !positioned) css.push(["flex-shrink", "0"]);
|
|
1675
|
+
css.push(...sizeDecls(node["width"], node["height"], positioned, parentLayout, scoped, warnings, label));
|
|
1676
|
+
css.push(...commonToCss(node, scoped));
|
|
1677
|
+
if (type !== "text") css.push(...fillsToCss(node["fill"], scoped, warnings, label));
|
|
1678
|
+
css.push(...strokeToCss(node, scoped, warnings, label));
|
|
1679
|
+
css.push(...effectsToCss(node["effect"], scoped, warnings, label));
|
|
1516
1680
|
let inner = "";
|
|
1517
1681
|
switch (type) {
|
|
1518
1682
|
case "text":
|
|
1519
|
-
css.push(...textToCss(node,
|
|
1520
|
-
inner = escapeHtml(typeof node["content"] === "string" ?
|
|
1683
|
+
css.push(...textToCss(node, scoped, warnings));
|
|
1684
|
+
inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
|
|
1521
1685
|
break;
|
|
1522
1686
|
case "icon":
|
|
1523
1687
|
case "icon_font": {
|
|
1524
|
-
const colorCss = fillsToCss(node["fill"],
|
|
1688
|
+
const colorCss = fillsToCss(node["fill"], scoped, warnings, label).find(([p]) => p === "background-color");
|
|
1525
1689
|
if (colorCss) css.push(["color", colorCss[1]], ["background", "transparent"]);
|
|
1526
1690
|
const fontVariant = type === "icon_font";
|
|
1527
1691
|
const libField = fontVariant ? node["iconFontFamily"] : node["library"];
|
|
1528
1692
|
const nameField = fontVariant ? node["iconFontName"] : node["icon"];
|
|
1529
1693
|
const lib = typeof libField === "string" && libField !== "" ? libField : "lucide";
|
|
1530
|
-
const iconName = typeof nameField === "string" ?
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1694
|
+
const iconName = typeof nameField === "string" ? scoped.resolve(nameField) : "";
|
|
1695
|
+
const lucideName = resolveIconName(lib, iconName);
|
|
1696
|
+
const svg = iconName === "" ? null : getLucideIcon(lucideName, warnings);
|
|
1697
|
+
if (svg === null && lucideName === iconName && lib !== "lucide" && lib !== "feather") warnings.push(`${label}: 图标库 "${lib}" 的 "${iconName}" 没有可用的 lucide 等价图标,渲染为占位框`);
|
|
1534
1698
|
inner = svg ? svg.replace("<svg", "<svg style=\"width:100%;height:100%;display:block\"") : placeholderBox(`${lib}:${iconName}`, []);
|
|
1535
1699
|
break;
|
|
1536
1700
|
}
|
|
1537
1701
|
case "note": {
|
|
1538
|
-
const bg = fillsToCss(node["fill"] ?? node["color"] ?? "#FEE484",
|
|
1702
|
+
const bg = fillsToCss(node["fill"] ?? node["color"] ?? "#FEE484", scoped, warnings, label).find(([p]) => p === "background-color")?.[1] ?? "#FEE484";
|
|
1539
1703
|
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" ?
|
|
1704
|
+
inner = escapeHtml(typeof node["content"] === "string" ? scoped.resolve(node["content"]) : "");
|
|
1541
1705
|
break;
|
|
1542
1706
|
}
|
|
1543
1707
|
case "ellipse": {
|
|
1544
1708
|
css.push(["border-radius", "50%"]);
|
|
1545
|
-
const inner2 =
|
|
1709
|
+
const inner2 = scoped.resolveNum(node["innerRadius"]);
|
|
1546
1710
|
if (inner2 !== void 0 && inner2 > 0) warnings.push(`${label}: innerRadius 环形暂按实心椭圆渲染`);
|
|
1547
|
-
const sweep =
|
|
1711
|
+
const sweep = scoped.resolveNum(node["sweepAngle"]);
|
|
1548
1712
|
if (sweep !== void 0 && sweep !== 360) warnings.push(`${label}: 弧形(sweepAngle=${sweep})暂按整圆渲染`);
|
|
1549
1713
|
break;
|
|
1550
1714
|
}
|
|
1551
1715
|
case "polygon": {
|
|
1552
|
-
const count =
|
|
1716
|
+
const count = scoped.resolveNum(node["polygonCount"]);
|
|
1553
1717
|
const n = count ?? 3;
|
|
1554
1718
|
if (count === void 0) warnings.push(`${label}: polygonCount 缺省按 3 渲染`);
|
|
1555
1719
|
if (node["cornerRadius"] !== void 0) warnings.push(`${label}: 多边形圆角暂忽略`);
|
|
@@ -1557,9 +1721,9 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
|
|
|
1557
1721
|
break;
|
|
1558
1722
|
}
|
|
1559
1723
|
case "path": {
|
|
1560
|
-
css.push(...cornerRadiusToCss(node["cornerRadius"],
|
|
1724
|
+
css.push(...cornerRadiusToCss(node["cornerRadius"], scoped));
|
|
1561
1725
|
const d = typeof node["geometry"] === "string" ? node["geometry"] : "";
|
|
1562
|
-
const color = fillsToCss(node["fill"],
|
|
1726
|
+
const color = fillsToCss(node["fill"], scoped, warnings, label).find(([p]) => p === "background-color")?.[1] ?? "#000000";
|
|
1563
1727
|
const vbRaw = node["viewBox"];
|
|
1564
1728
|
inner = `<svg viewBox="${(Array.isArray(vbRaw) && vbRaw.length === 4 && vbRaw.every((v) => typeof v === "number") ? vbRaw : [
|
|
1565
1729
|
0,
|
|
@@ -1570,11 +1734,11 @@ function renderNode(node, parentLayout, vars, warnings, extraAttrs = []) {
|
|
|
1570
1734
|
break;
|
|
1571
1735
|
}
|
|
1572
1736
|
default:
|
|
1573
|
-
css.push(...cornerRadiusToCss(node["cornerRadius"],
|
|
1737
|
+
css.push(...cornerRadiusToCss(node["cornerRadius"], scoped));
|
|
1574
1738
|
if (type === "group" && node["effect"] !== void 0) {}
|
|
1575
1739
|
}
|
|
1576
1740
|
const kidsRaw = node["children"];
|
|
1577
|
-
const childrenHtml = Array.isArray(kidsRaw) ? kidsRaw.filter(isRecord).map((k) => renderNode(k, effective,
|
|
1741
|
+
const childrenHtml = Array.isArray(kidsRaw) ? kidsRaw.filter(isRecord).map((k) => renderNode(k, effective, scoped, warnings)).join("") : "";
|
|
1578
1742
|
return openTag(node, css.map(([p, v]) => `${p}:${v}`).join(";"), extraAttrs) + inner + childrenHtml + "</div>";
|
|
1579
1743
|
}
|
|
1580
1744
|
const KNOWN_RENDER_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -1625,7 +1789,9 @@ function boardBoxes(nodes, vars) {
|
|
|
1625
1789
|
}
|
|
1626
1790
|
/** 渲染整棵(已展开的)树:顶层节点在画布上绝对定位,并带画布元数据 */
|
|
1627
1791
|
function renderTree(nodes, vars, warnings) {
|
|
1628
|
-
return nodes.filter(isRecord).map((n) =>
|
|
1792
|
+
return nodes.filter(isRecord).map((n) => {
|
|
1793
|
+
return renderNode(n, "none", vars, warnings, boardAttrs(n, isRecord(n["theme"]) ? vars.withTheme(n["theme"]) : vars));
|
|
1794
|
+
}).join("");
|
|
1629
1795
|
}
|
|
1630
1796
|
// src/render/entry.ts
|
|
1631
1797
|
/**
|