Claude GitHub Copilot Skill

document-design

Creates print-ready HTML that exports to PDF. Use to make a proposal, report, one-pager, newsletter, slides, or flyer.

LLM Mart · 0 points · 22 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download jamditis-claude-skills-journalism-pdf-playground_skills_document-design-9e8e419.zip · 41 KB
Part of jamditis/claude-skills-journalism — 60 skills

Install

skills CLI npx skills add https://github.com/jamditis/claude-skills-journalism/tree/master/pdf-playground/skills/document-design
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jamditis-claude-skills-journalism@llmmart
Git 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

  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:

: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:

  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

@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

  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.

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.

No comments yet.

Reviews (0)

No reviews yet.

Related