Sync multi-theme optimizations from dev (exclude Bento/Style C)

- Magazine (A) + Swiss (B) template updates: ?slide=N, ESC thumbnail fix, reduced-motion, spacing fixes
- Swiss validator: Playwright M1/M2 rendered measurement (overflow/whitespace/nav-safe/title-gap)
- SKILL.md: Step 0 upstream-update check, image-mixing decision tree, measure-before-edit ladder
- checklist.md: image-role guidance, quiet-zone rule, post-render measurement section
- Remove author-local golden-source absolute paths; point to in-repo template-swiss.html snapshot
- Add ppt-skill-showcase.png and A/B-only unification-plan.md
- Exclude all Bento/Style C assets, references, validator, and design-research docs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
郭浩andCursor committed 2026-07-22 14:53:09 +08:00
1 parent 82fe5ae129
commit ba12c5c9ad
11 files changed
+429 -95

No files matched your search

+201
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
const file = process.argv[2];
const allowExperimental = process.argv.includes('--allow-experimental');
@@ -14,6 +17,31 @@ const htmlForSlides = html.replace(/<!--[\s\S]*?-->/g, '');
const errors = [];
const warnings = [];
function overflowFix(px) {
const n = Math.round(px);
if (n <= 40) return `only ${n}px over: nudge content up or tighten one gap/padding by 20-40px; do not delete content`;
if (n <= 90) return `${n}px over: compact local gaps/padding and reduce one block height; avoid cutting copy`;
if (n <= 160) return `${n}px over: reduce a display title slightly or compress one paragraph before deleting content`;
return `${n}px over: switch to a higher-capacity layout or remove/merge content intentionally`;
}
async function loadPlaywright() {
const candidates = [
createRequire(import.meta.url),
createRequire(pathToFileURL(path.join(process.cwd(), 'package.json')).href),
];
for (const req of candidates) {
try {
const resolved = req.resolve('playwright');
const mod = await import(pathToFileURL(resolved).href);
return mod.default || mod;
} catch {
// Try the next resolution root.
}
}
return null;
}
const allowedLayouts = new Set([
'SWISS-COVER-ASCII',
'SWISS-CLOSING-ASCII',
@@ -96,6 +124,179 @@ slides.forEach((slide) => {
}
});
async function runRenderedMeasurements() {
const playwright = await loadPlaywright();
if (!playwright?.chromium) {
warnings.push('Rendered measurement skipped: Playwright is not resolvable from the skill folder or current project. Static Swiss checks still ran.');
return;
}
const browser = await playwright.chromium.launch({
args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'],
});
try {
const ctx = await browser.newContext({
viewport: { width: 1600, height: 900 },
deviceScaleFactor: 1,
});
const page = await ctx.newPage();
await page.goto(pathToFileURL(path.resolve(file)).href, { waitUntil: 'domcontentloaded' });
await Promise.race([
page.evaluate(() => document.fonts && document.fonts.ready),
page.waitForTimeout(1800),
]);
await page.waitForTimeout(800);
const measures = await page.$$eval('section.slide', (els) => {
const TRANSPARENT = /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0?\s*\)|transparent/;
const titleSelector = [
'.h-hero', '.h-hero-zh', '.h-xl', '.h-xl-zh', '.h-statement',
'.display', '.display-zh', '.h1-zh', '.h2-zh', '.h-md',
'.step-title', 'h1', 'h2', 'h3',
].join(',');
const hasDirectText = (n) => {
for (const c of n.childNodes) {
if (c.nodeType === 3 && c.textContent.trim().length > 0) return true;
}
return false;
};
const colorVisible = (c) => c && !TRANSPARENT.test(c);
const labelFor = (n) => {
const cls = n.className ? '.' + String(n.className).split(' ').filter(Boolean).join('.') : n.tagName.toLowerCase();
const text = n.textContent.trim().replace(/\s+/g, ' ').slice(0, 40);
return text ? `${cls} "${text}"` : cls;
};
const isMeaningful = (el, n) => {
const er = el.getBoundingClientRect();
const posterArea = er.width * er.height;
const tag = n.tagName;
const cs = getComputedStyle(n);
const r = n.getBoundingClientRect();
if (r.width < 6 || r.height < 6) return false;
if (n === el || n.classList.contains('canvas-card')) return false;
if (cs.position === 'fixed') return false;
if (cs.position === 'absolute' && r.width * r.height >= posterArea * 0.82) return false;
if (n.matches('canvas.ascii-bg, canvas.mag-bg, .grain, .dot-mat, .ring-mat, .cross-mat')) return false;
const isText = hasDirectText(n);
const isMedia = tag === 'IMG' || tag === 'CANVAS' || tag === 'SVG';
const isRule = tag === 'HR' || (r.height <= 4 && (
parseFloat(cs.borderTopWidth) >= 1 ||
parseFloat(cs.borderBottomWidth) >= 1 ||
colorVisible(cs.backgroundColor)
));
const hasFill = colorVisible(cs.backgroundColor) && r.width * r.height >= 1600 && !['MAIN', 'SECTION'].includes(tag);
const hasBorder = (parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) +
parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth)) >= 1 && r.width * r.height >= 1600;
return isText || isMedia || isRule || hasFill || hasBorder;
};
const titleGapChecks = (el, nodes) => {
const titles = Array.from(el.querySelectorAll(titleSelector)).filter((n) => n.textContent.trim());
const out = [];
for (const title of titles) {
const tr = title.getBoundingClientRect();
const isLocal = title.matches('.h-md, .step-title, h3');
const minGap = isLocal ? 14 : 32;
let nearest = null;
let nearestGap = Infinity;
for (const node of nodes) {
if (node === title || title.contains(node) || node.contains(title)) continue;
const nr = node.getBoundingClientRect();
const gap = nr.top - tr.bottom;
if (gap < -2) continue;
const overlap = Math.max(0, Math.min(tr.right, nr.right) - Math.max(tr.left, nr.left));
const overlapRatio = overlap / Math.min(tr.width, nr.width);
if (overlapRatio < 0.12 && gap < 96) continue;
if (gap < nearestGap) {
nearestGap = gap;
nearest = node;
}
}
if (nearest && nearestGap < minGap) {
out.push({ title: labelFor(title), next: labelFor(nearest), gap: Math.round(nearestGap), minGap });
}
}
return out;
};
return els.map((el, index) => {
const er = el.getBoundingClientRect();
const H = el.clientHeight;
const W = el.clientWidth;
const nodes = Array.from(el.querySelectorAll('*')).filter((n) => isMeaningful(el, n));
let top = Infinity;
let bottom = -Infinity;
let topNode = null;
let bottomNode = null;
for (const n of nodes) {
const r = n.getBoundingClientRect();
const itemTop = r.top - er.top;
const itemBottom = r.bottom - er.top;
if (itemTop < top) {
top = itemTop;
topNode = n;
}
if (itemBottom > bottom) {
bottom = itemBottom;
bottomNode = n;
}
}
if (!nodes.length) {
top = 0;
bottom = 0;
}
const safeBottom = Math.round(H * 0.93);
return {
idx: index + 1,
id: el.id || '',
layout: el.dataset.layout || '',
width: W,
height: H,
scrollOverflow: Math.max(0, Math.round(el.scrollHeight - H)),
visual: {
top: Math.round(top),
bottom: Math.round(bottom),
activeRatio: H ? (bottom - top) / H : 0,
bottomGap: Math.max(0, Math.round(H - bottom)),
topOverflow: Math.max(0, Math.round(-top)),
bottomOverflow: Math.max(0, Math.round(bottom - H)),
bottomNode: bottomNode ? labelFor(bottomNode) : '',
topNode: topNode ? labelFor(topNode) : '',
safeBottom,
},
titleGaps: titleGapChecks(el, nodes),
};
});
});
for (const m of measures) {
const prefix = `Slide ${m.idx}${m.layout ? ` (${m.layout})` : ''}`;
if (m.scrollOverflow > 4) {
errors.push(`${prefix}: M1 DOM overflow ${m.scrollOverflow}px. ${overflowFix(m.scrollOverflow)}.`);
}
if (m.visual.bottomOverflow > 4) {
errors.push(`${prefix}: M1 visual bottom overflow ${m.visual.bottomOverflow}px; lowest element is ${m.visual.bottomNode}. ${overflowFix(m.visual.bottomOverflow)}.`);
}
if (m.visual.topOverflow > 4) {
errors.push(`${prefix}: M1 visual top overflow ${m.visual.topOverflow}px; highest element is ${m.visual.topNode}. Move content down by the measured overflow plus 16-24px.`);
}
if (m.visual.bottom > m.visual.safeBottom) {
warnings.push(`${prefix}: M1 content reaches ${Math.round(m.visual.bottom)}px; nav-safe line is ${m.visual.safeBottom}px. Lift the lowest block or add .nav-safe-bottom.`);
}
if (m.visual.bottomGap > 170 && m.visual.activeRatio < 0.74) {
warnings.push(`${prefix}: M1 bottom whitespace ${m.visual.bottomGap}px; active content height ${Math.round(m.visual.activeRatio * 100)}%. Restore spacing/content instead of over-correcting overflow.`);
}
for (const gap of m.titleGaps) {
warnings.push(`${prefix}: M2 ${gap.title} has ${gap.gap}px gap before ${gap.next} (min ${gap.minGap}px).`);
}
}
} finally {
await browser.close();
}
}
await runRenderedMeasurements();
if (warnings.length) {
console.warn('Warnings:');
for (const warning of warnings) console.warn(`- ${warning}`);