/** * design-system.js * ---------------------------------------------------------------------------- * Design tokens + slide helpers for the LangChain Evolution deck * (lc-evo-deck, 100+ slides, code-heavy, dark theme, 16:9). * * Audience: engineers familiar with LLMs. No introductory filler. * Visual register: developer-tool aesthetic, deep navy, JetBrains Mono for * code, Inter for UI, Arial as the universal fallback (covers Cyrillic). * * USAGE * ----- * // 1. Import the module * const ds = require('./design-system'); * const { theme, helpers, layouts, fonts, sizes, spacing } = ds; * * // 2. Create a new presentation with the built-in 16:9 layout * const pptxgen = require('pptxgenjs'); * const pres = new pptxgen(); * pres.layout = 'LAYOUT_16x9'; // 10 x 5.625 inches * * // 3. Add a slide * const slide = pres.addSlide(); * helpers.slideBase(slide, pres, theme); * helpers.addHeader(slide, pres, theme, { * section: 'Stage 2: Chains', * sectionNumber: 2, * title: 'LCEL: a composable expression language', * eyebrow: 'STAGE 2', * }); * * // 4. Add a code block * helpers.addCodeBlock(slide, pres, theme, { * x: 0.5, y: layouts.CONTENT_TOP, * w: 5.0, h: 3.2, * language: 'python', * code: [ * 'from langchain_core.prompts import ChatPromptTemplate', * 'from langchain_openai import ChatOpenAI', * '', * 'prompt = ChatPromptTemplate.from_messages([', * ' ("system", "You are a helpful assistant."),', * ' ("human", "{question}"),', * '])', * 'model = ChatOpenAI(model="gpt-4o-mini")', * 'chain = prompt | model', * 'print(chain.invoke({"question": "What is LCEL?"}))', * ].join('\n'), * filePath: 'examples/lcel_basic.py', * startLine: 1, * }); * * // 5. Add a callout and a pros/cons panel * helpers.addCallout(slide, pres, theme, { * x: 5.8, y: layouts.CONTENT_TOP, w: 3.7, h: 1.2, * kind: 'info', * text: 'LCEL is the default composition language from v0.1 onward.', * }); * * helpers.addProsCons(slide, pres, theme, { * x: 5.8, y: 2.8, w: 3.7, h: 2.0, * pros: ['Composable via | operator', 'Streaming, async, batched for free'], * cons: ['Verbose for simple chains', 'Mental model differs from LangChain v0'], * }); * * // 6. Number the slide and add a source line * helpers.addPageNumber(slide, pres, theme, 7); * helpers.addSourceLine(slide, pres, theme, { * x: 0.5, y: layouts.FOOTER_Y + 0.1, w: 6.0, * source: 'python.langchain.com/docs/concepts/lcel', * }); * * // 7. Save * await pres.writeFile({ fileName: 'lc-evolution.pptx' }); * * LAYOUTS (PPTX 16:9, units = inches) * ----------------------------------- * HEADER_Y = 0.4 * CONTENT_TOP = 1.4 * CONTENT_BOTTOM = 5.05 * FOOTER_Y = 5.25 * * Vertical regions: * - Header band : [0.0 .. 1.4] eyebrow + h1 title * - Content body : [1.4 .. 5.05] main slide content * - Footer band : [5.05 .. 5.625] page number + source line * * Horizontal margins: 0.5 inches left/right by default. * * RULES * ----- * - No em-dash (--), en-dash (-), no smart quotes, no ellipsis (...) * - All source files are pure ASCII except inside string literals where * Cyrillic is allowed (Arial fallback guarantees rendering). * - Code blocks use JetBrains Mono; body uses Inter; fallback Arial. * * @module design-system */ 'use strict'; // --------------------------------------------------------------------------- // 1. PALETTE // --------------------------------------------------------------------------- const palette = { bg: { primary: '#0A1A2A', // main slide background elevated: '#142B3F', // cards, callouts, panels code: '#0F1E2E', // code block background (slightly darker) overlay: '#1B2F44', // hover / focus surfaces }, text: { primary: '#E6F0F7', secondary: '#B5C4D1', muted: '#8A9AAB', inverse: '#0A1A2A', // for text on bright accents }, accent: { primary: '#219EBC', // teal, default accent secondary: '#FFB703', // gold, important emphasis tertiary: '#8ECAE6', // light blue, soft accent }, border: { subtle: '#233A4F', strong: '#3A5670', accent: '#219EBC', }, code: { keyword: '#C586C0', // def, class, import, return string: '#CE9178', number: '#B5CEA8', comment: '#6A9955', function: '#DCDCAA', builtin: '#4EC9B0', text: '#D4D4D4', // default code body bg: '#0F1E2E', lineHighlight: '#1F2F44', }, state: { info: '#219EBC', success: '#4EC9B0', warning: '#FFB703', danger: '#F48771', infoBg: '#102A38', successBg: '#0F2A28', warningBg: '#3A2A0F', dangerBg: '#3A1A14', }, }; // --------------------------------------------------------------------------- // 2. TYPOGRAPHY // --------------------------------------------------------------------------- const fonts = { code: 'JetBrains Mono', ui: 'Inter', fallback: 'Arial', // universal fallback, supports Cyrillic }; const sizes = { h1: 36, h2: 28, h3: 20, body: 14, code: 10, caption: 9, eyebrow: 10, }; const spacing = { page: 0.5, card_pad: 0.25, gap: 0.15, }; // --------------------------------------------------------------------------- // 3. LAYOUTS // --------------------------------------------------------------------------- const layouts = { LAYOUT_16x9: 'LAYOUT_16x9', HEADER_Y: 0.4, CONTENT_TOP: 1.45, CONTENT_BOTTOM: 5.05, FOOTER_Y: 5.25, }; // --------------------------------------------------------------------------- // 4. THEME (bundled export of the above) // --------------------------------------------------------------------------- const theme = { palette, fonts, sizes, spacing, layouts, }; // --------------------------------------------------------------------------- // 5. HELPERS // --------------------------------------------------------------------------- /** * Resolve a font that always carries the Arial fallback. * pptxgenjs lets us pass an array; PowerPoint will pick the first available. */ function withFallback(family) { return [family, fonts.fallback]; } /** * Paint the slide background. * @param {object} slide - pptxgenjs slide instance * @param {object} _pres - pptxgenjs pres (kept for signature symmetry) * @param {object} t - theme bundle */ function slideBase(slide, _pres, t) { slide.background = { color: t.palette.bg.primary }; } /** * Add a header band: eyebrow (small caps, accent) + section number + title. * @param {object} slide * @param {object} pres * @param {object} t - theme * @param {object} opts * @param {string} [opts.section] - small caps section label (e.g. "Stage 2") * @param {number} [opts.sectionNumber] - large section number shown on the right * @param {string} opts.title - main title (h1) * @param {string} [opts.eyebrow] - eyebrow text (e.g. "STAGE 2: CHAINS") */ function addHeader(slide, pres, t, opts) { const o = opts || {}; const margin = t.spacing.page; const titleY = t.layouts.HEADER_Y + 0.4; const titleSize = o.titleSize || t.sizes.h2; // default to 28pt -- fits two-line titles without overflow const titleW = o.sectionNumber != null ? 8.4 : 9.0; if (o.eyebrow) { slide.addText(o.eyebrow, { x: margin, y: t.layouts.HEADER_Y, w: 6.0, h: 0.3, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.eyebrow, color: t.palette.accent.primary, bold: true, charSpacing: 4, }); } else if (o.section) { slide.addText(o.section, { x: margin, y: t.layouts.HEADER_Y, w: 6.0, h: 0.3, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.eyebrow, color: t.palette.accent.primary, bold: true, charSpacing: 4, }); } slide.addText(o.title || '', { x: margin, y: titleY, w: titleW, h: 0.75, fontFace: withFallback(t.fonts.ui), fontSize: titleSize, color: t.palette.text.primary, bold: true, valign: 'middle', fit: 'shrink', }); if (o.sectionNumber != null) { slide.addText(String(o.sectionNumber), { x: 9.0, y: t.layouts.HEADER_Y - 0.05, w: 0.7, h: 1.0, fontFace: withFallback(t.fonts.ui), fontSize: 56, color: t.palette.accent.secondary, bold: true, align: 'right', valign: 'top', }); } // Hairline separator below the header band slide.addShape(pres.ShapeType.line, { x: margin, y: 1.3, w: 10.0 - margin * 2, h: 0, line: { color: t.palette.border.subtle, width: 0.75 }, }); } /** * Render a code block as a card with monospaced text. * @param {object} slide * @param {object} pres * @param {object} t - theme * @param {object} opts * @param {string} opts.code * @param {number} opts.x * @param {number} opts.y * @param {number} opts.w * @param {number} opts.h * @param {string} [opts.language='python'] * @param {string} [opts.filePath] - optional, renders as "// path:line" header * @param {number} [opts.startLine=1] * @param {Array} [opts.highlightLines] - 1-based line numbers */ function addCodeBlock(slide, pres, t, opts) { const o = opts || {}; const code = String(o.code || ''); const x = o.x; const y = o.y; const w = o.w; const h = o.h; const headerH = o.filePath ? 0.3 : 0; const radius = 0.08; // Card background slide.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: w, h: h, fill: { color: t.palette.code.bg }, line: { color: t.palette.border.subtle, width: 0.75 }, rectRadius: radius, }); // Optional header strip with file path if (o.filePath) { const startLine = o.startLine || 1; const lineCount = code.split('\n').length; const endLine = startLine + lineCount - 1; slide.addText( '// ' + o.filePath + ':' + startLine + '-' + endLine, { x: x + t.spacing.card_pad, y: y + 0.05, w: w - t.spacing.card_pad * 2, h: 0.22, fontFace: withFallback(t.fonts.code), fontSize: t.sizes.caption, color: t.palette.text.muted, italic: true, } ); } // Body code const lines = code.split('\n'); const hasHighlight = Array.isArray(o.highlightLines) && o.highlightLines.length > 0; const highlightedSet = hasHighlight ? new Set(o.highlightLines.map(function (n) { return n; })) : null; // Pre-compute line height to fit within available area. const codeAreaY = y + headerH + 0.05; const codeAreaH = h - headerH - 0.1; // Use a tighter font + leading so 20-line snippets fit in 3-inch cards. const fontSize = o.fontSize || 11; const lineH = (fontSize / 72) * 1.25; // inches per line at 1.25 leading const text = lines.map(function (line, idx) { const lineNum = idx + 1; const isHi = highlightedSet && highlightedSet.has(lineNum); const prefix = isHi ? '> ' : ' '; return { text: prefix + (line.length === 0 ? ' ' : line) + '\n', options: { fontFace: withFallback(t.fonts.code), fontSize: fontSize, color: isHi ? t.palette.text.primary : t.palette.code.text, bold: false, highlight: isHi ? t.palette.code.lineHighlight : undefined, }, }; }); slide.addText(text, { x: x + t.spacing.card_pad, y: codeAreaY, w: w - t.spacing.card_pad * 2, h: codeAreaH, valign: 'top', paraSpaceBefore: 0, paraSpaceAfter: 0, lineSpacingMultiple: 1.0, isTextBox: true, }); // Caption warning if content may overflow. const maxLines = Math.floor(codeAreaH / lineH); if (lines.length > maxLines) { // Best-effort: append a small note. PowerPoint won't clip the text, but // it will overflow the card visually. Caller should reduce fontSize or // split the snippet. slide.addText( '// note: snippet has ' + lines.length + ' lines, card fits ~' + maxLines, { x: x + t.spacing.card_pad, y: y + h - 0.22, w: w - t.spacing.card_pad * 2, h: 0.18, fontFace: withFallback(t.fonts.code), fontSize: t.sizes.caption, color: t.palette.state.warning, italic: true, } ); } } /** * Render a code block with line highlighting. * Convenience wrapper over addCodeBlock. */ function addCodeBlockWithHighlight(slide, pres, t, opts) { return addCodeBlock(slide, pres, t, Object.assign({}, opts, { highlightLines: opts.lines || opts.highlightLines, })); } /** * Add a callout panel (info / warning / success / danger). * @param {object} slide * @param {object} pres * @param {object} t - theme * @param {object} opts * @param {number} opts.x * @param {number} opts.y * @param {number} opts.w * @param {number} opts.h * @param {('info'|'warning'|'success'|'danger')} [opts.kind='info'] * @param {string} opts.text * @param {string} [opts.title] */ function addCallout(slide, pres, t, opts) { const o = opts || {}; const kind = o.kind || 'info'; const accent = t.palette.state[kind] || t.palette.state.info; const bgKey = kind + 'Bg'; const bg = t.palette.state[bgKey] || t.palette.state.infoBg; // Background card slide.addShape(pres.ShapeType.roundRect, { x: o.x, y: o.y, w: o.w, h: o.h, fill: { color: bg }, line: { color: accent, width: 1 }, rectRadius: 0.08, }); // Left accent bar slide.addShape(pres.ShapeType.rect, { x: o.x, y: o.y, w: 0.08, h: o.h, fill: { color: accent }, line: { type: 'none' }, }); // Label slide.addText((kind || 'info').toUpperCase(), { x: o.x + 0.25, y: o.y + 0.1, w: o.w - 0.35, h: 0.25, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.eyebrow, color: accent, bold: true, charSpacing: 4, }); // Optional title let bodyY = o.y + 0.4; let bodyH = o.h - 0.5; if (o.title) { slide.addText(o.title, { x: o.x + 0.25, y: o.y + 0.35, w: o.w - 0.35, h: 0.3, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.h3, color: t.palette.text.primary, bold: true, }); bodyY = o.y + 0.7; bodyH = o.h - 0.8; } slide.addText(o.text || '', { x: o.x + 0.25, y: bodyY, w: o.w - 0.35, h: bodyH, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.body, color: t.palette.text.secondary, valign: 'top', }); } /** * Two-column pros / cons panel. * @param {object} slide * @param {object} pres * @param {object} t - theme * @param {object} opts * @param {number} opts.x * @param {number} opts.y * @param {number} opts.w * @param {number} opts.h * @param {string[]} opts.pros * @param {string[]} opts.cons */ function addProsCons(slide, pres, t, opts) { const o = opts || {}; const gap = 0.2; const colW = (o.w - gap) / 2; // Pros card slide.addShape(pres.ShapeType.roundRect, { x: o.x, y: o.y, w: colW, h: o.h, fill: { color: t.palette.state.successBg }, line: { color: t.palette.state.success, width: 1 }, rectRadius: 0.08, }); slide.addText('PROS', { x: o.x + 0.2, y: o.y + 0.1, w: colW - 0.3, h: 0.3, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.eyebrow, color: t.palette.state.success, bold: true, charSpacing: 4, }); const prosBody = (o.pros || []).map(function (item) { return { text: '+ ' + item + '\n', options: { fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.body, color: t.palette.text.primary, }, }; }); slide.addText(prosBody, { x: o.x + 0.2, y: o.y + 0.45, w: colW - 0.3, h: o.h - 0.55, valign: 'top', paraSpaceAfter: 4, }); // Cons card const cx = o.x + colW + gap; slide.addShape(pres.ShapeType.roundRect, { x: cx, y: o.y, w: colW, h: o.h, fill: { color: t.palette.state.dangerBg }, line: { color: t.palette.state.danger, width: 1 }, rectRadius: 0.08, }); slide.addText('CONS', { x: cx + 0.2, y: o.y + 0.1, w: colW - 0.3, h: 0.3, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.eyebrow, color: t.palette.state.danger, bold: true, charSpacing: 4, }); const consBody = (o.cons || []).map(function (item) { return { text: '- ' + item + '\n', options: { fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.body, color: t.palette.text.primary, }, }; }); slide.addText(consBody, { x: cx + 0.2, y: o.y + 0.45, w: colW - 0.3, h: o.h - 0.55, valign: 'top', paraSpaceAfter: 4, }); } /** * Add a page number in the bottom-right corner. */ function addPageNumber(slide, _pres, t, num) { slide.addText(String(num), { x: 9.2, y: t.layouts.FOOTER_Y, w: 0.6, h: 0.25, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.caption, color: t.palette.text.muted, align: 'right', valign: 'middle', }); } /** * Add a section divider slide: a large numeric label + title + intro paragraph. * @param {object} slide * @param {object} pres * @param {object} t - theme * @param {object} opts * @param {number} opts.number * @param {string} opts.title * @param {string} [opts.intro] * @param {string} [opts.eyebrow] */ function addSectionDivider(slide, pres, t, opts) { const o = opts || {}; slideBase(slide, pres, t); // Giant number on the left slide.addText(String(o.number), { x: 0.5, y: 1.0, w: 3.5, h: 3.5, fontFace: withFallback(t.fonts.ui), fontSize: 220, color: t.palette.accent.primary, bold: true, valign: 'middle', }); // Vertical divider slide.addShape(pres.ShapeType.line, { x: 4.2, y: 1.4, w: 0, h: 2.8, line: { color: t.palette.border.subtle, width: 1 }, }); // Title if (o.eyebrow) { slide.addText(o.eyebrow, { x: 4.5, y: 1.4, w: 5.0, h: 0.3, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.eyebrow, color: t.palette.accent.secondary, bold: true, charSpacing: 4, }); } slide.addText(o.title || '', { x: 4.5, y: o.eyebrow ? 1.7 : 1.4, w: 5.0, h: 1.0, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.h1, color: t.palette.text.primary, bold: true, valign: 'top', }); if (o.intro) { slide.addText(o.intro, { x: 4.5, y: 2.8, w: 5.0, h: 1.6, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.body, color: t.palette.text.secondary, valign: 'top', }); } } /** * Add an italic source line, e.g. "source: python.langchain.com/..." * @param {object} slide * @param {object} _pres * @param {object} t * @param {object} opts * @param {string} opts.source * @param {number} [opts.x] * @param {number} [opts.y] * @param {number} [opts.w] */ function addSourceLine(slide, _pres, t, opts) { const o = opts || {}; slide.addText('source: ' + (o.source || ''), { x: o.x != null ? o.x : t.spacing.page, y: o.y != null ? o.y : t.layouts.FOOTER_Y + 0.05, w: o.w != null ? o.w : 7.0, h: 0.25, fontFace: withFallback(t.fonts.ui), fontSize: t.sizes.caption, color: t.palette.text.muted, italic: true, valign: 'middle', }); } // --------------------------------------------------------------------------- // 6. OPTIONAL: PYTHON SYNTAX HIGHLIGHTING (pygments via subprocess) // --------------------------------------------------------------------------- /** * Best-effort Python syntax highlighter. * Calls the `pygmentize` CLI from pygments if available. * Returns an array of { text, color } tokens. * On any failure (missing binary, non-zero exit, parse error) returns a * single-token array with the whole snippet in the default code text color * so that the slide still renders something. * * @param {string} code - Python source code * @returns {Array<{text: string, color: string}>} */ function highlightPython(code) { const defaultColor = palette.code.text; const text = String(code || ''); if (!text) return [{ text: '', color: defaultColor }]; const { spawnSync } = require('child_process'); let result; try { result = spawnSync( 'pygmentize', ['-f', 'json', '-l', 'python'], { input: text, encoding: 'utf8', timeout: 4000 } ); } catch (e) { return [{ text: text, color: defaultColor }]; } if (result.error || result.status !== 0) { return [{ text: text, color: defaultColor }]; } let parsed; try { parsed = JSON.parse(result.stdout); } catch (e) { return [{ text: text, color: defaultColor }]; } // Pygments JSON output: { tokens: [ [type, value], ... ] } const tokens = Array.isArray(parsed.tokens) ? parsed.tokens : []; const out = []; for (const tok of tokens) { if (!Array.isArray(tok) || tok.length < 2) continue; const [type, value] = tok; out.push({ text: String(value), color: mapPygmentsTokenToColor(type) || defaultColor, }); } return out.length > 0 ? out : [{ text: text, color: defaultColor }]; } /** * Map a pygments token type to a palette color. * Falls back to the default code text color. */ function mapPygmentsTokenToColor(type) { if (!type) return null; // Pygments short token types we care about for Python: // Keyword, Keyword.Namespace, Keyword.Constant, String, Number, // Comment, Comment.Single, Name.Function, Name.Builtin, // Operator, Punctuation, Text, Error if (type === 'Keyword' || type.startsWith('Keyword.')) { return palette.code.keyword; } if (type.startsWith('String')) { return palette.code.string; } if (type === 'Number') { return palette.code.number; } if (type.startsWith('Comment')) { return palette.code.comment; } if (type === 'Name.Function' || type === 'Name.Function.Magic') { return palette.code.function; } if (type === 'Name.Builtin' || type === 'Name.Builtin.Pseudo') { return palette.code.builtin; } return null; } // --------------------------------------------------------------------------- // 7. EXPORTS // --------------------------------------------------------------------------- const helpers = { slideBase, addHeader, addCodeBlock, addCodeBlockWithHighlight, addCallout, addProsCons, addPageNumber, addSectionDivider, addSourceLine, withFallback, highlightPython, }; module.exports = { theme: theme, palette: palette, fonts: fonts, sizes: sizes, spacing: spacing, layouts: layouts, helpers: helpers, };