@morehao/dsh-skills-console 0.3.5 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/client.js +212 -128
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@ DSH(DeepSeek Harness)的**技能控制台**:装、更新、删除、接入
37
37
  - **两个入口**:默认在「设置 → 技能控制台」;页头 ⚙ 里可切到左侧栏独立面板,切换立即生效。
38
38
  - **一页两 tab**:已安装 / 订阅。页头右侧是「自动接入」(多选 Agent)与「添加技能 / 添加订阅」。
39
39
  - **每行五个动作**:开关(启停)、查看、更新、接入设置、删除;窄面板下隐藏「查看」,技能名本身仍是入口。状态标签覆盖最新 / 可更新 / 本地已改 / 上游已移除 / 软链断裂 / 无上游等。
40
- - **窄面板不跳版**:页头动作恒在右上角,只有面板窄到 520px 以下才上下堆叠;列表按面板自身宽度在 860px 处切两列;页面预留滚动条槽位,滚动条出现/消失、切 tab、窗口微调都不会让整页翻面。
40
+ - **窄面板不跳版**:页头按内容自然换行——放得下时动作组在标题行右侧,放不下(约 630px 以下,如设置页)就整组换到下一行并靠右,插件名始终完整;列表按面板自身宽度在 860px 处切两列;滚动只发生在列表区(页头、工具栏、筛选条固定不动),滚动条出现/消失、切 tab、窗口微调都不会让整页翻面。
41
41
  - **Agent 多了不挤**:每行最多展示 3 个,其余折成 `+n`,悬浮或聚焦即可看全。
42
42
  - **找得到**:支持按名称 / 来源 / Agent 搜索,按「可更新 / 待检查 / 已停用」筛选,平铺或按 Agent 分组。
43
43
  - **待检查 vs 无上游**:「待检查」只表示*这次*没拿到上游结论(限流、网络、仓库 404 等);再次点「检查更新」时,如果上次没有结论、上游又只回了 304,会无视 ETag 完整重读一次树,不会把「待检查」永久粘住。没有可用订阅来源的技能标「无上游」——它们无法参与更新检查,不计入待检查。
package/client.js CHANGED
@@ -470,8 +470,45 @@ try {
470
470
  } catch {
471
471
  ReactDOM = null;
472
472
  }
473
- function portalize(node) {
474
- return ReactDOM?.createPortal && typeof document !== "undefined" && document.body ? ReactDOM.createPortal(node, document.body) : node;
473
+ function overlayHost() {
474
+ if (typeof document === "undefined") return null;
475
+ const root = document.querySelector?.(".dshsc-root");
476
+ const dialog = root?.closest?.('[role="dialog"]');
477
+ return dialog || document.querySelector?.("body") || null;
478
+ }
479
+ function portalize(node, target) {
480
+ const host = target || (typeof document !== "undefined" ? document.body : null);
481
+ return ReactDOM?.createPortal && host ? ReactDOM.createPortal(node, host) : node;
482
+ }
483
+ var overlayLayers = [];
484
+ var escapeGuarded = false;
485
+ function onOverlayEscape(event) {
486
+ if (event.key !== "Escape" || overlayLayers.length === 0) return;
487
+ event.preventDefault?.();
488
+ event.stopPropagation?.();
489
+ event.stopImmediatePropagation?.();
490
+ overlayLayers[overlayLayers.length - 1].close();
491
+ }
492
+ function useOverlayLayer(active, close) {
493
+ const closeRef = import_react.default.useRef(close);
494
+ closeRef.current = close;
495
+ import_react.default.useEffect(() => {
496
+ if (!active || typeof document === "undefined") return void 0;
497
+ const layer = { close: () => closeRef.current?.() };
498
+ overlayLayers.push(layer);
499
+ if (!escapeGuarded) {
500
+ document.addEventListener("keydown", onOverlayEscape, true);
501
+ escapeGuarded = true;
502
+ }
503
+ return () => {
504
+ const index = overlayLayers.indexOf(layer);
505
+ if (index >= 0) overlayLayers.splice(index, 1);
506
+ if (overlayLayers.length === 0 && escapeGuarded) {
507
+ document.removeEventListener("keydown", onOverlayEscape, true);
508
+ escapeGuarded = false;
509
+ }
510
+ };
511
+ }, [active]);
475
512
  }
476
513
 
477
514
  // client/icons.js
@@ -815,32 +852,45 @@ var import_react5 = __toESM(require("react"), 1);
815
852
 
816
853
  // client/styles.js
817
854
  var CSS = `
818
- /* 主面板形态:与 DSH 自带管理页同一边距/滚动方式。
819
- scrollbar-gutter: stable 让滚动条出现/消失都不改变内容宽度——否则列表一长出来,
820
- 内容就窄 5~15px,贴着断点的布局会整页跳一次。 */
821
- .dshsc-page { box-sizing: border-box; width: 100%; height: 100%; overflow: auto;
822
- scrollbar-gutter: stable;
855
+ /* 主面板形态:页面本身固定不滚,滚动条只出现在列表区(.dshsc-listscroll)。
856
+ 页头 / 工具栏 / 筛选条是 flex:none 的固定行,行区域 flex:1 + overflow:auto。
857
+ scrollbar-gutter 跟着滚动条一起挪到列表区:行区域变长/变短都不改变自身宽度,
858
+ 贴着 860 断点的布局不会因为滚动条出现整页翻面。 */
859
+ .dshsc-page { box-sizing: border-box; width: 100%; height: 100%; overflow: hidden;
823
860
  padding: calc(28px + var(--dsh-frame-top-clearance, 0px)) clamp(24px, 4vw, 48px) 48px; }
824
861
  .dshsc-page .dshsc-root { max-width: 980px; margin: 0 auto; }
825
862
  .dshsc-page .dshsc-title { font-size: 20px; font-weight: 500; line-height: 28px; }
826
863
  .dshsc-page .dshsc-sub { font-size: 13px; line-height: 20px; }
827
- .dshsc-root { display: flex; flex-direction: column; gap: 14px; color: var(--dsw-alias-label-primary);
864
+ /* 高度取满挂载面:侧栏 main 是 100%,设置页是宿主 .options 的内容高。
865
+ 宿主没给确定高度时(height:100% 解析为 auto)退化成外层滚动,不会把内容裁掉。 */
866
+ .dshsc-root { display: flex; flex-direction: column; gap: 14px; height: 100%; min-height: 0;
867
+ color: var(--dsw-alias-label-primary);
828
868
  container-type: inline-size; container-name: dshsc; }
829
- /* 页头两列:左边标题块、右边业务动作。动作组恒在右上角,宽度自动;
830
- 页面宽度变化只让标题块收窄(身份胶囊省略、工具组内部换行),动作组不会整组跳到第二行。
831
- 只有面板真的窄(≤520,见下面容器查询)才改成上下两段。 */
832
- .dshsc-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 14px; }
833
- .dshsc-heading { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
869
+ /* 页头 / 工具栏 / 顶部提示条高度不参与压缩;tab 面板吃满剩余高度,
870
+ 面板里的筛选条与说明固定,只有行区域(.dshsc-listscroll)自己出现滚动条。 */
871
+ .dshsc-root > .dshsc-head, .dshsc-root > .dshsc-toolbar,
872
+ .dshsc-root > .dshsc-banner, .dshsc-root > .dshsc-meta { flex: none; }
873
+ .dshsc-tabpane { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 8px; }
874
+ .dshsc-tabpane > .dshsc-banner, .dshsc-tabpane > .dshsc-notice,
875
+ .dshsc-tabpane > .dshsc-listbar { flex: none; }
876
+ .dshsc-listscroll { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain;
877
+ scrollbar-gutter: stable; }
878
+ /* 页头:左标题块、右业务动作。动作组不靠固定断点换行——标题块的 flex-basis 用内容宽度
879
+ (auto),放得下就同排、放不下(约 630px 以下)动作组自然换到第二行并靠右。
880
+ 这样窄面板(设置页 ~565px)会回到「动作在下、标识在上」,身份胶囊永远是完整的,
881
+ 也不会像 900px 断点那样被滚动条/几像素窗口变化推过临界点。 */
882
+ .dshsc-head { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
883
+ .dshsc-heading { display: flex; align-items: flex-start; gap: 10px; min-width: 0; flex: 1 1 auto; }
834
884
  .dshsc-heading-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
835
885
  .dshsc-logo { flex: none; display: block; border-radius: 8px; }
836
886
  /* 标题不参与收窄:窄面板里让身份胶囊先省略,标题本身不折行 */
837
887
  .dshsc-title { font-size: 17px; font-weight: 600; letter-spacing: -.01em; margin: 0; flex: none; white-space: nowrap; }
838
888
  .dshsc-sub { font-size: 12px; line-height: 1.55; color: var(--dsw-alias-label-secondary); margin: 0; max-width: 64ch; }
839
889
  .dshsc-spacer { flex: 1; }
840
- .dshsc-head-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: wrap; }
841
- /* 标题行恒为一行:身份胶囊按剩余宽度省略,工具组不被挤走。
890
+ .dshsc-head-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: wrap; margin-left: auto; }
891
+ /* 标题行恒为一行:身份胶囊只在面板极窄(连标题行都放不下)时才省略,工具组不被挤走。
842
892
  不用 wrap 是因为一 wrap 就会按「身份胶囊整段」的宽度断行,窄面板里会折成 2~3 行、
843
- 把页头撑高;省略号 + title 提示比整行折行更稳,也不随版本号变长而改变布局。 */
893
+ 把页头撑高;行内换行交给外层页头(动作组下移),标识本身保持完整。 */
844
894
  .dshsc-title-row { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; min-width: 0; }
845
895
 
846
896
  /* 身份:按「安装时输入的包名」显示(@scope/name@version),等宽只读、不描边 */
@@ -1081,13 +1131,6 @@ var CSS = `
1081
1131
  .dshsc-row .dshsc-actions { grid-column: 2; grid-row: 1 / span 2; }
1082
1132
  .dshsc-row .dshsc-btn { padding: 0 5px; }
1083
1133
  }
1084
- /* 页头自己的断点,和上面的页面断点解耦:只有面板窄到放不下「标题块 + 动作组」才上下堆叠。
1085
- 520 远低于设置面板/侧栏面板的实际宽度,正常窗口里页头永远是右上角那一组。 */
1086
- @container dshsc (max-width: 520px) {
1087
- .dshsc-head { grid-template-columns: minmax(0, 1fr); }
1088
- .dshsc-head-actions { justify-content: flex-end; }
1089
- .dshsc-tools { border-left: 0; padding-left: 0; margin-left: 0; }
1090
- }
1091
1134
  .dshsc-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
1092
1135
 
1093
1136
  /* 与 DSH 自带开关同尺寸/同 token:关是 border-l3 轨道,开是 brand-primary 轨道 */
@@ -1319,8 +1362,11 @@ var CSS = `
1319
1362
  .dshsc-md-table th { background: var(--dsw-alias-bg-layer-2); font-weight: 600; }
1320
1363
  .dshsc-md-table tr:last-child td { border-bottom: 0; }
1321
1364
 
1322
- /* 整页(A1):返回条 + hero + 目录 + 720px 正文 */
1323
- .dshsc-reader-page { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
1365
+ /* 整页(A1):返回条 + hero + 目录 + 720px 正文。
1366
+ root 高度固定后,整页阅读器也自己撑满:顶栏 / hero 固定,正文列内部滚动,
1367
+ 返回按钮不会跟着长文滚走。 */
1368
+ .dshsc-reader-page { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 12px; }
1369
+ .dshsc-reader-page > .dshsc-reader-topbar, .dshsc-reader-page > .dshsc-reader-hero { flex: none; }
1324
1370
  .dshsc-reader-topbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1325
1371
  .dshsc-reader-topbar .dshsc-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px;
1326
1372
  box-sizing: border-box; height: 28px; padding: 0 10px; font-size: 12px; line-height: 1; white-space: nowrap; }
@@ -1340,7 +1386,8 @@ var CSS = `
1340
1386
  background: var(--dsw-alias-bg-layer-1); }
1341
1387
  .dshsc-reader-hero .dshsc-reader-head { border-bottom: 0; }
1342
1388
  .dshsc-reader-hero .dshsc-reader-desc { -webkit-line-clamp: 3; max-width: 78ch; }
1343
- .dshsc-reader-layout { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 18px; align-items: start; }
1389
+ .dshsc-reader-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: 180px minmax(0, 1fr);
1390
+ gap: 18px; align-items: stretch; }
1344
1391
  .dshsc-reader-toc { position: sticky; top: 0; display: flex; flex-direction: column; gap: 1px;
1345
1392
  max-height: 70vh; overflow: auto; padding-right: 4px; }
1346
1393
  .dshsc-reader-toc-title { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase;
@@ -1354,7 +1401,8 @@ var CSS = `
1354
1401
  .dshsc-reader-tocitem.on { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);
1355
1402
  color: var(--dsw-alias-brand-primary); }
1356
1403
  .dshsc-reader-tocitem:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
1357
- .dshsc-reader-col { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--dsw-alias-border-l1);
1404
+ .dshsc-reader-col { display: flex; flex-direction: column; min-width: 0; min-height: 0;
1405
+ border: 1px solid var(--dsw-alias-border-l1);
1358
1406
  border-radius: 12px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
1359
1407
  .dshsc-reader-col .dshsc-reader-tabs { border-top: 0; }
1360
1408
  .dshsc-reader-page .dshsc-reader-body { max-height: none; }
@@ -2203,17 +2251,35 @@ function InstalledTab({
2203
2251
  ) : null;
2204
2252
  }
2205
2253
  function content() {
2206
- if (!data) return h("div", { className: "dshsc-empty" }, t("empty.loading"));
2254
+ if (!data) {
2255
+ return h(
2256
+ "div",
2257
+ { className: "dshsc-tabpane" },
2258
+ h(
2259
+ "div",
2260
+ { className: "dshsc-listscroll" },
2261
+ h("div", { className: "dshsc-empty" }, t("empty.loading"))
2262
+ )
2263
+ );
2264
+ }
2207
2265
  if (rows.length === 0) {
2208
2266
  return h(
2209
2267
  "div",
2210
- { className: "dshsc-empty" },
2211
- h("div", { className: "dshsc-name" }, t("empty.skills")),
2212
- h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.skillsHint")),
2268
+ { className: "dshsc-tabpane" },
2213
2269
  h(
2214
2270
  "div",
2215
- { style: { marginTop: 10 } },
2216
- h("button", { className: "dshsc-btn primary", disabled: !writable, onClick: actions.onAddSkill }, t("action.addSkill"))
2271
+ { className: "dshsc-listscroll" },
2272
+ h(
2273
+ "div",
2274
+ { className: "dshsc-empty" },
2275
+ h("div", { className: "dshsc-name" }, t("empty.skills")),
2276
+ h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.skillsHint")),
2277
+ h(
2278
+ "div",
2279
+ { style: { marginTop: 10 } },
2280
+ h("button", { className: "dshsc-btn primary", disabled: !writable, onClick: actions.onAddSkill }, t("action.addSkill"))
2281
+ )
2282
+ )
2217
2283
  )
2218
2284
  );
2219
2285
  }
@@ -2231,11 +2297,15 @@ function InstalledTab({
2231
2297
  ))) : h("div", { className: "dshsc-list" }, visibleRows.map(rowView));
2232
2298
  return h(
2233
2299
  "div",
2234
- { style: { display: "flex", flexDirection: "column", gap: 8 } },
2300
+ { className: "dshsc-tabpane" },
2235
2301
  visibleOnlyBanners(),
2236
2302
  builtinNotice(),
2237
2303
  listBar(),
2238
- visibleRows.length === 0 ? filteredEmpty() : body
2304
+ h(
2305
+ "div",
2306
+ { className: "dshsc-listscroll" },
2307
+ visibleRows.length === 0 ? filteredEmpty() : body
2308
+ )
2239
2309
  );
2240
2310
  }
2241
2311
  return content();
@@ -2287,90 +2357,100 @@ function SubscriptionsTab({
2287
2357
  if (subscriptions.length === 0) {
2288
2358
  return h(
2289
2359
  "div",
2290
- { className: "dshsc-empty" },
2291
- h("div", { className: "dshsc-name" }, t("empty.subs")),
2292
- h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.subsHint")),
2360
+ { className: "dshsc-tabpane" },
2293
2361
  h(
2294
2362
  "div",
2295
- { style: { marginTop: 10 } },
2296
- h("button", { className: "dshsc-btn primary", disabled: !writable, onClick: onAdd }, t("action.addSubscription"))
2363
+ { className: "dshsc-listscroll" },
2364
+ h(
2365
+ "div",
2366
+ { className: "dshsc-empty" },
2367
+ h("div", { className: "dshsc-name" }, t("empty.subs")),
2368
+ h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.subsHint")),
2369
+ h(
2370
+ "div",
2371
+ { style: { marginTop: 10 } },
2372
+ h("button", { className: "dshsc-btn primary", disabled: !writable, onClick: onAdd }, t("action.addSubscription"))
2373
+ )
2374
+ )
2297
2375
  )
2298
2376
  );
2299
2377
  }
2300
- return h("div", { className: "dshsc-list" }, subscriptions.map((subscription) => {
2301
- const entries = subscription.snapshot?.entries || [];
2302
- const installedNames = new Set(rows.filter((row) => row.subscriptionId === subscription.id).map((row) => row.name));
2303
- const open = Boolean(expanded[subscription.id]);
2304
- return h(
2378
+ return h(
2379
+ "div",
2380
+ { className: "dshsc-tabpane" },
2381
+ h(
2305
2382
  "div",
2306
- { className: "dshsc-sub-block", key: subscription.id },
2307
- h(
2308
- "div",
2309
- { className: "dshsc-sub-row" },
2310
- h("button", {
2311
- className: "dshsc-caret",
2312
- "aria-expanded": String(open),
2313
- "aria-label": `${subscription.source} ${open ? t("action.collapse") : t("action.expand")}`,
2314
- onClick: () => onToggle(subscription.id)
2315
- }, h(CaretIcon, { open })),
2316
- h("span", { className: "dshsc-name" }, subscription.source),
2317
- h("span", { className: "dshsc-tag" }, subscription.ref || t("subscription.defaultRef")),
2318
- h(
2319
- "span",
2320
- { className: "dshsc-substat" },
2321
- `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedNames.size}`
2322
- ),
2323
- h(
2324
- "div",
2325
- { className: "dshsc-actions" },
2326
- h("button", {
2327
- className: "dshsc-btn sq",
2328
- disabled: busy !== null,
2329
- title: t("action.checkUpdates"),
2330
- "aria-label": `${t("action.checkUpdates")} ${subscription.source}`,
2331
- onClick: () => void onRefresh(subscription)
2332
- }, h(RefreshIcon, { size: 13 })),
2333
- h("button", { className: "dshsc-btn ghost danger", disabled: busy !== null, onClick: () => void onDelete(subscription) }, t("action.unsubscribe"))
2334
- )
2335
- ),
2336
- open ? h("div", { className: "dshsc-sub-list" }, entries.map((entry) => {
2337
- const installedAlready = installedNames.has(entry.name);
2383
+ { className: "dshsc-listscroll" },
2384
+ h("div", { className: "dshsc-list" }, subscriptions.map((subscription) => {
2385
+ const entries = subscription.snapshot?.entries || [];
2386
+ const installedNames = new Set(rows.filter((row) => row.subscriptionId === subscription.id).map((row) => row.name));
2387
+ const open = Boolean(expanded[subscription.id]);
2338
2388
  return h(
2339
2389
  "div",
2340
- { className: "dshsc-sub-entry", key: entry.name },
2341
- h("span", { className: "dshsc-name" }, entry.name),
2342
- h(
2343
- "span",
2344
- { className: `dshsc-state${installedAlready ? " ok" : ""}` },
2345
- installedAlready ? "✓ " : "",
2346
- installedAlready ? t("installed") : t("notInstalled")
2347
- ),
2390
+ { className: "dshsc-sub-block", key: subscription.id },
2348
2391
  h(
2349
2392
  "div",
2350
- { className: "dshsc-actions" },
2351
- installedAlready ? null : h("button", {
2352
- className: "dshsc-btn",
2353
- disabled: !writable || busy !== null,
2354
- onClick: () => void onInstall(subscription, entry)
2355
- }, t("action.install"))
2356
- )
2393
+ { className: "dshsc-sub-row" },
2394
+ h("button", {
2395
+ className: "dshsc-caret",
2396
+ "aria-expanded": String(open),
2397
+ "aria-label": `${subscription.source} ${open ? t("action.collapse") : t("action.expand")}`,
2398
+ onClick: () => onToggle(subscription.id)
2399
+ }, h(CaretIcon, { open })),
2400
+ h("span", { className: "dshsc-name" }, subscription.source),
2401
+ h("span", { className: "dshsc-tag" }, subscription.ref || t("subscription.defaultRef")),
2402
+ h(
2403
+ "span",
2404
+ { className: "dshsc-substat" },
2405
+ `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedNames.size}`
2406
+ ),
2407
+ h(
2408
+ "div",
2409
+ { className: "dshsc-actions" },
2410
+ h("button", {
2411
+ className: "dshsc-btn sq",
2412
+ disabled: busy !== null,
2413
+ title: t("action.checkUpdates"),
2414
+ "aria-label": `${t("action.checkUpdates")} ${subscription.source}`,
2415
+ onClick: () => void onRefresh(subscription)
2416
+ }, h(RefreshIcon, { size: 13 })),
2417
+ h("button", { className: "dshsc-btn ghost danger", disabled: busy !== null, onClick: () => void onDelete(subscription) }, t("action.unsubscribe"))
2418
+ )
2419
+ ),
2420
+ open ? h("div", { className: "dshsc-sub-list" }, entries.map((entry) => {
2421
+ const installedAlready = installedNames.has(entry.name);
2422
+ return h(
2423
+ "div",
2424
+ { className: "dshsc-sub-entry", key: entry.name },
2425
+ h("span", { className: "dshsc-name" }, entry.name),
2426
+ h(
2427
+ "span",
2428
+ { className: `dshsc-state${installedAlready ? " ok" : ""}` },
2429
+ installedAlready ? "✓ " : "",
2430
+ installedAlready ? t("installed") : t("notInstalled")
2431
+ ),
2432
+ h(
2433
+ "div",
2434
+ { className: "dshsc-actions" },
2435
+ installedAlready ? null : h("button", {
2436
+ className: "dshsc-btn",
2437
+ disabled: !writable || busy !== null,
2438
+ onClick: () => void onInstall(subscription, entry)
2439
+ }, t("action.install"))
2440
+ )
2441
+ );
2442
+ })) : null
2357
2443
  );
2358
- })) : null
2359
- );
2360
- }));
2444
+ }))
2445
+ )
2446
+ );
2361
2447
  }
2362
2448
 
2363
2449
  // client/views/modals.js
2364
2450
  var import_react3 = __toESM(require("react"), 1);
2365
2451
  function Overlay({ t, title, children, footer, onCancel }) {
2366
- import_react3.default.useEffect(() => {
2367
- const onKey = (event) => {
2368
- if (event.key === "Escape") onCancel?.();
2369
- };
2370
- window.addEventListener("keydown", onKey);
2371
- return () => window.removeEventListener("keydown", onKey);
2372
- }, [onCancel]);
2373
- return h(
2452
+ useOverlayLayer(true, onCancel);
2453
+ return portalize(h(
2374
2454
  "div",
2375
2455
  { className: "dshsc-overlay", onClick: (event) => {
2376
2456
  if (event.target === event.currentTarget) onCancel?.();
@@ -2390,7 +2470,7 @@ function Overlay({ t, title, children, footer, onCancel }) {
2390
2470
  children,
2391
2471
  h("div", { className: "dshsc-modal-foot" }, footer)
2392
2472
  )
2393
- );
2473
+ ), overlayHost());
2394
2474
  }
2395
2475
  function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
2396
2476
  const [form, setForm] = import_react3.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
@@ -3133,20 +3213,21 @@ var MARKDOWN = /\.(md|markdown)$/i;
3133
3213
  function isMarkdownPath(relPath) {
3134
3214
  return MARKDOWN.test(String(relPath || ""));
3135
3215
  }
3216
+ function isScroller(node) {
3217
+ if (!node || typeof getComputedStyle !== "function") return false;
3218
+ const style = getComputedStyle(node);
3219
+ return Boolean(style && /(auto|scroll)/.test(style.overflowY || "") && node.scrollHeight > node.clientHeight + 4);
3220
+ }
3136
3221
  function nearestScroller(node) {
3137
3222
  let current = node?.parentElement || null;
3138
3223
  while (current) {
3139
- if (typeof getComputedStyle === "function") {
3140
- const style = getComputedStyle(current);
3141
- if (style && /(auto|scroll)/.test(style.overflowY || "") && current.scrollHeight > current.clientHeight + 4) {
3142
- return current;
3143
- }
3144
- }
3224
+ if (isScroller(current)) return current;
3145
3225
  current = current.parentElement || null;
3146
3226
  }
3147
3227
  return typeof document !== "undefined" ? document.scrollingElement || null : null;
3148
3228
  }
3149
3229
  function scrollHost(node) {
3230
+ if (isScroller(node)) return node;
3150
3231
  return node && nearestScroller(node) || node;
3151
3232
  }
3152
3233
  var STATUS_KEY = {
@@ -3338,6 +3419,7 @@ function SkillReader({
3338
3419
  const bodyRef = import_react4.default.useRef(null);
3339
3420
  const dialogRef = import_react4.default.useRef(null);
3340
3421
  const scrollRef = import_react4.default.useRef(0);
3422
+ useOverlayLayer(variant === "drawer" || variant === "page", onClose);
3341
3423
  const name = row?.name || "";
3342
3424
  const tree = state.tree;
3343
3425
  const files = tree?.files || [];
@@ -3364,11 +3446,6 @@ function SkillReader({
3364
3446
  const node = dialogRef.current;
3365
3447
  node?.focus?.();
3366
3448
  const onKeyDown = (event) => {
3367
- if (event?.key === "Escape") {
3368
- event.stopPropagation?.();
3369
- onClose?.();
3370
- return;
3371
- }
3372
3449
  if (event?.key !== "Tab" || !node?.querySelectorAll) return;
3373
3450
  const focusables = Array.from(
3374
3451
  node.querySelectorAll('a[href],button:not([disabled]),[tabindex]:not([tabindex="-1"])')
@@ -3384,9 +3461,9 @@ function SkillReader({
3384
3461
  first.focus?.();
3385
3462
  }
3386
3463
  };
3387
- document.addEventListener("keydown", onKeyDown);
3464
+ document.addEventListener("keydown", onKeyDown, true);
3388
3465
  return () => {
3389
- document.removeEventListener("keydown", onKeyDown);
3466
+ document.removeEventListener("keydown", onKeyDown, true);
3390
3467
  previous?.focus?.();
3391
3468
  };
3392
3469
  }, [variant, onClose]);
@@ -3811,7 +3888,7 @@ function SkillReader({
3811
3888
  ref: dialogRef,
3812
3889
  tabIndex: -1
3813
3890
  }, header, tabsBar, body, actionsFooter)
3814
- ));
3891
+ ), overlayHost());
3815
3892
  }
3816
3893
 
3817
3894
  // client/section.js
@@ -3828,6 +3905,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3828
3905
  const [viewer, setViewer] = import_react5.default.useState(null);
3829
3906
  const rootRef = import_react5.default.useRef(null);
3830
3907
  const scrollPos = import_react5.default.useRef(null);
3908
+ const restoreScroll = import_react5.default.useRef(null);
3831
3909
  const [filter, setFilter] = import_react5.default.useState("all");
3832
3910
  const [builtinOpen, setBuiltinOpen] = import_react5.default.useState(false);
3833
3911
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
@@ -3875,30 +3953,36 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3875
3953
  }
3876
3954
  return typeof document !== "undefined" ? document.scrollingElement || null : null;
3877
3955
  }
3956
+ function listScroller() {
3957
+ const node = rootRef.current?.querySelector?.(".dshsc-listscroll");
3958
+ if (node && node.scrollHeight > node.clientHeight) return node;
3959
+ return getScrollParent(rootRef.current);
3960
+ }
3878
3961
  function openViewer(row, mode = "drawer") {
3879
3962
  if (!row) return;
3880
3963
  if (mode === "page") {
3881
- scrollPos.current = getScrollParent(rootRef.current)?.scrollTop ?? 0;
3964
+ scrollPos.current = listScroller()?.scrollTop ?? 0;
3882
3965
  }
3883
3966
  setViewer({ name: row.name, mode });
3884
3967
  }
3885
3968
  function openPage() {
3886
3969
  setViewer((prev) => {
3887
3970
  if (!prev) return prev;
3888
- scrollPos.current = getScrollParent(rootRef.current)?.scrollTop ?? 0;
3971
+ scrollPos.current = listScroller()?.scrollTop ?? 0;
3889
3972
  return { ...prev, mode: "page" };
3890
3973
  });
3891
3974
  }
3892
3975
  function closeViewer() {
3893
- const restore = viewer?.mode === "page" ? scrollPos.current : null;
3976
+ if (viewer?.mode === "page") restoreScroll.current = scrollPos.current;
3894
3977
  setViewer(null);
3895
- if (restore === null || typeof window === "undefined") return;
3896
- const apply2 = () => {
3897
- const scroller = getScrollParent(rootRef.current);
3898
- if (scroller) scroller.scrollTop = restore;
3899
- };
3900
- if (typeof window.requestAnimationFrame === "function") window.requestAnimationFrame(apply2);
3901
3978
  }
3979
+ import_react5.default.useLayoutEffect(() => {
3980
+ if (restoreScroll.current === null) return;
3981
+ const target = restoreScroll.current;
3982
+ restoreScroll.current = null;
3983
+ const scroller = listScroller();
3984
+ if (scroller) scroller.scrollTop = target;
3985
+ }, [viewer]);
3902
3986
  async function changePlacement(next) {
3903
3987
  if (next === placement || placementBusy) return;
3904
3988
  setPlacementBusy(next);
@@ -4027,7 +4111,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4027
4111
  h("style", null, CSS),
4028
4112
  viewer?.mode === "page" ? null : listNode,
4029
4113
  readerNode,
4030
- toast ? h("div", { className: `dshsc-toast ${toast.kind}` }, toast.text) : null,
4114
+ toast ? portalize(h("div", { className: `dshsc-toast ${toast.kind}` }, toast.text), overlayHost()) : null,
4031
4115
  modal ? h(Modals, {
4032
4116
  t,
4033
4117
  modal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-skills-console",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。A skill manager for DeepSeek Harness: install, update, remove, link and toggle agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "morehao <morehao@qq.com>",