openclacky 1.3.11 → 1.4.0

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.
@@ -49,6 +49,7 @@
49
49
  "published.confirm": "Unpublish {{id}} from the marketplace?",
50
50
  "err.generic": "Something went wrong: {{msg}}",
51
51
  "err.version_conflict": "Version {{ver}} has already been published. Please enter a higher version number above and try again.",
52
+ "err.invalid_version": "Invalid version. Use format like 1.0.0.",
52
53
  "pub.version.label": "Version",
53
54
 
54
55
  "publish.confirm.title": "Publish to the marketplace?",
@@ -118,6 +119,44 @@
118
119
  "extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
119
120
  "extlist.newExt.btn": "Create New Extension",
120
121
  "extlist.newExt.seed": "I want to build a new OpenClacky extension. Help me get started: ask me what the extension should do, then scaffold it and build it out step by step.",
122
+ "unit.panel": "panel",
123
+ "unit.panels": "panels",
124
+ "unit.agent": "agent",
125
+ "unit.agents": "agents",
126
+ "unit.api": "API",
127
+ "unit.apis": "APIs",
128
+ "unit.skill": "skill",
129
+ "unit.skills": "skills",
130
+
131
+ "meta.section": "Extension Info",
132
+ "meta.edit.title": "Edit Extension Info",
133
+ "meta.edit": "Edit",
134
+ "meta.name.label": "Display name",
135
+ "meta.desc.label": "Description",
136
+ "meta.save": "Save",
137
+ "meta.saving": "Saving…",
138
+ "meta.saved": "Saved",
139
+ "meta.save.err": "Save failed: {{msg}}",
140
+
141
+ "entry.warn.title": "Access entry point not configured",
142
+ "entry.warn.body": "This extension has UI panels but no entry point is set. Users won't know how to open it after installing.",
143
+ "entry.warn.setup": "Set up entry point",
144
+ "entry.warn.skip": "Ignore and continue",
145
+ "entry.hint": "Choose where users can open this extension after installing it.",
146
+ "entry.none": "This extension has no UI panels — no entry points needed.",
147
+ "entry.unset": "Not configured — click Edit to set up.",
148
+ "entry.slot.label": "Slot",
149
+ "entry.kind.panel": "Panel",
150
+ "entry.add": "Add entry point",
151
+ "entry.remove": "Remove",
152
+
153
+ "slot.sidebar.nav": "Sidebar nav",
154
+ "slot.sidebar.nav.top": "Sidebar nav (top)",
155
+ "slot.sidebar.nav.bottom": "Sidebar nav (bottom)",
156
+ "slot.sidebar.footer": "Sidebar footer",
157
+ "slot.main.workspace": "Main workspace",
158
+ "slot.session.aside": "Session aside",
159
+ "slot.settings.tabs": "Settings tab",
121
160
  "skills.section.cloud": "Cloud Skills",
122
161
  "skills.section.cloudHint": "Published to the platform",
123
162
  "skills.section.local": "Local Skills",
@@ -186,6 +225,7 @@
186
225
  "published.confirm": "确定要从市场下架 {{id}} 吗?",
187
226
  "err.generic": "出错了:{{msg}}",
188
227
  "err.version_conflict": "版本 {{ver}} 已发布过,请在上方输入更高的版本号后重试。",
228
+ "err.invalid_version": "版本号格式不对,请使用如 1.0.0 的格式。",
189
229
  "pub.version.label": "版本号",
190
230
 
191
231
  "publish.confirm.title": "确定发布到市场?",
@@ -255,6 +295,45 @@
255
295
  "extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
256
296
  "extlist.newExt.btn": "新建扩展",
257
297
  "extlist.newExt.seed": "我想开发一个新的 OpenClacky 扩展。请引导我开始:先问清楚这个扩展要做什么,然后帮我搭好骨架并一步步开发出来。",
298
+ "unit.panel": "个面板",
299
+ "unit.panels": "个面板",
300
+ "unit.agent": "个 Agent",
301
+ "unit.agents": "个 Agent",
302
+ "unit.api": "个 API",
303
+ "unit.apis": "个 API",
304
+ "unit.skill": "个技能",
305
+ "unit.skills": "个技能",
306
+
307
+ "meta.section": "扩展信息",
308
+ "meta.edit.title": "编辑扩展信息",
309
+ "meta.edit": "编辑",
310
+ "meta.name.label": "显示名称",
311
+ "meta.desc.label": "描述",
312
+ "meta.save": "保存",
313
+ "meta.saving": "保存中…",
314
+ "meta.saved": "已保存",
315
+ "meta.save.err": "保存失败:{{msg}}",
316
+
317
+ "entry.section": "访问入口",
318
+ "entry.warn.title": "未配置访问入口",
319
+ "entry.warn.body": "该扩展包含 UI 面板,但尚未设置访问入口。安装后用户可能不知道如何打开它。",
320
+ "entry.warn.setup": "去设置",
321
+ "entry.warn.skip": "忽略,继续发布",
322
+ "entry.hint": "选择用户安装扩展后可以从哪里打开它。",
323
+ "entry.none": "该扩展没有 UI 面板,无需配置访问入口。",
324
+ "entry.unset": "尚未配置 — 点击编辑设置。",
325
+ "entry.slot.label": "挂载位置",
326
+ "entry.kind.panel": "面板",
327
+ "entry.add": "添加入口",
328
+ "entry.remove": "删除",
329
+
330
+ "slot.sidebar.nav": "侧边栏导航",
331
+ "slot.sidebar.nav.top": "侧边栏导航(顶部)",
332
+ "slot.sidebar.nav.bottom": "侧边栏导航(底部)",
333
+ "slot.sidebar.footer": "侧边栏底部",
334
+ "slot.main.workspace": "主工作区",
335
+ "slot.session.aside": "会话侧栏",
336
+ "slot.settings.tabs": "设置页标签",
258
337
  "skills.section.cloud": "云端 Skills",
259
338
  "skills.section.cloudHint": "已发布到平台",
260
339
  "skills.section.local": "本地 Skills",
@@ -383,7 +462,6 @@
383
462
  box.appendChild(bodyEl);
384
463
  box.appendChild(footer);
385
464
  overlay.appendChild(box);
386
- overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
387
465
  document.body.appendChild(overlay);
388
466
  return { overlay, bodyEl, footer, close };
389
467
  }
@@ -477,14 +555,47 @@
477
555
  }
478
556
  }
479
557
 
558
+ function isSemver(v) {
559
+ return /^\d+\.\d+\.\d+$/.test(v);
560
+ }
561
+
480
562
  // Unified publish flow: one modal that walks the creator from a release form
481
563
  // (version + notes) through progress → device binding (if needed) → done.
482
564
  // The creator owns the version; if it conflicts with the latest published
483
565
  // version they can bump it inline — the modal writes it back to ext.yml
484
566
  // before uploading. `prevVersionOrPromise` may be a value or a Promise.
485
567
  // Resolves true when a publish succeeded, false otherwise.
486
- function runPublishFlow(ext, prevVersionOrPromise) {
568
+ function checkEntryPoints(ext) {
569
+ const hasPanels = (ext.units || []).some((u) => u.kind === "panel");
570
+ const hasEntries = Array.isArray(ext.entry_points) && ext.entry_points.length > 0;
571
+ if (!hasPanels || hasEntries) return Promise.resolve(true);
572
+
487
573
  return new Promise((resolve) => {
574
+ const overlay = el("div", { class: "studio-modal-overlay" });
575
+ const modal = el("div", { class: "studio-modal", style: "max-width:400px" });
576
+ overlay.appendChild(modal);
577
+ document.body.appendChild(overlay);
578
+
579
+ function close(result) { document.body.removeChild(overlay); resolve(result); }
580
+
581
+ modal.appendChild(el("h3", { class: "studio-modal-title", text: t("entry.warn.title") }));
582
+ modal.appendChild(el("p", { style: "font-size:13px;color:var(--color-text-secondary);margin:0 0 20px", text: t("entry.warn.body") }));
583
+
584
+ const footer = el("div", { class: "studio-modal-footer" });
585
+ const skipBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("entry.warn.skip") });
586
+ skipBtn.addEventListener("click", () => close(true));
587
+ const setupBtn = el("button", { class: "studio-btn studio-btn-primary", text: t("entry.warn.setup") });
588
+ setupBtn.addEventListener("click", () => { close(false); openMetaEditModal(ext, () => store.reload()); });
589
+ footer.appendChild(skipBtn);
590
+ footer.appendChild(setupBtn);
591
+ modal.appendChild(footer);
592
+ });
593
+ }
594
+
595
+ function runPublishFlow(ext, prevVersionOrPromise) {
596
+ return checkEntryPoints(ext).then((proceed) => {
597
+ if (!proceed) return false;
598
+ return new Promise((resolve) => {
488
599
  let currentVersion = ext.version || "";
489
600
  let isUpdate = false;
490
601
 
@@ -493,8 +604,15 @@
493
604
  const verInput = el("input", { class: "studio-input", type: "text", value: currentVersion, placeholder: "1.0.0" });
494
605
  verInput.addEventListener("input", () => {
495
606
  currentVersion = verInput.value.trim();
496
- publishBtn.disabled = !currentVersion;
497
- publishBtn.textContent = currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" });
607
+ const valid = isSemver(currentVersion);
608
+ verInput.classList.toggle("studio-input-error", !!currentVersion && !valid);
609
+ publishBtn.disabled = !valid;
610
+ publishBtn.textContent = valid ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: currentVersion || "?" });
611
+ if (currentVersion && !valid) {
612
+ setProgress(t("err.invalid_version"), true);
613
+ } else {
614
+ status.style.display = "none";
615
+ }
498
616
  });
499
617
  verField.appendChild(verInput);
500
618
 
@@ -563,6 +681,13 @@
563
681
  if (done || publishBtn.disabled) return;
564
682
  const ver = currentVersion;
565
683
  if (!ver) return;
684
+ if (!isSemver(ver)) {
685
+ setProgress(t("err.invalid_version", { ver }), true);
686
+ verInput.classList.add("studio-input-error");
687
+ verInput.focus();
688
+ verInput.select();
689
+ return;
690
+ }
566
691
 
567
692
  publishBtn.disabled = true;
568
693
  publishBtn.textContent = t("pub.btn.publishing");
@@ -609,9 +734,8 @@
609
734
  }
610
735
  }
611
736
  });
612
- }
737
+ }); }
613
738
 
614
- // ── Shared: extension list + current selection ─────────────────────────
615
739
  // Both tabs care about "which local extension am I working on", so we keep a
616
740
  // tiny page-level store and let each tab subscribe to changes.
617
741
  const store = {
@@ -642,7 +766,7 @@
642
766
  }
643
767
  const select = el("select", { class: "studio-select" });
644
768
  store.extensions.forEach((e) => {
645
- const opt = el("option", { value: e.id, text: `${e.name} (${e.id})` });
769
+ const opt = el("option", { value: e.id, text: e.name ? `${e.name} (${e.id})` : e.id });
646
770
  if (e.id === store.selectedId) opt.selected = true;
647
771
  select.appendChild(opt);
648
772
  });
@@ -714,29 +838,9 @@
714
838
  }
715
839
 
716
840
  function renderDetail() {
717
- const detail = container.querySelector(".studio-detail");
718
- if (!detail) return;
719
- detail.innerHTML = "";
720
841
  const ext = store.selected();
721
842
  if (!ext) return;
722
-
723
- detail.appendChild(el("h4", { class: "studio-detail-name", text: ext.name }));
724
- if (ext.description) detail.appendChild(el("p", { class: "studio-detail-desc", text: ext.description }));
725
-
726
- const meta = el("dl", { class: "studio-meta" });
727
- const row = (label, value) => { meta.appendChild(el("dt", { text: label })); meta.appendChild(el("dd", { text: value || "—" })); };
728
- row(t("detail.version"), ext.version);
729
- row(t("detail.layer"), ext.layer);
730
- detail.appendChild(meta);
731
-
732
- detail.appendChild(el("div", { class: "studio-label", text: t("detail.units") }));
733
- if (!ext.units.length) {
734
- detail.appendChild(el("p", { class: "studio-empty", text: t("detail.noUnits") }));
735
- } else {
736
- const list = el("div", { class: "studio-units" });
737
- ext.units.forEach((u) => list.appendChild(el("span", { class: "studio-unit-chip", text: `${u.kind} · ${u.id}` })));
738
- detail.appendChild(list);
739
- }
843
+ container.appendChild(buildExtInfoCard(ext, () => openMetaEditModal(ext, () => store.reload().then(rebuild))));
740
844
  }
741
845
 
742
846
  function rebuild() {
@@ -749,7 +853,7 @@
749
853
  return;
750
854
  }
751
855
 
752
- container.appendChild(el("div", { class: "studio-detail" }));
856
+ renderDetail();
753
857
 
754
858
  const bar = el("div", { class: "studio-actions" });
755
859
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-verify-status", text: t("btn.recheck"), onclick: runVerify }));
@@ -759,7 +863,6 @@
759
863
  container.appendChild(el("div", { class: "studio-verify" }));
760
864
  container.appendChild(el("p", { class: "studio-hint", text: t("hint.reload") }));
761
865
 
762
- renderDetail();
763
866
  runVerify();
764
867
  }
765
868
 
@@ -777,6 +880,166 @@
777
880
  }
778
881
 
779
882
  // ── Publish tab ──────────────────────────────────────────────────────────
883
+
884
+ const PANEL_SLOTS = [
885
+ { value: "sidebar.nav", labelKey: "slot.sidebar.nav" },
886
+ { value: "sidebar.nav.top", labelKey: "slot.sidebar.nav.top" },
887
+ { value: "sidebar.nav.bottom", labelKey: "slot.sidebar.nav.bottom" },
888
+ { value: "sidebar.footer", labelKey: "slot.sidebar.footer" },
889
+ { value: "main.workspace", labelKey: "slot.main.workspace" },
890
+ { value: "session.aside", labelKey: "slot.session.aside" },
891
+ { value: "settings.tabs", labelKey: "slot.settings.tabs" },
892
+ ];
893
+
894
+ function buildExtInfoCard(ext, onEdit) {
895
+ const card = el("div", { class: "studio-detail" });
896
+
897
+ const head = el("div", { class: "studio-detail-head" });
898
+ head.appendChild(el("h4", { class: "studio-detail-name", text: ext.name || ext.id }));
899
+ const editBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: t("meta.edit") });
900
+ editBtn.addEventListener("click", onEdit);
901
+ head.appendChild(editBtn);
902
+ card.appendChild(head);
903
+
904
+ if (ext.description) card.appendChild(el("p", { class: "studio-detail-desc", text: ext.description }));
905
+
906
+ const infoRow = el("div", { class: "studio-info-row" });
907
+ infoRow.appendChild(el("span", { class: "studio-info-label", text: t("detail.version") }));
908
+ infoRow.appendChild(el("span", { class: "studio-info-value", text: ext.version || "—" }));
909
+ card.appendChild(infoRow);
910
+
911
+ const panelUnits = (ext.units || []).filter((u) => u.kind === "panel");
912
+ if (panelUnits.length) {
913
+ card.appendChild(el("div", { class: "studio-label studio-section-label", text: t("entry.section") }));
914
+ const eps = Array.isArray(ext.entry_points) && ext.entry_points.length ? ext.entry_points : null;
915
+ if (eps) {
916
+ const chips = el("div", { class: "studio-units" });
917
+ eps.forEach((ep) => {
918
+ const slotDef = PANEL_SLOTS.find((s) => s.value === ep.slot);
919
+ const label = slotDef ? t(slotDef.labelKey) : (ep.slot || ep.kind);
920
+ chips.appendChild(el("span", { class: "studio-unit-chip", text: label }));
921
+ });
922
+ card.appendChild(chips);
923
+ } else {
924
+ card.appendChild(el("p", { class: "studio-empty", text: t("entry.unset") }));
925
+ }
926
+ }
927
+
928
+ card.appendChild(el("div", { class: "studio-label studio-section-label", text: t("detail.units") }));
929
+ if (!(ext.units || []).length) {
930
+ card.appendChild(el("p", { class: "studio-empty", text: t("detail.noUnits") }));
931
+ } else {
932
+ const list = el("div", { class: "studio-units" });
933
+ ext.units.forEach((u) => list.appendChild(el("span", { class: "studio-unit-chip", text: u.kind })));
934
+ card.appendChild(list);
935
+ }
936
+
937
+ return card;
938
+ }
939
+
940
+ function openMetaEditModal(ext, onSaved) {
941
+ let entries = Array.isArray(ext.entry_points) ? ext.entry_points.map((ep) => ({ ...ep })) : [];
942
+ const panelUnits = (ext.units || []).filter((u) => u.kind === "panel");
943
+
944
+ const overlay = el("div", { class: "studio-modal-overlay" });
945
+ overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
946
+
947
+ const modal = el("div", { class: "studio-modal" });
948
+ overlay.appendChild(modal);
949
+ document.body.appendChild(overlay);
950
+
951
+ function close() { document.body.removeChild(overlay); }
952
+
953
+ modal.appendChild(el("h3", { class: "studio-modal-title", text: t("meta.edit.title") }));
954
+
955
+ const nameField = el("div", { class: "studio-field" });
956
+ nameField.appendChild(el("label", { class: "studio-label", text: t("meta.name.label") }));
957
+ const nameInput = el("input", { class: "studio-input", value: ext.name || "" });
958
+ nameField.appendChild(nameInput);
959
+ modal.appendChild(nameField);
960
+
961
+ const descField = el("div", { class: "studio-field" });
962
+ descField.appendChild(el("label", { class: "studio-label", text: t("meta.desc.label") }));
963
+ const descInput = el("textarea", { class: "studio-textarea", rows: "5", text: ext.description || "" });
964
+ descField.appendChild(descInput);
965
+ modal.appendChild(descField);
966
+
967
+ if (panelUnits.length) {
968
+ const entryField = el("div", { class: "studio-field" });
969
+ entryField.appendChild(el("label", { class: "studio-label", text: t("entry.section") }));
970
+ entryField.appendChild(el("p", { class: "studio-hint", style: "margin:0 0 8px", text: t("entry.hint") }));
971
+
972
+ const listEl = el("div", { class: "studio-entry-list" });
973
+
974
+ function renderEntryList() {
975
+ listEl.innerHTML = "";
976
+ entries.forEach((ep, idx) => {
977
+ const row = el("div", { class: "studio-entry-row" });
978
+
979
+ const slotSel = el("select", { class: "studio-select" });
980
+ PANEL_SLOTS.forEach(({ value, labelKey }) => {
981
+ const opt = el("option", { value, text: t(labelKey) });
982
+ if (ep.slot === value) opt.selected = true;
983
+ slotSel.appendChild(opt);
984
+ });
985
+ if (!ep.slot) entries[idx].slot = PANEL_SLOTS[0].value;
986
+ slotSel.addEventListener("change", () => { entries[idx].slot = slotSel.value; });
987
+
988
+ const removeBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: "✕" });
989
+ removeBtn.title = t("entry.remove");
990
+ removeBtn.addEventListener("click", () => { entries.splice(idx, 1); renderEntryList(); });
991
+
992
+ row.appendChild(slotSel);
993
+ row.appendChild(removeBtn);
994
+ listEl.appendChild(row);
995
+ });
996
+
997
+ const addBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: `+ ${t("entry.add")}` });
998
+ addBtn.addEventListener("click", () => {
999
+ const du = panelUnits[0];
1000
+ entries.push({ panel_id: du ? du.id : "", slot: PANEL_SLOTS[0].value });
1001
+ renderEntryList();
1002
+ });
1003
+ listEl.appendChild(addBtn);
1004
+ }
1005
+
1006
+ renderEntryList();
1007
+ entryField.appendChild(listEl);
1008
+ modal.appendChild(entryField);
1009
+ }
1010
+
1011
+ const statusEl = el("span", { class: "studio-form-status" });
1012
+ const footer = el("div", { class: "studio-modal-footer" });
1013
+ const cancelBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("pub.btn.cancel"), onclick: close });
1014
+ const saveBtn = el("button", { class: "studio-btn studio-btn-primary", text: t("meta.save") });
1015
+
1016
+ saveBtn.addEventListener("click", async () => {
1017
+ saveBtn.disabled = true;
1018
+ saveBtn.textContent = t("meta.saving");
1019
+ statusEl.textContent = "";
1020
+ try {
1021
+ const payload = entries.map((ep) => ({ unit_id: ep.panel_id, slot: ep.slot }));
1022
+ await postJson("/set_meta", {
1023
+ ext_id: ext.id,
1024
+ name: nameInput.value.trim(),
1025
+ description: descInput.value.trim(),
1026
+ entry_points: payload,
1027
+ });
1028
+ close();
1029
+ if (onSaved) onSaved();
1030
+ } catch (e) {
1031
+ saveBtn.disabled = false;
1032
+ saveBtn.textContent = t("meta.save");
1033
+ statusEl.textContent = t("meta.save.err", { msg: e.message });
1034
+ }
1035
+ });
1036
+
1037
+ footer.appendChild(statusEl);
1038
+ footer.appendChild(cancelBtn);
1039
+ footer.appendChild(saveBtn);
1040
+ modal.appendChild(footer);
1041
+ }
1042
+
780
1043
  function createPublishPanel() {
781
1044
  let container = null;
782
1045
  let unsub = null;
@@ -814,13 +1077,27 @@
814
1077
  }
815
1078
  }
816
1079
 
1080
+ function renderPublishedSkeleton(box) {
1081
+ const card = el("div", { class: "studio-skill-card studio-skeleton-card" });
1082
+ const head = el("div", { class: "studio-skill-head" });
1083
+ head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-name" }));
1084
+ head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-badge" }));
1085
+ head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-ver" }));
1086
+ head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-btn" }));
1087
+ card.appendChild(head);
1088
+ box.appendChild(card);
1089
+ }
1090
+
817
1091
  async function loadPublished() {
818
1092
  const box = container.querySelector(".studio-published");
819
1093
  if (!box) return;
820
1094
  box.innerHTML = "";
821
1095
  box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
1096
+ renderPublishedSkeleton(box);
822
1097
  try {
823
1098
  const data = await getJson("/published");
1099
+ box.innerHTML = "";
1100
+ box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
824
1101
  const exts = data.extensions || [];
825
1102
  if (!exts.length) { box.appendChild(el("p", { class: "studio-empty", text: t("published.empty") })); return; }
826
1103
  exts.forEach((e) => {
@@ -830,7 +1107,7 @@
830
1107
  const statusKind = e.status === "draft" ? "local" : "published";
831
1108
  head.appendChild(el("span", { class: `studio-skill-badge studio-skill-badge-${statusKind}`, text: e.status === "draft" ? t("extlist.badge.draft") : t("extlist.badge.published") }));
832
1109
  if (e.version) head.appendChild(el("span", { style: "font-size:11px;color:var(--color-text-muted);", text: "v" + e.version }));
833
- const unpubBtn = el("button", { class: "studio-btn studio-btn-danger", text: t("btn.unpublish") });
1110
+ const unpubBtn = el("button", { class: "studio-btn studio-btn-danger studio-btn-sm", text: t("btn.unpublish") });
834
1111
  unpubBtn.style.marginLeft = "auto";
835
1112
  unpubBtn.addEventListener("click", async () => {
836
1113
  if (!window.confirm(t("published.confirm", { id: e.id }))) return;
@@ -843,6 +1120,8 @@
843
1120
  box.appendChild(card);
844
1121
  });
845
1122
  } catch (e) {
1123
+ box.innerHTML = "";
1124
+ box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
846
1125
  box.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) }));
847
1126
  }
848
1127
  }
@@ -852,7 +1131,10 @@
852
1131
  container.innerHTML = "";
853
1132
  container.appendChild(renderPicker(rebuild));
854
1133
 
855
- if (!store.selected()) return;
1134
+ const ext = store.selected();
1135
+ if (!ext) return;
1136
+
1137
+ container.appendChild(buildExtInfoCard(ext, () => openMetaEditModal(ext, () => store.reload().then(rebuild))));
856
1138
 
857
1139
  const bar = el("div", { class: "studio-actions" });
858
1140
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish }));
@@ -928,6 +1210,15 @@
928
1210
  return el("span", { class: "studio-skill-badge studio-skill-badge-" + kind, text });
929
1211
  }
930
1212
 
1213
+ function formatUnitCounts(counts) {
1214
+ if (!counts || typeof counts !== "object") return "";
1215
+ return Object.keys(counts).map((kind) => {
1216
+ const n = parseInt(counts[kind], 10);
1217
+ if (!n) return null;
1218
+ return `${n} ${t("unit." + kind + (n > 1 ? "s" : ""))}`;
1219
+ }).filter(Boolean).join(" · ");
1220
+ }
1221
+
931
1222
  function cloudCard(ext) {
932
1223
  const card = el("div", { class: "studio-skill-card" });
933
1224
  const head = el("div", { class: "studio-skill-head" });
@@ -942,7 +1233,7 @@
942
1233
 
943
1234
  const actions = el("div", { class: "studio-actions" });
944
1235
  actions.appendChild(el("button", { class: "studio-btn studio-btn-primary", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
945
- const un = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.unpublish") });
1236
+ const un = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: t("extlist.btn.unpublish") });
946
1237
  un.addEventListener("click", async () => {
947
1238
  if (!window.confirm(t("extlist.unpublish.confirm", { id: ext.id }))) return;
948
1239
  un.disabled = true;
@@ -969,13 +1260,14 @@
969
1260
 
970
1261
  const meta = el("div", { class: "studio-skill-meta" });
971
1262
  if (ext.version) meta.appendChild(el("span", { text: "v" + ext.version }));
972
- if (ext.units && ext.units.length) meta.appendChild(el("span", { text: ext.units.map((u) => u.kind).join(" · ") }));
1263
+ const unitsText = formatUnitCounts(ext.unit_counts);
1264
+ if (unitsText) meta.appendChild(el("span", { text: unitsText }));
973
1265
  card.appendChild(meta);
974
1266
 
975
1267
  const actions = el("div", { class: "studio-actions" });
976
1268
  const pub = el("button", { class: "studio-btn studio-btn-primary", text: published ? t("extlist.btn.update") : t("extlist.btn.publish") });
977
- pub.disabled = !licensed() || !!ext.error_count;
978
- pub.title = licensed() ? (ext.error_count ? t("extlist.verify.errors", { n: ext.error_count }) : "") : t("extlist.needLicense");
1269
+ pub.disabled = !!ext.error_count;
1270
+ pub.title = ext.error_count ? t("extlist.verify.errors", { n: ext.error_count }) : "";
979
1271
  pub.addEventListener("click", () => doPublish(ext, cloudEntry ? cloudEntry.version : null));
980
1272
  actions.appendChild(pub);
981
1273
 
@@ -1333,18 +1625,16 @@
1333
1625
  .studio-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
1334
1626
  .studio-label { font-size: 12px; font-weight: 500; color: var(--color-text-secondary); }
1335
1627
  .studio-select, .studio-textarea { width: 100%; box-sizing: border-box; background: var(--color-bg-input); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 7px 8px; color: var(--color-text-primary); font-size: 13px; font-family: inherit; }
1628
+ .studio-select { appearance: none; -webkit-appearance: none; padding-right: 28px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; }
1336
1629
  .studio-select:focus, .studio-textarea:focus { border-color: var(--color-accent-primary); outline: none; }
1337
1630
  .studio-empty { color: var(--color-text-muted); font-size: 12px; margin: 4px 0; }
1338
1631
  .studio-hint { color: var(--color-text-tertiary); font-size: 12px; line-height: 1.5; margin: 10px 0 0; }
1339
1632
  .studio-detail { border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 12px; margin-bottom: 14px; background: var(--color-bg-secondary); }
1340
- .studio-detail-name { margin: 0 0 4px; font-size: 14px; color: var(--color-text-primary); }
1341
- .studio-detail-desc { margin: 0 0 10px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1342
- .studio-meta { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0 0 10px; font-size: 12px; }
1343
- .studio-meta dt { color: var(--color-text-tertiary); }
1344
- .studio-meta dd { margin: 0; color: var(--color-text-primary); }
1633
+ .studio-detail-name { margin: 0; font-size: 14px; color: var(--color-text-primary); }
1634
+ .studio-detail-desc { margin: 4px 0 0; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1345
1635
  .studio-units { display: flex; flex-wrap: wrap; gap: 6px; }
1346
1636
  .studio-unit-chip { display: inline-flex; background: var(--color-bg-hover); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 2px 8px; font-size: 12px; }
1347
- .studio-actions { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
1637
+ .studio-actions { display: flex; align-items: center; gap: 8px; margin: 12px 0 0; }
1348
1638
  .studio-skill-promo .studio-actions { margin: 12px 0 0; }
1349
1639
  .studio-btn { padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid var(--color-border-primary); background: transparent; color: var(--color-text-secondary); cursor: pointer; font-size: 13px; font-weight: 500; }
1350
1640
  .studio-btn:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
@@ -1373,6 +1663,12 @@
1373
1663
  .studio-feedback-error { color: var(--color-error); }
1374
1664
  .studio-feedback-warn { color: var(--color-warning, var(--color-text-secondary)); }
1375
1665
  .studio-published { margin-top: 16px; border-top: 1px solid var(--color-border-primary); padding-top: 12px; }
1666
+ @keyframes studio-shimmer { 0% { background-position: -200px 0; } 100% { background-position: calc(200px + 100%) 0; } }
1667
+ .studio-skeleton { display: inline-block; border-radius: 4px; background: linear-gradient(90deg, var(--color-bg-hover) 25%, var(--color-border-primary) 50%, var(--color-bg-hover) 75%); background-size: 200px 100%; animation: studio-shimmer 1.4s infinite; }
1668
+ .studio-skeleton-name { width: 120px; height: 14px; }
1669
+ .studio-skeleton-badge { width: 44px; height: 16px; border-radius: 10px; }
1670
+ .studio-skeleton-ver { width: 36px; height: 12px; }
1671
+ .studio-skeleton-btn { width: 44px; height: 26px; margin-left: auto; border-radius: 6px; }
1376
1672
  .studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 0; font-size: 12px; border-bottom: 1px solid var(--color-border-primary); }
1377
1673
  .studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
1378
1674
  .studio-page { width: 100%; }
@@ -1395,8 +1691,8 @@
1395
1691
  .studio-skill-badge-local { background: var(--color-bg-hover); color: var(--color-text-tertiary); }
1396
1692
  .studio-skill-badge-changed { background: var(--color-warning-bg, var(--color-bg-hover)); color: var(--color-warning, var(--color-text-secondary)); }
1397
1693
  .studio-skill-badge-shadow { background: var(--color-bg-hover); color: var(--color-text-secondary); }
1398
- .studio-skill-desc { margin: 6px 0 8px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1399
- .studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); }
1694
+ .studio-skill-desc { margin: 6px 0 6px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1695
+ .studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); margin-top: 6px; }
1400
1696
  .studio-skill-promo { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 16px 18px; margin: 0 0 24px; background: var(--color-bg-secondary); }
1401
1697
  .studio-skill-promo-text { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
1402
1698
  .studio-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; animation: studio-overlay-in 0.15s ease; }
@@ -1419,6 +1715,21 @@
1419
1715
  .studio-modal-link { display: inline-block; font-size: 13px; color: var(--color-accent-primary); }
1420
1716
  .studio-modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--color-border-primary); }
1421
1717
  .studio-pub-success { margin-right: auto; font-size: 13px; font-weight: 500; color: var(--color-success); }
1718
+ .studio-section { margin-bottom: 20px; }
1719
+ .studio-detail-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
1720
+ .studio-info-row { display: flex; align-items: baseline; gap: 8px; margin: 8px 0 0; }
1721
+ .studio-info-label { font-size: 12px; color: var(--color-text-tertiary); flex-shrink: 0; }
1722
+ .studio-info-value { font-size: 13px; color: var(--color-text-primary); }
1723
+ .studio-section-label { margin-top: 10px; margin-bottom: 6px; }
1724
+ .studio-detail-head .studio-detail-name { margin: 0; }
1725
+ .studio-form-status { font-size: 12px; color: var(--color-text-tertiary); }
1726
+ .studio-entry-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
1727
+ .studio-entry-row { display: flex; align-items: stretch; gap: 8px; }
1728
+ .studio-entry-arrow { font-size: 12px; color: var(--color-text-tertiary); flex-shrink: 0; }
1729
+ .studio-entry-row .studio-select { flex: 1; width: auto; padding: 6px 28px 6px 8px; }
1730
+ .studio-entry-row .studio-select:focus { border-color: var(--color-accent-primary); outline: none; }
1731
+ .studio-btn-sm { padding: 4px 10px; font-size: 12px; }
1732
+ .studio-entry-row .studio-btn-sm { padding: 0 10px; }
1422
1733
  `;
1423
1734
  document.head.appendChild(style);
1424
1735
  })();