slides-grab 1.2.5 → 1.3.0
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/README-ko.md +256 -0
- package/README.md +27 -9
- package/bin/ppt-agent.js +125 -4
- package/package.json +11 -7
- package/scripts/editor-server.js +170 -14
- package/scripts/generate-image.js +44 -0
- package/skills/slides-grab/SKILL.md +3 -3
- package/skills/slides-grab/references/presentation-workflow-reference.md +3 -3
- package/skills/slides-grab-design/SKILL.md +8 -4
- package/skills/slides-grab-design/references/design-rules.md +3 -3
- package/skills/slides-grab-design/references/detailed-design-rules.md +2 -2
- package/skills/slides-grab-plan/SKILL.md +17 -4
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles.js +67 -2
- package/src/editor/codex-edit.js +43 -13
- package/src/editor/edit-subprocess.js +78 -5
- package/src/editor/editor-codex-prompt.md +2 -2
- package/src/editor/editor.html +3 -0
- package/src/editor/js/editor-state.js +2 -2
- package/src/editor/js/model-registry.js +38 -0
- package/src/god-tibo-imagen.js +135 -0
- package/src/nano-banana.js +332 -27
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# DESIGN.md → DESIGN.slides.md Conversion
|
|
2
|
+
|
|
3
|
+
Web-flavored `DESIGN.md` files (Google Stitch / voltagent/awesome-design-md
|
|
4
|
+
convention) describe **marketing websites**: top-nav, hero-band, CTA buttons,
|
|
5
|
+
pricing cards, footer-band, multi-section scrolling pages.
|
|
6
|
+
|
|
7
|
+
A slide is a **720pt × 405pt single frame, no scroll, no clicks, no nav**.
|
|
8
|
+
Pasting a web design system into slides produces deck pages that look like
|
|
9
|
+
landing pages — wrong slot.
|
|
10
|
+
|
|
11
|
+
This document is the canonical reference the agent uses to translate a
|
|
12
|
+
DESIGN.md into a sibling `DESIGN.slides.md` that fits the slide medium.
|
|
13
|
+
|
|
14
|
+
## Output contract
|
|
15
|
+
|
|
16
|
+
The agent MUST produce a file named `DESIGN.slides.md` next to the source
|
|
17
|
+
`DESIGN.md` at the deck root. The original `DESIGN.md` MUST be left untouched.
|
|
18
|
+
|
|
19
|
+
`DESIGN.slides.md` MUST contain the following sections, in this order:
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
---
|
|
23
|
+
version: alpha
|
|
24
|
+
name: <inherited from DESIGN.md `name`>
|
|
25
|
+
description: <one-sentence slide-flavored mood — not a marketing paragraph>
|
|
26
|
+
derived-from: <relative path to source DESIGN.md, e.g. ./DESIGN.md>
|
|
27
|
+
medium: slides-16x9
|
|
28
|
+
colors:
|
|
29
|
+
<token-name>: <#hex>
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
One short paragraph describing the slide deck's atmosphere — material, energy,
|
|
34
|
+
typographic voice. No mention of pages, scroll, nav, CTA, pricing, or routes.
|
|
35
|
+
|
|
36
|
+
## Background
|
|
37
|
+
Solid fills only. List 1-3 canvas modes that alternate across slides.
|
|
38
|
+
|
|
39
|
+
## Colors
|
|
40
|
+
Markdown table of the slide-active palette. Drop any role that exists only
|
|
41
|
+
for hover/focus/disabled states (those don't apply to static slides).
|
|
42
|
+
|
|
43
|
+
## Typography
|
|
44
|
+
Two faces: a display face for headlines and a body face for everything else.
|
|
45
|
+
Optionally a mono face for eyebrows / IDs / code motifs. Include working
|
|
46
|
+
font-stack fallbacks (Google Fonts that render in Playwright capture).
|
|
47
|
+
|
|
48
|
+
## Slide Layouts
|
|
49
|
+
Enumerate the slide archetypes this design system supports. ALWAYS include
|
|
50
|
+
at least these five:
|
|
51
|
+
- **Cover** — single dominant headline, no nav, no CTA buttons
|
|
52
|
+
- **Section divider** — short anchor headline, optional eyebrow
|
|
53
|
+
- **Content** — copy + single supporting visual (60/40 or 50/50 split)
|
|
54
|
+
- **Statistic** — one oversized number, one short caption
|
|
55
|
+
- **Closing** — final thesis line, footer/page-number strip
|
|
56
|
+
|
|
57
|
+
For each layout, describe the dominant rhythm in 1-3 bullets — what scale
|
|
58
|
+
carries the slide, what's allowed in the corners, what's NOT allowed.
|
|
59
|
+
|
|
60
|
+
## Signature Motifs
|
|
61
|
+
Two-to-five small visual elements that make the deck recognizably this brand.
|
|
62
|
+
Examples: a colored underline, a strike-through wordmark, a dot cluster,
|
|
63
|
+
a serif italic accent on one word per slide. Each motif MUST be reproducible
|
|
64
|
+
purely with inline HTML + CSS on a single slide.
|
|
65
|
+
|
|
66
|
+
## Avoid
|
|
67
|
+
Explicit list of web-only patterns the slides MUST NOT carry. ALWAYS include:
|
|
68
|
+
- top-nav bars, sticky headers, menu rows
|
|
69
|
+
- clickable CTA buttons ("Sign up", "Start free trial", "Read docs")
|
|
70
|
+
- multi-column footer-bands beyond a single thin strip with attribution + page number
|
|
71
|
+
- pricing tier grids
|
|
72
|
+
- hover/focus/active state styling
|
|
73
|
+
- atmospheric multi-stop gradients (unless the source spec mandates them)
|
|
74
|
+
Plus any AVOID rules carried over from the source DESIGN.md.
|
|
75
|
+
|
|
76
|
+
## Source mapping (for traceability)
|
|
77
|
+
A short bullet list showing how each web concept was mapped. Format:
|
|
78
|
+
- `<web concept from DESIGN.md>` → `<slide concept in DESIGN.slides.md>` OR `dropped`
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Web → slide mapping rules
|
|
82
|
+
|
|
83
|
+
Apply these mappings while converting. The left column is what DESIGN.md
|
|
84
|
+
contains; the right column is the slide-appropriate replacement.
|
|
85
|
+
|
|
86
|
+
| DESIGN.md (web) | DESIGN.slides.md (slide) |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `top-nav` / sticky header | A 12pt-tall mono **eyebrow strip** at the top of each slide with section number + brand wordmark — never a horizontal menu of items |
|
|
89
|
+
| `hero-band` (h1 + sub + dual CTA + illustration card) | **Cover layout** — single oversized headline, single sub, footer strip with page number. CTA buttons → discarded (no clicks possible). Illustration card → optional single visual anchor |
|
|
90
|
+
| `feature-card grid (3-up / 4-up)` | **Content layout** with 3-up grid OK, but cards must be cardless-leaning (whitespace-driven) — drop the marketing icon + blurb pattern, prefer pull quotes or per-track/per-member rows |
|
|
91
|
+
| `pricing tier grid` | **Discard entirely** — pricing has no slide analogue |
|
|
92
|
+
| `connector tile grid` | **Discard or convert to small badge row** — never a 4×4 grid of generic logos |
|
|
93
|
+
| `CTA banner` (full-bleed coral / brand) | **Section divider** or **closing** layout — keep the full-bleed color, replace CTA buttons with a thesis sentence and a page number |
|
|
94
|
+
| `product-mockup-card-dark` (code editor screenshot card) | **Content motif** — fine to keep as a single dark surface inside a content slide, but it can NOT dominate every slide |
|
|
95
|
+
| `footer-band` (4-column legal/sitemap) | **Single thin footer strip** — at most 1 line: brand wordmark left, page number right |
|
|
96
|
+
| `button-primary` / `button-secondary` | **Kicker text only** — write the verb as plain text ("Read the next issue") without box/border/click affordance |
|
|
97
|
+
| Hover / focus / pressed / disabled states | **Drop all of them** — slides are static |
|
|
98
|
+
| Multi-stop atmospheric gradients | **Drop unless source spec calls for one or two** — convert to a single accent shape |
|
|
99
|
+
| Spike-mark / asterisk-style brand glyph | **Keep as a small motif** — inline SVG at the slide eyebrow or footer |
|
|
100
|
+
|
|
101
|
+
## Preservation rules (do NOT translate away)
|
|
102
|
+
|
|
103
|
+
The conversion MUST carry over from DESIGN.md unchanged:
|
|
104
|
+
|
|
105
|
+
- The **color palette** (canvas, surface, ink, primary accent, secondary accents)
|
|
106
|
+
- The **type pairing** — display face vs body face vs mono face
|
|
107
|
+
- The **mood / atmosphere** — warm-editorial vs dark-product vs pastel-pop is the
|
|
108
|
+
whole reason the user imported this DESIGN.md; the slide deck must still feel
|
|
109
|
+
like the source brand
|
|
110
|
+
- **Brand-specific signature motifs** (spike-mark, hard offset shadow, slab-serif italic
|
|
111
|
+
accent, etc.) — translate the surface, not the identity
|
|
112
|
+
|
|
113
|
+
## Process checklist for the agent
|
|
114
|
+
|
|
115
|
+
1. Read `./DESIGN.md` in full.
|
|
116
|
+
2. Identify which sections are web-only and slot them through the mapping table.
|
|
117
|
+
3. Identify the design tokens (colors, type, spacing) that survive unchanged.
|
|
118
|
+
4. Draft `./DESIGN.slides.md` using the Output Contract template.
|
|
119
|
+
5. Show the user a short summary: 5–10 lines covering kept tokens + dropped
|
|
120
|
+
web sections + new slide layouts inferred.
|
|
121
|
+
6. Wait for explicit user approval ("looks good" or specific edits) before
|
|
122
|
+
moving to outline / slide generation.
|
|
123
|
+
7. Once approved, record `style: ./DESIGN.slides.md` in `slide-outline.md`.
|
|
124
|
+
|
|
125
|
+
## When DESIGN.slides.md should be re-converted
|
|
126
|
+
|
|
127
|
+
Suggest a re-conversion when:
|
|
128
|
+
- The source `DESIGN.md` is replaced / re-imported with a different brand
|
|
129
|
+
- The user explicitly asks for a different deck flavor (e.g. "more editorial",
|
|
130
|
+
"less code-heavy")
|
|
131
|
+
- A slide deck visibly carries web-only artifacts (nav bars, CTAs, footers
|
|
132
|
+
with link columns) that the current DESIGN.slides.md doesn't forbid
|
|
133
|
+
|
|
134
|
+
A `DESIGN.slides.md` is never regenerated automatically. It is the agent's
|
|
135
|
+
job, in conversation with the user.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
5
|
+
const ALLOWED_PROTOCOLS = new Set(['https:']);
|
|
6
|
+
|
|
7
|
+
function getHeader(headers, name) {
|
|
8
|
+
if (!headers) return '';
|
|
9
|
+
if (typeof headers.get === 'function') return headers.get(name) ?? '';
|
|
10
|
+
return headers[name] ?? headers[name.toLowerCase()] ?? '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validateFinalDesignUrl(rawUrl, { allowedProtocols = ALLOWED_PROTOCOLS } = {}) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(rawUrl);
|
|
17
|
+
} catch (cause) {
|
|
18
|
+
throw new DesignImportError(`Invalid final URL after redirects: ${rawUrl}`, { cause });
|
|
19
|
+
}
|
|
20
|
+
if (!allowedProtocols.has(url.protocol)) {
|
|
21
|
+
throw new DesignImportError(
|
|
22
|
+
`Redirect final URL protocol ${url.protocol} is not allowed. Allowed: ${[...allowedProtocols].join(', ')}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return url;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readResponseBody(response, { maxBytes }) {
|
|
29
|
+
if (response.body && typeof response.body.getReader === 'function') {
|
|
30
|
+
const reader = response.body.getReader();
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let total = 0;
|
|
33
|
+
try {
|
|
34
|
+
while (true) {
|
|
35
|
+
const { done, value } = await reader.read();
|
|
36
|
+
if (done) break;
|
|
37
|
+
const chunk = Buffer.from(value);
|
|
38
|
+
total += chunk.byteLength;
|
|
39
|
+
if (total > maxBytes) {
|
|
40
|
+
if (typeof reader.cancel === 'function') {
|
|
41
|
+
await reader.cancel().catch(() => {});
|
|
42
|
+
}
|
|
43
|
+
throw new DesignImportError(
|
|
44
|
+
`DESIGN.md exceeds max size (${total} > ${maxBytes} bytes).`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
if (typeof reader.releaseLock === 'function') reader.releaseLock();
|
|
51
|
+
}
|
|
52
|
+
return Buffer.concat(chunks, total);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
56
|
+
if (buffer.byteLength > maxBytes) {
|
|
57
|
+
throw new DesignImportError(
|
|
58
|
+
`DESIGN.md exceeds max size (${buffer.byteLength} > ${maxBytes} bytes).`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return buffer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class DesignImportError extends Error {
|
|
65
|
+
constructor(message, { cause } = {}) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = 'DesignImportError';
|
|
68
|
+
if (cause) this.cause = cause;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateDesignUrl(rawUrl, { allowedProtocols = ALLOWED_PROTOCOLS } = {}) {
|
|
73
|
+
if (typeof rawUrl !== 'string' || rawUrl.trim() === '') {
|
|
74
|
+
throw new DesignImportError('Design URL is required.');
|
|
75
|
+
}
|
|
76
|
+
let url;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(rawUrl);
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
throw new DesignImportError(`Invalid URL: ${rawUrl}`, { cause });
|
|
81
|
+
}
|
|
82
|
+
if (!allowedProtocols.has(url.protocol)) {
|
|
83
|
+
throw new DesignImportError(
|
|
84
|
+
`URL protocol ${url.protocol} is not allowed. Allowed: ${[...allowedProtocols].join(', ')}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function fetchDesignMarkdown(rawUrl, options = {}) {
|
|
91
|
+
const {
|
|
92
|
+
fetchImpl = globalThis.fetch,
|
|
93
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
94
|
+
timeoutMs = 15000,
|
|
95
|
+
allowedProtocols,
|
|
96
|
+
} = options;
|
|
97
|
+
|
|
98
|
+
if (typeof fetchImpl !== 'function') {
|
|
99
|
+
throw new DesignImportError('No fetch implementation available; Node.js >= 18 required.');
|
|
100
|
+
}
|
|
101
|
+
const url = validateDesignUrl(rawUrl, { allowedProtocols });
|
|
102
|
+
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
105
|
+
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await fetchImpl(url.toString(), {
|
|
109
|
+
redirect: 'follow',
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
headers: { 'User-Agent': 'slides-grab/import-design (+https://github.com/NomaDamas/slides-grab)' },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const finalUrl = validateFinalDesignUrl(response.url ?? url.toString(), { allowedProtocols });
|
|
115
|
+
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new DesignImportError(`Fetch returned HTTP ${response.status} for ${url}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const contentType = getHeader(response.headers, 'content-type');
|
|
121
|
+
const looksLikeText = contentType === '' ||
|
|
122
|
+
contentType.includes('text/') ||
|
|
123
|
+
contentType.includes('markdown') ||
|
|
124
|
+
contentType.includes('application/octet-stream');
|
|
125
|
+
if (!looksLikeText) {
|
|
126
|
+
throw new DesignImportError(
|
|
127
|
+
`Refusing to import non-text response (content-type: ${contentType}).`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const buffer = await readResponseBody(response, { maxBytes });
|
|
132
|
+
const text = buffer.toString('utf8');
|
|
133
|
+
return {
|
|
134
|
+
url: finalUrl.toString(),
|
|
135
|
+
contentType,
|
|
136
|
+
bytes: buffer.byteLength,
|
|
137
|
+
fetchedAt: new Date().toISOString(),
|
|
138
|
+
text,
|
|
139
|
+
};
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
if (cause instanceof DesignImportError) throw cause;
|
|
142
|
+
throw new DesignImportError(`Fetch failed for ${url}: ${cause.message}`, { cause });
|
|
143
|
+
} finally {
|
|
144
|
+
clearTimeout(timeoutHandle);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function formatImportedDesignMarkdown({ url, content, fetchedAt }) {
|
|
149
|
+
const banner = [
|
|
150
|
+
'<!--',
|
|
151
|
+
` Imported by slides-grab import-design`,
|
|
152
|
+
` source: ${url}`,
|
|
153
|
+
` fetched-at: ${fetchedAt}`,
|
|
154
|
+
'-->',
|
|
155
|
+
'',
|
|
156
|
+
].join('\n');
|
|
157
|
+
return `${banner}${content}\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function saveImportedDesign({ outputPath, markdown }) {
|
|
161
|
+
const absolutePath = resolve(outputPath);
|
|
162
|
+
writeFileSync(absolutePath, markdown, 'utf8');
|
|
163
|
+
return absolutePath;
|
|
164
|
+
}
|