dsh-chef 0.3.12 → 0.3.14

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
@@ -43,9 +43,12 @@ window.__ModuleLoader__.load({
43
43
  const RPC_ENDPOINT_CONFIG_GET = "config.get";
44
44
  const RPC_ENDPOINT_CONFIG_SAVE = "config.save";
45
45
  const RPC_ENDPOINT_CONFIG_RESET = "config.reset";
46
- /** 高级组标题与副文案(六家逐字相同,收在这里一处)。 */
46
+ /** 高级组标题(六家逐字相同,收在这里一处)。
47
+ *
48
+ * #934:原先还有一条副文案 `ADVANCED_GROUP_NOTE`(「不常改。留空=用默认值。」),
49
+ * 维护者裁定**整条删除、删后不另找地方说**——理由:三家高级项全是只读行、不参与保存、根本没有"留空",
50
+ * 而"留空会怎样"这条口径本就逐行住在每条的 `hint` 里(见本件 `ConfigItem.hint` 的契约)。 */
47
51
  const ADVANCED_GROUP_TITLE = "高级";
48
- const ADVANCED_GROUP_NOTE = "不常改。留空=用默认值。";
49
52
  /** 请求超时毫秒:与宿主侧 SPAWN_TIMEOUT_MS 同级,界面永不无限转圈。 */
50
53
  const READ_TIMEOUT_MS = 2e4;
51
54
  /** 复制那一枚按钮就地变「已复制」之后,多久变回「复制」(定稿 v3 第三条:1.5 秒,不弹提示)。 */
@@ -158,21 +161,6 @@ window.__ModuleLoader__.load({
158
161
  function noFollowKeys() {
159
162
  return [];
160
163
  }
161
- /** 这一行有没有「写的值 ≠ 生效值」这回事:只要是可改行、标了落点来源、写的非空、
162
- * 且技能算出来的生效值另有其值(去掉尾分隔符后仍不同),就报这两个事实。 */
163
- function fallbackFactsOf(item, raw, source) {
164
- if (item.readonly === true || item.prefillFrom === void 0) return null;
165
- const written = raw.trim();
166
- if (written === "") return null;
167
- const effective = prefillValueOf(source, item.prefillFrom);
168
- if (effective === void 0) return null;
169
- const strip = (path) => path.replace(/[\\/]+$/, "");
170
- if (strip(written) === strip(effective)) return null;
171
- return {
172
- written,
173
- effective
174
- };
175
- }
176
164
  /** 配置面报错 → 人话指引。
177
165
  *
178
166
  * 报文由技能侧给出(`base-link-core` 的 `ConfigError` 已带行号与文件名,技能 `cli/config.ts`
@@ -847,7 +835,7 @@ window.__ModuleLoader__.load({
847
835
  textOverflow: "ellipsis",
848
836
  whiteSpace: "nowrap"
849
837
  },
850
- error: { color: "var(--dsw-alias-label-error, #e66)" }
838
+ error: { color: "var(--dsw-alias-state-error-primary, #e66)" }
851
839
  };
852
840
  /** 面包屑:从文件系统根到当前层,每一格都是跳转目标(`listing.crumbs` 由取数方给)。 */
853
841
  function crumbRow(state, onEnter) {
@@ -1072,6 +1060,24 @@ window.__ModuleLoader__.load({
1072
1060
  *
1073
1061
  * 手写 `React.createElement`(本包 client 束禁 JSX)。样式表 `S` 就在本件里——面板怎么画只有这一处。
1074
1062
  *
1063
+ * **外观真源=`docs/agents/配置区域视觉模型-v3.1.html`(#920 阶段二起)**:本件的 `S` 逐项照它的
1064
+ * CSS(`:32-205`)、行模板照它的 `rowShell`/`masterShell`/`cardShell`(`:349-451`)、状态徽标照它的
1065
+ * `badgeHTML`(`:544-552`)那七档。**颜色/间距/圆角/字号/层级一律照取值,不许自己重新设计**;
1066
+ * 与本件不同的只有下面这四条(每条都在这里写明为什么,改之前先读 #920 的证据件):
1067
+ * ① **字号一律 em,没有 px**:跨包锁 `test/panel-type-739.test.mjs` 钉死「零 `font:` 简写/零字族/
1068
+ * 字号一律相对单位」——宿主没有界面字号 token,绝对 px 会在别的窗口尺寸上失真。v3.1 的 px 按它
1069
+ * 自己已锁死的那两对锚点换算(`13.5px↔1.08em`、`11.5px↔0.92em` ⇒ **1px=0.08em**),一个不丢。
1070
+ * ② **值列不写等宽字族**:v3.1 的值列要 `font-family: var(--ic-mono)`,而 `#739 ①` 明令共用件零
1071
+ * `fontFamily`/零 `font:` 简写 ⇒ 值继承宿主字族。这是与 v3.1 的**一处已知不同**(#920 证据件有账)。
1072
+ * ③ **换行判据交给 CSS,不按字符数估宽**:v3.1 靠 JS 量文本宽在两套模板间两选一(审查 P0-4/P0-5:
1073
+ * 每列 6.048px、真阈值 40–43 列而注释按 34 估 ⇒ 9 行被无谓拆两行、快面一次性长高 194px)。
1074
+ * 本件没有 DOM 可量,故照审查给的修法把同一个决定交给排版引擎:行是 `flex-wrap` 容器,标签列定宽、
1075
+ * 值列 `flex-basis:auto`(=内容的自然宽)——装不下时**整行让位**(值落第二行、按钮仍在第一行行尾,
1076
+ * 与 v3.1 的 `.is-wide` 同形);值再长就在自己那一行里 `overflow-wrap:anywhere` 折下去。
1077
+ * **零省略号、零截断**(`white-space` 不用 `nowrap`、`text-overflow` 一次都不出现)。
1078
+ * ④ **画不出来的三样**用等价形态顶上:分组箭头 `::before`/`[open]` 旋转/keyframes 动画内联样式
1079
+ * 给不出 ⇒ 画静态「›」与静态环(形状与 v3.1 同,只少了动)。
1080
+ *
1075
1081
  * 三条定稿(v3)在本件落地:
1076
1082
  * ① **只读行的目录按钮不画**(不画一枚点了也没反应的死按钮);
1077
1083
  * ② 目录行的按钮字面**两档合一**,一律「浏览文件夹」(入口供不了时整枚不画);
@@ -1079,148 +1085,484 @@ window.__ModuleLoader__.load({
1079
1085
  *
1080
1086
  * 对外只经 `config-panel-api.ts` 那一道门;本件自己不出门。
1081
1087
  */
1082
- /** 面板的样式表:颜色走 DSH 主题别名(深浅主题自适应,写死值只做回退),字号一律相对单位(em)。 */
1088
+ /** 面板的样式表:取值逐项照 v3.1(颜色走 DSH 主题别名,写死值只做回退;字号一律相对单位 em)。
1089
+ *
1090
+ * 键名一个都不许改:六家附加块真消费 `okText`/`error`/`rows`/`label`/`muted`/`hint`/`info`/
1091
+ * `bar`/`btn` 这九格(`plugin-memo-ilife/src/client.ts`、`plugin-schedule-ilife/src/client.ts` 的
1092
+ * `PanelStyleSlots`),跨包锁另钉 `card`/`input`/`btnPrimary`/`btnPick`/`summary`/`title`/`version`。 */
1093
+ /** #941/丁:**浅色档距自合成**——宿主在浅色下四层底同白(`bg-base = bg-layer-1/2/3`),
1094
+ * 拿不到可用的层级差,所以三处面(卡描边/沉底盒底/输入框描边)由我们自己按墨色派生。
1095
+ *
1096
+ * 取 `label-primary` 当墨色分母:浅色下它是近黑、深色下它是近白 ⇒ **同一条表达式两套主题都成立**
1097
+ * (浅色补出可用的档距,深色退化成今天那几档观感)。百分比来自维护者选定的方案丁:
1098
+ * 卡描边 19%/沉底盒底 9%/输入框描边 24%/骨架条 21%。这是本仓第一处"自定值",属真源增补
1099
+ * (v3.1 只有深色一套 token,从未覆盖浅色)。 */
1100
+ const PANEL_INK = "var(--dsw-alias-label-primary, #f9fafb)";
1101
+ const inkMix = (percent, into) => into === void 0 ? `color-mix(in srgb, ${PANEL_INK} ${percent}%, transparent)` : `color-mix(in srgb, ${PANEL_INK} ${percent}%, ${into})`;
1102
+ /** 卡片描边:浅色 ≈`#cfcfcf` ⇔ 深色 ≈`#4a4b4d`。 */
1103
+ const PANEL_CARD_EDGE = inkMix(19);
1104
+ /** 沉底盒底(母版行/高级组体/状态行):浅色 ≈`#e9e9e9` ⇔ 深色 ≈`#3a3b3d`。 */
1105
+ const PANEL_SUNKEN_FILL = inkMix(9, "var(--dsw-alias-bg-layer-1, #232324)");
1106
+ /** 输入框描边:浅色 ≈`#c4c4c4` ⇔ 深色 ≈`#5a5b5d`。 */
1107
+ const PANEL_FIELD_EDGE = inkMix(24);
1108
+ /** 骨架条:浅色 ≈`#c9ccd2`(1.6:1)⇔ 深色 ≈`#404143`。 */
1109
+ const PANEL_SKELETON_FILL = inkMix(21);
1083
1110
  const S = {
1111
+ /** `.ic-card`:`background:var(--ic-surface)`(=bg-layer-1)+`border:1px solid var(--ic-line-strong)`
1112
+ * +`border-radius:12px`+`padding:14px`+`box-shadow:inset 0 1px 0 rgba(255,255,255,.06)`。
1113
+ * 基准字号**不钉在卡片上**(#739 ⑤:跟着宿主走)。 */
1084
1114
  card: {
1085
- padding: "12px 14px",
1086
- borderRadius: 10,
1087
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
1088
- background: "var(--dsw-alias-bg-base, transparent)",
1089
- color: "var(--dsw-alias-label-primary, inherit)",
1090
- lineHeight: 1.6
1115
+ padding: 14,
1116
+ borderRadius: 12,
1117
+ border: "1px solid " + PANEL_CARD_EDGE,
1118
+ background: "var(--dsw-alias-bg-layer-1, #232324)",
1119
+ color: "var(--dsw-alias-label-primary, #f9fafb)",
1120
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,.06)"
1091
1121
  },
1122
+ /** `.ic-head{display:flex;align-items:center;gap:10px;min-height:22px}`(徽标靠 `margin-left:auto` 右贴)。 */
1123
+ head: {
1124
+ display: "flex",
1125
+ alignItems: "center",
1126
+ gap: 10,
1127
+ minHeight: 22
1128
+ },
1129
+ /** `.ic-title{font-size:13.5px;font-weight:640}`——字号按 60 行那条锚点换算成 `1.08em`。 */
1092
1130
  title: {
1093
1131
  fontSize: "1.08em",
1094
- fontWeight: 700,
1095
- marginBottom: 8
1132
+ fontWeight: 640
1133
+ },
1134
+ /** `.ic-file{margin-top:4px}` 的那 4px 挂在两行的公共容器上:`info` 自己一格 margin 都不写
1135
+ * (#743 ①:头部两行间距不一家一个数,故这一项一律不写 margin)。 */
1136
+ fileWrap: { marginTop: 4 },
1137
+ /** #939:骨架条(照真源 `.ic-sk`:`display:block;height:17px;border-radius:4px`)。
1138
+ * 高度按一行正文画——inline-block 跟着父级行高走会让骨架行比填充后的文字行高 4px、六家叠起来就是一片下移。
1139
+ * 底色走派生值(宿主专用骨架档 `bg-skeleton` 在浅色下只有 1.09:1,等于看不见)。 */
1140
+ skeleton: {
1141
+ display: "block",
1142
+ height: 17,
1143
+ borderRadius: 4,
1144
+ background: PANEL_SKELETON_FILL
1145
+ },
1146
+ /** #936:状态行那一块(照真源 `.ic-status`)——沉底面盒子,里面三行:状态行/路径行/官网行。 */
1147
+ statusBox: {
1148
+ marginTop: 11,
1149
+ padding: "10px 12px",
1150
+ background: PANEL_SUNKEN_FILL,
1151
+ border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
1152
+ borderRadius: 8
1096
1153
  },
1154
+ /** `.ic-srow{display:flex;align-items:center;gap:8px;font-size:12px;flex-wrap:wrap}`(12px ⇒ 0.86em)。 */
1155
+ statusRow: {
1156
+ display: "flex",
1157
+ alignItems: "center",
1158
+ gap: 8,
1159
+ fontSize: "0.86em",
1160
+ flexWrap: "wrap"
1161
+ },
1162
+ /** `.ic-dot{width:8px;height:8px;border-radius:50%}`——底色按档位在组件里补。 */
1163
+ statusDot: {
1164
+ width: 8,
1165
+ height: 8,
1166
+ borderRadius: "50%",
1167
+ flex: "0 0 auto"
1168
+ },
1169
+ /** `.ic-sname{font-weight:600;white-space:nowrap}`。 */
1170
+ statusName: {
1171
+ fontWeight: 600,
1172
+ whiteSpace: "nowrap"
1173
+ },
1174
+ /** `.ic-stxt{color:var(--ic-muted);min-width:0}`。 */
1175
+ statusText: {
1176
+ color: "var(--dsw-alias-label-secondary, #cfd3d6)",
1177
+ minWidth: 0
1178
+ },
1179
+ /** `.ic-sfind{display:flex;align-items:center;gap:8px;margin-top:7px}`。 */
1180
+ statusFind: {
1181
+ display: "flex",
1182
+ alignItems: "center",
1183
+ gap: 8,
1184
+ marginTop: 7
1185
+ },
1186
+ /** `.ic-spath{font-size:10.5px;color:var(--ic-faint);white-space:nowrap;overflow:hidden}`
1187
+ * ——单行不折、超出裁掉(**不写省略号**:本件有一条"零省略号"的跨包锁)。
1188
+ * **不写 `fontFamily`**:v3.1 这一格要 `var(--ic-mono)`,但 `#739 ①` 明令共用件零字族(宿主没有界面字族
1189
+ * token,钉死会在别的窗口尺寸上失真)——与真源的同一处已知不同,账在 #920 证据件里。 */
1190
+ statusPath: {
1191
+ fontSize: "0.75em",
1192
+ color: "var(--dsw-alias-label-tertiary, #adb2b8)",
1193
+ minWidth: 0,
1194
+ whiteSpace: "nowrap",
1195
+ overflow: "hidden"
1196
+ },
1197
+ /** `.ic-ver{font-size:10px;border:1px solid var(--ic-line-strong);border-radius:999px;padding:0 6px}`。 */
1198
+ statusVersion: {
1199
+ fontSize: "0.72em",
1200
+ color: "var(--dsw-alias-label-tertiary, #adb2b8)",
1201
+ border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
1202
+ borderRadius: 999,
1203
+ padding: "0 6px",
1204
+ whiteSpace: "nowrap"
1205
+ },
1206
+ /** `.ic-url{display:inline-block;margin-top:7px;font-size:10.5px;color:var(--ic-faint)}`
1207
+ * ——字族同样继承宿主(`#739 ①`:共用件零 `fontFamily`),理由同上一格。 */
1208
+ statusUrl: {
1209
+ display: "inline-block",
1210
+ marginTop: 7,
1211
+ fontSize: "0.75em",
1212
+ color: "var(--dsw-alias-label-tertiary, #adb2b8)"
1213
+ },
1214
+ /** `.ic-file b{color:var(--ic-faint);font-weight:400;margin-right:5px}`(「配置文件」「数据目录」那两个字)。 */
1215
+ fileLabel: {
1216
+ color: "var(--dsw-alias-label-tertiary, #adb2b8)",
1217
+ fontWeight: 400,
1218
+ marginRight: 5
1219
+ },
1220
+ /** `.ic-file{color:var(--ic-muted);font-size:11.5px;word-break:break-all}`(11.5px ⇒ `0.92em`)。 */
1221
+ info: {
1222
+ color: "var(--dsw-alias-label-secondary, #cfd3d6)",
1223
+ fontSize: "0.92em",
1224
+ wordBreak: "break-all"
1225
+ },
1226
+ /** `.ic-muted` 那一档(次要字)。 */
1097
1227
  muted: {
1098
- color: "var(--dsw-alias-label-secondary, #9a9a9a)",
1228
+ color: "var(--dsw-alias-label-secondary, #cfd3d6)",
1099
1229
  fontSize: "0.92em"
1100
1230
  },
1231
+ /** 错误字:取 v3.1 的 `--ic-danger`;字号 `1em`(#739 钉死)。 */
1101
1232
  error: {
1102
1233
  marginTop: 8,
1103
- color: "var(--dsw-alias-label-error, #b3261e)",
1234
+ color: "var(--dsw-alias-state-error-primary, #e0685f)",
1104
1235
  fontSize: "1em",
1105
1236
  whiteSpace: "pre-wrap"
1106
1237
  },
1238
+ /** 成功字:取 v3.1 的 `--ic-ok`;字号 `0.96em`(#739 钉死)。 */
1107
1239
  okText: {
1108
1240
  marginTop: 8,
1109
- color: "var(--dsw-alias-state-success-primary, #12805c)",
1241
+ color: "var(--dsw-alias-state-success-primary, #4ec9a0)",
1110
1242
  fontSize: "0.96em"
1111
1243
  },
1112
- rows: {
1113
- marginTop: 8,
1114
- borderTop: "1px solid var(--dsw-alias-border, rgba(128,128,128,.25))",
1115
- paddingTop: 8
1244
+ /** 行组的容器(六家附加块也拿它当外框;v3.1 没有对应类,故只给一个中性块)。 */
1245
+ rows: { display: "block" },
1246
+ /** `.ic-row`:三列栅格 + `gap:4px 10px` + `padding:7px 0` + 一条 `.ic-line` 下边线。
1247
+ * 本件把它落成**换行弹性盒**(见文件头 ③):标签定宽在第一行,值装不下时整行让位,
1248
+ * 动作槽绝对定位贴在行尾(`.ic-row .ic-slot[data-slot^="a:"]` 那一格)。 */
1249
+ row: {
1250
+ position: "relative",
1251
+ display: "flex",
1252
+ flexWrap: "wrap",
1253
+ alignItems: "center",
1254
+ gap: "4px 10px",
1255
+ padding: "7px 0",
1256
+ borderBottom: "1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06))"
1116
1257
  },
1117
- info: {
1118
- color: "var(--dsw-alias-label-secondary, #9a9a9a)",
1258
+ /** `.ic-row:last-child{border-bottom:0}`——内联样式没有 `:last-child`,由画它的人明说。 */
1259
+ rowLast: {
1260
+ position: "relative",
1261
+ display: "flex",
1262
+ flexWrap: "wrap",
1263
+ alignItems: "center",
1264
+ gap: "4px 10px",
1265
+ padding: "7px 0"
1266
+ },
1267
+ /** `.ic-under{margin:9px 0 0 8px;border-left:1px solid var(--ic-line-strong);padding-left:13px}`:
1268
+ * **跟随行**相对它的可改行(母版行)缩进 8+1+13=**22px**,左边挂一条线。
1269
+ *
1270
+ * 取值逐字照抄;只有一处形态差别要说清:`.ic-under` 在 v3.1 里是**一整块**(同一个可改行的一批
1271
+ * 跟随者共用一个 div、共一条左边线、块首还有 9px 上边距),而共用件手上只有「谁是跟随行」这一个
1272
+ * 集合(由六家交的 `followKeysOf` 钩子逐键探出),**没有**「这一行归哪一块」的分块归属
1273
+ * (同一个键可以同时挂在两张跟随表上,钩子只回一个去重后的并集)。故这里按**行**落这条缩进:
1274
+ * 缩进量与左边线同值,块首那 9px 上边距不落(落下去会在每两行之间多出一道 9px 的缝)。 */
1275
+ followerRow: {
1276
+ position: "relative",
1277
+ display: "flex",
1278
+ flexWrap: "wrap",
1279
+ alignItems: "center",
1280
+ gap: "4px 10px",
1281
+ padding: "7px 0 7px 13px",
1282
+ marginLeft: 8,
1283
+ borderLeft: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
1284
+ borderBottom: "1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06))"
1285
+ },
1286
+ /** 跟随行的最后一行:同上,去掉下边线(照 `.ic-row:last-child{border-bottom:0}`)。 */
1287
+ followerRowLast: {
1288
+ position: "relative",
1289
+ display: "flex",
1290
+ flexWrap: "wrap",
1291
+ alignItems: "center",
1292
+ gap: "4px 10px",
1293
+ padding: "7px 0 7px 13px",
1294
+ marginLeft: 8,
1295
+ borderLeft: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))"
1296
+ },
1297
+ /** `.ic-row .ic-k{font-size:11.5px;color:var(--ic-muted)}`;`--ic-kw:118px` 是标签列的定宽。 */
1298
+ rowKey: {
1299
+ flex: "0 0 auto",
1300
+ minWidth: 118,
1119
1301
  fontSize: "0.92em",
1120
- overflowWrap: "anywhere"
1302
+ color: "var(--dsw-alias-label-secondary, #cfd3d6)"
1303
+ },
1304
+ /** `.ic-row .ic-v{font-size:11px;min-width:0}`(11px ⇒ `0.88em`)+ 文件头 ③ 的让位口径:
1305
+ * `flex:1 1 auto` ⇒ 装不下整行让位;`overflow-wrap:anywhere` ⇒ 折下去、零省略号;
1306
+ * `paddingRight` 给行尾那枚按钮(+列间距)让出宽度——就是 `.ic-row` 第三列那一格。 */
1307
+ rowValue: {
1308
+ flex: "1 1 auto",
1309
+ minWidth: 0,
1310
+ paddingRight: 56,
1311
+ fontSize: "0.88em",
1312
+ whiteSpace: "normal",
1313
+ overflowWrap: "anywhere",
1314
+ lineHeight: 1.5
1121
1315
  },
1122
- row: { marginBottom: 10 },
1123
- label: { fontWeight: 600 },
1316
+ /** 行尾动作槽(复制/跟随标记):绝对定位贴行尾,行一换行它仍留在第一行行尾(=`.is-wide` 那一形)。 */
1317
+ rowActs: {
1318
+ position: "absolute",
1319
+ right: 0,
1320
+ top: 7,
1321
+ display: "flex",
1322
+ gap: 4,
1323
+ alignItems: "center"
1324
+ },
1325
+ /** `.ic-follow{font-size:11px;color:var(--ic-attn);white-space:nowrap}`(11px ⇒ `0.88em`)。 */
1326
+ follow: {
1327
+ fontSize: "0.88em",
1328
+ color: "var(--dsw-alias-state-warn-primary, #e0a33e)",
1329
+ whiteSpace: "nowrap",
1330
+ display: "inline-flex",
1331
+ alignItems: "center",
1332
+ gap: 4
1333
+ },
1334
+ /** `.ic-follow::before{content:'↳';opacity:.8}`——内联样式给不出伪元素,画成真的那一个字符。 */
1335
+ followMark: { opacity: .8 },
1336
+ /** `.ic-master`:母版行那一块盒子(`--ic-sunken` 底 + `border-radius:8px` + `padding:11px 12px`
1337
+ * + `margin-top:11px` + 内高光)。 */
1338
+ master: {
1339
+ background: PANEL_SUNKEN_FILL,
1340
+ border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
1341
+ borderRadius: 8,
1342
+ padding: "11px 12px",
1343
+ marginTop: 11,
1344
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,.05)"
1345
+ },
1346
+ /** `.ic-master .ic-lab{font-weight:600;display:flex;align-items:center;gap:7px}`;
1347
+ * 字号**不写**(#739 ④:`label` 这一项该继承宿主字号)。 */
1348
+ label: {
1349
+ display: "flex",
1350
+ alignItems: "center",
1351
+ gap: 7,
1352
+ fontWeight: 600
1353
+ },
1354
+ /** `.ic-chip{font-size:10.5px;border:1px solid var(--ic-accent);border-radius:999px;padding:0 6px}`(10.5px ⇒ `0.84em`)。 */
1355
+ chip: {
1356
+ fontSize: "0.84em",
1357
+ border: "1px solid var(--dsw-alias-brand-primary, #f9fafb)",
1358
+ borderRadius: 999,
1359
+ padding: "0 6px",
1360
+ color: "var(--dsw-alias-brand-primary, #f9fafb)",
1361
+ whiteSpace: "nowrap"
1362
+ },
1363
+ /** `.ic-master .ic-hint{color:var(--ic-muted);font-size:11.5px;margin-top:3px;line-height:1.5}`。 */
1124
1364
  hint: {
1125
- color: "var(--dsw-alias-label-secondary, #9a9a9a)",
1365
+ color: "var(--dsw-alias-label-secondary, #cfd3d6)",
1126
1366
  fontSize: "0.92em",
1127
- marginBottom: 4
1367
+ marginTop: 3,
1368
+ lineHeight: 1.5
1128
1369
  },
1370
+ /** `.ic-master input`:整行宽、`margin-top:7px`、`padding:5px 8px`、`--ic-bg` 底、`--ic-line-strong` 描边、
1371
+ * `border-radius:6px`。字号与字族**不写**(#739 ④/①:`input` 继承宿主)。 */
1129
1372
  input: {
1373
+ display: "block",
1130
1374
  width: "100%",
1375
+ marginTop: 7,
1376
+ padding: "5px 8px",
1131
1377
  boxSizing: "border-box",
1132
- padding: "4px 8px",
1378
+ background: "var(--dsw-alias-bg-base, #151517)",
1379
+ border: "1px solid " + PANEL_FIELD_EDGE,
1133
1380
  borderRadius: 6,
1134
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.45))",
1135
- background: "var(--dsw-alias-bg-base, transparent)",
1136
- color: "var(--dsw-alias-label-primary, inherit)"
1381
+ color: "var(--dsw-alias-label-primary, #f9fafb)"
1137
1382
  },
1138
- pickRow: {
1139
- display: "flex",
1140
- gap: 6,
1141
- alignItems: "center"
1383
+ /** `.ic-master input.is-dirty`:`border:2px solid var(--ic-attn)` + `padding:4px 7px`
1384
+ * + `box-shadow:0 0 0 3px <琥珀 22%>`;2px 描边与 5→4 的内边距对消,宽高不变
1385
+ * (审查「站得住」第 7 条)。 */
1386
+ inputDirty: {
1387
+ display: "block",
1388
+ width: "100%",
1389
+ marginTop: 7,
1390
+ padding: "4px 7px",
1391
+ boxSizing: "border-box",
1392
+ background: "var(--dsw-alias-bg-base, #151517)",
1393
+ border: "2px solid var(--dsw-alias-state-warn-primary, #e0a33e)",
1394
+ borderRadius: 6,
1395
+ color: "var(--dsw-alias-label-primary, #f9fafb)",
1396
+ boxShadow: "0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e0a33e) 22%, transparent)"
1142
1397
  },
1398
+ /** 布尔档那枚方框(v3.1 的母版行只有文本框,这一格按输入框的同一套外边距对齐)。 */
1399
+ checkbox: { marginTop: 7 },
1400
+ /** `.ic-acts{display:flex;gap:5px;margin-top:7px;flex-wrap:wrap;align-items:center}`(母版行的按钮行)。 */
1143
1401
  acts: {
1144
1402
  display: "flex",
1145
- gap: 6,
1146
- alignItems: "center",
1147
- flex: "0 0 auto"
1403
+ gap: 5,
1404
+ marginTop: 7,
1405
+ flexWrap: "wrap",
1406
+ alignItems: "center"
1148
1407
  },
1408
+ /** `.ic-btn.is-mini{font-size:11px;padding:2px 7px;color:var(--ic-muted)}`——`btnPick` 是锁钉死的那一格,
1409
+ * 字号**不写**(#739 ④:该继承宿主字号),形状照 v3.1。 */
1149
1410
  btnPick: {
1150
- flex: "0 0 auto",
1151
- padding: "4px 10px",
1411
+ padding: "2px 7px",
1152
1412
  borderRadius: 6,
1153
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.45))",
1154
- background: "var(--dsw-alias-bg-base, transparent)",
1155
- color: "var(--dsw-alias-label-primary, inherit)",
1156
- whiteSpace: "nowrap",
1157
- cursor: "pointer"
1413
+ border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
1414
+ background: "transparent",
1415
+ color: "var(--dsw-alias-label-secondary, #cfd3d6)",
1416
+ cursor: "pointer",
1417
+ whiteSpace: "nowrap"
1158
1418
  },
1419
+ /** `.ic-invalid{margin-top:7px;padding:7px 9px;border-radius:6px;font-size:11.5px;line-height:1.5;
1420
+ * background:color-mix(in srgb, var(--ic-attn) 16%, transparent);border:1px solid var(--ic-attn)}`。 */
1159
1421
  invalid: {
1160
- marginTop: 6,
1161
- padding: "6px 8px",
1422
+ marginTop: 7,
1423
+ padding: "7px 9px",
1162
1424
  borderRadius: 6,
1163
- background: "color-mix(in srgb, var(--dsw-alias-state-warning-primary, #b26a00) 16%, transparent)",
1164
- border: "1px solid var(--dsw-alias-state-warning-primary, #b26a00)",
1165
- color: "var(--dsw-alias-label-primary, inherit)",
1166
- fontSize: "0.92em"
1425
+ fontSize: "0.92em",
1426
+ lineHeight: 1.5,
1427
+ background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e0a33e) 16%, transparent)",
1428
+ border: "1px solid var(--dsw-alias-state-warn-primary, #e0a33e)",
1429
+ color: "var(--dsw-alias-label-primary, #f9fafb)"
1430
+ },
1431
+ /** `.ic-group{margin-top:14px}`(高级组默认收起)。 */
1432
+ advanced: { marginTop: 14 },
1433
+ /** `.ic-group > .ic-ghead{display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;
1434
+ * font-weight:600;padding:2px 0}`——字号**不写**(#739 ④:`summary` 该继承宿主字号)。 */
1435
+ summary: {
1436
+ display: "flex",
1437
+ alignItems: "center",
1438
+ gap: 8,
1439
+ cursor: "pointer",
1440
+ listStyle: "none",
1441
+ fontWeight: 600,
1442
+ padding: "2px 0"
1167
1443
  },
1444
+ /** `.ic-ghead::before{content:'›';color:var(--ic-muted)}`——画成真的那一个字符(内联样式给不出伪元素)。 */
1445
+ groupMark: { color: "var(--dsw-alias-label-secondary, #cfd3d6)" },
1446
+ /** #934:`.ic-gnote` 那条副文案的样式随之删除——副文案本身已按维护者裁定整条撤掉(零消费者的槽位不留)。 */
1447
+ /** `.ic-group > .ic-gbody{margin-top:9px;padding:11px 12px;background:var(--ic-sunken);
1448
+ * border-radius:8px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}`。 */
1449
+ groupBody: {
1450
+ marginTop: 9,
1451
+ padding: "11px 12px",
1452
+ background: PANEL_SUNKEN_FILL,
1453
+ border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
1454
+ borderRadius: 8,
1455
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,.04)"
1456
+ },
1457
+ /** `.ic-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:14px;padding-top:12px;
1458
+ * border-top:1px solid var(--ic-line)}` + **贴下沿吸住**(v3.1 的 `.ic-attnbar.is-sticky` 是它唯一的
1459
+ * 吸底口径:`position:sticky;bottom:6px;z-index:3`;本票按票面「贴下沿」取 0,好让底栏与下沿齐平)。 */
1168
1460
  bar: {
1169
1461
  display: "flex",
1170
1462
  gap: 8,
1171
- marginTop: 12,
1463
+ alignItems: "center",
1172
1464
  flexWrap: "wrap",
1465
+ marginTop: 14,
1466
+ paddingTop: 12,
1467
+ borderTop: "1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06))",
1173
1468
  position: "sticky",
1174
1469
  bottom: 0,
1175
- zIndex: 1,
1176
- background: "var(--dsw-alias-bg-layer-1, #232329)",
1177
- padding: "8px 0",
1178
- borderTop: "1px solid var(--dsw-alias-border, rgba(128,128,128,.25))"
1179
- },
1180
- saveMsg: {
1181
- marginRight: "auto",
1182
- alignSelf: "center",
1183
- color: "var(--dsw-alias-state-warning-primary, #b26a00)",
1184
- fontSize: "0.9em"
1470
+ zIndex: 3,
1471
+ background: "var(--dsw-alias-bg-layer-1, #232324)"
1185
1472
  },
1186
- dirtyDot: {
1187
- color: "var(--dsw-alias-state-warning-primary, #b26a00)",
1188
- fontSize: "0.85em",
1189
- marginLeft: 6,
1190
- fontWeight: 400
1473
+ /** `.ic-bar .ic-note{margin-left:auto;color:var(--ic-faint);font-size:11.5px}`(就绪态那半句)。 */
1474
+ barNote: {
1475
+ marginLeft: "auto",
1476
+ color: "var(--dsw-alias-label-tertiary, #adb2b8)",
1477
+ fontSize: "0.92em"
1191
1478
  },
1192
- followNote: {
1193
- color: "var(--dsw-alias-state-warning-primary, #b26a00)",
1194
- fontSize: "0.85em",
1195
- marginBottom: 4
1479
+ /** 有改动时底栏那半句(黄字,粘在按钮组左侧)。 */
1480
+ saveMsg: {
1481
+ marginLeft: "auto",
1482
+ color: "var(--dsw-alias-state-warn-primary, #e0a33e)",
1483
+ fontSize: "0.92em"
1196
1484
  },
1485
+ /** `.ic-btn{font-size:12px;padding:4px 10px;border-radius:6px;border:1px solid var(--ic-line-strong);
1486
+ * background:transparent;color:var(--ic-text)}`——字号**不写**(#739 ④)。 */
1197
1487
  btn: {
1198
- padding: "4px 12px",
1488
+ padding: "4px 10px",
1199
1489
  borderRadius: 6,
1200
- border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.45))",
1201
- background: "var(--dsw-alias-bg-base, transparent)",
1202
- color: "var(--dsw-alias-label-primary, inherit)",
1203
- cursor: "pointer"
1490
+ border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12))",
1491
+ background: "transparent",
1492
+ color: "var(--dsw-alias-label-primary, #f9fafb)",
1493
+ cursor: "pointer",
1494
+ whiteSpace: "nowrap"
1204
1495
  },
1496
+ /** `.ic-btn.is-primary{background:var(--ic-accent);border-color:var(--ic-accent);color:var(--ic-accent-ink);
1497
+ * font-weight:500}`——`--ic-accent` 取 `--dsw-alias-brand-primary`,前景取 `label-primary-foreground`。 */
1205
1498
  btnPrimary: {
1206
- padding: "4px 12px",
1499
+ padding: "4px 10px",
1207
1500
  borderRadius: 6,
1208
- border: "1px solid var(--dsw-alias-brand-primary, #2f6fed)",
1209
- background: "var(--dsw-alias-brand-primary, #2f6fed)",
1210
- color: "#fff",
1211
- cursor: "pointer"
1212
- },
1213
- advanced: { marginTop: 12 },
1214
- summary: {
1501
+ border: "1px solid var(--dsw-alias-brand-primary, #f9fafb)",
1502
+ background: "var(--dsw-alias-brand-primary, #f9fafb)",
1503
+ color: "var(--dsw-alias-label-primary-foreground, #0f1115)",
1504
+ fontWeight: 500,
1215
1505
  cursor: "pointer",
1506
+ whiteSpace: "nowrap"
1507
+ },
1508
+ /** #934:原先这里有一格 `version`(各家附加块的版本行字面)。版本行已按维护者裁定整条撤掉
1509
+ *(那两家面板内的渲染点 + 六家各自那份字面副本),这一格随之删除:没人取的槽位不留。 */
1510
+ /** `.ic-badge{font-size:11px;line-height:1;padding:4px 9px;border-radius:999px;white-space:nowrap;
1511
+ * display:inline-flex;align-items:center;gap:6px;border:1px solid transparent}`(11px ⇒ `0.88em`)。 */
1512
+ badge: {
1513
+ fontSize: "0.88em",
1514
+ lineHeight: 1,
1515
+ padding: "4px 9px",
1516
+ borderRadius: 999,
1517
+ whiteSpace: "nowrap",
1518
+ display: "inline-flex",
1519
+ alignItems: "center",
1520
+ gap: 6,
1521
+ border: "1px solid transparent"
1522
+ },
1523
+ /** `.ic-badge .ic-bd{width:6px;height:6px;border-radius:50%;background:currentColor}`。 */
1524
+ badgeDot: {
1525
+ width: 6,
1526
+ height: 6,
1527
+ borderRadius: "50%",
1528
+ background: "currentColor"
1529
+ },
1530
+ /** `.ic-spin{width:11px;height:11px;border-radius:50%;border:1.5px solid rgba(0,0,0,.25);
1531
+ * border-top-color:currentColor}`——旋转那半要 `@keyframes`(内联样式给不出),故只画环。 */
1532
+ badgeSpinner: {
1533
+ width: 11,
1534
+ height: 11,
1535
+ borderRadius: "50%",
1536
+ border: "1.5px solid rgba(0,0,0,.25)",
1537
+ borderTopColor: "currentColor",
1538
+ display: "inline-block"
1539
+ },
1540
+ /** `.ic-badge.is-dirty`:`--ic-attn` 实底 + `--ic-attn-ink` 深字(那 3:1 是设计专门解过的对比度)。 */
1541
+ badgeDirty: {
1542
+ color: "#241703",
1543
+ background: "var(--dsw-alias-state-warn-primary, #e0a33e)",
1544
+ borderColor: "var(--dsw-alias-state-warn-primary, #e0a33e)",
1216
1545
  fontWeight: 600
1217
1546
  },
1218
- version: {
1219
- marginTop: 8,
1220
- paddingTop: 8,
1221
- borderTop: "1px dashed var(--dsw-alias-border, rgba(128,128,128,.25))",
1222
- color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
1223
- fontSize: "0.92em"
1547
+ /** `.ic-badge.is-busy`:与 dirty 同色(写的时候还在那一档上)。 */
1548
+ badgeBusy: {
1549
+ color: "#241703",
1550
+ background: "var(--dsw-alias-state-warn-primary, #e0a33e)",
1551
+ borderColor: "var(--dsw-alias-state-warn-primary, #e0a33e)",
1552
+ fontWeight: 600
1553
+ },
1554
+ /** `.ic-badge.is-ok{color:var(--ic-ok);border-color:var(--ic-ok);background:color-mix(in srgb, var(--ic-ok) 14%, transparent)}`。 */
1555
+ badgeOk: {
1556
+ color: "var(--dsw-alias-state-success-primary, #4ec9a0)",
1557
+ borderColor: "var(--dsw-alias-state-success-primary, #4ec9a0)",
1558
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary, #4ec9a0) 14%, transparent)"
1559
+ },
1560
+ /** `.ic-badge.is-bad{color:var(--ic-danger-ink);background:var(--ic-danger);border-color:var(--ic-danger)}`。 */
1561
+ badgeBad: {
1562
+ color: "#fff5f4",
1563
+ background: "var(--dsw-alias-state-error-primary, #e0685f)",
1564
+ borderColor: "var(--dsw-alias-state-error-primary, #e0685f)",
1565
+ fontWeight: 600
1224
1566
  }
1225
1567
  };
1226
1568
  /** 复制那一枚按钮的字面(定稿 v3 第三条:就地变,不弹提示)。 */
@@ -1229,29 +1571,85 @@ window.__ModuleLoader__.load({
1229
1571
  if (state === "failed") return "复制失败";
1230
1572
  return "复制";
1231
1573
  }
1232
- /** 一行输入(四种控件对齐受限 YAML 子集:文本/数字/布尔/目录)。
1574
+ /** 状态 → 徽标那一档(v3.1 `paint()` 里的先后次序:读取失败/写入中/写入失败/值非法/有改动/已保存/就绪)。 */
1575
+ function panelBadgeOf(facts) {
1576
+ if (facts.readFailed) return "readFailed";
1577
+ if (facts.busy) return "busy";
1578
+ if (facts.writeFailed) return "writeFailed";
1579
+ if (facts.invalid) return "invalid";
1580
+ if (facts.dirtyCount > 0) return "dirty";
1581
+ if (facts.saved) return "saved";
1582
+ return "ready";
1583
+ }
1584
+ /** 徽标那一档画的字(v3.1 `badgeHTML` 的字面,只把「已保存 · 刚刚」的演示时刻换成档名)。 */
1585
+ function badgeTextOf(kind, dirtyCount) {
1586
+ if (kind === "dirty") return `${dirtyCount} 项未保存`;
1587
+ if (kind === "busy") return "正在写入";
1588
+ if (kind === "saved") return "已保存";
1589
+ if (kind === "invalid") return "配置里有值用不了";
1590
+ if (kind === "writeFailed") return "写入失败";
1591
+ return "";
1592
+ }
1593
+ /** 徽标那一档的底色(v3.1 的四条 `.ic-badge.is-*`)。 */
1594
+ function badgeStyleOf(kind) {
1595
+ if (kind === "dirty" || kind === "invalid") return {
1596
+ ...S.badge,
1597
+ ...S.badgeDirty
1598
+ };
1599
+ if (kind === "busy") return {
1600
+ ...S.badge,
1601
+ ...S.badgeBusy
1602
+ };
1603
+ if (kind === "saved") return {
1604
+ ...S.badge,
1605
+ ...S.badgeOk
1606
+ };
1607
+ if (kind === "writeFailed") return {
1608
+ ...S.badge,
1609
+ ...S.badgeBad
1610
+ };
1611
+ return S.badge;
1612
+ }
1613
+ /** 画卡片标题行右端那枚状态徽标(就绪态与读取失败那一支不画)。 */
1614
+ function PanelBadgeView(props) {
1615
+ const text = badgeTextOf(props.kind, props.dirtyCount);
1616
+ if (text === "") return null;
1617
+ return react.createElement("span", { style: badgeStyleOf(props.kind) }, props.kind === "busy" ? react.createElement("span", { style: S.badgeSpinner }) : react.createElement("span", { style: S.badgeDot }), text);
1618
+ }
1619
+ /** 一行怎么画(照 v3.1 的两套行模板):
1620
+ *
1621
+ * **只读行**(`.ic-row`)=标签—值同行、值装不下整行让位、行尾一枚「复制」——**不画输入框**
1622
+ * (值来自技能侧解析,页面上本来就不给改),也不画那行说明(v3.1 的 `rowShell` 里没有它)。
1623
+ * **可改行**(`.ic-master`)=标签 +「可改」小徽标 + 说明 + 整行输入框 + 按钮行。
1233
1624
  *
1234
1625
  * 目录档=文本框 + 一枚唤起目录选择的按钮;入口三态是 `none`(命名空间拿不到、或这条路已被拒)
1235
1626
  * **就不画按钮**,文本框照旧——那是该缝自己的契约(供不了就收起入口,不是失败)。
1236
- * **只读行**:控件一律 `disabled`、`onChange` 不接,目录行的按钮**一枚都不画**(定稿 v3 ①)。 */
1627
+ * 只读行:目录入口**一枚都不画**(定稿 v3 ①)。 */
1237
1628
  function Row(props) {
1238
1629
  const { item } = props;
1239
1630
  const readonly = item.readonly === true;
1240
1631
  const disabled = props.disabled || readonly;
1241
- const onChange = readonly ? void 0 : (e) => props.onChange(item.key, e.target.value);
1632
+ const copy = react.createElement("button", {
1633
+ style: S.btnPick,
1634
+ type: "button",
1635
+ disabled: props.onCopy === void 0,
1636
+ onClick: () => props.onCopy?.(item.key, props.value)
1637
+ }, copyLabelOf(props.copyState));
1638
+ const follow = props.follow === true ? react.createElement("span", { style: S.follow }, react.createElement("span", { style: S.followMark }, "↳"), "将跟随更新") : null;
1639
+ if (readonly) {
1640
+ /** 跟随行落 `.ic-under` 那一档缩进;其余按是不是本组最后一行挑有没有下边线。 */
1641
+ const rowStyle = props.nested === true ? props.last === true ? S.followerRowLast : S.followerRow : props.last === true ? S.rowLast : S.row;
1642
+ return react.createElement("div", { style: rowStyle }, react.createElement("span", { style: S.rowKey }, item.title), react.createElement("span", { style: S.rowValue }, props.value), react.createElement("span", { style: S.rowActs }, follow, copy));
1643
+ }
1644
+ const onChange = (e) => props.onChange(item.key, e.target.value);
1242
1645
  const control = item.control === "switch" ? react.createElement("input", {
1243
1646
  type: "checkbox",
1647
+ style: S.checkbox,
1244
1648
  checked: props.value === "true",
1245
1649
  disabled,
1246
- onChange: readonly ? void 0 : (e) => props.onChange(item.key, e.target.checked ? "true" : "false")
1650
+ onChange: (e) => props.onChange(item.key, e.target.checked ? "true" : "false")
1247
1651
  }) : react.createElement("input", {
1248
- style: props.dirty === true ? {
1249
- ...S.input,
1250
- borderColor: "var(--dsw-alias-state-warning-primary, #b26a00)"
1251
- } : readonly && props.follow === true ? {
1252
- ...S.input,
1253
- opacity: .55
1254
- } : S.input,
1652
+ style: props.dirty === true ? S.inputDirty : S.input,
1255
1653
  type: item.control === "number" ? "number" : "text",
1256
1654
  value: props.value,
1257
1655
  disabled,
@@ -1260,21 +1658,13 @@ window.__ModuleLoader__.load({
1260
1658
  });
1261
1659
  /** 目录入口:只读行不画(定稿 v3 ①),供不了(`none`/缺席)也不画。 */
1262
1660
  const entry = props.browser ?? null;
1263
- const browse = item.control === "directory" && !readonly && entry !== null && entry.mode !== "none" ? react.createElement("button", {
1661
+ const browse = item.control === "directory" && entry !== null && entry.mode !== "none" ? react.createElement("button", {
1264
1662
  style: S.btnPick,
1265
1663
  type: "button",
1266
1664
  disabled,
1267
1665
  onClick: () => entry.onOpen(item.key)
1268
1666
  }, "浏览文件夹") : null;
1269
- const copy = react.createElement("button", {
1270
- style: S.btnPick,
1271
- type: "button",
1272
- disabled: props.onCopy === void 0,
1273
- onClick: () => props.onCopy?.(item.key, props.value)
1274
- }, copyLabelOf(props.copyState));
1275
- const acts = react.createElement("div", { style: S.acts }, browse, copy);
1276
- const facts = props.fallback ?? null;
1277
- return react.createElement("div", { style: S.row }, react.createElement("div", { style: S.label }, item.title, props.dirty === true ? react.createElement("span", { style: S.dirtyDot }, "●已改动") : null), react.createElement("div", { style: S.hint }, item.hint), readonly && props.follow === true ? react.createElement("div", { style: S.followNote }, "将跟随更新,保存后生效。") : null, facts === null ? null : react.createElement("div", { style: S.invalid }, "配置里写的是 ", react.createElement("b", null, facts.written), ";现在生效的是 ", react.createElement("b", null, facts.effective), "。"), react.createElement("div", { style: S.pickRow }, control, acts));
1667
+ return react.createElement("div", { style: S.master }, react.createElement("div", { style: S.label }, item.title, react.createElement("span", { style: S.chip }, "可改")), react.createElement("div", { style: S.hint }, item.hint), control, react.createElement("div", { style: S.acts }, browse, copy), props.alert == null ? null : react.createElement("div", { style: S.invalid }, props.alert.message));
1278
1668
  }
1279
1669
  /** 应用内浏览器对话框的文案(面板自带,逐条对应图上那几个位置)。 */
1280
1670
  const BROWSER_LABELS = {
@@ -1301,14 +1691,38 @@ window.__ModuleLoader__.load({
1301
1691
  const surface = state.kind === "ready" ? state.surface : null;
1302
1692
  const headText = title === void 0 ? "配置" : `${title} · 配置`;
1303
1693
  const headDataDir = surface === null ? "" : surface.resolved?.["dbDir"] ?? surface.dataDir;
1304
- const head = react.createElement("div", null, react.createElement("div", { style: S.title }, headText), surface === null ? null : react.createElement("div", null, react.createElement("div", { style: S.info }, `配置文件 ${surface.path}`), react.createElement("div", { style: S.info }, `数据目录 ${headDataDir}`), surface.created ? react.createElement("div", { style: S.muted }, "(配置文件刚按默认值生成)") : null));
1694
+ const ready = state.kind === "ready";
1695
+ const dirtyCount = ready ? props.dirtyKeys.length : 0;
1696
+ const badge = panelBadgeOf({
1697
+ readFailed: state.kind === "failed",
1698
+ busy: props.busy,
1699
+ writeFailed: props.writeError !== null,
1700
+ invalid: props.invalid === true,
1701
+ dirtyCount,
1702
+ saved: props.notice !== null
1703
+ });
1704
+ const head = react.createElement("div", { style: S.head }, react.createElement("div", { style: S.title }, headText), react.createElement(PanelBadgeView, {
1705
+ kind: badge,
1706
+ dirtyCount
1707
+ }));
1708
+ /** #939:这一块的高度由**客户端常量**决定、不由回执时序决定——加载期就把两行连标签一起画出来,
1709
+ * 值位画骨架条(`.ic-sk`),回执到了**原地填值** ⇒ 回执零位移(原先加载期只画一行小灰字、
1710
+ * 回执到了才长出两行,整块下移 21px)。
1711
+ *
1712
+ * 判断式按 `state.kind`:**不许**照 `surface === null` 写——它同时覆盖失败态,照写会让失败屏长出两行假骨架。 */
1713
+ const loading = state.kind === "loading";
1714
+ const fileValue = (text) => text === null ? react.createElement("span", {
1715
+ style: S.skeleton,
1716
+ "data-ilife-skeleton": "value"
1717
+ }) : text;
1718
+ const fileRow = (label, text, tail) => react.createElement("div", { style: S.info }, react.createElement("b", { style: S.fileLabel }, label), fileValue(text), tail === void 0 ? null : tail);
1719
+ const fileLines = !loading && surface === null ? null : react.createElement("div", { style: S.fileWrap }, fileRow("配置文件", surface === null ? null : surface.path, surface !== null && surface.created ? react.createElement("span", { style: S.muted }, "(配置文件刚按默认值生成)") : null), fileRow("数据目录", surface === null ? null : headDataDir));
1305
1720
  const parts = {
1306
1721
  styles: S,
1307
1722
  reply: surface
1308
1723
  };
1309
1724
  const extra = props.extra === void 0 ? null : props.extra(parts);
1310
- if (state.kind === "loading") return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.muted }, "配置读取中"), extra);
1311
- if (state.kind === "failed") return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.error }, state.message), extra, react.createElement("div", { style: S.bar }, react.createElement("button", {
1725
+ if (state.kind === "failed") return react.createElement("div", { style: S.card }, head, fileLines, react.createElement("div", { style: S.error }, state.message), extra, react.createElement("div", { style: S.bar }, react.createElement("button", {
1312
1726
  style: S.btn,
1313
1727
  type: "button",
1314
1728
  onClick: props.onRetry
@@ -1319,37 +1733,49 @@ window.__ModuleLoader__.load({
1319
1733
  }, "重置为默认")));
1320
1734
  const common = props.items.filter((item) => item.tier === "common");
1321
1735
  const advanced = props.items.filter((item) => item.tier === "advanced");
1322
- const renderRow = (item) => react.createElement(Row, {
1736
+ /** 还没读到整面(`loading`)时**框架照画**:行表与控件形状是客户端常量,不必等任何回执。
1737
+ * 值先空着、控件与三枚底栏键一律不可用;回执到了由 `ready` 那一支把值填上(见 `config-panel.ts`)。 */
1738
+ const frozen = props.busy || !ready;
1739
+ const followers = props.followerKeys ?? [];
1740
+ const renderRow = (item, last) => react.createElement(Row, {
1323
1741
  key: item.key,
1324
1742
  item,
1325
1743
  value: props.draft[item.key] ?? "",
1326
- disabled: props.busy,
1744
+ disabled: frozen,
1327
1745
  onChange: props.onChange,
1328
1746
  browser: props.rowEntry,
1329
- dirty: props.dirtyKeys.includes(item.key),
1330
- follow: props.followKeys.includes(item.key),
1331
- onCopy: props.onCopy,
1747
+ dirty: ready && props.dirtyKeys.includes(item.key),
1748
+ follow: ready && props.followKeys.includes(item.key),
1749
+ onCopy: ready ? props.onCopy : void 0,
1332
1750
  copyState: props.copy !== null && props.copy.key === item.key ? props.copy.ok ? "done" : "failed" : "idle",
1333
- fallback: item.readonly !== true && item.prefillFrom !== void 0 ? fallbackFactsOf(item, props.draft[item.key] ?? "", draftSourceOf(surface)) : null
1751
+ alert: ready ? surface?.alerts?.[item.key] ?? null : null,
1752
+ last,
1753
+ nested: followers.includes(item.key)
1334
1754
  });
1335
- const dirtyCount = props.dirtyKeys.length;
1336
- const dirty = dirtyCount > 0;
1337
- return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.rows }, common.map(renderRow)), advanced.length === 0 ? null : react.createElement("details", { style: S.advanced }, react.createElement("summary", { style: S.summary }, ADVANCED_GROUP_TITLE), react.createElement("div", { style: S.muted }, ADVANCED_GROUP_NOTE), advanced.map(renderRow)), extra, react.createElement("div", { style: S.bar }, dirty ? react.createElement("div", { style: S.saveMsg }, `浏览改动后请点保存(${dirtyCount} 项未保存),保存后跟随项自动更新`) : null, react.createElement("button", {
1755
+ /** 每组最后那一行才免掉下边线(`.ic-row:last-child`);母版行自己不带线,不必管。 */
1756
+ const lastIndexOf = (list) => {
1757
+ for (let i = list.length - 1; i >= 0; i -= 1) if (list[i].readonly === true) return i;
1758
+ return -1;
1759
+ };
1760
+ const commonLast = lastIndexOf(common);
1761
+ const advancedLast = lastIndexOf(advanced);
1762
+ const dirty = ready && dirtyCount > 0;
1763
+ return react.createElement("div", { style: S.card }, head, fileLines, react.createElement("div", { style: S.rows }, common.map((item, index) => renderRow(item, index === commonLast))), advanced.length === 0 ? null : react.createElement("details", { style: S.advanced }, react.createElement("summary", { style: S.summary }, react.createElement("span", { style: S.groupMark }, "›"), ADVANCED_GROUP_TITLE), react.createElement("div", { style: S.groupBody }, advanced.map((item, index) => renderRow(item, index === advancedLast)))), extra, react.createElement("div", { style: S.bar }, react.createElement("button", {
1338
1764
  style: dirty ? S.btnPrimary : S.btn,
1339
1765
  type: "button",
1340
- disabled: props.busy || !dirty,
1766
+ disabled: frozen || !dirty,
1341
1767
  onClick: props.onSave
1342
1768
  }, props.busy ? "处理中" : dirty ? `保存(${dirtyCount} 项未保存)` : "保存"), react.createElement("button", {
1343
1769
  style: S.btn,
1344
1770
  type: "button",
1345
- disabled: props.busy,
1771
+ disabled: frozen,
1346
1772
  onClick: props.onReset
1347
1773
  }, "重置为默认"), react.createElement("button", {
1348
1774
  style: S.btn,
1349
1775
  type: "button",
1350
- disabled: props.busy,
1776
+ disabled: frozen,
1351
1777
  onClick: props.onRetry
1352
- }, "重新读取")), props.picking ? react.createElement("div", { style: S.muted }, "已唤起系统文件夹对话框:选中后自动填上,取消则不动。") : null, props.browseRow !== null ? react.createElement(DirectoryBrowserFromRow, {
1778
+ }, "重新读取"), dirty ? react.createElement("div", { style: S.saveMsg }, `浏览改动后请点保存(${dirtyCount} 项未保存),保存后跟随项自动更新`) : ready ? react.createElement("div", { style: S.barNote }, "没有未保存的改动") : react.createElement("div", { style: S.barNote }, "配置读取中")), props.picking ? react.createElement("div", { style: S.muted }, "已唤起系统文件夹对话框:选中后自动填上,取消则不动。") : null, props.browseRow !== null ? react.createElement(DirectoryBrowserFromRow, {
1353
1779
  open: true,
1354
1780
  row: props.browseRow,
1355
1781
  labels: BROWSER_LABELS
@@ -1358,15 +1784,6 @@ window.__ModuleLoader__.load({
1358
1784
  role: "alert"
1359
1785
  }, props.writeError) : null, props.error !== null ? react.createElement("div", { style: S.error }, props.error) : null, props.notice !== null ? react.createElement("div", { style: S.okText }, props.notice) : null);
1360
1786
  }
1361
- /** 「写的值 ≠ 生效值」两个事实要的那几格:整面回执里取值本身、落点顶层那一个、`resolved` 组。 */
1362
- function draftSourceOf(surface) {
1363
- if (surface === null) return { values: {} };
1364
- return {
1365
- values: surface.values,
1366
- dataDir: surface.dataDir,
1367
- resolved: surface.resolved
1368
- };
1369
- }
1370
1787
  //#endregion
1371
1788
  //#region ../plugin-manager/dist/directory-browser-roots.js
1372
1789
  /** 「本机根清单」的取数接线(客户端半):把一通电话变成图上那一行可跳转的根。
@@ -1504,6 +1921,18 @@ window.__ModuleLoader__.load({
1504
1921
  return false;
1505
1922
  }
1506
1923
  }
1924
+ /** **静态**跟随行名单:给行表里每个键喂一遍六家交的钩子,收它返回的跟随者。
1925
+ *
1926
+ * 为什么能这么探:`followKeysOf` 是**纯函数**,六家的实现都只是「脏键里有没有这一格」那一下判断
1927
+ * (各家 `src/client.ts`),喂单键即得「谁跟着它」——**与当刻脏不脏无关**。有了这个集合,共用件就能
1928
+ * 照 v3.1 的 `.ic-under` 把跟随行相对它的可改行缩进(#920 补),**六家源码一个字节不用改**。
1929
+ * 拿不到钩子时回空表(一行都不缩进,照旧平铺)。 */
1930
+ function followerKeysOf(items, hook) {
1931
+ if (hook === void 0) return [];
1932
+ const out = /* @__PURE__ */ new Set();
1933
+ for (const item of items) for (const key of hook([item.key])) out.add(key);
1934
+ return [...out];
1935
+ }
1507
1936
  /** 技能设置页:承载一家自己的全部可配置项(只配置,不干活)。 */
1508
1937
  function ConfigPanel(props) {
1509
1938
  const [state, setState] = react.useState({ kind: "loading" });
@@ -1662,6 +2091,10 @@ window.__ModuleLoader__.load({
1662
2091
  const baseline = surface !== null ? toDraft(props.items, surface.values, surface) : null;
1663
2092
  const dirtyKeys = baseline === null ? [] : dirtyKeysOf(props.items, draft, baseline);
1664
2093
  const followKeys = (props.followKeysOf ?? noFollowKeys)(dirtyKeys);
2094
+ /** 技能侧报出「有格子真用不了」⇒ 标题行那枚「配置里有值用不了」徽标才亮(#915 第二步)。 */
2095
+ const hasAlert = surface !== null && surface.alerts !== void 0 && Object.keys(surface.alerts).length > 0;
2096
+ /** 静态那一份(与脏不脏无关):画跟随行的缩进用(v3.1 的 `.ic-under`)。 */
2097
+ const followerKeys = followerKeysOf(props.items, props.followKeysOf);
1665
2098
  const mode = pickerModeOf(picker);
1666
2099
  const rowEntry = mode === "none" ? null : {
1667
2100
  mode,
@@ -1681,6 +2114,8 @@ window.__ModuleLoader__.load({
1681
2114
  rowEntry,
1682
2115
  dirtyKeys,
1683
2116
  followKeys,
2117
+ followerKeys,
2118
+ invalid: hasAlert,
1684
2119
  copy,
1685
2120
  onCopy,
1686
2121
  onChange,