openclacky 1.3.8 → 1.3.10

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.
@@ -29,7 +29,7 @@
29
29
  "verify.errors": "{{n}} error(s)",
30
30
  "verify.warnings": "{{n}} warning(s)",
31
31
  "verify.hint": "Hint",
32
- "hint.reload": "After fixing, reload this page extensions hot-reload per request.",
32
+ "hint.reload": "After fixing, reload this page to apply changes.",
33
33
  "publish.status": "Status",
34
34
  "publish.status.draft": "Draft",
35
35
  "publish.status.published": "Published",
@@ -39,18 +39,20 @@
39
39
  "btn.publish": "Publish to marketplace",
40
40
  "btn.publishing": "Publishing…",
41
41
  "btn.pack": "Pack (.zip)",
42
+ "btn.packing": "Packing…",
42
43
  "publish.needLicense": "Publishing requires an activated user license.",
43
44
  "publish.done": "Published {{id}} {{ver}} — {{status}}",
44
- "publish.packed": "Packed → {{path}}",
45
45
  "publish.already": "Already published. Enable \"publish a new version\" to ship.",
46
46
  "published.title": "Your published extensions",
47
47
  "published.empty": "You haven't published anything yet.",
48
48
  "btn.unpublish": "Unpublish",
49
49
  "published.confirm": "Unpublish {{id}} from the marketplace?",
50
50
  "err.generic": "Something went wrong: {{msg}}",
51
+ "err.version_conflict": "Version {{ver}} has already been published. Please enter a higher version number above and try again.",
52
+ "pub.version.label": "Version",
51
53
 
52
54
  "publish.confirm.title": "Publish to the marketplace?",
53
- "publish.confirm.body": "\"{{id}}\" will be packed and uploaded to the OpenClacky marketplace, where anyone can discover and install it. Make sure it's ready to share publicly.",
55
+ "publish.confirm.body": "\"{{id}}\" will be packed and uploaded to the extension marketplace, where anyone can discover and install it. Make sure it's ready to share publicly.",
54
56
  "publish.confirm.ok": "Publish",
55
57
  "publish.confirm.cancel": "Cancel",
56
58
  "bind.title": "Authorize this device",
@@ -67,7 +69,7 @@
67
69
 
68
70
  "pub.title.new": "Publish to the marketplace",
69
71
  "pub.title.update": "Publish a new version",
70
- "pub.intro": "\"{{name}}\" will be packed and uploaded to the OpenClacky marketplace, where anyone can discover and install it.",
72
+ "pub.intro": "\"{{name}}\" will be packed and uploaded to the extension marketplace, where anyone can discover and install it.",
71
73
  "pub.version.new": "First release · v{{ver}}",
72
74
  "pub.version.update": "v{{prev}} → v{{ver}}",
73
75
  "pub.version.missing": "No version in ext.yml — add a \"version\" field before publishing.",
@@ -77,10 +79,13 @@
77
79
  "pub.readme.ok": "README.md detected",
78
80
  "pub.readme.missing": "No README.md — consider adding one so users understand your extension.",
79
81
  "pub.btn.publish": "Publish v{{ver}}",
82
+ "pub.btn.publishing": "Publishing…",
83
+ "pub.btn.done": "Done",
80
84
  "pub.btn.cancel": "Cancel",
81
85
  "pub.progress.packing": "Packing…",
82
86
  "pub.progress.uploading": "Uploading to marketplace…",
83
87
  "pub.done": "Published {{id}} v{{ver}} — {{status}}",
88
+ "pub.success": "Published successfully",
84
89
  "pub.done.close": "Done",
85
90
  "pub.retry": "Try again",
86
91
  "extlist.section.cloud": "Published Extensions",
@@ -98,6 +103,7 @@
98
103
  "extlist.btn.publish": "Publish",
99
104
  "extlist.btn.update": "Update",
100
105
  "extlist.btn.pack": "Pack",
106
+ "extlist.btn.packing": "Packing…",
101
107
  "extlist.btn.unpublish": "Unpublish",
102
108
  "extlist.btn.iterate": "Iterate",
103
109
  "extlist.iterate.seed": "Iterate on extension {{id}}",
@@ -105,7 +111,6 @@
105
111
  "extlist.overwrite.confirm": "\"{{id}}\" is already published. Publish a new version?",
106
112
  "extlist.unpublish.confirm": "Unpublish {{id}} from the marketplace?",
107
113
  "extlist.publishing": "Publishing…",
108
- "extlist.packed": "Packed → {{path}}",
109
114
  "extlist.needLicense": "Publishing requires an activated user license.",
110
115
  "extlist.newExt.label": "Create a new extension",
111
116
  "extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
@@ -132,7 +137,7 @@
132
137
  "skills.shadow.tooltip": "Local copy shadows a same-named brand skill",
133
138
  "skills.newSkill.label": "Create a new skill with /skill-creator",
134
139
  "skills.newSkill.btn": "Create New Skill",
135
- "skills.promo.text": "Publish your skills & build your own brand on OpenClacky.",
140
+ "skills.promo.text": "Publish your skills & build your own brand.",
136
141
  "skills.promo.link": "Learn more →",
137
142
  "skills.locked": "Creator license required to publish cloud skills.",
138
143
  "skills.publishing": "Publishing…",
@@ -159,7 +164,7 @@
159
164
  "verify.errors": "{{n}} 个错误",
160
165
  "verify.warnings": "{{n}} 个警告",
161
166
  "verify.hint": "提示",
162
- "hint.reload": "修复后刷新本页 —— 扩展每次请求热重载。",
167
+ "hint.reload": "修复问题后,刷新页面即可生效。",
163
168
  "publish.status": "状态",
164
169
  "publish.status.draft": "草稿",
165
170
  "publish.status.published": "已发布",
@@ -169,18 +174,20 @@
169
174
  "btn.publish": "发布到市场",
170
175
  "btn.publishing": "发布中…",
171
176
  "btn.pack": "打包 (.zip)",
177
+ "btn.packing": "打包中…",
172
178
  "publish.needLicense": "发布需要已激活的用户授权。",
173
179
  "publish.done": "已发布 {{id}} {{ver}} — {{status}}",
174
- "publish.packed": "已打包 → {{path}}",
175
180
  "publish.already": "已发布过。勾选「发布新版本」后再试。",
176
181
  "published.title": "你发布的扩展",
177
182
  "published.empty": "你还没有发布任何扩展。",
178
183
  "btn.unpublish": "下架",
179
184
  "published.confirm": "确定要从市场下架 {{id}} 吗?",
180
185
  "err.generic": "出错了:{{msg}}",
186
+ "err.version_conflict": "版本 {{ver}} 已发布过,请在上方输入更高的版本号后重试。",
187
+ "pub.version.label": "版本号",
181
188
 
182
189
  "publish.confirm.title": "确定发布到市场?",
183
- "publish.confirm.body": "「{{id}}」将被打包并上传到 OpenClacky 市场,任何人都能发现并安装它。请确认它已准备好公开分享。",
190
+ "publish.confirm.body": "「{{id}}」将被打包并上传到扩展市场,任何人都能发现并安装它。请确认它已准备好公开分享。",
184
191
  "publish.confirm.ok": "发布",
185
192
  "publish.confirm.cancel": "取消",
186
193
  "bind.title": "授权此设备",
@@ -197,7 +204,7 @@
197
204
 
198
205
  "pub.title.new": "发布到市场",
199
206
  "pub.title.update": "发布新版本",
200
- "pub.intro": "「{{name}}」将被打包并上传到 OpenClacky 市场,任何人都能发现并安装它。",
207
+ "pub.intro": "「{{name}}」将被打包并上传到扩展市场,任何人都能发现并安装它。",
201
208
  "pub.version.new": "首次发布 · v{{ver}}",
202
209
  "pub.version.update": "v{{prev}} → v{{ver}}",
203
210
  "pub.version.missing": "ext.yml 里没有 version — 请先补上 version 字段再发布。",
@@ -207,10 +214,13 @@
207
214
  "pub.readme.ok": "已检测到 README.md",
208
215
  "pub.readme.missing": "未找到 README.md — 建议补一份,方便用户了解你的扩展。",
209
216
  "pub.btn.publish": "发布 v{{ver}}",
217
+ "pub.btn.publishing": "发布中…",
218
+ "pub.btn.done": "完成",
210
219
  "pub.btn.cancel": "取消",
211
220
  "pub.progress.packing": "打包中…",
212
221
  "pub.progress.uploading": "上传到市场…",
213
222
  "pub.done": "已发布 {{id}} v{{ver}} — {{status}}",
223
+ "pub.success": "已发布成功",
214
224
  "pub.done.close": "完成",
215
225
  "pub.retry": "重试",
216
226
  "extlist.section.cloud": "已发布扩展",
@@ -228,6 +238,7 @@
228
238
  "extlist.btn.publish": "发布",
229
239
  "extlist.btn.update": "更新",
230
240
  "extlist.btn.pack": "打包",
241
+ "extlist.btn.packing": "打包中…",
231
242
  "extlist.btn.unpublish": "下架",
232
243
  "extlist.btn.iterate": "迭代",
233
244
  "extlist.iterate.seed": "迭代扩展 {{id}}",
@@ -235,7 +246,6 @@
235
246
  "extlist.overwrite.confirm": "「{{id}}」已经发布过了。要发布新版本吗?",
236
247
  "extlist.unpublish.confirm": "确定要从市场下架 {{id}} 吗?",
237
248
  "extlist.publishing": "发布中…",
238
- "extlist.packed": "已打包 → {{path}}",
239
249
  "extlist.needLicense": "发布需要已激活的用户授权。",
240
250
  "extlist.newExt.label": "新建扩展",
241
251
  "extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
@@ -262,7 +272,7 @@
262
272
  "skills.shadow.tooltip": "本地副本覆盖了同名品牌 skill",
263
273
  "skills.newSkill.label": "用 /skill-creator 创建新 skill",
264
274
  "skills.newSkill.btn": "创建新 Skill",
265
- "skills.promo.text": "发布你的 skill,在 OpenClacky 上打造自己的品牌。",
275
+ "skills.promo.text": "发布你的 skill,打造自己的品牌。",
266
276
  "skills.promo.link": "了解更多 →",
267
277
  "skills.locked": "发布云端 skill 需要创作者授权。",
268
278
  "skills.publishing": "发布中…",
@@ -302,6 +312,28 @@
302
312
  return data;
303
313
  }
304
314
 
315
+ async function downloadPack(ext_id) {
316
+ const res = await fetch(api("/pack"), {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/json" },
319
+ body: JSON.stringify({ ext_id }),
320
+ });
321
+ if (!res.ok) {
322
+ let msg = `Request failed (${res.status})`;
323
+ try { msg = (await res.json()).error || msg; } catch (_e) {}
324
+ throw new Error(msg);
325
+ }
326
+ const blob = await res.blob();
327
+ const url = URL.createObjectURL(blob);
328
+ const a = document.createElement("a");
329
+ a.href = url;
330
+ a.download = `${ext_id}.zip`;
331
+ document.body.appendChild(a);
332
+ a.click();
333
+ document.body.removeChild(a);
334
+ URL.revokeObjectURL(url);
335
+ }
336
+
305
337
  // Skills tab talks to host-owned endpoints (not the ext prefix). We surface
306
338
  // the HTTP status so the caller can treat 403 as "locked" rather than error.
307
339
  async function getHost(path) {
@@ -443,47 +475,48 @@
443
475
 
444
476
  // Unified publish flow: one modal that walks the creator from a release form
445
477
  // (version + notes) through progress → device binding (if needed) → done.
446
- // The creator owns the version in ext.yml; a new release must be greater than
447
- // the currently published version — the backend enforces that. `prevVersion`
448
- // is the latest published version when updating, or null for a first release.
478
+ // The creator owns the version; if it conflicts with the latest published
479
+ // version they can bump it inline — the modal writes it back to ext.yml
480
+ // before uploading. `prevVersionOrPromise` may be a value or a Promise.
449
481
  // Resolves true when a publish succeeded, false otherwise.
450
- function runPublishFlow(ext, prevVersion) {
482
+ function runPublishFlow(ext, prevVersionOrPromise) {
451
483
  return new Promise((resolve) => {
452
- const version = ext.version;
453
- const isUpdate = !!prevVersion;
454
-
455
- const intro = el("p", { class: "studio-modal-status", text: t("pub.intro", { name: ext.name }) });
456
-
457
- const verText = !version
458
- ? t("pub.version.missing")
459
- : isUpdate
460
- ? t("pub.version.update", { prev: prevVersion, ver: version })
461
- : t("pub.version.new", { ver: version });
462
- const verLine = el("p", { class: "studio-modal-code", text: verText });
463
-
464
- const unitList = (ext.units || []).map((u) => u.kind).join(" · ");
465
- const unitsLine = unitList
466
- ? el("p", { class: "studio-skill-hint", text: t("pub.units", { units: unitList }) })
467
- : null;
484
+ let currentVersion = ext.version || "";
485
+ let isUpdate = false;
486
+
487
+ const verField = el("div", { class: "studio-field" });
488
+ verField.appendChild(el("label", { class: "studio-label", text: t("pub.version.label") }));
489
+ const verInput = el("input", { class: "studio-input", type: "text", value: currentVersion, placeholder: "1.0.0" });
490
+ verInput.addEventListener("input", () => {
491
+ currentVersion = verInput.value.trim();
492
+ publishBtn.disabled = !currentVersion;
493
+ publishBtn.textContent = currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" });
494
+ });
495
+ verField.appendChild(verInput);
468
496
 
469
- const notesLabel = el("label", { class: "studio-label", text: t("pub.notes.label") });
470
- const notes = el("textarea", { class: "studio-textarea", rows: "3", placeholder: t("pub.notes.placeholder") });
497
+ const notesField = el("div", { class: "studio-field" });
498
+ notesField.appendChild(el("label", { class: "studio-label", text: t("pub.notes.label") }));
499
+ const notes = el("textarea", { class: "studio-textarea", rows: "4", placeholder: t("pub.notes.placeholder") });
500
+ notesField.appendChild(notes);
471
501
 
472
502
  const status = el("p", { class: "studio-modal-status" });
473
503
  status.style.display = "none";
474
504
 
475
- const bodyChildren = [intro, verLine];
476
- if (unitsLine) bodyChildren.push(unitsLine);
477
- bodyChildren.push(notesLabel, notes, status);
505
+ const bodyChildren = [
506
+ el("p", { class: "studio-modal-intro", text: t("pub.intro", { name: ext.name }) }),
507
+ verField,
508
+ notesField,
509
+ status,
510
+ ];
478
511
 
479
512
  let done = false;
480
513
  const modal = openModal({
481
- title: isUpdate ? t("pub.title.update") : t("pub.title.new"),
514
+ title: t("pub.title.new"),
482
515
  body: el("div", null, bodyChildren),
483
516
  buttons: [
484
517
  { label: t("pub.btn.cancel"), keepOpen: false, onClick: () => { if (!done) resolve(false); } },
485
518
  {
486
- label: version ? t("pub.btn.publish", { ver: version }) : t("pub.btn.publish", { ver: "?" }),
519
+ label: currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" }),
487
520
  primary: true,
488
521
  keepOpen: true,
489
522
  onClick: () => submit(),
@@ -493,50 +526,82 @@
493
526
 
494
527
  const publishBtn = modal.footer.querySelector(".studio-btn-primary");
495
528
  const cancelBtn = modal.footer.querySelector(".studio-btn:not(.studio-btn-primary)");
496
- if (!version) publishBtn.disabled = true;
529
+ if (!currentVersion) publishBtn.disabled = true;
530
+
531
+ Promise.resolve(prevVersionOrPromise).then((prevVersion) => {
532
+ if (done || !prevVersion) return;
533
+ isUpdate = true;
534
+ const titleEl = modal.overlay.querySelector(".studio-modal-title");
535
+ if (titleEl) titleEl.textContent = t("pub.title.update");
536
+ if (prevVersion !== currentVersion) {
537
+ currentVersion = prevVersion;
538
+ verInput.value = currentVersion;
539
+ publishBtn.disabled = false;
540
+ publishBtn.textContent = t("pub.btn.publish", { ver: currentVersion });
541
+ }
542
+ });
497
543
 
498
- function setProgress(msg) {
544
+ function setProgress(msg, isError) {
499
545
  status.style.display = "";
500
546
  status.textContent = msg;
547
+ status.className = "studio-modal-status" + (isError ? " studio-modal-status-error" : "");
548
+ }
549
+
550
+ function resetButtons() {
551
+ publishBtn.disabled = !currentVersion;
552
+ publishBtn.textContent = currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" });
553
+ cancelBtn.disabled = false;
554
+ notes.disabled = false;
555
+ verInput.disabled = false;
501
556
  }
502
557
 
503
558
  async function submit() {
504
559
  if (done || publishBtn.disabled) return;
560
+ const ver = currentVersion;
561
+ if (!ver) return;
562
+
505
563
  publishBtn.disabled = true;
564
+ publishBtn.textContent = t("pub.btn.publishing");
506
565
  cancelBtn.disabled = true;
507
566
  notes.disabled = true;
508
- setProgress(t("pub.progress.packing"));
567
+ verInput.disabled = true;
568
+ status.style.display = "none";
569
+
509
570
  try {
510
- setProgress(t("pub.progress.uploading"));
571
+ if (ver !== ext.version) {
572
+ await postJson("/set_version", { ext_id: ext.id, version: ver });
573
+ }
574
+
511
575
  const data = await publishWithBinding({
512
576
  ext_id: ext.id,
513
577
  force: isUpdate,
514
578
  changelog: notes.value.trim(),
515
579
  });
516
- if (data === null) {
517
- publishBtn.disabled = false;
518
- cancelBtn.disabled = false;
519
- notes.disabled = false;
520
- status.style.display = "none";
521
- return;
522
- }
580
+
581
+ if (data === null) { resetButtons(); return; }
523
582
  if (!data.ok) throw new Error(data.error || "Publish failed");
583
+
524
584
  done = true;
525
- setProgress(t("pub.done", {
526
- id: data.ext_id,
527
- ver: data.version || version,
528
- status: data.status || "",
529
- }));
530
- publishBtn.style.display = "none";
531
- cancelBtn.textContent = t("pub.done.close");
532
- cancelBtn.disabled = false;
533
- cancelBtn.onclick = () => { modal.close(); resolve(true); };
534
- } catch (e) {
535
- setProgress(t("err.generic", { msg: e.message }));
585
+ const successLabel = el("span", { class: "studio-pub-success", text: t("pub.success") });
586
+ modal.footer.insertBefore(successLabel, modal.footer.firstChild);
587
+ cancelBtn.style.display = "none";
536
588
  publishBtn.disabled = false;
537
- publishBtn.textContent = t("pub.retry");
538
- cancelBtn.disabled = false;
539
- notes.disabled = false;
589
+ publishBtn.textContent = t("pub.btn.done");
590
+ publishBtn.onclick = () => { modal.close(); resolve(true); };
591
+ } catch (e) {
592
+ const msg = e.message || "";
593
+ const isConflict = /must be greater than/i.test(msg);
594
+ if (isConflict) {
595
+ setProgress(t("err.version_conflict", { ver }), true);
596
+ verInput.classList.add("studio-input-error");
597
+ verInput.disabled = false;
598
+ verInput.focus();
599
+ verInput.select();
600
+ verInput.addEventListener("input", () => verInput.classList.remove("studio-input-error"), { once: true });
601
+ } else {
602
+ setProgress(t("err.generic", { msg }), true);
603
+ }
604
+ resetButtons();
540
605
  }
541
606
  }
542
607
  });
@@ -586,6 +651,7 @@
586
651
  function createDebugPanel() {
587
652
  let container = null;
588
653
  let unsub = null;
654
+ let summaryFadeTimer = null;
589
655
 
590
656
  async function runVerify() {
591
657
  const ext = store.selected();
@@ -597,6 +663,8 @@
597
663
  renderVerify(data);
598
664
  } catch (e) {
599
665
  renderError(e);
666
+ } finally {
667
+ if (status) status.textContent = t("btn.recheck");
600
668
  }
601
669
  }
602
670
 
@@ -607,16 +675,22 @@
607
675
  const errs = (data.issues || []).filter((i) => i.level === "error");
608
676
  const warns = (data.issues || []).filter((i) => i.level === "warning");
609
677
 
610
- const summary = el("div", { class: data.ok ? "studio-verify-ok" : "studio-verify-fail" });
611
- if (data.ok && !warns.length) {
612
- summary.textContent = "✓ " + t("verify.ok");
613
- } else {
614
- const parts = [];
615
- if (errs.length) parts.push(t("verify.errors", { n: errs.length }));
616
- if (warns.length) parts.push(t("verify.warnings", { n: warns.length }));
617
- summary.textContent = parts.join(" · ");
678
+ const summary = container.querySelector(".studio-verify-summary");
679
+ if (summary) {
680
+ clearTimeout(summaryFadeTimer);
681
+ summary.textContent = "";
682
+ if (data.ok && !warns.length) {
683
+ summary.textContent = "✓ " + t("verify.ok");
684
+ summary.className = "studio-verify-summary studio-verify-ok";
685
+ summaryFadeTimer = setTimeout(() => { summary.textContent = ""; }, 3000);
686
+ } else {
687
+ const parts = [];
688
+ if (errs.length) parts.push(t("verify.errors", { n: errs.length }));
689
+ if (warns.length) parts.push(t("verify.warnings", { n: warns.length }));
690
+ summary.textContent = parts.join(" · ");
691
+ summary.className = "studio-verify-summary studio-verify-fail";
692
+ }
618
693
  }
619
- box.appendChild(summary);
620
694
 
621
695
  (data.issues || []).forEach((i) => {
622
696
  const item = el("div", { class: "studio-issue studio-issue-" + i.level });
@@ -630,7 +704,9 @@
630
704
 
631
705
  function renderError(e) {
632
706
  const box = container.querySelector(".studio-verify");
633
- if (box) { box.innerHTML = ""; box.appendChild(el("div", { class: "studio-verify-fail", text: t("err.generic", { msg: e.message }) })); }
707
+ const summary = container.querySelector(".studio-verify-summary");
708
+ if (summary) { summary.textContent = t("err.generic", { msg: e.message }); summary.className = "studio-verify-summary studio-verify-fail"; }
709
+ if (box) { box.innerHTML = ""; }
634
710
  }
635
711
 
636
712
  function renderDetail() {
@@ -647,7 +723,6 @@
647
723
  const row = (label, value) => { meta.appendChild(el("dt", { text: label })); meta.appendChild(el("dd", { text: value || "—" })); };
648
724
  row(t("detail.version"), ext.version);
649
725
  row(t("detail.layer"), ext.layer);
650
- row(t("detail.origin"), ext.origin);
651
726
  detail.appendChild(meta);
652
727
 
653
728
  detail.appendChild(el("div", { class: "studio-label", text: t("detail.units") }));
@@ -674,6 +749,7 @@
674
749
 
675
750
  const bar = el("div", { class: "studio-actions" });
676
751
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-verify-status", text: t("btn.recheck"), onclick: runVerify }));
752
+ bar.appendChild(el("span", { class: "studio-verify-summary" }));
677
753
  container.appendChild(bar);
678
754
 
679
755
  container.appendChild(el("div", { class: "studio-verify" }));
@@ -709,24 +785,28 @@
709
785
  async function doPublish() {
710
786
  const ext = store.selected();
711
787
  if (!ext) return;
712
- let prevVersion = null;
713
- try {
714
- const data = await getJson("/published");
715
- const match = (data.extensions || []).find((e) => e.id === ext.id);
716
- prevVersion = match ? match.version : null;
717
- } catch (_e) { /* treat as first release */ }
718
- const ok = await runPublishFlow(ext, prevVersion);
788
+ const prevVersionPromise = getJson("/published")
789
+ .then((data) => {
790
+ const match = (data.extensions || []).find((e) => e.id === ext.id);
791
+ return match ? match.version : null;
792
+ })
793
+ .catch(() => null);
794
+ const ok = await runPublishFlow(ext, prevVersionPromise);
719
795
  if (ok) loadPublished();
720
796
  }
721
797
 
722
798
  async function doPack() {
723
799
  const ext = store.selected();
724
800
  if (!ext) return;
801
+ const btn = container.querySelector(".studio-pack-btn");
802
+ const orig = btn ? btn.textContent : null;
803
+ if (btn) { btn.disabled = true; btn.textContent = t("btn.packing"); }
725
804
  try {
726
- const data = await postJson("/pack", { ext_id: ext.id });
727
- feedback(t("publish.packed", { path: data.path }), "success");
805
+ await downloadPack(ext.id);
728
806
  } catch (e) {
729
807
  feedback(t("err.generic", { msg: e.message }), "error");
808
+ } finally {
809
+ if (btn) { btn.disabled = false; btn.textContent = orig; }
730
810
  }
731
811
  }
732
812
 
@@ -740,17 +820,23 @@
740
820
  const exts = data.extensions || [];
741
821
  if (!exts.length) { box.appendChild(el("p", { class: "studio-empty", text: t("published.empty") })); return; }
742
822
  exts.forEach((e) => {
743
- const row = el("div", { class: "studio-published-row" });
744
- row.appendChild(el("span", { class: "studio-published-name", text: `${e.name}${e.version ? " v" + e.version : ""} [${e.status}]` }));
745
- row.appendChild(el("button", {
746
- class: "studio-btn studio-btn-danger", text: t("btn.unpublish"),
747
- onclick: async () => {
748
- if (!window.confirm(t("published.confirm", { id: e.id }))) return;
749
- try { await postJson("/unpublish", { ext_id: e.id }); loadPublished(); }
750
- catch (err) { feedback(t("err.generic", { msg: err.message }), "error"); }
751
- },
752
- }));
753
- box.appendChild(row);
823
+ const card = el("div", { class: "studio-skill-card" });
824
+ const head = el("div", { class: "studio-skill-head" });
825
+ head.appendChild(el("span", { class: "studio-skill-name", text: e.name || e.id }));
826
+ const statusKind = e.status === "draft" ? "local" : "published";
827
+ 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
+ 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") });
830
+ unpubBtn.style.marginLeft = "auto";
831
+ unpubBtn.addEventListener("click", async () => {
832
+ if (!window.confirm(t("published.confirm", { id: e.id }))) return;
833
+ unpubBtn.disabled = true;
834
+ try { await postJson("/unpublish", { ext_id: e.id }); loadPublished(); }
835
+ catch (err) { unpubBtn.disabled = false; feedback(t("err.generic", { msg: err.message }), "error"); }
836
+ });
837
+ head.appendChild(unpubBtn);
838
+ card.appendChild(head);
839
+ box.appendChild(card);
754
840
  });
755
841
  } catch (e) {
756
842
  box.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) }));
@@ -766,7 +852,7 @@
766
852
 
767
853
  const bar = el("div", { class: "studio-actions" });
768
854
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish }));
769
- bar.appendChild(el("button", { class: "studio-btn studio-btn-ghost", text: t("btn.pack"), onclick: doPack }));
855
+ bar.appendChild(el("button", { class: "studio-btn studio-btn-ghost studio-pack-btn", text: t("btn.pack"), onclick: doPack }));
770
856
  container.appendChild(bar);
771
857
 
772
858
  container.appendChild(el("div", { class: "studio-feedback" }));
@@ -890,17 +976,22 @@
890
976
  actions.appendChild(pub);
891
977
 
892
978
  actions.appendChild(el("button", { class: "studio-btn", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
893
- actions.appendChild(el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.pack"), onclick: () => doPack(ext) }));
979
+ const packBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.pack") });
980
+ packBtn.addEventListener("click", () => doPack(ext, packBtn));
981
+ actions.appendChild(packBtn);
894
982
  card.appendChild(actions);
895
983
  return card;
896
984
  }
897
985
 
898
- async function doPack(ext) {
986
+ async function doPack(ext, btn) {
987
+ const orig = btn ? btn.textContent : null;
988
+ if (btn) { btn.disabled = true; btn.textContent = t("extlist.btn.packing"); }
899
989
  try {
900
- const data = await postJson("/pack", { ext_id: ext.id });
901
- alert(t("extlist.packed", { path: data.path }));
990
+ await downloadPack(ext.id);
902
991
  } catch (e) {
903
992
  alert(t("err.generic", { msg: e.message }));
993
+ } finally {
994
+ if (btn) { btn.disabled = false; btn.textContent = orig; }
904
995
  }
905
996
  }
906
997
 
@@ -1210,6 +1301,8 @@
1210
1301
  .studio-page .studio-panel { padding: 0; }
1211
1302
  .studio-field { margin-bottom: 14px; }
1212
1303
  .studio-label { display: block; font-size: 12px; font-weight: 600; color: var(--color-text-tertiary); margin-bottom: 6px; }
1304
+ .studio-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
1305
+ .studio-label { font-size: 12px; font-weight: 500; color: var(--color-text-secondary); }
1213
1306
  .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; }
1214
1307
  .studio-select:focus, .studio-textarea:focus { border-color: var(--color-accent-primary); outline: none; }
1215
1308
  .studio-empty { color: var(--color-text-muted); font-size: 12px; margin: 4px 0; }
@@ -1222,7 +1315,7 @@
1222
1315
  .studio-meta dd { margin: 0; color: var(--color-text-primary); }
1223
1316
  .studio-units { display: flex; flex-wrap: wrap; gap: 6px; }
1224
1317
  .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; }
1225
- .studio-actions { display: flex; gap: 8px; margin: 12px 0; }
1318
+ .studio-actions { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
1226
1319
  .studio-skill-promo .studio-actions { margin: 12px 0 0; }
1227
1320
  .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; }
1228
1321
  .studio-btn:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
@@ -1231,11 +1324,13 @@
1231
1324
  .studio-btn-primary:disabled { opacity: 0.6; cursor: default; }
1232
1325
  .studio-btn-danger { color: var(--color-error); border-color: var(--color-error-border); padding: 4px 10px; }
1233
1326
  .studio-btn-danger:hover { background: var(--color-error-bg); color: var(--color-error); }
1234
- .studio-btn-ghost { border-color: transparent; color: var(--color-text-tertiary); font-weight: 400; }
1235
- .studio-btn-ghost:hover { background: var(--color-bg-hover); color: var(--color-text-secondary); }
1327
+ .studio-btn-ghost { color: var(--color-text-secondary); font-weight: 500; }
1328
+ .studio-btn-ghost:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
1236
1329
  .studio-verify { margin-top: 8px; }
1237
- .studio-verify-ok { color: var(--color-success); font-size: 12px; font-weight: 600; margin-bottom: 8px; }
1238
- .studio-verify-fail { color: var(--color-error); font-size: 12px; font-weight: 600; margin-bottom: 8px; }
1330
+ .studio-verify-summary { font-size: 13px; font-weight: 600; }
1331
+ .studio-verify-ok { color: var(--color-success); font-size: 12px; font-weight: 600; }
1332
+ .studio-verify-fail { color: var(--color-error); font-size: 12px; font-weight: 600; }
1333
+ .studio-verify > *:first-child { margin-bottom: 8px; }
1239
1334
  .studio-issue { border-left: 3px solid var(--color-border-primary); padding: 6px 10px; margin-bottom: 8px; background: var(--color-bg-secondary); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
1240
1335
  .studio-issue-error { border-left-color: var(--color-error); }
1241
1336
  .studio-issue-warning { border-left-color: var(--color-warning, var(--color-text-tertiary)); }
@@ -1244,13 +1339,13 @@
1244
1339
  .studio-issue-file { font-size: 11px; font-family: monospace; color: var(--color-text-muted); margin-top: 2px; }
1245
1340
  .studio-issue-hint { font-size: 11px; color: var(--color-text-secondary); margin-top: 4px; font-style: italic; }
1246
1341
  .studio-check { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--color-text-secondary); margin-bottom: 12px; cursor: pointer; }
1247
- .studio-feedback { font-size: 12px; margin: 8px 0; min-height: 14px; line-height: 1.4; }
1342
+ .studio-feedback { font-size: 12px; margin: 6px 0 0; line-height: 1.4; }
1248
1343
  .studio-feedback-success { color: var(--color-success); }
1249
1344
  .studio-feedback-error { color: var(--color-error); }
1250
1345
  .studio-feedback-warn { color: var(--color-warning, var(--color-text-secondary)); }
1251
1346
  .studio-published { margin-top: 16px; border-top: 1px solid var(--color-border-primary); padding-top: 12px; }
1252
- .studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; font-size: 12px; }
1253
- .studio-published-name { color: var(--color-text-primary); }
1347
+ .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
+ .studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
1254
1349
  .studio-page { width: 100%; }
1255
1350
  .studio-page-head { margin-bottom: 20px; }
1256
1351
  .studio-page-title { margin: 0; font-size: 22px; font-weight: 600; color: var(--color-text-primary); }
@@ -1263,7 +1358,7 @@
1263
1358
  .studio-skill-section { margin-bottom: 28px; }
1264
1359
  .studio-skill-section-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
1265
1360
  .studio-skill-hint { font-size: 11px; color: var(--color-text-muted); }
1266
- .studio-skill-card { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 14px 16px; margin: 0 0 10px; background: var(--color-bg-secondary); }
1361
+ .studio-skill-card { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 12px; margin: 0 0 10px; background: var(--color-bg-secondary); }
1267
1362
  .studio-skill-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1268
1363
  .studio-skill-name { font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
1269
1364
  .studio-skill-badge { font-size: 11px; padding: 1px 8px; border-radius: 10px; }
@@ -1272,17 +1367,29 @@
1272
1367
  .studio-skill-badge-changed { background: var(--color-warning-bg, var(--color-bg-hover)); color: var(--color-warning, var(--color-text-secondary)); }
1273
1368
  .studio-skill-badge-shadow { background: var(--color-bg-hover); color: var(--color-text-secondary); }
1274
1369
  .studio-skill-desc { margin: 6px 0 8px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1275
- .studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); margin-bottom: 8px; }
1370
+ .studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); }
1276
1371
  .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); }
1277
1372
  .studio-skill-promo-text { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
1278
- .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; }
1279
- .studio-modal { background: var(--color-bg-primary, var(--color-bg-secondary)); border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 20px 22px; width: 420px; max-width: calc(100vw - 40px); box-shadow: 0 12px 40px rgba(0,0,0,0.35); }
1280
- .studio-modal-title { margin: 0 0 10px; font-size: 16px; font-weight: 600; color: var(--color-text-primary); }
1373
+ .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; }
1374
+ .studio-modal { background: var(--color-bg-primary, var(--color-bg-secondary)); border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 28px 28px 22px; width: 520px; max-width: calc(100vw - 40px); box-shadow: 0 12px 40px rgba(0,0,0,0.35); animation: studio-modal-in 0.2s cubic-bezier(0.34,1.4,0.64,1); }
1375
+ @keyframes studio-overlay-in { from { opacity: 0; } to { opacity: 1; } }
1376
+ @keyframes studio-modal-in { from { opacity: 0; transform: scale(0.93) translateY(6px); } to { opacity: 1; transform: scale(1) translateY(0); } }
1377
+ .studio-modal-title { margin: 0 0 16px; font-size: 16px; font-weight: 600; color: var(--color-text-primary); }
1378
+ .studio-modal-intro { margin: 0 0 16px; font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1379
+ .studio-pub-meta { display: flex; align-items: center; gap: 10px; background: var(--color-bg-secondary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm, 6px); padding: 10px 14px; margin-bottom: 16px; }
1380
+ .studio-pub-version { font-size: 13px; font-weight: 600; color: var(--color-text-primary); font-family: monospace; }
1381
+ .studio-pub-units { font-size: 12px; color: var(--color-text-tertiary); }
1382
+ .studio-pub-units::before { content: "·"; margin-right: 10px; }
1281
1383
  .studio-modal-body { font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1282
1384
  .studio-modal-status { margin: 0 0 8px; font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1385
+ .studio-modal-status-error { color: var(--color-error); }
1386
+ .studio-input { 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 10px; color: var(--color-text-primary); font-size: 13px; font-family: monospace; }
1387
+ .studio-input:focus { border-color: var(--color-accent-primary); outline: none; }
1388
+ .studio-input-error { border-color: var(--color-error) !important; }
1283
1389
  .studio-modal-code { margin: 0 0 8px; font-size: 13px; font-family: monospace; color: var(--color-text-primary); }
1284
1390
  .studio-modal-link { display: inline-block; font-size: 13px; color: var(--color-accent-primary); }
1285
- .studio-modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
1391
+ .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
+ .studio-pub-success { margin-right: auto; font-size: 13px; font-weight: 500; color: var(--color-success); }
1286
1393
  `;
1287
1394
  document.head.appendChild(style);
1288
1395
  })();