document-design
Creates print-ready HTML that exports to PDF. Use to make a proposal, report, one-pager, newsletter, slides, or flyer.
Install
npx skills add https://github.com/jamditis/claude-skills-journalism/tree/master/pdf-playground/skills/document-design
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jamditis-claude-skills-journalism@llmmart
git clone https://github.com/jamditis/claude-skills-journalism.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jamditis/claude-skills-journalism collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Document design
Create professional, print-ready HTML documents that export to PDF with customizable branding.
Brand configuration
Before creating documents, check for pdf-playground.local.md in the project
root. If it is absent, check the legacy Claude Code path
.claude/pdf-playground.local.md. If both exist, use only the project-root
file. If neither exists, use sensible defaults or ask the user for their brand
colors.
Reading brand config
Parse the selected config file's YAML frontmatter:
---
brand:
name: "Organization Name"
tagline: "Tagline"
website: "https://example.com"
email: "contact@example.com"
colors:
primary: "#CA3553"
secondary: "#000000"
background: "#FFFFFF"
text: "#2d2a28"
muted: "#666666"
fonts:
heading: "Playfair Display"
body: "Source Sans 3"
style:
headingCase: "sentence"
useOxfordComma: true
---
Default brand values
If no config exists, use these defaults:
- Primary color:
#CA3553(red) - Secondary color:
#000000(black) - Heading font: Playfair Display
- Body font: Source Sans 3
- Heading case: sentence case
Core principles
- Print-first design: All documents target 8.5" × 11" letter size with proper margins
- Brand compliance: Use colors and fonts from brand configuration
- Sentence case by default: Unless brand config specifies "title" case
- Clean exports: Documents must render correctly when printed to PDF
CSS variables
Generate CSS variables from brand config:
:root {
--primary: [colors.primary];
--secondary: [colors.secondary];
--background: [colors.background];
--text: [colors.text];
--muted: [colors.muted];
/* Derived colors */
--primary-dark: [darken primary by 15%];
--gray-100: #f5f4f2;
--gray-200: #e8e6e3;
}
Print CSS fundamentals
Page setup
@page {
size: 8.5in 11in;
margin: 0;
}
@media print {
body {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.page {
page-break-after: always;
page-break-inside: avoid;
}
}
Fixed page dimensions
.page {
width: 8.5in;
height: 11in;
padding: 0.5in 0.75in;
padding-bottom: 1in; /* Space for footer */
position: relative;
box-sizing: border-box;
overflow: hidden;
}
Fixed footers
.page-footer {
position: absolute;
bottom: 0.4in;
left: 0.75in;
right: 0.75in;
font-size: 9pt;
border-top: 1px solid var(--gray-200);
padding-top: 0.1in;
background: var(--background);
}
Footer clearance (critical)
Content overlapping or touching the footer is a recurring issue.
Preferred layout, grid rows auto 1fr auto:
.page {
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
}
This makes the header and footer take their natural height, and the content fills the remaining space. No magic-number calc() needed, the footer clearance is structural.
Required safeguards:
- Use
grid-template-rows: auto 1fr autoon the page so content automatically gets the space between header and footer - Set
overflow: hiddenon the content container to prevent text bleeding past its bounds - Include
padding-bottom: 0.3in(minimum) inside the content area as a buffer - Never use hardcoded
height: calc(...)with magic numbers for header/footer heights, they drift when padding or font sizes change - After rendering, always screenshot and visually verify the bottom of the page before delivering
- If content overflows, reduce content, never shrink the footer gap. Tighten the header first if you need more room.
Typography patterns
Font loading
@import url('https://fonts.googleapis.com/css2?family=[heading-font]:wght@400;600;700&family=[body-font]:wght@400;500;600;700&display=swap');
body {
font-family: '[body-font]', Arial, sans-serif;
font-size: 11pt;
line-height: 1.6;
color: var(--text);
}
h1, h2, h3 {
font-family: '[heading-font]', Georgia, serif;
font-weight: 700;
}
Heading styles
.section-title {
font-size: 26pt;
color: var(--secondary);
margin-bottom: 0.25in;
}
.section-title::after {
content: '';
display: block;
width: 0.5in;
height: 3px;
background: var(--primary);
margin-top: 0.12in;
}
Common components
Cover page header
<header class="cover-header">
<div class="logo-bar">
<div class="logo-primary">[brand.name]</div>
</div>
<div class="cover-title-block">
<div class="cover-eyebrow">[Document type] • [Date]</div>
<h1 class="cover-title">[Title in configured case]</h1>
</div>
</header>
Budget table
.budget-table thead {
background: var(--secondary);
color: white;
}
.budget-table tbody tr:last-child {
background: var(--primary);
color: white;
font-weight: 700;
}
Highlight box
.highlight-box {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
padding: 0.3in;
}
Document creation workflow
- Check for brand config at project-root
pdf-playground.local.md, then the legacy.claude/pdf-playground.local.mdfallback - Locate this installed
SKILL.mdand resolve bundled resources from its directory. Do not assume a plugin-root environment variable is available. - Load template from the skill-relative
templates/directory - Apply brand settings to CSS variables and content
- Customize content based on user requirements
- Save HTML file in current working directory
- Offer preview with Playwright browser tools
PDF export instructions
- Open the HTML file in Chrome
- Press Ctrl+P (or Cmd+P on Mac)
- Set "Destination" to "Save as PDF"
- Set "Margins" to "None"
- Enable "Background graphics"
- Save the file
Additional resources
Templates
Pre-built templates in the installed skill's templates/ directory:
proposal-template.htmlreport-template.htmlonepager-template.htmlnewsletter-template.htmlslides-template.htmlevent-template.html
slides-template.html contains illustrative local photo and wordmark paths,
not bundled image assets. Before delivering a deck based on it, replace every
CSS url(...) and <img src> reference with an available asset. If the user
has no suitable images, remove every unresolved reference, use the template's
gradient or solid-color slide variants, and replace a missing wordmark image
with text. Never deliver a deck with an unresolved local asset path.
Brand examples
Example brand configurations in the installed skill's brands/ directory:
default.yaml- Default brand settingsccm.yaml- Center for Cooperative Mediaexample-newsroom.yaml- Sample newsroom config
Reference files
For detailed CSS patterns, use the installed skill's
references/css-patterns.md.
Preview controls
Reusable preview assets are in the installed skill's controls/ directory.
These files are resources for document generation; they do not turn the
Claude-only preview command or plugin hook into Codex features.
Files (claude-skills-journalism)
-
agents
-
openai.yaml 113 B
interface: display_name: "Document design" short_description: "Creates print-ready HTML that exports to PDF"
-
-
brands
-
ccm.yaml 1.6 KB
# Center for Cooperative Media brand configuration # Example of a complete brand setup --- brand: name: "Center for Cooperative Media" tagline: "Growing and strengthening local journalism" website: "https://centerforcooperativemedia.org" email: "info@centerforcooperativemedia.org" parent: "Montclair State University • College of Communication and Media" colors: primary: "#CA3553" # CCM Red secondary: "#000000" # Black background: "#FFFFFF" # White text: "#2d2a28" # Dark gray for body text muted: "#666666" # Medium gray cream: "#faf9f7" # Off-white for backgrounds lightGray: "#f5f4f2" # Light gray for cards fonts: heading: "Playfair Display" body: "Source Sans 3" style: headingCase: "sentence" useOxfordComma: true --- # CCM brand notes ## Writing style - Always use sentence case for headings (never title case) - Use the Oxford comma - Use commas to set off clauses, not em dashes - First reference: "Center for Cooperative Media" - Subsequent references: "the Center" (external) or "CCM" (internal only) ## Color usage - Use CCM Red (#CA3553) as primary accent - Black (#000000) for headlines and primary text - Avoid pure white backgrounds on cover pages - use cream (#faf9f7) - Red is for accents, highlights, and CTAs - not for large text blocks ## Logo placement - Logo appears in top right of cover pages - Include parent organization (Montclair State University) on formal documents - Use text-based logo mark when image logo unavailable ## Document footers Always include: - "Center for Cooperative Media • Montclair State University" - Page numbers on multi-page documents -
default.yaml 1.2 KB
# Default brand configuration # Codex installs: copy this to project-root pdf-playground.local.md and customize. # Claude Code commands read .claude/pdf-playground.local.md. --- brand: name: "Your Organization" tagline: "Your mission or tagline" website: "https://yourorg.com" email: "contact@yourorg.com" colors: primary: "#CA3553" # Main accent color (buttons, highlights) secondary: "#000000" # Secondary color (headers, borders) background: "#FFFFFF" # Page background text: "#2d2a28" # Body text color muted: "#666666" # Secondary/helper text fonts: heading: "Playfair Display" # Google Font for headings body: "Source Sans 3" # Google Font for body text style: headingCase: "sentence" # "sentence" or "title" useOxfordComma: true # Include Oxford comma in lists --- # Brand notes Add any additional brand guidelines or notes here that should be followed when creating documents. ## Logo usage Describe when and how to use your logo. ## Voice and tone Describe your organization's voice (formal, casual, authoritative, friendly, etc.) ## Specific requirements - Any specific formatting requirements - Colors to avoid - Required disclaimers or footers -
example-newsroom.yaml 1.7 KB
# Example newsroom brand configuration # A sample showing how a local news organization might configure their brand --- brand: name: "The Local Herald" tagline: "Serving Our Community Since 1952" website: "https://localherald.com" email: "editor@localherald.com" phone: "(555) 123-4567" address: "123 Main Street, Anytown, USA 12345" colors: primary: "#1a5f7a" # Teal blue - trustworthy, professional secondary: "#002b36" # Dark navy - authority accent: "#cb4b16" # Orange - for CTAs and highlights background: "#fdf6e3" # Warm cream - welcoming text: "#073642" # Dark text muted: "#586e75" # Secondary text fonts: heading: "Merriweather" # Classic serif for news feel body: "Open Sans" # Clean sans-serif for readability style: headingCase: "sentence" useOxfordComma: true --- # The Local Herald brand notes ## Our voice We are: - **Trustworthy** - Accurate, verified, fair - **Local** - Community-focused, accessible - **Clear** - Plain language, no jargon - **Engaged** - We care about our readers ## Logo usage - Full color logo on cover pages - Include founding year (1952) on formal documents - Tagline appears below logo on external documents ## Document standards - All proposals should reference our nonprofit status - Include our EIN on grant-related documents - Feature local impact metrics prominently ## Color guidance - Use teal (#1a5f7a) for headers and primary accents - Orange (#cb4b16) for calls-to-action only - Avoid using orange for body text or large areas - Cream background creates warm, inviting feel ## Photography When including images: - Prefer local community photos - Include photo credits - Avoid stock photography when possible
-
-
controls
-
template-maps
-
proposal.js 7.5 KB
/** * PDF Playground, Template map: Proposal * * Maps the proposal template's CSS variables, selectors, and sections * to human-readable control panel entries. The control-panel.js reads * this map to build the UI dynamically. */ (function () { "use strict"; window.PDFPlaygroundTemplateMap = { name: "proposal", // --- Color controls --- // Each entry: { variable, label, default } // "variable" is the CSS custom property name in :root colors: [ { variable: "--red", label: "Primary color", default: "#CA3553" }, { variable: "--red-dark", label: "Primary dark", default: "#a82a44" }, { variable: "--gray-800", label: "Text color", default: "#2d2a28" }, { variable: "--black", label: "Heading color", default: "#000000" }, { variable: "--white", label: "Background", default: "#ffffff" }, { variable: "--cream", label: "Accent bg", default: "#faf9f7" }, { variable: "--gray-100", label: "Light gray", default: "#f5f4f2" }, ], // --- Font controls --- // targets: CSS selectors that should get the font-family // options: list of Google Fonts to offer fonts: { heading: { label: "Heading font", targets: [ ".cover-title", ".section-title", ".logo-primary", ".stat-number", ".priority-number", ".case-study-org", ".data-callout-stat", ".highlight-text", ".mission-text", ".total-amount", ".footer-page", ".page-header-right", ".priority-content h3", ".contact-info h4", ".total-label span", ".logo-ccm-title" ], default: "Playfair Display", options: [ "Playfair Display", "Merriweather", "Fraunces", "Lora", "DM Serif Display", "Inter", "Montserrat", ], }, body: { label: "Body font", targets: ["body"], default: "Source Sans 3", options: [ "Source Sans 3", "Open Sans", "Inter", "Roboto", "Nunito", "Work Sans", "IBM Plex Sans", ], }, }, // --- Slider controls --- // Each: { property, label, unit, min, max, step, default, targets } // If "variable" is set, updates a CSS variable instead of a property sliders: [ { id: "body-font-size", label: "Body font size", property: "font-size", targets: ["body"], unit: "pt", min: 9, max: 14, step: 0.5, default: 11, }, { id: "heading-scale", label: "Heading scale", property: null, targets: [], unit: "x", min: 0.8, max: 1.3, step: 0.05, default: 1.0, // Special: multiplies all heading sizes by this factor isScale: true, scaleTargets: { ".cover-title": { base: 42, unit: "pt" }, ".section-title": { base: 26, unit: "pt" }, ".stat-number": { base: 36, unit: "pt" }, ".priority-number": { base: 28, unit: "pt" }, ".highlight-text": { base: 14, unit: "pt" }, ".data-callout-stat": { base: 18, unit: "pt" }, ".case-study-org": { base: 14, unit: "pt" }, ".total-amount": { base: 26, unit: "pt" }, }, }, { id: "page-padding", label: "Page padding", property: "padding-left", targets: [".content-page"], unit: "in", min: 0.4, max: 1.0, step: 0.05, default: 0.75, // Also update padding-right to match mirrorProperty: "padding-right", }, { id: "line-height", label: "Line height", property: "line-height", targets: ["body"], unit: "", min: 1.2, max: 2.0, step: 0.1, default: 1.6, }, ], // --- Section toggles --- // Each: { selector, label, default } // Toggling hides/shows the matched elements toggles: [ { id: "stat-grid", label: "Stat grid", selector: ".stat-grid", default: true, }, { id: "highlight-boxes", label: "Highlight boxes", selector: ".highlight-box", default: true, }, { id: "case-studies", label: "Case studies", selector: ".case-study", default: true, }, { id: "budget-table", label: "Budget table", selector: ".budget-table, .total-callout", default: true, }, { id: "mission-block", label: "Mission block", selector: ".cover-mission", default: true, }, ], // --- Layout controls --- layout: [ { id: "stat-columns", label: "Stat columns", type: "buttonGroup", target: ".stat-grid", property: "grid-template-columns", options: [ { value: "repeat(2, 1fr)", label: "2" }, { value: "repeat(3, 1fr)", label: "3" }, { value: "repeat(4, 1fr)", label: "4" }, ], default: "repeat(3, 1fr)", }, { id: "heading-case", label: "Heading case", type: "select", target: ".cover-title, .section-title, .priority-content h3", property: "text-transform", options: [ { value: "none", label: "Sentence case" }, { value: "capitalize", label: "Title case" }, { value: "uppercase", label: "Uppercase" }, ], default: "none", }, ], // --- Presets --- // Each preset overrides some or all color variables and optionally fonts presets: [ { id: "ccm-brand", label: "CCM brand", colors: { "--red": "#CA3553", "--red-dark": "#a82a44", "--gray-800": "#2d2a28", "--black": "#000000", "--white": "#ffffff", "--cream": "#faf9f7", "--gray-100": "#f5f4f2", }, headingFont: "Playfair Display", bodyFont: "Source Sans 3", }, { id: "professional-blue", label: "Professional blue", colors: { "--red": "#1a5f7a", "--red-dark": "#134a5e", "--gray-800": "#1e3040", "--black": "#0a1628", "--white": "#ffffff", "--cream": "#f0f5f7", "--gray-100": "#eef2f4", }, headingFont: "Merriweather", bodyFont: "Open Sans", }, { id: "modern-green", label: "Modern green", colors: { "--red": "#2d8659", "--red-dark": "#1f6b44", "--gray-800": "#1a2e24", "--black": "#0d1a12", "--white": "#ffffff", "--cream": "#f2f8f5", "--gray-100": "#edf5f0", }, headingFont: "Inter", bodyFont: "Work Sans", }, { id: "warm-earth", label: "Warm earth", colors: { "--red": "#b5651d", "--red-dark": "#8c4e17", "--gray-800": "#3d2e1f", "--black": "#1a1008", "--white": "#fffdf9", "--cream": "#faf5ee", "--gray-100": "#f7f2ea", }, headingFont: "Lora", bodyFont: "Nunito", }, { id: "elegant-purple", label: "Elegant purple", colors: { "--red": "#6b3fa0", "--red-dark": "#553080", "--gray-800": "#2a2040", "--black": "#130e20", "--white": "#ffffff", "--cream": "#f5f2fa", "--gray-100": "#f0ecf7", }, headingFont: "DM Serif Display", bodyFont: "IBM Plex Sans", }, ], }; })(); -
README.md 1.9 KB
# Template maps Each file in this directory defines the control panel layout for one document template. The control-panel.js reads the active map to build the correct set of controls. ## How it works A template map is a JS file that assigns `window.PDFPlaygroundTemplateMap` with the following sections: **colors**, Array of CSS custom property mappings. Each entry has `variable` (the CSS variable name in `:root`), `label` (displayed in the panel), and `default` (the starting hex value). **fonts**, Object with `heading` and `body` keys. Each has `targets` (CSS selectors to update), `default` (initial font name), and `options` (array of Google Font names to offer). **sliders**, Array of range controls. Each entry has `id`, `label`, `property` (CSS property to set), `targets` (selectors), `unit`, `min`, `max`, `step`, and `default`. Special cases: `isScale: true` multiplies base sizes for heading scaling. `mirrorProperty` copies the value to a second property. **toggles**, Array of show/hide switches. Each has `id`, `label`, `selector` (elements to toggle), and `default` (true = visible). **layout**, Array of layout controls. Each has `id`, `label`, `type` ("buttonGroup" or "select"), `target` (selector), `property` (CSS property), `options` (value/label pairs), and `default`. ## Creating a new map 1. Copy `proposal.js` as a starting point 2. Rename it to match your template (e.g., `report.js`) 3. Update `window.PDFPlaygroundTemplateMap.name` to match 4. Map the CSS variables and selectors from your template 5. The control panel auto-detects which template is loaded based on the map name ## Example: adding a color ```js { variable: "--accent", label: "Accent color", default: "#c9a227" } ``` ## Example: adding a toggle ```js { id: "testimonials", label: "Testimonials", selector: ".testimonial-block", default: true } ``` ## Supported templates - `proposal.js`, Funding proposal template
-
-
control-panel.css 11.9 KB · in bundle
-
control-panel.js 23.3 KB
/** * PDF Playground, Control panel * * Builds a sidebar with live design controls. When an iframe with * id="preview-frame" exists, all CSS changes target the iframe's * document (wrapper mode). Otherwise targets the current document * (injection mode, fallback). * * Requires prompt-generator.js and a template map to be loaded first. */ (function () { "use strict"; var map = window.PDFPlaygroundTemplateMap; var prompt = window.PDFPlaygroundPrompt; if (!map) { console.warn("[PDF Playground] No template map found. Control panel not loaded."); return; } if (!prompt) { console.warn("[PDF Playground] Prompt generator not found. Control panel not loaded."); return; } // Prevent double-init if (document.getElementById("pdf-playground-controls")) return; // --- Target document (iframe or current page) --- var previewFrame = document.getElementById("preview-frame"); var targetDoc = null; function getTargetDoc() { if (previewFrame && previewFrame.contentDocument) { return previewFrame.contentDocument; } return document; } // Wait for iframe to load before initializing controls function onTargetReady(callback) { if (!previewFrame) { targetDoc = document; callback(); return; } function check() { try { if (previewFrame.contentDocument && previewFrame.contentDocument.body) { targetDoc = previewFrame.contentDocument; callback(); return; } } catch (e) { // cross-origin, fall back targetDoc = document; callback(); return; } setTimeout(check, 100); } if (previewFrame.contentDocument && previewFrame.contentDocument.body) { targetDoc = previewFrame.contentDocument; callback(); } else { previewFrame.addEventListener("load", function () { targetDoc = previewFrame.contentDocument; callback(); }); // Also poll in case load already fired setTimeout(check, 200); } } // --- Helpers --- function el(tag, attrs, children) { var node = document.createElement(tag); if (attrs) { Object.keys(attrs).forEach(function (k) { if (k === "className") node.className = attrs[k]; else if (k === "textContent") node.textContent = attrs[k]; else if (k.indexOf("on") === 0) node.addEventListener(k.slice(2).toLowerCase(), attrs[k]); else node.setAttribute(k, attrs[k]); }); } if (children) { children.forEach(function (c) { if (typeof c === "string") node.appendChild(document.createTextNode(c)); else if (c) node.appendChild(c); }); } return node; } function setCSS(selector, prop, value) { var doc = getTargetDoc(); var targets = doc.querySelectorAll(selector); targets.forEach(function (t) { t.style.setProperty(prop, value); }); } function setCSSVariable(name, value) { var doc = getTargetDoc(); doc.documentElement.style.setProperty(name, value); } function getCurrentCSSVariable(name) { var doc = getTargetDoc(); return getComputedStyle(doc.documentElement).getPropertyValue(name).trim(); } // --- Google Fonts loader --- var loadedFonts = {}; function loadGoogleFont(fontName) { if (loadedFonts[fontName]) return; loadedFonts[fontName] = true; // Load into the target document (iframe) so the font renders there var doc = getTargetDoc(); var link = doc.createElement("link"); link.rel = "stylesheet"; link.href = "https://fonts.googleapis.com/css2?family=" + encodeURIComponent(fontName) + ":ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&display=swap"; doc.head.appendChild(link); } // --- Undo/redo stack --- var undoStack = []; var redoStack = []; var panel; // forward declaration function captureState() { var state = { colors: {}, headingFont: null, bodyFont: null }; if (map.colors) { map.colors.forEach(function (c) { state.colors[c.variable] = getCurrentCSSVariable(c.variable) || c.default; }); } var headingSelect = panel && panel.querySelector("[data-font-key='heading']"); var bodySelect = panel && panel.querySelector("[data-font-key='body']"); state.headingFont = headingSelect ? headingSelect.value : (map.fonts && map.fonts.heading ? map.fonts.heading.default : null); state.bodyFont = bodySelect ? bodySelect.value : (map.fonts && map.fonts.body ? map.fonts.body.default : null); return state; } function applyState(state) { Object.keys(state.colors).forEach(function (varName) { setCSSVariable(varName, state.colors[varName]); }); if (map.colors) { map.colors.forEach(function (c) { var val = state.colors[c.variable]; if (!val) return; var row = panel.querySelector("[data-color-var='" + c.variable + "']"); if (row) { var colorInput = row.querySelector(".ctrl-color-input"); var hexInput = row.querySelector(".ctrl-color-hex"); if (colorInput) colorInput.value = val; if (hexInput) hexInput.value = val; } }); } if (state.headingFont && map.fonts && map.fonts.heading) { loadGoogleFont(state.headingFont); map.fonts.heading.targets.forEach(function (selector) { setCSS(selector, "font-family", "'" + state.headingFont + "', Georgia, serif"); }); var headingSelect = panel.querySelector("[data-font-key='heading']"); if (headingSelect) headingSelect.value = state.headingFont; } if (state.bodyFont && map.fonts && map.fonts.body) { loadGoogleFont(state.bodyFont); map.fonts.body.targets.forEach(function (selector) { setCSS(selector, "font-family", "'" + state.bodyFont + "', -apple-system, BlinkMacSystemFont, sans-serif"); }); var bodySelect = panel.querySelector("[data-font-key='body']"); if (bodySelect) bodySelect.value = state.bodyFont; } } function pushUndo() { undoStack.push(captureState()); redoStack = []; updateUndoRedoButtons(); } function undo() { if (undoStack.length === 0) return; redoStack.push(captureState()); applyState(undoStack.pop()); updateUndoRedoButtons(); } function redo() { if (redoStack.length === 0) return; undoStack.push(captureState()); applyState(redoStack.pop()); updateUndoRedoButtons(); } var undoBtn, redoBtn; function updateUndoRedoButtons() { if (undoBtn) undoBtn.disabled = undoStack.length === 0; if (redoBtn) redoBtn.disabled = redoStack.length === 0; } // Keyboard shortcuts document.addEventListener("keydown", function (e) { if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) { e.preventDefault(); undo(); } else if ((e.ctrlKey || e.metaKey) && (e.key === "y" || (e.key === "z" && e.shiftKey))) { e.preventDefault(); redo(); } }); // --- Build panel --- panel = el("div", { id: "pdf-playground-controls" }); // Expand label (visible when collapsed) var expandLabel = el("span", { className: "panel-expand-label", textContent: "Controls", onClick: function () { panel.classList.remove("collapsed"); } }); // Header undoBtn = el("button", { className: "panel-btn", textContent: "\u21A9", title: "Undo (Ctrl+Z)", onClick: function () { undo(); } }); undoBtn.disabled = true; redoBtn = el("button", { className: "panel-btn", textContent: "\u21AA", title: "Redo (Ctrl+Y)", onClick: function () { redo(); } }); redoBtn.disabled = true; var header = el("div", { className: "panel-header" }, [ expandLabel, el("span", { className: "panel-title", textContent: "PDF Playground" }), el("div", { className: "panel-actions" }, [ undoBtn, redoBtn, el("button", { className: "panel-btn", textContent: "\u2192", title: "Collapse panel", onClick: function () { panel.classList.add("collapsed"); } }), ]), ]); panel.appendChild(header); // Scrollable body var body = el("div", { className: "panel-body" }); panel.appendChild(body); // --- Section builder --- function buildSection(label, buildFn) { var section = el("div", { className: "ctrl-section open" }); var sectionHeader = el("div", { className: "ctrl-section-header" }, [ el("span", { className: "ctrl-section-label", textContent: label }), el("span", { className: "ctrl-section-chevron", textContent: "\u25BC" }), ]); sectionHeader.addEventListener("click", function () { section.classList.toggle("open"); }); section.appendChild(sectionHeader); var sectionBody = el("div", { className: "ctrl-section-body" }); buildFn(sectionBody); section.appendChild(sectionBody); body.appendChild(section); } // --- Presets section --- if (map.presets && map.presets.length > 0) { buildSection("Presets", function (container) { var presetGrid = el("div", { className: "ctrl-preset-grid" }); map.presets.forEach(function (preset) { var swatch = el("div", { className: "ctrl-preset-swatch" }); swatch.style.background = preset.colors["--red"] || "#ccc"; var btn = el("button", { className: "ctrl-preset-btn", title: preset.label, onClick: function () { pushUndo(); Object.keys(preset.colors).forEach(function (varName) { setCSSVariable(varName, preset.colors[varName]); }); if (map.colors) { map.colors.forEach(function (c) { var val = preset.colors[c.variable]; if (!val) return; var row = panel.querySelector("[data-color-var='" + c.variable + "']"); if (row) { var ci = row.querySelector(".ctrl-color-input"); var hi = row.querySelector(".ctrl-color-hex"); if (ci) ci.value = val; if (hi) hi.value = val; } prompt.recordChange("color", c.label, { from: c.default, to: val }); }); } if (preset.headingFont && map.fonts && map.fonts.heading) { loadGoogleFont(preset.headingFont); map.fonts.heading.targets.forEach(function (sel) { setCSS(sel, "font-family", "'" + preset.headingFont + "', Georgia, serif"); }); var hs = panel.querySelector("[data-font-key='heading']"); if (hs) hs.value = preset.headingFont; prompt.recordChange("font", map.fonts.heading.label, { from: map.fonts.heading.default, to: preset.headingFont }); } if (preset.bodyFont && map.fonts && map.fonts.body) { loadGoogleFont(preset.bodyFont); map.fonts.body.targets.forEach(function (sel) { setCSS(sel, "font-family", "'" + preset.bodyFont + "', -apple-system, BlinkMacSystemFont, sans-serif"); }); var bs = panel.querySelector("[data-font-key='body']"); if (bs) bs.value = preset.bodyFont; prompt.recordChange("font", map.fonts.body.label, { from: map.fonts.body.default, to: preset.bodyFont }); } presetGrid.querySelectorAll(".ctrl-preset-btn").forEach(function (b) { b.classList.remove("active"); }); btn.classList.add("active"); }, }, [swatch, el("span", { className: "ctrl-preset-label", textContent: preset.label })]); presetGrid.appendChild(btn); }); container.appendChild(presetGrid); }); } // --- Colors section --- if (map.colors && map.colors.length > 0) { buildSection("Colors", function (container) { map.colors.forEach(function (c) { var hexInput = el("input", { className: "ctrl-color-hex", type: "text", value: c.default, }); var colorInput = el("input", { className: "ctrl-color-input", type: "color", value: c.default, }); function applyColor(newValue) { pushUndo(); setCSSVariable(c.variable, newValue); colorInput.value = newValue; hexInput.value = newValue; prompt.recordChange("color", c.label, { from: c.default, to: newValue }); } colorInput.addEventListener("input", function () { applyColor(this.value); }); hexInput.addEventListener("change", function () { var v = this.value.trim(); if (/^#[0-9a-fA-F]{3,8}$/.test(v)) { applyColor(v); } }); var row = el("div", { className: "ctrl-row", "data-color-var": c.variable }, [ el("span", { className: "ctrl-label", textContent: c.label }), el("div", { className: "ctrl-color-wrap" }, [colorInput, hexInput]), ]); container.appendChild(row); }); }); } // --- Fonts section --- if (map.fonts) { buildSection("Typography", function (container) { ["heading", "body"].forEach(function (key) { var cfg = map.fonts[key]; if (!cfg) return; var select = el("select", { className: "ctrl-font-select", "data-font-key": key }); cfg.options.forEach(function (fontName) { var opt = el("option", { value: fontName, textContent: fontName }); if (fontName === cfg.default) opt.selected = true; select.appendChild(opt); }); select.addEventListener("change", function () { pushUndo(); var fontName = this.value; loadGoogleFont(fontName); var fallback = key === "heading" ? ", Georgia, serif" : ", -apple-system, BlinkMacSystemFont, sans-serif"; cfg.targets.forEach(function (selector) { setCSS(selector, "font-family", "'" + fontName + "'" + fallback); }); prompt.recordChange("font", cfg.label, { from: cfg.default, to: fontName }); }); var row = el("div", { className: "ctrl-row" }, [ el("span", { className: "ctrl-label", textContent: cfg.label }), select, ]); container.appendChild(row); }); if (map.sliders) { map.sliders.forEach(function (s) { if (s.id === "body-font-size" || s.id === "heading-scale" || s.id === "line-height") { container.appendChild(buildSlider(s)); } }); } }); } // --- Slider builder --- function buildSlider(s) { var valueDisplay = el("span", { className: "ctrl-slider-value", textContent: s.default + s.unit, }); var slider = el("input", { className: "ctrl-slider", type: "range", min: String(s.min), max: String(s.max), step: String(s.step), value: String(s.default), }); slider.addEventListener("input", function () { var val = parseFloat(this.value); valueDisplay.textContent = (Math.round(val * 100) / 100) + s.unit; if (s.isScale && s.scaleTargets) { Object.keys(s.scaleTargets).forEach(function (selector) { var base = s.scaleTargets[selector]; var newSize = Math.round(base.base * val * 100) / 100; setCSS(selector, "font-size", newSize + base.unit); }); } else if (s.targets && s.targets.length > 0) { s.targets.forEach(function (selector) { setCSS(selector, s.property, val + s.unit); }); if (s.mirrorProperty) { s.targets.forEach(function (selector) { setCSS(selector, s.mirrorProperty, val + s.unit); }); } } prompt.recordChange("size", s.label, { from: s.default + s.unit, to: val + s.unit }); }); return el("div", { className: "ctrl-row" }, [ el("span", { className: "ctrl-label", textContent: s.label }), el("div", { className: "ctrl-slider-wrap" }, [slider, valueDisplay]), ]); } // --- Spacing section --- if (map.sliders) { var spacingSliders = map.sliders.filter(function (s) { return s.id === "page-padding"; }); if (spacingSliders.length > 0) { buildSection("Spacing", function (container) { spacingSliders.forEach(function (s) { container.appendChild(buildSlider(s)); }); }); } } // --- Sections (toggles) --- if (map.toggles && map.toggles.length > 0) { buildSection("Sections", function (container) { map.toggles.forEach(function (t) { var checkbox = el("input", { type: "checkbox" }); checkbox.checked = t.default; checkbox.addEventListener("change", function () { var visible = this.checked; var doc = getTargetDoc(); var elements = doc.querySelectorAll(t.selector); elements.forEach(function (elem) { elem.style.display = visible ? "" : "none"; }); prompt.recordChange("toggle", t.label, { action: visible ? "show" : "hide", target: t.selector, }); }); var toggle = el("label", { className: "ctrl-toggle" }, [ checkbox, el("span", { className: "ctrl-toggle-track" }), ]); var row = el("div", { className: "ctrl-row" }, [ el("span", { className: "ctrl-label", textContent: t.label }), toggle, ]); container.appendChild(row); }); }); } // --- Layout section --- if (map.layout && map.layout.length > 0) { buildSection("Layout", function (container) { map.layout.forEach(function (l) { if (l.type === "buttonGroup") { var group = el("div", { className: "ctrl-btn-group" }); l.options.forEach(function (opt) { var btn = el("button", { className: "ctrl-btn-option" + (opt.value === l.default ? " active" : ""), textContent: opt.label, }); btn.addEventListener("click", function () { group.querySelectorAll(".ctrl-btn-option").forEach(function (b) { b.classList.remove("active"); }); btn.classList.add("active"); setCSS(l.target, l.property, opt.value); prompt.recordChange("layout", l.label, { from: l.default, to: opt.label + " columns" }); }); group.appendChild(btn); }); var row = el("div", { className: "ctrl-row" }, [ el("span", { className: "ctrl-label", textContent: l.label }), group, ]); container.appendChild(row); } else if (l.type === "select") { var select = el("select", { className: "ctrl-font-select" }); l.options.forEach(function (opt) { var option = el("option", { value: opt.value, textContent: opt.label }); if (opt.value === l.default) option.selected = true; select.appendChild(option); }); select.addEventListener("change", function () { var val = this.value; var selectedLabel = l.options.find(function (o) { return o.value === val; }); l.target.split(",").forEach(function (selector) { setCSS(selector.trim(), l.property, val); }); prompt.recordChange("layout", l.label, { from: l.default, to: selectedLabel ? selectedLabel.label : val, }); }); var row = el("div", { className: "ctrl-row" }, [ el("span", { className: "ctrl-label", textContent: l.label }), select, ]); container.appendChild(row); } }); }); } // --- Footer (pending changes + actions) --- var footer = el("div", { className: "panel-footer" }); var pendingCountBadge = el("span", { className: "pending-count", textContent: "0" }); var pendingChevron = el("span", { className: "pending-chevron", textContent: "\u25B2" }); var pendingTitle = el("span", { className: "pending-title" }, ["Changes"]); var pendingList = el("div", { className: "pending-list" }); var pendingHeader = el("div", { className: "pending-header" }, [ pendingTitle, pendingCountBadge, pendingChevron ]); pendingHeader.addEventListener("click", function () { pendingList.classList.toggle("open"); pendingChevron.classList.toggle("open"); }); // Pending list is positioned absolutely (expands upward) footer.appendChild(pendingList); footer.appendChild(pendingHeader); // Action buttons var actionsBar = el("div", { className: "panel-actions-bar" }, [ el("button", { className: "action-btn action-btn-primary", textContent: "Copy changes", onClick: function () { prompt.copyToClipboard(); }, }), el("button", { className: "action-btn action-btn-secondary", textContent: "Reset", onClick: function () { prompt.clearAll(); if (previewFrame) { previewFrame.contentWindow.location.reload(); } else { window.location.reload(); } }, }), ]); footer.appendChild(actionsBar); panel.appendChild(footer); // --- Update pending list when changes happen --- function rebuildPendingList(changes) { pendingCountBadge.textContent = String(changes.length); while (pendingList.firstChild) { pendingList.removeChild(pendingList.firstChild); } changes.forEach(function (c, i) { var text = (i + 1) + ". " + prompt.generateSinglePrompt(c.type, c.label, c.details); var removeBtn = el("button", { className: "pending-item-remove", textContent: "\u2715", onClick: function (e) { e.stopPropagation(); prompt.removeChange(c.key); }, }); var item = el("div", { className: "pending-item" }, [ el("span", { className: "pending-item-text", textContent: text }), removeBtn, ]); pendingList.appendChild(item); }); if (changes.length > 0 && !pendingList.classList.contains("open")) { pendingList.classList.add("open"); pendingChevron.classList.add("open"); } } prompt.onUpdate(rebuildPendingList); // --- Inject panel and initialize --- document.body.appendChild(panel); // Pre-load fonts once the target document is ready onTargetReady(function () { if (map.fonts) { if (map.fonts.heading) loadGoogleFont(map.fonts.heading.default); if (map.fonts.body) loadGoogleFont(map.fonts.body.default); } if (map.presets) { map.presets.forEach(function (p) { if (p.headingFont) loadGoogleFont(p.headingFont); if (p.bodyFont) loadGoogleFont(p.bodyFont); }); } // Read current color values from the target document if (map.colors) { map.colors.forEach(function (c) { var currentValue = getCurrentCSSVariable(c.variable) || c.default; var row = panel.querySelector("[data-color-var='" + c.variable + "']"); if (row) { var colorInput = row.querySelector(".ctrl-color-input"); var hexInput = row.querySelector(".ctrl-color-hex"); if (colorInput) colorInput.value = currentValue; if (hexInput) hexInput.value = currentValue; } }); } console.log("[PDF Playground] Control panel ready for template: " + map.name); }); // Cleanup function window.PDFPlaygroundCleanup = function () { panel.remove(); var toast = document.getElementById("pdf-playground-toast"); if (toast) toast.remove(); }; console.log("[PDF Playground] Control panel loaded for template: " + map.name); })(); -
playground-wrapper.html 1.6 KB · in bundle
-
prompt-generator.js 5.4 KB
/** * PDF Playground, Prompt generator * * Tracks design changes made through the control panel and generates * Claude Code prompts that can be pasted back to apply those changes * to the HTML source file. */ (function () { "use strict"; // Pending changes keyed by "type::label" to deduplicate const pending = new Map(); // Callbacks to notify the UI when changes update const listeners = []; function onUpdate(fn) { listeners.push(fn); } function notifyListeners() { listeners.forEach((fn) => fn(getAll())); } // --- Recording changes --- /** * Record a single design change. * Deduplicates by type+label so changing the same slider twice * keeps only the latest value. * * @param {string} type - Category: "color", "font", "size", "toggle", "layout" * @param {string} label - Human-readable label: "Primary color", "Body font size" * @param {object} details - { from, to } or { action, target } depending on type */ function recordChange(type, label, details) { // Skip no-op changes (e.g. background #ffffff -> #ffffff) if (details.from && details.to && details.from.toLowerCase() === details.to.toLowerCase()) { return; } const key = type + "::" + label; pending.set(key, { type, label, details, time: Date.now() }); notifyListeners(); } /** * Remove a single pending change by its key. */ function removeChange(key) { pending.delete(key); notifyListeners(); } /** * Clear all pending changes. */ function clearAll() { pending.clear(); notifyListeners(); } // --- Prompt generation --- /** * Generate a prompt string for one change. */ function generateSinglePrompt(type, label, details) { switch (type) { case "color": return "Change the " + label.toLowerCase() + " from " + details.from + " to " + details.to; case "font": return "Switch the " + label.toLowerCase() + " to " + details.to; case "size": return "Set the " + label.toLowerCase() + " to " + details.to; case "toggle": if (details.action === "hide") { return "Remove the " + label.toLowerCase() + " section"; } return "Add back the " + label.toLowerCase() + " section"; case "layout": return "Change the " + label.toLowerCase() + " to " + details.to; default: return "Change " + label.toLowerCase() + " to " + (details.to || details.action); } } /** * Generate a combined prompt for all pending changes. * Returns a multi-line string ready to paste into Claude Code. */ function generateCombinedPrompt() { var entries = getAll(); if (entries.length === 0) return ""; var templateName = (window.PDFPlaygroundTemplateMap && window.PDFPlaygroundTemplateMap.name) || "document"; if (entries.length === 1) { var e = entries[0]; return generateSinglePrompt(e.type, e.label, e.details) + " in the " + templateName; } var lines = ["Apply the following changes to the " + templateName + ":"]; entries.forEach(function (entry, i) { lines.push((i + 1) + ". " + generateSinglePrompt(entry.type, entry.label, entry.details)); }); return lines.join("\n"); } /** * Get all pending changes as an array, sorted by time recorded. */ function getAll() { return Array.from(pending.entries()) .sort(function (a, b) { return a[1].time - b[1].time; }) .map(function (pair) { return Object.assign({ key: pair[0] }, pair[1]); }); } // --- Clipboard --- /** * Copy the combined prompt to the system clipboard and show a toast. */ function copyToClipboard() { var text = generateCombinedPrompt(); if (!text) { showToast("No changes to copy"); return; } navigator.clipboard.writeText(text).then(function () { showToast("Copied " + pending.size + " change" + (pending.size === 1 ? "" : "s") + " to clipboard"); }).catch(function () { // Fallback: select a temporary textarea var ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); showToast("Copied " + pending.size + " change" + (pending.size === 1 ? "" : "s") + " to clipboard"); } catch (_) { showToast("Copy failed, select and copy manually"); } document.body.removeChild(ta); }); } // --- Toast --- function showToast(message) { var existing = document.getElementById("pdf-playground-toast"); if (existing) existing.remove(); var toast = document.createElement("div"); toast.id = "pdf-playground-toast"; toast.textContent = message; document.body.appendChild(toast); // Trigger animation requestAnimationFrame(function () { requestAnimationFrame(function () { toast.classList.add("visible"); }); }); setTimeout(function () { toast.classList.remove("visible"); setTimeout(function () { toast.remove(); }, 300); }, 2500); } // --- Public API --- window.PDFPlaygroundPrompt = { recordChange: recordChange, removeChange: removeChange, clearAll: clearAll, getAll: getAll, generateSinglePrompt: generateSinglePrompt, generateCombinedPrompt: generateCombinedPrompt, copyToClipboard: copyToClipboard, onUpdate: onUpdate, showToast: showToast, }; })();
-
-
references
-
css-patterns.md 4.2 KB
# CSS patterns for print-ready documents ## Page setup ```css @page { size: 8.5in 11in; margin: 0; } @media print { body { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } } ``` ## Page breaks ```css /* Force page break before element */ .page-break-before { page-break-before: always; break-before: page; } /* Force page break after element */ .page-break-after { page-break-after: always; break-after: page; } /* Prevent page break inside element */ .keep-together { page-break-inside: avoid; break-inside: avoid; } ``` ## Fixed page dimensions ```css .page { width: 8.5in; height: 11in; padding: 0.75in; box-sizing: border-box; position: relative; overflow: hidden; } ``` ## Cover page layout ```css .cover-page { display: flex; flex-direction: column; justify-content: space-between; height: 100%; } .cover-header { /* Top section with logo/org name */ } .cover-content { /* Main title, subtitle, stats */ flex: 1; display: flex; flex-direction: column; justify-content: center; } .cover-footer { /* Date, contact info */ } ``` ## Content page layout ```css .content-page { display: flex; flex-direction: column; height: 100%; } .page-header { /* Document title, section name */ padding-bottom: 0.5in; } .page-body { flex: 1; overflow: hidden; } .page-footer { /* Page number, document title */ position: absolute; bottom: 0.5in; left: 0.75in; right: 0.75in; } ``` ## Typography scale ```css :root { --font-heading: 'Playfair Display', Georgia, serif; --font-body: 'Source Sans 3', 'Source Sans Pro', sans-serif; /* Type scale */ --text-xs: 0.75rem; /* 12px - fine print */ --text-sm: 0.875rem; /* 14px - captions */ --text-base: 1rem; /* 16px - body */ --text-lg: 1.125rem; /* 18px - lead text */ --text-xl: 1.25rem; /* 20px - subheadings */ --text-2xl: 1.5rem; /* 24px - section heads */ --text-3xl: 2rem; /* 32px - page titles */ --text-4xl: 2.5rem; /* 40px - cover title */ --text-5xl: 3rem; /* 48px - hero title */ } ``` ## Color variables ```css :root { --color-primary: #CA3553; --color-secondary: #000000; --color-background: #FFFFFF; --color-text: #2d2a28; --color-muted: #666666; --color-border: #e5e5e5; } ``` ## Budget tables ```css .budget-table { width: 100%; border-collapse: collapse; } .budget-table th { background: var(--color-primary); color: white; padding: 0.5rem 0.75rem; text-align: left; font-weight: 600; } .budget-table td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--color-border); } .budget-table .total-row { background: var(--color-primary); color: white; font-weight: 700; } .budget-table .amount { text-align: right; font-variant-numeric: tabular-nums; } ``` ## Stat blocks ```css .stats-row { display: flex; justify-content: space-around; gap: 1rem; } .stat-block { text-align: center; } .stat-number { font-size: var(--text-4xl); font-weight: 700; color: var(--color-primary); line-height: 1; } .stat-label { font-size: var(--text-sm); color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.05em; } ``` ## Pull quotes ```css .pull-quote { border-left: 4px solid var(--color-primary); padding-left: 1.5rem; margin: 1.5rem 0; font-size: var(--text-lg); font-style: italic; color: var(--color-text); } .pull-quote cite { display: block; margin-top: 0.5rem; font-size: var(--text-sm); font-style: normal; color: var(--color-muted); } ``` ## Two-column layout ```css .two-column { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; } .sidebar-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; } ``` ## Chromium PDF generation Due to snap confinement on some systems, use this path for PDF generation: ```bash # Copy HTML to snap-accessible location cp document.html ~/snap/chromium/common/pdf-work/ # Generate PDF chromium-browser --headless --disable-gpu \ --print-to-pdf="$HOME/snap/chromium/common/pdf-work/output.pdf" \ --no-pdf-header-footer \ "file://$HOME/snap/chromium/common/pdf-work/document.html" # Copy result back cp ~/snap/chromium/common/pdf-work/output.pdf ./ ```
-
-
templates
-
event-template.html 9.4 KB · in bundle
-
newsletter-template.html 10.3 KB · in bundle
-
onepager-template.html 9.2 KB · in bundle
-
proposal-template.html 28.7 KB · in bundle
-
report-template.html 10.8 KB · in bundle
-
slides-template.html 34.3 KB · in bundle
-
-
SKILL.md 7.3 KB
--- name: document-design description: Creates print-ready HTML that exports to PDF. Use to make a proposal, report, one-pager, newsletter, slides, or flyer. --- # Document design Create professional, print-ready HTML documents that export to PDF with customizable branding. ## Brand configuration Before creating documents, check for `pdf-playground.local.md` in the project root. If it is absent, check the legacy Claude Code path `.claude/pdf-playground.local.md`. If both exist, use only the project-root file. If neither exists, use sensible defaults or ask the user for their brand colors. ### Reading brand config Parse the selected config file's YAML frontmatter: ```yaml --- brand: name: "Organization Name" tagline: "Tagline" website: "https://example.com" email: "contact@example.com" colors: primary: "#CA3553" secondary: "#000000" background: "#FFFFFF" text: "#2d2a28" muted: "#666666" fonts: heading: "Playfair Display" body: "Source Sans 3" style: headingCase: "sentence" useOxfordComma: true --- ``` ### Default brand values If no config exists, use these defaults: - **Primary color**: `#CA3553` (red) - **Secondary color**: `#000000` (black) - **Heading font**: Playfair Display - **Body font**: Source Sans 3 - **Heading case**: sentence case ## Core principles 1. **Print-first design**: All documents target 8.5" × 11" letter size with proper margins 2. **Brand compliance**: Use colors and fonts from brand configuration 3. **Sentence case by default**: Unless brand config specifies "title" case 4. **Clean exports**: Documents must render correctly when printed to PDF ## CSS variables Generate CSS variables from brand config: ```css :root { --primary: [colors.primary]; --secondary: [colors.secondary]; --background: [colors.background]; --text: [colors.text]; --muted: [colors.muted]; /* Derived colors */ --primary-dark: [darken primary by 15%]; --gray-100: #f5f4f2; --gray-200: #e8e6e3; } ``` ## Print CSS fundamentals ### Page setup ```css @page { size: 8.5in 11in; margin: 0; } @media print { body { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } .page { page-break-after: always; page-break-inside: avoid; } } ``` ### Fixed page dimensions ```css .page { width: 8.5in; height: 11in; padding: 0.5in 0.75in; padding-bottom: 1in; /* Space for footer */ position: relative; box-sizing: border-box; overflow: hidden; } ``` ### Fixed footers ```css .page-footer { position: absolute; bottom: 0.4in; left: 0.75in; right: 0.75in; font-size: 9pt; border-top: 1px solid var(--gray-200); padding-top: 0.1in; background: var(--background); } ``` ### Footer clearance (critical) Content overlapping or touching the footer is a recurring issue. **Preferred layout, grid rows `auto 1fr auto`:** ```css .page { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; } ``` This makes the header and footer take their natural height, and the content fills the remaining space. No magic-number `calc()` needed, the footer clearance is structural. **Required safeguards:** 1. Use `grid-template-rows: auto 1fr auto` on the page so content automatically gets the space between header and footer 2. Set `overflow: hidden` on the content container to prevent text bleeding past its bounds 3. Include `padding-bottom: 0.3in` (minimum) inside the content area as a buffer 4. Never use hardcoded `height: calc(...)` with magic numbers for header/footer heights, they drift when padding or font sizes change 5. After rendering, always screenshot and visually verify the bottom of the page before delivering 6. If content overflows, **reduce content**, never shrink the footer gap. Tighten the header first if you need more room. ## Typography patterns ### Font loading ```css @import url('https://fonts.googleapis.com/css2?family=[heading-font]:wght@400;600;700&family=[body-font]:wght@400;500;600;700&display=swap'); body { font-family: '[body-font]', Arial, sans-serif; font-size: 11pt; line-height: 1.6; color: var(--text); } h1, h2, h3 { font-family: '[heading-font]', Georgia, serif; font-weight: 700; } ``` ### Heading styles ```css .section-title { font-size: 26pt; color: var(--secondary); margin-bottom: 0.25in; } .section-title::after { content: ''; display: block; width: 0.5in; height: 3px; background: var(--primary); margin-top: 0.12in; } ``` ## Common components ### Cover page header ```html <header class="cover-header"> <div class="logo-bar"> <div class="logo-primary">[brand.name]</div> </div> <div class="cover-title-block"> <div class="cover-eyebrow">[Document type] • [Date]</div> <h1 class="cover-title">[Title in configured case]</h1> </div> </header> ``` ### Budget table ```css .budget-table thead { background: var(--secondary); color: white; } .budget-table tbody tr:last-child { background: var(--primary); color: white; font-weight: 700; } ``` ### Highlight box ```css .highlight-box { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); color: white; padding: 0.3in; } ``` ## Document creation workflow 1. **Check for brand config** at project-root `pdf-playground.local.md`, then the legacy `.claude/pdf-playground.local.md` fallback 2. **Locate this installed `SKILL.md`** and resolve bundled resources from its directory. Do not assume a plugin-root environment variable is available. 3. **Load template** from the skill-relative `templates/` directory 4. **Apply brand settings** to CSS variables and content 5. **Customize content** based on user requirements 6. **Save HTML file** in current working directory 7. **Offer preview** with Playwright browser tools ## PDF export instructions 1. Open the HTML file in Chrome 2. Press Ctrl+P (or Cmd+P on Mac) 3. Set "Destination" to "Save as PDF" 4. Set "Margins" to "None" 5. Enable "Background graphics" 6. Save the file ## Additional resources ### Templates Pre-built templates in the installed skill's `templates/` directory: - `proposal-template.html` - `report-template.html` - `onepager-template.html` - `newsletter-template.html` - `slides-template.html` - `event-template.html` `slides-template.html` contains illustrative local photo and wordmark paths, not bundled image assets. Before delivering a deck based on it, replace every CSS `url(...)` and `<img src>` reference with an available asset. If the user has no suitable images, remove every unresolved reference, use the template's gradient or solid-color slide variants, and replace a missing wordmark image with text. Never deliver a deck with an unresolved local asset path. ### Brand examples Example brand configurations in the installed skill's `brands/` directory: - `default.yaml` - Default brand settings - `ccm.yaml` - Center for Cooperative Media - `example-newsroom.yaml` - Sample newsroom config ### Reference files For detailed CSS patterns, use the installed skill's `references/css-patterns.md`. ### Preview controls Reusable preview assets are in the installed skill's `controls/` directory. These files are resources for document generation; they do not turn the Claude-only preview command or plugin hook into Codex features.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.