Sync third-party and MCP marketplace plugins

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot
2026-06-19 00:01:23 +08:00
parent 0ad0decf6e
commit de4fde21b7
13 changed files with 325 additions and 100 deletions
+36 -36
View File
@@ -196,18 +196,6 @@
}, },
"category": "设计" "category": "设计"
}, },
{
"name": "shadcn",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/shadcn"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{ {
"name": "frontend-slides", "name": "frontend-slides",
"source": { "source": {
@@ -244,18 +232,6 @@
}, },
"category": "文档处理" "category": "文档处理"
}, },
{
"name": "ppt-master",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/ppt-master"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
},
{ {
"name": "next-skills", "name": "next-skills",
"source": { "source": {
@@ -280,18 +256,6 @@
}, },
"category": "开发工具" "category": "开发工具"
}, },
{
"name": "mcp-playwright",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/mcp-playwright"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "MCP"
},
{ {
"name": "eapil-llmapi-structured", "name": "eapil-llmapi-structured",
"source": { "source": {
@@ -315,6 +279,42 @@
"authentication": "ON_INSTALL" "authentication": "ON_INSTALL"
}, },
"category": "知识库与检索" "category": "知识库与检索"
},
{
"name": "shadcn",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/shadcn"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "ppt-master",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/ppt-master"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
},
{
"name": "mcp-playwright",
"source": {
"source": "local",
"path": "./plugins/codex/plugins/mcp-playwright"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "MCP"
} }
] ]
} }
+4 -4
View File
@@ -33,8 +33,8 @@
"repo": "https://github.com/shadcn-ui/ui.git", "repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "c879483c9694d7b135ed1efe8f73233d924ae551", "commit": "c2ddedf5d24e0cee05936c69e1f199a9975a07b2",
"syncedAt": "2026-06-17T07:05:29Z" "syncedAt": "2026-06-18T16:00:00Z"
}, },
{ {
"id": "frontend-slides", "id": "frontend-slides",
@@ -69,8 +69,8 @@
"repo": "https://github.com/hugohe3/ppt-master.git", "repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main", "ref": "main",
"adapter": "claude-skill", "adapter": "claude-skill",
"commit": "313ee6729dba50cbdd50b67debe601c483a5bcab", "commit": "7249902c363f0b33dee9a9f7112fb258ae9c3bf7",
"syncedAt": "2026-06-17T07:05:29Z" "syncedAt": "2026-06-18T16:00:00Z"
}, },
{ {
"id": "next-skills", "id": "next-skills",
+36 -36
View File
@@ -196,18 +196,6 @@
}, },
"category": "设计" "category": "设计"
}, },
{
"name": "shadcn",
"source": {
"source": "local",
"path": "./plugins/shadcn"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{ {
"name": "frontend-slides", "name": "frontend-slides",
"source": { "source": {
@@ -244,18 +232,6 @@
}, },
"category": "文档处理" "category": "文档处理"
}, },
{
"name": "ppt-master",
"source": {
"source": "local",
"path": "./plugins/ppt-master"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
},
{ {
"name": "next-skills", "name": "next-skills",
"source": { "source": {
@@ -280,18 +256,6 @@
}, },
"category": "开发工具" "category": "开发工具"
}, },
{
"name": "mcp-playwright",
"source": {
"source": "local",
"path": "./plugins/mcp-playwright"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "MCP"
},
{ {
"name": "eapil-llmapi-structured", "name": "eapil-llmapi-structured",
"source": { "source": {
@@ -315,6 +279,42 @@
"authentication": "ON_INSTALL" "authentication": "ON_INSTALL"
}, },
"category": "知识库与检索" "category": "知识库与检索"
},
{
"name": "shadcn",
"source": {
"source": "local",
"path": "./plugins/shadcn"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "ppt-master",
"source": {
"source": "local",
"path": "./plugins/ppt-master"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
},
{
"name": "mcp-playwright",
"source": {
"source": "local",
"path": "./plugins/mcp-playwright"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "MCP"
} }
] ]
} }
@@ -3,5 +3,5 @@
"name": "playwright浏览器自动化操作", "name": "playwright浏览器自动化操作",
"version": "20260605", "version": "20260605",
"keySource": "none", "keySource": "none",
"syncedAt": "2026-06-17T16:01:07Z" "syncedAt": "2026-06-18T16:01:22Z"
} }
@@ -2,8 +2,8 @@
"sourceId": "ppt-master", "sourceId": "ppt-master",
"repo": "https://github.com/hugohe3/ppt-master.git", "repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main", "ref": "main",
"commit": "313ee6729dba50cbdd50b67debe601c483a5bcab", "commit": "7249902c363f0b33dee9a9f7112fb258ae9c3bf7",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": "skills/ppt-master", "sourcePath": "skills/ppt-master",
"syncedAt": "2026-06-17T07:05:29Z" "syncedAt": "2026-06-18T16:00:00Z"
} }
@@ -10,6 +10,8 @@ Photo-led editorial — large full-bleed photography dominates the page, text po
- Decoration: thin rules, section numbering, small figure notes; nothing competes with the image. - Decoration: thin rules, section numbering, small figure notes; nothing competes with the image.
- Whitespace: generous around text; the photo fills, the type breathes beside it. Asymmetric magazine composition. - Whitespace: generous around text; the photo fills, the type breathes beside it. Asymmetric magazine composition.
> **No usable image → fall back to `editorial`.** This style's spine is the photograph; when a page has no suitable image available, render it in the `editorial` text-led layout (magazine columns) rather than a full-bleed placeholder — an empty / dashed image frame contradicts the style's whole premise. A deterministic, observable condition, not a judgment call.
## 2. Typography character ## 2. Typography character
- Editorial serif / CJK title × clean sans body; magazine-column cadence; small precise captions and figure notes. Words are concise — they point, they don't fill. - Editorial serif / CJK title × clean sans body; magazine-column cadence; small precise captions and figure notes. Words are concise — they point, they don't fill.
@@ -64,6 +64,8 @@
latin: "Latin", latin: "Latin",
sample_cjk: "数字化转型战略", sample_cjk: "数字化转型战略",
sample_latin: "Digital Transformation", sample_latin: "Digital Transformation",
style_preview_label: "Overall impression (color + typography)",
style_preview_body: "· rough feel only, not the actual slide layout",
mode_continuous_desc: "Generate the whole deck in one pass.", mode_continuous_desc: "Generate the whole deck in one pass.",
mode_split_desc: "Stop after the spec; resume SVG generation in a fresh window.", mode_split_desc: "Stop after the spec; resume SVG generation in a fresh window.",
refine_off_desc: "Spec is written in one go; the pipeline auto-proceeds.", refine_off_desc: "Spec is written in one go; the pipeline auto-proceeds.",
@@ -127,6 +129,8 @@
latin: "西文", latin: "西文",
sample_cjk: "数字化转型战略", sample_cjk: "数字化转型战略",
sample_latin: "Digital Transformation", sample_latin: "Digital Transformation",
style_preview_label: "整体形象(配色 + 字体)",
style_preview_body: "· 仅大致形象,非实际版式",
mode_continuous_desc: "一次性连续生成整份演示文稿。", mode_continuous_desc: "一次性连续生成整份演示文稿。",
mode_split_desc: "写完设计规范后停止,另开窗口继续生成页面。", mode_split_desc: "写完设计规范后停止,另开窗口继续生成页面。",
refine_off_desc: "设计规范一次写完,流程自动继续。", refine_off_desc: "设计规范一次写完,流程自动继续。",
@@ -445,6 +449,19 @@
return spec.candidates || spec.options || []; return spec.candidates || spec.options || [];
} }
function usesCustomImagePlanValue(value) {
var ids = (CAT.image_usage || []).map(function (item) { return item.id; });
return value && ids.indexOf(value) === -1;
}
function customImagePlanHasAiSignal() {
return imageStrategyCandidates().length > 0 || !!recId("image_ai_path");
}
function needsGeneratedImagesForUsage(value) {
return value === "ai" || (usesCustomImagePlanValue(value) && customImagePlanHasAiSignal());
}
function imageStrategySelectedIndex() { function imageStrategySelectedIndex() {
var spec = imageStrategySpec(); var spec = imageStrategySpec();
var idx = spec.selected || 0; var idx = spec.selected || 0;
@@ -495,6 +512,18 @@
"body_text" "body_text"
]; ];
function normHex(val) {
var v = (val || "").trim();
if (!/^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(v)) return null;
return v.charAt(0) === "#" ? v : "#" + v;
}
function hexOr(val, fallback) {
return normHex(val) || fallback;
}
// Replaced when the combined color+typography preview mounts; the color and
// typography sections call it after every change so the preview stays live.
var refreshStylePreview = function () {};
function renderColor(host) { function renderColor(host) {
var cands = (REC.color && REC.color.candidates) || []; var cands = (REC.color && REC.color.candidates) || [];
var sec = section(5, "sec_color"); var sec = section(5, "sec_color");
@@ -504,11 +533,6 @@
var cardSwatchRefs = []; // idx -> {role: swatchEl}, for live override feedback var cardSwatchRefs = []; // idx -> {role: swatchEl}, for live override feedback
var selectedIdx = -1; var selectedIdx = -1;
function normHex(val) {
var v = (val || "").trim();
if (!/^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(v)) return null;
return v.charAt(0) === "#" ? v : "#" + v;
}
function paintSwatch(elem, val) { function paintSwatch(elem, val) {
var n = normHex(val); var n = normHex(val);
elem.style.background = n || "transparent"; elem.style.background = n || "transparent";
@@ -526,6 +550,7 @@
STATE.color = { name: c.name || "", palette: Object.assign({}, normPalette(c)) }; STATE.color = { name: c.name || "", palette: Object.assign({}, normPalette(c)) };
grid.querySelectorAll(".color-card").forEach(function (card, i) { card.classList.toggle("selected", i === idx); }); grid.querySelectorAll(".color-card").forEach(function (card, i) { card.classList.toggle("selected", i === idx); });
applyHexInputs(STATE.color.palette); applyHexInputs(STATE.color.palette);
refreshStylePreview();
} }
cands.forEach(function (c, idx) { cands.forEach(function (c, idx) {
@@ -571,6 +596,7 @@
if (n && selectedIdx >= 0 && cardSwatchRefs[selectedIdx] && cardSwatchRefs[selectedIdx][role]) { if (n && selectedIdx >= 0 && cardSwatchRefs[selectedIdx] && cardSwatchRefs[selectedIdx][role]) {
cardSwatchRefs[selectedIdx][role].style.background = n; cardSwatchRefs[selectedIdx][role].style.background = n;
} }
refreshStylePreview();
}); });
hexInputs[role] = inp; hexSwatches[role] = sw; hexInputs[role] = inp; hexSwatches[role] = sw;
line.appendChild(sw); line.appendChild(inp); line.appendChild(sw); line.appendChild(inp);
@@ -593,11 +619,22 @@
host.appendChild(sec); host.appendChild(sec);
} }
function previewFontStack(primary, fallback) {
if (!primary) return fallback || "";
if (!fallback) return primary;
return primary + ", " + fallback;
}
function fontSample(box, slot, css) { function fontSample(box, slot, css) {
var line = el("div", "font-sample-line"); var line = el("div", "font-sample-line");
var cjk = el("span", "fs-cjk", slot.sample_cjk || t("sample_cjk")); var cjk = el("span", "fs-cjk", slot.sample_cjk || t("sample_cjk"));
var lat = el("span", "fs-latin", slot.sample_latin || t("sample_latin")); var lat = el("span", "fs-latin", slot.sample_latin || t("sample_latin"));
if (css) { cjk.style.fontFamily = css; lat.style.fontFamily = css; } var cjkStack = previewFontStack(slot.cjk, css);
var latinStack = previewFontStack(slot.latin, css);
if (cjkStack) cjk.style.fontFamily = cjkStack;
if (latinStack) lat.style.fontFamily = latinStack;
if (cjkStack) cjk.title = cjkStack;
if (latinStack) lat.title = latinStack;
line.appendChild(cjk); line.appendChild(lat); box.appendChild(line); line.appendChild(cjk); line.appendChild(lat); box.appendChild(line);
} }
@@ -622,6 +659,7 @@
if (sizeInput) sizeInput.value = STATE.typography.body_size || ""; if (sizeInput) sizeInput.value = STATE.typography.body_size || "";
customInput.style.display = "none"; customInput.style.display = "none";
grid.querySelectorAll(".font-card").forEach(function (card, i) { card.classList.toggle("selected", i === idx); }); grid.querySelectorAll(".font-card").forEach(function (card, i) { card.classList.toggle("selected", i === idx); });
refreshStylePreview();
} }
function selectCustomTypography() { function selectCustomTypography() {
@@ -636,6 +674,7 @@
customCard.classList.add("selected"); customCard.classList.add("selected");
customInput.style.display = "block"; customInput.style.display = "block";
customInput.focus(); customInput.focus();
refreshStylePreview();
} }
cands.forEach(function (c, idx) { cands.forEach(function (c, idx) {
@@ -663,6 +702,7 @@
customInput.addEventListener("input", function () { customInput.addEventListener("input", function () {
if (!STATE.typography || STATE.typography.name !== "custom") selectCustomTypography(); if (!STATE.typography || STATE.typography.name !== "custom") selectCustomTypography();
STATE.typography.custom = customInput.value; STATE.typography.custom = customInput.value;
refreshStylePreview();
}); });
sec.appendChild(customInput); sec.appendChild(customInput);
@@ -679,6 +719,7 @@
sizeInput.addEventListener("input", function () { sizeInput.addEventListener("input", function () {
if (!STATE.typography) STATE.typography = { name: "", heading: {}, body: {} }; if (!STATE.typography) STATE.typography = { name: "", heading: {}, body: {} };
STATE.typography.body_size = sizeInput.value; STATE.typography.body_size = sizeInput.value;
refreshStylePreview();
}); });
sizeRow.appendChild(sizeInput); sizeRow.appendChild(sizeInput);
sizeRow.appendChild(el("div", "toggle-desc", t("font_body_size_hint"))); sizeRow.appendChild(el("div", "toggle-desc", t("font_body_size_hint")));
@@ -702,6 +743,86 @@
} }
} }
// Combined color + typography preview — not a ninth confirmation, just a
// live "overall impression" of the two style choices made above. Kept
// deliberately abstract (a style chip, not a slide layout); page layout
// preview is the live-preview server's job (Step 6).
function renderStylePreview(host) {
var wrap = el("div", "style-preview");
var label = el("div", "style-preview-label");
label.appendChild(el("span", "spl-title", t("style_preview_label")));
// The "rough feel, not a slide layout" caveat sits in the label in the
// UI font — never rendered in the candidate's body font, so it cannot
// pose as sample content.
label.appendChild(el("span", "spl-note", t("style_preview_body")));
wrap.appendChild(label);
var card = el("div", "style-preview-card");
var textcol = el("div", "sp-textcol");
var title = el("div", "sp-title");
var titleCjk = el("span", "sp-title-cjk");
var titleLat = el("span", "sp-title-lat");
title.appendChild(titleCjk); title.appendChild(titleLat);
var bodyRow = el("div", "sp-body");
var accentBar = el("span", "sp-accent-bar");
var bodyWrap = el("div", "sp-body-wrap");
var bodyCjk = el("span", "sp-body-cjk");
var bodyLat = el("span", "sp-body-lat");
bodyWrap.appendChild(bodyCjk); bodyWrap.appendChild(bodyLat);
bodyRow.appendChild(accentBar); bodyRow.appendChild(bodyWrap);
textcol.appendChild(title); textcol.appendChild(bodyRow);
var chip = el("div", "sp-chip");
var chipDot = el("span", "sp-chip-dot");
var chipLabel = el("span", "sp-chip-label");
chip.appendChild(chipDot); chip.appendChild(chipLabel);
card.appendChild(textcol); card.appendChild(chip);
wrap.appendChild(card);
host.appendChild(wrap);
// Pin the strip just under the sticky topbar so it stays visible while
// the user scrolls through the color / icon / typography sections.
var topbar = document.getElementById("topbar");
if (topbar) wrap.style.top = topbar.offsetHeight + "px";
function paint() {
var pal = (STATE.color && STATE.color.palette) || {};
var typ = STATE.typography || {};
var head = typ.heading || {}, body = typ.body || {};
var bg = hexOr(pal.background, "#ffffff");
var sbg = hexOr(pal.secondary_bg, bg);
var pri = hexOr(pal.primary, "#1a3a6b");
var acc = hexOr(pal.accent, pri);
var sacc = hexOr(pal.secondary_accent, acc);
var txt = hexOr(pal.body_text, "#1d2430");
var bodyPx = Math.max(12, Math.min(26, parseInt(typ.body_size, 10) || 18));
var headStack = previewFontStack(head.cjk, head.css);
var headLatStack = previewFontStack(head.latin, head.css);
var bodyStack = previewFontStack(body.cjk, body.css);
var bodyLatStack = previewFontStack(body.latin, body.css);
card.style.background = bg;
titleCjk.textContent = head.sample_cjk || t("sample_cjk");
titleLat.textContent = head.sample_latin || t("sample_latin");
title.style.color = pri;
title.style.fontSize = Math.round(bodyPx * 1.7) + "px";
titleCjk.style.fontFamily = headStack || "";
titleLat.style.fontFamily = headLatStack || "";
// CJK and Latin previewed with their own stacks (mirrors the title
// and the per-card font samples) so each script's font is visible.
bodyCjk.textContent = body.sample_cjk || t("sample_cjk");
bodyLat.textContent = body.sample_latin || t("sample_latin");
bodyWrap.style.color = txt;
bodyWrap.style.fontSize = bodyPx + "px";
bodyCjk.style.fontFamily = bodyStack || "";
bodyLat.style.fontFamily = bodyLatStack || "";
accentBar.style.background = acc;
chip.style.background = sbg;
chipDot.style.background = sacc;
chipLabel.textContent = t("role_secondary_bg");
chipLabel.style.color = txt;
}
refreshStylePreview = paint;
paint();
}
function renderImages(host) { function renderImages(host) {
var sec = section(8, "sec_images"); var sec = section(8, "sec_images");
var sub = el("div", "subfield"); var sub = el("div", "subfield");
@@ -711,11 +832,10 @@
var strategyGrid = el("div", "font-grid"); var strategyGrid = el("div", "font-grid");
var strategyCands = imageStrategyCandidates(); var strategyCands = imageStrategyCandidates();
function usesCustomImagePlan() { function usesCustomImagePlan() {
var ids = (CAT.image_usage || []).map(function (item) { return item.id; }); return usesCustomImagePlanValue(STATE.image_usage);
return STATE.image_usage && ids.indexOf(STATE.image_usage) === -1;
} }
function needsGeneratedImages() { function needsGeneratedImages() {
return STATE.image_usage === "ai" || usesCustomImagePlan(); return needsGeneratedImagesForUsage(STATE.image_usage);
} }
function refreshAiControls() { function refreshAiControls() {
var needsAiPath = needsGeneratedImages(); var needsAiPath = needsGeneratedImages();
@@ -806,13 +926,23 @@
function renderAll() { function renderAll() {
var host = document.getElementById("sections"); var host = document.getElementById("sections");
host.innerHTML = ""; host.innerHTML = "";
// Detach the previous preview's repaint closure before the sections
// re-render: color/typography auto-select would otherwise call it and
// write to now-detached nodes until renderStylePreview remounts it.
refreshStylePreview = function () {};
renderCanvas(host); renderCanvas(host);
renderPages(host); renderPages(host);
renderAudience(host); renderAudience(host);
renderStyle(host); renderStyle(host);
renderColor(host); // Group the preview with the three sections it reflects so its sticky
renderIcons(host); // scope ends when typography scrolls past — it does not linger over the
renderTypography(host); // image / mode / refine sections below.
var styleGroup = el("div", "style-group");
renderStylePreview(styleGroup);
renderColor(styleGroup);
renderIcons(styleGroup);
renderTypography(styleGroup);
host.appendChild(styleGroup);
renderImages(host); renderImages(host);
renderMode(host); renderMode(host);
renderRefine(host); renderRefine(host);
@@ -871,8 +1001,7 @@
function confirm() { function confirm() {
var btn = document.getElementById("btn-confirm"); var btn = document.getElementById("btn-confirm");
var payload = Object.assign({}, STATE); var payload = Object.assign({}, STATE);
var imageUsageIds = (CAT.image_usage || []).map(function (item) { return item.id; }); var customImagePlan = usesCustomImagePlanValue(payload.image_usage);
var customImagePlan = payload.image_usage && imageUsageIds.indexOf(payload.image_usage) === -1;
if (payload.image_usage === "custom" || (customImagePlan && !String(payload.image_usage).trim())) { if (payload.image_usage === "custom" || (customImagePlan && !String(payload.image_usage).trim())) {
document.getElementById("confirm-status").textContent = t("image_usage_custom_required"); document.getElementById("confirm-status").textContent = t("image_usage_custom_required");
var customImageInput = document.querySelector(".image-usage-custom-input"); var customImageInput = document.querySelector(".image-usage-custom-input");
@@ -880,7 +1009,7 @@
return; return;
} }
if (customImagePlan) payload.image_usage = String(payload.image_usage).trim(); if (customImagePlan) payload.image_usage = String(payload.image_usage).trim();
if (payload.image_usage !== "ai" && !customImagePlan) { if (!needsGeneratedImagesForUsage(payload.image_usage)) {
delete payload.image_ai_path; delete payload.image_ai_path;
delete payload.image_strategy; delete payload.image_strategy;
} }
@@ -298,3 +298,63 @@ body {
} }
.cf-title { font-size: 22px; font-weight: 700; color: #067647; } .cf-title { font-size: 22px; font-weight: 700; color: #067647; }
.cf-hint { font-size: 14px; color: var(--muted); margin-top: 10px; line-height: 1.6; } .cf-hint { font-size: 14px; color: var(--muted); margin-top: 10px; line-height: 1.6; }
/* Combined color + typography "overall impression" preview — sticky strip
that rides along the color / icon / typography picking sections. */
.style-preview {
position: sticky;
z-index: 5;
background: var(--bg);
padding: 8px 0;
margin-bottom: 16px;
}
.style-preview-label {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px;
margin-bottom: 6px;
}
.spl-title { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.spl-note { font-size: 11.5px; color: var(--muted); }
.style-preview-card {
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 14px 18px;
display: flex;
align-items: center;
gap: 16px;
overflow: hidden;
}
.sp-textcol { flex: 1; min-width: 0; }
.sp-title {
font-weight: 700;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sp-title-lat { margin-left: 10px; opacity: 0.92; }
.sp-body { display: flex; align-items: stretch; gap: 10px; margin-top: 6px; }
.sp-accent-bar { flex: 0 0 4px; border-radius: 2px; }
.sp-body-wrap {
flex: 1;
min-width: 0;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sp-body-lat { margin-left: 10px; opacity: 0.92; }
.sp-chip {
flex: none;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.sp-chip-dot { width: 12px; height: 12px; border-radius: 50%; }
.sp-chip-label { font-size: 12.5px; }
@@ -103,9 +103,11 @@ Both files live under `<project_path>/confirm_ui/`.
- `recommend.*` names the recommended `id` for each enumerable field (must match a `catalogs.json` id, or be a free string for a recommended custom value). The page badges and pre-selects it. **Guarantee**: if a `recommend.*` is omitted, the page falls back to the first catalog option so every enumerable field always shows one badged recommendation — but the AI should still set them for a meaningful default. Legacy aliases are accepted for old files (`line``tabler-outline`, `filled``tabler-filled`, `monochrome``chunk-filled`, `search``web`, `default``auto`, `builtin``host-native`), but new files should write canonical ids. For `recommend.image_usage`, do not write bare `"custom"`; if several image sources are mixed, write the concrete prose plan directly, such as `"封面用 AI 生成,产品页用用户素材,行业页用网络来源"` / `"AI cover + user product assets + web industry images"`. - `recommend.*` names the recommended `id` for each enumerable field (must match a `catalogs.json` id, or be a free string for a recommended custom value). The page badges and pre-selects it. **Guarantee**: if a `recommend.*` is omitted, the page falls back to the first catalog option so every enumerable field always shows one badged recommendation — but the AI should still set them for a meaningful default. Legacy aliases are accepted for old files (`line``tabler-outline`, `filled``tabler-filled`, `monochrome``chunk-filled`, `search``web`, `default``auto`, `builtin``host-native`), but new files should write canonical ids. For `recommend.image_usage`, do not write bare `"custom"`; if several image sources are mixed, write the concrete prose plan directly, such as `"封面用 AI 生成,产品页用用户素材,行业页用网络来源"` / `"AI cover + user product assets + web industry images"`.
- When `recommend.image_usage` is `ai` or a custom plan that includes AI, also set `recommend.image_ai_path` to one of `auto` / `api` / `host-native` / `manual`; the page presents these as explicit choices. - When `recommend.image_usage` is `ai` or a custom plan that includes AI, also set `recommend.image_ai_path` to one of `auto` / `api` / `host-native` / `manual`; the page presents these as explicit choices.
- For a custom image plan, the page treats "may include AI" as true only when the recommendation includes `recommend.image_ai_path` or `image_strategy.candidates`; a custom plan without those signals is handled as non-AI and omits AI controls / fields.
- **Color candidates carry the user-facing core `palette`**: `background`, `secondary_bg`, `primary`, `accent`, `secondary_accent`, and `body_text`. The page renders labelled swatches and offers per-role override inputs. Legacy `text` is accepted as an alias for `body_text`, but new files should write `body_text`. Strategist derives secondary text, borders, state colors, and visual-style neutral tiers later when writing `design_spec.md` / `spec_lock.md`; those are not user-facing confirmation choices. - **Color candidates carry the user-facing core `palette`**: `background`, `secondary_bg`, `primary`, `accent`, `secondary_accent`, and `body_text`. The page renders labelled swatches and offers per-role override inputs. Legacy `text` is accepted as an alias for `body_text`, but new files should write `body_text`. Strategist derives secondary text, borders, state colors, and visual-style neutral tiers later when writing `design_spec.md` / `spec_lock.md`; those are not user-facing confirmation choices.
- **Candidate display text may be bilingual**: color / typography candidates can provide `name_zh` / `name_en` and `note_zh` / `note_en`; the page falls back to legacy `name` / `note`. - **Candidate display text may be bilingual**: color / typography candidates can provide `name_zh` / `name_en` and `note_zh` / `note_en`; the page falls back to legacy `name` / `note`.
- **Typography candidates split CJK and Latin** for both `heading` and `body`; `css` is the preview `font-family` stack. Each candidate should also include `body_size` (the body baseline in px; common values are 18 and 24, but any reasonable integer is valid). The page exposes `body_size` as an editable numeric field, and also offers a custom typography text box so the user is not limited to the proposed candidates. - **Typography candidates split CJK and Latin** for both `heading` and `body`; `css` is the fallback preview `font-family` stack. The page previews CJK sample text with `cjk + css` and Latin sample text with `latin + css`, so the two script choices are visible independently. Each candidate should also include `body_size` (the body baseline in px; common values are 18 and 24, but any reasonable integer is valid). The page exposes `body_size` as an editable numeric field, and also offers a custom typography text box so the user is not limited to the proposed candidates.
- **Combined style preview** — a compact live "overall impression" strip sits just above the color section and is **sticky**: it pins under the topbar so it stays visible while the user scrolls through the color / icon / typography sections, keeping the picking controls and their combined effect on screen together. It applies the currently selected color palette **and** typography (heading sample in `primary` over `background`, body sample in `body_text`, an `accent` bar, a `secondary_bg` chip) and repaints on every color / HEX-override / font / `body_size` change. It does not replace the per-candidate swatches or font samples (those stay for picking); it is deliberately an abstract style chip, **not** a slide-layout preview — page layout preview remains the live-preview server's job (Step 6). No schema field; it derives entirely from the existing color + typography selections.
- **Generated image style candidates** live in `image_strategy.candidates` and are shown only when `image_usage` is `ai` or a custom image plan may include generated images. Each candidate records `rendering`, `palette`, and short `visual` / `color` / `mood` lines from Strategist h.5. The chosen value is written to `result.json.image_strategy`; it is omitted when generated images are not part of the plan. - **Generated image style candidates** live in `image_strategy.candidates` and are shown only when `image_usage` is `ai` or a custom image plan may include generated images. Each candidate records `rendering`, `palette`, and short `visual` / `color` / `mood` lines from Strategist h.5. The chosen value is written to `result.json.image_strategy`; it is omitted when generated images are not part of the plan.
- `recommend.generation_mode` and `refine_spec` mirror the two mandatory notes in SKILL.md Step 4. Confirmed `generation_mode: "split"` / `refine_spec: true` are explicit user choices, equivalent to opting in through chat. - `recommend.generation_mode` and `refine_spec` mirror the two mandatory notes in SKILL.md Step 4. Confirmed `generation_mode: "split"` / `refine_spec: true` are explicit user choices, equivalent to opting in through chat.
- `lang` is a soft default; an explicit user language choice in the page (persisted to `localStorage`) wins. - `lang` is a soft default; an explicit user language choice in the page (persisted to `localStorage`) wins.
@@ -340,7 +340,12 @@ VECTOR_FIGURE_DPI = 180
VECTOR_CAPTION_SEARCH_HEIGHT = 380 VECTOR_CAPTION_SEARCH_HEIGHT = 380
VECTOR_CAPTION_HORIZONTAL_GAP = 90 VECTOR_CAPTION_HORIZONTAL_GAP = 90
MAX_VECTOR_BACKGROUND_AREA_RATIO = 1.9 MAX_VECTOR_BACKGROUND_AREA_RATIO = 1.9
FIGURE_CAPTION_RE = re.compile(r'^(?:Figure|Fig\.)\s*\d+\s*[:.]', re.IGNORECASE) # Caption delimiters: ``Figure 1:`` / ``Figure 1.`` (classic) and ``Figure 1 |``
# (the DeepMind / Distill / Nature house style used by many ML papers, incl.
# full-width ````). Without the pipe variants, captioned vector figures route
# to the generic per-drawing fallback, which discards fine-grained line plots
# (each plot is hundreds of tiny primitives, none large enough on its own).
FIGURE_CAPTION_RE = re.compile(r'^(?:Figure|Fig\.?)\s*\d+\s*[:.|]', re.IGNORECASE)
def should_keep_image( def should_keep_image(
@@ -229,9 +229,14 @@ html, body {
#svg-content svg { #svg-content svg {
display: block; display: block;
/* Both dimensions auto so the box hugs the slide at any aspect ratio: with
only height:auto, portrait formats (e.g. 1242x1660) keep their attribute
width while max-height shrinks the height, leaving large white bands
inside the white card. width:auto lets the element scale proportionally. */
width: auto;
height: auto;
max-width: 100%; max-width: 100%;
max-height: calc(100vh - 72px); max-height: calc(100vh - 72px);
height: auto;
} }
/* ---------- SVG element interaction states ---------- */ /* ---------- SVG element interaction states ---------- */
@@ -17,7 +17,7 @@ if __package__ in {None, ''}:
sys.modules.setdefault('svg_to_pptx', package) sys.modules.setdefault('svg_to_pptx', package)
__package__ = 'svg_to_pptx' __package__ = 'svg_to_pptx'
from .pptx_dimensions import CANVAS_FORMATS, get_project_info from .pptx_dimensions import CANVAS_FORMATS, get_project_info, get_viewbox_dimensions
from .pptx_discovery import find_svg_files, find_notes_files from .pptx_discovery import find_svg_files, find_notes_files
from .pptx_builder import create_pptx_with_native_svg from .pptx_builder import create_pptx_with_native_svg
from .pptx_narration import NARRATION_EXTENSIONS, find_narration_files, probe_audio_duration from .pptx_narration import NARRATION_EXTENSIONS, find_narration_files, probe_audio_duration
@@ -334,6 +334,28 @@ Recorded narration:
verbose = not args.quiet verbose = not args.quiet
# Honor the actual SVG pixels over a stale project-recorded format. The
# canvas_format read from project init can disagree with what the Executor
# actually drew — e.g. a mirror template imported at 2560×1440 while the
# project was initialized as ppt169 (1280×720). When the first SVG's real
# viewBox doesn't match the recorded format's dimensions, drop the format
# so the builder sizes the slide by pixels (custom_pixels path). Standard
# decks match exactly, so this only changes behavior on the conflict case.
# An explicit --format always wins and is never second-guessed.
if args.format is None and canvas_format:
fmt_info = CANVAS_FORMATS.get(canvas_format)
actual_dims = get_viewbox_dimensions(ref_files[0])
if fmt_info and actual_dims:
fmt_dims = (fmt_info.get('width'), fmt_info.get('height'))
if fmt_dims != actual_dims:
if verbose:
print(
f" Recorded format '{canvas_format}' "
f"({fmt_dims[0]}×{fmt_dims[1]}) differs from SVG viewBox "
f"({actual_dims[0]}×{actual_dims[1]}); exporting by SVG pixels"
)
canvas_format = None
enable_notes = not args.no_notes enable_notes = not args.no_notes
notes: dict[str, str] = {} notes: dict[str, str] = {}
if enable_notes: if enable_notes:
@@ -2,8 +2,8 @@
"sourceId": "shadcn", "sourceId": "shadcn",
"repo": "https://github.com/shadcn-ui/ui.git", "repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main", "ref": "main",
"commit": "c879483c9694d7b135ed1efe8f73233d924ae551", "commit": "c2ddedf5d24e0cee05936c69e1f199a9975a07b2",
"adapter": "claude-skill", "adapter": "claude-skill",
"sourcePath": "skills/shadcn", "sourcePath": "skills/shadcn",
"syncedAt": "2026-06-17T07:05:29Z" "syncedAt": "2026-06-18T16:00:00Z"
} }