weifuwu 0.55.2 → 0.56.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 +263 -29
- package/dist/client/app.d.ts +3 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +354 -88
- package/dist/client/reactive.d.ts +6 -0
- package/dist/client/render.d.ts +11 -0
- package/dist/client/route-match.d.ts +22 -0
- package/dist/client/types.d.ts +17 -0
- package/dist/client/vnode.d.ts +32 -1
- package/dist/components/Table/Table.d.ts +2 -0
- package/dist/components/ThemeSwitch/ThemeSwitch.d.ts +27 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +136 -19
- package/dist/components/style.css +285 -44
- package/dist/dev/index.js +23 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +320 -7
- package/dist/layout/weifuwu-layout.css +180 -15
- package/dist/ui/html-safe.d.ts +11 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/ssr-page.d.ts +41 -0
- package/dist/ui/ssr.d.ts +35 -0
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -7,6 +7,18 @@ var HttpError = class extends Error {
|
|
|
7
7
|
this.status = status;
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
|
+
function createMiddleware(config) {
|
|
11
|
+
const mw = async (req, ctx, next) => {
|
|
12
|
+
const injected = await config.setup(ctx);
|
|
13
|
+
Object.assign(ctx, injected);
|
|
14
|
+
return next(req, ctx);
|
|
15
|
+
};
|
|
16
|
+
mw.__meta = {
|
|
17
|
+
injects: config.injects,
|
|
18
|
+
depends: config.depends ?? []
|
|
19
|
+
};
|
|
20
|
+
return mw;
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
// src/core/serve.ts
|
|
12
24
|
import http from "node:http";
|
|
@@ -714,7 +726,9 @@ var Router = class _Router {
|
|
|
714
726
|
if (!next) return this._wildcardMatch(method, segments);
|
|
715
727
|
node = next;
|
|
716
728
|
}
|
|
717
|
-
|
|
729
|
+
const resolved = this._resolveMatch(node, method, params, segments.length);
|
|
730
|
+
if (resolved) return resolved;
|
|
731
|
+
return this._wildcardMatch(method, segments);
|
|
718
732
|
}
|
|
719
733
|
_wildcardMatch(method, segments) {
|
|
720
734
|
if (!this._hasWildcard) return null;
|
|
@@ -1069,23 +1083,219 @@ import { build } from "esbuild";
|
|
|
1069
1083
|
import { readFile, stat } from "node:fs/promises";
|
|
1070
1084
|
import { resolve as resolve2 } from "node:path";
|
|
1071
1085
|
import { fileURLToPath } from "node:url";
|
|
1086
|
+
|
|
1087
|
+
// src/ui/html-safe.ts
|
|
1072
1088
|
var HtmlSafe = class {
|
|
1089
|
+
value;
|
|
1073
1090
|
constructor(value) {
|
|
1074
1091
|
this.value = value;
|
|
1075
1092
|
}
|
|
1076
|
-
value;
|
|
1077
1093
|
toString() {
|
|
1078
1094
|
return this.value;
|
|
1079
1095
|
}
|
|
1080
1096
|
};
|
|
1097
|
+
|
|
1098
|
+
// src/client/vnode.ts
|
|
1099
|
+
var ASYNC_MARK = "__wfAsyncComponent";
|
|
1100
|
+
function isAsyncComponent(type) {
|
|
1101
|
+
return typeof type === "function" && type?.[ASYNC_MARK] === true;
|
|
1102
|
+
}
|
|
1103
|
+
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
1104
|
+
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
1105
|
+
|
|
1106
|
+
// src/client/reactive.ts
|
|
1107
|
+
function createReactiveState(dirty) {
|
|
1108
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
1109
|
+
const reactive = (target) => {
|
|
1110
|
+
if (target === null || typeof target !== "object") return target;
|
|
1111
|
+
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
1112
|
+
const proxy = new Proxy(target, {
|
|
1113
|
+
set(target2, key, value) {
|
|
1114
|
+
const old = Reflect.get(target2, key);
|
|
1115
|
+
if (old === value) return true;
|
|
1116
|
+
Reflect.set(target2, key, value);
|
|
1117
|
+
dirty();
|
|
1118
|
+
return true;
|
|
1119
|
+
},
|
|
1120
|
+
get(target2, key) {
|
|
1121
|
+
const value = Reflect.get(target2, key);
|
|
1122
|
+
if (typeof value === "object" && value !== null) return reactive(value);
|
|
1123
|
+
return value;
|
|
1124
|
+
},
|
|
1125
|
+
deleteProperty(target2, key) {
|
|
1126
|
+
if (Reflect.has(target2, key)) {
|
|
1127
|
+
Reflect.deleteProperty(target2, key);
|
|
1128
|
+
dirty();
|
|
1129
|
+
}
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
proxyCache.set(target, proxy);
|
|
1134
|
+
return proxy;
|
|
1135
|
+
};
|
|
1136
|
+
return reactive({});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/ui/ssr.ts
|
|
1081
1140
|
function escape(s) {
|
|
1082
1141
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1083
1142
|
}
|
|
1143
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
1144
|
+
"area",
|
|
1145
|
+
"base",
|
|
1146
|
+
"br",
|
|
1147
|
+
"col",
|
|
1148
|
+
"embed",
|
|
1149
|
+
"hr",
|
|
1150
|
+
"img",
|
|
1151
|
+
"input",
|
|
1152
|
+
"link",
|
|
1153
|
+
"meta",
|
|
1154
|
+
"param",
|
|
1155
|
+
"source",
|
|
1156
|
+
"track",
|
|
1157
|
+
"wbr"
|
|
1158
|
+
]);
|
|
1159
|
+
function classToString(v) {
|
|
1160
|
+
if (typeof v === "string") return v;
|
|
1161
|
+
if (Array.isArray(v)) return v.filter(Boolean).join(" ");
|
|
1162
|
+
if (v && typeof v === "object") {
|
|
1163
|
+
return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
|
|
1164
|
+
}
|
|
1165
|
+
return "";
|
|
1166
|
+
}
|
|
1167
|
+
function styleToString(v) {
|
|
1168
|
+
if (typeof v === "string") return v;
|
|
1169
|
+
if (v && typeof v === "object") {
|
|
1170
|
+
return Object.entries(v).filter(([, x]) => x != null).map(([k, x]) => `${k}:${typeof x === "number" ? x + "px" : String(x)}`).join(";");
|
|
1171
|
+
}
|
|
1172
|
+
return "";
|
|
1173
|
+
}
|
|
1174
|
+
function createSsrContext(serverCtx, dataStore) {
|
|
1175
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
1176
|
+
const ui2 = {
|
|
1177
|
+
$: () => createReactiveState(() => {
|
|
1178
|
+
}),
|
|
1179
|
+
dirty: () => {
|
|
1180
|
+
},
|
|
1181
|
+
render: () => {
|
|
1182
|
+
},
|
|
1183
|
+
selfId: (name) => {
|
|
1184
|
+
if (usedIds.has(name)) throw new Error(`[weifuwu] Duplicate component ID: "${name}"`);
|
|
1185
|
+
usedIds.add(name);
|
|
1186
|
+
},
|
|
1187
|
+
useMedia: () => {
|
|
1188
|
+
},
|
|
1189
|
+
useBreakpoint: () => {
|
|
1190
|
+
},
|
|
1191
|
+
usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
|
|
1192
|
+
} })
|
|
1193
|
+
};
|
|
1194
|
+
const ctx = Object.create(serverCtx ?? {});
|
|
1195
|
+
ctx.ui = ui2;
|
|
1196
|
+
ctx.data = {
|
|
1197
|
+
async get(key, fetcher) {
|
|
1198
|
+
if (dataStore.has(key)) return dataStore.get(key);
|
|
1199
|
+
if (!fetcher) return void 0;
|
|
1200
|
+
const val = await fetcher();
|
|
1201
|
+
dataStore.set(key, val);
|
|
1202
|
+
return val;
|
|
1203
|
+
},
|
|
1204
|
+
set(key, val) {
|
|
1205
|
+
dataStore.set(key, val);
|
|
1206
|
+
},
|
|
1207
|
+
has(key) {
|
|
1208
|
+
return dataStore.has(key);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
return ctx;
|
|
1212
|
+
}
|
|
1213
|
+
async function renderSsr(input, ctx) {
|
|
1214
|
+
if (input == null || typeof input === "boolean") return "";
|
|
1215
|
+
if (typeof input === "string" || typeof input === "number") return escape(String(input));
|
|
1216
|
+
if (Array.isArray(input)) {
|
|
1217
|
+
let s = "";
|
|
1218
|
+
for (const c of input) s += await renderSsr(c, ctx);
|
|
1219
|
+
return s;
|
|
1220
|
+
}
|
|
1221
|
+
const vnode = input;
|
|
1222
|
+
if (vnode.type === Portal) return renderSsr(vnode.props?.children, ctx);
|
|
1223
|
+
if (vnode.type === Fragment) return renderSsr(vnode.props?.children, ctx);
|
|
1224
|
+
if (typeof vnode.type === "function") {
|
|
1225
|
+
let def;
|
|
1226
|
+
if (isAsyncComponent(vnode.type)) {
|
|
1227
|
+
def = await vnode.type(ctx);
|
|
1228
|
+
if (typeof def !== "function") {
|
|
1229
|
+
throw new Error(
|
|
1230
|
+
`asyncComponent factory <${vnode.type.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
} else {
|
|
1234
|
+
def = vnode.type;
|
|
1235
|
+
}
|
|
1236
|
+
const childCtx = Object.create(ctx);
|
|
1237
|
+
const renderFn = def(vnode.props ?? {}, childCtx);
|
|
1238
|
+
if (typeof renderFn !== "function") {
|
|
1239
|
+
throw new Error(
|
|
1240
|
+
`Component ${vnode.type.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
return renderSsr(renderFn(vnode.props ?? {}), childCtx);
|
|
1244
|
+
}
|
|
1245
|
+
const tag = vnode.type;
|
|
1246
|
+
const props = vnode.props ?? {};
|
|
1247
|
+
const attrs = [];
|
|
1248
|
+
let innerHTML;
|
|
1249
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1250
|
+
if (key === "children" || key === "key") continue;
|
|
1251
|
+
if (key === "ref") continue;
|
|
1252
|
+
if (key.startsWith("on") && typeof value === "function") continue;
|
|
1253
|
+
if (key === "innerHTML") {
|
|
1254
|
+
innerHTML = String(value ?? "");
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (key === "class" || key === "className") {
|
|
1258
|
+
const cls = classToString(value);
|
|
1259
|
+
if (cls) attrs.push(` class="${escape(cls)}"`);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (key === "style" && value && typeof value === "object") {
|
|
1263
|
+
attrs.push(` style="${escape(styleToString(value))}"`);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (value === true) {
|
|
1267
|
+
attrs.push(` ${key}`);
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
if (value === false || value == null) continue;
|
|
1271
|
+
attrs.push(` ${key}="${escape(String(value))}"`);
|
|
1272
|
+
}
|
|
1273
|
+
const attrStr = attrs.join("");
|
|
1274
|
+
if (VOID_TAGS.has(tag)) return `<${tag}${attrStr}>`;
|
|
1275
|
+
if (innerHTML !== void 0) return `<${tag}${attrStr}>${innerHTML}</${tag}>`;
|
|
1276
|
+
const children = await renderSsr(props.children, ctx);
|
|
1277
|
+
return `<${tag}${attrStr}>${children}</${tag}>`;
|
|
1278
|
+
}
|
|
1279
|
+
async function ssrToString(Comp, props, serverCtx, opts = {}) {
|
|
1280
|
+
const dataStore = opts.data ?? /* @__PURE__ */ new Map();
|
|
1281
|
+
const ctx = createSsrContext(serverCtx, dataStore);
|
|
1282
|
+
const html = await renderSsr({ type: Comp, props: props ?? {}, key: void 0 }, ctx);
|
|
1283
|
+
return new HtmlSafe(html);
|
|
1284
|
+
}
|
|
1285
|
+
function serializeData(data) {
|
|
1286
|
+
const json = JSON.stringify(Object.fromEntries(data)).replace(/</g, "\\u003c");
|
|
1287
|
+
return `<script>window.__DATA__=${json};</script>`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/ui/index.ts
|
|
1291
|
+
function escape2(s) {
|
|
1292
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1293
|
+
}
|
|
1084
1294
|
function stringify(v) {
|
|
1085
1295
|
if (v == null || v === false || v === true) return "";
|
|
1086
1296
|
if (Array.isArray(v)) return v.map(stringify).join("");
|
|
1087
1297
|
if (v instanceof HtmlSafe) return v.value;
|
|
1088
|
-
return
|
|
1298
|
+
return escape2(String(v));
|
|
1089
1299
|
}
|
|
1090
1300
|
function unsafe(s) {
|
|
1091
1301
|
return new HtmlSafe(s);
|
|
@@ -1139,12 +1349,21 @@ function ui() {
|
|
|
1139
1349
|
}
|
|
1140
1350
|
ctx.ui = {
|
|
1141
1351
|
html: Object.assign(htmlTag, { unsafe }),
|
|
1352
|
+
/** SSR 渲染组件 → HTML 片段(HtmlSafe:内联进 ctx.ui.html 不二次转义) */
|
|
1353
|
+
async ssr(Comp, props, opts) {
|
|
1354
|
+
const safe = await ssrToString(Comp, props ?? {}, ctx, opts);
|
|
1355
|
+
return safe;
|
|
1356
|
+
},
|
|
1357
|
+
/** 序列化 SSR 数据存储 → window.__DATA__ 脚本(HtmlSafe) */
|
|
1358
|
+
ssrData(data) {
|
|
1359
|
+
return new HtmlSafe(serializeData(data));
|
|
1360
|
+
},
|
|
1142
1361
|
async js(entryPath) {
|
|
1143
1362
|
const absPath = resolveEntry(entryPath);
|
|
1144
1363
|
const cached = jsCache.get(absPath);
|
|
1145
1364
|
if (cached && await jsCacheFresh(cached.inputs)) {
|
|
1146
1365
|
return new Response(cached.code, {
|
|
1147
|
-
headers: { "Content-Type": "application/javascript" }
|
|
1366
|
+
headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
|
|
1148
1367
|
});
|
|
1149
1368
|
}
|
|
1150
1369
|
const result = await build({
|
|
@@ -1168,7 +1387,7 @@ function ui() {
|
|
|
1168
1387
|
}
|
|
1169
1388
|
jsCache.set(absPath, { code, inputs });
|
|
1170
1389
|
return new Response(code, {
|
|
1171
|
-
headers: { "Content-Type": "application/javascript" }
|
|
1390
|
+
headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
|
|
1172
1391
|
});
|
|
1173
1392
|
},
|
|
1174
1393
|
async css(entryPath) {
|
|
@@ -1177,7 +1396,7 @@ function ui() {
|
|
|
1177
1396
|
const cached = cssCache.get(absPath);
|
|
1178
1397
|
if (cached && cached.mtime === st.mtimeMs) {
|
|
1179
1398
|
return new Response(cached.code, {
|
|
1180
|
-
headers: { "Content-Type": "text/css; charset=utf-8" }
|
|
1399
|
+
headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
|
|
1181
1400
|
});
|
|
1182
1401
|
}
|
|
1183
1402
|
let code = await readFile(absPath, "utf-8");
|
|
@@ -1195,7 +1414,7 @@ function ui() {
|
|
|
1195
1414
|
}
|
|
1196
1415
|
cssCache.set(absPath, { code, mtime: st.mtimeMs });
|
|
1197
1416
|
return new Response(code, {
|
|
1198
|
-
headers: { "Content-Type": "text/css; charset=utf-8" }
|
|
1417
|
+
headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
|
|
1199
1418
|
});
|
|
1200
1419
|
}
|
|
1201
1420
|
};
|
|
@@ -1203,6 +1422,98 @@ function ui() {
|
|
|
1203
1422
|
};
|
|
1204
1423
|
}
|
|
1205
1424
|
|
|
1425
|
+
// src/client/route-match.ts
|
|
1426
|
+
function joinPaths(a, b) {
|
|
1427
|
+
if (!b || b === "/") return a || "/";
|
|
1428
|
+
const left = a.endsWith("/") ? a.slice(0, -1) : a;
|
|
1429
|
+
const right = b.startsWith("/") ? b : "/" + b;
|
|
1430
|
+
return left + right;
|
|
1431
|
+
}
|
|
1432
|
+
function compilePath(path) {
|
|
1433
|
+
const keys = [];
|
|
1434
|
+
const reStr = path.replace(/:(\w+)/g, (_, key) => {
|
|
1435
|
+
keys.push(key);
|
|
1436
|
+
return "([^/]+)";
|
|
1437
|
+
}).replace(/\*/g, ".*");
|
|
1438
|
+
return { re: new RegExp(`^${reStr}$`), keys };
|
|
1439
|
+
}
|
|
1440
|
+
function flattenRoutes(routes, basePath = "", chain = []) {
|
|
1441
|
+
const result = [];
|
|
1442
|
+
for (const route of routes) {
|
|
1443
|
+
const fullPath = joinPaths(basePath, route.path);
|
|
1444
|
+
const { re, keys } = compilePath(fullPath);
|
|
1445
|
+
const fullChain = [...chain, route];
|
|
1446
|
+
result.push({ re, keys, def: route, chain: fullChain });
|
|
1447
|
+
if (route.children) {
|
|
1448
|
+
result.push(...flattenRoutes(route.children, fullPath, fullChain));
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return result;
|
|
1452
|
+
}
|
|
1453
|
+
function matchRoute(path, routes) {
|
|
1454
|
+
let best = null;
|
|
1455
|
+
for (const fr of routes) {
|
|
1456
|
+
if (path.match(fr.re)) {
|
|
1457
|
+
if (!best || fr.chain.length > best.chain.length) best = fr;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return best;
|
|
1461
|
+
}
|
|
1462
|
+
function extractParams(path, match) {
|
|
1463
|
+
const params = {};
|
|
1464
|
+
const m = path.match(match.re);
|
|
1465
|
+
if (m) {
|
|
1466
|
+
for (let i = 0; i < match.keys.length; i++) {
|
|
1467
|
+
params[match.keys[i]] = decodeURIComponent(m[i + 1]);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return params;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/ui/ssr-page.ts
|
|
1474
|
+
var DEFAULT_TEMPLATE = (p) => `<!DOCTYPE html>
|
|
1475
|
+
<html lang="zh-CN">
|
|
1476
|
+
<head>
|
|
1477
|
+
<meta charset="UTF-8">
|
|
1478
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1479
|
+
<title>${escapeHtml(p.title)}</title>
|
|
1480
|
+
${(p.styles ?? []).map((s) => `<link rel="stylesheet" href="${escapeHtml(s)}">`).join("\n ")}
|
|
1481
|
+
</head>
|
|
1482
|
+
<body>
|
|
1483
|
+
<div id="root">${p.html}</div>
|
|
1484
|
+
${p.dataScript}
|
|
1485
|
+
${p.bundle ? `<script src="${escapeHtml(p.bundle)}"></script>` : ""}
|
|
1486
|
+
</body>
|
|
1487
|
+
</html>`;
|
|
1488
|
+
function escapeHtml(s) {
|
|
1489
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1490
|
+
}
|
|
1491
|
+
function uiSsr(opts) {
|
|
1492
|
+
const flatRoutes = flattenRoutes(opts.routes);
|
|
1493
|
+
const template = opts.template ?? DEFAULT_TEMPLATE;
|
|
1494
|
+
return async (req, ctx, next) => {
|
|
1495
|
+
if (req.method !== "GET") return next(req, ctx);
|
|
1496
|
+
const url = new URL(req.url);
|
|
1497
|
+
const match = matchRoute(url.pathname, flatRoutes);
|
|
1498
|
+
if (!match) return next(req, ctx);
|
|
1499
|
+
const params = extractParams(url.pathname, match);
|
|
1500
|
+
const def = match.chain[match.chain.length - 1] ?? match.def;
|
|
1501
|
+
const pageCtx = Object.assign(Object.create(ctx), {
|
|
1502
|
+
params,
|
|
1503
|
+
route: { path: url.pathname, params, query: Object.fromEntries(url.searchParams) }
|
|
1504
|
+
});
|
|
1505
|
+
if (!def.component) return next(req, ctx);
|
|
1506
|
+
const data = /* @__PURE__ */ new Map();
|
|
1507
|
+
const html = await ssrToString(def.component, {}, pageCtx, { data });
|
|
1508
|
+
const title = opts.title ? opts.title({ params, def }) : def.title || "weifuwu";
|
|
1509
|
+
const dataScript = new HtmlSafe(serializeData(data)).toString();
|
|
1510
|
+
const body = template({ html: html.toString(), dataScript, title, bundle: opts.bundle, styles: opts.styles });
|
|
1511
|
+
return new Response(body, {
|
|
1512
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1513
|
+
});
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1206
1517
|
// src/db/postgres/connection.ts
|
|
1207
1518
|
import net from "node:net";
|
|
1208
1519
|
import crypto2 from "node:crypto";
|
|
@@ -2933,6 +3244,7 @@ export {
|
|
|
2933
3244
|
badRequest,
|
|
2934
3245
|
conflict,
|
|
2935
3246
|
cors,
|
|
3247
|
+
createMiddleware,
|
|
2936
3248
|
created,
|
|
2937
3249
|
forbidden,
|
|
2938
3250
|
noContent,
|
|
@@ -2947,6 +3259,7 @@ export {
|
|
|
2947
3259
|
serverError,
|
|
2948
3260
|
tooManyRequests,
|
|
2949
3261
|
ui,
|
|
3262
|
+
uiSsr,
|
|
2950
3263
|
unauthorized,
|
|
2951
3264
|
unprocessable
|
|
2952
3265
|
};
|
|
@@ -35,8 +35,20 @@
|
|
|
35
35
|
--wf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
|
|
36
36
|
|
|
37
37
|
/* ── 层级 ── */
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/* 浮层全部 createPortal 到 body,全局参与堆叠。
|
|
39
|
+
顺序:sticky < overlay < modal/drawer < 弹出面板族 < toast < tooltip。
|
|
40
|
+
modal 与 drawer 互斥出现;面板族同级(dropdown/select/datepicker/popover)用 DOM 顺序裁决。 */
|
|
41
|
+
--wf-z-sticky: 40;
|
|
42
|
+
--wf-z-overlay: 80;
|
|
43
|
+
--wf-z-modal: 100;
|
|
44
|
+
--wf-z-popover: 120; /* dropdown/select/datepicker 面板与 popover 同层 */
|
|
45
|
+
--wf-z-toast: 140;
|
|
46
|
+
--wf-z-tooltip: 160;
|
|
47
|
+
--wf-z-dropdown: var(--wf-z-popover); /* 语义别名:下拉菜单属弹出面板族 */
|
|
48
|
+
|
|
49
|
+
/* ── 兼容别名(旧 token 名,语义近似映射) ── */
|
|
50
|
+
--wf-pop-z: var(--wf-z-popover);
|
|
51
|
+
--wf-cover-z: var(--wf-z-modal);
|
|
40
52
|
|
|
41
53
|
/* ── 色彩 — 品牌 ── */
|
|
42
54
|
--wf-color-primary: #4f6ef7;
|
|
@@ -71,15 +83,15 @@
|
|
|
71
83
|
--wf-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
72
84
|
--wf-font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
73
85
|
|
|
74
|
-
--wf-font-size-xs:
|
|
75
|
-
--wf-font-size-sm:
|
|
76
|
-
--wf-font-size-base:
|
|
77
|
-
--wf-font-size-lg:
|
|
78
|
-
--wf-font-size-xl:
|
|
79
|
-
--wf-font-size-2xl:
|
|
86
|
+
--wf-font-size-xs: 12px;
|
|
87
|
+
--wf-font-size-sm: 13px;
|
|
88
|
+
--wf-font-size-base: 14px;
|
|
89
|
+
--wf-font-size-lg: 15px;
|
|
90
|
+
--wf-font-size-xl: 16px;
|
|
91
|
+
--wf-font-size-2xl: 21px;
|
|
80
92
|
--wf-font-size-3xl: 24px;
|
|
81
|
-
--wf-font-size-4xl:
|
|
82
|
-
--wf-font-size-5xl:
|
|
93
|
+
--wf-font-size-4xl: 30px;
|
|
94
|
+
--wf-font-size-5xl: 36px;
|
|
83
95
|
|
|
84
96
|
--wf-font-weight-normal: 400;
|
|
85
97
|
--wf-font-weight-medium: 500;
|
|
@@ -110,6 +122,10 @@
|
|
|
110
122
|
--wf-accent-color: var(--wf-color-primary);
|
|
111
123
|
--wf-caret-color: var(--wf-color-primary);
|
|
112
124
|
|
|
125
|
+
/* 控件尺寸:垂直内边距 + 目标高度(移动端在 _base.css 提升到 44px) */
|
|
126
|
+
--wf-control-pad-y: 10px;
|
|
127
|
+
--wf-control-height: 36px;
|
|
128
|
+
|
|
113
129
|
/* ── 断点 ── */
|
|
114
130
|
--wf-bp-sm: 640px;
|
|
115
131
|
--wf-bp-md: 768px;
|
|
@@ -126,8 +142,21 @@
|
|
|
126
142
|
--wf-letter-spacing-wider: 1px;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
/* weifuwu/theme — 暗色模式
|
|
130
|
-
|
|
145
|
+
/* weifuwu/theme — 暗色模式
|
|
146
|
+
*
|
|
147
|
+
* 两种激活方式(优先级:显式 data-theme > 系统偏好):
|
|
148
|
+
* 1. 手动:<html data-theme="dark">(或 data-theme="light" 强制亮色)
|
|
149
|
+
* 2. 自动:系统偏好 prefers-color-scheme: dark,无显式覆盖时生效
|
|
150
|
+
*
|
|
151
|
+
* ⚠️ 两段暗色覆盖值必须保持一致(静态 CSS 无法共享选择器列表,改动需同步)。
|
|
152
|
+
* 每段末尾都设置 color-scheme: dark,让原生控件/滚动条跟随。
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
/* ── 激活方式一:手动 data-theme="dark" ── */
|
|
156
|
+
/* color-scheme 用 :root[data-theme] 提升优先级,压过 _base.css 的 :root 定义(导入顺序在后者) */
|
|
157
|
+
:root[data-theme="dark"] {
|
|
158
|
+
color-scheme: dark;
|
|
159
|
+
|
|
131
160
|
--wf-color-primary: #6b8aff;
|
|
132
161
|
--wf-color-primary-hover: #8ba3ff;
|
|
133
162
|
--wf-color-primary-bg: #1e293b;
|
|
@@ -166,6 +195,50 @@
|
|
|
166
195
|
--wf-surface-bg: var(--wf-color-bg);
|
|
167
196
|
}
|
|
168
197
|
|
|
198
|
+
/* ── 激活方式二:系统暗色偏好(data-theme="light" 可强制亮色) ── */
|
|
199
|
+
@media (prefers-color-scheme: dark) {
|
|
200
|
+
:root:not([data-theme="light"]) {
|
|
201
|
+
color-scheme: dark;
|
|
202
|
+
|
|
203
|
+
--wf-color-primary: #6b8aff;
|
|
204
|
+
--wf-color-primary-hover: #8ba3ff;
|
|
205
|
+
--wf-color-primary-bg: #1e293b;
|
|
206
|
+
|
|
207
|
+
--wf-color-success: #4ade80;
|
|
208
|
+
--wf-color-success-bg: #14532d;
|
|
209
|
+
--wf-color-warning: #fbbf24;
|
|
210
|
+
--wf-color-warning-bg: #451a03;
|
|
211
|
+
--wf-color-error: #f87171;
|
|
212
|
+
--wf-color-error-bg: #451a1a;
|
|
213
|
+
|
|
214
|
+
--wf-color-info: #60a5fa;
|
|
215
|
+
--wf-color-info-bg: #172554;
|
|
216
|
+
|
|
217
|
+
--wf-color-text: #f1f5f9;
|
|
218
|
+
--wf-color-text-secondary: #94a3b8;
|
|
219
|
+
--wf-color-text-tertiary: #64748b;
|
|
220
|
+
--wf-color-text-disabled: #475569;
|
|
221
|
+
|
|
222
|
+
--wf-color-bg: #0f172a;
|
|
223
|
+
--wf-color-bg-secondary: #1e293b;
|
|
224
|
+
--wf-color-bg-tertiary: #334155;
|
|
225
|
+
|
|
226
|
+
--wf-color-border: #334155;
|
|
227
|
+
--wf-color-border-light: #1e293b;
|
|
228
|
+
--wf-color-border-dark: #475569;
|
|
229
|
+
|
|
230
|
+
--wf-surface-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
231
|
+
--wf-shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
|
|
232
|
+
--wf-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
233
|
+
--wf-shadow-md: 0 4px 12px rgba(0,0,0,0.4);
|
|
234
|
+
--wf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.4);
|
|
235
|
+
|
|
236
|
+
--wf-focus-ring: 0 0 0 2px var(--wf-color-primary-bg);
|
|
237
|
+
|
|
238
|
+
--wf-surface-bg: var(--wf-color-bg);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
169
242
|
/* weifuwu/theme — 基础元素默认样式 */
|
|
170
243
|
*,
|
|
171
244
|
*::before,
|
|
@@ -173,6 +246,11 @@
|
|
|
173
246
|
box-sizing: border-box;
|
|
174
247
|
}
|
|
175
248
|
|
|
249
|
+
:root {
|
|
250
|
+
/* 原生控件/滚动条跟随主题;暗色激活时在 _dark.css 中切为 dark */
|
|
251
|
+
color-scheme: light;
|
|
252
|
+
}
|
|
253
|
+
|
|
176
254
|
body {
|
|
177
255
|
margin: 0;
|
|
178
256
|
background: var(--wf-color-bg);
|
|
@@ -210,7 +288,7 @@ select {
|
|
|
210
288
|
color: var(--wf-color-text);
|
|
211
289
|
border: var(--wf-border-width) solid var(--wf-color-border);
|
|
212
290
|
border-radius: var(--wf-radius);
|
|
213
|
-
padding: var(--wf-
|
|
291
|
+
padding: var(--wf-control-pad-y) var(--wf-space);
|
|
214
292
|
}
|
|
215
293
|
|
|
216
294
|
input:focus,
|
|
@@ -235,7 +313,8 @@ button {
|
|
|
235
313
|
color: var(--wf-color-text);
|
|
236
314
|
border: var(--wf-border-width) solid var(--wf-color-border);
|
|
237
315
|
border-radius: var(--wf-radius);
|
|
238
|
-
padding: var(--wf-
|
|
316
|
+
padding: var(--wf-control-pad-y) var(--wf-space-md);
|
|
317
|
+
min-height: var(--wf-control-height);
|
|
239
318
|
cursor: pointer;
|
|
240
319
|
}
|
|
241
320
|
button:hover {
|
|
@@ -243,6 +322,31 @@ button:hover {
|
|
|
243
322
|
color: var(--wf-color-primary);
|
|
244
323
|
}
|
|
245
324
|
|
|
325
|
+
/* 键盘焦点:button 与 [tabindex] 统一 focus ring(仅键盘触发,鼠标点击不闪) */
|
|
326
|
+
button:focus-visible,
|
|
327
|
+
[tabindex]:focus-visible {
|
|
328
|
+
outline: none;
|
|
329
|
+
box-shadow: var(--wf-focus-ring);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* 触屏设备(coarse pointer):交互目标提升到 44px(Apple HIG / WCAG 2.5.8) */
|
|
333
|
+
@media (pointer: coarse) {
|
|
334
|
+
button,
|
|
335
|
+
[role="button"],
|
|
336
|
+
input,
|
|
337
|
+
textarea,
|
|
338
|
+
select,
|
|
339
|
+
.wf-card--clickable,
|
|
340
|
+
.wf-table-th--sortable,
|
|
341
|
+
.wf-accordion-summary,
|
|
342
|
+
.wf-tag-close,
|
|
343
|
+
.wf-checkbox,
|
|
344
|
+
.wf-switch,
|
|
345
|
+
.wf-radio {
|
|
346
|
+
min-height: 44px;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
246
350
|
h1, h2, h3, h4, h5, h6 {
|
|
247
351
|
font-weight: var(--wf-font-weight-semibold);
|
|
248
352
|
line-height: var(--wf-line-height-tight);
|
|
@@ -314,6 +418,18 @@ code {
|
|
|
314
418
|
border-radius: var(--wf-radius-sm);
|
|
315
419
|
}
|
|
316
420
|
|
|
421
|
+
/* ── 无障碍:尊重系统减弱动效偏好 ── */
|
|
422
|
+
@media (prefers-reduced-motion: reduce) {
|
|
423
|
+
*,
|
|
424
|
+
*::before,
|
|
425
|
+
*::after {
|
|
426
|
+
animation-duration: 0.01ms !important;
|
|
427
|
+
animation-iteration-count: 1 !important;
|
|
428
|
+
transition-duration: 0.01ms !important;
|
|
429
|
+
scroll-behavior: auto !important;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
317
433
|
/* stack — 纵向堆叠 */
|
|
318
434
|
.wf-stack {
|
|
319
435
|
display: flex;
|
|
@@ -558,7 +674,9 @@ code {
|
|
|
558
674
|
min-height: 0;
|
|
559
675
|
}
|
|
560
676
|
|
|
561
|
-
/*
|
|
677
|
+
/* 排版工具类 — 字号 / 色阶 / 字重 / 截断(零 CSS 用户自排版) */
|
|
678
|
+
|
|
679
|
+
/* ── text-align ── */
|
|
562
680
|
|
|
563
681
|
.wf-text-left {
|
|
564
682
|
text-align: left;
|
|
@@ -571,3 +689,50 @@ code {
|
|
|
571
689
|
.wf-text-right {
|
|
572
690
|
text-align: right;
|
|
573
691
|
}
|
|
692
|
+
|
|
693
|
+
/* ── 字号 ── */
|
|
694
|
+
|
|
695
|
+
.wf-text-xs { font-size: var(--wf-font-size-xs); }
|
|
696
|
+
.wf-text-sm { font-size: var(--wf-font-size-sm); }
|
|
697
|
+
.wf-text-base { font-size: var(--wf-font-size-base); }
|
|
698
|
+
.wf-text-lg { font-size: var(--wf-font-size-lg); }
|
|
699
|
+
.wf-text-xl { font-size: var(--wf-font-size-xl); }
|
|
700
|
+
.wf-text-2xl { font-size: var(--wf-font-size-2xl); }
|
|
701
|
+
.wf-text-3xl { font-size: var(--wf-font-size-3xl); }
|
|
702
|
+
.wf-text-4xl { font-size: var(--wf-font-size-4xl); }
|
|
703
|
+
.wf-text-5xl { font-size: var(--wf-font-size-5xl); }
|
|
704
|
+
|
|
705
|
+
/* ── 色阶 ── */
|
|
706
|
+
|
|
707
|
+
.wf-text-secondary { color: var(--wf-color-text-secondary); }
|
|
708
|
+
.wf-text-tertiary { color: var(--wf-color-text-tertiary); }
|
|
709
|
+
.wf-text-disabled { color: var(--wf-color-text-disabled); }
|
|
710
|
+
.wf-text-brand { color: var(--wf-color-primary); }
|
|
711
|
+
|
|
712
|
+
/* ── 字重 ── */
|
|
713
|
+
|
|
714
|
+
.wf-text-medium { font-weight: var(--wf-font-weight-medium); }
|
|
715
|
+
.wf-text-semibold { font-weight: var(--wf-font-weight-semibold); }
|
|
716
|
+
.wf-text-bold { font-weight: var(--wf-font-weight-bold); }
|
|
717
|
+
|
|
718
|
+
/* ── 截断 ── */
|
|
719
|
+
|
|
720
|
+
.wf-truncate {
|
|
721
|
+
overflow: hidden;
|
|
722
|
+
text-overflow: ellipsis;
|
|
723
|
+
white-space: nowrap;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.wf-line-clamp-2 {
|
|
727
|
+
display: -webkit-box;
|
|
728
|
+
-webkit-line-clamp: 2;
|
|
729
|
+
-webkit-box-orient: vertical;
|
|
730
|
+
overflow: hidden;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.wf-line-clamp-3 {
|
|
734
|
+
display: -webkit-box;
|
|
735
|
+
-webkit-line-clamp: 3;
|
|
736
|
+
-webkit-box-orient: vertical;
|
|
737
|
+
overflow: hidden;
|
|
738
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HtmlSafe — 标记"已是安全 HTML"的字符串(SSR 输出/unsafe 用户输入)。
|
|
3
|
+
*
|
|
4
|
+
* ctx.ui.html 的 stringify 对插值默认转义;instanceof HtmlSafe 的值原样内联。
|
|
5
|
+
* 与 ctx.ui.ssr 的产物共用同一类,保证模板内联不二次转义。
|
|
6
|
+
*/
|
|
7
|
+
export declare class HtmlSafe {
|
|
8
|
+
value: string;
|
|
9
|
+
constructor(value: string);
|
|
10
|
+
toString(): string;
|
|
11
|
+
}
|