dsh-life-pack 0.2.6 → 0.2.7

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/dist/client.js CHANGED
@@ -494,17 +494,40 @@ window.__ModuleLoader__.load({
494
494
  marginBottom: 8,
495
495
  borderRadius: 10,
496
496
  border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))",
497
+ borderLeftWidth: 3,
497
498
  fontSize: 13,
498
499
  lineHeight: 1.7
499
500
  },
500
501
  rowHead: {
501
- fontWeight: 700,
502
+ display: "flex",
503
+ alignItems: "baseline",
504
+ gap: 6,
502
505
  marginBottom: 2
503
506
  },
507
+ rowTitle: {
508
+ fontWeight: 700,
509
+ color: "var(--dsw-alias-label-primary, inherit)"
510
+ },
511
+ rowPkg: {
512
+ color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
513
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
514
+ fontSize: 12
515
+ },
504
516
  rowMeta: {
505
517
  color: "var(--dsw-alias-label-secondary, #9a9a9a)",
506
518
  fontSize: 12
507
519
  },
520
+ /** 版本号用等宽字:数字要能对齐着读(「当前」「最新」两列一眼比大小)。 */
521
+ ver: {
522
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
523
+ color: "var(--dsw-alias-label-primary, inherit)",
524
+ fontWeight: 600
525
+ },
526
+ /** 结论那一行:**只有要动的那几档上色**(绿/灰不抢注意力,照 health-view 的三档口径)。 */
527
+ verdictLine: {
528
+ fontSize: 12.5,
529
+ marginTop: 2
530
+ },
508
531
  reason: {
509
532
  color: "var(--dsw-alias-state-error-primary, #ff6b6b)",
510
533
  fontSize: 12,
@@ -513,7 +536,26 @@ window.__ModuleLoader__.load({
513
536
  skill: {
514
537
  color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
515
538
  fontSize: 12,
516
- marginTop: 4
539
+ marginTop: 4,
540
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace"
541
+ },
542
+ /** 行内主按钮(装上/装上更新):品牌底色 —— 全屏只有「能点的」这一种是这个颜色。 */
543
+ btnPrimary: {
544
+ border: "1px solid transparent",
545
+ background: "var(--dsw-alias-brand-primary, #0a84ff)",
546
+ color: "#fff",
547
+ borderRadius: 8,
548
+ padding: "5px 14px",
549
+ fontSize: 13,
550
+ fontWeight: 700,
551
+ cursor: "pointer",
552
+ marginTop: 6
553
+ },
554
+ /** 分档标记:**形状是第二条读数**(灰度截图、色弱视角下也分得开哪一行要动)。 */
555
+ mark: {
556
+ fontWeight: 700,
557
+ fontSize: 13,
558
+ lineHeight: 1
517
559
  },
518
560
  overlay: {
519
561
  position: "fixed",
@@ -525,12 +567,14 @@ window.__ModuleLoader__.load({
525
567
  padding: 16,
526
568
  background: "rgba(0,0,0,.45)"
527
569
  },
570
+ /** 浮层是**竖排两段**:题头固定、结果列表自己滚(题头跟着滚会把关闭键滚出视野 —— 用户实测反馈)。 */
528
571
  dialog: {
529
572
  width: "100%",
530
573
  maxWidth: 560,
531
574
  maxHeight: "80vh",
532
- overflow: "auto",
533
- padding: "12px 14px",
575
+ display: "flex",
576
+ flexDirection: "column",
577
+ overflow: "hidden",
534
578
  borderRadius: 12,
535
579
  border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.4))",
536
580
  background: "var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #222))",
@@ -542,12 +586,21 @@ window.__ModuleLoader__.load({
542
586
  alignItems: "center",
543
587
  justifyContent: "space-between",
544
588
  gap: 8,
545
- marginBottom: 8
589
+ flex: "0 0 auto",
590
+ padding: "12px 14px",
591
+ borderBottom: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))"
546
592
  },
547
593
  dialogTitle: {
548
594
  fontSize: 14,
549
595
  fontWeight: 700
550
596
  },
597
+ /** 唯一会滚的那一段。 */
598
+ dialogBody: {
599
+ flex: "1 1 auto",
600
+ minHeight: 0,
601
+ overflowY: "auto",
602
+ padding: "12px 14px"
603
+ },
551
604
  close: {
552
605
  border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
553
606
  background: "transparent",
@@ -565,6 +618,45 @@ window.__ModuleLoader__.load({
565
618
  marginTop: 4
566
619
  }
567
620
  };
621
+ const ROW_TONE = {
622
+ update: {
623
+ mark: "↑",
624
+ color: "var(--dsw-alias-brand-primary, #0a84ff)",
625
+ tint: "rgba(10,132,255,.08)"
626
+ },
627
+ blocked: {
628
+ mark: "!",
629
+ color: "var(--dsw-alias-state-warning-primary, #d8a300)",
630
+ tint: "rgba(216,163,0,.10)"
631
+ },
632
+ failed: {
633
+ mark: "✕",
634
+ color: "var(--dsw-alias-state-error-primary, #ff6b6b)",
635
+ tint: "rgba(255,107,107,.10)"
636
+ },
637
+ ok: {
638
+ mark: "✓",
639
+ color: "var(--dsw-alias-state-success-primary, #4ec9a0)",
640
+ tint: "transparent"
641
+ },
642
+ idle: {
643
+ mark: "·",
644
+ color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
645
+ tint: "transparent"
646
+ }
647
+ };
648
+ /** 这一行算哪一档:取数失败 > 装不上 > 有新版 > 待重启/源码装 > 已是最新 > 未查。 */
649
+ function rowToneOf(row, verdict) {
650
+ if (row.failure !== null || row.phase === "failed") return "failed";
651
+ if (row.phase === "installing") return "update";
652
+ if (verdict === null) return "idle";
653
+ switch (verdict.kind) {
654
+ case "update-available": return "update";
655
+ case "blocked": return "blocked";
656
+ case "up-to-date": return "ok";
657
+ default: return "idle";
658
+ }
659
+ }
568
660
  const IDLE$1 = {
569
661
  phase: "idle",
570
662
  outcome: null,
@@ -749,18 +841,43 @@ window.__ModuleLoader__.load({
749
841
  const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
750
842
  const busy = row.phase === "installing";
751
843
  const showManual = manual !== null && (row.phase === "failed" || verdict?.kind === "blocked");
844
+ const tone = rowToneOf(row, verdict);
845
+ const paint = ROW_TONE[tone];
846
+ /** 这一行「当前/最新」两个版本号:不等才算有新版,把「最新」那格上色(用户扫一眼先看有没有箭头)。 */
847
+ const running = snapshot?.runningVersion ?? target.runningVersion ?? null;
848
+ const latest = snapshot?.latestVersion ?? null;
849
+ const newer = running !== null && latest !== null && latest !== running;
850
+ const lit = tone === "update" || tone === "blocked" || tone === "failed";
752
851
  return react.createElement("div", {
753
852
  key: target.key,
754
- style: PANEL_STYLE.row
755
- }, react.createElement("div", { style: PANEL_STYLE.rowHead }, target.title + " · " + target.packageName), react.createElement("div", { style: PANEL_STYLE.rowMeta }, "当前 " + String(snapshot?.runningVersion ?? target.runningVersion ?? "未安装") + " · 最新 " + String(snapshot?.latestVersion ?? "未查")), react.createElement("div", null, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), verdict?.action ? react.createElement("button", {
853
+ style: {
854
+ ...PANEL_STYLE.row,
855
+ borderLeftColor: paint.color,
856
+ background: paint.tint
857
+ }
858
+ }, react.createElement("div", { style: PANEL_STYLE.rowHead }, react.createElement("span", {
859
+ style: {
860
+ ...PANEL_STYLE.mark,
861
+ color: paint.color
862
+ },
863
+ "aria-hidden": "true"
864
+ }, paint.mark), react.createElement("span", { style: PANEL_STYLE.rowTitle }, target.title), react.createElement("span", { style: PANEL_STYLE.rowPkg }, "· " + target.packageName)), react.createElement("div", { style: PANEL_STYLE.rowMeta }, "当前 ", react.createElement("b", { style: PANEL_STYLE.ver }, String(running ?? "未安装")), " · 最新 ", react.createElement("b", { style: newer ? {
865
+ ...PANEL_STYLE.ver,
866
+ color: paint.color
867
+ } : PANEL_STYLE.ver }, String(latest ?? "未查"))), react.createElement("div", { style: lit ? {
868
+ ...PANEL_STYLE.verdictLine,
869
+ color: paint.color,
870
+ fontWeight: 600
871
+ } : {
872
+ ...PANEL_STYLE.verdictLine,
873
+ color: "var(--dsw-alias-label-secondary, #9a9a9a)"
874
+ } }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), verdict?.action ? react.createElement("button", {
756
875
  type: "button",
757
876
  style: busy ? {
758
- ...PANEL_STYLE.btn,
759
- opacity: .55
760
- } : {
761
- ...PANEL_STYLE.btn,
762
- marginTop: 6
763
- },
877
+ ...PANEL_STYLE.btnPrimary,
878
+ opacity: .55,
879
+ cursor: "default"
880
+ } : PANEL_STYLE.btnPrimary,
764
881
  disabled: busy,
765
882
  onClick: () => props.face.act(target)
766
883
  }, verdict.action === "install" ? "装上" : "装上更新") : null, row.failure ? react.createElement("div", { style: PANEL_STYLE.reason }, "装不上:" + row.failure.message) : null, versionLines(target, snapshot).map((line, index) => react.createElement("div", {
@@ -782,13 +899,13 @@ window.__ModuleLoader__.load({
782
899
  onKeyDown: (event) => {
783
900
  if (event.key === "Escape") close();
784
901
  }
785
- }, react.createElement("div", { style: PANEL_STYLE.dialogHead }, react.createElement("div", { style: PANEL_STYLE.dialogTitle }, "检查更新(七家)"), react.createElement("button", {
902
+ }, react.createElement("div", { style: PANEL_STYLE.dialogHead }, react.createElement("div", { style: PANEL_STYLE.dialogTitle }, "检查更新"), react.createElement("button", {
786
903
  type: "button",
787
904
  style: PANEL_STYLE.close,
788
905
  onClick: () => close(),
789
906
  title: "关闭",
790
907
  "aria-label": "关闭"
791
- }, "×")), body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。"))) : null);
908
+ }, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。")))) : null);
792
909
  }
793
910
  /** 缺席卡:**按两处独立事实分三态**,每态只给能改变状态的动作。
794
911
  *
@@ -1070,45 +1187,40 @@ window.__ModuleLoader__.load({
1070
1187
  fontWeight: 700,
1071
1188
  color: INK
1072
1189
  },
1073
- /** 「体检一次」不挤在标题右端:它单独占一行、与标题同一起始边(视觉复评两轮点「孤立在右上缺层次」)。 */
1074
- runRow: { margin: "8px 0 10px" },
1075
- btn: {
1076
- border: "1px solid " + BORDER,
1077
- background: "transparent",
1078
- color: INK,
1079
- borderRadius: 8,
1080
- padding: "6px 16px",
1081
- fontSize: 13,
1082
- fontWeight: 600,
1083
- cursor: "pointer"
1084
- },
1085
- lightsRow: {
1190
+ /** 顶部那一行汇总(票 #732):一行装下「配置体检 + 一句总账 + 体检按钮」,三样都不独占行。
1191
+ * 它替掉了原先「标题行 + 单独一行的按钮 + 一排灯按钮 + 两行图例」(同屏实测 153px → 一行)。 */
1192
+ summaryRow: {
1086
1193
  display: "flex",
1087
- flexWrap: "wrap",
1088
- gap: 8
1089
- },
1090
- light: {
1091
- display: "inline-flex",
1092
1194
  alignItems: "center",
1093
- gap: 4,
1094
- background: "transparent",
1095
- border: "1px solid " + BORDER,
1096
- borderRadius: 999,
1097
- padding: "3px 10px",
1098
- fontSize: 12.5,
1099
- color: INK,
1100
- cursor: "pointer"
1195
+ gap: 10,
1196
+ marginBottom: 10
1101
1197
  },
1102
- lightIdle: {
1103
- cursor: "default",
1104
- color: INK_DIM
1198
+ summaryText: {
1199
+ flex: "1 1 auto",
1200
+ minWidth: 0,
1201
+ fontSize: 12.5,
1202
+ whiteSpace: "nowrap",
1203
+ overflow: "hidden",
1204
+ textOverflow: "ellipsis"
1105
1205
  },
1206
+ /** 各家那张表里的说明行(「还没体检:点上面那个…」那几句)。 */
1106
1207
  meta: {
1107
1208
  color: INK_DIM,
1108
1209
  fontSize: 12,
1109
1210
  lineHeight: 1.7,
1110
1211
  marginTop: 12
1111
1212
  },
1213
+ btn: {
1214
+ border: "1px solid " + BORDER,
1215
+ background: "transparent",
1216
+ color: INK,
1217
+ borderRadius: 8,
1218
+ flex: "0 0 auto",
1219
+ padding: "3px 10px",
1220
+ fontSize: 12.5,
1221
+ fontWeight: 600,
1222
+ cursor: "pointer"
1223
+ },
1112
1224
  error: {
1113
1225
  color: STATUS_COLOR.red,
1114
1226
  fontSize: 12,
@@ -1192,11 +1304,12 @@ window.__ModuleLoader__.load({
1192
1304
  id: tab.id,
1193
1305
  title: tab.title,
1194
1306
  status: report ? worstStatus(items) : null,
1195
- counts: countByStatus(items)
1307
+ counts: countByStatus(items),
1308
+ hasChannel: tab.hasChannel ?? true
1196
1309
  };
1197
1310
  });
1198
1311
  }
1199
- /** 一家的一句话读数:「红 1 黄 2」,全绿就是「绿」。 */
1312
+ /** 一家的一句话读数:「红 1 黄 2」,全绿就是「绿」。面板侧要**上色**的那几处不用它(见 `countSegments`)。 */
1200
1313
  function countsText(counts) {
1201
1314
  const parts = [];
1202
1315
  if (counts.red > 0) parts.push("红 " + String(counts.red));
@@ -1204,25 +1317,46 @@ window.__ModuleLoader__.load({
1204
1317
  if (parts.length === 0) return "绿";
1205
1318
  return parts.join(" · ");
1206
1319
  }
1207
- /** 「红 2 · 黄 3」那一截**按档上色**:每一档用它自己的字色、分隔符走淡色。
1208
- * 为什么必须拆开:原先整截塞进一个 span、染「最严重那一档」的颜色,六家都红时「黄 3」就跟着印成红字——
1209
- * 本页自己说「黄=还没配」,文字层却把它涂成红(出图复评五份里三份独立指到,取色同为 `#c03828`)。 */
1210
- function countSegments(light) {
1211
- if (light.status === null) return [react.createElement("span", {
1212
- key: "dash",
1213
- style: {
1214
- color: INK_DIM,
1215
- fontWeight: 700
1216
- }
1217
- }, "—")];
1218
- const parts = [];
1320
+ /** 把一份计数拆成**按档分色**的几截。红黄各自一截、各自上色,互不串色;
1321
+ * 「黄 3」这一截为什么不能被染成红——见 `countSegments` 的注释(出图复评逮到的真缺陷)。 */
1322
+ function countSegsOf(light) {
1323
+ if (light.status === null) return [{
1324
+ status: null,
1325
+ text: "—"
1326
+ }];
1327
+ const segs = [];
1219
1328
  for (const status of ["red", "yellow"]) {
1220
1329
  const count = light.counts[status];
1330
+ if (count > 0) segs.push({
1331
+ status,
1332
+ text: STATUS_LABEL[status] + " " + String(count)
1333
+ });
1334
+ }
1335
+ return segs.length > 0 ? segs : [{
1336
+ status: "green",
1337
+ text: "绿"
1338
+ }];
1339
+ }
1340
+ /** 几截读数之间那个分隔符:不许染档位色(它不属于任何一档)。 */
1341
+ const SEG_SEP = " · ";
1342
+ /** 汇总行那一句:绿要显眼(它是「正常」这件事本身),没通道的那几家不冒充绿。 */
1343
+ function overallText(lights) {
1344
+ const pending = lights.filter((light) => !light.hasChannel).length;
1345
+ const withReport = lights.filter((light) => light.hasChannel);
1346
+ const tail = pending > 0 ? " · " + String(pending) + " 家还没体检" : "";
1347
+ if (withReport.length > 0 && withReport.every((light) => light.status === "green")) return react.createElement("span", { style: {
1348
+ color: STATUS_COLOR.green,
1349
+ fontWeight: 700
1350
+ } }, "六家正常" + tail);
1351
+ const parts = [];
1352
+ for (const status of ["red", "yellow"]) {
1353
+ let count = 0;
1354
+ for (const light of lights) count += light.counts[status];
1221
1355
  if (count === 0) continue;
1222
1356
  if (parts.length > 0) parts.push(react.createElement("span", {
1223
1357
  key: status + "-sep",
1224
1358
  style: { color: INK_DIM }
1225
- }, " · "));
1359
+ }, SEG_SEP));
1226
1360
  parts.push(react.createElement("span", {
1227
1361
  key: status,
1228
1362
  style: {
@@ -1231,21 +1365,55 @@ window.__ModuleLoader__.load({
1231
1365
  }
1232
1366
  }, STATUS_LABEL[status] + " " + String(count)));
1233
1367
  }
1234
- if (parts.length === 0) return [react.createElement("span", {
1235
- key: "green",
1236
- style: {
1237
- color: INK_DIM,
1238
- fontWeight: 700
1239
- }
1240
- }, "绿")];
1241
- return parts;
1368
+ if (parts.length === 0) return react.createElement("span", { style: { color: INK_DIM } }, "还没体检" + tail);
1369
+ parts.push(react.createElement("span", {
1370
+ key: "todo",
1371
+ style: { color: INK }
1372
+ }, " 要处理" + tail));
1373
+ return react.createElement("span", null, parts);
1374
+ }
1375
+ /** 那枚小数字用的底色(比灯色浅一档,压在页签上不抢眼)。 */
1376
+ const STATUS_DOT = {
1377
+ red: STATUS_COLOR.red,
1378
+ yellow: STATUS_COLOR.yellow,
1379
+ green: STATUS_COLOR.green
1380
+ };
1381
+ /** 页签圆点的颜色与语义(票 #732):从「实心/空心=装没装」改成「颜色=体检档位」。 */
1382
+ const TAB_DOT = {
1383
+ /** 没装(页签槽账本里没有这一家)。 */
1384
+ absent: STATUS_DOT.green,
1385
+ /** 装了、但还没跑过体检。 */
1386
+ unchecked: INK_DIM
1387
+ };
1388
+ /** 页签圆点该用什么色:装了按档位(红黄绿),没装/没跑过走缺席态。 */
1389
+ function tabDotColor(light) {
1390
+ if (!light.hasChannel) return TAB_DOT.absent;
1391
+ return light.status === null ? TAB_DOT.unchecked : STATUS_DOT[light.status];
1392
+ }
1393
+ /** 页签上那枚小数字(票 #732):红黄那几家,家名后带一条计数;全绿与没跑过不带。 */
1394
+ const TAB_NOTE_STYLE = {
1395
+ marginLeft: 5,
1396
+ border: "1px solid " + BORDER,
1397
+ borderRadius: 999,
1398
+ padding: "0 5px",
1399
+ fontSize: 11,
1400
+ fontWeight: 700
1401
+ };
1402
+ /** 那枚小数字写什么:只报「要处理」的那两档;一家里红黄都有时先报红那一档(圆点已经按最严重的档上色了)。 */
1403
+ function tabNote(light) {
1404
+ if (!light.hasChannel || light.status === null || light.status === "green") return null;
1405
+ return countSegsOf(light)[0] ?? null;
1242
1406
  }
1243
- /** 顶部那行总览:六家一盏灯 + 一个「体检一次」按钮。 */
1244
- function HealthOverview(props) {
1407
+ /** 顶部那一行汇总(票 #732:不再一家一盏灯按钮,一行总账 + 一个体检按钮)。
1408
+ *
1409
+ * 与页签的分工:这行只答「一共几条要处理」;「哪几家、各几条」由页签的圆点与小数字答;
1410
+ * 「具体哪一条坏了、去哪修」由那家页签里的 `HealthTable` 答。 */
1411
+ function HealthSummaryLine(props) {
1412
+ const hasLights = props.lights.length > 0;
1245
1413
  return react.createElement("div", {
1246
- style: HEALTH_STYLE.box,
1247
- "data-ilife-health": "overview"
1248
- }, react.createElement("div", { style: HEALTH_STYLE.headRow }, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检")), react.createElement("div", { style: HEALTH_STYLE.runRow }, react.createElement("button", {
1414
+ style: HEALTH_STYLE.summaryRow,
1415
+ "data-ilife-health": "summary"
1416
+ }, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检"), react.createElement("div", { style: HEALTH_STYLE.summaryText }, props.error !== null ? react.createElement("span", { style: { color: STATUS_TEXT.red } }, props.error) : hasLights ? overallText(props.lights) : react.createElement("span", { style: { color: INK_DIM } }, "还没体检")), react.createElement("button", {
1249
1417
  type: "button",
1250
1418
  style: props.running ? {
1251
1419
  ...HEALTH_STYLE.btn,
@@ -1254,63 +1422,8 @@ window.__ModuleLoader__.load({
1254
1422
  } : HEALTH_STYLE.btn,
1255
1423
  disabled: props.running,
1256
1424
  onClick: props.onRun,
1257
- title: "对六家各跑一次配置体检(只看不改:不建目录、不改配置)"
1258
- }, props.running ? "体检中…" : "体检一次")), react.createElement("div", {
1259
- style: HEALTH_STYLE.lightsRow,
1260
- "data-ilife-health": "lights"
1261
- }, props.lights.map((light) => react.createElement("button", {
1262
- key: light.id,
1263
- type: "button",
1264
- "data-ilife-health": "light",
1265
- "data-skill": light.id,
1266
- style: light.status === null ? {
1267
- ...HEALTH_STYLE.light,
1268
- ...HEALTH_STYLE.lightIdle
1269
- } : {
1270
- ...HEALTH_STYLE.light,
1271
- borderColor: STATUS_COLOR[light.status],
1272
- borderWidth: 1.5,
1273
- background: STATUS_TINT[light.status]
1274
- },
1275
- disabled: light.status === null,
1276
- onClick: () => props.onJump(light.id),
1277
- title: light.status === null ? "还没体检(或这一家没装)" : light.title + ":" + countsText(light.counts)
1278
- }, react.createElement("span", { style: {
1279
- width: 10,
1280
- height: 10,
1281
- borderRadius: "50%",
1282
- background: light.status === null ? INK_DIM : STATUS_COLOR[light.status],
1283
- display: "inline-block"
1284
- } }), react.createElement("span", { style: {
1285
- color: INK,
1286
- fontWeight: 650
1287
- } }, light.title), ...countSegments(light)))), react.createElement("div", { style: HEALTH_STYLE.meta }, props.error !== null ? props.error : [
1288
- react.createElement("span", {
1289
- key: "r",
1290
- style: {
1291
- color: STATUS_TEXT.red,
1292
- fontWeight: 700
1293
- }
1294
- }, "红"),
1295
- react.createElement("span", { key: "r2" }, "=坏了 · "),
1296
- react.createElement("span", {
1297
- key: "y",
1298
- style: {
1299
- color: STATUS_TEXT.yellow,
1300
- fontWeight: 700
1301
- }
1302
- }, "黄"),
1303
- react.createElement("span", { key: "y2" }, "=还没配 · "),
1304
- react.createElement("span", {
1305
- key: "g",
1306
- style: {
1307
- color: STATUS_TEXT.green,
1308
- fontWeight: 700
1309
- }
1310
- }, "绿"),
1311
- react.createElement("span", { key: "g2" }, "=正常 | 只看不改:不建目录、不改配置、不自动重置"),
1312
- react.createElement("span", { key: "tab" }, " · 上面那排圆点是体检灯,点一下跳到那家配置页;下面那排圆点是页签(实心=装上了)")
1313
- ]));
1425
+ title: "对六家各跑一次配置体检(只看不改:不建目录、不改配置、不自动重置)"
1426
+ }, props.running ? "体检中…" : "体检一次"));
1314
1427
  }
1315
1428
  /** 正正常常的那些:绿条只留「名字」,一句话与「去哪修」都不印(票面验收第一条:正常的收成一行)。 */
1316
1429
  function isAttentionItem(item) {
@@ -1608,6 +1721,13 @@ window.__ModuleLoader__.load({
1608
1721
  id: row.id,
1609
1722
  channel: row.channel
1610
1723
  })), [rows]));
1724
+ /** 六家的灯(票 #732):按**页签槽账本**那一排过(装了但没体检通道的也在内,它显缺席态)。 */
1725
+ const lights = react.useMemo(() => lightsOf(rows.map((row) => ({
1726
+ id: row.id,
1727
+ title: row.label,
1728
+ hasChannel: row.channel.length > 0
1729
+ })), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))), [rows, health.rows]);
1730
+ const lightOf = (id) => lights.find((light) => light.id === id);
1611
1731
  const [activeId, setActiveId] = react.useState(void 0);
1612
1732
  const [visitedIds, setVisitedIds] = react.useState(() => /* @__PURE__ */ new Set());
1613
1733
  const active = MANAGER_TABS.some((t) => t.plugin === activeId) ? activeId : MANAGER_TABS[0].plugin;
@@ -1621,13 +1741,6 @@ window.__ModuleLoader__.load({
1621
1741
  const row = rows.find((r) => r.id === tab.plugin);
1622
1742
  return row && row.label.length > 0 ? row.label : tab.title;
1623
1743
  }
1624
- /** 按包名取页签标题(总览那行按账本行过,账本里没有的退到导航表的标题)。 */
1625
- function labelForTab(id) {
1626
- const row = rows.find((r) => r.id === id);
1627
- if (row && row.label.length > 0) return row.label;
1628
- const tab = MANAGER_TABS.find((t) => t.plugin === id);
1629
- return tab ? tab.title : id;
1630
- }
1631
1744
  function targetForTab(tab) {
1632
1745
  return face.targets.find((t) => t.packageName === tab.plugin) ?? null;
1633
1746
  }
@@ -1653,21 +1766,20 @@ window.__ModuleLoader__.load({
1653
1766
  setActiveId(next.plugin);
1654
1767
  tabRefs.current[nextIndex]?.focus();
1655
1768
  }
1656
- return react.createElement("div", null, react.createElement("div", { style: S.headRow }, react.createElement("div", { style: S.head }, "爱生活"), react.createElement("div", { style: S.headActions }, react.createElement(CheckUpdateButton, { face }), react.createElement(PanelActions, null))), react.createElement("div", { style: S.meta }, react.createElement("div", null, "总开关 · 开关(缺省启用,只读)"), react.createElement("div", null, "总管 dsh-life-pack · 0.2.6")), react.createElement(UpdateResults, { face }), react.createElement(HealthOverview, {
1657
- lights: lightsOf(rows.map((row) => ({
1658
- id: row.id,
1659
- title: labelForTab(row.id)
1660
- })), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))),
1769
+ return react.createElement("div", null, react.createElement("div", { style: S.headRow }, react.createElement("div", { style: S.head }, "爱生活"), react.createElement("div", { style: S.headActions }, react.createElement(CheckUpdateButton, { face }), react.createElement(PanelActions, null))), react.createElement("div", { style: S.meta }, react.createElement("div", null, "总开关 · 开关(缺省启用,只读)"), react.createElement("div", null, "总管 dsh-life-pack · 0.2.6")), react.createElement(UpdateResults, { face }), react.createElement(HealthSummaryLine, {
1770
+ lights,
1661
1771
  running: health.running,
1662
1772
  error: health.error,
1663
- onRun: health.run,
1664
- onJump: setActiveId
1773
+ onRun: health.run
1665
1774
  }), react.createElement("div", {
1666
1775
  role: "tablist",
1667
1776
  "aria-label": "爱生活技能页签",
1668
1777
  style: S.tablist
1669
1778
  }, MANAGER_TABS.map((tab, index) => {
1670
1779
  const selected = tab.plugin === active;
1780
+ const light = lightOf(tab.plugin);
1781
+ const dotColor = light === void 0 ? present.has(tab.plugin) ? TAB_DOT.unchecked : TAB_DOT.absent : tabDotColor(light);
1782
+ const note = light === void 0 ? null : tabNote(light);
1671
1783
  return react.createElement("button", {
1672
1784
  key: tab.plugin,
1673
1785
  ref: (element) => {
@@ -1688,8 +1800,25 @@ window.__ModuleLoader__.load({
1688
1800
  },
1689
1801
  onKeyDown: (event) => {
1690
1802
  onTabKeyDown(event, index);
1803
+ },
1804
+ title: light === void 0 ? "这一家没装(或产物没注册页签槽)" : light.title + ":" + (light.hasChannel ? light.status === null ? "还没体检" : "体检读数见本页签下方的表" : "没有配置体检出口")
1805
+ }, react.createElement("span", {
1806
+ "data-ilife-health": "tab-dot",
1807
+ style: {
1808
+ width: 6,
1809
+ height: 6,
1810
+ borderRadius: "50%",
1811
+ background: dotColor,
1812
+ display: "inline-block",
1813
+ flex: "0 0 auto"
1814
+ }
1815
+ }), labelFor(tab), note === null ? null : react.createElement("span", {
1816
+ "data-ilife-health": "tab-note",
1817
+ style: {
1818
+ ...TAB_NOTE_STYLE,
1819
+ color: note.status === null ? void 0 : STATUS_TEXT[note.status]
1691
1820
  }
1692
- }, (present.has(tab.plugin) ? "● " : "○ ") + labelFor(tab));
1821
+ }, note.text));
1693
1822
  })), MANAGER_TABS.filter((tab) => tab.plugin === active || visitedIds.has(tab.plugin)).map((tab) => {
1694
1823
  const selected = tab.plugin === active;
1695
1824
  return react.createElement("div", {