@taloswork/ose-shared 0.1.7 → 0.1.8
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.
- package/package.json +3 -3
- package/skills/document/bin/doc-gen.cjs +3 -6
- package/skills/document/bin/html2pptx.cjs +17 -9
- package/skills/document/bin/pptx-clone.cjs +41 -30
- package/skills/document/bin/pptx-embed-fonts.cjs +6 -2
- package/skills/document/bin/pptx-slide.cjs +46 -50
- package/skills/document/content-pipeline.md +54 -5
- package/skills/document/layouts/README.md +8 -0
- package/skills/document/layouts/blocks-2x2.tpl +19 -9
- package/skills/document/layouts/cards-3.tpl +1 -1
- package/skills/document/layouts/org-tree.tpl +1 -1
- package/skills/document/layouts/steps-vertical.tpl +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taloswork/ose-shared",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Talos cross-OSE shared library — skills + MCP servers auto-loaded into every OSE (P93 2026-07-27).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
|
-
"_talosPublishHash": "sha256-
|
|
14
|
-
"_talosPublishedAt": "2026-09-
|
|
13
|
+
"_talosPublishHash": "sha256-Jwek2npsPD2O5y281xlaBct9hS3AkgkMGHd-LXNgHCw",
|
|
14
|
+
"_talosPublishedAt": "2026-09-16T17:21:18.072Z"
|
|
15
15
|
}
|
|
@@ -152164,7 +152164,7 @@ function themeCssVars(theme) {
|
|
|
152164
152164
|
`--secondary:${c.secondary}`,
|
|
152165
152165
|
`--text:${c.text}`,
|
|
152166
152166
|
`--muted:${c.muted}`,
|
|
152167
|
-
`--card
|
|
152167
|
+
`--surface-card:${c.tableHeaderBg}`,
|
|
152168
152168
|
`--border:${c.tableBorder}`,
|
|
152169
152169
|
`--slide-bg:${theme.pptx.background}`,
|
|
152170
152170
|
`--title-color:${theme.pptx.titleColor}`,
|
|
@@ -154065,11 +154065,8 @@ img { max-width: 100%; margin: 8pt 0; }
|
|
|
154065
154065
|
.cover img.logo { max-height: 60pt; margin-bottom: 24pt; }
|
|
154066
154066
|
`.trim();
|
|
154067
154067
|
}
|
|
154068
|
-
function foldFullwidthAlnum2(s) {
|
|
154069
|
-
return s.replace(/[0-9A-Za-z]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 65248)).replace(/ /g, " ");
|
|
154070
|
-
}
|
|
154071
154068
|
function norm(s) {
|
|
154072
|
-
return escapeHtml(
|
|
154069
|
+
return escapeHtml(foldFullwidthAlnum(String(s)));
|
|
154073
154070
|
}
|
|
154074
154071
|
function paragraphHtml(text) {
|
|
154075
154072
|
return `<p>${norm(text).replace(/\r?\n/g, "<br>")}</p>`;
|
|
@@ -154254,7 +154251,7 @@ function interpolate(src, data) {
|
|
|
154254
154251
|
}
|
|
154255
154252
|
|
|
154256
154253
|
// packages/core/dist/document/html-templates.js
|
|
154257
|
-
var CARD = "background:var(--card
|
|
154254
|
+
var CARD = "background:var(--surface-card);border:1px solid var(--border);border-radius:16px";
|
|
154258
154255
|
var TEMPLATES = {
|
|
154259
154256
|
cover: {
|
|
154260
154257
|
html: `<section class="slide" style="padding:0;display:flex;flex-direction:column;justify-content:center;padding-left:88px">
|
|
@@ -146935,7 +146935,7 @@ function themeCssVars(theme) {
|
|
|
146935
146935
|
`--secondary:${c.secondary}`,
|
|
146936
146936
|
`--text:${c.text}`,
|
|
146937
146937
|
`--muted:${c.muted}`,
|
|
146938
|
-
`--card
|
|
146938
|
+
`--surface-card:${c.tableHeaderBg}`,
|
|
146939
146939
|
`--border:${c.tableBorder}`,
|
|
146940
146940
|
`--slide-bg:${theme.pptx.background}`,
|
|
146941
146941
|
`--title-color:${theme.pptx.titleColor}`,
|
|
@@ -148261,7 +148261,7 @@ async function latinFontFaceCss(doc) {
|
|
|
148261
148261
|
}
|
|
148262
148262
|
return _latinFontCss;
|
|
148263
148263
|
}
|
|
148264
|
-
async function withHeadlessPage(html, fn) {
|
|
148264
|
+
async function withHeadlessPage(html, fn, density = DOCUMENT_CAPTURE_DENSITY) {
|
|
148265
148265
|
const headless = (process.env.TALOS_HTML2PPTX_HEADLESS ?? "").trim().toLowerCase();
|
|
148266
148266
|
if (headless === "off" || headless === "none" || headless === "0" || headless === "false") return null;
|
|
148267
148267
|
let pw;
|
|
@@ -148280,7 +148280,7 @@ async function withHeadlessPage(html, fn) {
|
|
|
148280
148280
|
try {
|
|
148281
148281
|
const page = await browser2.newPage({
|
|
148282
148282
|
viewport: { width: 1280, height: 720 },
|
|
148283
|
-
deviceScaleFactor:
|
|
148283
|
+
deviceScaleFactor: density
|
|
148284
148284
|
});
|
|
148285
148285
|
await page.setContent(html, { waitUntil: "load" });
|
|
148286
148286
|
await page.evaluate("document.fonts && document.fonts.ready").catch(() => {
|
|
@@ -148290,7 +148290,7 @@ async function withHeadlessPage(html, fn) {
|
|
|
148290
148290
|
screenshot: async (clip) => {
|
|
148291
148291
|
const png = await page.screenshot({ clip });
|
|
148292
148292
|
const w = png.readUInt32BE(16), h = png.readUInt32BE(20);
|
|
148293
|
-
const want = [clip.width *
|
|
148293
|
+
const want = [clip.width * density, clip.height * density];
|
|
148294
148294
|
if (w !== want[0] || h !== want[1]) throw new Error(`captured ${w}x${h}, expected ${want[0]}x${want[1]}`);
|
|
148295
148295
|
return png;
|
|
148296
148296
|
}
|
|
@@ -148370,11 +148370,16 @@ async function main() {
|
|
|
148370
148370
|
const previewPagesIdx = args.indexOf("--preview-pages");
|
|
148371
148371
|
const previewPagesArg = previewPagesIdx >= 0 ? args[previewPagesIdx + 1] : void 0;
|
|
148372
148372
|
const previewIndexes = parsePreviewPages(previewPagesArg);
|
|
148373
|
-
const
|
|
148373
|
+
const previewDensityIdx = args.indexOf("--preview-density");
|
|
148374
|
+
const previewDensityArg = previewDensityIdx >= 0 ? args[previewDensityIdx + 1] : void 0;
|
|
148375
|
+
const previewDensity = previewDensityArg === void 0 ? DOCUMENT_CAPTURE_DENSITY : Number(previewDensityArg);
|
|
148376
|
+
const consumed = /* @__PURE__ */ new Set([themeIdx, themeIdx >= 0 ? themeIdx + 1 : -1, styleIdx, styleIdx >= 0 ? styleIdx + 1 : -1, mediaIdx, mediaIdx >= 0 ? mediaIdx + 1 : -1, bdIdx, bdIdx >= 0 ? bdIdx + 1 : -1, lineIdx, lineIdx >= 0 ? lineIdx + 1 : -1, modeIdx, modeIdx >= 0 ? modeIdx + 1 : -1, notesIdx, notesIdx >= 0 ? notesIdx + 1 : -1, previewIdx, previewIdx >= 0 ? previewIdx + 1 : -1, previewPagesIdx, previewPagesIdx >= 0 ? previewPagesIdx + 1 : -1, previewDensityIdx, previewDensityIdx >= 0 ? previewDensityIdx + 1 : -1]);
|
|
148374
148377
|
const positional = args.filter((a, i) => a !== "--rasterize" && a !== "--editable" && a !== "--theme-v2" && !consumed.has(i));
|
|
148375
148378
|
const [input, out] = positional;
|
|
148376
|
-
if (!input || !out && !previewDir) die("usage: html2pptx <in.html|in.layout.json> [out.pptx] [--theme <theme.json>] [--style <name|dir>] [--theme-v2] [--media-style <id>] [--backdrop <variant>] [--line <name|none>] [--editable] [--rasterize] [--notes-doc <out.md>] [--preview-dir <dir>] [--preview-pages <all|N,N-M>]", 64);
|
|
148379
|
+
if (!input || !out && !previewDir) die("usage: html2pptx <in.html|in.layout.json> [out.pptx] [--theme <theme.json>] [--style <name|dir>] [--theme-v2] [--media-style <id>] [--backdrop <variant>] [--line <name|none>] [--editable] [--rasterize] [--notes-doc <out.md>] [--preview-dir <dir>] [--preview-pages <all|N,N-M>] [--preview-density <1..4>]", 64);
|
|
148377
148380
|
if (previewPagesArg && !previewDir) die("--preview-pages requires --preview-dir", 64);
|
|
148381
|
+
if (previewDensityArg !== void 0 && !previewDir) die("--preview-density requires --preview-dir", 64);
|
|
148382
|
+
if (!(previewDensity > 0 && previewDensity <= 4)) die("--preview-density must be a number in (0, 4]", 64);
|
|
148378
148383
|
if (previewDir && out) die("--preview-dir is preview-only; omit the .pptx output", 64);
|
|
148379
148384
|
if (previewDir && (rasterize || forceEditable || notesDocPath)) die("--preview-dir cannot be combined with --rasterize, --editable/--mode, or --notes-doc", 64);
|
|
148380
148385
|
if (themeV2 && !styleArg) die("--theme-v2 requires --style <catalog-id>", 64);
|
|
@@ -148528,9 +148533,12 @@ async function main() {
|
|
|
148528
148533
|
const html = injectHead(rawInlined, `${fontCss ? `<style>${fontCss}</style>` : ""}${tw ? `<style>${tw}</style>` : ""}<style>${themeCss}${SKELETON_CSS}${skinCss}</style>`);
|
|
148529
148534
|
if (previewDir) {
|
|
148530
148535
|
let images;
|
|
148531
|
-
if (hasService) ({ images } = await callService("/shot", html,
|
|
148536
|
+
if (hasService) ({ images } = await callService("/shot", html, {
|
|
148537
|
+
...previewIndexes ? { pages: previewIndexes } : {},
|
|
148538
|
+
...previewDensityArg !== void 0 ? { density: previewDensity } : {}
|
|
148539
|
+
}).catch((e) => die(e.message, 1)));
|
|
148532
148540
|
else {
|
|
148533
|
-
images = await withHeadlessPage(html, (p) => rasterizeHeadless(p, previewIndexes ?? void 0)).catch((e) => die(e.message, 1));
|
|
148541
|
+
images = await withHeadlessPage(html, (p) => rasterizeHeadless(p, previewIndexes ?? void 0), previewDensity).catch((e) => die(e.message, 1));
|
|
148534
148542
|
if (images == null) die(NO_BROWSER, 69);
|
|
148535
148543
|
}
|
|
148536
148544
|
await (0, import_promises.mkdir)(previewDir, { recursive: true });
|
|
@@ -148541,7 +148549,7 @@ async function main() {
|
|
|
148541
148549
|
await (0, import_promises.writeFile)(path, Buffer.from(images[i], "base64"));
|
|
148542
148550
|
files.push(path);
|
|
148543
148551
|
}
|
|
148544
|
-
process.stdout.write(JSON.stringify({ ok: true, engine: hasService ? "preview" : "preview (headless)", slides: images.length, pages: pageNumbers, files }, null, 1) + "\n");
|
|
148552
|
+
process.stdout.write(JSON.stringify({ ok: true, engine: hasService ? "preview" : "preview (headless)", slides: images.length, pages: pageNumbers, density: previewDensity, files }, null, 1) + "\n");
|
|
148545
148553
|
return;
|
|
148546
148554
|
}
|
|
148547
148555
|
if (rasterize) {
|
|
@@ -9860,9 +9860,9 @@ function unescXml(s) {
|
|
|
9860
9860
|
}
|
|
9861
9861
|
});
|
|
9862
9862
|
}
|
|
9863
|
-
function visualWidth(
|
|
9863
|
+
function visualWidth(text3) {
|
|
9864
9864
|
let w = 0;
|
|
9865
|
-
for (const ch of
|
|
9865
|
+
for (const ch of text3) {
|
|
9866
9866
|
const cp = ch.codePointAt(0);
|
|
9867
9867
|
if (ch === " " || ch === " ")
|
|
9868
9868
|
w += 0.35;
|
|
@@ -9891,9 +9891,9 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
9891
9891
|
const segs = [];
|
|
9892
9892
|
let flat = "";
|
|
9893
9893
|
for (const m of para.matchAll(textRe)) {
|
|
9894
|
-
const
|
|
9895
|
-
segs.push({ el: m[0], attrs: m[1] ?? "", text:
|
|
9896
|
-
flat +=
|
|
9894
|
+
const text3 = unescXml(m[2]);
|
|
9895
|
+
segs.push({ el: m[0], attrs: m[1] ?? "", text: text3, start: flat.length });
|
|
9896
|
+
flat += text3;
|
|
9897
9897
|
}
|
|
9898
9898
|
if (!flat.includes(find))
|
|
9899
9899
|
return para;
|
|
@@ -9936,9 +9936,36 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
9936
9936
|
return { xml: rewritten, count };
|
|
9937
9937
|
}
|
|
9938
9938
|
|
|
9939
|
-
// packages/core/dist/document/pptx-
|
|
9939
|
+
// packages/core/dist/document/pptx-register-slide.js
|
|
9940
9940
|
var SLIDE_CT = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
9941
9941
|
var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
9942
|
+
async function text(zip, path) {
|
|
9943
|
+
const f = zip.file(path);
|
|
9944
|
+
if (!f)
|
|
9945
|
+
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
9946
|
+
return f.async("string");
|
|
9947
|
+
}
|
|
9948
|
+
async function registerSlidePart(zip, newNum, newXmlPath) {
|
|
9949
|
+
const ctPath = "[Content_Types].xml";
|
|
9950
|
+
const ct = await text(zip, ctPath);
|
|
9951
|
+
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
9952
|
+
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
9953
|
+
}
|
|
9954
|
+
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
9955
|
+
const presRels = await text(zip, presRelsPath);
|
|
9956
|
+
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
9957
|
+
const newRid = `rId${maxRid + 1}`;
|
|
9958
|
+
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
9959
|
+
const presPath = "ppt/presentation.xml";
|
|
9960
|
+
const pres = await text(zip, presPath);
|
|
9961
|
+
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
9962
|
+
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
9963
|
+
if (updated === pres)
|
|
9964
|
+
throw new Error("could not locate the slide list in presentation.xml");
|
|
9965
|
+
zip.file(presPath, updated);
|
|
9966
|
+
}
|
|
9967
|
+
|
|
9968
|
+
// packages/core/dist/document/pptx-clone.js
|
|
9942
9969
|
var EMU_PER_PT = 12700;
|
|
9943
9970
|
function capacityWarning(slideXml, token, value) {
|
|
9944
9971
|
const idx = slideXml.indexOf(token);
|
|
@@ -9971,7 +9998,7 @@ function capacityWarning(slideXml, token, value) {
|
|
|
9971
9998
|
return void 0;
|
|
9972
9999
|
return `${token}: replacement is ~${Math.ceil(width)} width-units, slot fits ~${Math.floor(capacity)} (${Math.round(widthPt)}\xD7${Math.round(heightPt)}pt box @ ${fontPt}pt) \u2014 likely overflow. Shorten the wording; do NOT shrink the font to make it fit.`;
|
|
9973
10000
|
}
|
|
9974
|
-
async function
|
|
10001
|
+
async function text2(zip, path) {
|
|
9975
10002
|
const f = zip.file(path);
|
|
9976
10003
|
if (!f)
|
|
9977
10004
|
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
@@ -9996,7 +10023,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
9996
10023
|
const newNum = Math.max(...slideNums) + 1;
|
|
9997
10024
|
const srcXmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
9998
10025
|
const newXmlPath = `ppt/slides/slide${newNum}.xml`;
|
|
9999
|
-
let slideXml = await
|
|
10026
|
+
let slideXml = await text2(zip, srcXmlPath);
|
|
10000
10027
|
const replaced = [];
|
|
10001
10028
|
const missing = [];
|
|
10002
10029
|
const warnings = [];
|
|
@@ -10024,23 +10051,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
10024
10051
|
const rels = removeElement(await srcRels.async("string"), "Relationship", (el) => /Type="[^"]*\/notesSlide"/.test(el)).xml;
|
|
10025
10052
|
zip.file(`ppt/slides/_rels/slide${newNum}.xml.rels`, rels);
|
|
10026
10053
|
}
|
|
10027
|
-
|
|
10028
|
-
const ct = await text(zip, ctPath);
|
|
10029
|
-
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
10030
|
-
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
10031
|
-
}
|
|
10032
|
-
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10033
|
-
const presRels = await text(zip, presRelsPath);
|
|
10034
|
-
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
10035
|
-
const newRid = `rId${maxRid + 1}`;
|
|
10036
|
-
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
10037
|
-
const presPath = "ppt/presentation.xml";
|
|
10038
|
-
const pres = await text(zip, presPath);
|
|
10039
|
-
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
10040
|
-
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
10041
|
-
if (updated === pres)
|
|
10042
|
-
throw new Error("could not locate the slide list in presentation.xml");
|
|
10043
|
-
zip.file(presPath, updated);
|
|
10054
|
+
await registerSlidePart(zip, newNum, newXmlPath);
|
|
10044
10055
|
const out = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
10045
10056
|
return { bytes: new Uint8Array(out), newSlideNumber: newNum, replaced, missing, warnings };
|
|
10046
10057
|
}
|
|
@@ -10065,7 +10076,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10065
10076
|
const xmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10066
10077
|
const relsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
|
|
10067
10078
|
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10068
|
-
const presRels = await
|
|
10079
|
+
const presRels = await text2(zip, presRelsPath);
|
|
10069
10080
|
const targetRe = new RegExp(`Target="(/ppt/)?slides/slide${slideNumber}\\.xml"`);
|
|
10070
10081
|
const relMatch = findElement(presRels, "Relationship", (r) => r.includes(SLIDE_REL_TYPE) && targetRe.test(r));
|
|
10071
10082
|
if (!relMatch)
|
|
@@ -10075,19 +10086,19 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10075
10086
|
throw new Error(`slide ${slideNumber}'s relationship carries no Id (corrupt deck?)`);
|
|
10076
10087
|
zip.file(presRelsPath, presRels.replace(relMatch, ""));
|
|
10077
10088
|
const presPath = "ppt/presentation.xml";
|
|
10078
|
-
const pres = await
|
|
10089
|
+
const pres = await text2(zip, presPath);
|
|
10079
10090
|
const sld = removeElement(pres, "p:sldId", (el) => el.includes(`r:id="${rId}"`));
|
|
10080
10091
|
if (!sld.removed)
|
|
10081
10092
|
throw new Error(`slide ${slideNumber} (${rId}) not found in the slide list (corrupt deck?)`);
|
|
10082
10093
|
zip.file(presPath, sld.xml);
|
|
10083
|
-
const slideRels = zip.file(relsPath) ? await
|
|
10094
|
+
const slideRels = zip.file(relsPath) ? await text2(zip, relsPath) : "";
|
|
10084
10095
|
const notesTarget = /<Relationship\b[^>]*Type="[^"]*\/notesSlide"[^>]*Target="\.\.\/(notesSlides\/notesSlide\d+\.xml)"/.exec(slideRels)?.[1];
|
|
10085
10096
|
const removedParts = [xmlPath, relsPath];
|
|
10086
10097
|
if (notesTarget) {
|
|
10087
10098
|
const otherSlideRels = Object.keys(zip.files).filter((p) => /^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/.test(p) && p !== relsPath);
|
|
10088
10099
|
let shared = false;
|
|
10089
10100
|
for (const p of otherSlideRels) {
|
|
10090
|
-
if ((await
|
|
10101
|
+
if ((await text2(zip, p)).includes(notesTarget)) {
|
|
10091
10102
|
shared = true;
|
|
10092
10103
|
break;
|
|
10093
10104
|
}
|
|
@@ -10095,7 +10106,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10095
10106
|
if (!shared)
|
|
10096
10107
|
removedParts.push(`ppt/${notesTarget}`, `ppt/notesSlides/_rels/${notesTarget.split("/")[1]}.rels`);
|
|
10097
10108
|
}
|
|
10098
|
-
let ct = await
|
|
10109
|
+
let ct = await text2(zip, "[Content_Types].xml");
|
|
10099
10110
|
for (const part of removedParts) {
|
|
10100
10111
|
zip.remove(part);
|
|
10101
10112
|
ct = removeElement(ct, "Override", (el) => el.includes(`PartName="/${part}"`)).xml;
|
|
@@ -9820,10 +9820,14 @@ var import_promises = require("node:fs/promises");
|
|
|
9820
9820
|
|
|
9821
9821
|
// packages/core/dist/document/pptx-fonts.js
|
|
9822
9822
|
var import_jszip = __toESM(require_lib3(), 1);
|
|
9823
|
-
|
|
9823
|
+
|
|
9824
|
+
// packages/core/dist/document/ooxml-util.js
|
|
9824
9825
|
function escXml(s) {
|
|
9825
|
-
return s.replace(
|
|
9826
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9826
9827
|
}
|
|
9828
|
+
|
|
9829
|
+
// packages/core/dist/document/pptx-fonts.js
|
|
9830
|
+
var FONT_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
|
|
9827
9831
|
async function text(zip, path) {
|
|
9828
9832
|
const f = zip.file(path);
|
|
9829
9833
|
if (!f)
|
|
@@ -9820,11 +9820,40 @@ var import_promises = require("node:fs/promises");
|
|
|
9820
9820
|
|
|
9821
9821
|
// packages/core/dist/document/pptx-slide.js
|
|
9822
9822
|
var import_jszip = __toESM(require_lib3(), 1);
|
|
9823
|
+
|
|
9824
|
+
// packages/core/dist/document/pptx-register-slide.js
|
|
9823
9825
|
var SLIDE_CT = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
9824
9826
|
var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
9827
|
+
async function text(zip, path) {
|
|
9828
|
+
const f = zip.file(path);
|
|
9829
|
+
if (!f)
|
|
9830
|
+
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
9831
|
+
return f.async("string");
|
|
9832
|
+
}
|
|
9833
|
+
async function registerSlidePart(zip, newNum, newXmlPath) {
|
|
9834
|
+
const ctPath = "[Content_Types].xml";
|
|
9835
|
+
const ct = await text(zip, ctPath);
|
|
9836
|
+
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
9837
|
+
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
9838
|
+
}
|
|
9839
|
+
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
9840
|
+
const presRels = await text(zip, presRelsPath);
|
|
9841
|
+
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
9842
|
+
const newRid = `rId${maxRid + 1}`;
|
|
9843
|
+
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
9844
|
+
const presPath = "ppt/presentation.xml";
|
|
9845
|
+
const pres = await text(zip, presPath);
|
|
9846
|
+
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
9847
|
+
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
9848
|
+
if (updated === pres)
|
|
9849
|
+
throw new Error("could not locate the slide list in presentation.xml");
|
|
9850
|
+
zip.file(presPath, updated);
|
|
9851
|
+
}
|
|
9852
|
+
|
|
9853
|
+
// packages/core/dist/document/pptx-slide.js
|
|
9825
9854
|
var LAYOUT_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
|
|
9826
9855
|
var BLANK_SLIDE_XML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>';
|
|
9827
|
-
async function
|
|
9856
|
+
async function text2(zip, path) {
|
|
9828
9857
|
const f = zip.file(path);
|
|
9829
9858
|
if (!f)
|
|
9830
9859
|
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
@@ -9872,29 +9901,14 @@ async function addBlankSlide(bytes, opts = {}) {
|
|
|
9872
9901
|
zip.file(newXmlPath, BLANK_SLIDE_XML);
|
|
9873
9902
|
zip.file(`ppt/slides/_rels/slide${newNum}.xml.rels`, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
9874
9903
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="${LAYOUT_REL_TYPE}" Target="../slideLayouts/slideLayout${layout}.xml"/></Relationships>`);
|
|
9875
|
-
|
|
9876
|
-
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
9877
|
-
zip.file("[Content_Types].xml", ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
9878
|
-
}
|
|
9879
|
-
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
9880
|
-
const presRels = await text(zip, presRelsPath);
|
|
9881
|
-
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
9882
|
-
const newRid = `rId${maxRid + 1}`;
|
|
9883
|
-
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
9884
|
-
const presPath = "ppt/presentation.xml";
|
|
9885
|
-
const pres = await text(zip, presPath);
|
|
9886
|
-
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
9887
|
-
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
9888
|
-
if (updated === pres)
|
|
9889
|
-
throw new Error("could not locate the slide list in presentation.xml");
|
|
9890
|
-
zip.file(presPath, updated);
|
|
9904
|
+
await registerSlidePart(zip, newNum, newXmlPath);
|
|
9891
9905
|
const out2 = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
9892
9906
|
return { bytes: new Uint8Array(out2), newSlideNumber: newNum, layout };
|
|
9893
9907
|
}
|
|
9894
9908
|
async function reorderPptxSlides(bytes, order) {
|
|
9895
9909
|
const zip = await load(bytes);
|
|
9896
9910
|
const presPath = "ppt/presentation.xml";
|
|
9897
|
-
const pres = await
|
|
9911
|
+
const pres = await text2(zip, presPath);
|
|
9898
9912
|
const listMatch = /(<p:sldIdLst\b[^>]*>)([\s\S]*?)(<\/p:sldIdLst>)/.exec(pres);
|
|
9899
9913
|
if (!listMatch)
|
|
9900
9914
|
throw new Error("no <p:sldIdLst> in presentation.xml (corrupt deck?)");
|
|
@@ -9965,9 +9979,9 @@ function unescXml(s) {
|
|
|
9965
9979
|
}
|
|
9966
9980
|
});
|
|
9967
9981
|
}
|
|
9968
|
-
function visualWidth(
|
|
9982
|
+
function visualWidth(text4) {
|
|
9969
9983
|
let w = 0;
|
|
9970
|
-
for (const ch of
|
|
9984
|
+
for (const ch of text4) {
|
|
9971
9985
|
const cp = ch.codePointAt(0);
|
|
9972
9986
|
if (ch === " " || ch === " ")
|
|
9973
9987
|
w += 0.35;
|
|
@@ -9996,9 +10010,9 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
9996
10010
|
const segs = [];
|
|
9997
10011
|
let flat = "";
|
|
9998
10012
|
for (const m of para.matchAll(textRe)) {
|
|
9999
|
-
const
|
|
10000
|
-
segs.push({ el: m[0], attrs: m[1] ?? "", text:
|
|
10001
|
-
flat +=
|
|
10013
|
+
const text4 = unescXml(m[2]);
|
|
10014
|
+
segs.push({ el: m[0], attrs: m[1] ?? "", text: text4, start: flat.length });
|
|
10015
|
+
flat += text4;
|
|
10002
10016
|
}
|
|
10003
10017
|
if (!flat.includes(find))
|
|
10004
10018
|
return para;
|
|
@@ -10042,8 +10056,6 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
10042
10056
|
}
|
|
10043
10057
|
|
|
10044
10058
|
// packages/core/dist/document/pptx-clone.js
|
|
10045
|
-
var SLIDE_CT2 = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
10046
|
-
var SLIDE_REL_TYPE2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
10047
10059
|
var EMU_PER_PT = 12700;
|
|
10048
10060
|
function capacityWarning(slideXml, token, value) {
|
|
10049
10061
|
const idx = slideXml.indexOf(token);
|
|
@@ -10076,7 +10088,7 @@ function capacityWarning(slideXml, token, value) {
|
|
|
10076
10088
|
return void 0;
|
|
10077
10089
|
return `${token}: replacement is ~${Math.ceil(width)} width-units, slot fits ~${Math.floor(capacity)} (${Math.round(widthPt)}\xD7${Math.round(heightPt)}pt box @ ${fontPt}pt) \u2014 likely overflow. Shorten the wording; do NOT shrink the font to make it fit.`;
|
|
10078
10090
|
}
|
|
10079
|
-
async function
|
|
10091
|
+
async function text3(zip, path) {
|
|
10080
10092
|
const f = zip.file(path);
|
|
10081
10093
|
if (!f)
|
|
10082
10094
|
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
@@ -10101,7 +10113,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
10101
10113
|
const newNum = Math.max(...slideNums) + 1;
|
|
10102
10114
|
const srcXmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10103
10115
|
const newXmlPath = `ppt/slides/slide${newNum}.xml`;
|
|
10104
|
-
let slideXml = await
|
|
10116
|
+
let slideXml = await text3(zip, srcXmlPath);
|
|
10105
10117
|
const replaced = [];
|
|
10106
10118
|
const missing = [];
|
|
10107
10119
|
const warnings = [];
|
|
@@ -10129,23 +10141,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
10129
10141
|
const rels = removeElement(await srcRels.async("string"), "Relationship", (el) => /Type="[^"]*\/notesSlide"/.test(el)).xml;
|
|
10130
10142
|
zip.file(`ppt/slides/_rels/slide${newNum}.xml.rels`, rels);
|
|
10131
10143
|
}
|
|
10132
|
-
|
|
10133
|
-
const ct = await text2(zip, ctPath);
|
|
10134
|
-
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
10135
|
-
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT2}"/></Types>`));
|
|
10136
|
-
}
|
|
10137
|
-
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10138
|
-
const presRels = await text2(zip, presRelsPath);
|
|
10139
|
-
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
10140
|
-
const newRid = `rId${maxRid + 1}`;
|
|
10141
|
-
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE2}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
10142
|
-
const presPath = "ppt/presentation.xml";
|
|
10143
|
-
const pres = await text2(zip, presPath);
|
|
10144
|
-
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
10145
|
-
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
10146
|
-
if (updated === pres)
|
|
10147
|
-
throw new Error("could not locate the slide list in presentation.xml");
|
|
10148
|
-
zip.file(presPath, updated);
|
|
10144
|
+
await registerSlidePart(zip, newNum, newXmlPath);
|
|
10149
10145
|
const out2 = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
10150
10146
|
return { bytes: new Uint8Array(out2), newSlideNumber: newNum, replaced, missing, warnings };
|
|
10151
10147
|
}
|
|
@@ -10170,9 +10166,9 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10170
10166
|
const xmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10171
10167
|
const relsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
|
|
10172
10168
|
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10173
|
-
const presRels = await
|
|
10169
|
+
const presRels = await text3(zip, presRelsPath);
|
|
10174
10170
|
const targetRe = new RegExp(`Target="(/ppt/)?slides/slide${slideNumber}\\.xml"`);
|
|
10175
|
-
const relMatch = findElement(presRels, "Relationship", (r) => r.includes(
|
|
10171
|
+
const relMatch = findElement(presRels, "Relationship", (r) => r.includes(SLIDE_REL_TYPE) && targetRe.test(r));
|
|
10176
10172
|
if (!relMatch)
|
|
10177
10173
|
throw new Error(`slide ${slideNumber} is not wired into presentation.xml.rels (corrupt deck?)`);
|
|
10178
10174
|
const rId = /Id="(rId\d+)"/.exec(relMatch)?.[1];
|
|
@@ -10180,19 +10176,19 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10180
10176
|
throw new Error(`slide ${slideNumber}'s relationship carries no Id (corrupt deck?)`);
|
|
10181
10177
|
zip.file(presRelsPath, presRels.replace(relMatch, ""));
|
|
10182
10178
|
const presPath = "ppt/presentation.xml";
|
|
10183
|
-
const pres = await
|
|
10179
|
+
const pres = await text3(zip, presPath);
|
|
10184
10180
|
const sld = removeElement(pres, "p:sldId", (el) => el.includes(`r:id="${rId}"`));
|
|
10185
10181
|
if (!sld.removed)
|
|
10186
10182
|
throw new Error(`slide ${slideNumber} (${rId}) not found in the slide list (corrupt deck?)`);
|
|
10187
10183
|
zip.file(presPath, sld.xml);
|
|
10188
|
-
const slideRels = zip.file(relsPath) ? await
|
|
10184
|
+
const slideRels = zip.file(relsPath) ? await text3(zip, relsPath) : "";
|
|
10189
10185
|
const notesTarget = /<Relationship\b[^>]*Type="[^"]*\/notesSlide"[^>]*Target="\.\.\/(notesSlides\/notesSlide\d+\.xml)"/.exec(slideRels)?.[1];
|
|
10190
10186
|
const removedParts = [xmlPath, relsPath];
|
|
10191
10187
|
if (notesTarget) {
|
|
10192
10188
|
const otherSlideRels = Object.keys(zip.files).filter((p) => /^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/.test(p) && p !== relsPath);
|
|
10193
10189
|
let shared = false;
|
|
10194
10190
|
for (const p of otherSlideRels) {
|
|
10195
|
-
if ((await
|
|
10191
|
+
if ((await text3(zip, p)).includes(notesTarget)) {
|
|
10196
10192
|
shared = true;
|
|
10197
10193
|
break;
|
|
10198
10194
|
}
|
|
@@ -10200,7 +10196,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10200
10196
|
if (!shared)
|
|
10201
10197
|
removedParts.push(`ppt/${notesTarget}`, `ppt/notesSlides/_rels/${notesTarget.split("/")[1]}.rels`);
|
|
10202
10198
|
}
|
|
10203
|
-
let ct = await
|
|
10199
|
+
let ct = await text3(zip, "[Content_Types].xml");
|
|
10204
10200
|
for (const part of removedParts) {
|
|
10205
10201
|
zip.remove(part);
|
|
10206
10202
|
ct = removeElement(ct, "Override", (el) => el.includes(`PartName="/${part}"`)).xml;
|
|
@@ -88,21 +88,37 @@ pages). The required roles are a HARD checklist — Phase 2 must include them al
|
|
|
88
88
|
|
|
89
89
|
| 场景 | 语气 | 密度 | 必备章节(required roles — all MUST appear) | 叙事(Phase 2) | 默认页数 |
|
|
90
90
|
|---|---|---|---|---|---|
|
|
91
|
-
| **pitch** 融资/路演 | 自信 · 有张力 · 故事驱动 | 稀疏 · 大数字 | Problem · Solution · Market · Product/Traction · Team · **Ask
|
|
91
|
+
| **pitch** 融资/路演 | 自信 · 有张力 · 故事驱动 | 稀疏 · 大数字 | Problem/问题 · Solution/解决方案 · Market/市场 · Product/Traction/产品/进展 · Team/团队 · **Ask/融资需求** | SCQA / 问题-方案 | 10–12 |
|
|
92
92
|
| **内审** | 客观 · 直接 · 结论先行 | 略密(容明细/数据) | 结论/进展 · 关键指标 · 问题与风险 · 下一步/决策项 | 结论先行 | 6–8 |
|
|
93
|
-
| **销售** | 利益驱动 · 客户中心 · 说服 | 中 · 重价值与佐证 | 痛点 · 方案/价值主张 · 差异化 · 客户案例 · ROI/定价 · **CTA
|
|
93
|
+
| **销售** | 利益驱动 · 客户中心 · 说服 | 中 · 重价值与佐证 | 痛点 · 方案/价值主张 · 差异化 · 客户案例 · ROI/定价 · **CTA/行动号召** | 问题-方案 | 8–12 |
|
|
94
94
|
| **讲座** 学术 | 讲解 · 循序渐进 | 中 · 概念+例子 | 学习目标 · 背景/基础 · 核心概念 · 案例/推导 · 小结 | 由浅入深 | 10–15 |
|
|
95
|
-
| **发布会** keynote | 情绪化 · 戏剧性 · 揭晓感 | **极稀疏 · 一页一信息 · 超大视觉** | 钩子 · 背景/痛点 · 产品揭晓 · 亮点特性 · Demo · 价格上市 · **行动号召** | 情绪线 | 15–20 |
|
|
96
|
-
| **培训** | 教学 · 清晰 · 可操作 | 中 · 分模块 | Objectives ·
|
|
95
|
+
| **发布会** keynote | 情绪化 · 戏剧性 · 揭晓感 | **极稀疏 · 一页一信息 · 超大视觉** | 钩子 · 背景/痛点 · 产品揭晓 · 亮点特性 · Demo/演示 · 价格上市 · **行动号召** | 情绪线 | 15–20 |
|
|
96
|
+
| **培训** | 教学 · 清晰 · 可操作 | 中 · 分模块 | Objectives/学习目标 · Module/课程模块 · 练习/示例/Exercise · Quiz/随堂测验 · Summary/小结 | 由浅入深 | 8–15 |
|
|
97
97
|
| **大会** talk | 观点鲜明 · 启发 · 记忆点 | 稀疏 · 一图一句 | 开场钩子 · 核心论点 · 论据/案例 · 反直觉洞察 · 收束记忆点 | 情绪线 / SCQA | 15–20 |
|
|
98
98
|
| **工作汇报** 周月季报 | 客观 · 简洁 · 结论先行 | 中 | 概要 · 进展/成果 · 数据指标 · 问题 · 下一步计划 | 结论先行 | 5–10 |
|
|
99
|
-
| **述职/晋升** | 自信 · 有据 · 个人视角 | 中 | 职责概述 ·
|
|
99
|
+
| **述职/晋升** | 自信 · 有据 · 个人视角 | 中 | 职责概述 · 关键成果/STAR · 数据佐证 · 能力成长 · 规划 | 结论先行 | 8–12 |
|
|
100
100
|
| **项目立项/评审** | 严谨 · 说服 | 中偏密 | 背景/痛点 · 目标 · 方案 · 计划/里程碑 · 资源/预算 · 风险 | 问题-方案 | 8–14 |
|
|
101
101
|
| **年度总结/年会** | 振奋 · 仪式感 | 稀疏 · 大视觉 | 回顾/高光 · 关键数据 · 里程碑 · 团队 · 来年展望 | 情绪线 | 12–20 |
|
|
102
102
|
| **商业计划书** BP | 自信 · 全面 · 数据支撑 | 中偏密 | 问题 · 方案 · 市场 · 产品 · 商业模式 · 竞争 · 财务预测 · 团队 · **融资** | SCQA | 15–25 |
|
|
103
103
|
| **投标** 招投标 | 正式 · 要素齐全 · 得分导向 | 密 | 需求理解 · 方案 · 团队/资质 · 案例业绩 · 实施计划 · **报价** · 售后 | 结论先行 | 15–30 |
|
|
104
104
|
| **政务/党政汇报** | 严肃 · 规范 | 中偏密 | 背景/依据 · 工作举措 · 成效数据 · 存在问题 · 下步部署 | 结论先行 | 8–15 |
|
|
105
105
|
| **公司/产品介绍** | 品牌 · 自信 · 利益导向 | 中 · 重视觉 | 公司/愿景 · 核心能力 · 产品/方案 · 标杆案例 · 数据 · 合作邀请 | 问题-方案 | 8–15 |
|
|
106
|
+
| **产品调研** 竞品分析 市场研究 | 客观 · 有据 · 不替结论下判断 | 中 | 市场规模 · 竞品格局 · 用户需求 · 差距与机会 · 结论建议 | 先摆事实,再给判断 | 10–15 |
|
|
107
|
+
| **会议纪要** 会后同步 | 简洁 · 可执行 · 不复述过程 | 低 | 议题 · 结论 · 待办 · 待决事项 | 结论先行 | 4–8 |
|
|
108
|
+
| **数据分析** 数据解读 指标复盘 | 结论先行 · 可追溯 | 中 | 结论 · 口径与数据源 · 维度拆解 · 归因 · 建议 | 结论先行 | 6–10 |
|
|
109
|
+
|
|
110
|
+
**必备章节要中英双写(`English/中文`)。** 这张 checklist 的用法是拿每个词去**生成
|
|
111
|
+
出来的大纲标题**里找子串(斜杠分段,命中任意一段就算有)。写成纯英文时,一份写得
|
|
112
|
+
好好的中文稿一个都命中不了 —— 于是每次都判缺、每次都白花一轮模型回喂(10–30 秒
|
|
113
|
+
+一次调用的钱)。反过来整行换成中文也不对:中文路演稿**真的**会写 Problem / Ask /
|
|
114
|
+
Demo,换掉只是把漏判翻到另一边。中文那半边要挑够窄的词:写「融资」的话,一张叫
|
|
115
|
+
「我们的融资故事」的封面就能把 Ask 顶掉,而那一节缺了正是这张表存在的理由。
|
|
116
|
+
括号写法(`关键成果(STAR)`)同样不行 —— 它一个斜杠都没有,整串当一个词去找,
|
|
117
|
+
大纲里必须原样出现那七个字符才算命中。
|
|
118
|
+
|
|
119
|
+
还有两个容易漏的:**用单数**(大纲会写 `Module 1`,`Modules` 匹配不上,而 `Module`
|
|
120
|
+
两种都命中);**本来就是中文的那几段也要补英文**(`练习/示例` 漏了 `Exercise`,
|
|
121
|
+
一份英文培训大纲就在这一项上判缺)。
|
|
106
122
|
|
|
107
123
|
Tone also biases the catalog style ranking: 张力/情绪(pitch·发布会·大会)→ 深色酷炫(cool/apple/
|
|
108
124
|
black-gold);务实(内审·销售)→ 白底咨询(mckinsey);清晰(讲座·培训)→ 中性/教育风.
|
|
@@ -131,6 +147,39 @@ Search 8–12 queries across layers, then organize into 6 dimensions:
|
|
|
131
147
|
|
|
132
148
|
Dimensions: 概述 / 背景 / 关键信息 / 证据(案例·数据) / 分析 / 展望.
|
|
133
149
|
|
|
150
|
+
**层次是兜底,场景才是方向。** 上面那五层是**开放式议题**的默认铺法(用户只给了
|
|
151
|
+
一个主题、没有材料)。一旦 Phase 0 选定了场景,就照下表按**这份稿子真正需要的东西**
|
|
152
|
+
去查 —— 每条焦点出 1–2 条查询词,再从上面五层补 2–4 条防止十条围着一个角度。
|
|
153
|
+
用户在 Phase 0 自己交代的那句话优先级最高,先查他明说要的。
|
|
154
|
+
|
|
155
|
+
焦点直接对着上面场景矩阵的 `必备章节` —— 查回来的东西要能填进那些章节,
|
|
156
|
+
否则这一轮检索就是白花的(按次计费)。
|
|
157
|
+
|
|
158
|
+
| 场景 | 检索焦点 |
|
|
159
|
+
|---|---|
|
|
160
|
+
| **pitch** 融资/路演 | 市场规模与增速(TAM/SAM) · 可比公司融资与估值 · 同类产品与差异 · 目标客群痛点证据 |
|
|
161
|
+
| **内审** | 行业对标指标 · 口径定义 · 外部风险事件 |
|
|
162
|
+
| **销售** | 客户所在行业痛点 · 竞品方案与差异 · 同行落地案例与 ROI · 公开定价 |
|
|
163
|
+
| **讲座** 学术 | 概念定义与权威教材 · 关键论文与综述 · 经典案例/推导 · 常见误解 |
|
|
164
|
+
| **发布会** keynote | 同类产品痛点与差评 · 竞品参数对比 · 竞品定价与上市节奏 · 可引用的用户场景 |
|
|
165
|
+
| **培训** | 权威教程与标准 · 分步操作示例 · 常见错误与排错 · 练习题与考点 |
|
|
166
|
+
| **大会** talk | 反直觉数据与研究 · 争议观点与反例 · 一手案例/失败教训 · 可引用金句 |
|
|
167
|
+
| **工作汇报** 周月季报 | 行业基准与同行数据 · 指标口径定义 · 外部环境变化 |
|
|
168
|
+
| **述职/晋升** | 同岗位能力模型 · 行业对标数据 · 该领域公认的成果衡量口径 |
|
|
169
|
+
| **项目立项/评审** | 同类项目投入产出 · 技术方案与选型对比 · 合规与风险案例 · 人力/预算基准 |
|
|
170
|
+
| **年度总结/年会** | 行业年度大事 · 同行里程碑对照 · 来年趋势预测 |
|
|
171
|
+
| **商业计划书** BP | 市场规模与分层 · 竞争格局与份额 · 商业模式与单位经济 · 可比公司财务与融资 |
|
|
172
|
+
| **投标** 招投标 | 招标方背景与既往采购 · 评分办法与资质要求 · 同类中标公告与报价区间 · 标杆案例 |
|
|
173
|
+
| **政务/党政汇报** | 政策文件与依据原文 · 上级部署口径 · 同级地区做法 · 公开统计数据 |
|
|
174
|
+
| **公司/产品介绍** | 行业地位与第三方报告 · 竞品对比 · 可公开引用的客户案例 · 行业趋势 |
|
|
175
|
+
| **产品调研** 竞品分析 市场研究 | 市场规模与增速 · 竞品功能与定价对比 · 用户评价与差评 · 渠道与份额数据 |
|
|
176
|
+
| **会议纪要** 会后同步 | 议题涉及的外部事实 · 相关口径与定义 · 同类决策的公开先例 |
|
|
177
|
+
| **数据分析** 数据解读 指标复盘 | 指标口径定义 · 行业基准值 · 同类归因案例与方法 |
|
|
178
|
+
|
|
179
|
+
内部数据型的几档(内审 · 述职 · 工作汇报 · 年度总结)焦点**刻意收窄到对标与口径**:
|
|
180
|
+
它们的主体材料在用户自己手里,联网能补的只有"外面是什么水位"。照五层去查行业定义
|
|
181
|
+
和历史,查回来的东西一条都填不进它们的必备章节。
|
|
182
|
+
|
|
134
183
|
**Fan out, then cite:** run layers in PARALLEL (many searches at once, or a
|
|
135
184
|
sub-agent per dimension) — breadth beats depth-first. **Every data point
|
|
136
185
|
(number/percent/year/quote) carries its source** — keep a `{claim → URL}` list; a
|
|
@@ -61,6 +61,14 @@ figure row missing its numbers — **fails the build** (exit 1, nothing written)
|
|
|
61
61
|
shipping the template's demo content. Regenerate the index with `node scripts/deck/gen-slots.mjs`
|
|
62
62
|
after touching a `.tpl`.
|
|
63
63
|
|
|
64
|
+
**不要给可变内容写死容器高度。** `.card{height:…px}` 这类写法在皮肤给 `.card` 设了
|
|
65
|
+
`overflow:hidden` 时会把超出的文字**静默裁掉**(`blocks-2x2` 就这么带着缺陷出厂过)。
|
|
66
|
+
让高度由内容决定:网格用 `grid-auto-rows:min-content`(下限必须显式写 `min-content` ——
|
|
67
|
+
`1fr`/`auto` 的自动下限对 `overflow` 非 visible 的网格项一律归零),余量交给外层
|
|
68
|
+
`justify-content:space-between`。这样内容超量时会把后续元素顶出页面,被 `deck-qa` 的
|
|
69
|
+
`off-canvas`/`overlap` 抓到,而不是变成一排缺了下半截的字。`deck-qa` 的第三条硬失败
|
|
70
|
+
`clipped` 专门守这件事,并且对**整册版式库**逐个版式扫,不只是 `sweep-spec.json` 里那几个。
|
|
71
|
+
|
|
64
72
|
## Catalog
|
|
65
73
|
|
|
66
74
|
Each slide carries `data-layout="<id>"` (drives the diversity lint + traceability).
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
<!-- layouts/blocks-2x2 — structure-only (paint via styles/<skin>/skin.css). 招牌"2×2 大色块 + 引言条":标题 + 两段引子 + 四个大编号块(skin 决定实色/柔彩/描边)+ 底部引言条 + 参考框. data-layout drives P1/P2/P3 + dedup. -->
|
|
2
|
+
<!-- 引子/色块/引言条走一条 flex 纵向流(.blocks-flow),卡片不再写死高度 —— `.card` 曾是
|
|
3
|
+
`height:118px`,而卡内 26px padding + 34px 编号 + 22px 标题 + 14px 正文实际要 148–178px,
|
|
4
|
+
12 个把 `.card` 设成 overflow:hidden 的族因此把正文最后一行拦腰切断(deck-qa-sweep 的
|
|
5
|
+
`clipped` 判据守的就是它)。现在行高 `min-content` 咬死内容,
|
|
6
|
+
余量由 `justify-content:space-between` 分给引子与引言条之间 —— 内容再长也只会把引言条
|
|
7
|
+
顶向页脚(闸门报 off-canvas/overlap),不会静默裁字。下限必须写成显式的 `min-content`:
|
|
8
|
+
`1fr`/`auto` 的自动下限对 `overflow:hidden` 的网格项一律归零,而 `.card` 的 overflow
|
|
9
|
+
恰恰是皮肤设的 —— 写 `1fr` 等于没有下限,卡片照样会被压扁再裁字。 -->
|
|
2
10
|
<section class="slide" data-layout="blocks-2x2" style="position:relative;width:1280px;height:720px;overflow:hidden"><div class="slide-backdrop"></div>
|
|
3
11
|
|
|
4
12
|
|
|
5
13
|
<div class="eyebrow-row" style="position:absolute;left:calc(96px * var(--space-scale,1));top:124px;display:flex;align-items:center;gap:14px"><span class="accent-bar" style="width:30px;height:1px;display:inline-block"></span><span class="eyebrow" style="font-size:calc(13px * var(--type-scale,1));letter-spacing:.28em">{{eyebrow|PRINCIPLES}}</span></div>
|
|
6
14
|
<h2 class="deck-title" style="position:absolute;left:calc(92px * var(--space-scale,1));top:146px;font-size:calc(42px * var(--type-scale,1));letter-spacing:-.4px">{{title|章节主标题}}</h2>
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
<div class="card" style="position:relative;padding:26px
|
|
11
|
-
<div class="card" style="position:relative;padding:26px
|
|
12
|
-
<div class="card" style="position:relative;padding:26px
|
|
15
|
+
<div class="blocks-flow" style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));top:212px;bottom:62px;display:flex;flex-direction:column;justify-content:space-between;gap:14px">
|
|
16
|
+
<p class="card-body" style="flex:none;font-size:calc(16px * var(--type-scale,1));line-height:1.7">{{intro|一段引子说明,两三行,交代这一章的核心观点与背景,替换为真实内容。承接标题,引出下面四个要点。}}</p>
|
|
17
|
+
<div class="card-grid" style="flex:none;display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:min-content;gap:16px">
|
|
18
|
+
<div class="card" style="position:relative;padding:16px 26px"><span class="card-rail" style="position:absolute;left:0;top:20px;width:3px;height:34px;border-radius:2px"></span><span class="card-idx" style="font-size:calc(26px * var(--type-scale,1));font-weight:800;line-height:1">01</span><div class="card-title" style="font-size:calc(22px * var(--type-scale,1));font-weight:700;margin-top:4px">{{block1_title|要点一标题}}</div><p class="card-body" style="font-size:calc(14px * var(--type-scale,1));line-height:1.5;margin-top:4px">{{block1_body|一句说明,替换为真实内容。}}</p></div>
|
|
19
|
+
<div class="card" style="position:relative;padding:16px 26px"><span class="card-rail" style="position:absolute;left:0;top:20px;width:3px;height:34px;border-radius:2px"></span><span class="card-idx" style="font-size:calc(26px * var(--type-scale,1));font-weight:800;line-height:1">02</span><div class="card-title" style="font-size:calc(22px * var(--type-scale,1));font-weight:700;margin-top:4px">{{block2_title|要点二标题}}</div><p class="card-body" style="font-size:calc(14px * var(--type-scale,1));line-height:1.5;margin-top:4px">{{block2_body|一句说明,替换为真实内容。}}</p></div>
|
|
20
|
+
<div class="card" style="position:relative;padding:16px 26px"><span class="card-rail" style="position:absolute;left:0;top:20px;width:3px;height:34px;border-radius:2px"></span><span class="card-idx" style="font-size:calc(26px * var(--type-scale,1));font-weight:800;line-height:1">03</span><div class="card-title" style="font-size:calc(22px * var(--type-scale,1));font-weight:700;margin-top:4px">{{block3_title|要点三标题}}</div><p class="card-body" style="font-size:calc(14px * var(--type-scale,1));line-height:1.5;margin-top:4px">{{block3_body|一句说明,替换为真实内容。}}</p></div>
|
|
21
|
+
<div class="card" style="position:relative;padding:16px 26px"><span class="card-rail" style="position:absolute;left:0;top:20px;width:3px;height:34px;border-radius:2px"></span><span class="card-idx" style="font-size:calc(26px * var(--type-scale,1));font-weight:800;line-height:1">04</span><div class="card-title" style="font-size:calc(22px * var(--type-scale,1));font-weight:700;margin-top:4px">{{block4_title|要点四标题}}</div><p class="card-body" style="font-size:calc(14px * var(--type-scale,1));line-height:1.5;margin-top:4px">{{block4_body|一句说明,替换为真实内容。}}</p></div>
|
|
22
|
+
</div>
|
|
23
|
+
<div style="flex:none;display:flex;gap:18px">
|
|
24
|
+
<div class="quote-bar" style="flex:1.7;padding:14px 22px;border-radius:var(--card-radius)"><div class="quote-txt" style="font-size:calc(17px * var(--type-scale,1));font-weight:700">{{quote|"一句金句,点睛这一章。"}}</div><div class="quote-attr" style="font-size:calc(13px * var(--type-scale,1));margin-top:4px">{{quote_attr|— 出处 / 作者}}</div></div>
|
|
25
|
+
<div class="footnote" style="flex:1;padding:14px 20px;border-radius:var(--card-radius)"><span class="footnote-label" style="font-size:calc(12px * var(--type-scale,1));font-weight:700">参考资料:</span><span class="footnote-body" style="font-size:calc(12px * var(--type-scale,1))"> {{refs|来源一;来源二}}</span></div>
|
|
13
26
|
</div>
|
|
14
|
-
<div style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));bottom:76px;display:flex;gap:18px">
|
|
15
|
-
<div class="quote-bar" style="flex:1.7;padding:18px 24px;border-radius:var(--card-radius)"><div class="quote-txt" style="font-size:calc(17px * var(--type-scale,1));font-weight:700">{{quote|"一句金句,点睛这一章。"}}</div><div class="quote-attr" style="font-size:calc(13px * var(--type-scale,1));margin-top:4px">{{quote_attr|— 出处 / 作者}}</div></div>
|
|
16
|
-
<div class="footnote" style="flex:1;padding:16px 20px;border-radius:var(--card-radius)"><span class="footnote-label" style="font-size:calc(12px * var(--type-scale,1));font-weight:700">参考资料:</span><span class="footnote-body" style="font-size:calc(12px * var(--type-scale,1))"> {{refs|来源一;来源二}}</span></div>
|
|
17
27
|
</div>
|
|
18
28
|
<div class="rule rule-foot" style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));bottom:40px;height:1px"></div><div class="deck-footer" style="position:absolute;left:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1));letter-spacing:.1em">{{footer|报告标题 / 章节}}</div><div class="deck-pageno" style="position:absolute;right:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1))">{{pageno|01}}</div>
|
|
19
29
|
</section>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!-- layouts/cards-3 — structure-only (paint via styles/<skin>/skin.css). data-layout drives P1/P2/P3 + dedup. -->
|
|
2
2
|
<section class="slide" data-layout="cards-3" style="position:relative;width:1280px;height:720px;overflow:hidden"><div class="slide-backdrop"></div><div class="eyebrow-row" style="position:absolute;left:calc(96px * var(--space-scale,1));top:132px;display:flex;align-items:center;gap:14px"><span class="accent-bar" style="width:30px;height:1px;display:inline-block"></span><span class="eyebrow" style="font-size:calc(13px * var(--type-scale,1));letter-spacing:.28em">{{eyebrow|OVERVIEW}}</span></div><h2 class="deck-title" style="position:absolute;left:calc(92px * var(--space-scale,1));top:158px;font-size:calc(48px * var(--type-scale,1));letter-spacing:-.4px">{{title|三个要点}}</h2>
|
|
3
|
-
<div class="card-grid" style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));top:290px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:24px"><div class="card" style="position:relative;padding:32px 34px;height:
|
|
3
|
+
<div class="card-grid" style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));top:290px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:24px"><div class="card" style="position:relative;padding:32px 34px;min-height:180px"><span class="card-rail" style="position:absolute;left:0;top:28px;width:3px;height:40px;border-radius:2px"></span><div style="display:flex;align-items:center;gap:10px"><span class="card-dot" style="width:7px;height:7px;border-radius:50%;display:inline-block"></span><span class="card-idx" style="font-size:calc(12px * var(--type-scale,1));letter-spacing:.2em">01</span></div><div class="card-title" style="font-size:calc(23px * var(--type-scale,1));font-weight:700;margin-top:14px">{{card1_title|要点标题}}</div><p class="card-body" style="font-size:calc(15.5px * var(--type-scale,1));line-height:1.6;margin-top:10px">{{card1_body|一句说明,替换为真实内容。}}</p></div><div class="card" style="position:relative;padding:32px 34px;min-height:180px"><span class="card-rail" style="position:absolute;left:0;top:28px;width:3px;height:40px;border-radius:2px"></span><div style="display:flex;align-items:center;gap:10px"><span class="card-dot" style="width:7px;height:7px;border-radius:50%;display:inline-block"></span><span class="card-idx" style="font-size:calc(12px * var(--type-scale,1));letter-spacing:.2em">02</span></div><div class="card-title" style="font-size:calc(23px * var(--type-scale,1));font-weight:700;margin-top:14px">{{card2_title|要点标题}}</div><p class="card-body" style="font-size:calc(15.5px * var(--type-scale,1));line-height:1.6;margin-top:10px">{{card2_body|一句说明,替换为真实内容。}}</p></div><div class="card" style="position:relative;padding:32px 34px;min-height:180px"><span class="card-rail" style="position:absolute;left:0;top:28px;width:3px;height:40px;border-radius:2px"></span><div style="display:flex;align-items:center;gap:10px"><span class="card-dot" style="width:7px;height:7px;border-radius:50%;display:inline-block"></span><span class="card-idx" style="font-size:calc(12px * var(--type-scale,1));letter-spacing:.2em">03</span></div><div class="card-title" style="font-size:calc(23px * var(--type-scale,1));font-weight:700;margin-top:14px">{{card3_title|要点标题}}</div><p class="card-body" style="font-size:calc(15.5px * var(--type-scale,1));line-height:1.6;margin-top:10px">{{card3_body|一句说明,替换为真实内容。}}</p></div></div>
|
|
4
4
|
<div class="rule rule-foot" style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));bottom:40px;height:1px"></div><div class="deck-footer" style="position:absolute;left:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1));letter-spacing:.1em">{{footer|报告标题 / 章节}}</div><div class="deck-pageno" style="position:absolute;right:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1))">{{pageno|01}}</div></section>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!-- layouts/org-tree — structure-only variant (paint via skin). -->
|
|
2
2
|
<section class="slide" data-layout="org-tree" style="position:relative;width:1280px;height:720px;overflow:hidden"><div class="slide-backdrop"></div><div class="eyebrow-row" style="position:absolute;left:calc(96px * var(--space-scale,1));top:132px;display:flex;align-items:center;gap:14px"><span class="accent-bar" style="width:30px;height:1px;display:inline-block"></span><span class="eyebrow" style="font-size:calc(13px * var(--type-scale,1));letter-spacing:.28em">{{eyebrow|STRUCTURE}}</span></div><h2 class="deck-title" style="position:absolute;left:calc(92px * var(--space-scale,1));top:158px;font-size:calc(48px * var(--type-scale,1));letter-spacing:-.4px">{{title|组织架构}}</h2>
|
|
3
|
-
<svg width="1280" height="720" viewBox="0 0 1280 720" style="position:absolute;inset:0;pointer-events:none"><g class="hub-link" fill="none" stroke-width="1.5"><line x1="640" y1="352" x2="640" y2="416"/><line x1="300" y1="416" x2="980" y2="416"/><line x1="300" y1="416" x2="300" y2="452"/><line x1="640" y1="416" x2="640" y2="452"/><line x1="980" y1="416" x2="980" y2="452"/></g></svg><div class="card" style="position:absolute;left:520px;top:290px;width:240px;height:62px;display:flex;align-items:center;justify-content:center"><div class="card-title" style="font-size:calc(20px * var(--type-scale,1));font-weight:700">{{top|最高负责人}}</div></div><div class="card" style="position:absolute;left:180px;top:452px;width:240px;height:
|
|
3
|
+
<svg width="1280" height="720" viewBox="0 0 1280 720" style="position:absolute;inset:0;pointer-events:none"><g class="hub-link" fill="none" stroke-width="1.5"><line x1="640" y1="352" x2="640" y2="416"/><line x1="300" y1="416" x2="980" y2="416"/><line x1="300" y1="416" x2="300" y2="452"/><line x1="640" y1="416" x2="640" y2="452"/><line x1="980" y1="416" x2="980" y2="452"/></g></svg><div class="card" style="position:absolute;left:520px;top:290px;width:240px;height:62px;display:flex;align-items:center;justify-content:center"><div class="card-title" style="font-size:calc(20px * var(--type-scale,1));font-weight:700">{{top|最高负责人}}</div></div><div class="card" style="position:absolute;left:180px;top:452px;width:240px;min-height:96px;padding:22px"><div class="card-title" style="font-size:calc(18px * var(--type-scale,1));font-weight:700">{{dept1_title|技术部}}</div><p class="card-body" style="font-size:calc(14px * var(--type-scale,1));line-height:1.5;margin-top:8px">{{dept1_body|职能一句说明。}}</p></div><div class="card" style="position:absolute;left:520px;top:452px;width:240px;min-height:96px;padding:22px"><div class="card-title" style="font-size:calc(18px * var(--type-scale,1));font-weight:700">{{dept2_title|产品部}}</div><p class="card-body" style="font-size:calc(14px * var(--type-scale,1));line-height:1.5;margin-top:8px">{{dept2_body|职能一句说明。}}</p></div><div class="card" style="position:absolute;left:860px;top:452px;width:240px;min-height:96px;padding:22px"><div class="card-title" style="font-size:calc(18px * var(--type-scale,1));font-weight:700">{{dept3_title|运营部}}</div><p class="card-body" style="font-size:calc(14px * var(--type-scale,1));line-height:1.5;margin-top:8px">{{dept3_body|职能一句说明。}}</p></div>
|
|
4
4
|
<div class="rule rule-foot" style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));bottom:40px;height:1px"></div><div class="deck-footer" style="position:absolute;left:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1));letter-spacing:.1em">{{footer|报告标题 / 章节}}</div><div class="deck-pageno" style="position:absolute;right:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1))">{{pageno|01}}</div></section>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!-- layouts/steps-vertical — structure-only variant (paint via skin). rows.steps{name,body} — the numbered circle uses the row index ({{_n}}); the connector line spans the list, so 3–5 steps all sit right. -->
|
|
2
2
|
<section class="slide" data-layout="steps-vertical" style="position:relative;width:1280px;height:720px;overflow:hidden"><div class="slide-backdrop"></div><div class="eyebrow-row" style="position:absolute;left:calc(96px * var(--space-scale,1));top:132px;display:flex;align-items:center;gap:14px"><span class="accent-bar" style="width:30px;height:1px;display:inline-block"></span><span class="eyebrow" style="font-size:calc(13px * var(--type-scale,1));letter-spacing:.28em">{{eyebrow|PROCESS}}</span></div><h2 class="deck-title" style="position:absolute;left:calc(92px * var(--space-scale,1));top:158px;font-size:calc(48px * var(--type-scale,1));letter-spacing:-.4px">{{title|实施步骤}}</h2>
|
|
3
|
-
<div style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));top:290px;display:flex;flex-direction:column;gap:20px"><div class="step-line" style="position:absolute;left:
|
|
3
|
+
<div style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));top:290px;display:flex;flex-direction:column;gap:20px"><div class="step-line" style="position:absolute;left:31px;top:41px;bottom:41px;width:2px"></div>{{#steps}}<div style="position:relative;display:flex;align-items:center;gap:28px;height:82px"><div class="step-circle" style="width:64px;height:64px;border-radius:50%;flex:0 0 auto;display:flex;align-items:center;justify-content:center;text-align:center;line-height:1;font-size:calc(26px * var(--type-scale,1));font-weight:700">{{_n|1}}</div><div><div class="step-title" style="font-size:calc(21px * var(--type-scale,1));font-weight:700">{{name|占位步骤}}</div><p class="step-body" style="font-size:calc(15px * var(--type-scale,1));line-height:1.5;margin-top:4px">{{body|一句说明,替换为真实内容。}}</p></div></div>{{/steps}}</div>
|
|
4
4
|
<div class="rule rule-foot" style="position:absolute;left:calc(96px * var(--space-scale,1));right:calc(96px * var(--space-scale,1));bottom:40px;height:1px"></div><div class="deck-footer" style="position:absolute;left:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1));letter-spacing:.1em">{{footer|报告标题 / 章节}}</div><div class="deck-pageno" style="position:absolute;right:calc(96px * var(--space-scale,1));bottom:17px;font-size:calc(12px * var(--type-scale,1))">{{pageno|01}}</div></section>
|