@nanmicoder/dsh-skills-hub 0.0.1 → 0.0.2

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 (48) hide show
  1. package/CONTRIBUTING.md +18 -0
  2. package/README.md +31 -82
  3. package/README_EN.md +31 -82
  4. package/README_ZH.md +2 -0
  5. package/compatibility.json +1 -1
  6. package/docs/COMPATIBILITY.md +34 -0
  7. package/docs/CONFIGURATION.md +38 -0
  8. package/docs/CONTRACT.md +25 -7
  9. package/docs/RELEASE.md +5 -5
  10. package/lib/client.js +1916 -1025
  11. package/lib/client.js.map +1 -1
  12. package/lib/config.js +2 -0
  13. package/lib/index.js +16 -6
  14. package/lib/market/cache.js +104 -30
  15. package/lib/market/disk-cache.js +113 -0
  16. package/lib/market/install-service.js +8 -6
  17. package/lib/market/market-service.js +79 -29
  18. package/lib/market/provider-fetch.js +9 -2
  19. package/lib/market/types.js +2 -1
  20. package/lib/skills/management.js +182 -4
  21. package/lib/skills/root.js +1 -1
  22. package/lib/types/client/api.d.ts +33 -5
  23. package/lib/types/client/components/InstallConfirmDialog.d.ts +6 -0
  24. package/lib/types/client/components/InstalledSkillDetail.d.ts +24 -0
  25. package/lib/types/client/components/InstalledSkills.d.ts +18 -1
  26. package/lib/types/client/components/MarkdownView.d.ts +7 -1
  27. package/lib/types/client/components/SkillAvatar.d.ts +1 -1
  28. package/lib/types/client/components/SkillDetailShell.d.ts +63 -0
  29. package/lib/types/client/components/SkillDetailView.d.ts +4 -6
  30. package/lib/types/client/components/SkillFiles.d.ts +32 -0
  31. package/lib/types/client/components/SourceStatusBar.d.ts +4 -0
  32. package/lib/types/client/icons.d.ts +4 -0
  33. package/lib/types/client/locales.d.ts +30 -0
  34. package/lib/types/client/relative-time.d.ts +13 -0
  35. package/lib/types/client/skill-markdown.d.ts +39 -0
  36. package/lib/types/client/state.d.ts +13 -4
  37. package/lib/types/config.d.ts +2 -0
  38. package/lib/types/market/cache.d.ts +62 -14
  39. package/lib/types/market/disk-cache.d.ts +16 -0
  40. package/lib/types/market/market-service.d.ts +15 -3
  41. package/lib/types/market/provider-fetch.d.ts +2 -0
  42. package/lib/types/skills/management.d.ts +63 -2
  43. package/lib/types/skills/root.d.ts +8 -0
  44. package/lib/types/web-routes.d.ts +2 -0
  45. package/lib/web-routes.js +42 -4
  46. package/package.json +1 -1
  47. package/release-notes/v0.0.1.md +1 -1
  48. package/release-notes/v0.0.2.md +12 -0
package/lib/client.js CHANGED
@@ -85,6 +85,30 @@ window.__ModuleLoader__.load({
85
85
  return t;
86
86
  }
87
87
  //#endregion
88
+ //#region lib/client/relative-time.js
89
+ /**
90
+ * How old a snapshot is, in the panel's own vocabulary.
91
+ *
92
+ * The panel shows cached catalogue data, so "when was this actually fetched" is
93
+ * part of the answer rather than a debug detail: a reader who is about to
94
+ * install third-party code is entitled to know whether the security report in
95
+ * front of them was read a minute ago or yesterday.
96
+ */
97
+ /** Coarse buckets: the first match wins, and precision beyond them is noise. */
98
+ function formatAge(t, from, now = Date.now()) {
99
+ const minutes = Math.floor(Math.max(0, now - from) / 6e4);
100
+ if (minutes < 1) return t("ageJustNow");
101
+ if (minutes < 60) return t("ageMinutes", { count: minutes });
102
+ const hours = Math.floor(minutes / 60);
103
+ if (hours < 24) return t("ageHours", { count: hours });
104
+ return t("ageDays", { count: Math.floor(hours / 24) });
105
+ }
106
+ /** Absolute timestamp for the hover text, so the bucket never hides the fact. */
107
+ function formatStamp(from) {
108
+ const date = new Date(from);
109
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString();
110
+ }
111
+ //#endregion
88
112
  //#region lib/client/icons.js
89
113
  /** Attributes every icon shares; spread first so a caller's props win. */
90
114
  const stroke = {
@@ -347,15 +371,47 @@ window.__ModuleLoader__.load({
347
371
  })]
348
372
  });
349
373
  }
374
+ /** Folder: leading glyph of the installed skill's location line. */
375
+ function FolderIcon({ size = 16, className }) {
376
+ return (0, react_jsx_runtime.jsx)("svg", {
377
+ width: size,
378
+ height: size,
379
+ viewBox: "0 0 16 16",
380
+ className,
381
+ strokeWidth: 1.5,
382
+ ...stroke,
383
+ "aria-hidden": "true",
384
+ focusable: "false",
385
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M1.9 4.6a1.3 1.3 0 0 1 1.3-1.3h2.5l1.4 1.6h5.7a1.3 1.3 0 0 1 1.3 1.3v5.6a1.3 1.3 0 0 1-1.3 1.3H3.2a1.3 1.3 0 0 1-1.3-1.3z" })
386
+ });
387
+ }
388
+ /** Arrow leaving a frame: open the same skill in the marketplace. */
389
+ function ExternalLinkIcon({ size = 16, className }) {
390
+ return (0, react_jsx_runtime.jsxs)("svg", {
391
+ width: size,
392
+ height: size,
393
+ viewBox: "0 0 16 16",
394
+ className,
395
+ strokeWidth: 1.5,
396
+ ...stroke,
397
+ "aria-hidden": "true",
398
+ focusable: "false",
399
+ children: [
400
+ (0, react_jsx_runtime.jsx)("path", { d: "M9.4 2.6h4v4" }),
401
+ (0, react_jsx_runtime.jsx)("path", { d: "M13.4 2.6 7.9 8.1" }),
402
+ (0, react_jsx_runtime.jsx)("path", { d: "M12.2 9.6v3.1a1.3 1.3 0 0 1-1.3 1.3H3.6a1.3 1.3 0 0 1-1.3-1.3V5.4a1.3 1.3 0 0 1 1.3-1.3h3.1" })
403
+ ]
404
+ });
405
+ }
350
406
  //#endregion
351
407
  //#region \0dsh-css:src/client/components/SecurityBadge.module.css.mjs
352
- const css$12 = ".DlDA7a_badge{box-sizing:border-box;white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;height:20px;padding:0 7px;font-size:11px;font-weight:600;line-height:18px;display:inline-flex}.DlDA7a_compact{height:18px;padding:0 6px;font-size:10.5px}.DlDA7a_success{color:var(--dsw-alias-state-success-primary,#22c55e);background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 12%, transparent)}.DlDA7a_neutral{color:var(--dsw-alias-label-secondary,#61666b);background:var(--dsw-alias-bg-module-platform,#f5f6f7)}.DlDA7a_danger{color:var(--dsw-alias-state-error-primary,#ec1313);background:color-mix(in srgb, var(--dsw-alias-state-error-primary,#ec1313) 12%, transparent)}";
353
- const tagId$12 = "@nanmicoder/dsh-skills-hub/SecurityBadge.module.css";
354
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$12) + "]") === null) {
408
+ const css$14 = ".DlDA7a_badge{box-sizing:border-box;white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;height:20px;padding:0 7px;font-size:11px;font-weight:600;line-height:18px;display:inline-flex}.DlDA7a_compact{height:18px;padding:0 6px;font-size:10.5px}.DlDA7a_success{color:var(--dsw-alias-state-success-primary,#22c55e);background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 12%, transparent)}.DlDA7a_neutral{color:var(--dsw-alias-label-secondary,#61666b);background:var(--dsw-alias-bg-module-platform,#f5f6f7)}.DlDA7a_danger{color:var(--dsw-alias-state-error-primary,#ec1313);background:color-mix(in srgb, var(--dsw-alias-state-error-primary,#ec1313) 12%, transparent)}";
409
+ const tagId$14 = "@nanmicoder/dsh-skills-hub/SecurityBadge.module.css";
410
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$14) + "]") === null) {
355
411
  const tag = document.createElement("style");
356
412
  tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
357
- tag.dataset.pluginCss = tagId$12;
358
- tag.textContent = css$12;
413
+ tag.dataset.pluginCss = tagId$14;
414
+ tag.textContent = css$14;
359
415
  document.head.appendChild(tag);
360
416
  }
361
417
  var SecurityBadge_module_css_default = {
@@ -398,13 +454,13 @@ window.__ModuleLoader__.load({
398
454
  }
399
455
  //#endregion
400
456
  //#region \0dsh-css:src/client/components/InstallConfirmDialog.module.css.mjs
401
- const css$11 = ".UTdxCa_facts{margin:0}.UTdxCa_row{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);justify-content:space-between;align-items:baseline;gap:16px;min-width:0;padding:8px 0;display:flex}.UTdxCa_row:last-child{border-bottom:none}.UTdxCa_label{color:var(--dsw-alias-label-tertiary,#81858c);flex:none;font-size:12.5px;line-height:19px}.UTdxCa_value{min-width:0;color:var(--dsw-alias-label-primary,#0f1115);text-align:right;overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:600;line-height:19px}.UTdxCa_path{color:var(--dsw-alias-label-secondary,#61666b);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;font-weight:500}.UTdxCa_risk{border-radius:var(--dsw-radius-sm,8px);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 10%, transparent);color:var(--dsw-alias-label-secondary,#61666b);align-items:flex-start;gap:8px;margin:12px 0 0;padding:9px 11px;font-size:12px;line-height:19px;display:flex}.UTdxCa_riskIcon{color:var(--dsw-alias-state-warn-primary,#f59e0b);flex:none;margin-top:1px}.UTdxCa_actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;width:100%;display:flex}";
402
- const tagId$11 = "@nanmicoder/dsh-skills-hub/InstallConfirmDialog.module.css";
403
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$11) + "]") === null) {
457
+ const css$13 = ".UTdxCa_facts{margin:0}.UTdxCa_row{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);justify-content:space-between;align-items:baseline;gap:16px;min-width:0;padding:8px 0;display:flex}.UTdxCa_row:last-child{border-bottom:none}.UTdxCa_label{color:var(--dsw-alias-label-tertiary,#81858c);flex:none;font-size:12.5px;line-height:19px}.UTdxCa_value{min-width:0;color:var(--dsw-alias-label-primary,#0f1115);text-align:right;overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:600;line-height:19px}.UTdxCa_path{color:var(--dsw-alias-label-secondary,#61666b);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;font-weight:500}.UTdxCa_risk{border-radius:var(--dsw-radius-sm,8px);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 10%, transparent);color:var(--dsw-alias-label-secondary,#61666b);align-items:flex-start;gap:8px;margin:12px 0 0;padding:9px 11px;font-size:12px;line-height:19px;display:flex}.UTdxCa_riskIcon{color:var(--dsw-alias-state-warn-primary,#f59e0b);flex:none;margin-top:1px}.UTdxCa_actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;width:100%;display:flex}";
458
+ const tagId$13 = "@nanmicoder/dsh-skills-hub/InstallConfirmDialog.module.css";
459
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$13) + "]") === null) {
404
460
  const tag = document.createElement("style");
405
461
  tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
406
- tag.dataset.pluginCss = tagId$11;
407
- tag.textContent = css$11;
462
+ tag.dataset.pluginCss = tagId$13;
463
+ tag.textContent = css$13;
408
464
  document.head.appendChild(tag);
409
465
  }
410
466
  var InstallConfirmDialog_module_css_default = {
@@ -472,72 +528,84 @@ window.__ModuleLoader__.load({
472
528
  children: busy ? t("installing") : t("confirm")
473
529
  })]
474
530
  }),
475
- children: [(0, react_jsx_runtime.jsxs)("dl", {
476
- className: InstallConfirmDialog_module_css_default.facts,
477
- children: [
478
- (0, react_jsx_runtime.jsxs)("div", {
479
- className: InstallConfirmDialog_module_css_default.row,
480
- children: [(0, react_jsx_runtime.jsx)("dt", {
481
- className: InstallConfirmDialog_module_css_default.label,
482
- children: t("filter.source")
483
- }), (0, react_jsx_runtime.jsx)("dd", {
484
- className: InstallConfirmDialog_module_css_default.value,
485
- children: sourceLabel
486
- })]
487
- }),
488
- skill.version !== void 0 && skill.version !== "" && (0, react_jsx_runtime.jsxs)("div", {
489
- className: InstallConfirmDialog_module_css_default.row,
490
- children: [(0, react_jsx_runtime.jsx)("dt", {
491
- className: InstallConfirmDialog_module_css_default.label,
492
- children: t("version")
493
- }), (0, react_jsx_runtime.jsxs)("dd", {
494
- className: InstallConfirmDialog_module_css_default.value,
495
- children: ["v", skill.version]
496
- })]
497
- }),
498
- (0, react_jsx_runtime.jsxs)("div", {
499
- className: InstallConfirmDialog_module_css_default.row,
500
- children: [(0, react_jsx_runtime.jsx)("dt", {
501
- className: InstallConfirmDialog_module_css_default.label,
502
- children: t("author")
503
- }), (0, react_jsx_runtime.jsx)("dd", {
504
- className: InstallConfirmDialog_module_css_default.value,
505
- children: skill.authorName
506
- })]
507
- }),
508
- (0, react_jsx_runtime.jsxs)("div", {
509
- className: InstallConfirmDialog_module_css_default.row,
510
- children: [(0, react_jsx_runtime.jsx)("dt", {
511
- className: InstallConfirmDialog_module_css_default.label,
512
- children: t("security")
513
- }), (0, react_jsx_runtime.jsx)("dd", {
514
- className: InstallConfirmDialog_module_css_default.value,
515
- children: (0, react_jsx_runtime.jsx)(SecurityBadge, { status: skill.securityStatus })
516
- })]
517
- }),
518
- (0, react_jsx_runtime.jsxs)("div", {
519
- className: InstallConfirmDialog_module_css_default.row,
520
- children: [(0, react_jsx_runtime.jsx)("dt", {
521
- className: InstallConfirmDialog_module_css_default.label,
522
- children: t("installLocation")
523
- }), (0, react_jsx_runtime.jsxs)("dd", {
524
- className: `${InstallConfirmDialog_module_css_default.value} ${InstallConfirmDialog_module_css_default.path}`,
525
- children: [
526
- "…/skills/",
527
- slugOf(skill.id).toLowerCase(),
528
- "/"
529
- ]
530
- })]
531
- })
532
- ]
533
- }), risky && (0, react_jsx_runtime.jsxs)("p", {
534
- className: InstallConfirmDialog_module_css_default.risk,
535
- role: "note",
536
- children: [(0, react_jsx_runtime.jsx)(AlertIcon, {
537
- size: 16,
538
- className: InstallConfirmDialog_module_css_default.riskIcon
539
- }), (0, react_jsx_runtime.jsx)("span", { children: t("unoaudited") })]
540
- })]
531
+ children: [
532
+ (0, react_jsx_runtime.jsxs)("dl", {
533
+ className: InstallConfirmDialog_module_css_default.facts,
534
+ children: [
535
+ (0, react_jsx_runtime.jsxs)("div", {
536
+ className: InstallConfirmDialog_module_css_default.row,
537
+ children: [(0, react_jsx_runtime.jsx)("dt", {
538
+ className: InstallConfirmDialog_module_css_default.label,
539
+ children: t("filter.source")
540
+ }), (0, react_jsx_runtime.jsx)("dd", {
541
+ className: InstallConfirmDialog_module_css_default.value,
542
+ children: sourceLabel
543
+ })]
544
+ }),
545
+ skill.version !== void 0 && skill.version !== "" && (0, react_jsx_runtime.jsxs)("div", {
546
+ className: InstallConfirmDialog_module_css_default.row,
547
+ children: [(0, react_jsx_runtime.jsx)("dt", {
548
+ className: InstallConfirmDialog_module_css_default.label,
549
+ children: t("version")
550
+ }), (0, react_jsx_runtime.jsxs)("dd", {
551
+ className: InstallConfirmDialog_module_css_default.value,
552
+ children: ["v", skill.version]
553
+ })]
554
+ }),
555
+ (0, react_jsx_runtime.jsxs)("div", {
556
+ className: InstallConfirmDialog_module_css_default.row,
557
+ children: [(0, react_jsx_runtime.jsx)("dt", {
558
+ className: InstallConfirmDialog_module_css_default.label,
559
+ children: t("author")
560
+ }), (0, react_jsx_runtime.jsx)("dd", {
561
+ className: InstallConfirmDialog_module_css_default.value,
562
+ children: skill.authorName
563
+ })]
564
+ }),
565
+ (0, react_jsx_runtime.jsxs)("div", {
566
+ className: InstallConfirmDialog_module_css_default.row,
567
+ children: [(0, react_jsx_runtime.jsx)("dt", {
568
+ className: InstallConfirmDialog_module_css_default.label,
569
+ children: t("security")
570
+ }), (0, react_jsx_runtime.jsx)("dd", {
571
+ className: InstallConfirmDialog_module_css_default.value,
572
+ children: (0, react_jsx_runtime.jsx)(SecurityBadge, { status: skill.securityStatus })
573
+ })]
574
+ }),
575
+ (0, react_jsx_runtime.jsxs)("div", {
576
+ className: InstallConfirmDialog_module_css_default.row,
577
+ children: [(0, react_jsx_runtime.jsx)("dt", {
578
+ className: InstallConfirmDialog_module_css_default.label,
579
+ children: t("installLocation")
580
+ }), (0, react_jsx_runtime.jsxs)("dd", {
581
+ className: `${InstallConfirmDialog_module_css_default.value} ${InstallConfirmDialog_module_css_default.path}`,
582
+ children: [
583
+ "…/skills/",
584
+ slugOf(skill.id).toLowerCase(),
585
+ "/"
586
+ ]
587
+ })]
588
+ })
589
+ ]
590
+ }),
591
+ skill.snapshotAt !== void 0 && (0, react_jsx_runtime.jsxs)("p", {
592
+ className: InstallConfirmDialog_module_css_default.risk,
593
+ role: "note",
594
+ title: formatStamp(skill.snapshotAt),
595
+ children: [(0, react_jsx_runtime.jsx)(AlertIcon, {
596
+ size: 16,
597
+ className: InstallConfirmDialog_module_css_default.riskIcon
598
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("installSnapshot", { age: formatAge(t, skill.snapshotAt) }) })]
599
+ }),
600
+ risky && (0, react_jsx_runtime.jsxs)("p", {
601
+ className: InstallConfirmDialog_module_css_default.risk,
602
+ role: "note",
603
+ children: [(0, react_jsx_runtime.jsx)(AlertIcon, {
604
+ size: 16,
605
+ className: InstallConfirmDialog_module_css_default.riskIcon
606
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("unoaudited") })]
607
+ })
608
+ ]
541
609
  });
542
610
  }
543
611
  //#endregion
@@ -653,7 +721,7 @@ window.__ModuleLoader__.load({
653
721
  throw new SkillsHubApiError("BAD_REQUEST", 400, `"${id}" is not a market skill id (expected "<source>:<slug>")`);
654
722
  }
655
723
  /** Search the catalogue (market sources) — the remote list page. */
656
- async function fetchMarketList(query, signal) {
724
+ async function fetchMarketList(query, signal, options = {}) {
657
725
  const search = new URLSearchParams();
658
726
  const q = query.q?.trim();
659
727
  if (q) search.set("q", q);
@@ -662,13 +730,15 @@ window.__ModuleLoader__.load({
662
730
  if (query.installed !== "all") search.set("installed", query.installed);
663
731
  if (query.cursor) search.set("cursor", query.cursor);
664
732
  if (typeof query.limit === "number" && Number.isFinite(query.limit)) search.set("limit", String(query.limit));
733
+ if (options.refresh === true) search.set("refresh", "1");
665
734
  const suffix = search.toString();
666
735
  return getJson(`/skills${suffix === "" ? "" : `?${suffix}`}`, signal);
667
736
  }
668
737
  /** One skill's full detail plus the health of the source that served it. */
669
- async function fetchSkillDetail(id, signal) {
738
+ async function fetchSkillDetail(id, signal, options = {}) {
670
739
  const { source, slug } = parseMarketId(id);
671
- return getJson(`/skills/${source}/${encodeURIComponent(slug)}`, signal);
740
+ const suffix = options.refresh === true ? "?refresh=1" : "";
741
+ return getJson(`/skills/${source}/${encodeURIComponent(slug)}${suffix}`, signal);
672
742
  }
673
743
  /** One file of a skill, for the Files tab. */
674
744
  async function fetchSkillFile(id, path, signal) {
@@ -703,130 +773,479 @@ window.__ModuleLoader__.load({
703
773
  function fetchInstalledDetail(key, signal) {
704
774
  return getJson(`/installed/detail?key=${encodeURIComponent(key)}`, signal);
705
775
  }
776
+ /** One file of an installed skill; the Host validates the key and contains the path. */
777
+ async function fetchInstalledFile(key, path, signal) {
778
+ return (await getJson(`/installed/file?key=${encodeURIComponent(key)}&path=${encodeURIComponent(path)}`, signal)).file;
779
+ }
706
780
  /** Remove a specifically selected local entry after the management UI confirmation. */
707
781
  function removeInstalled(key) {
708
782
  return postJson("/installed/uninstall", { key });
709
783
  }
710
784
  //#endregion
711
785
  //#region lib/client/skill-markdown.js
712
- /** Frontmatter is metadata, not part of the skill's rendered instructions. */
713
- function stripSkillFrontmatter(markdown) {
714
- return markdown.replace(/^\uFEFF?---[ \t]*\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/, "");
786
+ /**
787
+ * Frontmatter handling for skill documents.
788
+ *
789
+ * A `SKILL.md` opens with a YAML block, and feeding that block to a markdown
790
+ * renderer is not merely ugly — it is wrong: a lone `---` is a setext underline,
791
+ * so the closing fence promotes the whole metadata block into one giant heading.
792
+ * The block is split off here instead, so the UI can render it as structured
793
+ * metadata (`FrontmatterPanel`) while the rest of the document goes to the
794
+ * renderer. The approach mirrors the desktop project's `skillFrontmatter.ts`.
795
+ *
796
+ * The parser is a deliberately small YAML subset — scalars, inline and block
797
+ * sequences, `|`/`>` block scalars, and one level of nested mapping that is kept
798
+ * as the raw YAML it is — because skill frontmatter is flat by convention and a
799
+ * full YAML engine is a dependency this browser bundle does not need. It never
800
+ * throws: a document without a fence, or with an unterminated one, comes back as
801
+ * body only.
802
+ */
803
+ /** A fence closing the block; it must sit at column zero. */
804
+ const CLOSING_FENCE = /^(?:---|\.\.\.)[ \t]*$/;
805
+ /** Top-level `key:` or `key: value`. Indented lines belong to the value above. */
806
+ const KEY_LINE = /^([A-Za-z0-9_$][\w.$-]*)[ \t]*:(?:[ \t]+(.*))?$/;
807
+ /** `|`, `>`, optionally with chomping and indentation indicators. */
808
+ const BLOCK_SCALAR = /^([|>])([-+]?)(\d*)$/;
809
+ function isBlank(line) {
810
+ return line.trim() === "";
811
+ }
812
+ /** Visual indentation width; a tab counts as two columns, as YAML's own rules do. */
813
+ function indentWidth(line) {
814
+ let width = 0;
815
+ for (const char of line) if (char === " ") width += 1;
816
+ else if (char === " ") width += 2;
817
+ else break;
818
+ return width;
819
+ }
820
+ /** One pair of surrounding quotes, with the escapes YAML defines for it. */
821
+ function stripQuotes(raw) {
822
+ if (raw.length >= 2) {
823
+ const first = raw[0];
824
+ const last = raw[raw.length - 1];
825
+ if (first === "\"" && last === "\"") return {
826
+ value: raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\"),
827
+ quoted: true
828
+ };
829
+ if (first === "'" && last === "'") return {
830
+ value: raw.slice(1, -1).replace(/''/g, "'"),
831
+ quoted: true
832
+ };
833
+ }
834
+ return {
835
+ value: raw,
836
+ quoted: false
837
+ };
838
+ }
839
+ /** Drop a trailing ` # comment`, but only when it sits outside quotes. */
840
+ function stripComment(raw) {
841
+ let quote = null;
842
+ for (let index = 0; index < raw.length; index++) {
843
+ const char = raw[index];
844
+ if (quote !== null) {
845
+ if (char === quote) quote = null;
846
+ continue;
847
+ }
848
+ if (char === "\"" || char === "'") {
849
+ quote = char;
850
+ continue;
851
+ }
852
+ if (char === "#" && (index === 0 || raw[index - 1] === " " || raw[index - 1] === " ")) return raw.slice(0, index);
853
+ }
854
+ return raw;
855
+ }
856
+ /** Scalars keep their YAML meaning where it is unambiguous, and stay text otherwise. */
857
+ function coerceScalar(raw) {
858
+ const trimmed = raw.trim();
859
+ if (trimmed === "") return "";
860
+ const { value, quoted } = stripQuotes(trimmed);
861
+ if (quoted) return value;
862
+ const lower = value.toLowerCase();
863
+ if (lower === "true") return true;
864
+ if (lower === "false") return false;
865
+ if (lower === "null" || lower === "~") return null;
866
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return Number(value);
867
+ return value;
868
+ }
869
+ /** Split `[a, "b, c", d]` on top-level commas only. */
870
+ function splitInlineSequence(inner) {
871
+ const parts = [];
872
+ let current = "";
873
+ let quote = null;
874
+ let depth = 0;
875
+ for (const char of inner) {
876
+ if (quote !== null) {
877
+ current += char;
878
+ if (char === quote) quote = null;
879
+ continue;
880
+ }
881
+ if (char === "\"" || char === "'") {
882
+ quote = char;
883
+ current += char;
884
+ continue;
885
+ }
886
+ if (char === "[" || char === "{") depth += 1;
887
+ if (char === "]" || char === "}") depth -= 1;
888
+ if (char === "," && depth === 0) {
889
+ parts.push(current);
890
+ current = "";
891
+ continue;
892
+ }
893
+ current += char;
894
+ }
895
+ parts.push(current);
896
+ return parts.map((part) => part.trim()).filter((part) => part !== "");
715
897
  }
716
- //#endregion
717
- //#region node_modules/comma-separated-tokens/index.js
718
898
  /**
719
- * Serialize an array of strings or numbers to comma-separated tokens.
899
+ * Remove a block's common indentation and its blank edges.
720
900
  *
721
- * @param {Array<string|number>} values
722
- * List of tokens.
723
- * @param {Options} [options]
724
- * Configuration for `stringify` (optional).
725
- * @returns {string}
726
- * Comma-separated tokens.
901
+ * @param lines - the block's lines, verbatim.
902
+ * @param folded - `>` joins the lines into one paragraph; `|` keeps them.
727
903
  */
728
- function stringify$1(values, options) {
729
- const settings = options || {};
730
- return (values[values.length - 1] === "" ? [...values, ""] : values).join((settings.padRight ? " " : "") + "," + (settings.padLeft === false ? "" : " ")).trim();
904
+ function dedent(lines, folded) {
905
+ const margin = lines.reduce((min, line) => isBlank(line) ? min : Math.min(min, indentWidth(line)), Number.MAX_SAFE_INTEGER);
906
+ const dedented = lines.map((line) => isBlank(line) ? "" : line.slice(margin === Number.MAX_SAFE_INTEGER ? 0 : margin));
907
+ while (dedented.length > 0 && isBlank(dedented[0] ?? "")) dedented.shift();
908
+ while (dedented.length > 0 && isBlank(dedented[dedented.length - 1] ?? "")) dedented.pop();
909
+ return folded ? dedented.join(" ").replace(/\s+/g, " ").trim() : dedented.join("\n");
910
+ }
911
+ /** Parse the top-level keys of one YAML block. Shapes this subset does not model stay verbatim. */
912
+ function parseBlock(yaml) {
913
+ const lines = yaml.split("\n");
914
+ const result = {};
915
+ let index = 0;
916
+ while (index < lines.length) {
917
+ const line = lines[index] ?? "";
918
+ index += 1;
919
+ if (isBlank(line) || line.trimStart().startsWith("#") || indentWidth(line) > 0) continue;
920
+ const match = KEY_LINE.exec(line);
921
+ if (match === null) continue;
922
+ const key = match[1] ?? "";
923
+ const inline = match[2] ?? "";
924
+ const blockScalar = BLOCK_SCALAR.exec(inline.trim());
925
+ if (blockScalar !== null) {
926
+ const collected = [];
927
+ while (index < lines.length) {
928
+ const next = lines[index] ?? "";
929
+ if (!isBlank(next) && indentWidth(next) === 0) break;
930
+ collected.push(next);
931
+ index += 1;
932
+ }
933
+ result[key] = dedent(collected, blockScalar[1] === ">");
934
+ continue;
935
+ }
936
+ const value = stripComment(inline).trim();
937
+ if (value !== "") {
938
+ result[key] = value.startsWith("[") && value.endsWith("]") ? splitInlineSequence(value.slice(1, -1)).map(coerceScalar) : coerceScalar(value);
939
+ continue;
940
+ }
941
+ const children = [];
942
+ while (index < lines.length) {
943
+ const next = lines[index] ?? "";
944
+ if (!isBlank(next) && indentWidth(next) === 0) break;
945
+ children.push(next);
946
+ index += 1;
947
+ }
948
+ const meaningful = children.filter((entry) => !isBlank(entry) && !entry.trimStart().startsWith("#"));
949
+ if (meaningful.length === 0) {
950
+ result[key] = null;
951
+ continue;
952
+ }
953
+ if (meaningful.every((entry) => entry.trimStart().startsWith("- "))) {
954
+ result[key] = meaningful.map((entry) => coerceScalar(stripComment(entry.trimStart().slice(2))));
955
+ continue;
956
+ }
957
+ result[key] = dedent(children, false);
958
+ }
959
+ return result;
731
960
  }
732
- //#endregion
733
- //#region node_modules/estree-util-is-identifier-name/lib/index.js
734
- const nameRe = /^[$_\p{ID_Start}][$_\u{200C}\u{200D}\p{ID_Continue}]*$/u;
735
- const nameReJsx = /^[$_\p{ID_Start}][-$_\u{200C}\u{200D}\p{ID_Continue}]*$/u;
736
- /** @type {Options} */
737
- const emptyOptions$3 = {};
738
961
  /**
739
- * Checks if the given value is a valid identifier name.
962
+ * Parse a metadata block that has already been extracted — the Host bounds its
963
+ * size before it reaches the browser, so this entry point works on that text.
740
964
  *
741
- * @param {string} name
742
- * Identifier to check.
743
- * @param {Options | null | undefined} [options]
744
- * Configuration (optional).
745
- * @returns {boolean}
746
- * Whether `name` can be an identifier.
965
+ * @param yaml - the block's text, without its fences.
966
+ * @returns the parsed keys, or `null` when the block carries nothing to show.
747
967
  */
748
- function name$1(name, options) {
749
- return ((options || emptyOptions$3).jsx ? nameReJsx : nameRe).test(name);
968
+ function parseSkillFrontmatter(yaml) {
969
+ if (yaml.trim() === "") return null;
970
+ try {
971
+ const parsed = parseBlock(yaml.replace(/\r\n/g, "\n"));
972
+ return Object.keys(parsed).length === 0 ? null : parsed;
973
+ } catch {
974
+ return null;
975
+ }
750
976
  }
751
- //#endregion
752
- //#region node_modules/hast-util-whitespace/lib/index.js
753
977
  /**
754
- * @typedef {import('hast').Nodes} Nodes
755
- */
756
- const re = /[ \t\n\f\r]/g;
757
- /**
758
- * Check if the given value is *inter-element whitespace*.
978
+ * Separate a document's frontmatter from its body.
759
979
  *
760
- * @param {Nodes | string} thing
761
- * Thing to check (`Node` or `string`).
762
- * @returns {boolean}
763
- * Whether the `value` is inter-element whitespace (`boolean`): consisting of
764
- * zero or more of space, tab (`\t`), line feed (`\n`), carriage return
765
- * (`\r`), or form feed (`\f`); if a node is passed it must be a `Text` node,
766
- * whose `value` field is checked.
980
+ * @param markdown - the whole document.
981
+ * @returns the parsed metadata (or `null`) plus the body to render.
767
982
  */
768
- function whitespace(thing) {
769
- return typeof thing === "object" ? thing.type === "text" ? empty$1(thing.value) : false : empty$1(thing);
983
+ function splitSkillFrontmatter(markdown) {
984
+ const lines = (markdown.startsWith("") ? markdown.slice(1) : markdown).replace(/\r\n/g, "\n").split("\n");
985
+ if (!/^---[ \t]*$/.test(lines[0] ?? "")) return {
986
+ frontmatter: null,
987
+ body: markdown
988
+ };
989
+ for (let index = 1; index < lines.length; index++) {
990
+ if (!CLOSING_FENCE.test(lines[index] ?? "")) continue;
991
+ const body = lines.slice(index + 1).join("\n").replace(/^\n+/, "");
992
+ return {
993
+ frontmatter: parseSkillFrontmatter(lines.slice(1, index).join("\n")),
994
+ body
995
+ };
996
+ }
997
+ return {
998
+ frontmatter: null,
999
+ body: markdown
1000
+ };
770
1001
  }
771
- /**
772
- * @param {string} value
773
- * @returns {boolean}
774
- */
775
- function empty$1(value) {
776
- return value.replace(re, "") === "";
1002
+ /** Frontmatter is metadata, not part of the skill's rendered instructions. */
1003
+ function stripSkillFrontmatter(markdown) {
1004
+ return splitSkillFrontmatter(markdown).body;
777
1005
  }
778
1006
  //#endregion
779
- //#region node_modules/property-information/lib/util/schema.js
780
- /**
781
- * @import {Schema as SchemaType, Space} from 'property-information'
782
- */
783
- /** @type {SchemaType} */
784
- var Schema = class {
785
- /**
786
- * @param {SchemaType['property']} property
787
- * Property.
788
- * @param {SchemaType['normal']} normal
789
- * Normal.
790
- * @param {Space | undefined} [space]
791
- * Space.
792
- * @returns
793
- * Schema.
794
- */
795
- constructor(property, normal, space) {
796
- this.normal = normal;
797
- this.property = property;
798
- if (space) this.space = space;
799
- }
1007
+ //#region \0dsh-css:src/client/components/FrontmatterPanel.module.css.mjs
1008
+ const css$12 = ".hR0bda_panel{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-layer-1,#fff);flex-direction:column;min-width:0;display:flex;overflow:hidden}.hR0bda_head{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);background:var(--dsw-alias-bg-module-platform,#f5f6f7);align-items:center;gap:8px;padding:8px 12px;display:flex}.hR0bda_title{min-width:0;color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.08em;text-transform:uppercase;flex:1;margin:0;font-size:10.5px;font-weight:600}.hR0bda_count{color:var(--dsw-alias-label-tertiary,#81858c);font-variant-numeric:tabular-nums;font-size:11px}.hR0bda_list{max-height:320px;margin:0;padding:0;overflow-y:auto}.hR0bda_row{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);flex-direction:column;gap:3px;min-width:0;padding:8px 12px;display:flex}.hR0bda_row:last-child{border-bottom:none}.hR0bda_key{overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.02em;font-size:10.5px;font-weight:600}.hR0bda_value{min-width:0;color:var(--dsw-alias-label-primary,#0f1115);margin:0;font-size:12.5px;line-height:19px}.hR0bda_text{overflow-wrap:anywhere}.hR0bda_chips{flex-wrap:wrap;gap:4px;display:flex}.hR0bda_chip{background:var(--dsw-alias-bg-module-platform,#f5f6f7);max-width:100%;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;border-radius:999px;padding:1px 7px;font-size:11px;line-height:17px;display:inline-flex}.hR0bda_chipOn{background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 12%, transparent);color:var(--dsw-alias-state-success-primary,#22c55e)}.hR0bda_block{border-radius:var(--dsw-radius-xs,4px);background:var(--dsw-alias-bg-module-platform,#f5f6f7);max-height:200px;color:var(--dsw-alias-label-secondary,#61666b);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:18px;overflow:auto}";
1009
+ const tagId$12 = "@nanmicoder/dsh-skills-hub/FrontmatterPanel.module.css";
1010
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$12) + "]") === null) {
1011
+ const tag = document.createElement("style");
1012
+ tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
1013
+ tag.dataset.pluginCss = tagId$12;
1014
+ tag.textContent = css$12;
1015
+ document.head.appendChild(tag);
1016
+ }
1017
+ var FrontmatterPanel_module_css_default = {
1018
+ "block": "hR0bda_block",
1019
+ "chip": "hR0bda_chip",
1020
+ "chipOn": "hR0bda_chipOn",
1021
+ "chips": "hR0bda_chips",
1022
+ "count": "hR0bda_count",
1023
+ "head": "hR0bda_head",
1024
+ "key": "hR0bda_key",
1025
+ "list": "hR0bda_list",
1026
+ "panel": "hR0bda_panel",
1027
+ "row": "hR0bda_row",
1028
+ "text": "hR0bda_text",
1029
+ "title": "hR0bda_title",
1030
+ "value": "hR0bda_value"
800
1031
  };
801
- Schema.prototype.normal = {};
802
- Schema.prototype.property = {};
803
- Schema.prototype.space = void 0;
804
1032
  //#endregion
805
- //#region node_modules/property-information/lib/util/merge.js
806
- /**
807
- * @import {Info, Space} from 'property-information'
808
- */
809
- /**
810
- * @param {ReadonlyArray<Schema>} definitions
811
- * Definitions.
812
- * @param {Space | undefined} [space]
813
- * Space.
814
- * @returns {Schema}
815
- * Schema.
816
- */
817
- function merge(definitions, space) {
818
- /** @type {Record<string, Info>} */
819
- const property = {};
820
- /** @type {Record<string, string>} */
821
- const normal = {};
822
- for (const definition of definitions) {
823
- Object.assign(property, definition.property);
824
- Object.assign(normal, definition.normal);
825
- }
826
- return new Schema(property, normal, space);
1033
+ //#region lib/client/components/FrontmatterPanel.js
1034
+ /** Longest JSON we are willing to render inline, in characters. */
1035
+ const MAX_JSON_LENGTH = 600;
1036
+ /** Narrow an `unknown` frontmatter value to a plain object (arrays excluded). */
1037
+ function isRecord(value) {
1038
+ return typeof value === "object" && value !== null && !Array.isArray(value);
827
1039
  }
828
- //#endregion
829
- //#region node_modules/property-information/lib/normalize.js
1040
+ /** Scalar rendering for anything that is not an array, object or block string. */
1041
+ function scalarText(value) {
1042
+ if (value === null || value === void 0) return "—";
1043
+ if (typeof value === "string") return value;
1044
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
1045
+ if (typeof value === "symbol") return value.description ?? "symbol";
1046
+ if (typeof value === "function") return "fn";
1047
+ return "—";
1048
+ }
1049
+ /** Objects go out as JSON; a pathological value must not fill the whole rail. */
1050
+ function jsonText(value) {
1051
+ try {
1052
+ const text = JSON.stringify(value, null, 2);
1053
+ if (text === void 0) return "—";
1054
+ return text.length > MAX_JSON_LENGTH ? `${text.slice(0, MAX_JSON_LENGTH)}…` : text;
1055
+ } catch {
1056
+ return "—";
1057
+ }
1058
+ }
1059
+ /** One value cell; the shape decides between a chip row, a code block and text. */
1060
+ function ValueView({ value }) {
1061
+ if (Array.isArray(value)) {
1062
+ if (value.length === 0) return (0, react_jsx_runtime.jsx)("span", {
1063
+ className: FrontmatterPanel_module_css_default.text,
1064
+ children: "—"
1065
+ });
1066
+ return (0, react_jsx_runtime.jsx)("span", {
1067
+ className: FrontmatterPanel_module_css_default.chips,
1068
+ children: value.map((item, index) => (0, react_jsx_runtime.jsx)("span", {
1069
+ className: FrontmatterPanel_module_css_default.chip,
1070
+ children: scalarText(item)
1071
+ }, `${String(index)}`))
1072
+ });
1073
+ }
1074
+ if (typeof value === "boolean") return (0, react_jsx_runtime.jsx)("span", {
1075
+ className: `${FrontmatterPanel_module_css_default.chip} ${value ? FrontmatterPanel_module_css_default.chipOn : ""}`,
1076
+ children: value ? "true" : "false"
1077
+ });
1078
+ if (typeof value === "string" && value.includes("\n")) return (0, react_jsx_runtime.jsx)("pre", {
1079
+ className: FrontmatterPanel_module_css_default.block,
1080
+ children: value
1081
+ });
1082
+ if (isRecord(value)) return (0, react_jsx_runtime.jsx)("pre", {
1083
+ className: FrontmatterPanel_module_css_default.block,
1084
+ children: jsonText(value)
1085
+ });
1086
+ return (0, react_jsx_runtime.jsx)("span", {
1087
+ className: FrontmatterPanel_module_css_default.text,
1088
+ children: scalarText(value)
1089
+ });
1090
+ }
1091
+ /**
1092
+ * Structured view of a SKILL.md frontmatter block.
1093
+ *
1094
+ * SKILL.md metadata is data, not prose: pushing it through the markdown
1095
+ * renderer turns a dozen short fields into a wall of setext headings. It is
1096
+ * also reference material rather than the document itself, so it renders as a
1097
+ * label/value list under its own heading, with the list itself scrolling once a
1098
+ * skill declares more than a screenful of keys.
1099
+ *
1100
+ * `data` is typed `unknown`-valued on purpose — it is upstream YAML, and every
1101
+ * cell is derived defensively rather than trusted.
1102
+ */
1103
+ function FrontmatterPanel(props) {
1104
+ const { data } = props;
1105
+ const t = useT();
1106
+ const entries = Object.entries(data);
1107
+ if (entries.length === 0) return (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
1108
+ return (0, react_jsx_runtime.jsxs)("section", {
1109
+ className: FrontmatterPanel_module_css_default.panel,
1110
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1111
+ className: FrontmatterPanel_module_css_default.head,
1112
+ children: [(0, react_jsx_runtime.jsx)("h3", {
1113
+ className: FrontmatterPanel_module_css_default.title,
1114
+ children: t("frontmatter")
1115
+ }), (0, react_jsx_runtime.jsx)("span", {
1116
+ className: FrontmatterPanel_module_css_default.count,
1117
+ children: entries.length
1118
+ })]
1119
+ }), (0, react_jsx_runtime.jsx)("dl", {
1120
+ className: FrontmatterPanel_module_css_default.list,
1121
+ children: entries.map(([key, value]) => (0, react_jsx_runtime.jsxs)("div", {
1122
+ className: FrontmatterPanel_module_css_default.row,
1123
+ children: [(0, react_jsx_runtime.jsx)("dt", {
1124
+ className: FrontmatterPanel_module_css_default.key,
1125
+ title: key,
1126
+ children: key
1127
+ }), (0, react_jsx_runtime.jsx)("dd", {
1128
+ className: FrontmatterPanel_module_css_default.value,
1129
+ children: (0, react_jsx_runtime.jsx)(ValueView, { value })
1130
+ })]
1131
+ }, key))
1132
+ })]
1133
+ });
1134
+ }
1135
+ //#endregion
1136
+ //#region node_modules/comma-separated-tokens/index.js
1137
+ /**
1138
+ * Serialize an array of strings or numbers to comma-separated tokens.
1139
+ *
1140
+ * @param {Array<string|number>} values
1141
+ * List of tokens.
1142
+ * @param {Options} [options]
1143
+ * Configuration for `stringify` (optional).
1144
+ * @returns {string}
1145
+ * Comma-separated tokens.
1146
+ */
1147
+ function stringify$1(values, options) {
1148
+ const settings = options || {};
1149
+ return (values[values.length - 1] === "" ? [...values, ""] : values).join((settings.padRight ? " " : "") + "," + (settings.padLeft === false ? "" : " ")).trim();
1150
+ }
1151
+ //#endregion
1152
+ //#region node_modules/estree-util-is-identifier-name/lib/index.js
1153
+ const nameRe = /^[$_\p{ID_Start}][$_\u{200C}\u{200D}\p{ID_Continue}]*$/u;
1154
+ const nameReJsx = /^[$_\p{ID_Start}][-$_\u{200C}\u{200D}\p{ID_Continue}]*$/u;
1155
+ /** @type {Options} */
1156
+ const emptyOptions$3 = {};
1157
+ /**
1158
+ * Checks if the given value is a valid identifier name.
1159
+ *
1160
+ * @param {string} name
1161
+ * Identifier to check.
1162
+ * @param {Options | null | undefined} [options]
1163
+ * Configuration (optional).
1164
+ * @returns {boolean}
1165
+ * Whether `name` can be an identifier.
1166
+ */
1167
+ function name$1(name, options) {
1168
+ return ((options || emptyOptions$3).jsx ? nameReJsx : nameRe).test(name);
1169
+ }
1170
+ //#endregion
1171
+ //#region node_modules/hast-util-whitespace/lib/index.js
1172
+ /**
1173
+ * @typedef {import('hast').Nodes} Nodes
1174
+ */
1175
+ const re = /[ \t\n\f\r]/g;
1176
+ /**
1177
+ * Check if the given value is *inter-element whitespace*.
1178
+ *
1179
+ * @param {Nodes | string} thing
1180
+ * Thing to check (`Node` or `string`).
1181
+ * @returns {boolean}
1182
+ * Whether the `value` is inter-element whitespace (`boolean`): consisting of
1183
+ * zero or more of space, tab (`\t`), line feed (`\n`), carriage return
1184
+ * (`\r`), or form feed (`\f`); if a node is passed it must be a `Text` node,
1185
+ * whose `value` field is checked.
1186
+ */
1187
+ function whitespace(thing) {
1188
+ return typeof thing === "object" ? thing.type === "text" ? empty$1(thing.value) : false : empty$1(thing);
1189
+ }
1190
+ /**
1191
+ * @param {string} value
1192
+ * @returns {boolean}
1193
+ */
1194
+ function empty$1(value) {
1195
+ return value.replace(re, "") === "";
1196
+ }
1197
+ //#endregion
1198
+ //#region node_modules/property-information/lib/util/schema.js
1199
+ /**
1200
+ * @import {Schema as SchemaType, Space} from 'property-information'
1201
+ */
1202
+ /** @type {SchemaType} */
1203
+ var Schema = class {
1204
+ /**
1205
+ * @param {SchemaType['property']} property
1206
+ * Property.
1207
+ * @param {SchemaType['normal']} normal
1208
+ * Normal.
1209
+ * @param {Space | undefined} [space]
1210
+ * Space.
1211
+ * @returns
1212
+ * Schema.
1213
+ */
1214
+ constructor(property, normal, space) {
1215
+ this.normal = normal;
1216
+ this.property = property;
1217
+ if (space) this.space = space;
1218
+ }
1219
+ };
1220
+ Schema.prototype.normal = {};
1221
+ Schema.prototype.property = {};
1222
+ Schema.prototype.space = void 0;
1223
+ //#endregion
1224
+ //#region node_modules/property-information/lib/util/merge.js
1225
+ /**
1226
+ * @import {Info, Space} from 'property-information'
1227
+ */
1228
+ /**
1229
+ * @param {ReadonlyArray<Schema>} definitions
1230
+ * Definitions.
1231
+ * @param {Space | undefined} [space]
1232
+ * Space.
1233
+ * @returns {Schema}
1234
+ * Schema.
1235
+ */
1236
+ function merge(definitions, space) {
1237
+ /** @type {Record<string, Info>} */
1238
+ const property = {};
1239
+ /** @type {Record<string, string>} */
1240
+ const normal = {};
1241
+ for (const definition of definitions) {
1242
+ Object.assign(property, definition.property);
1243
+ Object.assign(normal, definition.normal);
1244
+ }
1245
+ return new Schema(property, normal, space);
1246
+ }
1247
+ //#endregion
1248
+ //#region node_modules/property-information/lib/normalize.js
830
1249
  /**
831
1250
  * Get the cleaned case insensitive form of an attribute or property.
832
1251
  *
@@ -22485,72 +22904,71 @@ function(property) {
22485
22904
  toMarkdownExtensions.push(gfmToMarkdown(settings));
22486
22905
  }
22487
22906
  //#endregion
22488
- //#region \0dsh-css:src/client/components/SkillDetailView.module.css.mjs
22489
- const css$10 = ".xO9T5G_detail{box-sizing:border-box;min-width:0;color:var(--dsw-alias-label-primary,#0f1115);flex-direction:column;gap:12px;padding:16px 20px 28px;display:flex}.xO9T5G_back{display:flex}.xO9T5G_header{align-items:flex-start;gap:18px;min-width:0;display:flex}.xO9T5G_headerText{flex:1;min-width:0}.xO9T5G_eyebrow{align-items:center;gap:7px;margin:0;font-size:11px;line-height:16px;display:flex}.xO9T5G_source{color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.1em;text-transform:uppercase;font-weight:700}.xO9T5G_version{color:var(--dsw-alias-label-tertiary,#81858c);font-variant-numeric:tabular-nums}.xO9T5G_name{letter-spacing:-.014em;overflow-wrap:anywhere;margin:4px 0 0;font-size:26px;font-weight:700;line-height:34px}.xO9T5G_name:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:3px;border-radius:var(--dsw-radius-xs,4px)}.xO9T5G_badges{flex-wrap:wrap;align-items:center;gap:6px;margin-top:10px;display:flex}.xO9T5G_summary{max-width:68ch;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;margin:10px 0 0;font-size:14px;line-height:22px}.xO9T5G_warning{border-radius:var(--dsw-radius-sm,8px);background:color-mix(in srgb, var(--dsw-alias-state-error-primary,#ec1313) 10%, transparent);color:var(--dsw-alias-label-primary,#0f1115);align-items:center;gap:8px;margin:0;padding:8px 12px;font-size:12.5px;line-height:19px;display:flex}.xO9T5G_warningIcon{color:var(--dsw-alias-state-error-primary,#ec1313);flex:none}.xO9T5G_reports{border-top:1px solid var(--dsw-alias-border-l1,#0000000a);border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);flex-wrap:wrap;align-items:center;gap:8px 14px;padding:10px 2px;font-size:12.5px;line-height:19px;display:flex}.xO9T5G_reportsLabel{color:var(--dsw-alias-label-tertiary,#81858c)}.xO9T5G_report{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.xO9T5G_reportVendor{color:var(--dsw-alias-label-primary,#0f1115);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;font-weight:600}.xO9T5G_reportStatus{color:var(--dsw-alias-label-secondary,#61666b)}.xO9T5G_reportLink{background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 12%, transparent);color:var(--dsw-alias-state-business-primary,#4176e6);border-radius:999px;align-items:center;padding:2px 9px;font-size:12px;font-weight:600;text-decoration:none;display:inline-flex}.xO9T5G_reportLink:hover{background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 20%, transparent)}.xO9T5G_reportLink:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:2px}.xO9T5G_columns{flex-wrap:wrap;align-items:flex-start;gap:20px;min-width:0;display:flex}.xO9T5G_main{flex-direction:column;flex:340px;gap:14px;min-width:0;display:flex}.xO9T5G_panel{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);min-width:0;padding:16px 18px}.xO9T5G_markdown{overflow-wrap:anywhere;max-width:74ch;font-size:13.5px;line-height:1.7}.xO9T5G_frontmatter{margin-top:18px}.xO9T5G_muted{color:var(--dsw-alias-label-tertiary,#81858c);align-items:center;gap:6px;margin:0;padding:6px 0;font-size:12.5px;line-height:19px;display:flex}.xO9T5G_files{flex-wrap:wrap;align-items:flex-start;gap:14px;min-width:0;display:flex}.xO9T5G_fileList{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);flex:220px;min-width:0;max-height:420px;margin:0;padding:4px;list-style:none;overflow-y:auto}.xO9T5G_fileItem{box-sizing:border-box;border-radius:var(--dsw-radius-xs,4px);width:100%;color:var(--dsw-alias-label-secondary,#61666b);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:7px;margin:0;padding:6px 8px;font-size:12px;line-height:18px;display:flex}.xO9T5G_fileItem:hover{background:var(--dsw-alias-interactive-bg-hover,#2631480f)}.xO9T5G_fileItem:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:-1px}.xO9T5G_fileItemActive{background:var(--dsw-alias-button-ghost-active-fill,#ebeef2);color:var(--dsw-alias-label-primary,#0f1115);font-weight:600}.xO9T5G_fileIcon{color:var(--dsw-alias-label-tertiary,#81858c);flex:none}.xO9T5G_filePath{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.xO9T5G_fileFlag{background:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 14%, transparent);color:var(--dsw-alias-state-warn-primary,#f59e0b);border-radius:999px;flex:none;padding:0 6px;font-size:10.5px;font-weight:600;line-height:17px}.xO9T5G_fileMeta{color:var(--dsw-alias-label-tertiary,#81858c);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;font-size:11px}.xO9T5G_fileView{flex:2 320px;min-width:0}.xO9T5G_fileBar{align-items:center;gap:8px;min-width:0;margin-bottom:8px;display:flex}.xO9T5G_fileBarPath{min-width:0;color:var(--dsw-alias-label-secondary,#61666b);text-overflow:ellipsis;white-space:nowrap;flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;overflow:hidden}.xO9T5G_copy{flex:none}.xO9T5G_rail{flex:260px;min-width:0}.xO9T5G_railCard{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);flex-direction:column;min-width:0;display:flex;overflow:hidden}.xO9T5G_railAction{flex-direction:column;gap:8px;padding:14px 14px 10px;display:flex}.xO9T5G_railAction>button{width:100%}.xO9T5G_facts{margin:0;padding:0 14px 6px}.xO9T5G_fact{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);justify-content:space-between;align-items:baseline;gap:14px;min-width:0;padding:9px 0;display:flex}.xO9T5G_fact:last-child{border-bottom:none}.xO9T5G_factLabel{color:var(--dsw-alias-label-tertiary,#81858c);flex:none;font-size:12.5px;line-height:19px}.xO9T5G_factValue{min-width:0;color:var(--dsw-alias-label-primary,#0f1115);text-align:right;overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:600;line-height:19px}.xO9T5G_srOnly{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.xO9T5G_markdown>:first-child{margin-top:0}.xO9T5G_markdown>:last-child{margin-bottom:0}.xO9T5G_markdown h1,.xO9T5G_markdown h2,.xO9T5G_markdown h3{margin:1.4em 0 .6em;line-height:1.35}.xO9T5G_markdown h1{font-size:1.7em}.xO9T5G_markdown h2{font-size:1.4em}.xO9T5G_markdown h3{font-size:1.15em}.xO9T5G_markdown p,.xO9T5G_markdown ul,.xO9T5G_markdown ol{margin:.75em 0}.xO9T5G_markdown ul,.xO9T5G_markdown ol{padding-left:1.7em}.xO9T5G_markdown a{color:var(--dsw-alias-state-business-primary,#4176e6);text-decoration:underline}.xO9T5G_markdown pre{background:var(--dsw-alias-bg-layer-2,#f2f3f5);border-radius:8px;padding:14px;overflow-x:auto}.xO9T5G_markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}.xO9T5G_markdown :not(pre)>code{background:var(--dsw-alias-bg-layer-2,#f2f3f5);border-radius:4px;padding:2px 5px}.xO9T5G_markdown blockquote{border-left:3px solid var(--dsw-alias-border-l2,#0000001a);color:var(--dsw-alias-label-secondary,#61666b);margin:1em 0;padding-left:14px}.xO9T5G_markdown table{border-collapse:collapse;max-width:100%;display:block;overflow-x:auto}.xO9T5G_markdown th,.xO9T5G_markdown td{border:1px solid var(--dsw-alias-border-l2,#0000001a);text-align:left;padding:7px 10px}.xO9T5G_markdown img{max-width:100%;height:auto}.xO9T5G_markdown hr{border:0;border-top:1px solid var(--dsw-alias-border-l2,#0000001a);margin:20px 0}.xO9T5G_skeletonAvatar,.xO9T5G_skeletonTitle,.xO9T5G_skeletonLine{background:var(--dsw-alias-border-l2,#0000001a);border-radius:5px;animation:1.6s ease-in-out infinite alternate xO9T5G_skeletonPulse;display:block}.xO9T5G_skeletonAvatar{border-radius:18px;flex:none;width:84px;height:84px}.xO9T5G_skeletonTitle{width:55%;height:28px;margin:8px 0 20px}.xO9T5G_skeletonLine{width:100%;height:13px;margin:16px 0}.xO9T5G_skeletonLine:nth-child(3n){width:72%}@keyframes xO9T5G_skeletonPulse{to{opacity:.4}}@media (prefers-reduced-motion:reduce){.xO9T5G_skeletonAvatar,.xO9T5G_skeletonTitle,.xO9T5G_skeletonLine{animation:none}}";
22490
- const tagId$10 = "@nanmicoder/dsh-skills-hub/SkillDetailView.module.css";
22491
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$10) + "]") === null) {
22907
+ //#region \0dsh-css:src/client/components/SkillDetailShell.module.css.mjs
22908
+ const css$11 = "._58q8UW_detail{box-sizing:border-box;min-width:0;color:var(--dsw-alias-label-primary,#0f1115);flex-direction:column;gap:12px;padding:16px 20px 28px;display:flex}._58q8UW_back{display:flex}._58q8UW_header{align-items:flex-start;gap:18px;min-width:0;display:flex}._58q8UW_headerText{flex:1;min-width:0}._58q8UW_eyebrow{align-items:center;gap:7px;margin:0;font-size:11px;line-height:16px;display:flex}._58q8UW_source{color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.1em;text-transform:uppercase;font-weight:700}._58q8UW_version{color:var(--dsw-alias-label-tertiary,#81858c);font-variant-numeric:tabular-nums}._58q8UW_name{letter-spacing:-.014em;overflow-wrap:anywhere;margin:4px 0 0;font-size:26px;font-weight:700;line-height:34px}._58q8UW_name:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:3px;border-radius:var(--dsw-radius-xs,4px)}._58q8UW_badges{flex-wrap:wrap;align-items:center;gap:6px;margin-top:10px;display:flex}._58q8UW_summary{max-width:68ch;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;margin:10px 0 0;font-size:14px;line-height:22px}._58q8UW_columns{flex-wrap:wrap;align-items:flex-start;gap:20px;min-width:0;display:flex}._58q8UW_main{flex-direction:column;flex:340px;gap:14px;min-width:0;display:flex}._58q8UW_panel{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);min-width:0;padding:16px 18px}._58q8UW_markdown{overflow-wrap:anywhere;max-width:74ch;font-size:13.5px;line-height:1.7}._58q8UW_frontmatter{margin-top:18px}._58q8UW_muted{color:var(--dsw-alias-label-tertiary,#81858c);align-items:center;gap:6px;margin:0;padding:6px 0;font-size:12.5px;line-height:19px;display:flex}._58q8UW_files{flex-wrap:wrap;align-items:flex-start;gap:14px;min-width:0;display:flex}._58q8UW_fileList{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);flex:220px;min-width:0;max-height:420px;margin:0;padding:4px;list-style:none;overflow-y:auto}._58q8UW_fileItem{box-sizing:border-box;border-radius:var(--dsw-radius-xs,4px);width:100%;color:var(--dsw-alias-label-secondary,#61666b);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:7px;margin:0;padding:6px 8px;font-size:12px;line-height:18px;display:flex}._58q8UW_fileItem:hover{background:var(--dsw-alias-interactive-bg-hover,#2631480f)}._58q8UW_fileItem:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:-1px}._58q8UW_fileItemActive{background:var(--dsw-alias-button-ghost-active-fill,#ebeef2);color:var(--dsw-alias-label-primary,#0f1115);font-weight:600}._58q8UW_fileIcon{color:var(--dsw-alias-label-tertiary,#81858c);flex:none}._58q8UW_filePath{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}._58q8UW_fileFlag{background:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 14%, transparent);color:var(--dsw-alias-state-warn-primary,#f59e0b);border-radius:999px;flex:none;padding:0 6px;font-size:10.5px;font-weight:600;line-height:17px}._58q8UW_fileMeta{color:var(--dsw-alias-label-tertiary,#81858c);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;font-size:11px}._58q8UW_fileView{flex:2 320px;min-width:0}._58q8UW_fileBar{align-items:center;gap:8px;min-width:0;margin-bottom:8px;display:flex}._58q8UW_fileBarPath{min-width:0;color:var(--dsw-alias-label-secondary,#61666b);text-overflow:ellipsis;white-space:nowrap;flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;overflow:hidden}._58q8UW_copy{flex:none}._58q8UW_viewToggle{border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-layer-2,#f0f2f5);border-radius:999px;flex:none;gap:2px;padding:2px;display:inline-flex}._58q8UW_viewToggle button{color:var(--dsw-alias-label-secondary,#61666b);font:inherit;cursor:pointer;background:0 0;border:0;border-radius:999px;padding:2px 10px;font-size:11.5px;line-height:18px}._58q8UW_viewToggle button[aria-pressed=true]{background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#0f1115);font-weight:600}._58q8UW_viewToggle button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px}._58q8UW_rail{flex:260px;min-width:0}._58q8UW_railCard{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);flex-direction:column;min-width:0;display:flex;overflow:hidden}._58q8UW_railAction{flex-direction:column;gap:8px;padding:14px 14px 10px;display:flex}._58q8UW_railAction>button{width:100%}._58q8UW_facts{margin:0;padding:0 14px 6px}._58q8UW_fact{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);justify-content:space-between;align-items:baseline;gap:14px;min-width:0;padding:9px 0;display:flex}._58q8UW_fact:last-child{border-bottom:none}._58q8UW_factLabel{color:var(--dsw-alias-label-tertiary,#81858c);flex:none;font-size:12.5px;line-height:19px}._58q8UW_factValue{min-width:0;color:var(--dsw-alias-label-primary,#0f1115);text-align:right;overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:600;line-height:19px}._58q8UW_srOnly{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}._58q8UW_markdown>:first-child{margin-top:0}._58q8UW_markdown>:last-child{margin-bottom:0}._58q8UW_markdown h1,._58q8UW_markdown h2,._58q8UW_markdown h3{margin:1.4em 0 .6em;line-height:1.35}._58q8UW_markdown h1{font-size:1.7em}._58q8UW_markdown h2{font-size:1.4em}._58q8UW_markdown h3{font-size:1.15em}._58q8UW_markdown p,._58q8UW_markdown ul,._58q8UW_markdown ol{margin:.75em 0}._58q8UW_markdown ul,._58q8UW_markdown ol{padding-left:1.7em}._58q8UW_markdown a{color:var(--dsw-alias-state-business-primary,#4176e6);text-decoration:underline}._58q8UW_markdown pre{background:var(--dsw-alias-bg-layer-2,#f2f3f5);border-radius:8px;padding:14px;overflow-x:auto}._58q8UW_markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}._58q8UW_markdown :not(pre)>code{background:var(--dsw-alias-bg-layer-2,#f2f3f5);border-radius:4px;padding:2px 5px}._58q8UW_markdown blockquote{border-left:3px solid var(--dsw-alias-border-l2,#0000001a);color:var(--dsw-alias-label-secondary,#61666b);margin:1em 0;padding-left:14px}._58q8UW_markdown table{border-collapse:collapse;max-width:100%;display:block;overflow-x:auto}._58q8UW_markdown th,._58q8UW_markdown td{border:1px solid var(--dsw-alias-border-l2,#0000001a);text-align:left;padding:7px 10px}._58q8UW_markdown img{max-width:100%;height:auto}._58q8UW_markdown hr{border:0;border-top:1px solid var(--dsw-alias-border-l2,#0000001a);margin:20px 0}._58q8UW_skeletonAvatar,._58q8UW_skeletonTitle,._58q8UW_skeletonLine{background:var(--dsw-alias-border-l2,#0000001a);border-radius:5px;animation:1.6s ease-in-out infinite alternate _58q8UW_skeletonPulse;display:block}._58q8UW_skeletonAvatar{border-radius:18px;flex:none;width:84px;height:84px}._58q8UW_skeletonTitle{width:55%;height:28px;margin:8px 0 20px}._58q8UW_skeletonLine{width:100%;height:13px;margin:16px 0}._58q8UW_skeletonLine:nth-child(3n){width:72%}@keyframes _58q8UW_skeletonPulse{to{opacity:.4}}@media (prefers-reduced-motion:reduce){._58q8UW_skeletonAvatar,._58q8UW_skeletonTitle,._58q8UW_skeletonLine{animation:none}}";
22909
+ const tagId$11 = "@nanmicoder/dsh-skills-hub/SkillDetailShell.module.css";
22910
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$11) + "]") === null) {
22492
22911
  const tag = document.createElement("style");
22493
22912
  tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
22494
- tag.dataset.pluginCss = tagId$10;
22495
- tag.textContent = css$10;
22913
+ tag.dataset.pluginCss = tagId$11;
22914
+ tag.textContent = css$11;
22496
22915
  document.head.appendChild(tag);
22497
22916
  }
22498
- var SkillDetailView_module_css_default = {
22499
- "back": "xO9T5G_back",
22500
- "badges": "xO9T5G_badges",
22501
- "columns": "xO9T5G_columns",
22502
- "copy": "xO9T5G_copy",
22503
- "detail": "xO9T5G_detail",
22504
- "eyebrow": "xO9T5G_eyebrow",
22505
- "fact": "xO9T5G_fact",
22506
- "factLabel": "xO9T5G_factLabel",
22507
- "factValue": "xO9T5G_factValue",
22508
- "facts": "xO9T5G_facts",
22509
- "fileBar": "xO9T5G_fileBar",
22510
- "fileBarPath": "xO9T5G_fileBarPath",
22511
- "fileFlag": "xO9T5G_fileFlag",
22512
- "fileIcon": "xO9T5G_fileIcon",
22513
- "fileItem": "xO9T5G_fileItem",
22514
- "fileItemActive": "xO9T5G_fileItemActive",
22515
- "fileList": "xO9T5G_fileList",
22516
- "fileMeta": "xO9T5G_fileMeta",
22517
- "filePath": "xO9T5G_filePath",
22518
- "fileView": "xO9T5G_fileView",
22519
- "files": "xO9T5G_files",
22520
- "frontmatter": "xO9T5G_frontmatter",
22521
- "header": "xO9T5G_header",
22522
- "headerText": "xO9T5G_headerText",
22523
- "main": "xO9T5G_main",
22524
- "markdown": "xO9T5G_markdown",
22525
- "muted": "xO9T5G_muted",
22526
- "name": "xO9T5G_name",
22527
- "panel": "xO9T5G_panel",
22528
- "rail": "xO9T5G_rail",
22529
- "railAction": "xO9T5G_railAction",
22530
- "railCard": "xO9T5G_railCard",
22531
- "report": "xO9T5G_report",
22532
- "reportLink": "xO9T5G_reportLink",
22533
- "reportStatus": "xO9T5G_reportStatus",
22534
- "reportVendor": "xO9T5G_reportVendor",
22535
- "reports": "xO9T5G_reports",
22536
- "reportsLabel": "xO9T5G_reportsLabel",
22537
- "skeletonAvatar": "xO9T5G_skeletonAvatar",
22538
- "skeletonLine": "xO9T5G_skeletonLine",
22539
- "skeletonPulse": "xO9T5G_skeletonPulse",
22540
- "skeletonTitle": "xO9T5G_skeletonTitle",
22541
- "source": "xO9T5G_source",
22542
- "srOnly": "xO9T5G_srOnly",
22543
- "summary": "xO9T5G_summary",
22544
- "version": "xO9T5G_version",
22545
- "warning": "xO9T5G_warning",
22546
- "warningIcon": "xO9T5G_warningIcon"
22917
+ var SkillDetailShell_module_css_default = {
22918
+ "back": "_58q8UW_back",
22919
+ "badges": "_58q8UW_badges",
22920
+ "columns": "_58q8UW_columns",
22921
+ "copy": "_58q8UW_copy",
22922
+ "detail": "_58q8UW_detail",
22923
+ "eyebrow": "_58q8UW_eyebrow",
22924
+ "fact": "_58q8UW_fact",
22925
+ "factLabel": "_58q8UW_factLabel",
22926
+ "factValue": "_58q8UW_factValue",
22927
+ "facts": "_58q8UW_facts",
22928
+ "fileBar": "_58q8UW_fileBar",
22929
+ "fileBarPath": "_58q8UW_fileBarPath",
22930
+ "fileFlag": "_58q8UW_fileFlag",
22931
+ "fileIcon": "_58q8UW_fileIcon",
22932
+ "fileItem": "_58q8UW_fileItem",
22933
+ "fileItemActive": "_58q8UW_fileItemActive",
22934
+ "fileList": "_58q8UW_fileList",
22935
+ "fileMeta": "_58q8UW_fileMeta",
22936
+ "filePath": "_58q8UW_filePath",
22937
+ "fileView": "_58q8UW_fileView",
22938
+ "files": "_58q8UW_files",
22939
+ "frontmatter": "_58q8UW_frontmatter",
22940
+ "header": "_58q8UW_header",
22941
+ "headerText": "_58q8UW_headerText",
22942
+ "main": "_58q8UW_main",
22943
+ "markdown": "_58q8UW_markdown",
22944
+ "muted": "_58q8UW_muted",
22945
+ "name": "_58q8UW_name",
22946
+ "panel": "_58q8UW_panel",
22947
+ "rail": "_58q8UW_rail",
22948
+ "railAction": "_58q8UW_railAction",
22949
+ "railCard": "_58q8UW_railCard",
22950
+ "skeletonAvatar": "_58q8UW_skeletonAvatar",
22951
+ "skeletonLine": "_58q8UW_skeletonLine",
22952
+ "skeletonPulse": "_58q8UW_skeletonPulse",
22953
+ "skeletonTitle": "_58q8UW_skeletonTitle",
22954
+ "source": "_58q8UW_source",
22955
+ "srOnly": "_58q8UW_srOnly",
22956
+ "summary": "_58q8UW_summary",
22957
+ "version": "_58q8UW_version",
22958
+ "viewToggle": "_58q8UW_viewToggle"
22547
22959
  };
22548
22960
  //#endregion
22549
22961
  //#region lib/client/components/MarkdownView.js
22550
- /** Self-contained GFM rendering for untrusted marketplace and local skill docs. */
22962
+ /**
22963
+ * Self-contained GFM rendering for untrusted marketplace and local skill docs.
22964
+ *
22965
+ * Raw HTML is skipped and external links are opened with `noopener noreferrer`;
22966
+ * the typography lives in the detail shell so a rendered document looks the same
22967
+ * in the overview tab and in the file viewer.
22968
+ */
22551
22969
  function MarkdownView({ content }) {
22552
22970
  return (0, react_jsx_runtime.jsx)("div", {
22553
- className: SkillDetailView_module_css_default.markdown,
22971
+ className: SkillDetailShell_module_css_default.markdown,
22554
22972
  children: (0, react_jsx_runtime.jsx)(Markdown, {
22555
22973
  remarkPlugins: [remarkGfm],
22556
22974
  skipHtml: true,
@@ -22565,51 +22983,840 @@ function(property) {
22565
22983
  });
22566
22984
  }
22567
22985
  //#endregion
22568
- //#region \0dsh-css:src/client/components/InstalledSkills.module.css.mjs
22569
- const css$9 = ".eYwU2a_root{box-sizing:border-box;width:100%;max-width:1280px;margin:0 auto;padding:28px}.eYwU2a_header{justify-content:space-between;align-items:flex-start;gap:20px;display:flex}.eYwU2a_header h1{margin:0;font-size:24px}.eYwU2a_header p{color:var(--dsw-alias-label-secondary,#666);max-width:760px;font-size:13px;line-height:1.6}.eYwU2a_search{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#ddd);background:var(--dsw-alias-bg-base,white);width:100%;color:inherit;font:inherit;border-radius:8px;padding:10px 12px}.eYwU2a_list{gap:12px;display:grid}.eYwU2a_card{border:1px solid var(--dsw-alias-border-l2,#e5e5e5);border-radius:12px;justify-content:space-between;align-items:center;gap:20px;padding:18px;display:flex}.eYwU2a_info{min-width:0}.eYwU2a_name{color:inherit;font:inherit;cursor:pointer;text-align:left;background:0 0;border:0;margin-right:10px;padding:0;font-weight:600}.eYwU2a_name:hover{text-decoration:underline}.eYwU2a_badge{background:var(--dsw-alias-bg-layer-2,#f0f2f5);border-radius:4px;margin:2px 6px 2px 0;padding:2px 6px;font-size:11px;display:inline-block}.eYwU2a_version,.eYwU2a_count{color:var(--dsw-alias-label-secondary,#777);font-size:12px}.eYwU2a_summary{margin:8px 0;font-size:13px;line-height:1.6}.eYwU2a_path{overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary,#777);margin-bottom:0;font-family:monospace;font-size:11px;line-height:1.7}.eYwU2a_actions{flex-shrink:0;gap:8px;display:flex}.eYwU2a_empty{text-align:center;color:var(--dsw-alias-label-secondary,#777);padding:50px 10px}.eYwU2a_error{color:var(--dsw-alias-state-error-primary,#c33)}.eYwU2a_skeleton{background:var(--dsw-alias-bg-layer-2,#f0f2f5);border-radius:12px;height:108px;margin-top:10px;animation:1.5s ease-in-out infinite eYwU2a_pulse}.eYwU2a_preview{max-height:60vh;padding:12px 0;overflow:auto}.eYwU2a_markdown{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;line-height:1.7}@keyframes eYwU2a_pulse{50%{opacity:.45}}@media (prefers-reduced-motion:reduce){.eYwU2a_skeleton{animation:none}}@media (width<=640px){.eYwU2a_root{padding:18px}.eYwU2a_card{flex-direction:column;align-items:stretch}.eYwU2a_actions{justify-content:flex-end}}";
22570
- const tagId$9 = "@nanmicoder/dsh-skills-hub/InstalledSkills.module.css";
22571
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$9) + "]") === null) {
22986
+ //#region \0dsh-css:src/client/components/SkillAvatar.module.css.mjs
22987
+ const css$10 = ".C6tuwq_image{object-fit:cover;border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-module-platform,#f5f6f7);flex:none}.C6tuwq_letter{box-sizing:border-box;letter-spacing:-.02em;color:var(--dsw-alias-label-primary-inverted,#fff);user-select:none;box-shadow:inset 0 1px 0 color-mix(in srgb, var(--dsw-alias-label-primary-inverted,#fff) 12%, transparent);flex:none;justify-content:center;align-items:center;font-weight:700;line-height:1;display:inline-flex;overflow:hidden}.C6tuwq_tone1{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 74%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 48%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone2{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 80%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 54%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone5{background:linear-gradient(145deg, color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 55%, var(--dsw-alias-state-success-primary,#22c55e)) 78%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 55%, var(--dsw-alias-state-success-primary,#22c55e)) 50%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone0{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 82%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 56%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone4{background:linear-gradient(145deg, color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 58%, var(--dsw-alias-state-success-primary,#22c55e)) 80%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 58%, var(--dsw-alias-state-success-primary,#22c55e)) 52%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone3{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-idle-primary,#d4d4d4) 62%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-idle-primary,#d4d4d4) 38%, var(--dsw-alias-label-primary,#0f1115)))}";
22988
+ const tagId$10 = "@nanmicoder/dsh-skills-hub/SkillAvatar.module.css";
22989
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$10) + "]") === null) {
22572
22990
  const tag = document.createElement("style");
22573
22991
  tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
22574
- tag.dataset.pluginCss = tagId$9;
22575
- tag.textContent = css$9;
22992
+ tag.dataset.pluginCss = tagId$10;
22993
+ tag.textContent = css$10;
22576
22994
  document.head.appendChild(tag);
22577
22995
  }
22578
- var InstalledSkills_module_css_default = {
22579
- "actions": "eYwU2a_actions",
22580
- "badge": "eYwU2a_badge",
22581
- "card": "eYwU2a_card",
22582
- "count": "eYwU2a_count",
22583
- "empty": "eYwU2a_empty",
22584
- "error": "eYwU2a_error",
22585
- "header": "eYwU2a_header",
22586
- "info": "eYwU2a_info",
22587
- "list": "eYwU2a_list",
22588
- "markdown": "eYwU2a_markdown",
22589
- "name": "eYwU2a_name",
22590
- "path": "eYwU2a_path",
22591
- "preview": "eYwU2a_preview",
22592
- "pulse": "eYwU2a_pulse",
22593
- "root": "eYwU2a_root",
22594
- "search": "eYwU2a_search",
22595
- "skeleton": "eYwU2a_skeleton",
22596
- "summary": "eYwU2a_summary",
22597
- "version": "eYwU2a_version"
22598
- };
22599
- //#endregion
22996
+ var SkillAvatar_module_css_default = {
22997
+ "image": "C6tuwq_image",
22998
+ "letter": "C6tuwq_letter",
22999
+ "tone0": "C6tuwq_tone0",
23000
+ "tone1": "C6tuwq_tone1",
23001
+ "tone2": "C6tuwq_tone2",
23002
+ "tone3": "C6tuwq_tone3",
23003
+ "tone4": "C6tuwq_tone4",
23004
+ "tone5": "C6tuwq_tone5"
23005
+ };
23006
+ //#endregion
23007
+ //#region lib/client/components/SkillAvatar.js
23008
+ /** Palette slots; each maps to one `.toneN` class in the module stylesheet. */
23009
+ const TONE_COUNT = 6;
23010
+ /**
23011
+ * Per-source tone family. The `source` prop is otherwise unused by a letter
23012
+ * avatar, so it earns its place as the family selector: ClawHub entries read
23013
+ * warm, SkillHub entries read cool, and a mixed grid still shows at a glance
23014
+ * where a card came from. A locally authored skill has no marketplace family,
23015
+ * so its tile stays neutral rather than borrowing one.
23016
+ */
23017
+ const SOURCE_TONES = {
23018
+ clawhub: [
23019
+ 2,
23020
+ 5,
23021
+ 1
23022
+ ],
23023
+ skillhub: [
23024
+ 0,
23025
+ 4,
23026
+ 3
23027
+ ],
23028
+ local: [3]
23029
+ };
23030
+ /** Class name per tone slot, resolved once so the render path stays trivial. */
23031
+ const TONE_CLASSES = [
23032
+ SkillAvatar_module_css_default["tone0"] ?? "",
23033
+ SkillAvatar_module_css_default["tone1"] ?? "",
23034
+ SkillAvatar_module_css_default["tone2"] ?? "",
23035
+ SkillAvatar_module_css_default["tone3"] ?? "",
23036
+ SkillAvatar_module_css_default["tone4"] ?? "",
23037
+ SkillAvatar_module_css_default["tone5"] ?? ""
23038
+ ];
23039
+ /**
23040
+ * Deterministic palette index: the same name always keeps the same identity
23041
+ * color across sessions, pages and re-renders.
23042
+ */
23043
+ function hashIndex(input) {
23044
+ let hash = 0;
23045
+ for (let i = 0; i < input.length; i++) hash = hash * 31 + input.charCodeAt(i) | 0;
23046
+ return Math.abs(hash) % TONE_COUNT;
23047
+ }
23048
+ /**
23049
+ * First visible character, uppercased. `Array.from` rather than `name[0]` so a
23050
+ * CJK or astral-plane initial is not cut in half by UTF-16 indexing.
23051
+ */
23052
+ function initialOf(name) {
23053
+ const first = Array.from(name.trim())[0];
23054
+ return first ? first.toUpperCase() : "?";
23055
+ }
23056
+ /**
23057
+ * Skill icon tile.
23058
+ *
23059
+ * An `<img>` is rendered only for an `https:` URL: the string arrives from a
23060
+ * third-party catalogue, and allowing `javascript:` or `data:` here would turn
23061
+ * an upstream payload into script execution inside the DSH page.
23062
+ */
23063
+ function SkillAvatar(props) {
23064
+ const { name, source, iconUrl, size } = props;
23065
+ const radius = Math.round(size * .24);
23066
+ if (iconUrl !== void 0 && iconUrl.startsWith("https://")) return (0, react_jsx_runtime.jsx)("img", {
23067
+ src: iconUrl,
23068
+ alt: "",
23069
+ width: size,
23070
+ height: size,
23071
+ loading: "lazy",
23072
+ decoding: "async",
23073
+ className: SkillAvatar_module_css_default.image,
23074
+ style: {
23075
+ width: size,
23076
+ height: size,
23077
+ borderRadius: radius
23078
+ }
23079
+ });
23080
+ const family = SOURCE_TONES[source];
23081
+ const tone = family[hashIndex(name) % family.length] ?? 0;
23082
+ return (0, react_jsx_runtime.jsx)("span", {
23083
+ "aria-hidden": "true",
23084
+ className: `${SkillAvatar_module_css_default.letter} ${TONE_CLASSES[tone] ?? ""}`,
23085
+ style: {
23086
+ width: size,
23087
+ height: size,
23088
+ borderRadius: radius,
23089
+ fontSize: Math.round(size * .4)
23090
+ },
23091
+ children: initialOf(name)
23092
+ });
23093
+ }
23094
+ //#endregion
23095
+ //#region lib/client/components/SkillDetailShell.js
23096
+ /**
23097
+ * The detail surface shared by the market catalogue and the installed inventory.
23098
+ *
23099
+ * Both pages answer the same question — "what is inside this skill?" — and the
23100
+ * only real difference is where the facts come from: an upstream provider or the
23101
+ * filesystem. Everything that is *reading* rather than *fetching* therefore
23102
+ * lives here: the back affordance, the identity header, the two tabs, the
23103
+ * document column, the facts rail and the focus move that keeps a keyboard user
23104
+ * from being left at the top of the document when the list is replaced.
23105
+ *
23106
+ * The two pages are not identical, so the shell takes slots rather than a data
23107
+ * model: `badges`, `notice`, `overview`, `files` and `rail` are composed by the
23108
+ * caller, while each caller owns its own loading state and its own choice of tab
23109
+ * storage (the market page keeps it in `MarketState`, the local page in its own
23110
+ * component state).
23111
+ */
23112
+ /** Stable DOM ids for the tab list and its panels; one detail page is mounted at a time. */
23113
+ const TAB_ID = "skills-hub-detail-tab";
23114
+ const PANEL_ID$1 = "skills-hub-detail-panel";
23115
+ /**
23116
+ * The shell's stylesheet, re-exported for its callers.
23117
+ *
23118
+ * The pages built on this shell also render shell-shaped fragments (a report
23119
+ * row, a path line, a facts list) and reaching them through an import is more
23120
+ * honest than re-declaring near-copies of the same rules in a second module.
23121
+ */
23122
+ const detailStyles = SkillDetailShell_module_css_default;
23123
+ /** One label/value row of the facts rail. */
23124
+ function Fact(props) {
23125
+ return (0, react_jsx_runtime.jsxs)("div", {
23126
+ className: SkillDetailShell_module_css_default.fact,
23127
+ children: [(0, react_jsx_runtime.jsx)("dt", {
23128
+ className: SkillDetailShell_module_css_default.factLabel,
23129
+ children: props.label
23130
+ }), (0, react_jsx_runtime.jsx)("dd", {
23131
+ className: SkillDetailShell_module_css_default.factValue,
23132
+ children: props.children
23133
+ })]
23134
+ });
23135
+ }
23136
+ function SkillDetailShell(props) {
23137
+ const { name, activeTab, onTabChange } = props;
23138
+ const headingRef = (0, react.useRef)(null);
23139
+ (0, react.useEffect)(() => {
23140
+ headingRef.current?.focus();
23141
+ }, [props.focusKey]);
23142
+ const tabs = [{
23143
+ value: "overview",
23144
+ label: props.overviewLabel,
23145
+ id: `${TAB_ID}-overview`,
23146
+ panelId: `${PANEL_ID$1}-overview`
23147
+ }, {
23148
+ value: "files",
23149
+ label: props.filesLabel,
23150
+ id: `${TAB_ID}-files`,
23151
+ panelId: `${PANEL_ID$1}-files`
23152
+ }];
23153
+ return (0, react_jsx_runtime.jsxs)("div", {
23154
+ className: SkillDetailShell_module_css_default.detail,
23155
+ children: [
23156
+ (0, react_jsx_runtime.jsx)("div", {
23157
+ className: SkillDetailShell_module_css_default.back,
23158
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23159
+ variant: "ghost",
23160
+ size: "sm",
23161
+ icon: (0, react_jsx_runtime.jsx)(ArrowLeftIcon, { size: 16 }),
23162
+ onClick: props.onBack,
23163
+ children: props.backLabel
23164
+ })
23165
+ }),
23166
+ (0, react_jsx_runtime.jsxs)("header", {
23167
+ className: SkillDetailShell_module_css_default.header,
23168
+ children: [props.avatar, (0, react_jsx_runtime.jsxs)("div", {
23169
+ className: SkillDetailShell_module_css_default.headerText,
23170
+ children: [
23171
+ (0, react_jsx_runtime.jsx)("p", {
23172
+ className: SkillDetailShell_module_css_default.eyebrow,
23173
+ children: props.eyebrow
23174
+ }),
23175
+ (0, react_jsx_runtime.jsx)("h1", {
23176
+ ref: headingRef,
23177
+ tabIndex: -1,
23178
+ className: SkillDetailShell_module_css_default.name,
23179
+ children: name
23180
+ }),
23181
+ props.badges === void 0 ? null : (0, react_jsx_runtime.jsx)("div", {
23182
+ className: SkillDetailShell_module_css_default.badges,
23183
+ children: props.badges
23184
+ }),
23185
+ props.summary === void 0 || props.summary === "" ? null : (0, react_jsx_runtime.jsx)("p", {
23186
+ className: SkillDetailShell_module_css_default.summary,
23187
+ children: props.summary
23188
+ })
23189
+ ]
23190
+ })]
23191
+ }),
23192
+ props.notice,
23193
+ (0, react_jsx_runtime.jsxs)("div", {
23194
+ className: SkillDetailShell_module_css_default.columns,
23195
+ children: [(0, react_jsx_runtime.jsxs)("main", {
23196
+ className: SkillDetailShell_module_css_default.main,
23197
+ children: [
23198
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
23199
+ items: tabs,
23200
+ value: activeTab,
23201
+ onChange: onTabChange,
23202
+ label: name
23203
+ }),
23204
+ activeTab === "overview" && (0, react_jsx_runtime.jsx)("section", {
23205
+ className: SkillDetailShell_module_css_default.panel,
23206
+ id: `${PANEL_ID$1}-overview`,
23207
+ role: "tabpanel",
23208
+ "aria-labelledby": `${TAB_ID}-overview`,
23209
+ children: props.overview
23210
+ }),
23211
+ activeTab === "files" && (0, react_jsx_runtime.jsx)("section", {
23212
+ className: SkillDetailShell_module_css_default.panel,
23213
+ id: `${PANEL_ID$1}-files`,
23214
+ role: "tabpanel",
23215
+ "aria-labelledby": `${TAB_ID}-files`,
23216
+ children: props.files
23217
+ })
23218
+ ]
23219
+ }), (0, react_jsx_runtime.jsx)("aside", {
23220
+ className: SkillDetailShell_module_css_default.rail,
23221
+ children: (0, react_jsx_runtime.jsx)("div", {
23222
+ className: SkillDetailShell_module_css_default.railCard,
23223
+ children: props.rail
23224
+ })
23225
+ })]
23226
+ })
23227
+ ]
23228
+ });
23229
+ }
23230
+ /**
23231
+ * Keeps the detail layout stable while its request is in flight.
23232
+ *
23233
+ * It renders the same boxes as the real page rather than a spinner, because the
23234
+ * click that opens a detail is the moment the panel changes shape; a centered
23235
+ * "loading" line would make that change happen twice.
23236
+ */
23237
+ function SkillDetailSkeleton({ onBack }) {
23238
+ const t = useT();
23239
+ return (0, react_jsx_runtime.jsxs)("div", {
23240
+ className: SkillDetailShell_module_css_default.detail,
23241
+ "aria-busy": "true",
23242
+ children: [(0, react_jsx_runtime.jsx)("div", {
23243
+ className: SkillDetailShell_module_css_default.back,
23244
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23245
+ variant: "ghost",
23246
+ size: "sm",
23247
+ icon: (0, react_jsx_runtime.jsx)(ArrowLeftIcon, { size: 16 }),
23248
+ onClick: onBack,
23249
+ children: t("back")
23250
+ })
23251
+ }), (0, react_jsx_runtime.jsx)("div", {
23252
+ role: "status",
23253
+ "aria-label": t("loading"),
23254
+ children: (0, react_jsx_runtime.jsxs)("div", {
23255
+ "aria-hidden": "true",
23256
+ children: [(0, react_jsx_runtime.jsxs)("div", {
23257
+ className: SkillDetailShell_module_css_default.header,
23258
+ children: [(0, react_jsx_runtime.jsx)("span", { className: SkillDetailShell_module_css_default.skeletonAvatar }), (0, react_jsx_runtime.jsxs)("div", {
23259
+ className: SkillDetailShell_module_css_default.headerText,
23260
+ children: [
23261
+ (0, react_jsx_runtime.jsx)("span", { className: SkillDetailShell_module_css_default.skeletonTitle }),
23262
+ (0, react_jsx_runtime.jsx)("span", { className: SkillDetailShell_module_css_default.skeletonLine }),
23263
+ (0, react_jsx_runtime.jsx)("span", { className: SkillDetailShell_module_css_default.skeletonLine })
23264
+ ]
23265
+ })]
23266
+ }), (0, react_jsx_runtime.jsxs)("div", {
23267
+ className: SkillDetailShell_module_css_default.columns,
23268
+ style: { marginTop: 24 },
23269
+ children: [(0, react_jsx_runtime.jsx)("div", {
23270
+ className: SkillDetailShell_module_css_default.main,
23271
+ children: (0, react_jsx_runtime.jsx)("div", {
23272
+ className: SkillDetailShell_module_css_default.panel,
23273
+ children: Array.from({ length: 10 }, (_, index) => (0, react_jsx_runtime.jsx)("span", { className: SkillDetailShell_module_css_default.skeletonLine }, index))
23274
+ })
23275
+ }), (0, react_jsx_runtime.jsx)("div", {
23276
+ className: SkillDetailShell_module_css_default.rail,
23277
+ children: (0, react_jsx_runtime.jsx)("div", {
23278
+ className: SkillDetailShell_module_css_default.panel,
23279
+ children: Array.from({ length: 5 }, (_, index) => (0, react_jsx_runtime.jsx)("span", { className: SkillDetailShell_module_css_default.skeletonLine }, index))
23280
+ })
23281
+ })]
23282
+ })]
23283
+ })
23284
+ })]
23285
+ });
23286
+ }
23287
+ //#endregion
23288
+ //#region lib/client/components/SkillFiles.js
23289
+ /**
23290
+ * Files tab body, shared by the two detail pages.
23291
+ *
23292
+ * The market page lists provider file metadata and the local page lists the
23293
+ * bytes on disk, but both are the same control: a picker on the left, one
23294
+ * document on the right, and a copy button that confirms itself. The only
23295
+ * difference the caller keeps is how a selection is fetched.
23296
+ *
23297
+ * A markdown document opens *rendered*: SKILL.md is prose meant to be read, and
23298
+ * showing its source first answers a question nobody asked. Installing still has
23299
+ * to be a trust decision, though, so the raw bytes are one toggle away — with
23300
+ * line numbers and the copy button pointed at exactly what is on disk. Other
23301
+ * files have no rendered form and stay source-only.
23302
+ */
23303
+ /** How long the copy button stays in its confirmed state. */
23304
+ const COPIED_RESET_MS$1 = 2e3;
23305
+ /**
23306
+ * A rendered markdown document: its metadata as structure, its body as prose.
23307
+ *
23308
+ * The split matters more than it looks: handed to the renderer, the closing
23309
+ * `---` of the frontmatter turns the whole block into one setext heading.
23310
+ */
23311
+ function MarkdownFilePreview({ content }) {
23312
+ const split = (0, react.useMemo)(() => splitSkillFrontmatter(content), [content]);
23313
+ return (0, react_jsx_runtime.jsxs)("div", {
23314
+ className: detailStyles.markdown,
23315
+ children: [split.frontmatter !== null && (0, react_jsx_runtime.jsx)("div", {
23316
+ className: detailStyles.frontmatter,
23317
+ children: (0, react_jsx_runtime.jsx)(FrontmatterPanel, { data: split.frontmatter })
23318
+ }), (0, react_jsx_runtime.jsx)(MarkdownView, { content: split.body })]
23319
+ });
23320
+ }
23321
+ function SkillFiles(props) {
23322
+ const { files, selected, loading, error = null, onSelect } = props;
23323
+ const t = useT();
23324
+ const [copied, setCopied] = (0, react.useState)(false);
23325
+ const [mode, setMode] = (0, react.useState)("preview");
23326
+ const resetTimer = (0, react.useRef)(void 0);
23327
+ (0, react.useEffect)(() => () => {
23328
+ if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
23329
+ }, []);
23330
+ const selectedPath = selected?.path ?? null;
23331
+ /** Only markdown has a rendered form; a toggle on a script or a JSON file would be a lie. */
23332
+ const renderable = selected !== null && selected.language === "markdown";
23333
+ (0, react.useEffect)(() => {
23334
+ setMode("preview");
23335
+ }, [selectedPath]);
23336
+ const copyFile = (0, react.useCallback)(() => {
23337
+ const content = selected?.content;
23338
+ if (content === void 0) return;
23339
+ (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(content).then((ok) => {
23340
+ if (!ok) return;
23341
+ setCopied(true);
23342
+ if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
23343
+ resetTimer.current = setTimeout(() => setCopied(false), COPIED_RESET_MS$1);
23344
+ });
23345
+ }, [selected]);
23346
+ if (files.length === 0) return loading ? (0, react_jsx_runtime.jsxs)("p", {
23347
+ className: detailStyles.muted,
23348
+ role: "status",
23349
+ "aria-live": "polite",
23350
+ children: [
23351
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
23352
+ state: "ongoing",
23353
+ size: 14
23354
+ }),
23355
+ " ",
23356
+ t("loading")
23357
+ ]
23358
+ }) : (0, react_jsx_runtime.jsx)("p", {
23359
+ className: detailStyles.muted,
23360
+ children: t("noFiles")
23361
+ });
23362
+ return (0, react_jsx_runtime.jsxs)("div", {
23363
+ className: detailStyles.files,
23364
+ children: [(0, react_jsx_runtime.jsx)("ul", {
23365
+ className: detailStyles.fileList,
23366
+ children: files.map((meta) => {
23367
+ const active = meta.path === selectedPath;
23368
+ return (0, react_jsx_runtime.jsx)("li", { children: (0, react_jsx_runtime.jsxs)("button", {
23369
+ type: "button",
23370
+ className: `${detailStyles.fileItem} ${active ? detailStyles.fileItemActive : ""}`,
23371
+ "aria-current": active ? "true" : void 0,
23372
+ onClick: () => onSelect(meta.path),
23373
+ children: [
23374
+ (0, react_jsx_runtime.jsx)(FileIcon, {
23375
+ size: 14,
23376
+ className: detailStyles.fileIcon
23377
+ }),
23378
+ (0, react_jsx_runtime.jsx)("span", {
23379
+ className: detailStyles.filePath,
23380
+ title: meta.path,
23381
+ children: meta.path
23382
+ }),
23383
+ meta.tooBig === true && (0, react_jsx_runtime.jsx)("span", {
23384
+ className: detailStyles.fileFlag,
23385
+ children: t("fileTooLarge")
23386
+ }),
23387
+ (0, react_jsx_runtime.jsxs)("span", {
23388
+ className: detailStyles.fileMeta,
23389
+ children: [
23390
+ (0, _deepseek_ai_dsh_client_ui_primitives.fileSizeText)(meta.size),
23391
+ " · ",
23392
+ meta.language
23393
+ ]
23394
+ })
23395
+ ]
23396
+ }) }, meta.path);
23397
+ })
23398
+ }), (0, react_jsx_runtime.jsx)("div", {
23399
+ className: detailStyles.fileView,
23400
+ children: error !== null ? (0, react_jsx_runtime.jsxs)("p", {
23401
+ className: detailStyles.muted,
23402
+ role: "alert",
23403
+ children: [
23404
+ (0, react_jsx_runtime.jsx)(AlertIcon, { size: 14 }),
23405
+ " ",
23406
+ error
23407
+ ]
23408
+ }) : selected === null ? loading ? (0, react_jsx_runtime.jsxs)("p", {
23409
+ className: detailStyles.muted,
23410
+ role: "status",
23411
+ "aria-live": "polite",
23412
+ children: [
23413
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
23414
+ state: "ongoing",
23415
+ size: 14
23416
+ }),
23417
+ " ",
23418
+ t("loading")
23419
+ ]
23420
+ }) : null : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
23421
+ (0, react_jsx_runtime.jsxs)("div", {
23422
+ className: detailStyles.fileBar,
23423
+ children: [
23424
+ (0, react_jsx_runtime.jsx)("span", {
23425
+ className: detailStyles.fileBarPath,
23426
+ title: selected.path,
23427
+ children: selected.path
23428
+ }),
23429
+ renderable && (0, react_jsx_runtime.jsxs)("div", {
23430
+ className: detailStyles.viewToggle,
23431
+ role: "group",
23432
+ "aria-label": t("fileView"),
23433
+ children: [(0, react_jsx_runtime.jsx)("button", {
23434
+ type: "button",
23435
+ "aria-pressed": mode === "preview",
23436
+ onClick: () => setMode("preview"),
23437
+ children: t("filePreview")
23438
+ }), (0, react_jsx_runtime.jsx)("button", {
23439
+ type: "button",
23440
+ "aria-pressed": mode === "source",
23441
+ onClick: () => setMode("source"),
23442
+ children: t("fileSource")
23443
+ })]
23444
+ }),
23445
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23446
+ variant: "outline",
23447
+ size: "sm",
23448
+ className: detailStyles.copy,
23449
+ "aria-label": copied ? t("copied") : t("copy"),
23450
+ onClick: copyFile,
23451
+ icon: copied ? (0, react_jsx_runtime.jsx)(CheckIcon, { size: 14 }) : (0, react_jsx_runtime.jsx)(CopyIcon, { size: 14 }),
23452
+ children: copied ? t("copied") : t("copy")
23453
+ })
23454
+ ]
23455
+ }),
23456
+ selected.truncated && (0, react_jsx_runtime.jsx)("p", {
23457
+ className: detailStyles.muted,
23458
+ children: t("fileTruncated")
23459
+ }),
23460
+ renderable && mode === "preview" ? (0, react_jsx_runtime.jsx)(MarkdownFilePreview, { content: selected.content }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
23461
+ code: selected.content,
23462
+ lang: selected.language,
23463
+ showHeader: false,
23464
+ lineNumbers: true,
23465
+ copyLabel: t("copy"),
23466
+ copiedLabel: t("copied")
23467
+ }),
23468
+ (0, react_jsx_runtime.jsx)("p", {
23469
+ className: detailStyles.srOnly,
23470
+ role: "status",
23471
+ "aria-live": "polite",
23472
+ children: copied ? t("copied") : ""
23473
+ })
23474
+ ] })
23475
+ })]
23476
+ });
23477
+ }
23478
+ //#endregion
23479
+ //#region \0dsh-css:src/client/components/InstalledSkillDetail.module.css.mjs
23480
+ const css$9 = "._4PqW5a_chip{box-sizing:border-box;background:var(--dsw-alias-bg-layer-2,#f0f2f5);height:20px;color:var(--dsw-alias-label-secondary,#61666b);white-space:nowrap;border-radius:999px;flex:none;align-items:center;padding:0 7px;font-size:11px;font-weight:600;line-height:18px;display:inline-flex}._4PqW5a_chipOn{background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 12%, transparent);color:var(--dsw-alias-state-business-primary,#4176e6)}._4PqW5a_pathRow{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-layer-1,#fff);align-items:center;gap:8px;min-width:0;padding:7px 10px;display:flex}._4PqW5a_pathIcon{color:var(--dsw-alias-label-tertiary,#81858c);flex:none}._4PqW5a_pathText{min-width:0;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:18px}._4PqW5a_pathCopy{flex:none}._4PqW5a_provenance{border-top:1px solid var(--dsw-alias-border-l1,#0000000a);color:var(--dsw-alias-label-tertiary,#81858c);margin:0;padding:12px 14px 14px;font-size:12px;line-height:18px}._4PqW5a_failure{flex-direction:column;align-items:flex-start;gap:8px;display:flex}._4PqW5a_failureTitle{margin:0;font-size:13.5px;font-weight:600}._4PqW5a_failureText{color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;margin:0;font-size:12.5px;line-height:19px}";
23481
+ const tagId$9 = "@nanmicoder/dsh-skills-hub/InstalledSkillDetail.module.css";
23482
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$9) + "]") === null) {
23483
+ const tag = document.createElement("style");
23484
+ tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
23485
+ tag.dataset.pluginCss = tagId$9;
23486
+ tag.textContent = css$9;
23487
+ document.head.appendChild(tag);
23488
+ }
23489
+ var InstalledSkillDetail_module_css_default = {
23490
+ "chip": "_4PqW5a_chip",
23491
+ "chipOn": "_4PqW5a_chipOn",
23492
+ "failure": "_4PqW5a_failure",
23493
+ "failureText": "_4PqW5a_failureText",
23494
+ "failureTitle": "_4PqW5a_failureTitle",
23495
+ "pathCopy": "_4PqW5a_pathCopy",
23496
+ "pathIcon": "_4PqW5a_pathIcon",
23497
+ "pathRow": "_4PqW5a_pathRow",
23498
+ "pathText": "_4PqW5a_pathText",
23499
+ "provenance": "_4PqW5a_provenance"
23500
+ };
23501
+ //#endregion
23502
+ //#region lib/client/components/InstalledSkillDetail.js
23503
+ /**
23504
+ * Installed skill detail page.
23505
+ *
23506
+ * The list's "view" affordance used to open a 380px confirmation-style modal,
23507
+ * which is the wrong container for a document: SKILL.md is a page of prose with
23508
+ * tables and code, and the marketplace already answers the same question with a
23509
+ * full-panel page. This page is that same reading surface (`SkillDetailShell`),
23510
+ * fed by the filesystem instead of a provider.
23511
+ *
23512
+ * What it deliberately does *not* do is pretend a local skill is a market
23513
+ * entry. There is no author, download count or security report to show, so
23514
+ * those rows are absent rather than zero. Where the two identities do meet —
23515
+ * a skill installed from a market, proven by its `.skills-hub.json` sidecar —
23516
+ * the page links to the market detail instead of duplicating it, which keeps
23517
+ * upstream data upstream and this page readable with the network down.
23518
+ */
23519
+ /** How long the path copy button stays in its confirmed state. */
23520
+ const COPIED_RESET_MS = 2e3;
23521
+ /** Reader-facing text for anything thrown by the client. */
23522
+ function messageOf$1(reason) {
23523
+ return reason instanceof Error ? reason.message : String(reason);
23524
+ }
23525
+ /** Local install timestamps are ISO strings; an unparseable one renders as nothing. */
23526
+ function formatInstalledAt(value) {
23527
+ const date = new Date(value);
23528
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString();
23529
+ }
23530
+ function InstalledSkillDetail(props) {
23531
+ const { item } = props;
23532
+ const t = useT();
23533
+ const [detail, setDetail] = (0, react.useState)(null);
23534
+ const [error, setError] = (0, react.useState)(null);
23535
+ const [revision, setRevision] = (0, react.useState)(0);
23536
+ const [tab, setTab] = (0, react.useState)("overview");
23537
+ const [selectedPath, setSelectedPath] = (0, react.useState)(null);
23538
+ const [file, setFile] = (0, react.useState)(null);
23539
+ const [fileLoading, setFileLoading] = (0, react.useState)(false);
23540
+ const [fileError, setFileError] = (0, react.useState)(null);
23541
+ const [copied, setCopied] = (0, react.useState)(false);
23542
+ const resetTimer = (0, react.useRef)(void 0);
23543
+ (0, react.useEffect)(() => {
23544
+ const abort = new AbortController();
23545
+ setError(null);
23546
+ fetchInstalledDetail(item.key, abort.signal).then((result) => {
23547
+ if (!abort.signal.aborted) setDetail(result);
23548
+ }).catch((reason) => {
23549
+ if (!abort.signal.aborted && !isAbortError(reason)) setError(messageOf$1(reason));
23550
+ });
23551
+ return () => abort.abort();
23552
+ }, [item.key, revision]);
23553
+ (0, react.useEffect)(() => {
23554
+ if (tab !== "files" || detail === null || selectedPath !== null) return;
23555
+ const preferred = detail.files.find((entry) => entry.path.toLowerCase() === "skill.md") ?? detail.files[0];
23556
+ if (preferred !== void 0) setSelectedPath(preferred.path);
23557
+ }, [
23558
+ tab,
23559
+ detail,
23560
+ selectedPath
23561
+ ]);
23562
+ (0, react.useEffect)(() => {
23563
+ if (selectedPath === null) return;
23564
+ const abort = new AbortController();
23565
+ setFileLoading(true);
23566
+ setFileError(null);
23567
+ fetchInstalledFile(item.key, selectedPath, abort.signal).then((result) => {
23568
+ if (!abort.signal.aborted) setFile(result);
23569
+ }).catch((reason) => {
23570
+ if (abort.signal.aborted || isAbortError(reason)) return;
23571
+ setFile(null);
23572
+ setFileError(messageOf$1(reason));
23573
+ }).finally(() => {
23574
+ if (!abort.signal.aborted) setFileLoading(false);
23575
+ });
23576
+ return () => abort.abort();
23577
+ }, [item.key, selectedPath]);
23578
+ (0, react.useEffect)(() => () => {
23579
+ if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
23580
+ }, []);
23581
+ const copyPath = () => {
23582
+ (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(item.dirPath).then((ok) => {
23583
+ if (!ok) return;
23584
+ setCopied(true);
23585
+ if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
23586
+ resetTimer.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);
23587
+ });
23588
+ };
23589
+ const marketId = item.managed && item.source !== "local" ? item.id : null;
23590
+ const body = detail === null ? "" : stripSkillFrontmatter(detail.markdown);
23591
+ const metadata = (0, react.useMemo)(() => detail?.frontmatter == null ? null : parseSkillFrontmatter(detail.frontmatter), [detail]);
23592
+ const baseName = item.dirPath.split(/[\\/]/).pop() ?? item.dirName;
23593
+ const flatSkill = detail !== null && detail.files.length === 1 && detail.files[0]?.path === baseName;
23594
+ const installedAt = item.installedAt === void 0 ? "" : formatInstalledAt(item.installedAt);
23595
+ return (0, react_jsx_runtime.jsx)(SkillDetailShell, {
23596
+ focusKey: item.key,
23597
+ onBack: props.onBack,
23598
+ backLabel: t("back"),
23599
+ avatar: (0, react_jsx_runtime.jsx)(SkillAvatar, {
23600
+ name: item.name,
23601
+ source: item.source,
23602
+ size: 84
23603
+ }),
23604
+ eyebrow: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
23605
+ className: detailStyles.source,
23606
+ children: t(`source.${item.source}`)
23607
+ }), item.version !== void 0 && item.version !== "" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
23608
+ "aria-hidden": "true",
23609
+ children: "·"
23610
+ }), (0, react_jsx_runtime.jsxs)("span", {
23611
+ className: detailStyles.version,
23612
+ children: ["v", item.version]
23613
+ })] })] }),
23614
+ name: item.name,
23615
+ badges: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
23616
+ className: `${InstalledSkillDetail_module_css_default.chip} ${item.managed ? InstalledSkillDetail_module_css_default.chipOn : ""}`,
23617
+ children: item.managed ? t("managedByHub") : t("unmanagedSkill")
23618
+ }), item.linked && (0, react_jsx_runtime.jsx)("span", {
23619
+ className: InstalledSkillDetail_module_css_default.chip,
23620
+ children: t("symlinkSkill")
23621
+ })] }),
23622
+ summary: item.summary,
23623
+ notice: (0, react_jsx_runtime.jsxs)("div", {
23624
+ className: InstalledSkillDetail_module_css_default.pathRow,
23625
+ children: [
23626
+ (0, react_jsx_runtime.jsx)(FolderIcon, {
23627
+ size: 14,
23628
+ className: InstalledSkillDetail_module_css_default.pathIcon
23629
+ }),
23630
+ (0, react_jsx_runtime.jsx)("span", {
23631
+ className: InstalledSkillDetail_module_css_default.pathText,
23632
+ title: item.dirPath,
23633
+ children: item.dirPath
23634
+ }),
23635
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23636
+ variant: "ghost",
23637
+ size: "sm",
23638
+ className: InstalledSkillDetail_module_css_default.pathCopy,
23639
+ "aria-label": copied ? t("copiedPath") : t("copyPath"),
23640
+ onClick: copyPath,
23641
+ icon: copied ? (0, react_jsx_runtime.jsx)(CheckIcon, { size: 14 }) : (0, react_jsx_runtime.jsx)(CopyIcon, { size: 14 }),
23642
+ children: copied ? t("copiedPath") : t("copyPath")
23643
+ })
23644
+ ]
23645
+ }),
23646
+ overviewLabel: t("overview"),
23647
+ filesLabel: `${t("files")} (${detail?.files.length ?? item.fileCount})`,
23648
+ activeTab: tab,
23649
+ onTabChange: setTab,
23650
+ overview: error !== null ? (0, react_jsx_runtime.jsxs)("div", {
23651
+ className: InstalledSkillDetail_module_css_default.failure,
23652
+ role: "alert",
23653
+ children: [
23654
+ (0, react_jsx_runtime.jsx)("p", {
23655
+ className: InstalledSkillDetail_module_css_default.failureTitle,
23656
+ children: t("installedReadFailed")
23657
+ }),
23658
+ (0, react_jsx_runtime.jsx)("p", {
23659
+ className: InstalledSkillDetail_module_css_default.failureText,
23660
+ children: error
23661
+ }),
23662
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23663
+ variant: "outline",
23664
+ size: "sm",
23665
+ onClick: () => setRevision((value) => value + 1),
23666
+ children: t("retry")
23667
+ })
23668
+ ]
23669
+ }) : detail === null ? (0, react_jsx_runtime.jsxs)("p", {
23670
+ className: detailStyles.muted,
23671
+ role: "status",
23672
+ "aria-live": "polite",
23673
+ children: [
23674
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
23675
+ state: "ongoing",
23676
+ size: 14
23677
+ }),
23678
+ " ",
23679
+ t("loading")
23680
+ ]
23681
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [metadata !== null && (0, react_jsx_runtime.jsx)("div", {
23682
+ className: detailStyles.frontmatter,
23683
+ children: (0, react_jsx_runtime.jsx)(FrontmatterPanel, { data: metadata })
23684
+ }), body.trim() === "" ? (0, react_jsx_runtime.jsx)("p", {
23685
+ className: detailStyles.muted,
23686
+ children: t("emptyDocument")
23687
+ }) : (0, react_jsx_runtime.jsx)(MarkdownView, { content: body })] }),
23688
+ files: error !== null ? (0, react_jsx_runtime.jsx)("p", {
23689
+ className: detailStyles.muted,
23690
+ role: "alert",
23691
+ children: t("installedReadFailed")
23692
+ }) : (0, react_jsx_runtime.jsx)(SkillFiles, {
23693
+ files: detail?.files ?? [],
23694
+ selected: file,
23695
+ loading: fileLoading || detail === null,
23696
+ error: fileError,
23697
+ onSelect: (path) => {
23698
+ setFile(null);
23699
+ setSelectedPath(path);
23700
+ }
23701
+ }),
23702
+ rail: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
23703
+ (0, react_jsx_runtime.jsxs)("div", {
23704
+ className: detailStyles.railAction,
23705
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23706
+ variant: "outline",
23707
+ size: "md",
23708
+ disabled: !item.removable,
23709
+ title: item.removable ? t("uninstallConfirm") : t("uninstallDisabled"),
23710
+ icon: (0, react_jsx_runtime.jsx)(TrashIcon, { size: 16 }),
23711
+ onClick: () => props.onUninstall(item),
23712
+ children: t("uninstall")
23713
+ }), marketId !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23714
+ variant: "ghost",
23715
+ size: "md",
23716
+ icon: (0, react_jsx_runtime.jsx)(ExternalLinkIcon, { size: 16 }),
23717
+ onClick: () => props.onOpenMarket(marketId),
23718
+ children: t("openInMarket")
23719
+ })]
23720
+ }),
23721
+ (0, react_jsx_runtime.jsxs)("dl", {
23722
+ className: detailStyles.facts,
23723
+ children: [
23724
+ (0, react_jsx_runtime.jsx)(Fact, {
23725
+ label: t("sourceLabel"),
23726
+ children: t(`source.${item.source}`)
23727
+ }),
23728
+ item.version !== void 0 && item.version !== "" && (0, react_jsx_runtime.jsxs)(Fact, {
23729
+ label: t("version"),
23730
+ children: ["v", item.version]
23731
+ }),
23732
+ installedAt !== "" && (0, react_jsx_runtime.jsx)(Fact, {
23733
+ label: t("installedAtLabel"),
23734
+ children: installedAt
23735
+ }),
23736
+ detail !== null && (0, react_jsx_runtime.jsx)(Fact, {
23737
+ label: t("skillKind"),
23738
+ children: flatSkill ? t("flatSkill") : t("bundleSkill")
23739
+ }),
23740
+ (0, react_jsx_runtime.jsxs)(Fact, {
23741
+ label: t("files"),
23742
+ children: [
23743
+ t("filesCount", { count: item.fileCount }),
23744
+ " · ",
23745
+ (0, _deepseek_ai_dsh_client_ui_primitives.fileSizeText)(item.bytes)
23746
+ ]
23747
+ })
23748
+ ]
23749
+ }),
23750
+ marketId !== null && (0, react_jsx_runtime.jsx)("p", {
23751
+ className: InstalledSkillDetail_module_css_default.provenance,
23752
+ children: t("marketProvenance")
23753
+ })
23754
+ ] })
23755
+ });
23756
+ }
23757
+ //#endregion
23758
+ //#region \0dsh-css:src/client/components/InstalledSkills.module.css.mjs
23759
+ const css$8 = ".eYwU2a_root{box-sizing:border-box;width:100%;max-width:1280px;margin:0 auto;padding:28px}.eYwU2a_header{justify-content:space-between;align-items:flex-start;gap:20px;display:flex}.eYwU2a_header h1{margin:0;font-size:24px}.eYwU2a_header p{max-width:760px;color:var(--dsw-alias-label-secondary,#61666b);margin:8px 0 0;font-size:13px;line-height:1.6}.eYwU2a_search{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-base,#fff);width:100%;color:inherit;font:inherit;margin-top:20px;padding:10px 12px}.eYwU2a_search:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px}.eYwU2a_list{gap:12px;display:grid}.eYwU2a_card{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-md,12px);justify-content:space-between;align-items:center;gap:20px;padding:18px;display:flex}.eYwU2a_info{min-width:0}.eYwU2a_name{color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;margin:0 10px 0 0;padding:0;font-weight:600}.eYwU2a_name:hover{text-decoration:underline}.eYwU2a_name:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:2px;border-radius:var(--dsw-radius-xs,4px)}.eYwU2a_badge{border-radius:var(--dsw-radius-xs,4px);background:var(--dsw-alias-bg-layer-2,#f0f2f5);margin:2px 6px 2px 0;padding:2px 6px;font-size:11px;display:inline-block}.eYwU2a_version,.eYwU2a_count{color:var(--dsw-alias-label-secondary,#61666b);font-size:12px}.eYwU2a_count{margin:16px 0 12px}.eYwU2a_summary{margin:8px 0;font-size:13px;line-height:1.6}.eYwU2a_path{color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:1.7}.eYwU2a_actions{flex-shrink:0;gap:8px;display:flex}.eYwU2a_empty{color:var(--dsw-alias-label-secondary,#61666b);text-align:center;padding:50px 10px}.eYwU2a_error{color:var(--dsw-alias-state-error-primary,#ec1313)}.eYwU2a_skeleton{border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-2,#f0f2f5);height:108px;margin-top:10px;animation:1.5s ease-in-out infinite eYwU2a_pulse}@keyframes eYwU2a_pulse{50%{opacity:.45}}@media (prefers-reduced-motion:reduce){.eYwU2a_skeleton{animation:none}}@media (width<=640px){.eYwU2a_root{padding:18px}.eYwU2a_card{flex-direction:column;align-items:stretch}.eYwU2a_actions{justify-content:flex-end}}";
23760
+ const tagId$8 = "@nanmicoder/dsh-skills-hub/InstalledSkills.module.css";
23761
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$8) + "]") === null) {
23762
+ const tag = document.createElement("style");
23763
+ tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
23764
+ tag.dataset.pluginCss = tagId$8;
23765
+ tag.textContent = css$8;
23766
+ document.head.appendChild(tag);
23767
+ }
23768
+ var InstalledSkills_module_css_default = {
23769
+ "actions": "eYwU2a_actions",
23770
+ "badge": "eYwU2a_badge",
23771
+ "card": "eYwU2a_card",
23772
+ "count": "eYwU2a_count",
23773
+ "empty": "eYwU2a_empty",
23774
+ "error": "eYwU2a_error",
23775
+ "header": "eYwU2a_header",
23776
+ "info": "eYwU2a_info",
23777
+ "list": "eYwU2a_list",
23778
+ "name": "eYwU2a_name",
23779
+ "path": "eYwU2a_path",
23780
+ "pulse": "eYwU2a_pulse",
23781
+ "root": "eYwU2a_root",
23782
+ "search": "eYwU2a_search",
23783
+ "skeleton": "eYwU2a_skeleton",
23784
+ "summary": "eYwU2a_summary",
23785
+ "version": "eYwU2a_version"
23786
+ };
23787
+ //#endregion
22600
23788
  //#region lib/client/components/InstalledSkills.js
22601
- function InstalledSkills({ onChanged }) {
23789
+ /**
23790
+ * Installed skills: the local inventory and its reading page.
23791
+ *
23792
+ * Two views, one component. The detail page replaces the list *inside* this
23793
+ * component rather than in `MarketPage`, so the list is never unmounted: the
23794
+ * search query, the loaded rows and the scroll position are all still there when
23795
+ * the reader comes back. That is also why the selected row is stored as a key
23796
+ * and resolved against the current rows — a refresh that drops the entry closes
23797
+ * the page instead of leaving it pointing at something that no longer exists.
23798
+ *
23799
+ * Removal stays a confirmation modal on purpose: it is a short yes/no about a
23800
+ * destructive action, which is exactly what the primitive's dialog is for. The
23801
+ * skill's *content* is not, which is why it moved to a page.
23802
+ */
23803
+ /** Reader-facing text for anything thrown by the client. */
23804
+ function messageOf(reason) {
23805
+ return reason instanceof Error ? reason.message : String(reason);
23806
+ }
23807
+ function InstalledSkills(props) {
22602
23808
  const t = useT();
22603
23809
  const [items, setItems] = (0, react.useState)([]);
22604
23810
  const [loading, setLoading] = (0, react.useState)(true);
22605
23811
  const [error, setError] = (0, react.useState)(null);
22606
23812
  const [query, setQuery] = (0, react.useState)("");
22607
23813
  const [revision, setRevision] = (0, react.useState)(0);
22608
- const [selected, setSelected] = (0, react.useState)(null);
22609
- const [markdown, setMarkdown] = (0, react.useState)(null);
22610
- const [detailError, setDetailError] = (0, react.useState)(null);
23814
+ const [selectedKey, setSelectedKey] = (0, react.useState)(null);
22611
23815
  const [pending, setPending] = (0, react.useState)(null);
22612
23816
  const [busy, setBusy] = (0, react.useState)(false);
23817
+ const [returnFocusKey, setReturnFocusKey] = (0, react.useState)(null);
23818
+ /** Row buttons by skill key, so the detail page can hand focus back. */
23819
+ const rowButtons = (0, react.useRef)(/* @__PURE__ */ new Map());
22613
23820
  (0, react.useEffect)(() => {
22614
23821
  const abort = new AbortController();
22615
23822
  setLoading(true);
@@ -22617,44 +23824,46 @@ function(property) {
22617
23824
  fetchInstalled(abort.signal).then((result) => {
22618
23825
  if (!abort.signal.aborted) setItems(result);
22619
23826
  }).catch((reason) => {
22620
- if (!abort.signal.aborted && !isAbortError(reason)) setError(String(reason.message ?? reason));
23827
+ if (!abort.signal.aborted && !isAbortError(reason)) setError(messageOf(reason));
22621
23828
  }).finally(() => {
22622
23829
  if (!abort.signal.aborted) setLoading(false);
22623
23830
  });
22624
23831
  return () => abort.abort();
22625
23832
  }, [revision]);
22626
- (0, react.useEffect)(() => {
22627
- setMarkdown(null);
22628
- setDetailError(null);
22629
- if (!selected) return;
22630
- const abort = new AbortController();
22631
- fetchInstalledDetail(selected.key, abort.signal).then((result) => {
22632
- if (!abort.signal.aborted) setMarkdown(stripSkillFrontmatter(result.markdown));
22633
- }).catch((reason) => {
22634
- if (!abort.signal.aborted && !isAbortError(reason)) setDetailError(String(reason.message ?? reason));
22635
- });
22636
- return () => abort.abort();
22637
- }, [selected]);
22638
- async function uninstall() {
22639
- if (!pending || busy) return;
23833
+ const uninstall = async (target) => {
23834
+ if (busy) return;
22640
23835
  setBusy(true);
22641
23836
  setError(null);
22642
23837
  try {
22643
- await removeInstalled(pending.key);
22644
- setItems((current) => current.filter((item) => item.key !== pending.key));
22645
- if (selected?.key === pending.key) setSelected(null);
23838
+ await removeInstalled(target.key);
23839
+ setItems((current) => current.filter((item) => item.key !== target.key));
23840
+ setSelectedKey((current) => current === target.key ? null : current);
22646
23841
  setPending(null);
22647
- onChanged();
23842
+ props.onChanged();
22648
23843
  } catch (reason) {
22649
- setError(reason instanceof Error ? reason.message : String(reason));
23844
+ setError(messageOf(reason));
22650
23845
  setPending(null);
22651
23846
  } finally {
22652
23847
  setBusy(false);
22653
23848
  }
22654
- }
23849
+ };
23850
+ const selected = selectedKey === null ? null : items.find((item) => item.key === selectedKey) ?? null;
23851
+ (0, react.useEffect)(() => {
23852
+ if (returnFocusKey === null) return;
23853
+ rowButtons.current.get(returnFocusKey)?.focus();
23854
+ setReturnFocusKey(null);
23855
+ }, [returnFocusKey]);
22655
23856
  const needle = query.trim().toLocaleLowerCase();
22656
23857
  const visible = items.filter((item) => `${item.name} ${item.summary ?? ""} ${item.dirPath}`.toLocaleLowerCase().includes(needle));
22657
- return (0, react_jsx_runtime.jsxs)("section", {
23858
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [selected !== null ? (0, react_jsx_runtime.jsx)(InstalledSkillDetail, {
23859
+ item: selected,
23860
+ onBack: () => {
23861
+ setReturnFocusKey(selected.key);
23862
+ setSelectedKey(null);
23863
+ },
23864
+ onUninstall: setPending,
23865
+ onOpenMarket: props.onOpenMarket
23866
+ }, selected.key) : (0, react_jsx_runtime.jsxs)("section", {
22658
23867
  className: InstalledSkills_module_css_default.root,
22659
23868
  children: [
22660
23869
  (0, react_jsx_runtime.jsxs)("header", {
@@ -22675,7 +23884,7 @@ function(property) {
22675
23884
  value: query,
22676
23885
  onChange: (event) => setQuery(event.target.value)
22677
23886
  }),
22678
- error && (0, react_jsx_runtime.jsx)("p", {
23887
+ error !== null && (0, react_jsx_runtime.jsx)("p", {
22679
23888
  role: "alert",
22680
23889
  className: InstalledSkills_module_css_default.error,
22681
23890
  children: error
@@ -22700,7 +23909,11 @@ function(property) {
22700
23909
  children: [
22701
23910
  (0, react_jsx_runtime.jsx)("button", {
22702
23911
  className: InstalledSkills_module_css_default.name,
22703
- onClick: () => setSelected(item),
23912
+ ref: (node) => {
23913
+ if (node === null) rowButtons.current.delete(item.key);
23914
+ else rowButtons.current.set(item.key, node);
23915
+ },
23916
+ onClick: () => setSelectedKey(item.key),
22704
23917
  children: item.name
22705
23918
  }),
22706
23919
  (0, react_jsx_runtime.jsx)("span", {
@@ -22711,11 +23924,11 @@ function(property) {
22711
23924
  className: InstalledSkills_module_css_default.badge,
22712
23925
  children: t("linkedSkill")
22713
23926
  }),
22714
- item.version && (0, react_jsx_runtime.jsxs)("span", {
23927
+ item.version !== void 0 && item.version !== "" && (0, react_jsx_runtime.jsxs)("span", {
22715
23928
  className: InstalledSkills_module_css_default.version,
22716
23929
  children: ["v", item.version]
22717
23930
  }),
22718
- item.summary && (0, react_jsx_runtime.jsx)("p", {
23931
+ item.summary !== void 0 && item.summary !== "" && (0, react_jsx_runtime.jsx)("p", {
22719
23932
  className: InstalledSkills_module_css_default.summary,
22720
23933
  children: item.summary
22721
23934
  }),
@@ -22729,79 +23942,59 @@ function(property) {
22729
23942
  children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
22730
23943
  variant: "outline",
22731
23944
  size: "sm",
22732
- onClick: () => setSelected(item),
23945
+ onClick: () => setSelectedKey(item.key),
22733
23946
  children: t("viewInstalled")
22734
23947
  }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
22735
23948
  variant: "outline",
22736
23949
  size: "sm",
22737
23950
  disabled: !item.removable,
23951
+ title: item.removable ? void 0 : t("uninstallDisabled"),
22738
23952
  onClick: () => setPending(item),
22739
23953
  children: t("uninstall")
22740
23954
  })]
22741
23955
  })]
22742
23956
  }, item.key))
22743
- })] }),
22744
- selected && (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
22745
- open: true,
22746
- title: selected.name,
22747
- closeLabel: t("close"),
22748
- onClose: () => setSelected(null),
22749
- children: [(0, react_jsx_runtime.jsx)("p", {
22750
- className: InstalledSkills_module_css_default.path,
22751
- children: selected.dirPath
22752
- }), (0, react_jsx_runtime.jsx)("div", {
22753
- className: InstalledSkills_module_css_default.preview,
22754
- children: detailError ? (0, react_jsx_runtime.jsx)("p", {
22755
- role: "alert",
22756
- children: detailError
22757
- }) : markdown === null ? (0, react_jsx_runtime.jsxs)("div", {
22758
- role: "status",
22759
- "aria-label": t("loading"),
22760
- children: [(0, react_jsx_runtime.jsx)("div", { className: InstalledSkills_module_css_default.skeleton }), (0, react_jsx_runtime.jsx)("div", { className: InstalledSkills_module_css_default.skeleton })]
22761
- }) : (0, react_jsx_runtime.jsx)(MarkdownView, { content: markdown })
22762
- })]
22763
- }),
22764
- pending && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
22765
- open: true,
22766
- title: t("uninstallTitle", { name: pending.name }),
22767
- closeLabel: t("close"),
22768
- description: t(pending.linked ? "unlinkDescription" : "localUninstallDescription"),
22769
- onClose: () => {
22770
- if (!busy) setPending(null);
22771
- },
22772
- footer: (0, react_jsx_runtime.jsxs)("div", {
22773
- className: InstalledSkills_module_css_default.actions,
22774
- children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
22775
- variant: "outline",
22776
- size: "md",
22777
- disabled: busy,
22778
- onClick: () => setPending(null),
22779
- children: t("cancel")
22780
- }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
22781
- variant: "primary",
22782
- size: "md",
22783
- disabled: busy,
22784
- onClick: () => void uninstall(),
22785
- children: t(busy ? "uninstalling" : "uninstallConfirm")
22786
- })]
22787
- }),
22788
- children: (0, react_jsx_runtime.jsx)("p", {
22789
- className: InstalledSkills_module_css_default.path,
22790
- children: pending.dirPath
22791
- })
22792
- })
23957
+ })] })
22793
23958
  ]
22794
- });
23959
+ }), pending !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
23960
+ open: true,
23961
+ title: t("uninstallTitle", { name: pending.name }),
23962
+ closeLabel: t("close"),
23963
+ description: t(pending.linked ? "unlinkDescription" : "localUninstallDescription"),
23964
+ onClose: () => {
23965
+ if (!busy) setPending(null);
23966
+ },
23967
+ footer: (0, react_jsx_runtime.jsxs)("div", {
23968
+ className: InstalledSkills_module_css_default.actions,
23969
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23970
+ variant: "outline",
23971
+ size: "md",
23972
+ disabled: busy,
23973
+ onClick: () => setPending(null),
23974
+ children: t("cancel")
23975
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23976
+ variant: "primary",
23977
+ size: "md",
23978
+ disabled: busy,
23979
+ onClick: () => void uninstall(pending),
23980
+ children: t(busy ? "uninstalling" : "uninstallConfirm")
23981
+ })]
23982
+ }),
23983
+ children: (0, react_jsx_runtime.jsx)("p", {
23984
+ className: InstalledSkills_module_css_default.path,
23985
+ children: pending.dirPath
23986
+ })
23987
+ })] });
22795
23988
  }
22796
23989
  //#endregion
22797
23990
  //#region \0dsh-css:src/client/components/FilterBar.module.css.mjs
22798
- const css$8 = ".iX7c8W_bar{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:flex}.iX7c8W_search{flex:220px;align-items:center;gap:6px;min-width:0;display:flex}.iX7c8W_searchInput{flex:auto;min-width:0}.iX7c8W_clear{flex:none}.iX7c8W_field{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-layer-1,#fff);cursor:pointer;flex:none;align-items:center;gap:6px;height:32px;padding:0 6px 0 10px;display:inline-flex}.iX7c8W_field:focus-within{border-color:var(--dsw-alias-state-business-primary,#4176e6)}.iX7c8W_fieldLabel{color:var(--dsw-alias-label-tertiary,#81858c);white-space:nowrap;font-size:12px;line-height:18px}.iX7c8W_select{max-width:148px;color:var(--dsw-alias-label-primary,#0f1115);font:inherit;cursor:pointer;text-overflow:ellipsis;background:0 0;border:none;outline:none;font-size:12.5px;font-weight:600;line-height:18px}.iX7c8W_select:disabled{color:var(--dsw-alias-label-tertiary,#81858c);cursor:not-allowed}.iX7c8W_select option{background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#0f1115)}.iX7c8W_srOnly{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}";
22799
- const tagId$8 = "@nanmicoder/dsh-skills-hub/FilterBar.module.css";
22800
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$8) + "]") === null) {
23991
+ const css$7 = ".iX7c8W_bar{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:flex}.iX7c8W_search{flex:220px;align-items:center;gap:6px;min-width:0;display:flex}.iX7c8W_searchInput{flex:auto;min-width:0}.iX7c8W_clear{flex:none}.iX7c8W_field{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-layer-1,#fff);cursor:pointer;flex:none;align-items:center;gap:6px;height:32px;padding:0 6px 0 10px;display:inline-flex}.iX7c8W_field:focus-within{border-color:var(--dsw-alias-state-business-primary,#4176e6)}.iX7c8W_fieldLabel{color:var(--dsw-alias-label-tertiary,#81858c);white-space:nowrap;font-size:12px;line-height:18px}.iX7c8W_select{max-width:148px;color:var(--dsw-alias-label-primary,#0f1115);font:inherit;cursor:pointer;text-overflow:ellipsis;background:0 0;border:none;outline:none;font-size:12.5px;font-weight:600;line-height:18px}.iX7c8W_select:disabled{color:var(--dsw-alias-label-tertiary,#81858c);cursor:not-allowed}.iX7c8W_select option{background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#0f1115)}.iX7c8W_srOnly{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}";
23992
+ const tagId$7 = "@nanmicoder/dsh-skills-hub/FilterBar.module.css";
23993
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$7) + "]") === null) {
22801
23994
  const tag = document.createElement("style");
22802
23995
  tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
22803
- tag.dataset.pluginCss = tagId$8;
22804
- tag.textContent = css$8;
23996
+ tag.dataset.pluginCss = tagId$7;
23997
+ tag.textContent = css$7;
22805
23998
  document.head.appendChild(tag);
22806
23999
  }
22807
24000
  var FilterBar_module_css_default = {
@@ -22928,13 +24121,13 @@ function(property) {
22928
24121
  }
22929
24122
  //#endregion
22930
24123
  //#region \0dsh-css:src/client/components/MarketDisclaimer.module.css.mjs
22931
- const css$7 = ".e2Evdq_banner{box-sizing:border-box;border:1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 38%, transparent);border-radius:var(--dsw-radius-sm,8px);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 10%, transparent);align-items:flex-start;gap:10px;padding:10px 12px;display:flex}.e2Evdq_icon{color:var(--dsw-alias-state-warn-primary,#f59e0b);flex:none;margin-top:1px}.e2Evdq_text{overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-secondary,#61666b);flex:1;margin:0;font-size:12.5px;line-height:1.65}.e2Evdq_dismiss{flex:none;margin:-4px -6px 0 0}";
22932
- const tagId$7 = "@nanmicoder/dsh-skills-hub/MarketDisclaimer.module.css";
22933
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$7) + "]") === null) {
24124
+ const css$6 = ".e2Evdq_banner{box-sizing:border-box;border:1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 38%, transparent);border-radius:var(--dsw-radius-sm,8px);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 10%, transparent);align-items:flex-start;gap:10px;padding:10px 12px;display:flex}.e2Evdq_icon{color:var(--dsw-alias-state-warn-primary,#f59e0b);flex:none;margin-top:1px}.e2Evdq_text{overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-secondary,#61666b);flex:1;margin:0;font-size:12.5px;line-height:1.65}.e2Evdq_dismiss{flex:none;margin:-4px -6px 0 0}";
24125
+ const tagId$6 = "@nanmicoder/dsh-skills-hub/MarketDisclaimer.module.css";
24126
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
22934
24127
  const tag = document.createElement("style");
22935
24128
  tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
22936
- tag.dataset.pluginCss = tagId$7;
22937
- tag.textContent = css$7;
24129
+ tag.dataset.pluginCss = tagId$6;
24130
+ tag.textContent = css$6;
22938
24131
  document.head.appendChild(tag);
22939
24132
  }
22940
24133
  var MarketDisclaimer_module_css_default = {
@@ -22980,13 +24173,13 @@ function(property) {
22980
24173
  }
22981
24174
  //#endregion
22982
24175
  //#region \0dsh-css:src/client/components/InstallStateBadge.module.css.mjs
22983
- const css$6 = ".WSc5Pa_badge{box-sizing:border-box;white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;height:20px;padding:0 7px;font-size:11px;font-weight:600;line-height:18px;display:inline-flex}.WSc5Pa_success{color:var(--dsw-alias-state-success-primary,#22c55e);background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 12%, transparent)}.WSc5Pa_brand{color:var(--dsw-alias-state-business-primary,#4176e6);background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 12%, transparent)}.WSc5Pa_danger{color:var(--dsw-alias-state-error-primary,#ec1313);background:color-mix(in srgb, var(--dsw-alias-state-error-primary,#ec1313) 12%, transparent)}";
22984
- const tagId$6 = "@nanmicoder/dsh-skills-hub/InstallStateBadge.module.css";
22985
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
24176
+ const css$5 = ".WSc5Pa_badge{box-sizing:border-box;white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;height:20px;padding:0 7px;font-size:11px;font-weight:600;line-height:18px;display:inline-flex}.WSc5Pa_success{color:var(--dsw-alias-state-success-primary,#22c55e);background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 12%, transparent)}.WSc5Pa_brand{color:var(--dsw-alias-state-business-primary,#4176e6);background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 12%, transparent)}.WSc5Pa_danger{color:var(--dsw-alias-state-error-primary,#ec1313);background:color-mix(in srgb, var(--dsw-alias-state-error-primary,#ec1313) 12%, transparent)}";
24177
+ const tagId$5 = "@nanmicoder/dsh-skills-hub/InstallStateBadge.module.css";
24178
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
22986
24179
  const tag = document.createElement("style");
22987
24180
  tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
22988
- tag.dataset.pluginCss = tagId$6;
22989
- tag.textContent = css$6;
24181
+ tag.dataset.pluginCss = tagId$5;
24182
+ tag.textContent = css$5;
22990
24183
  document.head.appendChild(tag);
22991
24184
  }
22992
24185
  var InstallStateBadge_module_css_default = {
@@ -23021,113 +24214,6 @@ function(property) {
23021
24214
  });
23022
24215
  }
23023
24216
  //#endregion
23024
- //#region \0dsh-css:src/client/components/SkillAvatar.module.css.mjs
23025
- const css$5 = ".C6tuwq_image{object-fit:cover;border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-module-platform,#f5f6f7);flex:none}.C6tuwq_letter{box-sizing:border-box;letter-spacing:-.02em;color:var(--dsw-alias-label-primary-inverted,#fff);user-select:none;box-shadow:inset 0 1px 0 color-mix(in srgb, var(--dsw-alias-label-primary-inverted,#fff) 12%, transparent);flex:none;justify-content:center;align-items:center;font-weight:700;line-height:1;display:inline-flex;overflow:hidden}.C6tuwq_tone1{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 74%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 48%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone2{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 80%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 54%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone5{background:linear-gradient(145deg, color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 55%, var(--dsw-alias-state-success-primary,#22c55e)) 78%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 55%, var(--dsw-alias-state-success-primary,#22c55e)) 50%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone0{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 82%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 56%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone4{background:linear-gradient(145deg, color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 58%, var(--dsw-alias-state-success-primary,#22c55e)) 80%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 58%, var(--dsw-alias-state-success-primary,#22c55e)) 52%, var(--dsw-alias-label-primary,#0f1115)))}.C6tuwq_tone3{background:linear-gradient(145deg, color-mix(in srgb, var(--dsw-alias-state-idle-primary,#d4d4d4) 62%, var(--dsw-alias-label-primary,#0f1115)), color-mix(in srgb, var(--dsw-alias-state-idle-primary,#d4d4d4) 38%, var(--dsw-alias-label-primary,#0f1115)))}";
23026
- const tagId$5 = "@nanmicoder/dsh-skills-hub/SkillAvatar.module.css";
23027
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
23028
- const tag = document.createElement("style");
23029
- tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
23030
- tag.dataset.pluginCss = tagId$5;
23031
- tag.textContent = css$5;
23032
- document.head.appendChild(tag);
23033
- }
23034
- var SkillAvatar_module_css_default = {
23035
- "image": "C6tuwq_image",
23036
- "letter": "C6tuwq_letter",
23037
- "tone0": "C6tuwq_tone0",
23038
- "tone1": "C6tuwq_tone1",
23039
- "tone2": "C6tuwq_tone2",
23040
- "tone3": "C6tuwq_tone3",
23041
- "tone4": "C6tuwq_tone4",
23042
- "tone5": "C6tuwq_tone5"
23043
- };
23044
- //#endregion
23045
- //#region lib/client/components/SkillAvatar.js
23046
- /** Palette slots; each maps to one `.toneN` class in the module stylesheet. */
23047
- const TONE_COUNT = 6;
23048
- /**
23049
- * Per-source tone family. The `source` prop is otherwise unused by a letter
23050
- * avatar, so it earns its place as the family selector: ClawHub entries read
23051
- * warm, SkillHub entries read cool, and a mixed grid still shows at a glance
23052
- * where a card came from.
23053
- */
23054
- const SOURCE_TONES = {
23055
- clawhub: [
23056
- 2,
23057
- 5,
23058
- 1
23059
- ],
23060
- skillhub: [
23061
- 0,
23062
- 4,
23063
- 3
23064
- ]
23065
- };
23066
- /** Class name per tone slot, resolved once so the render path stays trivial. */
23067
- const TONE_CLASSES = [
23068
- SkillAvatar_module_css_default["tone0"] ?? "",
23069
- SkillAvatar_module_css_default["tone1"] ?? "",
23070
- SkillAvatar_module_css_default["tone2"] ?? "",
23071
- SkillAvatar_module_css_default["tone3"] ?? "",
23072
- SkillAvatar_module_css_default["tone4"] ?? "",
23073
- SkillAvatar_module_css_default["tone5"] ?? ""
23074
- ];
23075
- /**
23076
- * Deterministic palette index: the same name always keeps the same identity
23077
- * color across sessions, pages and re-renders.
23078
- */
23079
- function hashIndex(input) {
23080
- let hash = 0;
23081
- for (let i = 0; i < input.length; i++) hash = hash * 31 + input.charCodeAt(i) | 0;
23082
- return Math.abs(hash) % TONE_COUNT;
23083
- }
23084
- /**
23085
- * First visible character, uppercased. `Array.from` rather than `name[0]` so a
23086
- * CJK or astral-plane initial is not cut in half by UTF-16 indexing.
23087
- */
23088
- function initialOf(name) {
23089
- const first = Array.from(name.trim())[0];
23090
- return first ? first.toUpperCase() : "?";
23091
- }
23092
- /**
23093
- * Skill icon tile.
23094
- *
23095
- * An `<img>` is rendered only for an `https:` URL: the string arrives from a
23096
- * third-party catalogue, and allowing `javascript:` or `data:` here would turn
23097
- * an upstream payload into script execution inside the DSH page.
23098
- */
23099
- function SkillAvatar(props) {
23100
- const { name, source, iconUrl, size } = props;
23101
- const radius = Math.round(size * .24);
23102
- if (iconUrl !== void 0 && iconUrl.startsWith("https://")) return (0, react_jsx_runtime.jsx)("img", {
23103
- src: iconUrl,
23104
- alt: "",
23105
- width: size,
23106
- height: size,
23107
- loading: "lazy",
23108
- decoding: "async",
23109
- className: SkillAvatar_module_css_default.image,
23110
- style: {
23111
- width: size,
23112
- height: size,
23113
- borderRadius: radius
23114
- }
23115
- });
23116
- const family = SOURCE_TONES[source];
23117
- const tone = family[hashIndex(name) % family.length] ?? 0;
23118
- return (0, react_jsx_runtime.jsx)("span", {
23119
- "aria-hidden": "true",
23120
- className: `${SkillAvatar_module_css_default.letter} ${TONE_CLASSES[tone] ?? ""}`,
23121
- style: {
23122
- width: size,
23123
- height: size,
23124
- borderRadius: radius,
23125
- fontSize: Math.round(size * .4)
23126
- },
23127
- children: initialOf(name)
23128
- });
23129
- }
23130
- //#endregion
23131
24217
  //#region \0dsh-css:src/client/components/SkillCard.module.css.mjs
23132
24218
  const css$4 = ".cii49a_card{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);flex-direction:column;gap:10px;min-width:0;min-height:188px;padding:14px;transition:border-color .14s,background-color .14s;display:flex;position:relative}.cii49a_card:hover{border-color:var(--dsw-alias-border-l3,#00000029);background:var(--dsw-alias-interactive-bg-hover,#2631480f)}.cii49a_card:focus-within{border-color:var(--dsw-alias-state-business-primary,#4176e6)}.cii49a_open{z-index:0;border-radius:inherit;cursor:pointer;background:0 0;border:none;margin:0;padding:0;position:absolute;inset:0}.cii49a_open:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:2px}.cii49a_head,.cii49a_summary,.cii49a_tags,.cii49a_footer{z-index:1;pointer-events:none;position:relative}.cii49a_head{align-items:center;gap:12px;min-width:0;display:flex}.cii49a_headText{flex:1;min-width:0}.cii49a_titleRow{align-items:baseline;gap:8px;min-width:0;display:flex}.cii49a_title{min-width:0;color:var(--dsw-alias-label-primary,#0f1115);letter-spacing:-.01em;text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:15px;font-weight:700;line-height:22px;overflow:hidden}.cii49a_version{color:var(--dsw-alias-label-tertiary,#81858c);font-variant-numeric:tabular-nums;flex:none;font-size:11.5px;font-weight:600}.cii49a_origin{align-items:center;gap:6px;min-width:0;margin:2px 0 0;font-size:11px;line-height:16px;display:flex}.cii49a_source{color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.06em;text-transform:uppercase;flex:none;font-weight:700}.cii49a_author{min-width:0;color:var(--dsw-alias-label-secondary,#61666b);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cii49a_summary{min-height:42px;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;-webkit-line-clamp:3;-webkit-box-orient:vertical;margin:0;font-size:13px;line-height:21px;display:-webkit-box;overflow:hidden}.cii49a_tags{min-height:18px;color:var(--dsw-alias-label-tertiary,#81858c);flex-wrap:wrap;align-items:center;gap:4px 8px;margin:0;font-size:12px;line-height:18px;display:flex}.cii49a_tagIcon{color:var(--dsw-alias-label-tertiary,#81858c);flex:none}.cii49a_tag{text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.cii49a_footer{border-top:1px solid var(--dsw-alias-border-l1,#0000000a);flex-wrap:wrap;align-items:center;gap:8px;margin-top:auto;padding-top:10px;display:flex}.cii49a_badges{flex-wrap:wrap;align-items:center;gap:6px;min-width:0;display:flex}.cii49a_stats{color:var(--dsw-alias-label-secondary,#61666b);font-variant-numeric:tabular-nums;flex-wrap:wrap;align-items:center;gap:10px;margin-left:auto;font-size:12px;line-height:18px;display:flex}.cii49a_stat{white-space:nowrap;align-items:center;gap:4px;display:inline-flex}.cii49a_install{z-index:2;pointer-events:auto;position:relative}";
23133
24219
  const tagId$4 = "@nanmicoder/dsh-skills-hub/SkillCard.module.css";
@@ -23291,7 +24377,7 @@ function(property) {
23291
24377
  }
23292
24378
  //#endregion
23293
24379
  //#region \0dsh-css:src/client/components/SourceStatusBar.module.css.mjs
23294
- const css$3 = ".QI7v1q_bar{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.QI7v1q_list{flex-wrap:wrap;align-items:center;gap:6px;min-width:0;margin:0;padding:0;list-style:none;display:flex}.QI7v1q_item{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);white-space:nowrap;border-radius:999px;align-items:center;gap:6px;height:24px;padding:0 9px;font-size:12px;line-height:18px;display:inline-flex}.QI7v1q_dot{font-size:10px;line-height:10px}.QI7v1q_name{color:var(--dsw-alias-label-primary,#0f1115);font-weight:600}.QI7v1q_status{color:var(--dsw-alias-label-tertiary,#81858c)}.QI7v1q_refresh{flex:none}.QI7v1q_spinning{transform-origin:50%;animation:.9s linear infinite QI7v1q_skills-hub-spin}@keyframes QI7v1q_skills-hub-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.QI7v1q_spinning{animation:none}}";
24380
+ const css$3 = ".QI7v1q_bar{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.QI7v1q_list{flex-wrap:wrap;align-items:center;gap:6px;min-width:0;margin:0;padding:0;list-style:none;display:flex}.QI7v1q_item{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);white-space:nowrap;border-radius:999px;align-items:center;gap:6px;height:24px;padding:0 9px;font-size:12px;line-height:18px;display:inline-flex}.QI7v1q_dot{font-size:10px;line-height:10px}.QI7v1q_name{color:var(--dsw-alias-label-primary,#0f1115);font-weight:600}.QI7v1q_status{color:var(--dsw-alias-label-tertiary,#81858c)}.QI7v1q_snapshot{border-left:1px solid var(--dsw-alias-border-l2,#0000001a);color:var(--dsw-alias-label-secondary,#61666b);font-variant-numeric:tabular-nums;padding-left:6px}.QI7v1q_refresh{flex:none}.QI7v1q_spinning{transform-origin:50%;animation:.9s linear infinite QI7v1q_skills-hub-spin}@keyframes QI7v1q_skills-hub-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.QI7v1q_spinning{animation:none}}";
23295
24381
  const tagId$3 = "@nanmicoder/dsh-skills-hub/SourceStatusBar.module.css";
23296
24382
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
23297
24383
  const tag = document.createElement("style");
@@ -23308,6 +24394,7 @@ function(property) {
23308
24394
  "name": "QI7v1q_name",
23309
24395
  "refresh": "QI7v1q_refresh",
23310
24396
  "skills-hub-spin": "QI7v1q_skills-hub-spin",
24397
+ "snapshot": "QI7v1q_snapshot",
23311
24398
  "spinning": "QI7v1q_spinning",
23312
24399
  "status": "QI7v1q_status"
23313
24400
  };
@@ -23332,6 +24419,10 @@ function(property) {
23332
24419
  * The upstream error string rides the pill's `title` instead of being printed:
23333
24420
  * a 720px panel has no room for a stack trace next to two source names, and an
23334
24421
  * operator who needs it can hover.
24422
+ *
24423
+ * The snapshot age *is* printed: a healthy source can still be answering from a
24424
+ * cache, and a reader deciding whether to install third-party code is entitled
24425
+ * to see that the page in front of them was read minutes ago rather than now.
23335
24426
  */
23336
24427
  function SourceStatusBar(props) {
23337
24428
  const { sources, onRefresh, refreshing } = props;
@@ -23342,6 +24433,7 @@ function(property) {
23342
24433
  className: SourceStatusBar_module_css_default.list,
23343
24434
  children: SOURCES.map((source) => {
23344
24435
  const info = sources[source];
24436
+ const snapshotAt = info.fromCache === true ? info.fetchedAt : void 0;
23345
24437
  return (0, react_jsx_runtime.jsxs)("li", {
23346
24438
  className: SourceStatusBar_module_css_default.item,
23347
24439
  title: info.error,
@@ -23357,18 +24449,23 @@ function(property) {
23357
24449
  (0, react_jsx_runtime.jsx)("span", {
23358
24450
  className: SourceStatusBar_module_css_default.status,
23359
24451
  children: t(`sourceStatus.${info.status}`)
24452
+ }),
24453
+ snapshotAt !== void 0 && (0, react_jsx_runtime.jsx)("span", {
24454
+ className: SourceStatusBar_module_css_default.snapshot,
24455
+ title: formatStamp(snapshotAt),
24456
+ children: t("snapshotAge", { age: formatAge(t, snapshotAt) })
23360
24457
  })
23361
24458
  ]
23362
24459
  }, source);
23363
24460
  })
23364
24461
  }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
23365
- label: t("retry"),
24462
+ label: t("refreshNow"),
23366
24463
  children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23367
24464
  variant: "ghost",
23368
24465
  size: "sm",
23369
24466
  className: SourceStatusBar_module_css_default.refresh,
23370
24467
  disabled: refreshing,
23371
- "aria-label": t("retry"),
24468
+ "aria-label": t("refreshNow"),
23372
24469
  onClick: onRefresh,
23373
24470
  icon: (0, react_jsx_runtime.jsx)(RefreshIcon, {
23374
24471
  size: 16,
@@ -23420,6 +24517,27 @@ function(property) {
23420
24517
  //#endregion
23421
24518
  //#region lib/client/components/MarketHome.js
23422
24519
  /**
24520
+ * How far ahead of the end of the list the next page is fetched.
24521
+ *
24522
+ * The catalogue scrolls inside the host's dock rather than the document, and an
24523
+ * observer whose root is the viewport is clipped by that dock: `rootMargin` on
24524
+ * the viewport is dead weight there (measured: the same sentinel reports
24525
+ * "not intersecting" 412px below the fold with a viewport root, and
24526
+ * "intersecting" with the dock as root). The margin therefore only means
24527
+ * anything once the observer is given the real scroll container.
24528
+ */
24529
+ const PREFETCH_MARGIN = "900px 0px";
24530
+ /** Nearest scrollable ancestor, or `null` when the list scrolls with the document. */
24531
+ function scrollContainerOf(element) {
24532
+ let node = element.parentElement;
24533
+ while (node !== null && node !== document.body) {
24534
+ const overflowY = getComputedStyle(node).overflowY;
24535
+ if ((overflowY === "auto" || overflowY === "scroll") && node.scrollHeight > node.clientHeight) return node;
24536
+ node = node.parentElement;
24537
+ }
24538
+ return null;
24539
+ }
24540
+ /**
23423
24541
  * Market catalogue page.
23424
24542
  *
23425
24543
  * Presentational by contract: every interaction below is a call on the
@@ -23430,16 +24548,31 @@ function(property) {
23430
24548
  const { state, controller } = props;
23431
24549
  const t = useT();
23432
24550
  const sentinelRef = (0, react.useRef)(null);
24551
+ const [scrolled, setScrolled] = (0, react.useState)(false);
24552
+ (0, react.useEffect)(() => {
24553
+ if (scrolled) return;
24554
+ const mark = () => setScrolled(true);
24555
+ document.addEventListener("scroll", mark, {
24556
+ capture: true,
24557
+ passive: true,
24558
+ once: true
24559
+ });
24560
+ return () => document.removeEventListener("scroll", mark, { capture: true });
24561
+ }, [scrolled]);
23433
24562
  (0, react.useEffect)(() => {
23434
24563
  const sentinel = sentinelRef.current;
23435
- if (sentinel === null || state.nextCursor === null || state.loading || state.loadingMore || state.error !== null) return;
24564
+ if (sentinel === null || !scrolled || state.nextCursor === null || state.loading || state.loadingMore || state.error !== null) return;
23436
24565
  const observer = new IntersectionObserver((entries) => {
23437
24566
  if (entries.some((entry) => entry.isIntersecting)) controller.loadMore();
23438
- }, { rootMargin: "240px 0px" });
24567
+ }, {
24568
+ root: scrollContainerOf(sentinel),
24569
+ rootMargin: PREFETCH_MARGIN
24570
+ });
23439
24571
  observer.observe(sentinel);
23440
24572
  return () => observer.disconnect();
23441
24573
  }, [
23442
24574
  controller,
24575
+ scrolled,
23443
24576
  state.nextCursor,
23444
24577
  state.loading,
23445
24578
  state.loadingMore,
@@ -23488,7 +24621,7 @@ function(property) {
23488
24621
  children: t("count", { count: state.items.length })
23489
24622
  }), (0, react_jsx_runtime.jsx)(SourceStatusBar, {
23490
24623
  sources: state.sources,
23491
- onRefresh: () => void controller.refresh(),
24624
+ onRefresh: () => void controller.refresh({ force: true }),
23492
24625
  refreshing: state.loading
23493
24626
  })]
23494
24627
  }),
@@ -23559,171 +24692,60 @@ function(property) {
23559
24692
  className: MarketHome_module_css_default.emptyText,
23560
24693
  children: narrowed ? t("emptySearchHint") : t("emptyHint")
23561
24694
  })
23562
- ]
23563
- }),
23564
- hasItems && (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: (0, react_jsx_runtime.jsx)("div", {
23565
- className: MarketHome_module_css_default.grid,
23566
- children: state.items.map((skill) => (0, react_jsx_runtime.jsx)(SkillCard, {
23567
- skill,
23568
- installing: state.installingIds.has(skill.id),
23569
- onOpen: (id) => void controller.openDetail(id),
23570
- onInstall: (id) => controller.requestInstall(id)
23571
- }, skill.id))
23572
- }) }),
23573
- state.nextCursor !== null && (0, react_jsx_runtime.jsx)("div", {
23574
- ref: sentinelRef,
23575
- className: MarketHome_module_css_default.more,
23576
- children: state.loadingMore && (0, react_jsx_runtime.jsxs)("p", {
23577
- className: MarketHome_module_css_default.loading,
23578
- role: "status",
23579
- "aria-live": "polite",
23580
- children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
23581
- state: "ongoing",
23582
- size: 16
23583
- }), t("loadingMore")]
23584
- })
23585
- })
23586
- ]
23587
- })
23588
- ]
23589
- });
23590
- }
23591
- //#endregion
23592
- //#region \0dsh-css:src/client/components/FrontmatterPanel.module.css.mjs
23593
- const css$1 = ".hR0bda_panel{border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-layer-1,#fff);flex-direction:column;min-width:0;display:flex;overflow:hidden}.hR0bda_head{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);background:var(--dsw-alias-bg-module-platform,#f5f6f7);align-items:center;gap:8px;padding:8px 12px;display:flex}.hR0bda_title{min-width:0;color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.08em;text-transform:uppercase;flex:1;margin:0;font-size:10.5px;font-weight:600}.hR0bda_count{color:var(--dsw-alias-label-tertiary,#81858c);font-variant-numeric:tabular-nums;font-size:11px}.hR0bda_list{max-height:320px;margin:0;padding:0;overflow-y:auto}.hR0bda_row{border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);flex-direction:column;gap:3px;min-width:0;padding:8px 12px;display:flex}.hR0bda_row:last-child{border-bottom:none}.hR0bda_key{overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-tertiary,#81858c);letter-spacing:.02em;font-size:10.5px;font-weight:600}.hR0bda_value{min-width:0;color:var(--dsw-alias-label-primary,#0f1115);margin:0;font-size:12.5px;line-height:19px}.hR0bda_text{overflow-wrap:anywhere}.hR0bda_chips{flex-wrap:wrap;gap:4px;display:flex}.hR0bda_chip{background:var(--dsw-alias-bg-module-platform,#f5f6f7);max-width:100%;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere;border-radius:999px;padding:1px 7px;font-size:11px;line-height:17px;display:inline-flex}.hR0bda_chipOn{background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#22c55e) 12%, transparent);color:var(--dsw-alias-state-success-primary,#22c55e)}.hR0bda_block{border-radius:var(--dsw-radius-xs,4px);background:var(--dsw-alias-bg-module-platform,#f5f6f7);max-height:200px;color:var(--dsw-alias-label-secondary,#61666b);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:18px;overflow:auto}";
23594
- const tagId$1 = "@nanmicoder/dsh-skills-hub/FrontmatterPanel.module.css";
23595
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
23596
- const tag = document.createElement("style");
23597
- tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
23598
- tag.dataset.pluginCss = tagId$1;
23599
- tag.textContent = css$1;
23600
- document.head.appendChild(tag);
23601
- }
23602
- var FrontmatterPanel_module_css_default = {
23603
- "block": "hR0bda_block",
23604
- "chip": "hR0bda_chip",
23605
- "chipOn": "hR0bda_chipOn",
23606
- "chips": "hR0bda_chips",
23607
- "count": "hR0bda_count",
23608
- "head": "hR0bda_head",
23609
- "key": "hR0bda_key",
23610
- "list": "hR0bda_list",
23611
- "panel": "hR0bda_panel",
23612
- "row": "hR0bda_row",
23613
- "text": "hR0bda_text",
23614
- "title": "hR0bda_title",
23615
- "value": "hR0bda_value"
23616
- };
23617
- //#endregion
23618
- //#region lib/client/components/FrontmatterPanel.js
23619
- /** Longest JSON we are willing to render inline, in characters. */
23620
- const MAX_JSON_LENGTH = 600;
23621
- /** Narrow an `unknown` frontmatter value to a plain object (arrays excluded). */
23622
- function isRecord(value) {
23623
- return typeof value === "object" && value !== null && !Array.isArray(value);
23624
- }
23625
- /** Scalar rendering for anything that is not an array, object or block string. */
23626
- function scalarText(value) {
23627
- if (value === null || value === void 0) return "—";
23628
- if (typeof value === "string") return value;
23629
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
23630
- if (typeof value === "symbol") return value.description ?? "symbol";
23631
- if (typeof value === "function") return "fn";
23632
- return "—";
23633
- }
23634
- /** Objects go out as JSON; a pathological value must not fill the whole rail. */
23635
- function jsonText(value) {
23636
- try {
23637
- const text = JSON.stringify(value, null, 2);
23638
- if (text === void 0) return "—";
23639
- return text.length > MAX_JSON_LENGTH ? `${text.slice(0, MAX_JSON_LENGTH)}…` : text;
23640
- } catch {
23641
- return "—";
23642
- }
23643
- }
23644
- /** One value cell; the shape decides between a chip row, a code block and text. */
23645
- function ValueView({ value }) {
23646
- if (Array.isArray(value)) {
23647
- if (value.length === 0) return (0, react_jsx_runtime.jsx)("span", {
23648
- className: FrontmatterPanel_module_css_default.text,
23649
- children: "—"
23650
- });
23651
- return (0, react_jsx_runtime.jsx)("span", {
23652
- className: FrontmatterPanel_module_css_default.chips,
23653
- children: value.map((item, index) => (0, react_jsx_runtime.jsx)("span", {
23654
- className: FrontmatterPanel_module_css_default.chip,
23655
- children: scalarText(item)
23656
- }, `${String(index)}`))
23657
- });
23658
- }
23659
- if (typeof value === "boolean") return (0, react_jsx_runtime.jsx)("span", {
23660
- className: `${FrontmatterPanel_module_css_default.chip} ${value ? FrontmatterPanel_module_css_default.chipOn : ""}`,
23661
- children: value ? "true" : "false"
23662
- });
23663
- if (typeof value === "string" && value.includes("\n")) return (0, react_jsx_runtime.jsx)("pre", {
23664
- className: FrontmatterPanel_module_css_default.block,
23665
- children: value
23666
- });
23667
- if (isRecord(value)) return (0, react_jsx_runtime.jsx)("pre", {
23668
- className: FrontmatterPanel_module_css_default.block,
23669
- children: jsonText(value)
23670
- });
23671
- return (0, react_jsx_runtime.jsx)("span", {
23672
- className: FrontmatterPanel_module_css_default.text,
23673
- children: scalarText(value)
24695
+ ]
24696
+ }),
24697
+ hasItems && (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: (0, react_jsx_runtime.jsx)("div", {
24698
+ className: MarketHome_module_css_default.grid,
24699
+ children: state.items.map((skill) => (0, react_jsx_runtime.jsx)(SkillCard, {
24700
+ skill,
24701
+ installing: state.installingIds.has(skill.id),
24702
+ onOpen: (id) => void controller.openDetail(id),
24703
+ onInstall: (id) => controller.requestInstall(id)
24704
+ }, skill.id))
24705
+ }) }),
24706
+ state.nextCursor !== null && (0, react_jsx_runtime.jsx)("div", {
24707
+ ref: sentinelRef,
24708
+ className: MarketHome_module_css_default.more,
24709
+ children: state.loadingMore && (0, react_jsx_runtime.jsxs)("p", {
24710
+ className: MarketHome_module_css_default.loading,
24711
+ role: "status",
24712
+ "aria-live": "polite",
24713
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
24714
+ state: "ongoing",
24715
+ size: 16
24716
+ }), t("loadingMore")]
24717
+ })
24718
+ })
24719
+ ]
24720
+ })
24721
+ ]
23674
24722
  });
23675
24723
  }
23676
- /**
23677
- * Structured view of a SKILL.md frontmatter block.
23678
- *
23679
- * SKILL.md metadata is data, not prose: pushing it through the markdown
23680
- * renderer turns a dozen short fields into a wall of setext headings. It is
23681
- * also reference material rather than the document itself, so it renders as a
23682
- * label/value list under its own heading, with the list itself scrolling once a
23683
- * skill declares more than a screenful of keys.
23684
- *
23685
- * `data` is typed `unknown`-valued on purpose — it is upstream YAML, and every
23686
- * cell is derived defensively rather than trusted.
23687
- */
23688
- function FrontmatterPanel(props) {
23689
- const { data } = props;
23690
- const t = useT();
23691
- const entries = Object.entries(data);
23692
- if (entries.length === 0) return (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
23693
- return (0, react_jsx_runtime.jsxs)("section", {
23694
- className: FrontmatterPanel_module_css_default.panel,
23695
- children: [(0, react_jsx_runtime.jsxs)("div", {
23696
- className: FrontmatterPanel_module_css_default.head,
23697
- children: [(0, react_jsx_runtime.jsx)("h3", {
23698
- className: FrontmatterPanel_module_css_default.title,
23699
- children: t("frontmatter")
23700
- }), (0, react_jsx_runtime.jsx)("span", {
23701
- className: FrontmatterPanel_module_css_default.count,
23702
- children: entries.length
23703
- })]
23704
- }), (0, react_jsx_runtime.jsx)("dl", {
23705
- className: FrontmatterPanel_module_css_default.list,
23706
- children: entries.map(([key, value]) => (0, react_jsx_runtime.jsxs)("div", {
23707
- className: FrontmatterPanel_module_css_default.row,
23708
- children: [(0, react_jsx_runtime.jsx)("dt", {
23709
- className: FrontmatterPanel_module_css_default.key,
23710
- title: key,
23711
- children: key
23712
- }), (0, react_jsx_runtime.jsx)("dd", {
23713
- className: FrontmatterPanel_module_css_default.value,
23714
- children: (0, react_jsx_runtime.jsx)(ValueView, { value })
23715
- })]
23716
- }, key))
23717
- })]
23718
- });
24724
+ //#endregion
24725
+ //#region \0dsh-css:src/client/components/SkillDetailView.module.css.mjs
24726
+ const css$1 = ".xO9T5G_warning{border-radius:var(--dsw-radius-sm,8px);background:color-mix(in srgb, var(--dsw-alias-state-error-primary,#ec1313) 10%, transparent);color:var(--dsw-alias-label-primary,#0f1115);align-items:center;gap:8px;margin:0;padding:8px 12px;font-size:12.5px;line-height:19px;display:flex}.xO9T5G_warningIcon{color:var(--dsw-alias-state-error-primary,#ec1313);flex:none}.xO9T5G_reports{border-top:1px solid var(--dsw-alias-border-l1,#0000000a);border-bottom:1px solid var(--dsw-alias-border-l1,#0000000a);flex-wrap:wrap;align-items:center;gap:8px 14px;padding:10px 2px;font-size:12.5px;line-height:19px;display:flex}.xO9T5G_reportsLabel{color:var(--dsw-alias-label-tertiary,#81858c)}.xO9T5G_report{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.xO9T5G_reportVendor{color:var(--dsw-alias-label-primary,#0f1115);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;font-weight:600}.xO9T5G_reportStatus{color:var(--dsw-alias-label-secondary,#61666b)}.xO9T5G_reportLink{background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 12%, transparent);color:var(--dsw-alias-state-business-primary,#4176e6);border-radius:999px;align-items:center;padding:2px 9px;font-size:12px;font-weight:600;text-decoration:none;display:inline-flex}.xO9T5G_reportLink:hover{background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 20%, transparent)}.xO9T5G_reportLink:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:2px}.xO9T5G_snapshot{border-radius:var(--dsw-radius-sm,8px);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary,#f59e0b) 12%, transparent);color:var(--dsw-alias-label-primary,#0f1115);flex-wrap:wrap;align-items:center;gap:10px;margin:0;padding:8px 12px;font-size:12.5px;line-height:19px;display:flex}.xO9T5G_snapshotAction{border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#0f1115);font:inherit;cursor:pointer;border-radius:999px;padding:2px 9px;font-size:12px;font-weight:600}.xO9T5G_snapshotAction:disabled{opacity:.6;cursor:default}.xO9T5G_snapshotAction:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:2px}";
24727
+ const tagId$1 = "@nanmicoder/dsh-skills-hub/SkillDetailView.module.css";
24728
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
24729
+ const tag = document.createElement("style");
24730
+ tag.dataset.plugin = "@nanmicoder/dsh-skills-hub";
24731
+ tag.dataset.pluginCss = tagId$1;
24732
+ tag.textContent = css$1;
24733
+ document.head.appendChild(tag);
23719
24734
  }
24735
+ var SkillDetailView_module_css_default = {
24736
+ "report": "xO9T5G_report",
24737
+ "reportLink": "xO9T5G_reportLink",
24738
+ "reportStatus": "xO9T5G_reportStatus",
24739
+ "reportVendor": "xO9T5G_reportVendor",
24740
+ "reports": "xO9T5G_reports",
24741
+ "reportsLabel": "xO9T5G_reportsLabel",
24742
+ "snapshot": "xO9T5G_snapshot",
24743
+ "snapshotAction": "xO9T5G_snapshotAction",
24744
+ "warning": "xO9T5G_warning",
24745
+ "warningIcon": "xO9T5G_warningIcon"
24746
+ };
23720
24747
  //#endregion
23721
24748
  //#region lib/client/components/SkillDetailView.js
23722
- /** How long the copy button stays in its confirmed state. */
23723
- const COPIED_RESET_MS = 2e3;
23724
- /** Stable DOM ids for the tab list and its panels; one detail view is mounted at a time. */
23725
- const TAB_ID = "skills-hub-detail-tab";
23726
- const PANEL_ID$1 = "skills-hub-detail-panel";
23727
24749
  /** Compact counts, matching the catalogue cards. */
23728
24750
  function formatCount(value) {
23729
24751
  if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
@@ -23743,25 +24765,14 @@ function(property) {
23743
24765
  function safeReportUrl(url) {
23744
24766
  return url !== void 0 && /^https?:\/\//i.test(url) ? url : void 0;
23745
24767
  }
23746
- /** One label/value row of the right-hand facts rail. */
23747
- function Fact(props) {
23748
- return (0, react_jsx_runtime.jsxs)("div", {
23749
- className: SkillDetailView_module_css_default.fact,
23750
- children: [(0, react_jsx_runtime.jsx)("dt", {
23751
- className: SkillDetailView_module_css_default.factLabel,
23752
- children: props.label
23753
- }), (0, react_jsx_runtime.jsx)("dd", {
23754
- className: SkillDetailView_module_css_default.factValue,
23755
- children: props.children
23756
- })]
23757
- });
23758
- }
23759
24768
  /**
23760
- * Skill detail page.
24769
+ * Market skill detail page.
23761
24770
  *
23762
24771
  * Like the home page this component owns no data: the tab, the selected file
23763
24772
  * and the file contents all live in `MarketState`, so switching tabs or
23764
- * re-opening a skill does not lose its place.
24773
+ * re-opening a skill does not lose its place. The reading surface itself — back
24774
+ * affordance, header, tabs, facts rail — is `SkillDetailShell`, which the local
24775
+ * installed page uses too.
23765
24776
  *
23766
24777
  * The bundled GFM renderer handles catalogue Markdown without relying on a
23767
24778
  * host renderer delegate. Raw HTML is disabled and URLs use its safe default.
@@ -23769,101 +24780,39 @@ function(property) {
23769
24780
  function SkillDetailView(props) {
23770
24781
  const { detail, state, controller } = props;
23771
24782
  const t = useT();
23772
- const headingRef = (0, react.useRef)(null);
23773
- const [copied, setCopied] = (0, react.useState)(false);
23774
- const resetTimer = (0, react.useRef)(void 0);
23775
- (0, react.useEffect)(() => {
23776
- headingRef.current?.focus();
23777
- }, [detail.id]);
23778
- (0, react.useEffect)(() => () => {
23779
- if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
23780
- }, []);
23781
- const copyFile = (0, react.useCallback)(() => {
23782
- const content = state.file?.content;
23783
- if (content === void 0) return;
23784
- (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(content).then((ok) => {
23785
- if (!ok) return;
23786
- setCopied(true);
23787
- if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
23788
- resetTimer.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);
23789
- });
23790
- }, [state.file]);
23791
- const tabs = [{
23792
- value: "overview",
23793
- label: t("overview"),
23794
- id: `${TAB_ID}-overview`,
23795
- panelId: `${PANEL_ID$1}-overview`
23796
- }, {
23797
- value: "files",
23798
- label: `${t("files")} (${detail.files.length})`,
23799
- id: `${TAB_ID}-files`,
23800
- panelId: `${PANEL_ID$1}-files`
23801
- }];
23802
24783
  const installationInFlight = state.installingIds.has(detail.id);
23803
24784
  const author = detail.author.displayName ?? detail.author.handle;
23804
- const file = state.file;
23805
- const selectedPath = file?.path ?? null;
23806
- const selectFile = (meta) => {
23807
- if (meta.path === selectedPath) return;
23808
- controller.selectFile(meta.path);
23809
- };
23810
- return (0, react_jsx_runtime.jsxs)("div", {
23811
- className: SkillDetailView_module_css_default.detail,
23812
- children: [
23813
- (0, react_jsx_runtime.jsx)("div", {
23814
- className: SkillDetailView_module_css_default.back,
23815
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23816
- variant: "ghost",
23817
- size: "sm",
23818
- icon: (0, react_jsx_runtime.jsx)(ArrowLeftIcon, { size: 16 }),
23819
- onClick: () => controller.closeDetail(),
23820
- children: t("back")
23821
- })
23822
- }),
23823
- (0, react_jsx_runtime.jsxs)("header", {
23824
- className: SkillDetailView_module_css_default.header,
23825
- children: [(0, react_jsx_runtime.jsx)(SkillAvatar, {
23826
- name: detail.name,
23827
- source: detail.source,
23828
- iconUrl: detail.iconUrl,
23829
- size: 84
23830
- }), (0, react_jsx_runtime.jsxs)("div", {
23831
- className: SkillDetailView_module_css_default.headerText,
23832
- children: [
23833
- (0, react_jsx_runtime.jsxs)("p", {
23834
- className: SkillDetailView_module_css_default.eyebrow,
23835
- children: [(0, react_jsx_runtime.jsx)("span", {
23836
- className: SkillDetailView_module_css_default.source,
23837
- children: t(`source.${detail.source}`)
23838
- }), detail.version !== void 0 && detail.version !== "" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
23839
- "aria-hidden": "true",
23840
- children: "·"
23841
- }), (0, react_jsx_runtime.jsxs)("span", {
23842
- className: SkillDetailView_module_css_default.version,
23843
- children: ["v", detail.version]
23844
- })] })]
23845
- }),
23846
- (0, react_jsx_runtime.jsx)("h1", {
23847
- ref: headingRef,
23848
- tabIndex: -1,
23849
- className: SkillDetailView_module_css_default.name,
23850
- children: detail.name
23851
- }),
23852
- (0, react_jsx_runtime.jsxs)("div", {
23853
- className: SkillDetailView_module_css_default.badges,
23854
- children: [(0, react_jsx_runtime.jsx)(SecurityBadge, {
23855
- status: detail.securityStatus,
23856
- reports: detail.securityReports
23857
- }), (0, react_jsx_runtime.jsx)(InstallStateBadge, { state: detail.installState })]
23858
- }),
23859
- detail.summary !== "" && (0, react_jsx_runtime.jsx)("p", {
23860
- className: SkillDetailView_module_css_default.summary,
23861
- children: detail.summary
23862
- })
23863
- ]
23864
- })]
23865
- }),
23866
- detail.installState === "not-installable" && detail.notInstallableReason !== void 0 && (0, react_jsx_runtime.jsxs)("p", {
24785
+ const reports = detail.securityReports ?? [];
24786
+ const snapshotAt = state.detailStatus?.fromCache === true ? state.detailStatus.fetchedAt : void 0;
24787
+ const notInstallable = detail.installState === "not-installable" && detail.notInstallableReason !== void 0;
24788
+ return (0, react_jsx_runtime.jsx)(SkillDetailShell, {
24789
+ focusKey: detail.id,
24790
+ onBack: () => controller.closeDetail(),
24791
+ backLabel: t("back"),
24792
+ avatar: (0, react_jsx_runtime.jsx)(SkillAvatar, {
24793
+ name: detail.name,
24794
+ source: detail.source,
24795
+ iconUrl: detail.iconUrl,
24796
+ size: 84
24797
+ }),
24798
+ eyebrow: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
24799
+ className: detailStyles.source,
24800
+ children: t(`source.${detail.source}`)
24801
+ }), detail.version !== void 0 && detail.version !== "" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
24802
+ "aria-hidden": "true",
24803
+ children: "·"
24804
+ }), (0, react_jsx_runtime.jsxs)("span", {
24805
+ className: detailStyles.version,
24806
+ children: ["v", detail.version]
24807
+ })] })] }),
24808
+ name: detail.name,
24809
+ badges: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(SecurityBadge, {
24810
+ status: detail.securityStatus,
24811
+ reports: detail.securityReports
24812
+ }), (0, react_jsx_runtime.jsx)(InstallStateBadge, { state: detail.installState })] }),
24813
+ summary: detail.summary,
24814
+ notice: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
24815
+ notInstallable && (0, react_jsx_runtime.jsxs)("p", {
23867
24816
  className: SkillDetailView_module_css_default.warning,
23868
24817
  role: "note",
23869
24818
  title: detail.notInstallableReason,
@@ -23872,12 +24821,12 @@ function(property) {
23872
24821
  className: SkillDetailView_module_css_default.warningIcon
23873
24822
  }), (0, react_jsx_runtime.jsx)("span", { children: t("notInstallable") })]
23874
24823
  }),
23875
- detail.securityReports !== void 0 && detail.securityReports.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
24824
+ reports.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
23876
24825
  className: SkillDetailView_module_css_default.reports,
23877
24826
  children: [(0, react_jsx_runtime.jsx)("span", {
23878
24827
  className: SkillDetailView_module_css_default.reportsLabel,
23879
24828
  children: t("security")
23880
- }), detail.securityReports.map((report) => {
24829
+ }), reports.map((report) => {
23881
24830
  const url = safeReportUrl(report.reportUrl);
23882
24831
  return (0, react_jsx_runtime.jsxs)("span", {
23883
24832
  className: SkillDetailView_module_css_default.report,
@@ -23901,245 +24850,96 @@ function(property) {
23901
24850
  }, `${report.vendor}:${report.status}`);
23902
24851
  })]
23903
24852
  }),
23904
- (0, react_jsx_runtime.jsxs)("div", {
23905
- className: SkillDetailView_module_css_default.columns,
23906
- children: [(0, react_jsx_runtime.jsxs)("main", {
23907
- className: SkillDetailView_module_css_default.main,
23908
- children: [
23909
- (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
23910
- items: tabs,
23911
- value: state.activeTab,
23912
- onChange: (value) => controller.setTab(value),
23913
- label: detail.name
23914
- }),
23915
- state.activeTab === "overview" && (0, react_jsx_runtime.jsxs)("section", {
23916
- className: SkillDetailView_module_css_default.panel,
23917
- id: `${PANEL_ID$1}-overview`,
23918
- role: "tabpanel",
23919
- "aria-labelledby": `${TAB_ID}-overview`,
23920
- children: [detail.description.trim() !== "" ? (0, react_jsx_runtime.jsx)(MarkdownView, { content: detail.description }) : detail.summary !== "" && (0, react_jsx_runtime.jsx)("p", {
23921
- className: SkillDetailView_module_css_default.summary,
23922
- children: detail.summary
23923
- }), detail.descriptionFrontmatter !== void 0 && Object.keys(detail.descriptionFrontmatter).length > 0 && (0, react_jsx_runtime.jsx)("div", {
23924
- className: SkillDetailView_module_css_default.frontmatter,
23925
- children: (0, react_jsx_runtime.jsx)(FrontmatterPanel, { data: detail.descriptionFrontmatter })
23926
- })]
23927
- }),
23928
- state.activeTab === "files" && (0, react_jsx_runtime.jsx)("section", {
23929
- className: SkillDetailView_module_css_default.panel,
23930
- id: `${PANEL_ID$1}-files`,
23931
- role: "tabpanel",
23932
- "aria-labelledby": `${TAB_ID}-files`,
23933
- children: detail.files.length === 0 ? (0, react_jsx_runtime.jsx)("p", {
23934
- className: SkillDetailView_module_css_default.muted,
23935
- children: t("noFiles")
23936
- }) : (0, react_jsx_runtime.jsxs)("div", {
23937
- className: SkillDetailView_module_css_default.files,
23938
- children: [(0, react_jsx_runtime.jsx)("ul", {
23939
- className: SkillDetailView_module_css_default.fileList,
23940
- children: detail.files.map((meta) => {
23941
- const active = meta.path === selectedPath;
23942
- return (0, react_jsx_runtime.jsx)("li", { children: (0, react_jsx_runtime.jsxs)("button", {
23943
- type: "button",
23944
- className: `${SkillDetailView_module_css_default.fileItem} ${active ? SkillDetailView_module_css_default.fileItemActive : ""}`,
23945
- "aria-current": active ? "true" : void 0,
23946
- onClick: () => selectFile(meta),
23947
- children: [
23948
- (0, react_jsx_runtime.jsx)(FileIcon, {
23949
- size: 14,
23950
- className: SkillDetailView_module_css_default.fileIcon
23951
- }),
23952
- (0, react_jsx_runtime.jsx)("span", {
23953
- className: SkillDetailView_module_css_default.filePath,
23954
- title: meta.path,
23955
- children: meta.path
23956
- }),
23957
- meta.tooBig && (0, react_jsx_runtime.jsx)("span", {
23958
- className: SkillDetailView_module_css_default.fileFlag,
23959
- children: t("fileTooLarge")
23960
- }),
23961
- (0, react_jsx_runtime.jsxs)("span", {
23962
- className: SkillDetailView_module_css_default.fileMeta,
23963
- children: [
23964
- (0, _deepseek_ai_dsh_client_ui_primitives.fileSizeText)(meta.size),
23965
- " · ",
23966
- meta.language
23967
- ]
23968
- })
23969
- ]
23970
- }) }, meta.path);
23971
- })
23972
- }), (0, react_jsx_runtime.jsx)("div", {
23973
- className: SkillDetailView_module_css_default.fileView,
23974
- children: file === null ? state.fileLoading ? (0, react_jsx_runtime.jsxs)("p", {
23975
- className: SkillDetailView_module_css_default.muted,
23976
- role: "status",
23977
- "aria-live": "polite",
23978
- children: [
23979
- (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
23980
- state: "ongoing",
23981
- size: 14
23982
- }),
23983
- " ",
23984
- t("loading")
23985
- ]
23986
- }) : null : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
23987
- (0, react_jsx_runtime.jsxs)("div", {
23988
- className: SkillDetailView_module_css_default.fileBar,
23989
- children: [(0, react_jsx_runtime.jsx)("span", {
23990
- className: SkillDetailView_module_css_default.fileBarPath,
23991
- title: file.path,
23992
- children: file.path
23993
- }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
23994
- variant: "outline",
23995
- size: "sm",
23996
- className: SkillDetailView_module_css_default.copy,
23997
- "aria-label": copied ? t("copied") : t("copy"),
23998
- onClick: copyFile,
23999
- icon: copied ? (0, react_jsx_runtime.jsx)(CheckIcon, { size: 14 }) : (0, react_jsx_runtime.jsx)(CopyIcon, { size: 14 }),
24000
- children: copied ? t("copied") : t("copy")
24001
- })]
24002
- }),
24003
- file.truncated && (0, react_jsx_runtime.jsx)("p", {
24004
- className: SkillDetailView_module_css_default.muted,
24005
- children: t("fileTooLarge")
24006
- }),
24007
- (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
24008
- code: file.content,
24009
- lang: file.language,
24010
- showHeader: false,
24011
- lineNumbers: true,
24012
- copyLabel: t("copy"),
24013
- copiedLabel: t("copied")
24014
- }),
24015
- (0, react_jsx_runtime.jsx)("p", {
24016
- className: SkillDetailView_module_css_default.srOnly,
24017
- role: "status",
24018
- "aria-live": "polite",
24019
- children: copied ? t("copied") : ""
24020
- })
24021
- ] })
24022
- })]
24023
- })
24024
- })
24025
- ]
24026
- }), (0, react_jsx_runtime.jsx)("aside", {
24027
- className: SkillDetailView_module_css_default.rail,
24028
- children: (0, react_jsx_runtime.jsxs)("div", {
24029
- className: SkillDetailView_module_css_default.railCard,
24030
- children: [(0, react_jsx_runtime.jsxs)("div", {
24031
- className: SkillDetailView_module_css_default.railAction,
24032
- children: [
24033
- detail.installState === "installable" && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
24034
- variant: "primary",
24035
- size: "md",
24036
- disabled: installationInFlight,
24037
- icon: (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 16 }),
24038
- onClick: () => controller.requestInstall(detail.id),
24039
- children: installationInFlight ? t("installing") : t("install")
24040
- }),
24041
- detail.installState === "installed" && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
24042
- variant: "outline",
24043
- size: "md",
24044
- disabled: installationInFlight,
24045
- title: t("uninstallConfirm"),
24046
- icon: (0, react_jsx_runtime.jsx)(TrashIcon, { size: 16 }),
24047
- onClick: () => void controller.uninstall(detail.id),
24048
- children: t("uninstall")
24049
- }),
24050
- detail.installState === "not-installable" && (0, react_jsx_runtime.jsx)("p", {
24051
- className: SkillDetailView_module_css_default.muted,
24052
- children: t("notInstallable")
24053
- })
24054
- ]
24055
- }), (0, react_jsx_runtime.jsxs)("dl", {
24056
- className: SkillDetailView_module_css_default.facts,
24057
- children: [
24058
- (0, react_jsx_runtime.jsx)(Fact, {
24059
- label: t("author"),
24060
- children: author
24061
- }),
24062
- (0, react_jsx_runtime.jsx)(Fact, {
24063
- label: t("downloads"),
24064
- children: formatCount(detail.stats.downloads)
24065
- }),
24066
- detail.stats.installs !== void 0 && (0, react_jsx_runtime.jsx)(Fact, {
24067
- label: t("installs"),
24068
- children: formatCount(detail.stats.installs)
24069
- }),
24070
- detail.stats.stars !== void 0 && (0, react_jsx_runtime.jsx)(Fact, {
24071
- label: t("stars"),
24072
- children: formatCount(detail.stats.stars)
24073
- }),
24074
- detail.updatedAt !== void 0 && (0, react_jsx_runtime.jsx)(Fact, {
24075
- label: t("updated"),
24076
- children: formatDate(detail.updatedAt)
24077
- }),
24078
- detail.license !== void 0 && detail.license !== "" && (0, react_jsx_runtime.jsx)(Fact, {
24079
- label: t("license"),
24080
- children: detail.license
24081
- }),
24082
- detail.version !== void 0 && detail.version !== "" && (0, react_jsx_runtime.jsxs)(Fact, {
24083
- label: t("version"),
24084
- children: ["v", detail.version]
24085
- })
24086
- ]
24087
- })]
24088
- })
24089
- })]
24090
- })
24091
- ]
24092
- });
24093
- }
24094
- /** Keeps the detail layout stable while its network request is in flight. */
24095
- function SkillDetailSkeleton({ onBack }) {
24096
- const t = useT();
24097
- return (0, react_jsx_runtime.jsxs)("div", {
24098
- className: SkillDetailView_module_css_default.detail,
24099
- "aria-busy": "true",
24100
- children: [(0, react_jsx_runtime.jsx)("div", {
24101
- className: SkillDetailView_module_css_default.back,
24102
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
24103
- variant: "ghost",
24104
- size: "sm",
24105
- icon: (0, react_jsx_runtime.jsx)(ArrowLeftIcon, { size: 16 }),
24106
- onClick: onBack,
24107
- children: t("back")
24108
- })
24109
- }), (0, react_jsx_runtime.jsx)("div", {
24110
- role: "status",
24111
- "aria-label": t("loading"),
24112
- children: (0, react_jsx_runtime.jsxs)("div", {
24113
- "aria-hidden": "true",
24114
- children: [(0, react_jsx_runtime.jsxs)("div", {
24115
- className: SkillDetailView_module_css_default.header,
24116
- children: [(0, react_jsx_runtime.jsx)("span", { className: SkillDetailView_module_css_default.skeletonAvatar }), (0, react_jsx_runtime.jsxs)("div", {
24117
- className: SkillDetailView_module_css_default.headerText,
24118
- children: [
24119
- (0, react_jsx_runtime.jsx)("span", { className: SkillDetailView_module_css_default.skeletonTitle }),
24120
- (0, react_jsx_runtime.jsx)("span", { className: SkillDetailView_module_css_default.skeletonLine }),
24121
- (0, react_jsx_runtime.jsx)("span", { className: SkillDetailView_module_css_default.skeletonLine })
24122
- ]
24123
- })]
24124
- }), (0, react_jsx_runtime.jsxs)("div", {
24125
- className: SkillDetailView_module_css_default.columns,
24126
- style: { marginTop: 24 },
24127
- children: [(0, react_jsx_runtime.jsx)("div", {
24128
- className: SkillDetailView_module_css_default.main,
24129
- children: (0, react_jsx_runtime.jsx)("div", {
24130
- className: SkillDetailView_module_css_default.panel,
24131
- children: Array.from({ length: 10 }, (_, index) => (0, react_jsx_runtime.jsx)("span", { className: SkillDetailView_module_css_default.skeletonLine }, index))
24132
- })
24133
- }), (0, react_jsx_runtime.jsx)("div", {
24134
- className: SkillDetailView_module_css_default.rail,
24135
- children: (0, react_jsx_runtime.jsx)("div", {
24136
- className: SkillDetailView_module_css_default.panel,
24137
- children: Array.from({ length: 5 }, (_, index) => (0, react_jsx_runtime.jsx)("span", { className: SkillDetailView_module_css_default.skeletonLine }, index))
24138
- })
24139
- })]
24853
+ snapshotAt !== void 0 && (0, react_jsx_runtime.jsxs)("p", {
24854
+ className: SkillDetailView_module_css_default.snapshot,
24855
+ role: "note",
24856
+ children: [(0, react_jsx_runtime.jsx)("span", {
24857
+ title: formatStamp(snapshotAt),
24858
+ children: t("detailSnapshot", { age: formatAge(t, snapshotAt) })
24859
+ }), (0, react_jsx_runtime.jsx)("button", {
24860
+ type: "button",
24861
+ className: SkillDetailView_module_css_default.snapshotAction,
24862
+ disabled: state.detailLoading,
24863
+ onClick: () => void controller.openDetail(detail.id, { refresh: true }),
24864
+ children: t("refreshSnapshot")
24140
24865
  })]
24141
24866
  })
24142
- })]
24867
+ ] }),
24868
+ overviewLabel: t("overview"),
24869
+ filesLabel: `${t("files")} (${detail.files.length})`,
24870
+ activeTab: state.activeTab,
24871
+ onTabChange: (tab) => controller.setTab(tab),
24872
+ overview: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [detail.description.trim() !== "" ? (0, react_jsx_runtime.jsx)(MarkdownView, { content: detail.description }) : detail.summary !== "" && (0, react_jsx_runtime.jsx)("p", {
24873
+ className: detailStyles.summary,
24874
+ children: detail.summary
24875
+ }), detail.descriptionFrontmatter !== void 0 && Object.keys(detail.descriptionFrontmatter).length > 0 && (0, react_jsx_runtime.jsx)("div", {
24876
+ className: detailStyles.frontmatter,
24877
+ children: (0, react_jsx_runtime.jsx)(FrontmatterPanel, { data: detail.descriptionFrontmatter })
24878
+ })] }),
24879
+ files: (0, react_jsx_runtime.jsx)(SkillFiles, {
24880
+ files: detail.files,
24881
+ selected: state.file,
24882
+ loading: state.fileLoading,
24883
+ onSelect: (path) => void controller.selectFile(path)
24884
+ }),
24885
+ rail: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
24886
+ className: detailStyles.railAction,
24887
+ children: [
24888
+ detail.installState === "installable" && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
24889
+ variant: "primary",
24890
+ size: "md",
24891
+ disabled: installationInFlight,
24892
+ icon: (0, react_jsx_runtime.jsx)(DownloadIcon, { size: 16 }),
24893
+ onClick: () => controller.requestInstall(detail.id),
24894
+ children: installationInFlight ? t("installing") : t("install")
24895
+ }),
24896
+ detail.installState === "installed" && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
24897
+ variant: "outline",
24898
+ size: "md",
24899
+ disabled: installationInFlight,
24900
+ title: t("uninstallConfirm"),
24901
+ icon: (0, react_jsx_runtime.jsx)(TrashIcon, { size: 16 }),
24902
+ onClick: () => void controller.uninstall(detail.id),
24903
+ children: t("uninstall")
24904
+ }),
24905
+ detail.installState === "not-installable" && (0, react_jsx_runtime.jsx)("p", {
24906
+ className: detailStyles.muted,
24907
+ children: t("notInstallable")
24908
+ })
24909
+ ]
24910
+ }), (0, react_jsx_runtime.jsxs)("dl", {
24911
+ className: detailStyles.facts,
24912
+ children: [
24913
+ (0, react_jsx_runtime.jsx)(Fact, {
24914
+ label: t("author"),
24915
+ children: author
24916
+ }),
24917
+ (0, react_jsx_runtime.jsx)(Fact, {
24918
+ label: t("downloads"),
24919
+ children: formatCount(detail.stats.downloads)
24920
+ }),
24921
+ detail.stats.installs !== void 0 && (0, react_jsx_runtime.jsx)(Fact, {
24922
+ label: t("installs"),
24923
+ children: formatCount(detail.stats.installs)
24924
+ }),
24925
+ detail.stats.stars !== void 0 && (0, react_jsx_runtime.jsx)(Fact, {
24926
+ label: t("stars"),
24927
+ children: formatCount(detail.stats.stars)
24928
+ }),
24929
+ detail.updatedAt !== void 0 && (0, react_jsx_runtime.jsx)(Fact, {
24930
+ label: t("updated"),
24931
+ children: formatDate(detail.updatedAt)
24932
+ }),
24933
+ detail.license !== void 0 && detail.license !== "" && (0, react_jsx_runtime.jsx)(Fact, {
24934
+ label: t("license"),
24935
+ children: detail.license
24936
+ }),
24937
+ detail.version !== void 0 && detail.version !== "" && (0, react_jsx_runtime.jsxs)(Fact, {
24938
+ label: t("version"),
24939
+ children: ["v", detail.version]
24940
+ })
24941
+ ]
24942
+ })] })
24143
24943
  });
24144
24944
  }
24145
24945
  //#endregion
@@ -24159,8 +24959,8 @@ function(property) {
24159
24959
  * already holds.
24160
24960
  * - **in-flight guards**: one install/uninstall per id, one page per cursor, one
24161
24961
  * detail/file request per target.
24162
- * - **detail/file caching**: reopening a skill is instant and cannot blank the
24163
- * pane with a spinner.
24962
+ * - **detail snapshots**: reopening can show the previous copy immediately,
24963
+ * while the Host rechecks its TTL and current install annotations.
24164
24964
  * - **local management**: the InstalledSkills component independently lists what is already
24165
24965
  * on disk (`fetchInstalled`) instead of asking the market, matching the
24166
24966
  * contract's §5.3 behaviour.
@@ -24205,11 +25005,13 @@ function(property) {
24205
25005
  items: [],
24206
25006
  nextCursor: null,
24207
25007
  sources: emptySources(),
25008
+ itemStatuses: {},
24208
25009
  loading: false,
24209
25010
  loadingMore: false,
24210
25011
  error: null,
24211
25012
  view: { kind: "home" },
24212
25013
  detail: null,
25014
+ detailStatus: null,
24213
25015
  detailLoading: false,
24214
25016
  detailError: null,
24215
25017
  activeTab: "overview",
@@ -24258,6 +25060,9 @@ function(property) {
24258
25060
  [source]: status
24259
25061
  };
24260
25062
  }
25063
+ function itemStatuses(items, sources) {
25064
+ return Object.fromEntries(items.map((item) => [item.id, sources[item.source]]));
25065
+ }
24261
25066
  /** Copy the install-related fields of a fresh skill onto a cached detail. */
24262
25067
  function patchDetail(detail, updated) {
24263
25068
  return {
@@ -24271,7 +25076,6 @@ function(property) {
24271
25076
  let state = initialState();
24272
25077
  const listeners = /* @__PURE__ */ new Set();
24273
25078
  const detailCache = /* @__PURE__ */ new Map();
24274
- const fileCache = /* @__PURE__ */ new Map();
24275
25079
  let listSequence = 0;
24276
25080
  let detailSequence = 0;
24277
25081
  let fileSequence = 0;
@@ -24337,11 +25141,17 @@ function(property) {
24337
25141
  } : item).filter(keepUnderInstalledFilter) };
24338
25142
  if (state.detail !== null && state.detail.id === updated.id) {
24339
25143
  const patched = patchDetail(state.detail, updated);
24340
- detailCache.set(updated.id, patched);
25144
+ detailCache.set(updated.id, {
25145
+ skill: patched,
25146
+ status: state.detailStatus ?? { status: "ok" }
25147
+ });
24341
25148
  patch.detail = patched;
24342
25149
  } else {
24343
25150
  const cached = detailCache.get(updated.id);
24344
- if (cached !== void 0) detailCache.set(updated.id, patchDetail(cached, updated));
25151
+ if (cached !== void 0) detailCache.set(updated.id, {
25152
+ skill: patchDetail(cached.skill, updated),
25153
+ status: cached.status
25154
+ });
24345
25155
  }
24346
25156
  commit(patch);
24347
25157
  }
@@ -24353,7 +25163,7 @@ function(property) {
24353
25163
  * development double-invocation repeats it. A different filter set always
24354
25164
  * issues a fresh request.
24355
25165
  */
24356
- async function refresh() {
25166
+ async function refresh(options = {}) {
24357
25167
  cancelDebounce();
24358
25168
  detailCache.clear();
24359
25169
  const key = currentListKey();
@@ -24370,6 +25180,7 @@ function(property) {
24370
25180
  loadingMore: false,
24371
25181
  error: null,
24372
25182
  items: [],
25183
+ itemStatuses: {},
24373
25184
  nextCursor: null
24374
25185
  });
24375
25186
  const promise = (async () => {
@@ -24380,12 +25191,13 @@ function(property) {
24380
25191
  security: filters.security,
24381
25192
  installed: filters.installed,
24382
25193
  limit: PAGE_SIZE
24383
- }, controller.signal);
25194
+ }, controller.signal, { refresh: options.force === true });
24384
25195
  if (sequence !== listSequence) return;
24385
25196
  commit({
24386
25197
  items: result.items,
24387
25198
  nextCursor: result.nextCursor,
24388
25199
  sources: result.sources,
25200
+ itemStatuses: itemStatuses(result.items, result.sources),
24389
25201
  loading: false
24390
25202
  });
24391
25203
  } catch (error) {
@@ -24433,6 +25245,10 @@ function(property) {
24433
25245
  items: [...state.items, ...appended],
24434
25246
  nextCursor: result.nextCursor,
24435
25247
  sources: result.sources,
25248
+ itemStatuses: {
25249
+ ...state.itemStatuses,
25250
+ ...itemStatuses(appended, result.sources)
25251
+ },
24436
25252
  loadingMore: false
24437
25253
  });
24438
25254
  } catch (error) {
@@ -24471,32 +25287,42 @@ function(property) {
24471
25287
  function isCurrentDetail(id) {
24472
25288
  return state.view.kind === "detail" && state.view.id === id;
24473
25289
  }
24474
- async function loadDetail(id) {
25290
+ async function loadDetail(id, options = {}) {
24475
25291
  const sequence = ++detailSequence;
24476
25292
  detailAbort?.abort();
24477
25293
  const controller = new AbortController();
24478
25294
  detailAbort = controller;
24479
25295
  try {
24480
- const { skill, sourceStatus } = await fetchSkillDetail(id, controller.signal);
25296
+ const { skill, sourceStatus } = await fetchSkillDetail(id, controller.signal, { refresh: options.refresh === true });
24481
25297
  if (sequence !== detailSequence || !isCurrentDetail(id)) return;
24482
- detailCache.set(id, skill);
25298
+ detailCache.set(id, {
25299
+ skill,
25300
+ status: sourceStatus
25301
+ });
24483
25302
  commit({
24484
25303
  detail: skill,
25304
+ detailStatus: sourceStatus,
24485
25305
  detailLoading: false,
24486
25306
  detailError: null,
24487
25307
  sources: mergeSourceStatus(state.sources, skill.source, sourceStatus)
24488
25308
  });
24489
25309
  } catch (error) {
24490
25310
  if (sequence !== detailSequence || isAbortError(error)) return;
25311
+ detailCache.delete(id);
24491
25312
  commit({
25313
+ detail: null,
25314
+ detailStatus: null,
24492
25315
  detailLoading: false,
24493
25316
  detailError: errorMessage(error)
24494
25317
  });
24495
25318
  }
24496
25319
  }
24497
- /** Open one skill: cache first, network only when the cache misses. */
24498
- async function openDetail(id) {
24499
- const cached = detailCache.get(id);
25320
+ /** Show a previous copy immediately, then check the Host's authoritative cache. */
25321
+ async function openDetail(id, options = {}) {
25322
+ const cached = options.refresh === true ? void 0 : detailCache.get(id);
25323
+ fileSequence += 1;
25324
+ fileAbort?.abort();
25325
+ inFlightFileKey = null;
24500
25326
  commit({
24501
25327
  view: {
24502
25328
  kind: "detail",
@@ -24507,18 +25333,20 @@ function(property) {
24507
25333
  fileLoading: false,
24508
25334
  detailError: null
24509
25335
  });
24510
- if (cached !== void 0) {
24511
- commit({
24512
- detail: cached,
24513
- detailLoading: false
24514
- });
24515
- return;
24516
- }
24517
- commit({
25336
+ if (cached !== void 0) commit({
25337
+ detail: cached.skill,
25338
+ detailStatus: {
25339
+ ...cached.status,
25340
+ fromCache: true
25341
+ },
25342
+ detailLoading: false
25343
+ });
25344
+ else commit({
24518
25345
  detail: null,
25346
+ detailStatus: null,
24519
25347
  detailLoading: true
24520
25348
  });
24521
- await loadDetail(id);
25349
+ await loadDetail(id, options);
24522
25350
  }
24523
25351
  function closeDetail() {
24524
25352
  detailSequence += 1;
@@ -24528,6 +25356,7 @@ function(property) {
24528
25356
  commit({
24529
25357
  view: { kind: "home" },
24530
25358
  detail: null,
25359
+ detailStatus: null,
24531
25360
  detailLoading: false,
24532
25361
  detailError: null,
24533
25362
  file: null,
@@ -24545,19 +25374,11 @@ function(property) {
24545
25374
  if (first === void 0) return;
24546
25375
  selectFile(first.path);
24547
25376
  }
24548
- /** Load one file of the open skill, cache first and one request per path. */
25377
+ /** Read through the Host's TTL cache, with one in-flight request per path. */
24549
25378
  async function selectFile(path) {
24550
25379
  if (state.view.kind !== "detail") return;
24551
25380
  const id = state.view.id;
24552
25381
  const key = fileCacheKey(id, path);
24553
- const cached = fileCache.get(key);
24554
- if (cached !== void 0) {
24555
- commit({
24556
- file: cached,
24557
- fileLoading: false
24558
- });
24559
- return;
24560
- }
24561
25382
  if (inFlightFileKey === key) return;
24562
25383
  const sequence = ++fileSequence;
24563
25384
  fileAbort?.abort();
@@ -24571,7 +25392,6 @@ function(property) {
24571
25392
  try {
24572
25393
  const file = await fetchSkillFile(id, path, controller.signal);
24573
25394
  if (sequence !== fileSequence) return;
24574
- fileCache.set(key, file);
24575
25395
  commit({
24576
25396
  file,
24577
25397
  fileLoading: false
@@ -24768,10 +25588,16 @@ function(property) {
24768
25588
  }),
24769
25589
  (0, react_jsx_runtime.jsx)("div", {
24770
25590
  className: MarketPage_module_css_default.body,
24771
- children: section === "installed" ? (0, react_jsx_runtime.jsx)(InstalledSkills, { onChanged: () => {
24772
- controller.closeDetail();
24773
- controller.refresh();
24774
- } }) : state.view.kind === "home" ? (0, react_jsx_runtime.jsx)(MarketHome, {
25591
+ children: section === "installed" ? (0, react_jsx_runtime.jsx)(InstalledSkills, {
25592
+ onChanged: () => {
25593
+ controller.closeDetail();
25594
+ controller.refresh();
25595
+ },
25596
+ onOpenMarket: (id) => {
25597
+ setSection("market");
25598
+ controller.openDetail(id);
25599
+ }
25600
+ }) : state.view.kind === "home" ? (0, react_jsx_runtime.jsx)(MarketHome, {
24775
25601
  state,
24776
25602
  controller
24777
25603
  }) : state.detail !== null ? (0, react_jsx_runtime.jsx)(SkillDetailView, {
@@ -24839,14 +25665,19 @@ function(property) {
24839
25665
  */
24840
25666
  function confirmSkillOf(state, id) {
24841
25667
  const skill = (state.detail !== null && state.detail.id === id ? state.detail : null) ?? state.items.find((item) => item.id === id) ?? null;
24842
- if (skill !== null) return {
24843
- id: skill.id,
24844
- name: skill.name,
24845
- source: skill.source,
24846
- version: skill.version,
24847
- securityStatus: skill.securityStatus,
24848
- authorName: skill.author.displayName ?? skill.author.handle
24849
- };
25668
+ if (skill !== null) {
25669
+ const status = state.detail !== null && state.detail.id === id ? state.detailStatus : state.itemStatuses[skill.id];
25670
+ const snapshotAt = status?.fromCache === true ? status.fetchedAt : void 0;
25671
+ return {
25672
+ id: skill.id,
25673
+ name: skill.name,
25674
+ source: skill.source,
25675
+ version: skill.version,
25676
+ securityStatus: skill.securityStatus,
25677
+ authorName: skill.author.displayName ?? skill.author.handle,
25678
+ ...snapshotAt === void 0 ? {} : { snapshotAt }
25679
+ };
25680
+ }
24850
25681
  return {
24851
25682
  id,
24852
25683
  name: id,
@@ -24885,6 +25716,34 @@ function(property) {
24885
25716
  localUninstallDescription: "将删除下方路径的技能及其全部文件。共享 agents 目录中的技能也会影响其他使用它的工具。",
24886
25717
  unlinkDescription: "仅移除此技能的符号链接,保留链接目标及其文件。",
24887
25718
  uninstalling: "正在卸载…",
25719
+ uninstallDisabled: "卸载已在插件配置中禁用。",
25720
+ copyPath: "复制路径",
25721
+ copiedPath: "已复制路径",
25722
+ sourceLabel: "来源",
25723
+ managedByHub: "由 Skills Hub 安装",
25724
+ unmanagedSkill: "手工放置",
25725
+ symlinkSkill: "符号链接",
25726
+ installedAtLabel: "安装时间",
25727
+ skillKind: "形态",
25728
+ bundleSkill: "目录包",
25729
+ flatSkill: "单文件",
25730
+ filesCount: "{count} 个文件",
25731
+ emptyDocument: "这个技能没有正文内容。",
25732
+ installedReadFailed: "读取本地技能失败。",
25733
+ openInMarket: "在市场查看",
25734
+ fileView: "文件视图",
25735
+ filePreview: "预览",
25736
+ fileSource: "源码",
25737
+ snapshotAge: "快照 {age}",
25738
+ ageJustNow: "刚刚",
25739
+ ageMinutes: "{count} 分钟前",
25740
+ ageHours: "{count} 小时前",
25741
+ ageDays: "{count} 天前",
25742
+ refreshNow: "重新获取(跳过缓存)",
25743
+ refreshSnapshot: "重新获取",
25744
+ detailSnapshot: "以下内容来自 {age} 的缓存快照,作者可能已经更新。",
25745
+ installSnapshot: "这里显示的市场数据是 {age} 的快照;点确认后会重新拉取最新清单再安装。",
25746
+ marketProvenance: "这个技能来自技能市场:作者说明、统计与安全报告在市场的详情页里;本地这一份可能已经改动过。",
24888
25747
  panel: "技能市场",
24889
25748
  title: "技能市场",
24890
25749
  subtitle: "浏览、预览并安装来自 ClawHub 与 SkillHub 的第三方技能。",
@@ -24893,6 +25752,7 @@ function(property) {
24893
25752
  "source.all": "全部来源",
24894
25753
  "source.clawhub": "ClawHub",
24895
25754
  "source.skillhub": "SkillHub",
25755
+ "source.local": "本机技能",
24896
25756
  "security.all": "全部安全状态",
24897
25757
  "security.verified": "已验证",
24898
25758
  "security.benign": "无风险",
@@ -24952,6 +25812,7 @@ function(property) {
24952
25812
  uninstallDone: "已卸载",
24953
25813
  noFiles: "这个技能没有可预览的文件。",
24954
25814
  fileTooLarge: "文件过大,无法预览。",
25815
+ fileTruncated: "文件过大,仅显示开头部分。",
24955
25816
  copy: "复制",
24956
25817
  copied: "已复制",
24957
25818
  unoaudited: "未审计"
@@ -24974,6 +25835,34 @@ function(property) {
24974
25835
  localUninstallDescription: "Delete the skill and all files at the path below. Removing shared agents skills also affects other tools using them.",
24975
25836
  unlinkDescription: "Remove only this skill’s symbolic link. The target and its files will be kept.",
24976
25837
  uninstalling: "Uninstalling…",
25838
+ uninstallDisabled: "Uninstalling is disabled by the plugin configuration.",
25839
+ copyPath: "Copy path",
25840
+ copiedPath: "Path copied",
25841
+ sourceLabel: "Source",
25842
+ managedByHub: "Installed by Skills Hub",
25843
+ unmanagedSkill: "Placed by hand",
25844
+ symlinkSkill: "Symlink",
25845
+ installedAtLabel: "Installed",
25846
+ skillKind: "Layout",
25847
+ bundleSkill: "Directory bundle",
25848
+ flatSkill: "Single file",
25849
+ filesCount: "{count} files",
25850
+ emptyDocument: "This skill has no document body.",
25851
+ installedReadFailed: "Could not read the local skill.",
25852
+ openInMarket: "View in marketplace",
25853
+ fileView: "File view",
25854
+ filePreview: "Preview",
25855
+ fileSource: "Source",
25856
+ snapshotAge: "Snapshot {age}",
25857
+ ageJustNow: "just now",
25858
+ ageMinutes: "{count} min ago",
25859
+ ageHours: "{count} h ago",
25860
+ ageDays: "{count} d ago",
25861
+ refreshNow: "Refetch (skip cache)",
25862
+ refreshSnapshot: "Refetch",
25863
+ detailSnapshot: "This content is a cached snapshot from {age}; the author may have published since.",
25864
+ installSnapshot: "The marketplace data shown here is a snapshot from {age}; confirming refetches the current manifest before installing.",
25865
+ marketProvenance: "This skill came from a marketplace: the author description, statistics and security reports live on its marketplace page, while this local copy may have changed since.",
24977
25866
  panel: "Skills Hub",
24978
25867
  title: "Skills Hub",
24979
25868
  subtitle: "Browse, preview and install third-party skills from ClawHub and SkillHub.",
@@ -24982,6 +25871,7 @@ function(property) {
24982
25871
  "source.all": "All sources",
24983
25872
  "source.clawhub": "ClawHub",
24984
25873
  "source.skillhub": "SkillHub",
25874
+ "source.local": "Local skill",
24985
25875
  "security.all": "Any security status",
24986
25876
  "security.verified": "Verified",
24987
25877
  "security.benign": "Benign",
@@ -25041,6 +25931,7 @@ function(property) {
25041
25931
  uninstallDone: "Uninstalled",
25042
25932
  noFiles: "This skill has no previewable files.",
25043
25933
  fileTooLarge: "The file is too large to preview.",
25934
+ fileTruncated: "The file is too large; only the beginning is shown.",
25044
25935
  copy: "Copy",
25045
25936
  copied: "Copied",
25046
25937
  unoaudited: "Not audited"