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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/clacky/agent/skill_manager.rb +1 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +58 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +355 -44
- data/lib/clacky/default_skills/media-gen/SKILL.md +182 -7
- data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +5 -3
- data/lib/clacky/extension/packager.rb +11 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +41 -0
- data/lib/clacky/media/volcengine.rb +394 -0
- data/lib/clacky/server/http_server.rb +48 -4
- data/lib/clacky/tools/terminal.rb +24 -8
- data/lib/clacky/ui2/layout_manager.rb +4 -0
- data/lib/clacky/ui2/screen_buffer.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -8
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +110 -16
- data/lib/clacky/web/features/extensions/view.js +42 -7
- data/lib/clacky/web/features/skills/view.js +61 -1
- data/lib/clacky/web/i18n.js +24 -0
- data/lib/clacky/web/index.html +6 -11
- data/lib/clacky/web/sessions.js +12 -3
- metadata +2 -1
|
@@ -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
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
978
|
-
pub.title =
|
|
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
|
|
1341
|
-
.studio-detail-desc { margin: 0 0
|
|
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
|
|
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
|
})();
|