openclacky 1.3.10 → 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?",
@@ -100,12 +101,14 @@
100
101
  "extlist.verify.ok": "Checks passed",
101
102
  "extlist.verify.errors": "{{n}} error(s)",
102
103
  "extlist.verify.warnings": "{{n}} warning(s)",
103
- "extlist.btn.publish": "Publish",
104
+ "extlist.btn.publish": "Publish to Marketplace",
104
105
  "extlist.btn.update": "Update",
105
- "extlist.btn.pack": "Pack",
106
+ "extlist.btn.pack": "Pack (.zip)",
106
107
  "extlist.btn.packing": "Packing…",
107
108
  "extlist.btn.unpublish": "Unpublish",
108
109
  "extlist.btn.iterate": "Iterate",
110
+ "extlist.btn.delete": "Delete",
111
+ "extlist.delete.confirm": "Delete local extension \"{{id}}\"? This cannot be undone.",
109
112
  "extlist.iterate.seed": "Iterate on extension {{id}}",
110
113
  "extlist.changelog.prompt": "Changelog (optional):",
111
114
  "extlist.overwrite.confirm": "\"{{id}}\" is already published. Publish a new version?",
@@ -116,6 +119,44 @@
116
119
  "extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
117
120
  "extlist.newExt.btn": "Create New Extension",
118
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",
119
160
  "skills.section.cloud": "Cloud Skills",
120
161
  "skills.section.cloudHint": "Published to the platform",
121
162
  "skills.section.local": "Local Skills",
@@ -184,6 +225,7 @@
184
225
  "published.confirm": "确定要从市场下架 {{id}} 吗?",
185
226
  "err.generic": "出错了:{{msg}}",
186
227
  "err.version_conflict": "版本 {{ver}} 已发布过,请在上方输入更高的版本号后重试。",
228
+ "err.invalid_version": "版本号格式不对,请使用如 1.0.0 的格式。",
187
229
  "pub.version.label": "版本号",
188
230
 
189
231
  "publish.confirm.title": "确定发布到市场?",
@@ -235,12 +277,14 @@
235
277
  "extlist.verify.ok": "检查通过",
236
278
  "extlist.verify.errors": "{{n}} 个错误",
237
279
  "extlist.verify.warnings": "{{n}} 个警告",
238
- "extlist.btn.publish": "发布",
239
- "extlist.btn.update": "更新",
240
- "extlist.btn.pack": "打包",
280
+ "extlist.btn.publish": "发布到市场",
281
+ "extlist.btn.update": "更新到市场",
282
+ "extlist.btn.pack": "打包(.zip)",
241
283
  "extlist.btn.packing": "打包中…",
242
284
  "extlist.btn.unpublish": "下架",
243
285
  "extlist.btn.iterate": "迭代",
286
+ "extlist.btn.delete": "删除",
287
+ "extlist.delete.confirm": "确定要删除本地扩展「{{id}}」吗?此操作不可恢复。",
244
288
  "extlist.iterate.seed": "迭代扩展 {{id}}",
245
289
  "extlist.changelog.prompt": "更新说明(可选):",
246
290
  "extlist.overwrite.confirm": "「{{id}}」已经发布过了。要发布新版本吗?",
@@ -251,6 +295,45 @@
251
295
  "extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
252
296
  "extlist.newExt.btn": "新建扩展",
253
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": "设置页标签",
254
337
  "skills.section.cloud": "云端 Skills",
255
338
  "skills.section.cloudHint": "已发布到平台",
256
339
  "skills.section.local": "本地 Skills",
@@ -379,7 +462,6 @@
379
462
  box.appendChild(bodyEl);
380
463
  box.appendChild(footer);
381
464
  overlay.appendChild(box);
382
- overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
383
465
  document.body.appendChild(overlay);
384
466
  return { overlay, bodyEl, footer, close };
385
467
  }
@@ -473,14 +555,47 @@
473
555
  }
474
556
  }
475
557
 
558
+ function isSemver(v) {
559
+ return /^\d+\.\d+\.\d+$/.test(v);
560
+ }
561
+
476
562
  // Unified publish flow: one modal that walks the creator from a release form
477
563
  // (version + notes) through progress → device binding (if needed) → done.
478
564
  // The creator owns the version; if it conflicts with the latest published
479
565
  // version they can bump it inline — the modal writes it back to ext.yml
480
566
  // before uploading. `prevVersionOrPromise` may be a value or a Promise.
481
567
  // Resolves true when a publish succeeded, false otherwise.
482
- 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
+
483
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) => {
484
599
  let currentVersion = ext.version || "";
485
600
  let isUpdate = false;
486
601
 
@@ -489,8 +604,15 @@
489
604
  const verInput = el("input", { class: "studio-input", type: "text", value: currentVersion, placeholder: "1.0.0" });
490
605
  verInput.addEventListener("input", () => {
491
606
  currentVersion = verInput.value.trim();
492
- publishBtn.disabled = !currentVersion;
493
- 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
+ }
494
616
  });
495
617
  verField.appendChild(verInput);
496
618
 
@@ -559,6 +681,13 @@
559
681
  if (done || publishBtn.disabled) return;
560
682
  const ver = currentVersion;
561
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
+ }
562
691
 
563
692
  publishBtn.disabled = true;
564
693
  publishBtn.textContent = t("pub.btn.publishing");
@@ -605,9 +734,8 @@
605
734
  }
606
735
  }
607
736
  });
608
- }
737
+ }); }
609
738
 
610
- // ── Shared: extension list + current selection ─────────────────────────
611
739
  // Both tabs care about "which local extension am I working on", so we keep a
612
740
  // tiny page-level store and let each tab subscribe to changes.
613
741
  const store = {
@@ -638,7 +766,7 @@
638
766
  }
639
767
  const select = el("select", { class: "studio-select" });
640
768
  store.extensions.forEach((e) => {
641
- 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 });
642
770
  if (e.id === store.selectedId) opt.selected = true;
643
771
  select.appendChild(opt);
644
772
  });
@@ -710,29 +838,9 @@
710
838
  }
711
839
 
712
840
  function renderDetail() {
713
- const detail = container.querySelector(".studio-detail");
714
- if (!detail) return;
715
- detail.innerHTML = "";
716
841
  const ext = store.selected();
717
842
  if (!ext) return;
718
-
719
- detail.appendChild(el("h4", { class: "studio-detail-name", text: ext.name }));
720
- if (ext.description) detail.appendChild(el("p", { class: "studio-detail-desc", text: ext.description }));
721
-
722
- const meta = el("dl", { class: "studio-meta" });
723
- const row = (label, value) => { meta.appendChild(el("dt", { text: label })); meta.appendChild(el("dd", { text: value || "—" })); };
724
- row(t("detail.version"), ext.version);
725
- row(t("detail.layer"), ext.layer);
726
- detail.appendChild(meta);
727
-
728
- detail.appendChild(el("div", { class: "studio-label", text: t("detail.units") }));
729
- if (!ext.units.length) {
730
- detail.appendChild(el("p", { class: "studio-empty", text: t("detail.noUnits") }));
731
- } else {
732
- const list = el("div", { class: "studio-units" });
733
- ext.units.forEach((u) => list.appendChild(el("span", { class: "studio-unit-chip", text: `${u.kind} · ${u.id}` })));
734
- detail.appendChild(list);
735
- }
843
+ container.appendChild(buildExtInfoCard(ext, () => openMetaEditModal(ext, () => store.reload().then(rebuild))));
736
844
  }
737
845
 
738
846
  function rebuild() {
@@ -745,7 +853,7 @@
745
853
  return;
746
854
  }
747
855
 
748
- container.appendChild(el("div", { class: "studio-detail" }));
856
+ renderDetail();
749
857
 
750
858
  const bar = el("div", { class: "studio-actions" });
751
859
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-verify-status", text: t("btn.recheck"), onclick: runVerify }));
@@ -755,7 +863,6 @@
755
863
  container.appendChild(el("div", { class: "studio-verify" }));
756
864
  container.appendChild(el("p", { class: "studio-hint", text: t("hint.reload") }));
757
865
 
758
- renderDetail();
759
866
  runVerify();
760
867
  }
761
868
 
@@ -773,6 +880,166 @@
773
880
  }
774
881
 
775
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
+
776
1043
  function createPublishPanel() {
777
1044
  let container = null;
778
1045
  let unsub = null;
@@ -810,13 +1077,27 @@
810
1077
  }
811
1078
  }
812
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
+
813
1091
  async function loadPublished() {
814
1092
  const box = container.querySelector(".studio-published");
815
1093
  if (!box) return;
816
1094
  box.innerHTML = "";
817
1095
  box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
1096
+ renderPublishedSkeleton(box);
818
1097
  try {
819
1098
  const data = await getJson("/published");
1099
+ box.innerHTML = "";
1100
+ box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
820
1101
  const exts = data.extensions || [];
821
1102
  if (!exts.length) { box.appendChild(el("p", { class: "studio-empty", text: t("published.empty") })); return; }
822
1103
  exts.forEach((e) => {
@@ -826,7 +1107,7 @@
826
1107
  const statusKind = e.status === "draft" ? "local" : "published";
827
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") }));
828
1109
  if (e.version) head.appendChild(el("span", { style: "font-size:11px;color:var(--color-text-muted);", text: "v" + e.version }));
829
- 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") });
830
1111
  unpubBtn.style.marginLeft = "auto";
831
1112
  unpubBtn.addEventListener("click", async () => {
832
1113
  if (!window.confirm(t("published.confirm", { id: e.id }))) return;
@@ -839,6 +1120,8 @@
839
1120
  box.appendChild(card);
840
1121
  });
841
1122
  } catch (e) {
1123
+ box.innerHTML = "";
1124
+ box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
842
1125
  box.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) }));
843
1126
  }
844
1127
  }
@@ -848,7 +1131,10 @@
848
1131
  container.innerHTML = "";
849
1132
  container.appendChild(renderPicker(rebuild));
850
1133
 
851
- 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))));
852
1138
 
853
1139
  const bar = el("div", { class: "studio-actions" });
854
1140
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish }));
@@ -924,6 +1210,15 @@
924
1210
  return el("span", { class: "studio-skill-badge studio-skill-badge-" + kind, text });
925
1211
  }
926
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
+
927
1222
  function cloudCard(ext) {
928
1223
  const card = el("div", { class: "studio-skill-card" });
929
1224
  const head = el("div", { class: "studio-skill-head" });
@@ -938,7 +1233,7 @@
938
1233
 
939
1234
  const actions = el("div", { class: "studio-actions" });
940
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 })) }));
941
- 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") });
942
1237
  un.addEventListener("click", async () => {
943
1238
  if (!window.confirm(t("extlist.unpublish.confirm", { id: ext.id }))) return;
944
1239
  un.disabled = true;
@@ -965,13 +1260,14 @@
965
1260
 
966
1261
  const meta = el("div", { class: "studio-skill-meta" });
967
1262
  if (ext.version) meta.appendChild(el("span", { text: "v" + ext.version }));
968
- 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 }));
969
1265
  card.appendChild(meta);
970
1266
 
971
1267
  const actions = el("div", { class: "studio-actions" });
972
1268
  const pub = el("button", { class: "studio-btn studio-btn-primary", text: published ? t("extlist.btn.update") : t("extlist.btn.publish") });
973
- pub.disabled = !licensed() || !!ext.error_count;
974
- 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 }) : "";
975
1271
  pub.addEventListener("click", () => doPublish(ext, cloudEntry ? cloudEntry.version : null));
976
1272
  actions.appendChild(pub);
977
1273
 
@@ -980,6 +1276,12 @@
980
1276
  packBtn.addEventListener("click", () => doPack(ext, packBtn));
981
1277
  actions.appendChild(packBtn);
982
1278
  card.appendChild(actions);
1279
+
1280
+ if (!published) {
1281
+ const delBtn = el("button", { class: "studio-btn studio-btn-danger", text: t("extlist.btn.delete") });
1282
+ delBtn.addEventListener("click", () => doDelete(ext, delBtn));
1283
+ actions.appendChild(delBtn);
1284
+ }
983
1285
  return card;
984
1286
  }
985
1287
 
@@ -995,6 +1297,25 @@
995
1297
  }
996
1298
  }
997
1299
 
1300
+ async function doDelete(ext, btn) {
1301
+ if (!confirm(t("extlist.delete.confirm", { id: ext.id }))) return;
1302
+ btn.disabled = true;
1303
+ try {
1304
+ const res = await fetch("/api/ext/ext-studio/local", {
1305
+ method: "DELETE",
1306
+ headers: { "Content-Type": "application/json" },
1307
+ body: JSON.stringify({ ext_id: ext.id }),
1308
+ });
1309
+ let data = {};
1310
+ try { data = await res.json(); } catch (_e) {}
1311
+ if (!res.ok) throw new Error(data?.error || `Error ${res.status}`);
1312
+ await reload(); rebuild();
1313
+ } catch (e) {
1314
+ alert(t("err.generic", { msg: e.message }));
1315
+ btn.disabled = false;
1316
+ }
1317
+ }
1318
+
998
1319
  async function doPublish(ext, prevVersion) {
999
1320
  const ok = await runPublishFlow(ext, prevVersion);
1000
1321
  if (ok) { await reload(); rebuild(); }
@@ -1304,25 +1625,23 @@
1304
1625
  .studio-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
1305
1626
  .studio-label { font-size: 12px; font-weight: 500; color: var(--color-text-secondary); }
1306
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; }
1307
1629
  .studio-select:focus, .studio-textarea:focus { border-color: var(--color-accent-primary); outline: none; }
1308
1630
  .studio-empty { color: var(--color-text-muted); font-size: 12px; margin: 4px 0; }
1309
1631
  .studio-hint { color: var(--color-text-tertiary); font-size: 12px; line-height: 1.5; margin: 10px 0 0; }
1310
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); }
1311
- .studio-detail-name { margin: 0 0 4px; font-size: 14px; color: var(--color-text-primary); }
1312
- .studio-detail-desc { margin: 0 0 10px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1313
- .studio-meta { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0 0 10px; font-size: 12px; }
1314
- .studio-meta dt { color: var(--color-text-tertiary); }
1315
- .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; }
1316
1635
  .studio-units { display: flex; flex-wrap: wrap; gap: 6px; }
1317
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; }
1318
- .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; }
1319
1638
  .studio-skill-promo .studio-actions { margin: 12px 0 0; }
1320
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; }
1321
1640
  .studio-btn:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
1322
1641
  .studio-btn-primary { background: var(--color-button-primary); color: var(--color-button-primary-text); border-color: transparent; }
1323
1642
  .studio-btn-primary:hover { background: var(--color-button-primary-hover); color: var(--color-button-primary-text); }
1324
1643
  .studio-btn-primary:disabled { opacity: 0.6; cursor: default; }
1325
- .studio-btn-danger { color: var(--color-error); border-color: var(--color-error-border); padding: 4px 10px; }
1644
+ .studio-btn-danger { color: var(--color-error); border-color: var(--color-error-border); }
1326
1645
  .studio-btn-danger:hover { background: var(--color-error-bg); color: var(--color-error); }
1327
1646
  .studio-btn-ghost { color: var(--color-text-secondary); font-weight: 500; }
1328
1647
  .studio-btn-ghost:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
@@ -1344,6 +1663,12 @@
1344
1663
  .studio-feedback-error { color: var(--color-error); }
1345
1664
  .studio-feedback-warn { color: var(--color-warning, var(--color-text-secondary)); }
1346
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; }
1347
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); }
1348
1673
  .studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
1349
1674
  .studio-page { width: 100%; }
@@ -1366,8 +1691,8 @@
1366
1691
  .studio-skill-badge-local { background: var(--color-bg-hover); color: var(--color-text-tertiary); }
1367
1692
  .studio-skill-badge-changed { background: var(--color-warning-bg, var(--color-bg-hover)); color: var(--color-warning, var(--color-text-secondary)); }
1368
1693
  .studio-skill-badge-shadow { background: var(--color-bg-hover); color: var(--color-text-secondary); }
1369
- .studio-skill-desc { margin: 6px 0 8px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1370
- .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; }
1371
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); }
1372
1697
  .studio-skill-promo-text { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
1373
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; }
@@ -1390,6 +1715,21 @@
1390
1715
  .studio-modal-link { display: inline-block; font-size: 13px; color: var(--color-accent-primary); }
1391
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); }
1392
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; }
1393
1733
  `;
1394
1734
  document.head.appendChild(style);
1395
1735
  })();