mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(skills): integrate lewislulu/html-ppt-skill as html-ppt + 15 per-template Examples cards
Bring the MIT-licensed lewislulu/html-ppt-skill upstream into skills/html-ppt/
with its full asset tree (36 themes, 31 single-page layouts, 27 CSS + 20
canvas-FX animations, runtime + presenter mode, all 15 full-deck templates,
and the upstream LICENSE preserved verbatim).
Surface each full-deck template as its own Examples gallery card via thin
wrapper skills under skills/html-ppt-<template>/. Each wrapper ships:
- SKILL.md with `od.mode=deck`, scenario, `featured: 20-34` (slotting after
the existing curated cards), an `od.example_prompt` tuned to the template,
and `od.upstream` pointing at the upstream repo. Clicking "Use this prompt"
on a card now wires up `kind=deck` + `speakerNotes=true` and seeds the
composer with the upstream's authoring flow so the prompt -> output path
matches the upstream demo.
- example.html baked self-contained (fonts/base/animations/style/theme CSS
inlined, runtime <script> stripped) so the gallery srcdoc iframe renders
the upstream look without external paths.
scripts/scaffold-html-ppt-skills.mjs and scripts/bake-html-ppt-examples.mjs
are idempotent generators — re-run after editing skills/html-ppt/ to re-sync
all per-template wrappers and their baked examples.
Add a Credits section + extend the License section in README.md /
README.zh-CN.md / README.ko.md to credit the upstream alongside the
already-cited op7418/guizang-ppt-skill.
* fix(scripts): allowlist html-ppt skill JS for residual-js check
Add scripts/bake-html-ppt-examples.mjs and scripts/scaffold-html-ppt-skills.mjs
to allowedExactPaths, and skills/html-ppt/assets/ to allowedPathPrefixes so
pnpm check:residual-js no longer flags the vendored upstream runtime JS or the
new maintainer-only .mjs scripts.
* fix(skills): keep all slides in baked html-ppt examples + correct asset guidance
The bake script's `STATIC_FALLBACK_CSS` set `.slide+.slide{display:none}`,
which silently truncated every baked `example.html` to slide 1. That artifact
is also served by `/api/skills/:id/example` and reused by the Examples
preview modal's share/export and print-to-PDF, so the rule dropped the rest
of the deck from those flows. Drop the rule — slides now stack in the
print-style flow the surrounding comment already described, the gallery
thumbnail iframe still naturally lands on slide 1 (each `.slide` is `100vh`),
and modal/share/export contains the full deck.
The wrapper SKILL.md authoring instructions told agents to copy
`index.html` + `style.css` into a project while keeping the upstream
`../../../assets/...` links, but those parent-relative URLs only resolve
in-tree (the template sits three folders deep). Once the file lives in a
project artifact, `base.css`, `animations.css`, and `runtime.js` 404 and
the deck never activates. Replace step 3 with two recipes — copy the
shared assets into a project-local `assets/` and rewrite the four tags,
or inline the CSS/JS directly — and re-emit all 15 wrapper SKILL.md
files via the scaffold generator.
133 lines
5.2 KiB
JavaScript
133 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Bake self-contained example.html files for each html-ppt full-deck template.
|
|
//
|
|
// The Examples gallery in apps/web renders each skill's example via an iframe
|
|
// `srcdoc`, which has no opener path and can't reach companion CSS files.
|
|
// The upstream `templates/full-decks/<name>/index.html` references shared
|
|
// assets via `../../../assets/...` paths — we inline those + the per-deck
|
|
// `style.css`, drop the runtime <script> (the gallery only shows a static
|
|
// snapshot of slide 1), and write the result to:
|
|
//
|
|
// skills/html-ppt-<name>/example.html
|
|
//
|
|
// Each per-template skill folder must already exist with a SKILL.md.
|
|
|
|
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const HTML_PPT = path.join(ROOT, 'skills', 'html-ppt');
|
|
const ASSETS = path.join(HTML_PPT, 'assets');
|
|
const FULL_DECKS = path.join(HTML_PPT, 'templates', 'full-decks');
|
|
const SKILLS = path.join(ROOT, 'skills');
|
|
|
|
async function readMaybe(p) {
|
|
try {
|
|
return await readFile(p, 'utf8');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
const sharedFonts = await readMaybe(path.join(ASSETS, 'fonts.css'));
|
|
const sharedBase = await readMaybe(path.join(ASSETS, 'base.css'));
|
|
const sharedAnimations = await readMaybe(
|
|
path.join(ASSETS, 'animations', 'animations.css'),
|
|
);
|
|
|
|
// Without runtime.js, no slide gets `.is-active`, so the deck would render
|
|
// blank. For a static preview we surface every slide and stack them in
|
|
// print-style flow: each slide is 100vh, so the gallery thumbnail iframe
|
|
// (fixed viewport) naturally lands on slide 1, while the modal/export and
|
|
// print-to-PDF flows scroll/page through the full deck. We deliberately do
|
|
// not hide later slides — this artifact is also served via
|
|
// `/api/skills/:id/example` and reused by share/export, where dropping
|
|
// everything past slide 1 would silently truncate the deck.
|
|
const STATIC_FALLBACK_CSS = `
|
|
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
|
.deck{height:auto;min-height:100vh;overflow:visible}
|
|
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
|
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
|
.notes{display:none!important}
|
|
`;
|
|
|
|
function inlineLink(html, href, content) {
|
|
// Replace <link rel="stylesheet" href="..."> regardless of attribute order.
|
|
const re = new RegExp(
|
|
`<link[^>]*href=["']${href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*>`,
|
|
'g',
|
|
);
|
|
return html.replace(re, `<style>${content}\n</style>`);
|
|
}
|
|
|
|
async function bakeOne(name) {
|
|
const indexPath = path.join(FULL_DECKS, name, 'index.html');
|
|
const stylePath = path.join(FULL_DECKS, name, 'style.css');
|
|
let html = await readMaybe(indexPath);
|
|
if (!html) {
|
|
console.warn(`[bake] missing ${indexPath}`);
|
|
return false;
|
|
}
|
|
const style = await readMaybe(stylePath);
|
|
|
|
html = inlineLink(html, '../../../assets/fonts.css', sharedFonts);
|
|
html = inlineLink(html, '../../../assets/base.css', sharedBase);
|
|
html = inlineLink(
|
|
html,
|
|
'../../../assets/animations/animations.css',
|
|
sharedAnimations,
|
|
);
|
|
html = inlineLink(html, 'style.css', style);
|
|
|
|
// Some templates ship a `<link id="theme-link" href="../../../assets/themes/<theme>.css">`
|
|
// so the runtime can cycle themes via `T`. The static gallery has no runtime
|
|
// and srcdoc can't follow `../../../`, so inline whatever theme the template
|
|
// shipped with — that's the look the upstream README screenshots show.
|
|
html = html.replace(
|
|
/<link[^>]*href=["']\.\.\/\.\.\/\.\.\/assets\/themes\/([\w-]+)\.css["'][^>]*>/g,
|
|
(_match, themeName) => {
|
|
try {
|
|
const css = readFileSync(
|
|
path.join(ASSETS, 'themes', `${themeName}.css`),
|
|
'utf8',
|
|
);
|
|
return `<style data-theme="${themeName}">${css}\n</style>`;
|
|
} catch {
|
|
return '';
|
|
}
|
|
},
|
|
);
|
|
|
|
// Drop the runtime + any FX runtime references — the static gallery only
|
|
// shows slide 1 and these scripts would 404 inside the srcdoc sandbox.
|
|
html = html.replace(
|
|
/<script[^>]*src=["'][^"']*runtime\.js["'][^>]*><\/script>/g,
|
|
'',
|
|
);
|
|
html = html.replace(
|
|
/<script[^>]*src=["'][^"']*fx-runtime\.js["'][^>]*><\/script>/g,
|
|
'',
|
|
);
|
|
|
|
// Append the static fallback at the very end of <head> so it overrides
|
|
// base.css's `.slide{opacity:0}`. We append rather than prepend to win
|
|
// specificity ties without bumping selectors.
|
|
html = html.replace(/<\/head>/i, `<style>${STATIC_FALLBACK_CSS}</style></head>`);
|
|
|
|
const outDir = path.join(SKILLS, `html-ppt-${name}`);
|
|
await mkdir(outDir, { recursive: true });
|
|
await writeFile(path.join(outDir, 'example.html'), html, 'utf8');
|
|
return true;
|
|
}
|
|
|
|
const entries = await readdir(FULL_DECKS, { withFileTypes: true });
|
|
const names = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
|
|
let baked = 0;
|
|
for (const name of names) {
|
|
if (await bakeOne(name)) baked++;
|
|
}
|
|
console.log(`[bake] wrote ${baked}/${names.length} example.html files`);
|
|
console.log(`[bake] templates: ${names.join(', ')}`);
|