openclacky 1.4.0 → 1.4.1
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 +29 -0
- data/lib/clacky/agent/session_serializer.rb +11 -4
- data/lib/clacky/agent.rb +4 -5
- data/lib/clacky/brand_config.rb +53 -3
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +144 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +412 -173
- data/lib/clacky/extension/packager.rb +10 -1
- data/lib/clacky/platform_http_client.rb +9 -7
- data/lib/clacky/providers.rb +4 -4
- data/lib/clacky/server/http_server.rb +19 -18
- data/lib/clacky/tools/grep.rb +6 -1
- data/lib/clacky/utils/environment_detector.rb +16 -9
- data/lib/clacky/utils/file_processor.rb +21 -24
- data/lib/clacky/utils/model_pricing.rb +49 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +121 -17
- data/lib/clacky/web/components/onboard.js +14 -4
- data/lib/clacky/web/features/backup/view.js +4 -3
- data/lib/clacky/web/features/extensions/store.js +2 -2
- data/lib/clacky/web/features/extensions/view.js +14 -19
- data/lib/clacky/web/features/mcp/view.js +5 -2
- data/lib/clacky/web/features/workspace/store.js +11 -0
- data/lib/clacky/web/features/workspace/view.js +10 -4
- data/lib/clacky/web/i18n.js +6 -18
- data/lib/clacky/web/index.html +23 -2
- data/lib/clacky/web/sessions.js +33 -5
- data/scripts/uninstall.sh +1 -1
- metadata +1 -1
|
@@ -50,6 +50,12 @@
|
|
|
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
52
|
"err.invalid_version": "Invalid version. Use format like 1.0.0.",
|
|
53
|
+
"err.name_taken": "The name \"{{id}}\" is already taken by another user.",
|
|
54
|
+
"rename.label": "Choose a new ID for your extension:",
|
|
55
|
+
"rename.placeholder": "e.g. my-ext-yourname",
|
|
56
|
+
"rename.hint": "Only lowercase letters, digits and hyphens. This will rename the local folder and update ext.yml.",
|
|
57
|
+
"rename.btn": "Rename & Publish",
|
|
58
|
+
"rename.renaming": "Renaming…",
|
|
53
59
|
"pub.version.label": "Version",
|
|
54
60
|
|
|
55
61
|
"publish.confirm.title": "Publish to the marketplace?",
|
|
@@ -89,6 +95,27 @@
|
|
|
89
95
|
"pub.success": "Published successfully",
|
|
90
96
|
"pub.done.close": "Done",
|
|
91
97
|
"pub.retry": "Try again",
|
|
98
|
+
"pub.readme.placeholder": "## Overview\n\nDescribe what your extension does...",
|
|
99
|
+
"pub.screenshot.upload": "Upload screenshot",
|
|
100
|
+
"pub.screenshot.uploading": "Uploading…",
|
|
101
|
+
"pub.screenshot.section": "Screenshots",
|
|
102
|
+
"pub.screenshot.hint": "PNG / JPG / GIF, up to 5 MB each",
|
|
103
|
+
"pub.screenshot.err": "Upload failed: {{msg}}",
|
|
104
|
+
"pub.screenshot.err.type": "Only PNG / JPG / GIF are allowed",
|
|
105
|
+
"pub.screenshot.err.size": "File size must be under 5 MB",
|
|
106
|
+
"pub.readme.edit.title": "Edit README",
|
|
107
|
+
"readme.pane.editor": "Markdown editor",
|
|
108
|
+
"readme.pane.preview": "Preview",
|
|
109
|
+
"pub.readme.edit": "Edit",
|
|
110
|
+
"pub.readme.section": "Description",
|
|
111
|
+
"pub.readme.empty": "No description yet — click Edit to add one.",
|
|
112
|
+
"pub.readme.save.empty": "Description cannot be empty.",
|
|
113
|
+
"pub.readme.required": "Please add a description (README) before publishing.",
|
|
114
|
+
"pub.readme.required.skip": "Skip, publish anyway",
|
|
115
|
+
"pub.readme.required.edit": "Add description",
|
|
116
|
+
"pub.readme.save": "Save",
|
|
117
|
+
"pub.readme.saving": "Saving…",
|
|
118
|
+
"pub.readme.skip": "Skip",
|
|
92
119
|
"extlist.section.cloud": "Published Extensions",
|
|
93
120
|
"extlist.section.cloudHint": "Live on the marketplace",
|
|
94
121
|
"extlist.section.local": "Local Extensions",
|
|
@@ -138,25 +165,6 @@
|
|
|
138
165
|
"meta.saved": "Saved",
|
|
139
166
|
"meta.save.err": "Save failed: {{msg}}",
|
|
140
167
|
|
|
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",
|
|
160
168
|
"skills.section.cloud": "Cloud Skills",
|
|
161
169
|
"skills.section.cloudHint": "Published to the platform",
|
|
162
170
|
"skills.section.local": "Local Skills",
|
|
@@ -226,6 +234,12 @@
|
|
|
226
234
|
"err.generic": "出错了:{{msg}}",
|
|
227
235
|
"err.version_conflict": "版本 {{ver}} 已发布过,请在上方输入更高的版本号后重试。",
|
|
228
236
|
"err.invalid_version": "版本号格式不对,请使用如 1.0.0 的格式。",
|
|
237
|
+
"err.name_taken": "扩展名「{{id}}」已被其他用户占用。",
|
|
238
|
+
"rename.label": "请为你的扩展选择一个新 ID:",
|
|
239
|
+
"rename.placeholder": "例如 my-ext-yourname",
|
|
240
|
+
"rename.hint": "只能使用小写字母、数字和连字符。",
|
|
241
|
+
"rename.btn": "重命名并发布",
|
|
242
|
+
"rename.renaming": "重命名中…",
|
|
229
243
|
"pub.version.label": "版本号",
|
|
230
244
|
|
|
231
245
|
"publish.confirm.title": "确定发布到市场?",
|
|
@@ -265,6 +279,27 @@
|
|
|
265
279
|
"pub.success": "已发布成功",
|
|
266
280
|
"pub.done.close": "完成",
|
|
267
281
|
"pub.retry": "重试",
|
|
282
|
+
"pub.readme.placeholder": "## 简介\n\n描述你的扩展的功能…",
|
|
283
|
+
"pub.screenshot.upload": "上传截图",
|
|
284
|
+
"pub.screenshot.uploading": "上传中…",
|
|
285
|
+
"pub.screenshot.section": "截图",
|
|
286
|
+
"pub.screenshot.hint": "PNG / JPG / GIF,每张最大 5 MB",
|
|
287
|
+
"pub.screenshot.err": "上传失败:{{msg}}",
|
|
288
|
+
"pub.screenshot.err.type": "仅支持 PNG / JPG / GIF 格式",
|
|
289
|
+
"pub.screenshot.err.size": "文件大小不能超过 5 MB",
|
|
290
|
+
"pub.readme.edit.title": "编辑使用说明",
|
|
291
|
+
"readme.pane.editor": "Markdown 编写",
|
|
292
|
+
"readme.pane.preview": "预览",
|
|
293
|
+
"pub.readme.edit": "编辑",
|
|
294
|
+
"pub.readme.section": "使用说明",
|
|
295
|
+
"pub.readme.empty": "暂无说明 — 点击编辑添加。",
|
|
296
|
+
"pub.readme.save.empty": "使用说明不能为空。",
|
|
297
|
+
"pub.readme.required": "发布前请先填写使用说明。",
|
|
298
|
+
"pub.readme.required.skip": "忽略,继续发布",
|
|
299
|
+
"pub.readme.required.edit": "去填写",
|
|
300
|
+
"pub.readme.save": "保存",
|
|
301
|
+
"pub.readme.saving": "保存中…",
|
|
302
|
+
"pub.readme.skip": "跳过",
|
|
268
303
|
"extlist.section.cloud": "已发布扩展",
|
|
269
304
|
"extlist.section.cloudHint": "已上架到市场",
|
|
270
305
|
"extlist.section.local": "本地扩展",
|
|
@@ -314,26 +349,6 @@
|
|
|
314
349
|
"meta.saved": "已保存",
|
|
315
350
|
"meta.save.err": "保存失败:{{msg}}",
|
|
316
351
|
|
|
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": "设置页标签",
|
|
337
352
|
"skills.section.cloud": "云端 Skills",
|
|
338
353
|
"skills.section.cloudHint": "已发布到平台",
|
|
339
354
|
"skills.section.local": "本地 Skills",
|
|
@@ -395,6 +410,22 @@
|
|
|
395
410
|
return data;
|
|
396
411
|
}
|
|
397
412
|
|
|
413
|
+
async function patchJson(path, body) {
|
|
414
|
+
const res = await fetch(api(path), {
|
|
415
|
+
method: "PATCH",
|
|
416
|
+
headers: { "Content-Type": "application/json" },
|
|
417
|
+
body: JSON.stringify(body || {}),
|
|
418
|
+
});
|
|
419
|
+
const data = await res.json();
|
|
420
|
+
if (!res.ok) {
|
|
421
|
+
const err = new Error(data.error || `Request failed (${res.status})`);
|
|
422
|
+
err.status = res.status;
|
|
423
|
+
err.data = data;
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
return data;
|
|
427
|
+
}
|
|
428
|
+
|
|
398
429
|
async function downloadPack(ext_id) {
|
|
399
430
|
const res = await fetch(api("/pack"), {
|
|
400
431
|
method: "POST",
|
|
@@ -560,42 +591,12 @@
|
|
|
560
591
|
}
|
|
561
592
|
|
|
562
593
|
// Unified publish flow: one modal that walks the creator from a release form
|
|
563
|
-
// (version + notes) through progress →
|
|
564
|
-
//
|
|
565
|
-
// version they can bump it inline — the modal writes it back to ext.yml
|
|
566
|
-
// before uploading. `prevVersionOrPromise` may be a value or a Promise.
|
|
594
|
+
// (version + notes) through progress → done.
|
|
595
|
+
// `prevVersionOrPromise` may be a value or a Promise.
|
|
567
596
|
// Resolves true when a publish succeeded, false otherwise.
|
|
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
597
|
|
|
598
|
+
function runPublishFlow(ext, prevVersionOrPromise, { onPublishDone } = {}) {
|
|
573
599
|
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) => {
|
|
599
600
|
let currentVersion = ext.version || "";
|
|
600
601
|
let isUpdate = false;
|
|
601
602
|
|
|
@@ -708,15 +709,20 @@
|
|
|
708
709
|
});
|
|
709
710
|
|
|
710
711
|
if (data === null) { resetButtons(); return; }
|
|
712
|
+
|
|
713
|
+
// Name taken by another user — show inline rename form.
|
|
714
|
+
if (!data.ok && data.name_taken) {
|
|
715
|
+
showRenameForm(ext, data.error);
|
|
716
|
+
modal.close();
|
|
717
|
+
resolve(false);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
711
721
|
if (!data.ok) throw new Error(data.error || "Publish failed");
|
|
712
722
|
|
|
713
723
|
done = true;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
cancelBtn.style.display = "none";
|
|
717
|
-
publishBtn.disabled = false;
|
|
718
|
-
publishBtn.textContent = t("pub.btn.done");
|
|
719
|
-
publishBtn.onclick = () => { modal.close(); resolve(true); };
|
|
724
|
+
modal.close();
|
|
725
|
+
resolve(true);
|
|
720
726
|
} catch (e) {
|
|
721
727
|
const msg = e.message || "";
|
|
722
728
|
const isConflict = /must be greater than/i.test(msg);
|
|
@@ -734,7 +740,84 @@
|
|
|
734
740
|
}
|
|
735
741
|
}
|
|
736
742
|
});
|
|
737
|
-
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Show an inline modal that lets the creator pick a new extension ID when
|
|
746
|
+
// the current name is already taken by another user on the marketplace.
|
|
747
|
+
// On confirm: calls set_id to rename the local folder + ext.yml, then
|
|
748
|
+
// immediately re-triggers the full publish flow with the new id.
|
|
749
|
+
function showRenameForm(ext, serverError) {
|
|
750
|
+
const slugFormat = /^[a-z0-9][a-z0-9\-]*[a-z0-9]$/;
|
|
751
|
+
|
|
752
|
+
const errorMsg = el("p", { class: "studio-modal-status studio-modal-status-error",
|
|
753
|
+
text: t("err.name_taken", { id: ext.id }) });
|
|
754
|
+
|
|
755
|
+
const field = el("div", { class: "studio-field" });
|
|
756
|
+
field.appendChild(el("label", { class: "studio-label", text: t("rename.label") }));
|
|
757
|
+
const input = el("input", { class: "studio-input", type: "text",
|
|
758
|
+
value: ext.id, placeholder: t("rename.placeholder") });
|
|
759
|
+
const hint = el("p", { class: "studio-field-hint", text: t("rename.hint") });
|
|
760
|
+
field.appendChild(input);
|
|
761
|
+
field.appendChild(hint);
|
|
762
|
+
|
|
763
|
+
const status = el("p", { class: "studio-modal-status" });
|
|
764
|
+
status.style.display = "none";
|
|
765
|
+
|
|
766
|
+
let done = false;
|
|
767
|
+
const modal = openModal({
|
|
768
|
+
title: t("rename.btn"),
|
|
769
|
+
body: el("div", null, [field, errorMsg, status]),
|
|
770
|
+
buttons: [
|
|
771
|
+
{ label: t("pub.btn.cancel"), keepOpen: false, onClick: () => {} },
|
|
772
|
+
{ label: t("rename.btn"), primary: true, keepOpen: true, onClick: () => submit() },
|
|
773
|
+
],
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const confirmBtn = modal.footer.querySelector(".studio-btn-primary");
|
|
777
|
+
const cancelBtn = modal.footer.querySelector(".studio-btn:not(.studio-btn-primary)");
|
|
778
|
+
|
|
779
|
+
input.addEventListener("input", () => {
|
|
780
|
+
const val = input.value.trim();
|
|
781
|
+
const valid = val.length >= 2 && val.length <= 64 && slugFormat.test(val) && val !== ext.id;
|
|
782
|
+
confirmBtn.disabled = !valid;
|
|
783
|
+
input.classList.toggle("studio-input-error", !!val && !slugFormat.test(val));
|
|
784
|
+
status.style.display = "none";
|
|
785
|
+
});
|
|
786
|
+
// Disable confirm until user types a valid different id.
|
|
787
|
+
confirmBtn.disabled = true;
|
|
788
|
+
|
|
789
|
+
async function submit() {
|
|
790
|
+
if (done || confirmBtn.disabled) return;
|
|
791
|
+
const newId = input.value.trim();
|
|
792
|
+
confirmBtn.disabled = true;
|
|
793
|
+
confirmBtn.textContent = t("rename.renaming");
|
|
794
|
+
cancelBtn.disabled = true;
|
|
795
|
+
input.disabled = true;
|
|
796
|
+
status.style.display = "none";
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
await postJson("/set_id", { ext_id: ext.id, new_id: newId });
|
|
800
|
+
done = true;
|
|
801
|
+
modal.close();
|
|
802
|
+
// Re-trigger the full publish flow with the renamed extension.
|
|
803
|
+
// Reload store first so the new id is visible.
|
|
804
|
+
await store.reload();
|
|
805
|
+
const renamed = store.extensions.find((e) => e.id === newId);
|
|
806
|
+
if (renamed) {
|
|
807
|
+
const ok = await runPublishFlow(renamed, null, { skipEntryCheck: true });
|
|
808
|
+
if (ok) store.reload();
|
|
809
|
+
}
|
|
810
|
+
} catch (e) {
|
|
811
|
+
status.textContent = t("err.generic", { msg: e.message });
|
|
812
|
+
status.style.display = "";
|
|
813
|
+
status.className = "studio-modal-status studio-modal-status-error";
|
|
814
|
+
confirmBtn.disabled = false;
|
|
815
|
+
confirmBtn.textContent = t("rename.btn");
|
|
816
|
+
cancelBtn.disabled = false;
|
|
817
|
+
input.disabled = false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
738
821
|
|
|
739
822
|
// Both tabs care about "which local extension am I working on", so we keep a
|
|
740
823
|
// tiny page-level store and let each tab subscribe to changes.
|
|
@@ -837,12 +920,6 @@
|
|
|
837
920
|
if (box) { box.innerHTML = ""; }
|
|
838
921
|
}
|
|
839
922
|
|
|
840
|
-
function renderDetail() {
|
|
841
|
-
const ext = store.selected();
|
|
842
|
-
if (!ext) return;
|
|
843
|
-
container.appendChild(buildExtInfoCard(ext, () => openMetaEditModal(ext, () => store.reload().then(rebuild))));
|
|
844
|
-
}
|
|
845
|
-
|
|
846
923
|
function rebuild() {
|
|
847
924
|
if (!container) return;
|
|
848
925
|
container.innerHTML = "";
|
|
@@ -853,8 +930,6 @@
|
|
|
853
930
|
return;
|
|
854
931
|
}
|
|
855
932
|
|
|
856
|
-
renderDetail();
|
|
857
|
-
|
|
858
933
|
const bar = el("div", { class: "studio-actions" });
|
|
859
934
|
bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-verify-status", text: t("btn.recheck"), onclick: runVerify }));
|
|
860
935
|
bar.appendChild(el("span", { class: "studio-verify-summary" }));
|
|
@@ -881,16 +956,6 @@
|
|
|
881
956
|
|
|
882
957
|
// ── Publish tab ──────────────────────────────────────────────────────────
|
|
883
958
|
|
|
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
959
|
function buildExtInfoCard(ext, onEdit) {
|
|
895
960
|
const card = el("div", { class: "studio-detail" });
|
|
896
961
|
|
|
@@ -908,23 +973,6 @@
|
|
|
908
973
|
infoRow.appendChild(el("span", { class: "studio-info-value", text: ext.version || "—" }));
|
|
909
974
|
card.appendChild(infoRow);
|
|
910
975
|
|
|
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
976
|
card.appendChild(el("div", { class: "studio-label studio-section-label", text: t("detail.units") }));
|
|
929
977
|
if (!(ext.units || []).length) {
|
|
930
978
|
card.appendChild(el("p", { class: "studio-empty", text: t("detail.noUnits") }));
|
|
@@ -937,10 +985,30 @@
|
|
|
937
985
|
return card;
|
|
938
986
|
}
|
|
939
987
|
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
const panelUnits = (ext.units || []).filter((u) => u.kind === "panel");
|
|
988
|
+
function buildReadmeCard(extId, currentReadme, onEdit) {
|
|
989
|
+
const card = el("div", { class: "studio-detail studio-readme-card" });
|
|
943
990
|
|
|
991
|
+
const head = el("div", { class: "studio-detail-head" });
|
|
992
|
+
head.appendChild(el("h4", { class: "studio-detail-name", text: t("pub.readme.section") }));
|
|
993
|
+
const editBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: t("pub.readme.edit") });
|
|
994
|
+
editBtn.addEventListener("click", onEdit);
|
|
995
|
+
head.appendChild(editBtn);
|
|
996
|
+
card.appendChild(head);
|
|
997
|
+
|
|
998
|
+
if (currentReadme && currentReadme.trim()) {
|
|
999
|
+
const preview = el("div", { class: "studio-readme-preview extension-readme-body studio-readme-card-preview" });
|
|
1000
|
+
preview.innerHTML = typeof marked !== "undefined"
|
|
1001
|
+
? marked.parse(currentReadme, { breaks: true, gfm: true })
|
|
1002
|
+
: `<pre>${currentReadme}</pre>`;
|
|
1003
|
+
card.appendChild(preview);
|
|
1004
|
+
} else {
|
|
1005
|
+
card.appendChild(el("p", { class: "studio-empty", text: t("pub.readme.empty") }));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return card;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function openMetaEditModal(ext, onSaved) {
|
|
944
1012
|
const overlay = el("div", { class: "studio-modal-overlay" });
|
|
945
1013
|
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
946
1014
|
|
|
@@ -964,50 +1032,6 @@
|
|
|
964
1032
|
descField.appendChild(descInput);
|
|
965
1033
|
modal.appendChild(descField);
|
|
966
1034
|
|
|
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
1035
|
const statusEl = el("span", { class: "studio-form-status" });
|
|
1012
1036
|
const footer = el("div", { class: "studio-modal-footer" });
|
|
1013
1037
|
const cancelBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("pub.btn.cancel"), onclick: close });
|
|
@@ -1018,12 +1042,10 @@
|
|
|
1018
1042
|
saveBtn.textContent = t("meta.saving");
|
|
1019
1043
|
statusEl.textContent = "";
|
|
1020
1044
|
try {
|
|
1021
|
-
const payload = entries.map((ep) => ({ unit_id: ep.panel_id, slot: ep.slot }));
|
|
1022
1045
|
await postJson("/set_meta", {
|
|
1023
1046
|
ext_id: ext.id,
|
|
1024
1047
|
name: nameInput.value.trim(),
|
|
1025
1048
|
description: descInput.value.trim(),
|
|
1026
|
-
entry_points: payload,
|
|
1027
1049
|
});
|
|
1028
1050
|
close();
|
|
1029
1051
|
if (onSaved) onSaved();
|
|
@@ -1040,6 +1062,199 @@
|
|
|
1040
1062
|
modal.appendChild(footer);
|
|
1041
1063
|
}
|
|
1042
1064
|
|
|
1065
|
+
// Standalone README + screenshot editor modal, reachable from the published
|
|
1066
|
+
// extension list at any time (not just after first publish).
|
|
1067
|
+
|
|
1068
|
+
// Prompt user to fill in README before publishing.
|
|
1069
|
+
// Returns a Promise that resolves when the user is ready to continue.
|
|
1070
|
+
// If readme is missing, shows a dialog: "Add description" opens the editor,
|
|
1071
|
+
// "Skip" proceeds without filling in.
|
|
1072
|
+
// Options:
|
|
1073
|
+
// onSaved(readme) — called after user saves the readme in the editor,
|
|
1074
|
+
// use this to trigger a UI refresh in the calling panel.
|
|
1075
|
+
function confirmReadme(ext, { onSaved } = {}) {
|
|
1076
|
+
if (ext.readme && ext.readme.trim()) return Promise.resolve();
|
|
1077
|
+
return new Promise((resolve) => {
|
|
1078
|
+
const overlay = el("div", { class: "studio-modal-overlay" });
|
|
1079
|
+
const box = el("div", { class: "studio-modal", style: "max-width:420px;padding:24px 28px 20px" });
|
|
1080
|
+
box.appendChild(el("h3", { class: "studio-modal-title", style: "margin-bottom:10px", text: t("pub.readme.section") }));
|
|
1081
|
+
box.appendChild(el("p", { style: "margin-bottom:20px;color:var(--color-text-secondary);font-size:14px", text: t("pub.readme.required") }));
|
|
1082
|
+
const footer = el("div", { class: "studio-modal-footer" });
|
|
1083
|
+
const skipBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("pub.readme.required.skip") });
|
|
1084
|
+
const editBtn = el("button", { class: "studio-btn studio-btn-primary", text: t("pub.readme.required.edit") });
|
|
1085
|
+
function close(goEdit) {
|
|
1086
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
1087
|
+
if (goEdit) {
|
|
1088
|
+
openReadmeEditModal(ext.id, ext.readme || "", (saved) => {
|
|
1089
|
+
ext.readme = saved;
|
|
1090
|
+
if (onSaved) onSaved(saved);
|
|
1091
|
+
resolve();
|
|
1092
|
+
});
|
|
1093
|
+
} else {
|
|
1094
|
+
resolve();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
skipBtn.addEventListener("click", () => close(false));
|
|
1098
|
+
editBtn.addEventListener("click", () => close(true));
|
|
1099
|
+
footer.appendChild(skipBtn);
|
|
1100
|
+
footer.appendChild(editBtn);
|
|
1101
|
+
box.appendChild(footer);
|
|
1102
|
+
overlay.appendChild(box);
|
|
1103
|
+
document.body.appendChild(overlay);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function openReadmeEditModal(extId, initialReadme, onSaved) {
|
|
1108
|
+
// Use a wider full-overlay modal with a split editor/preview layout.
|
|
1109
|
+
const overlay = el("div", { class: "studio-modal-overlay studio-readme-overlay" });
|
|
1110
|
+
const box = el("div", { class: "studio-modal studio-readme-modal" });
|
|
1111
|
+
overlay.appendChild(box);
|
|
1112
|
+
document.body.appendChild(overlay);
|
|
1113
|
+
function close() { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); }
|
|
1114
|
+
|
|
1115
|
+
box.appendChild(el("h3", { class: "studio-modal-title", text: t("pub.readme.edit.title") }));
|
|
1116
|
+
|
|
1117
|
+
// Split pane: left = textarea, right = rendered preview
|
|
1118
|
+
const splitPane = el("div", { class: "studio-readme-split" });
|
|
1119
|
+
|
|
1120
|
+
const editorPane = el("div", { class: "studio-readme-pane" });
|
|
1121
|
+
editorPane.appendChild(el("div", { class: "studio-readme-pane-label", text: t("readme.pane.editor") }));
|
|
1122
|
+
const readmeArea = el("textarea", {
|
|
1123
|
+
class: "studio-textarea studio-readme-textarea",
|
|
1124
|
+
placeholder: t("pub.readme.placeholder"),
|
|
1125
|
+
});
|
|
1126
|
+
// Pre-fill with existing readme content
|
|
1127
|
+
if (initialReadme) {
|
|
1128
|
+
readmeArea.value = initialReadme;
|
|
1129
|
+
}
|
|
1130
|
+
editorPane.appendChild(readmeArea);
|
|
1131
|
+
splitPane.appendChild(editorPane);
|
|
1132
|
+
|
|
1133
|
+
const previewPane = el("div", { class: "studio-readme-pane" });
|
|
1134
|
+
previewPane.appendChild(el("div", { class: "studio-readme-pane-label", text: t("readme.pane.preview") }));
|
|
1135
|
+
const previewBody = el("div", { class: "studio-readme-preview extension-readme-body" });
|
|
1136
|
+
previewPane.appendChild(previewBody);
|
|
1137
|
+
splitPane.appendChild(previewPane);
|
|
1138
|
+
|
|
1139
|
+
box.appendChild(splitPane);
|
|
1140
|
+
|
|
1141
|
+
// Render helper
|
|
1142
|
+
function renderPreview() {
|
|
1143
|
+
const md = readmeArea.value;
|
|
1144
|
+
previewBody.innerHTML = md && typeof marked !== "undefined"
|
|
1145
|
+
? marked.parse(md, { breaks: true, gfm: true })
|
|
1146
|
+
: (md ? `<pre>${md}</pre>` : "");
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Live preview on input
|
|
1150
|
+
readmeArea.addEventListener("input", renderPreview);
|
|
1151
|
+
|
|
1152
|
+
// Initial preview if there's pre-filled content
|
|
1153
|
+
if (readmeArea.value) renderPreview();
|
|
1154
|
+
|
|
1155
|
+
// Screenshots section
|
|
1156
|
+
const screenshotsSection = el("div", { class: "studio-readme-screenshots" });
|
|
1157
|
+
|
|
1158
|
+
const screenshotRow = el("div", { class: "studio-readme-screenshots-row" });
|
|
1159
|
+
|
|
1160
|
+
const fileInput = el("input", { type: "file", accept: "image/png,image/jpeg,image/gif", style: "display:none" });
|
|
1161
|
+
screenshotsSection.appendChild(fileInput);
|
|
1162
|
+
|
|
1163
|
+
const uploadBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: t("pub.screenshot.upload") });
|
|
1164
|
+
uploadBtn.addEventListener("click", () => fileInput.click());
|
|
1165
|
+
screenshotRow.appendChild(uploadBtn);
|
|
1166
|
+
screenshotRow.appendChild(el("span", { class: "studio-readme-screenshots-hint", text: t("pub.screenshot.hint") }));
|
|
1167
|
+
screenshotsSection.appendChild(screenshotRow);
|
|
1168
|
+
|
|
1169
|
+
const screenshotErr = el("p", { class: "studio-modal-status studio-modal-status-error", style: "display:none;margin-top:6px" });
|
|
1170
|
+
screenshotsSection.appendChild(screenshotErr);
|
|
1171
|
+
fileInput.addEventListener("change", async () => {
|
|
1172
|
+
const file = fileInput.files[0];
|
|
1173
|
+
if (!file) return;
|
|
1174
|
+
fileInput.value = "";
|
|
1175
|
+
screenshotErr.style.display = "none";
|
|
1176
|
+
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
|
1177
|
+
screenshotErr.textContent = t("pub.screenshot.err", { msg: t("pub.screenshot.err.type") });
|
|
1178
|
+
screenshotErr.style.display = "";
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
1182
|
+
screenshotErr.textContent = t("pub.screenshot.err", { msg: t("pub.screenshot.err.size") });
|
|
1183
|
+
screenshotErr.style.display = "";
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
uploadBtn.disabled = true;
|
|
1187
|
+
uploadBtn.textContent = t("pub.screenshot.uploading");
|
|
1188
|
+
screenshotErr.style.display = "none";
|
|
1189
|
+
try {
|
|
1190
|
+
const form = new FormData();
|
|
1191
|
+
form.append("ext_id", extId);
|
|
1192
|
+
form.append("file", file);
|
|
1193
|
+
const res = await fetch("/api/ext/ext-studio/screenshot", { method: "POST", body: form });
|
|
1194
|
+
const json = await res.json();
|
|
1195
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "upload failed");
|
|
1196
|
+
// Insert  at cursor position in the editor.
|
|
1197
|
+
// Ensure the image tag is on its own line by adding newlines if needed.
|
|
1198
|
+
const mdImg = ``;
|
|
1199
|
+
const start = readmeArea.selectionStart;
|
|
1200
|
+
const end = readmeArea.selectionEnd;
|
|
1201
|
+
const before = readmeArea.value.slice(0, start);
|
|
1202
|
+
const after = readmeArea.value.slice(end);
|
|
1203
|
+
const prefix = before.length > 0 && !before.endsWith("\n") ? "\n" : "";
|
|
1204
|
+
const suffix = after.length > 0 && !after.startsWith("\n") ? "\n" : "";
|
|
1205
|
+
const inserted = prefix + mdImg + suffix;
|
|
1206
|
+
readmeArea.value = before + inserted + after;
|
|
1207
|
+
readmeArea.selectionStart = readmeArea.selectionEnd = start + inserted.length;
|
|
1208
|
+
readmeArea.focus();
|
|
1209
|
+
readmeArea.dispatchEvent(new Event("input"));
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
screenshotErr.textContent = t("pub.screenshot.err", { msg: e.message });
|
|
1212
|
+
screenshotErr.style.display = "";
|
|
1213
|
+
} finally {
|
|
1214
|
+
uploadBtn.disabled = false;
|
|
1215
|
+
uploadBtn.textContent = t("pub.screenshot.upload");
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
box.appendChild(screenshotsSection);
|
|
1220
|
+
|
|
1221
|
+
const statusEl = el("p", { class: "studio-modal-status", style: "display:none;margin-top:8px" });
|
|
1222
|
+
box.appendChild(statusEl);
|
|
1223
|
+
|
|
1224
|
+
const footer = el("div", { class: "studio-modal-footer" });
|
|
1225
|
+
const cancelBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("pub.btn.cancel") });
|
|
1226
|
+
cancelBtn.addEventListener("click", close);
|
|
1227
|
+
const saveBtn = el("button", { class: "studio-btn studio-btn-primary", text: t("pub.readme.save") });
|
|
1228
|
+
saveBtn.addEventListener("click", async () => {
|
|
1229
|
+
const readme = readmeArea.value.trim();
|
|
1230
|
+
if (!readme) {
|
|
1231
|
+
statusEl.className = "studio-modal-status studio-modal-status-error";
|
|
1232
|
+
statusEl.textContent = t("pub.readme.save.empty");
|
|
1233
|
+
statusEl.style.display = "";
|
|
1234
|
+
readmeArea.focus();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
saveBtn.disabled = true;
|
|
1238
|
+
saveBtn.textContent = t("pub.readme.saving");
|
|
1239
|
+
cancelBtn.disabled = true;
|
|
1240
|
+
statusEl.style.display = "none";
|
|
1241
|
+
try {
|
|
1242
|
+
await patchJson("/readme", { ext_id: extId, readme });
|
|
1243
|
+
close();
|
|
1244
|
+
if (onSaved) onSaved(readme);
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
statusEl.textContent = t("err.generic", { msg: e.message });
|
|
1247
|
+
statusEl.style.display = "";
|
|
1248
|
+
saveBtn.disabled = false;
|
|
1249
|
+
saveBtn.textContent = t("pub.readme.save");
|
|
1250
|
+
cancelBtn.disabled = false;
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
footer.appendChild(cancelBtn);
|
|
1254
|
+
footer.appendChild(saveBtn);
|
|
1255
|
+
box.appendChild(footer);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1043
1258
|
function createPublishPanel() {
|
|
1044
1259
|
let container = null;
|
|
1045
1260
|
let unsub = null;
|
|
@@ -1052,13 +1267,14 @@
|
|
|
1052
1267
|
async function doPublish() {
|
|
1053
1268
|
const ext = store.selected();
|
|
1054
1269
|
if (!ext) return;
|
|
1270
|
+
await confirmReadme(ext, { onSaved: () => rebuild() });
|
|
1055
1271
|
const prevVersionPromise = getJson("/published")
|
|
1056
1272
|
.then((data) => {
|
|
1057
1273
|
const match = (data.extensions || []).find((e) => e.id === ext.id);
|
|
1058
1274
|
return match ? match.version : null;
|
|
1059
1275
|
})
|
|
1060
1276
|
.catch(() => null);
|
|
1061
|
-
const ok = await runPublishFlow(ext, prevVersionPromise);
|
|
1277
|
+
const ok = await runPublishFlow(ext, prevVersionPromise, { onPublishDone: loadPublished });
|
|
1062
1278
|
if (ok) loadPublished();
|
|
1063
1279
|
}
|
|
1064
1280
|
|
|
@@ -1134,7 +1350,16 @@
|
|
|
1134
1350
|
const ext = store.selected();
|
|
1135
1351
|
if (!ext) return;
|
|
1136
1352
|
|
|
1137
|
-
container.appendChild(buildExtInfoCard(
|
|
1353
|
+
container.appendChild(buildExtInfoCard(
|
|
1354
|
+
ext,
|
|
1355
|
+
() => openMetaEditModal(ext, () => store.reload().then(rebuild))
|
|
1356
|
+
));
|
|
1357
|
+
|
|
1358
|
+
container.appendChild(buildReadmeCard(
|
|
1359
|
+
ext.id,
|
|
1360
|
+
ext.readme,
|
|
1361
|
+
() => openReadmeEditModal(ext.id, ext.readme, () => store.reload().then(rebuild))
|
|
1362
|
+
));
|
|
1138
1363
|
|
|
1139
1364
|
const bar = el("div", { class: "studio-actions" });
|
|
1140
1365
|
bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish }));
|
|
@@ -1233,7 +1458,7 @@
|
|
|
1233
1458
|
|
|
1234
1459
|
const actions = el("div", { class: "studio-actions" });
|
|
1235
1460
|
actions.appendChild(el("button", { class: "studio-btn studio-btn-primary", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
|
|
1236
|
-
const un = el("button", { class: "studio-btn studio-btn-ghost
|
|
1461
|
+
const un = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.unpublish") });
|
|
1237
1462
|
un.addEventListener("click", async () => {
|
|
1238
1463
|
if (!window.confirm(t("extlist.unpublish.confirm", { id: ext.id }))) return;
|
|
1239
1464
|
un.disabled = true;
|
|
@@ -1317,7 +1542,9 @@
|
|
|
1317
1542
|
}
|
|
1318
1543
|
|
|
1319
1544
|
async function doPublish(ext, prevVersion) {
|
|
1320
|
-
|
|
1545
|
+
await confirmReadme(ext, { onSaved: () => { reload().then(rebuild); } });
|
|
1546
|
+
const onDone = async () => { await reload(); rebuild(); };
|
|
1547
|
+
const ok = await runPublishFlow(ext, prevVersion, { onPublishDone: onDone });
|
|
1321
1548
|
if (ok) { await reload(); rebuild(); }
|
|
1322
1549
|
}
|
|
1323
1550
|
|
|
@@ -1723,13 +1950,25 @@
|
|
|
1723
1950
|
.studio-section-label { margin-top: 10px; margin-bottom: 6px; }
|
|
1724
1951
|
.studio-detail-head .studio-detail-name { margin: 0; }
|
|
1725
1952
|
.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
1953
|
.studio-btn-sm { padding: 4px 10px; font-size: 12px; }
|
|
1732
|
-
.studio-
|
|
1954
|
+
.studio-readme-overlay { align-items: center; justify-content: center; }
|
|
1955
|
+
.studio-readme-modal { width: min(1200px, calc(100vw - 48px)); height: calc(100vh - 80px); max-width: calc(100vw - 48px); border-radius: var(--radius-md, 8px); margin: 0; padding: 24px 28px 20px; display: flex; flex-direction: column; }
|
|
1956
|
+
.studio-readme-modal .studio-modal-title { margin-bottom: 16px; flex-shrink: 0; }
|
|
1957
|
+
.studio-readme-split { display: flex; gap: 14px; flex: 1; min-height: 0; margin-bottom: 18px; }
|
|
1958
|
+
.studio-readme-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
1959
|
+
.studio-readme-pane-label { font-size: 12px; font-weight: 600; color: var(--color-text-muted); letter-spacing: 0.02em; padding-bottom: 6px; border-bottom: 1px solid var(--color-border-primary); margin-bottom: 8px; }
|
|
1960
|
+
.studio-readme-textarea { flex: 1; resize: none; font-family: monospace; font-size: 12.5px; line-height: 1.65; border-radius: var(--radius-sm); }
|
|
1961
|
+
.studio-readme-preview { flex: 1; overflow-y: auto; overflow-x: hidden; border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 12px 16px; background: var(--color-bg-secondary); font-size: 13px; line-height: 1.75; }
|
|
1962
|
+
.studio-readme-preview > *:first-child { margin-top: 0; }
|
|
1963
|
+
.studio-readme-preview img { max-width: 100%; height: auto; display: block; border-radius: var(--radius-sm); }
|
|
1964
|
+
.studio-readme-screenshots { border-top: 1px solid var(--color-border-primary); padding-top: 14px; margin-bottom: 6px; }
|
|
1965
|
+
.studio-readme-screenshots-row { display: flex; align-items: center; gap: 10px; }
|
|
1966
|
+
.studio-readme-screenshots-hint { font-size: 11px; color: var(--color-text-muted); }
|
|
1967
|
+
.studio-readme-card { margin-top: 8px; }
|
|
1968
|
+
.studio-readme-card-preview { max-height: 200px; overflow-y: auto; font-size: 13px; }
|
|
1969
|
+
.extension-readme-body img { max-width: min(800px, 100%); height: auto; }
|
|
1970
|
+
.studio-link-btn { background: none; border: none; padding: 0; font-size: 12px; color: var(--color-accent-primary); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
|
1971
|
+
.studio-link-btn:hover { color: var(--color-accent-hover); }
|
|
1733
1972
|
`;
|
|
1734
1973
|
document.head.appendChild(style);
|
|
1735
1974
|
})();
|