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.
@@ -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
+ }