@maiyunnet/kebab 9.14.2 → 9.15.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/sys/route.js CHANGED
@@ -41,6 +41,20 @@ function respond500(res) {
41
41
  res.end(content);
42
42
  }
43
43
  }
44
+ /**
45
+ * --- 安全写入外部输入键,避免 __proto__ 等特殊键改变对象原型 ---
46
+ * @param target 目标对象
47
+ * @param key 键
48
+ * @param value 值
49
+ */
50
+ function setInputValue(target, key, value) {
51
+ Object.defineProperty(target, key, {
52
+ 'configurable': true,
53
+ 'enumerable': true,
54
+ 'value': value,
55
+ 'writable': true,
56
+ });
57
+ }
44
58
  /**
45
59
  * --- 输出 404 错误响应 ---
46
60
  * @param res 响应对象
@@ -58,7 +72,7 @@ function respond404(res, config, path) {
58
72
  }
59
73
  return;
60
74
  }
61
- const content = '[Error] Controller not found, path: ' + path + '.';
75
+ const content = '[Error] Controller not found, path: ' + lText.htmlescape(path) + '.';
62
76
  if (!res.headersSent) {
63
77
  res.setHeader('content-type', 'text/html; charset=utf-8');
64
78
  res.setHeader('content-length', Buffer.byteLength(content));
@@ -245,17 +259,17 @@ export async function run(data) {
245
259
  const key = cookie.slice(0, eqIndex).trim();
246
260
  const rawVal = cookie.slice(eqIndex + 1);
247
261
  try {
248
- cookies[key] = decodeURIComponent(rawVal);
262
+ setInputValue(cookies, key, decodeURIComponent(rawVal));
249
263
  }
250
264
  catch {
251
- cookies[key] = rawVal;
265
+ setInputValue(cookies, key, rawVal);
252
266
  }
253
267
  }
254
268
  }
255
269
  // --- 处理 headers ---
256
270
  const headers = {};
257
271
  for (const key in data.req.headers) {
258
- headers[key.toLowerCase()] = data.req.headers[key];
272
+ setInputValue(headers, key.toLowerCase(), data.req.headers[key]);
259
273
  }
260
274
  headers['authorization'] ??= '';
261
275
  /** --- 开发者返回值 --- */
@@ -922,9 +936,9 @@ export function getFormData(req, events = {}, limits = {}) {
922
936
  resolve({ 'post': {}, 'files': {} });
923
937
  return;
924
938
  }
925
- /** --- boundary 位置 --- */
926
- const clio = ct.lastIndexOf('boundary=');
927
- if (clio === -1) {
939
+ /** --- 获取 boundary,兼容带引号及后续参数的标准写法 --- */
940
+ const boundaryMatch = /(?:^|;)\s*boundary\s*=\s*(?:"([^"]+)"|([^;\s]+))/i.exec(ct);
941
+ if (!boundaryMatch) {
928
942
  resolve({ 'post': {}, 'files': {} });
929
943
  return;
930
944
  }
@@ -934,7 +948,11 @@ export function getFormData(req, events = {}, limits = {}) {
934
948
  'files': {}
935
949
  };
936
950
  /** --- 获取的 boundary 文本 --- */
937
- const boundary = ct.slice(clio + 9);
951
+ const boundary = boundaryMatch[1] ?? boundaryMatch[2];
952
+ if (!boundary || (boundary.length > 200) || /[\r\n]/.test(boundary)) {
953
+ resolve(false);
954
+ return;
955
+ }
938
956
  // --- 超时护盾:防止网络静默断开时 Promise 永不 resolve ---
939
957
  /** --- 超时定时器 --- */
940
958
  let timer;
@@ -978,6 +996,10 @@ export function getFormData(req, events = {}, limits = {}) {
978
996
  let readEnd = false;
979
997
  /** --- 是否有文件被限制拒绝(整体返回 false) --- */
980
998
  let rejected = false;
999
+ /** --- 已接收的请求体字节数 --- */
1000
+ let totalSize = 0;
1001
+ /** --- 已解析的字段与文件数量 --- */
1002
+ let partCount = 0;
981
1003
  /** --- 清理 rtn.files 中所有已写入的临时文件 --- */
982
1004
  function cleanupFiles() {
983
1005
  for (const key in rtn.files) {
@@ -990,6 +1012,16 @@ export function getFormData(req, events = {}, limits = {}) {
990
1012
  }
991
1013
  }
992
1014
  }
1015
+ /** --- 清理仍在写入、尚未加入 rtn.files 的临时文件 --- */
1016
+ function cleanupActiveFile() {
1017
+ if ((state !== EState.FILE) || !ftmpName) {
1018
+ return;
1019
+ }
1020
+ ftmpStream.destroy();
1021
+ lFs.unlink(kebab.FTMP_CWD + ftmpName).catch(() => { });
1022
+ ftmpName = '';
1023
+ --writeFileLength;
1024
+ }
993
1025
  // --- 启动超时定时器(在所有变量声明之后,确保回调闭包可引用) ---
994
1026
  const timeoutMs = limits.timeout ?? 300_000;
995
1027
  if (timeoutMs > 0) {
@@ -998,9 +1030,7 @@ export function getFormData(req, events = {}, limits = {}) {
998
1030
  return;
999
1031
  }
1000
1032
  finished = true;
1001
- if ((state === EState.FILE) && ftmpName && ftmpStream) {
1002
- ftmpStream.destroy();
1003
- }
1033
+ cleanupActiveFile();
1004
1034
  lCore.debug('[ROUTE][GETFORMDATA] formdata request timeout');
1005
1035
  lCore.log({}, '[ROUTE][GETFORMDATA] formdata request timeout after ' + timeoutMs + 'ms', '-error');
1006
1036
  cleanupFiles();
@@ -1032,6 +1062,16 @@ export function getFormData(req, events = {}, limits = {}) {
1032
1062
  }
1033
1063
  // --- 开始读取 ---
1034
1064
  req.on('data', function (chunk) {
1065
+ if (finished || rejected) {
1066
+ return;
1067
+ }
1068
+ totalSize += chunk.length;
1069
+ if ((limits.maxTotalSize !== undefined) && (limits.maxTotalSize > 0) && (totalSize > limits.maxTotalSize)) {
1070
+ rejected = true;
1071
+ cleanupActiveFile();
1072
+ buffer = Buffer.from('');
1073
+ return;
1074
+ }
1035
1075
  buffer = Buffer.concat([buffer, chunk], buffer.length + chunk.length);
1036
1076
  while (true) {
1037
1077
  switch (state) {
@@ -1039,9 +1079,19 @@ export function getFormData(req, events = {}, limits = {}) {
1039
1079
  /** --- 中断符位置 --- */
1040
1080
  const io = buffer.indexOf('\r\n\r\n');
1041
1081
  if (io === -1) {
1082
+ if (buffer.length > (limits.maxHeaderSize ?? 16 * 1024)) {
1083
+ rejected = true;
1084
+ buffer = Buffer.from('');
1085
+ }
1042
1086
  return;
1043
1087
  }
1044
1088
  // --- 头部已经读取完毕 ---
1089
+ ++partCount;
1090
+ if (partCount > (limits.maxParts ?? 1000)) {
1091
+ rejected = true;
1092
+ buffer = Buffer.from('');
1093
+ return;
1094
+ }
1045
1095
  const head = buffer.subarray(0, io).toString();
1046
1096
  // --- 除头部外剩下的 buffer ---
1047
1097
  buffer = buffer.subarray(io + 4);
@@ -1084,8 +1134,20 @@ export function getFormData(req, events = {}, limits = {}) {
1084
1134
  date.getUTCDate().toString().padStart(2, '0') +
1085
1135
  date.getUTCHours().toString().padStart(2, '0') +
1086
1136
  date.getUTCMinutes().toString().padStart(2, '0') + '_' + lCore.random() + '.ftmp';
1087
- ftmpStream = lFs.createWriteStream(kebab.FTMP_CWD + ftmpName);
1088
- ftmpStream.on('error', () => { });
1137
+ const activeName = ftmpName;
1138
+ const activePath = kebab.FTMP_CWD + activeName;
1139
+ const activeStream = lFs.createWriteStream(activePath);
1140
+ ftmpStream = activeStream;
1141
+ activeStream.on('error', (error) => {
1142
+ rejected = true;
1143
+ lFs.unlink(activePath).catch(() => { });
1144
+ if (ftmpStream === activeStream) {
1145
+ ftmpName = '';
1146
+ }
1147
+ --writeFileLength;
1148
+ req.resume();
1149
+ lCore.log({}, `[ROUTE][GETFORMDATA] temporary file error: ${error.message}`, '-error');
1150
+ });
1089
1151
  ftmpSize = 0;
1090
1152
  }
1091
1153
  else {
@@ -1102,13 +1164,14 @@ export function getFormData(req, events = {}, limits = {}) {
1102
1164
  const maxField = limits.maxFieldSize ?? 1_048_576;
1103
1165
  if (buffer.length > maxField + boundary.length + 4) {
1104
1166
  rejected = true;
1167
+ buffer = Buffer.from('');
1105
1168
  return;
1106
1169
  }
1107
1170
  return;
1108
1171
  }
1109
1172
  // --- 找到结束标语,写入 POST ---
1110
1173
  const val = buffer.subarray(0, io).toString();
1111
- if (rtn.post[name]) {
1174
+ if (Object.hasOwn(rtn.post, name)) {
1112
1175
  if (Array.isArray(rtn.post[name])) {
1113
1176
  rtn.post[name].push(val);
1114
1177
  }
@@ -1117,7 +1180,7 @@ export function getFormData(req, events = {}, limits = {}) {
1117
1180
  }
1118
1181
  }
1119
1182
  else {
1120
- rtn.post[name] = val;
1183
+ setInputValue(rtn.post, name, val);
1121
1184
  }
1122
1185
  // --- 重置状态机 ---
1123
1186
  state = EState.WAIT;
@@ -1138,7 +1201,10 @@ export function getFormData(req, events = {}, limits = {}) {
1138
1201
  rejectFile();
1139
1202
  }
1140
1203
  else {
1141
- ftmpStream.write(writeBuffer);
1204
+ if (!ftmpStream.write(writeBuffer)) {
1205
+ req.pause();
1206
+ ftmpStream.once('drain', () => { req.resume(); });
1207
+ }
1142
1208
  ftmpSize += Buffer.byteLength(writeBuffer);
1143
1209
  }
1144
1210
  }
@@ -1188,7 +1254,7 @@ export function getFormData(req, events = {}, limits = {}) {
1188
1254
  'size': ftmpSize,
1189
1255
  'path': kebab.FTMP_CWD + ftmpName
1190
1256
  };
1191
- if (rtn.files[name]) {
1257
+ if (Object.hasOwn(rtn.files, name)) {
1192
1258
  if (Array.isArray(rtn.files[name])) {
1193
1259
  rtn.files[name].push(val);
1194
1260
  }
@@ -1197,7 +1263,7 @@ export function getFormData(req, events = {}, limits = {}) {
1197
1263
  }
1198
1264
  }
1199
1265
  else {
1200
- rtn.files[name] = val;
1266
+ setInputValue(rtn.files, name, val);
1201
1267
  }
1202
1268
  }
1203
1269
  }
@@ -1223,9 +1289,7 @@ export function getFormData(req, events = {}, limits = {}) {
1223
1289
  }
1224
1290
  finished = true;
1225
1291
  clearTimer();
1226
- if ((state === EState.FILE) && ftmpName) {
1227
- ftmpStream.destroy();
1228
- }
1292
+ cleanupActiveFile();
1229
1293
  lCore.debug('[ROUTE][GETFORMDATA] request error before getFormData: ' + e.message);
1230
1294
  lCore.log({}, '[ROUTE][GETFORMDATA] request error before getFormData: ' + (e.stack ?? ''), '-error');
1231
1295
  cleanupFiles();
@@ -1237,12 +1301,10 @@ export function getFormData(req, events = {}, limits = {}) {
1237
1301
  return;
1238
1302
  }
1239
1303
  // --- 若数据未读完且连接已关闭,视为传输中断(多数是用户主动取消,无需记录错误) ---
1240
- if (!readEnd && writeFileLength > 0) {
1304
+ if (!readEnd) {
1241
1305
  finished = true;
1242
1306
  clearTimer();
1243
- if ((state === EState.FILE) && ftmpName && ftmpStream) {
1244
- ftmpStream.destroy();
1245
- }
1307
+ cleanupActiveFile();
1246
1308
  lCore.debug('[ROUTE][GETFORMDATA] connection closed before formdata complete');
1247
1309
  cleanupFiles();
1248
1310
  resolve(false);
@@ -2386,7 +2386,7 @@ result: <pre>${lText.htmlescape(JSON.stringify(json2, null, 4))}</pre>`);
2386
2386
  }];
2387
2387
  }
2388
2388
  /** --- 测试 ProxyAgent --- */
2389
- _undiciProxyAgent = lUndici.getProxyAgent('http://192.168.0.10:20809');
2389
+ _undiciProxyAgent = lUndici.getProxyAgent('http://192.168.31.10:20809');
2390
2390
  async undiciProxy() {
2391
2391
  const echo = [];
2392
2392
  const res = await lUndici.get('https://www.google.com/', { 'reuse': this._undiciProxyAgent });
@@ -3685,7 +3685,8 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
3685
3685
  });</pre>`);
3686
3686
  if (imgResult && imgResult.list?.length) {
3687
3687
  for (const img of imgResult.list) {
3688
- echo.push(`<div><img src="${img.url.startsWith('http') ? img.url : 'data:image/png;base64,' + img.url}" style="max-width: 512px;" /></div>${lText.htmlescape(img.text)}`);
3688
+ const url = img.url.startsWith('http') ? img.url : 'data:image/png;base64,' + img.url;
3689
+ echo.push(`<div><img src="${lText.htmlescape(url)}" style="max-width: 512px;" /></div>${lText.htmlescape(img.text)}`);
3689
3690
  }
3690
3691
  echo.push('<br>request: ' + imgResult.request, ', seed: ' + imgResult.seed);
3691
3692
  }
@@ -3736,7 +3737,8 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
3736
3737
  <img src="https://img.alicdn.com/imgextra/i3/O1CN01SfG4J41UYn9WNt4X1_!!6000000002530-49-tps-1696-960.webp" style="max-width: 256px;" />
3737
3738
  </div>`);
3738
3739
  for (const img of imgResult.list) {
3739
- echo.push(`<div><img src="${img.url.startsWith('http') ? img.url : 'data:image/png;base64,' + img.url}" style="max-width: 512px;" /></div>${lText.htmlescape(img.text || prompt)}`);
3740
+ const url = img.url.startsWith('http') ? img.url : 'data:image/png;base64,' + img.url;
3741
+ echo.push(`<div><img src="${lText.htmlescape(url)}" style="max-width: 512px;" /></div>${lText.htmlescape(img.text || prompt)}`);
3740
3742
  }
3741
3743
  echo.push('<br>request: ' + imgResult.request, ', seed: ' + imgResult.seed);
3742
3744
  }
@@ -11,5 +11,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { useState } from 'react';
12
12
  export default function HelloPage({ _urlStc, _staticVer, greeting }) {
13
13
  const [count, setCount] = useState(0);
14
- return (_jsxs("html", { lang: "zh-CN", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: "Hello - Kebab" }), _jsx("link", { href: `${_urlStc}view/hello.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { children: _jsxs("div", { className: "p-8", children: [_jsx("h1", { className: "text-2xl font-bold mb-4", children: greeting }), _jsx("p", { className: "text-slate-500 mb-4", children: "\u70B9\u51FB\u6309\u94AE\u6D4B\u8BD5\u5BA2\u6237\u7AEF\u6C34\u5408\u662F\u5426\u6B63\u5E38\u5DE5\u4F5C\uFF1A" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => setCount(c => c - 1), className: "w-10 h-10 rounded-lg bg-slate-200 hover:bg-slate-300 font-bold text-xl cursor-pointer", children: "-" }), _jsx("span", { className: "w-10 text-center tabular-nums text-lg", children: count }), _jsx("button", { onClick: () => setCount(c => c + 1), className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }) })] }));
14
+ return (_jsxs("html", { lang: "zh-CN", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: "Hello - Kebab" }), _jsx("link", { href: `${_urlStc}view/hello.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { children: _jsxs("div", { className: "p-8", children: [_jsx("h1", { className: "text-2xl font-bold mb-4", children: greeting }), _jsx("p", { className: "text-slate-500 mb-4", children: "\u70B9\u51FB\u6309\u94AE\u6D4B\u8BD5\u5BA2\u6237\u7AEF\u6C34\u5408\u662F\u5426\u6B63\u5E38\u5DE5\u4F5C\uFF1A" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => { setCount(c => c - 1); }, className: "w-10 h-10 rounded-lg bg-slate-200 hover:bg-slate-300 font-bold text-xl cursor-pointer", children: "-" }), _jsx("span", { className: "w-10 text-center tabular-nums text-lg", children: count }), _jsx("button", { onClick: () => { setCount(c => c + 1); }, className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }) })] }));
15
15
  }
@@ -33,12 +33,12 @@ export default function HelloPage({ _urlStc, _staticVer, greeting }: IProps): Re
33
33
  <p className="text-slate-500 mb-4">点击按钮测试客户端水合是否正常工作:</p>
34
34
  <div className="flex items-center gap-4">
35
35
  <button
36
- onClick={() => setCount(c => c - 1)}
36
+ onClick={() => { setCount(c => c - 1); }}
37
37
  className="w-10 h-10 rounded-lg bg-slate-200 hover:bg-slate-300 font-bold text-xl cursor-pointer"
38
38
  >-</button>
39
39
  <span className="w-10 text-center tabular-nums text-lg">{count}</span>
40
40
  <button
41
- onClick={() => setCount(c => c + 1)}
41
+ onClick={() => { setCount(c => c + 1); }}
42
42
  className="w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer"
43
43
  >+</button>
44
44
  </div>
@@ -65,7 +65,7 @@ function PageHome({ serverTime, node }) {
65
65
  function PageAbout() {
66
66
  const navigate = useNavigate();
67
67
  const location = useLocation();
68
- return (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-slate-600 text-sm", children: ["About page: demonstrates ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "useNavigate()" }), " programmatic navigation."] }), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "flex gap-2 flex-wrap", children: [_jsx("button", { onClick: () => navigate('/'), className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors", children: "Back to Home" }), _jsx("button", { onClick: () => navigate('/user/42'), className: "inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium cursor-pointer transition-colors", children: "Go to User #42" })] })] }));
68
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-slate-600 text-sm", children: ["About page: demonstrates ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "useNavigate()" }), " programmatic navigation."] }), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "flex gap-2 flex-wrap", children: [_jsx("button", { onClick: () => { Promise.resolve(navigate('/')).catch(() => { }); }, className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors", children: "Back to Home" }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/user/42')).catch(() => { }); }, className: "inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium cursor-pointer transition-colors", children: "Go to User #42" })] })] }));
69
69
  }
70
70
  /** --- 用户列表页 --- */
71
71
  function PageUsers({ users: initialUsers, urlBase }) {
@@ -86,7 +86,7 @@ function PageUsers({ users: initialUsers, urlBase }) {
86
86
  }
87
87
  setLoading(false);
88
88
  })
89
- .catch(() => setLoading(false));
89
+ .catch(() => { setLoading(false); });
90
90
  }, []);
91
91
  return (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-slate-600 text-sm", children: "User list \u2014 click to view details (with nested /profile route)." }), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), loading && _jsx("p", { className: "text-slate-400 text-sm", children: "Loading..." }), users && (_jsx("ul", { className: "space-y-2", children: users.map(u => (_jsx("li", { className: "flex items-center gap-3", children: _jsxs(Link, { to: `/user/${u.id}`, className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 text-slate-700 text-xs font-medium transition-colors", children: [u.name, " (id=", u.id, ") \u2014 /user/", u.id] }) }, u.id))) }))] }));
92
92
  }
@@ -114,9 +114,9 @@ function PageUserDetail({ user: initialUser, urlBase }) {
114
114
  }
115
115
  setLoading(false);
116
116
  })
117
- .catch(() => setLoading(false));
117
+ .catch(() => { setLoading(false); });
118
118
  }, [id]);
119
- return (_jsxs("div", { className: "space-y-4", children: [loading && _jsx("p", { className: "text-slate-400 text-sm", children: "Loading..." }), user && (_jsxs("p", { className: "text-slate-700 text-sm font-medium", children: ["User Detail: ", _jsx("code", { className: "bg-slate-100 px-1.5 rounded font-mono", children: user.name }), "\u00A0", _jsxs("span", { className: "text-slate-400 text-xs", children: ["(id=\"", id, "\", email=", user.email, ")"] })] })), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "border border-dashed border-slate-300 rounded-lg p-4", children: [_jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Nested route ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id/profile" }), " (rendered via Outlet):"] }), _jsx(Outlet, {}), location.pathname === `/user/${id}` && (_jsx(Link, { to: `/user/${id}/profile`, className: "inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium transition-colors", children: "View Profile" }))] }), _jsx("button", { onClick: () => navigate('/user'), className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors", children: "Back to Users" })] }));
119
+ return (_jsxs("div", { className: "space-y-4", children: [loading && _jsx("p", { className: "text-slate-400 text-sm", children: "Loading..." }), user && (_jsxs("p", { className: "text-slate-700 text-sm font-medium", children: ["User Detail: ", _jsx("code", { className: "bg-slate-100 px-1.5 rounded font-mono", children: user.name }), "\u00A0", _jsxs("span", { className: "text-slate-400 text-xs", children: ["(id=\"", id, "\", email=", user.email, ")"] })] })), _jsxs("div", { className: "bg-slate-50 rounded-lg p-3 text-sm", children: [_jsx("div", { className: "text-slate-500 text-xs mb-1", children: "Current pathname" }), _jsx("div", { className: "font-mono text-slate-800", children: location.pathname })] }), _jsxs("div", { className: "border border-dashed border-slate-300 rounded-lg p-4", children: [_jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Nested route ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id/profile" }), " (rendered via Outlet):"] }), _jsx(Outlet, {}), location.pathname === `/user/${id ?? ''}` && (_jsx(Link, { to: `/user/${id ?? ''}/profile`, className: "inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium transition-colors", children: "View Profile" }))] }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/user')).catch(() => { }); }, className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors", children: "Back to Users" })] }));
120
120
  }
121
121
  /** --- 嵌套子路由:用户 Profile --- */
122
122
  function PageUserProfile() {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * --- Kebab React BrowserRouter 全页面示例组件 ---
3
- *
3
+ *
4
4
  * node ./source/main build -d source/www/example/stc
5
5
  *
6
6
  * 【特性】
@@ -54,7 +54,7 @@ interface IProps {
54
54
  // --- 基础控件 ---
55
55
 
56
56
  /** --- 卡片容器 --- */
57
- function Card({ children, className = '' }: { 'children': React.ReactNode; 'className'?: string }) {
57
+ function Card({ children, className = '' }: { 'children': React.ReactNode; 'className'?: string; }) {
58
58
  return (
59
59
  <div className={`bg-white rounded-xl shadow-sm border border-slate-200 p-6 ${className}`}>
60
60
  {children}
@@ -84,7 +84,7 @@ function Badge({ children, variant = 'default' }: {
84
84
  /** --- 顶部导航栏,NavLink 自动高亮当前路由 --- */
85
85
  function NavBar() {
86
86
  /** --- NavLink className 回调:激活时高亮 --- */
87
- const cls = ({ isActive }: { 'isActive': boolean }): string =>
87
+ const cls = ({ isActive }: { 'isActive': boolean; }): string =>
88
88
  isActive
89
89
  ? 'px-3 py-1.5 rounded-lg bg-blue-500 text-white text-sm font-medium transition-colors'
90
90
  : 'px-3 py-1.5 rounded-lg text-slate-600 hover:bg-slate-100 text-sm font-medium transition-colors';
@@ -100,7 +100,7 @@ function NavBar() {
100
100
  // --- 路由页面 ---
101
101
 
102
102
  /** --- 首页 --- */
103
- function PageHome({ serverTime, node }: { 'serverTime': string; 'node': string }) {
103
+ function PageHome({ serverTime, node }: { 'serverTime': string; 'node': string; }) {
104
104
  const location = useLocation();
105
105
  const [hydrated, setHydrated] = useState(false);
106
106
  useEffect(() => {
@@ -162,13 +162,13 @@ function PageAbout() {
162
162
  </div>
163
163
  <div className="flex gap-2 flex-wrap">
164
164
  <button
165
- onClick={() => navigate('/')}
165
+ onClick={() => { Promise.resolve(navigate('/')).catch(() => {}); }}
166
166
  className="inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors"
167
167
  >
168
168
  Back to Home
169
169
  </button>
170
170
  <button
171
- onClick={() => navigate('/user/42')}
171
+ onClick={() => { Promise.resolve(navigate('/user/42')).catch(() => {}); }}
172
172
  className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium cursor-pointer transition-colors"
173
173
  >
174
174
  Go to User #42
@@ -179,7 +179,7 @@ function PageAbout() {
179
179
  }
180
180
 
181
181
  /** --- 用户列表页 --- */
182
- function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBase': string }) {
182
+ function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBase': string; }) {
183
183
  const location = useLocation();
184
184
  const [users, setUsers] = useState(initialUsers);
185
185
  const [loading, setLoading] = useState(false);
@@ -197,7 +197,7 @@ function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBa
197
197
  }
198
198
  setLoading(false);
199
199
  })
200
- .catch(() => setLoading(false));
200
+ .catch(() => { setLoading(false); });
201
201
  }, []);
202
202
  return (
203
203
  <div className="space-y-4">
@@ -229,8 +229,8 @@ function PageUsers({ users: initialUsers, urlBase }: { 'users'?: IUser[]; 'urlBa
229
229
  * --- 用户详情页(含嵌套路由 Outlet) ---
230
230
  * 子路由 /profile 通过 <Outlet /> 渲染在此处
231
231
  */
232
- function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBase': string }) {
233
- const { id } = useParams<{ 'id': string }>();
232
+ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBase': string; }) {
233
+ const { id } = useParams<{ 'id': string; }>();
234
234
  const navigate = useNavigate();
235
235
  const location = useLocation();
236
236
  const [user, setUser] = useState<IUser | undefined>(
@@ -251,7 +251,7 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
251
251
  }
252
252
  setLoading(false);
253
253
  })
254
- .catch(() => setLoading(false));
254
+ .catch(() => { setLoading(false); });
255
255
  }, [id]);
256
256
  return (
257
257
  <div className="space-y-4">
@@ -272,9 +272,9 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
272
272
  Nested route <code className="bg-slate-100 px-1 rounded">/user/:id/profile</code> (rendered via Outlet):
273
273
  </p>
274
274
  <Outlet />
275
- {location.pathname === `/user/${id}` && (
275
+ {location.pathname === `/user/${id ?? ''}` && (
276
276
  <Link
277
- to={`/user/${id}/profile`}
277
+ to={`/user/${id ?? ''}/profile`}
278
278
  className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium transition-colors"
279
279
  >
280
280
  View Profile
@@ -282,7 +282,7 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
282
282
  )}
283
283
  </div>
284
284
  <button
285
- onClick={() => navigate('/user')}
285
+ onClick={() => { Promise.resolve(navigate('/user')).catch(() => {}); }}
286
286
  className="inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer transition-colors"
287
287
  >
288
288
  Back to Users
@@ -293,7 +293,7 @@ function PageUserDetail({ user: initialUser, urlBase }: { 'user'?: IUser; 'urlBa
293
293
 
294
294
  /** --- 嵌套子路由:用户 Profile --- */
295
295
  function PageUserProfile() {
296
- const { id } = useParams<{ 'id': string }>();
296
+ const { id } = useParams<{ 'id': string; }>();
297
297
  const location = useLocation();
298
298
  return (
299
299
  <div className="bg-blue-50 rounded-lg p-3 mb-3 space-y-2 text-sm">
@@ -335,7 +335,6 @@ export default function ReactRouterPage({
335
335
  <head>
336
336
  <meta charSet="UTF-8" />
337
337
  <meta name="viewport" content="width=device-width, initial-scale=1" />
338
- {/* eslint-disable-next-line @typescript-eslint/naming-convention */}
339
338
  <title suppressHydrationWarning>{title}</title>
340
339
  {/* --- import map 由框架自动注入在此标签前,无需手动添加 --- */}
341
340
  {/* --- dev: 需先执行 node ./source/main build -d source/www/example/stc 生成 CSS --- */}
@@ -73,13 +73,13 @@ function RouterHome() {
73
73
  /** --- 路由 About 页 --- */
74
74
  function RouterAbout() {
75
75
  const navigate = useNavigate();
76
- return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-3", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/about" })] }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "useNavigate() programmatic navigation (without Link component):" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => navigate('/'), className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer", children: "\u2190 Back to /" }), _jsx("button", { onClick: () => navigate('/user/99'), className: "inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium cursor-pointer", children: "\u2192 /user/99" })] })] }));
76
+ return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-3", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/about" })] }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "useNavigate() programmatic navigation (without Link component):" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => { Promise.resolve(navigate('/')).catch(() => { }); }, className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer", children: "\u2190 Back to /" }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/user/99')).catch(() => { }); }, className: "inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium cursor-pointer", children: "\u2192 /user/99" })] })] }));
77
77
  }
78
78
  /** --- 路由 User 动态参数页 --- */
79
79
  function RouterUser() {
80
80
  const { id } = useParams();
81
81
  const navigate = useNavigate();
82
- return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-1", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id" })] }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["useParams() dynamic segment: ", _jsxs("code", { className: "bg-slate-100 px-1 rounded", children: ["id = \"", id, "\""] })] }), _jsx("button", { onClick: () => navigate('/'), className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer", children: "\u2190 Back to /" })] }));
82
+ return (_jsxs("div", { children: [_jsxs("p", { className: "text-slate-700 text-sm font-medium mb-1", children: ["Current route: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "/user/:id" })] }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["useParams() dynamic segment: ", _jsxs("code", { className: "bg-slate-100 px-1 rounded", children: ["id = \"", id, "\""] })] }), _jsx("button", { onClick: () => { Promise.resolve(navigate('/')).catch(() => { }); }, className: "inline-flex items-center px-3 py-1.5 rounded-lg border border-slate-300 hover:bg-slate-50 text-slate-700 text-xs font-medium cursor-pointer", children: "\u2190 Back to /" })] }));
83
83
  }
84
84
  // ─── shadcn/ui 演示区 ────────────────────────────────────────────────────────
85
85
  /**
@@ -102,7 +102,7 @@ function ShadcnDemo() {
102
102
  e.preventDefault();
103
103
  setSubmitted(true);
104
104
  }
105
- return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "shadcn/ui Components" }), _jsxs("p", { className: "text-slate-500 text-xs leading-relaxed", children: ["All components below are imported from ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "stc/lib/ui/" }), ", following the same ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " structure recommended by shadcn/ui. Built on Radix UI primitives, styled with Tailwind CSS. In dev mode, the framework auto-scans all bare imports and generates the import map automatically."] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Label + Input" }), _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: nameId, children: "Username" }), _jsx(Input, { id: nameId, type: "text", placeholder: "Enter your username", value: name, onChange: e => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: emailId, children: "Email" }), _jsx(Input, { id: emailId, type: "email", placeholder: "you@example.com", value: email, onChange: e => setEmail(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-slate-400 mt-3", children: "Click the Label text to focus the corresponding input (Radix UI a11y semantics)" })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Checkbox" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: agreeId, checked: agree, onCheckedChange: setAgree }), _jsx(Label, { htmlFor: agreeId, children: "I have read and agree to the Terms of Service" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: String(agree) }), "\u00A0(supports ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "true | false | 'indeterminate'" }), " tri-state)"] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Switch" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: newsletterId, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: newsletterId, children: "Subscribe to product updates" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: newsletter ? 'true' : 'false' })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Combined Form (Submit Demo)" }), submitted ? (_jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("p", { className: "text-green-700 font-medium", children: "\u2713 Submitted! Received data:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-700 overflow-auto", children: JSON.stringify({ name, email, agree: Boolean(agree), newsletter }, null, 2) }), _jsx("button", { onClick: () => setSubmitted(false), className: "text-xs text-blue-500 hover:underline cursor-pointer", children: "Reset" })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${nameId}-form`, children: ["Username ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${nameId}-form`, required: true, placeholder: "At least 2 characters", value: name, onChange: e => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${emailId}-form`, children: ["Email ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${emailId}-form`, type: "email", required: true, placeholder: "you@example.com", value: email, onChange: e => setEmail(e.target.value) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `${agreeId}-form`, checked: agree, onCheckedChange: setAgree, required: true }), _jsx(Label, { htmlFor: `${agreeId}-form`, children: "Agree to terms" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `${newsletterId}-form`, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: `${newsletterId}-form`, children: "Subscribe" })] }), _jsx("button", { type: "submit", className: "inline-flex items-center px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium cursor-pointer", children: "Submit" })] }))] })] }));
105
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "shadcn/ui Components" }), _jsxs("p", { className: "text-slate-500 text-xs leading-relaxed", children: ["All components below are imported from ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "stc/lib/ui/" }), ", following the same ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " structure recommended by shadcn/ui. Built on Radix UI primitives, styled with Tailwind CSS. In dev mode, the framework auto-scans all bare imports and generates the import map automatically."] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Label + Input" }), _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: nameId, children: "Username" }), _jsx(Input, { id: nameId, type: "text", placeholder: "Enter your username", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: emailId, children: "Email" }), _jsx(Input, { id: emailId, type: "email", placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] })] }), _jsx("p", { className: "text-xs text-slate-400 mt-3", children: "Click the Label text to focus the corresponding input (Radix UI a11y semantics)" })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Checkbox" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: agreeId, checked: agree, onCheckedChange: setAgree }), _jsx(Label, { htmlFor: agreeId, children: "I have read and agree to the Terms of Service" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: String(agree) }), "\u00A0(supports ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "true | false | 'indeterminate'" }), " tri-state)"] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Switch" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: newsletterId, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: newsletterId, children: "Subscribe to product updates" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: newsletter ? 'true' : 'false' })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Combined Form (Submit Demo)" }), submitted ? (_jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("p", { className: "text-green-700 font-medium", children: "\u2713 Submitted! Received data:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-700 overflow-auto", children: JSON.stringify({ name, email, agree: Boolean(agree), newsletter }, null, 2) }), _jsx("button", { onClick: () => { setSubmitted(false); }, className: "text-xs text-blue-500 hover:underline cursor-pointer", children: "Reset" })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${nameId}-form`, children: ["Username ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${nameId}-form`, required: true, placeholder: "At least 2 characters", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${emailId}-form`, children: ["Email ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${emailId}-form`, type: "email", required: true, placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `${agreeId}-form`, checked: agree, onCheckedChange: setAgree, required: true }), _jsx(Label, { htmlFor: `${agreeId}-form`, children: "Agree to terms" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `${newsletterId}-form`, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: `${newsletterId}-form`, children: "Subscribe" })] }), _jsx("button", { type: "submit", className: "inline-flex items-center px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium cursor-pointer", children: "Submit" })] }))] })] }));
106
106
  }
107
107
  // ─── 页面主组件 ────────────────────────────────────────────────────────────────
108
108
  /** --- Kebab React 全页面演示 --- */
@@ -136,10 +136,10 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
136
136
  setIsFetching(false);
137
137
  });
138
138
  }
139
- return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("link", { href: `${_urlStc}view/react.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { className: "bg-slate-50 min-h-screen font-sans", children: _jsxs("div", { className: "max-w-3xl mx-auto px-4 py-10 space-y-6", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex gap-2 mb-2", children: [_jsx(Badge, { children: "SSR \u00B7 Kebab" }), _jsx(Badge, { variant: hydrated ? 'success' : 'warn', children: hydrated ? 'Hydrated ✓' : 'Rendering...' })] }), _jsx("h1", { className: "text-2xl font-bold text-slate-900", children: title }), _jsxs("p", { className: "text-slate-500 text-sm mt-1", children: [serverTime, " \u00B7 ", node] })] }), _jsx("div", { className: "flex gap-1 bg-slate-100 p-1 rounded-lg w-fit", children: ['overview', 'routing', 'fetch', 'shadcn'].map(t => (_jsx("button", { onClick: () => setTab(t), className: [
139
+ return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), _jsx("title", { children: title }), _jsx("link", { href: `${_urlStc}view/react.page.css?v=${_staticVer}`, rel: "stylesheet" })] }), _jsx("body", { className: "bg-slate-50 min-h-screen font-sans", children: _jsxs("div", { className: "max-w-3xl mx-auto px-4 py-10 space-y-6", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex gap-2 mb-2", children: [_jsx(Badge, { children: "SSR \u00B7 Kebab" }), _jsx(Badge, { variant: hydrated ? 'success' : 'warn', children: hydrated ? 'Hydrated ✓' : 'Rendering...' })] }), _jsx("h1", { className: "text-2xl font-bold text-slate-900", children: title }), _jsxs("p", { className: "text-slate-500 text-sm mt-1", children: [serverTime, " \u00B7 ", node] })] }), _jsx("div", { className: "flex gap-1 bg-slate-100 p-1 rounded-lg w-fit", children: ['overview', 'routing', 'fetch', 'shadcn'].map(t => (_jsx("button", { onClick: () => { setTab(t); }, className: [
140
140
  'px-4 py-2 rounded-md text-sm font-medium transition-colors cursor-pointer',
141
141
  tab === t ? 'bg-white shadow text-slate-900' : 'text-slate-500 hover:text-slate-900',
142
- ].join(' '), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))) }), tab === 'overview' && (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "Counter\uFF08useState + hydration\uFF09" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "SSR renders with initial value 0; props are serialized as inline JSON. After hydration the state becomes interactive \u2014 click the buttons to test:" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => setCount(c => c - 1), className: "w-10 h-10 rounded-lg border border-slate-300 hover:bg-slate-50 font-bold text-slate-700 text-xl cursor-pointer", children: "\u2212" }), _jsx("span", { className: "text-3xl font-bold text-slate-900 w-10 text-center tabular-nums", children: count }), _jsx("button", { onClick: () => setCount(c => c + 1), className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Server Props (from Ctr method)" }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Passed via", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "_loadReactPage(path, props)" }), ". The framework auto-injects constants like _urlBase and serializes all props as inline JSON, reused directly during client hydration \u2014 no extra request needed."] }), _jsx("div", { className: "space-y-2", children: [
142
+ ].join(' '), children: t.charAt(0).toUpperCase() + t.slice(1) }, t))) }), tab === 'overview' && (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "Counter\uFF08useState + hydration\uFF09" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "SSR renders with initial value 0; props are serialized as inline JSON. After hydration the state becomes interactive \u2014 click the buttons to test:" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("button", { onClick: () => { setCount(c => c - 1); }, className: "w-10 h-10 rounded-lg border border-slate-300 hover:bg-slate-50 font-bold text-slate-700 text-xl cursor-pointer", children: "\u2212" }), _jsx("span", { className: "text-3xl font-bold text-slate-900 w-10 text-center tabular-nums", children: count }), _jsx("button", { onClick: () => { setCount(c => c + 1); }, className: "w-10 h-10 rounded-lg bg-blue-500 hover:bg-blue-600 text-white font-bold text-xl cursor-pointer", children: "+" })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Server Props (from Ctr method)" }), _jsxs("p", { className: "text-slate-500 text-xs mb-3", children: ["Passed via", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "_loadReactPage(path, props)" }), ". The framework auto-injects constants like _urlBase and serializes all props as inline JSON, reused directly during client hydration \u2014 no extra request needed."] }), _jsx("div", { className: "space-y-2", children: [
143
143
  ['serverTime', serverTime],
144
144
  ['node', node],
145
145
  ['_urlBase', _urlBase],
@@ -153,7 +153,7 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
153
153
  checkbox.tsx # shadcn Checkbox
154
154
  switch.tsx # shadcn Switch
155
155
  view/
156
- react-page.tsx # Page component (import + compose)` }), _jsxs("p", { className: "text-slate-500 text-xs mt-3", children: ["Mirrors the shadcn/ui ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " convention. In dev mode, the framework auto-scans imports \u2014 no manual import map configuration needed."] })] })] })), tab === 'routing' && (_jsxs(MemoryRouter, { children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "React Router Demo" }), _jsxs("p", { className: "text-slate-500 text-xs mb-4", children: ["Uses ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "MemoryRouter" }), ' ', "to demonstrate route navigation, dynamic params (useParams), and programmatic navigation (useNavigate). Works on both server SSR and client hydration with no extra server configuration."] }), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(RouterHome, {}) }), _jsx(Route, { path: "/about", element: _jsx(RouterAbout, {}) }), _jsx(Route, { path: "/user/:id", element: _jsx(RouterUser, {}) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "Full-Page BrowserRouter Setup" }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "To let React Router manage the real URL (e.g. /app, /app/about), replace MemoryRouter with BrowserRouter and route all sub-paths to the same Ctr method on the server:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: `// 1. route.json: 将所有子路径路由到同一 Ctr 方法
156
+ react-page.tsx # Page component (import + compose)` }), _jsxs("p", { className: "text-slate-500 text-xs mt-3", children: ["Mirrors the shadcn/ui", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " convention. In dev mode, the framework auto-scans imports \u2014 no manual import map configuration needed."] })] })] })), tab === 'routing' && (_jsxs(MemoryRouter, { children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "React Router Demo" }), _jsxs("p", { className: "text-slate-500 text-xs mb-4", children: ["Uses ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "MemoryRouter" }), ' ', "to demonstrate route navigation, dynamic params (useParams), and programmatic navigation (useNavigate). Works on both server SSR and client hydration with no extra server configuration."] }), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(RouterHome, {}) }), _jsx(Route, { path: "/about", element: _jsx(RouterAbout, {}) }), _jsx(Route, { path: "/user/:id", element: _jsx(RouterUser, {}) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "Full-Page BrowserRouter Setup" }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "To let React Router manage the real URL (e.g. /app, /app/about), replace MemoryRouter with BrowserRouter and route all sub-paths to the same Ctr method on the server:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: `// 1. route.json: 将所有子路径路由到同一 Ctr 方法
157
157
  {
158
158
  "app": "ctr/app@reactPage",
159
159
  "app\\/.*": "ctr/app@reactPage"
@@ -169,5 +169,5 @@ await this._loadReactPage('view/my', { ...props }, {
169
169
  <Routes>
170
170
  <Route path="/" element={<Home />} />
171
171
  <Route path="/user/:id" element={<User />} />
172
- </Routes>` })] })] })), tab === 'shadcn' && (_jsx(ShadcnDemo, {})), tab === 'fetch' && (_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Client Fetch Demo" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "After hydration, click a button to send a GET request. setState triggers a partial re-render \u2014 no page refresh." }), _jsxs("div", { className: "flex gap-3 flex-wrap", children: [_jsx(Btn, { onClick: () => doFetch(`${_urlBase}test/json?type=4`), disabled: isFetching, children: isFetching ? 'Fetching...' : 'GET /test/json?type=4' }), _jsx(Btn, { outline: true, onClick: () => doFetch(`${_urlBase}test/json?type=2`), disabled: isFetching, children: "GET type=2 (error resp)" })] }), fetchResult !== null ? (_jsx("pre", { className: "mt-4 bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: fetchResult })) : (_jsx("p", { className: "mt-3 text-slate-400 text-xs", children: "Click a button above to see the response" }))] }))] }) })] }));
172
+ </Routes>` })] })] })), tab === 'shadcn' && (_jsx(ShadcnDemo, {})), tab === 'fetch' && (_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-3", children: "Client Fetch Demo" }), _jsx("p", { className: "text-slate-500 text-xs mb-4", children: "After hydration, click a button to send a GET request. setState triggers a partial re-render \u2014 no page refresh." }), _jsxs("div", { className: "flex gap-3 flex-wrap", children: [_jsx(Btn, { onClick: () => { doFetch(`${_urlBase}test/json?type=4`); }, disabled: isFetching, children: isFetching ? 'Fetching...' : 'GET /test/json?type=4' }), _jsx(Btn, { outline: true, onClick: () => { doFetch(`${_urlBase}test/json?type=2`); }, disabled: isFetching, children: "GET type=2 (error resp)" })] }), fetchResult !== null ? (_jsx("pre", { className: "mt-4 bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: fetchResult })) : (_jsx("p", { className: "mt-3 text-slate-400 text-xs", children: "Click a button above to see the response" }))] }))] }) })] }));
173
173
  }