@morehao/dsh-skills-console 0.3.6 → 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 +203 -113
  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
- - **窄面板不跳版**:页头按内容自然换行——放得下时动作组在标题行右侧,放不下(约 630px 以下,如设置页)就整组换到下一行并靠右,插件名始终完整;列表按面板自身宽度在 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,17 +852,29 @@ 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; }
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; }
829
878
  /* 页头:左标题块、右业务动作。动作组不靠固定断点换行——标题块的 flex-basis 用内容宽度
830
879
  (auto),放得下就同排、放不下(约 630px 以下)动作组自然换到第二行并靠右。
831
880
  这样窄面板(设置页 ~565px)会回到「动作在下、标识在上」,身份胶囊永远是完整的,
@@ -1313,8 +1362,11 @@ var CSS = `
1313
1362
  .dshsc-md-table th { background: var(--dsw-alias-bg-layer-2); font-weight: 600; }
1314
1363
  .dshsc-md-table tr:last-child td { border-bottom: 0; }
1315
1364
 
1316
- /* 整页(A1):返回条 + hero + 目录 + 720px 正文 */
1317
- .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; }
1318
1370
  .dshsc-reader-topbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1319
1371
  .dshsc-reader-topbar .dshsc-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px;
1320
1372
  box-sizing: border-box; height: 28px; padding: 0 10px; font-size: 12px; line-height: 1; white-space: nowrap; }
@@ -1334,7 +1386,8 @@ var CSS = `
1334
1386
  background: var(--dsw-alias-bg-layer-1); }
1335
1387
  .dshsc-reader-hero .dshsc-reader-head { border-bottom: 0; }
1336
1388
  .dshsc-reader-hero .dshsc-reader-desc { -webkit-line-clamp: 3; max-width: 78ch; }
1337
- .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; }
1338
1391
  .dshsc-reader-toc { position: sticky; top: 0; display: flex; flex-direction: column; gap: 1px;
1339
1392
  max-height: 70vh; overflow: auto; padding-right: 4px; }
1340
1393
  .dshsc-reader-toc-title { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase;
@@ -1348,7 +1401,8 @@ var CSS = `
1348
1401
  .dshsc-reader-tocitem.on { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);
1349
1402
  color: var(--dsw-alias-brand-primary); }
1350
1403
  .dshsc-reader-tocitem:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
1351
- .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);
1352
1406
  border-radius: 12px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
1353
1407
  .dshsc-reader-col .dshsc-reader-tabs { border-top: 0; }
1354
1408
  .dshsc-reader-page .dshsc-reader-body { max-height: none; }
@@ -2197,17 +2251,35 @@ function InstalledTab({
2197
2251
  ) : null;
2198
2252
  }
2199
2253
  function content() {
2200
- 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
+ }
2201
2265
  if (rows.length === 0) {
2202
2266
  return h(
2203
2267
  "div",
2204
- { className: "dshsc-empty" },
2205
- h("div", { className: "dshsc-name" }, t("empty.skills")),
2206
- h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.skillsHint")),
2268
+ { className: "dshsc-tabpane" },
2207
2269
  h(
2208
2270
  "div",
2209
- { style: { marginTop: 10 } },
2210
- 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
+ )
2211
2283
  )
2212
2284
  );
2213
2285
  }
@@ -2225,11 +2297,15 @@ function InstalledTab({
2225
2297
  ))) : h("div", { className: "dshsc-list" }, visibleRows.map(rowView));
2226
2298
  return h(
2227
2299
  "div",
2228
- { style: { display: "flex", flexDirection: "column", gap: 8 } },
2300
+ { className: "dshsc-tabpane" },
2229
2301
  visibleOnlyBanners(),
2230
2302
  builtinNotice(),
2231
2303
  listBar(),
2232
- visibleRows.length === 0 ? filteredEmpty() : body
2304
+ h(
2305
+ "div",
2306
+ { className: "dshsc-listscroll" },
2307
+ visibleRows.length === 0 ? filteredEmpty() : body
2308
+ )
2233
2309
  );
2234
2310
  }
2235
2311
  return content();
@@ -2281,90 +2357,100 @@ function SubscriptionsTab({
2281
2357
  if (subscriptions.length === 0) {
2282
2358
  return h(
2283
2359
  "div",
2284
- { className: "dshsc-empty" },
2285
- h("div", { className: "dshsc-name" }, t("empty.subs")),
2286
- h("div", { className: "dshsc-meta", style: { justifyContent: "center" } }, t("empty.subsHint")),
2360
+ { className: "dshsc-tabpane" },
2287
2361
  h(
2288
2362
  "div",
2289
- { style: { marginTop: 10 } },
2290
- 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
+ )
2291
2375
  )
2292
2376
  );
2293
2377
  }
2294
- return h("div", { className: "dshsc-list" }, subscriptions.map((subscription) => {
2295
- const entries = subscription.snapshot?.entries || [];
2296
- const installedNames = new Set(rows.filter((row) => row.subscriptionId === subscription.id).map((row) => row.name));
2297
- const open = Boolean(expanded[subscription.id]);
2298
- return h(
2378
+ return h(
2379
+ "div",
2380
+ { className: "dshsc-tabpane" },
2381
+ h(
2299
2382
  "div",
2300
- { className: "dshsc-sub-block", key: subscription.id },
2301
- h(
2302
- "div",
2303
- { className: "dshsc-sub-row" },
2304
- h("button", {
2305
- className: "dshsc-caret",
2306
- "aria-expanded": String(open),
2307
- "aria-label": `${subscription.source} ${open ? t("action.collapse") : t("action.expand")}`,
2308
- onClick: () => onToggle(subscription.id)
2309
- }, h(CaretIcon, { open })),
2310
- h("span", { className: "dshsc-name" }, subscription.source),
2311
- h("span", { className: "dshsc-tag" }, subscription.ref || t("subscription.defaultRef")),
2312
- h(
2313
- "span",
2314
- { className: "dshsc-substat" },
2315
- `${entries.length} ${t("skillCount")} · ${t("installed")} ${installedNames.size}`
2316
- ),
2317
- h(
2318
- "div",
2319
- { className: "dshsc-actions" },
2320
- h("button", {
2321
- className: "dshsc-btn sq",
2322
- disabled: busy !== null,
2323
- title: t("action.checkUpdates"),
2324
- "aria-label": `${t("action.checkUpdates")} ${subscription.source}`,
2325
- onClick: () => void onRefresh(subscription)
2326
- }, h(RefreshIcon, { size: 13 })),
2327
- h("button", { className: "dshsc-btn ghost danger", disabled: busy !== null, onClick: () => void onDelete(subscription) }, t("action.unsubscribe"))
2328
- )
2329
- ),
2330
- open ? h("div", { className: "dshsc-sub-list" }, entries.map((entry) => {
2331
- 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]);
2332
2388
  return h(
2333
2389
  "div",
2334
- { className: "dshsc-sub-entry", key: entry.name },
2335
- h("span", { className: "dshsc-name" }, entry.name),
2336
- h(
2337
- "span",
2338
- { className: `dshsc-state${installedAlready ? " ok" : ""}` },
2339
- installedAlready ? "✓ " : "",
2340
- installedAlready ? t("installed") : t("notInstalled")
2341
- ),
2390
+ { className: "dshsc-sub-block", key: subscription.id },
2342
2391
  h(
2343
2392
  "div",
2344
- { className: "dshsc-actions" },
2345
- installedAlready ? null : h("button", {
2346
- className: "dshsc-btn",
2347
- disabled: !writable || busy !== null,
2348
- onClick: () => void onInstall(subscription, entry)
2349
- }, t("action.install"))
2350
- )
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
2351
2443
  );
2352
- })) : null
2353
- );
2354
- }));
2444
+ }))
2445
+ )
2446
+ );
2355
2447
  }
2356
2448
 
2357
2449
  // client/views/modals.js
2358
2450
  var import_react3 = __toESM(require("react"), 1);
2359
2451
  function Overlay({ t, title, children, footer, onCancel }) {
2360
- import_react3.default.useEffect(() => {
2361
- const onKey = (event) => {
2362
- if (event.key === "Escape") onCancel?.();
2363
- };
2364
- window.addEventListener("keydown", onKey);
2365
- return () => window.removeEventListener("keydown", onKey);
2366
- }, [onCancel]);
2367
- return h(
2452
+ useOverlayLayer(true, onCancel);
2453
+ return portalize(h(
2368
2454
  "div",
2369
2455
  { className: "dshsc-overlay", onClick: (event) => {
2370
2456
  if (event.target === event.currentTarget) onCancel?.();
@@ -2384,7 +2470,7 @@ function Overlay({ t, title, children, footer, onCancel }) {
2384
2470
  children,
2385
2471
  h("div", { className: "dshsc-modal-foot" }, footer)
2386
2472
  )
2387
- );
2473
+ ), overlayHost());
2388
2474
  }
2389
2475
  function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
2390
2476
  const [form, setForm] = import_react3.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
@@ -3127,20 +3213,21 @@ var MARKDOWN = /\.(md|markdown)$/i;
3127
3213
  function isMarkdownPath(relPath) {
3128
3214
  return MARKDOWN.test(String(relPath || ""));
3129
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
+ }
3130
3221
  function nearestScroller(node) {
3131
3222
  let current = node?.parentElement || null;
3132
3223
  while (current) {
3133
- if (typeof getComputedStyle === "function") {
3134
- const style = getComputedStyle(current);
3135
- if (style && /(auto|scroll)/.test(style.overflowY || "") && current.scrollHeight > current.clientHeight + 4) {
3136
- return current;
3137
- }
3138
- }
3224
+ if (isScroller(current)) return current;
3139
3225
  current = current.parentElement || null;
3140
3226
  }
3141
3227
  return typeof document !== "undefined" ? document.scrollingElement || null : null;
3142
3228
  }
3143
3229
  function scrollHost(node) {
3230
+ if (isScroller(node)) return node;
3144
3231
  return node && nearestScroller(node) || node;
3145
3232
  }
3146
3233
  var STATUS_KEY = {
@@ -3332,6 +3419,7 @@ function SkillReader({
3332
3419
  const bodyRef = import_react4.default.useRef(null);
3333
3420
  const dialogRef = import_react4.default.useRef(null);
3334
3421
  const scrollRef = import_react4.default.useRef(0);
3422
+ useOverlayLayer(variant === "drawer" || variant === "page", onClose);
3335
3423
  const name = row?.name || "";
3336
3424
  const tree = state.tree;
3337
3425
  const files = tree?.files || [];
@@ -3358,11 +3446,6 @@ function SkillReader({
3358
3446
  const node = dialogRef.current;
3359
3447
  node?.focus?.();
3360
3448
  const onKeyDown = (event) => {
3361
- if (event?.key === "Escape") {
3362
- event.stopPropagation?.();
3363
- onClose?.();
3364
- return;
3365
- }
3366
3449
  if (event?.key !== "Tab" || !node?.querySelectorAll) return;
3367
3450
  const focusables = Array.from(
3368
3451
  node.querySelectorAll('a[href],button:not([disabled]),[tabindex]:not([tabindex="-1"])')
@@ -3378,9 +3461,9 @@ function SkillReader({
3378
3461
  first.focus?.();
3379
3462
  }
3380
3463
  };
3381
- document.addEventListener("keydown", onKeyDown);
3464
+ document.addEventListener("keydown", onKeyDown, true);
3382
3465
  return () => {
3383
- document.removeEventListener("keydown", onKeyDown);
3466
+ document.removeEventListener("keydown", onKeyDown, true);
3384
3467
  previous?.focus?.();
3385
3468
  };
3386
3469
  }, [variant, onClose]);
@@ -3805,7 +3888,7 @@ function SkillReader({
3805
3888
  ref: dialogRef,
3806
3889
  tabIndex: -1
3807
3890
  }, header, tabsBar, body, actionsFooter)
3808
- ));
3891
+ ), overlayHost());
3809
3892
  }
3810
3893
 
3811
3894
  // client/section.js
@@ -3822,6 +3905,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3822
3905
  const [viewer, setViewer] = import_react5.default.useState(null);
3823
3906
  const rootRef = import_react5.default.useRef(null);
3824
3907
  const scrollPos = import_react5.default.useRef(null);
3908
+ const restoreScroll = import_react5.default.useRef(null);
3825
3909
  const [filter, setFilter] = import_react5.default.useState("all");
3826
3910
  const [builtinOpen, setBuiltinOpen] = import_react5.default.useState(false);
3827
3911
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
@@ -3869,30 +3953,36 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
3869
3953
  }
3870
3954
  return typeof document !== "undefined" ? document.scrollingElement || null : null;
3871
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
+ }
3872
3961
  function openViewer(row, mode = "drawer") {
3873
3962
  if (!row) return;
3874
3963
  if (mode === "page") {
3875
- scrollPos.current = getScrollParent(rootRef.current)?.scrollTop ?? 0;
3964
+ scrollPos.current = listScroller()?.scrollTop ?? 0;
3876
3965
  }
3877
3966
  setViewer({ name: row.name, mode });
3878
3967
  }
3879
3968
  function openPage() {
3880
3969
  setViewer((prev) => {
3881
3970
  if (!prev) return prev;
3882
- scrollPos.current = getScrollParent(rootRef.current)?.scrollTop ?? 0;
3971
+ scrollPos.current = listScroller()?.scrollTop ?? 0;
3883
3972
  return { ...prev, mode: "page" };
3884
3973
  });
3885
3974
  }
3886
3975
  function closeViewer() {
3887
- const restore = viewer?.mode === "page" ? scrollPos.current : null;
3976
+ if (viewer?.mode === "page") restoreScroll.current = scrollPos.current;
3888
3977
  setViewer(null);
3889
- if (restore === null || typeof window === "undefined") return;
3890
- const apply2 = () => {
3891
- const scroller = getScrollParent(rootRef.current);
3892
- if (scroller) scroller.scrollTop = restore;
3893
- };
3894
- if (typeof window.requestAnimationFrame === "function") window.requestAnimationFrame(apply2);
3895
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]);
3896
3986
  async function changePlacement(next) {
3897
3987
  if (next === placement || placementBusy) return;
3898
3988
  setPlacementBusy(next);
@@ -4021,7 +4111,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4021
4111
  h("style", null, CSS),
4022
4112
  viewer?.mode === "page" ? null : listNode,
4023
4113
  readerNode,
4024
- 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,
4025
4115
  modal ? h(Modals, {
4026
4116
  t,
4027
4117
  modal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-skills-console",
3
- "version": "0.3.6",
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>",