From 44c8912066d6059ab30e69db434322c010b6ae1d Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Tue, 19 May 2026 14:40:42 -0700 Subject: [PATCH] feat(skills): design picker build script + skill docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/build-design-picker.py — renders the picker HTML from the design-picker.html template, injecting template/palette/typepair JSON and a manifest of installed presentations. Auto-detects a project's DESIGN.html and pre-seeds a 'user-design' specimen when present. Supporting build scripts: tokenize-templates, build-design-templates, build-summaries, detokenize-summaries, inject-tp-tokens, lint-design-html, extract-template-structure, build-template-picker. Skill docs: design-picker.md is the contract for picker data and the polarity rule for supplemental palettes (primary = light, secondary = dark); design-html.md / design-html-templates.md / design-showcase.md / shader-backgrounds.md / figma-to-design-html.md / prompt-expansion.md cover the surrounding workflow. SKILL.md / visual-styles.md / house-style.md updated to reference the picker. --- skills/hyperframes/SKILL.md | 67 +- skills/hyperframes/house-style.md | 8 +- .../references/design-html-templates.md | 108 +++ skills/hyperframes/references/design-html.md | 400 ++++++++++ .../hyperframes/references/design-picker.md | 692 ++++++++++++++++- .../hyperframes/references/design-showcase.md | 644 ++++++++++++++++ .../references/figma-to-design-html.md | 272 +++++++ .../references/prompt-expansion.md | 8 +- .../references/shader-backgrounds.md | 173 +++++ .../references/video-composition.md | 18 +- .../scripts/build-design-picker.py | 436 +++++++++++ .../scripts/build-design-templates.py | 490 ++++++++++++ skills/hyperframes/scripts/build-summaries.py | 277 +++++++ .../scripts/build-template-picker.py | 163 ++++ .../scripts/detokenize-summaries.py | 236 ++++++ .../scripts/extract-template-structure.py | 257 +++++++ .../hyperframes/scripts/inject-tp-tokens.py | 208 +++++ .../hyperframes/scripts/lint-design-html.py | 156 ++++ .../hyperframes/scripts/tokenize-templates.py | 255 +++++++ skills/hyperframes/templates/index.json | 711 ++++++++++++++++++ .../templates/presentations-index.json | 711 ++++++++++++++++++ 21 files changed, 6245 insertions(+), 45 deletions(-) create mode 100644 skills/hyperframes/references/design-html-templates.md create mode 100644 skills/hyperframes/references/design-html.md create mode 100644 skills/hyperframes/references/design-showcase.md create mode 100644 skills/hyperframes/references/figma-to-design-html.md create mode 100644 skills/hyperframes/references/shader-backgrounds.md create mode 100644 skills/hyperframes/scripts/build-design-picker.py create mode 100644 skills/hyperframes/scripts/build-design-templates.py create mode 100644 skills/hyperframes/scripts/build-summaries.py create mode 100755 skills/hyperframes/scripts/build-template-picker.py create mode 100644 skills/hyperframes/scripts/detokenize-summaries.py create mode 100644 skills/hyperframes/scripts/extract-template-structure.py create mode 100644 skills/hyperframes/scripts/inject-tp-tokens.py create mode 100644 skills/hyperframes/scripts/lint-design-html.py create mode 100644 skills/hyperframes/scripts/tokenize-templates.py create mode 100644 skills/hyperframes/templates/index.json create mode 100644 skills/hyperframes/templates/presentations-index.json diff --git a/skills/hyperframes/SKILL.md b/skills/hyperframes/SKILL.md index f68c52dac..f37141bca 100644 --- a/skills/hyperframes/SKILL.md +++ b/skills/hyperframes/SKILL.md @@ -9,32 +9,68 @@ HTML is the source of truth for video. A composition is an HTML file with `data- ## Approach -### Discovery (exploratory requests only) +### Brief (exploratory requests only) -For open-ended requests ("make me a product launch video", "create something for our brand") where the user hasn't committed to a direction, understand intent before picking colors: +For open-ended requests ("make me a product launch video", "create something for our brand"), gather what's missing from the prompt before building a design system. For specific requests ("add a title card", "fix the timing on scene 3"), skip this. -- **Audience** — who watches this? Developers? Executives? General consumers? -- **Platform** — where does it play? Social (15s), website hero, product demo, internal? -- **Priority** — what matters most? Motion quality? Content accuracy? Brand fidelity? Speed? -- **Variations** — does the user want options, or a single best shot? +Extract what you can from the prompt itself — only ask about what's genuinely missing. Ask **one question at a time** and let each answer inform the next. Combine two questions in one message when they're naturally related, but never dump a list. -For specific requests ("add a title card", "fix the timing on scene 3"), skip discovery. +**Format each question as a lettered multiple choice** (A, B, C, D) so the user can respond with just a letter. Include 2-4 contextual options plus the option to give a custom answer. Tailor the options to the specific prompt — don't use generic choices. -For exploratory requests, consider offering 2-3 variations that differ meaningfully — not just color swaps, but different pacing, energy levels, or structural approaches. One safe/expected, one ambitious. Don't mandate this — it's a tool available when appropriate. +The questions (in rough priority order): + +- **Audience** — who watches this? (Developers, executives, consumers — changes type scale, density, easing) +- **Emotion** — what should the viewer feel? (Maps directly to mood board clustering in the picker. Sharpen this based on the audience answer — "technical precision or breakthrough excitement?" is better than "what mood?") +- **Brand assets** — do they have a logo, existing colors, fonts, website? (If yes, palettes and type pairings derive from them. If no, skip — full creative freedom.) +- **Light or dark?** (Eliminates half the palette options. Can often combine with another question.) +- **Surface** — where does this play? (Social → portrait/punchy, website hero → landscape/looping, presentation → widescreen/breathable, internal → information-dense) +- **Key takeaway** — what's the one thing the viewer should remember? (Drives focal hierarchy in architecture previews) + +Most prompts already answer 2-3 of these — a typical brief is 2-3 questions, not 6. These answers seed the design picker in Step 1 — pass them when generating mood boards, palettes, and architectures. ### Step 1: Design system -If `design.md` or `DESIGN.md` exists in the project, read it first (check both casings — they're different files on Linux). It's the source of truth for brand colors, fonts, and constraints. Use its exact values — don't invent colors or substitute fonts. Any format works (YAML frontmatter, prose, tables — just extract the values). +**Always check for DESIGN.html first.** At the start of any composition task, look for `DESIGN.html` in the project root. This is the primary design system format — a self-contained HTML document with rendered sections for palette, typography, surface, motion, background shader, guidelines, and template slide gallery. + +**Check in this order:** + +1. **`DESIGN.html` exists** → Read it using the guide at [references/design-html.md](references/design-html.md). The guide covers both token extraction AND template extraction (the page has TWO design systems — build from the templates, not the page chrome). If a shader background exists, port it using the GSAP-proxy pattern in the guide. Proceed to Step 2. + +2. **No `DESIGN.html`, but `design.md` or `DESIGN.md` exists** → Generate a bespoke DESIGN.html: + + Read [references/design-showcase.md](references/design-showcase.md) for the full generation process. This is NOT a template fill — the agent must craft a page that embodies the brand's visual personality. An HP design.md produces angular chevrons and sharp 4px buttons. A Meta design.md produces pill-shaped elements and photography-first 32px-radius cards. Same sections, completely different visual treatment. + + The generation process: + 1. Extract palette, typography, motion, surface tokens from the design.md + 2. Place the system on character axes (rounded vs sharp, flat vs elevated, photo-first vs type-first, warm vs clinical) + 3. Write each section (cover, palette, type, surface, motion, background, guidelines, templates) styled to the brand's character + 4. The cover/hero section must have a signature decorative gesture derived from the brand (not generic) + 5. Save as `DESIGN.html` in the project root + + Then offer the picker for fine-tuning: read [references/design-picker.md](references/design-picker.md) to serve the visual picker with the generated DESIGN.html pre-loaded. The user can adjust palette, typography, corners, density, depth, and shader background. The picker loads the DESIGN.html in its iframe preview. + + If the user declines both generation and picker → extract palette, font, and constraint values directly from the markdown. Proceed to Step 2. + +3. **User provides a Figma URL** → Read [references/figma-to-design-html.md](references/figma-to-design-html.md) for the lossless extraction process. Pull colors, typography, and component properties directly from the Figma API. Craft a bespoke DESIGN.html using exact Figma values — no approximation. Then offer the picker for fine-tuning. + +4. **Neither exists and no Figma URL** → Prompt the user: -If it names fonts you can't find locally (no `fonts/` directory with `.woff2` files, not a built-in font), warn the user before writing HTML: "design.md specifies [font name] but no font files found. Please add .woff2 files to `fonts/` or I'll fall back to [closest built-in alternative]." + > "No design file found. Would you like to: + > **A) Browse templates visually** — Open a template picker in your browser. Choose from 34 visual directions, then configure palette, typography, corners, motion, and shader background. Exports a DESIGN.html ready for video composition. + > **B) Describe a style** — Tell me the mood, energy, or a style reference (e.g. "dark editorial", "warm minimal", "brutalist"). I'll match it to one of 8 named presets. + > **C) Skip design, go fast** — Jump straight to building. I'll ask 2-3 quick questions and pick a palette." + - **A) Template picker** → Read [references/design-picker.md](references/design-picker.md) for the full workflow. + - **B) Style or mood** → Read [visual-styles.md](./visual-styles.md) for the 8 named presets. Pick the closest match. + - **C) Fast path** → Ask: mood, light or dark, any brand colors/fonts? Then pick a palette from [house-style.md](./house-style.md). -If no `design.md` exists, offer the user a choice: +**Font resolution:** When a design file names a font, resolve it in this order: -1. **User named a style or mood?** → Read [visual-styles.md](./visual-styles.md) for the 8 named presets. Pick the closest match. -2. **Want to browse options visually?** → Run the design picker: read [references/design-picker.md](references/design-picker.md) for the full workflow. This serves a visual picker page. The user configures mood, palette, typography, and motion in the browser, then copies the generated design.md and pastes it back into the conversation. -3. **Want to skip and go fast?** → Ask: mood, light or dark, any brand colors/fonts? Then pick a palette from [house-style.md](./house-style.md). +1. Check `fonts/` directory for `.woff2` files matching the family name → use `@font-face` with local files +2. Check if the font is on Google Fonts (IBM Plex Sans, Inter, etc.) → use `` tag directly +3. Check the design.md's "Font Substitutes" section → use the first recommended substitute +4. If none found, warn: "The design specifies [font name] but no font files found. Add .woff2 files to `fonts/` or I'll fall back to [closest built-in alternative]." -**design.md defines the brand. It does not define video composition rules.** Those come from [references/video-composition.md](references/video-composition.md) and [house-style.md](./house-style.md). Use brand colors at video-appropriate scale — not at web-UI opacity. +**The design file defines the brand. It does not define video composition rules.** Those come from [references/video-composition.md](references/video-composition.md) and [house-style.md](./house-style.md). Use brand colors at video-appropriate scale — not at web-UI opacity. ### Step 2: Prompt expansion @@ -476,6 +512,7 @@ Skip on small edits (fixing a color, adjusting one duration). Run on new composi - **[references/techniques.md](references/techniques.md)** — 11 visual techniques with code patterns: SVG drawing, Canvas 2D, CSS 3D, kinetic type, Lottie, video compositing, typing effect, variable fonts, MotionPath, velocity transitions, audio-reactive. Read when planning techniques per beat. - **[references/narration.md](references/narration.md)** — Pacing, tone, script structure, number pronunciation, opening line patterns. Read when the composition includes voiceover or TTS. - **[references/design-picker.md](references/design-picker.md)** — Create a design.md via visual picker. Read when no design.md exists and the user wants to create one. +- **[references/shader-backgrounds.md](references/shader-backgrounds.md)** — GLSL shader patterns for contextual scene backgrounds. Noise library, domain warping, pattern recipes by context (travel, medical, tech, industrial). Read when building shader backgrounds for compositions or the design picker. - **[visual-styles.md](visual-styles.md)** — 8 named visual styles with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating design.md. - **[house-style.md](house-style.md)** — Default motion, sizing, and color palettes when no design.md is specified. - **[patterns.md](patterns.md)** — PiP, title cards, slide show patterns. diff --git a/skills/hyperframes/house-style.md b/skills/hyperframes/house-style.md index 892c2ebed..3994c02d2 100644 --- a/skills/hyperframes/house-style.md +++ b/skills/hyperframes/house-style.md @@ -32,9 +32,9 @@ If the content genuinely calls for one of these — centered layout for a solemn ## Background Layer -Every scene needs visual depth — persistent decorative elements that stay visible while content animates in. Without these, scenes feel empty during entrance staggering. +Decoratives add depth when a scene needs it — during entrance staggering, during holds, or when the content is sparse and the frame feels flat. They're a tool, not a requirement. -Ideas (mix and match, 2-5 per scene): +Ideas: - Radial glows (accent-tinted, low opacity, breathing scale) - Ghost text (theme words at 3-8% opacity, very large, slow drift) @@ -42,9 +42,7 @@ Ideas (mix and match, 2-5 per scene): - Grain/noise overlay, geometric shapes, grid patterns - Thematic decoratives (orbit rings for space, vinyl grooves for music, grid lines for data) -All decoratives should have slow ambient GSAP animation — breathing, drift, pulse. Static decoratives feel dead. - -**Decorative count vs motion count.** The "2-5 per scene" count refers to decorative _elements_. If a project's `design.md` says "single ambient motion per scene", it means one looping motion applied to these decoratives (a shared breath/drift/pulse) — not one element total. A scene with 4 decoratives sharing one breathing motion is correct; a scene with 1 decorative is under-dressed. +Use as many or as few as the scene earns. A data-dense scene with 4 decoratives and shared breathing motion is rich. A single word on a black frame with no decoration is powerful. Both are valid — the question is whether the density matches the emotional beat. ## Motion diff --git a/skills/hyperframes/references/design-html-templates.md b/skills/hyperframes/references/design-html-templates.md new file mode 100644 index 000000000..68e331461 --- /dev/null +++ b/skills/hyperframes/references/design-html-templates.md @@ -0,0 +1,108 @@ +# Design HTML Template Extraction + +A design.html with embedded templates contains TWO separate design systems. Mix them up and the output looks nothing like the reference. This guide prevents that. + +## The Two Systems + +Every design.html with a template gallery has: + +| Layer | What it is | Where it lives | Fonts | Use for | +| ------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------- | +| **Page chrome** | The design system document itself — headers, swatches, specimen rows, rules grid | ` +``` + +```html + +``` + +```html + +``` + +**Placeholders only.** Use `{{label}}`, `{{headline}}`, `{{body}}`, `{{number}}`, `{{text}}` — never real example copy. Real copy adds 5–10kb of bloat per template and confuses the downstream agent. + +The runtime script that injects + scales the gallery should be the last thing in ``: + +```html + +``` + +The `.tmpl-thumb` has `aspect-ratio: 16/9` and `overflow: hidden`. The `.scale-wrap` is exactly `1920px × 1080px` with `transform-origin: top left`. The script reads each thumb's width and applies the matching scale. + +If the system has a shader background (Three.js + GLSL), keep the GLSL in collapsible `
` + `
` / `
` blocks. The runtime reads them at startup — the GLSL is documentation AND code.
+
+---
+
+## 9. Voice — write like the system speaks
+
+Most failed showcases are technically correct but mute. The voice of every visible string — section numbers, manifesto, do/don't bullets, endcap — must sound like the system's character.
+
+| Character       | Section numbers          | Manifesto tone                                              | Endcap                           |
+| --------------- | ------------------------ | ----------------------------------------------------------- | -------------------------------- |
+| Loud editorial  | `01 — palette`           | "type is _image_. accent is _environment_."                 | "end." (huge, lowercase, accent) |
+| Scholarly       | `i — Palette`            | "the page is _record_. type is _voice_."                    | "_fin._" (italic, on paper)      |
+| Brutalist       | `▶ 01 / PALETTE`         | "borders are _structural_. shadows are _weight_."           | "END OF FILE." (caps, accent)    |
+| Playful         | `~ palette ~`            | "_yes please._ / _no thank you._" (with handwritten asides) | "thanks everyone!" (handwritten) |
+| Industrial B2B  | `01 — Palette`           | "the page is a _document_. type is _precise_."              | "Thank you." (small, restrained) |
+| Win95 retro     | `Palette.exe — 01 of 06` | "Surfaces are _3D_. Chrome is _structural_."                | "Shut Down." (in a window)       |
+| 8-bit arcade    | `01 // PALETTE`          | "type is _display_. color is _signal_."                     | "GAME OVER." (blinking)          |
+| Activist poster | `★ 01 / PALETTE`         | "the page is a _protest_. type is _volume_."                | "JOIN US." (caps, accent)        |
+
+The mono labels everywhere should also speak in voice. A scholarly system writes `// folio · pp. 04 / 16`. A brutalist system writes `▶ CARD · EXAMPLE`. An arcade writes `▸ PRESS START`. The mono is the system's footnote voice — let it carry character too.
+
+---
+
+## 10. Concrete failure modes and fixes
+
+| Failure                                                 | Fix                                                                                                                                                         |
+| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Neutral grey-on-white page chrome                       | Re-skin every section in the system's palette. The body background isn't neutral — it's `--secondary` or `--primary`.                                       |
+| Cover headline at 48–64px                               | Push to `clamp(80px, 15vw, 280px)` minimum. The cover dominates or fails.                                                                                   |
+| Accent used as a 12% background wash everywhere         | Reserve accent for solid hits: one swatch, one section-head highlight, one cover-foot cell, one stat. Power comes from repetition + restraint, not opacity. |
+| Filled example copy inside slide gallery                | Replace with `{{placeholders}}`.                                                                                                                            |
+| Same section-head treatment six times in a row          | Vary: cover huge, manifesto on a slab, palette normal, type paired with a label. Pulse the rhythm.                                                          |
+| More than 4 palette tokens at the top                   | Compress to 4. Move extras into `.ds-slide-frame` as `--c-pink`, `--c-yellow` costume vars.                                                                 |
+| Reveal-on-scroll animations bolted on                   | Cut them. They bloat the file.                                                                                                                              |
+| Identical 12-section showcase regardless of system      | Match section count to system density. Loud poster system needs 5 sections; scholarly system uses all 9.                                                    |
+| Display font is Inter or Helvetica or system-ui         | Pick a memorable display face from §3.                                                                                                                      |
+| All cards drop-shadowed identically                     | Brutalist: offset solid. Quiet: none. Modern B2B: 4% opacity. Don't mix philosophies.                                                                       |
+| Manifesto reads like marketing                          | Rewrite in the system's voice. Three clauses, two italicized emphasis words, fits in 22–30ch.                                                               |
+| Endcap is a meta block with the same chrome as the rest | Make it a single huge word at maximum scale, on accent or inverted canvas. Mirror the cover.                                                                |
+
+---
+
+## 11. Process (the actual sequence)
+
+1. **Read the source completely.** Don't skim. Note palette, fonts, mood words, slide variants.
+2. **Write the one-sentence character.** If hazy, re-read.
+3. **Place on the six axes.** Loud/quiet, hard/soft, modern/retro, sans/serif, single/multi, editorial/industrial.
+4. **Pick three fonts.** Display by character (§3), body to pair, mono usually JetBrains.
+5. **Write the `:root` block.** Four palette tokens, three fonts, derived `--ink-dim`/`--hairline`, padding clamps.
+6. **Write the `template-css` block.** All slide variants with internal alias vars (`--c-bg`, `--c-fg`, `--c-accent`) so slides re-skin cleanly.
+7. **Build the outer chrome — embodying the system at every step.** Sticky rail → cover → manifesto → palette → type → surface → motion → guidelines → templates → endcap.
+8. **Re-read the cover.** If you only saw the cover, would you keep going? If no, the cover is too quiet.
+9. **Re-read the manifesto.** Could this sentence work as a slide in a real deck made with this system? If no, the voice is wrong.
+10. **Mental squint test.** Imagine this showcase next to a generic startup landing page. Are they unmistakably different objects? If they look the same, the chrome isn't committing.
+
+---
+
+## 12. The three tests that catch failures
+
+Before delivering, apply these three:
+
+1. **Cover test.** Could a stranger glance at the cover and describe the system in one sentence? If they'd only see "design system," your cover is mute.
+2. **Voice test.** Read the manifesto out loud. Does it sound like a sentence the system itself would speak? Or does it sound like generic design-system copy?
+3. **Squint test.** Squint at the page so type blurs. The shapes and colors alone should communicate the system's character — loud color blocks, quiet hairlines, soft tilted cards, hard grid lines. If squinting reveals a generic page structure, the chrome isn't committing to the system.
+
+A showcase that passes all three feels like a portfolio piece. A showcase that fails any of them feels like a spec sheet.
+
+---
+
+## 13. Mining the source for slide templates — the BMW M test
+
+The default 7-template set (cover, chapter, statement, stats, quote, list, end) is a _floor_. A great showcase mines the source document for system-specific components and turns each one into its own slide variant. If the source mentions a `spec-cell`, the slide gallery has a `slide--spec` template. If it mentions a `motorsport-photo-card`, the gallery has a `slide--photo-band` template. **One signature component in the source = one slide variant in the gallery.**
+
+Common system-specific components that must become slide variants:
+
+| Source component pattern                            | Slide variant                                                                        | Why                                                                                 |
+| --------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
+| "Photo band" / "hero photo" / "full-bleed image"    | `slide--photo-band`                                                                  | A 16:9 placeholder block + overlay caption. Full-bleed inside the 1920×1080 canvas. |
+| "Spec cell" / "spec table" / numbered metrics grid  | `slide--spec`                                                                        | 3 or 4-up grid of large numbers (`{{value}}` at 96px+) with mono labels below       |
+| "Model card" / "product card" / 3-up image cards    | `slide--lineup`                                                                      | Three columns of photo placeholder + name + caption + accent link                   |
+| "Magazine grid" / "article cards" / editorial cards | `slide--magazine`                                                                    | Photo + category tag + title + excerpt, 2- or 3-up                                  |
+| "Configurator" / "comparison" / option pickers      | `slide--compare`                                                                     | Two-column with swatches/options on left, summary on right                          |
+| "Motorsport / racing / hero feature"                | `slide--feature`                                                                     | Large photo placeholder with single overlay headline                                |
+| "Ledger" / "spec table rows" / 2-column data        | `slide--ledger`                                                                      | Hairline-divided rows with key/value pairs                                          |
+| "CTA band" / pre-footer photo CTA                   | `slide--cta`                                                                         | Photo placeholder + centered headline + outlined button                             |
+| Tricolor stripe / signature divider                 | Used **inside** other slides, not its own variant — a structural marker at slide top |
+
+For the BMW M source: `cover`, `statement`, `spec`, `stat`, `quote`, `ledger`, `split`, `end` is good — but it should also include `photo-band` (full-bleed car image), `lineup` (3-up model cards), and `motorsport-feature` (full-bleed photo with overlay caption). Eight slides is the floor for a system this rich; ten to twelve is the target.
+
+### Photography-led systems get photo placeholders
+
+When the source says "photography is the brand voice" / "full-bleed automotive photography fills entire bands" / "cars are the visual subject," **every slide except the pure-statement slide gets a photo placeholder zone.** Render the placeholder as a styled empty block:
+
+```css
+.photo-placeholder {
+  width: 100%;
+  aspect-ratio: 16/9;
+  background: linear-gradient(135deg, var(--c-surface-elevated), var(--c-surface-card));
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+.photo-placeholder::before {
+  content: "// PHOTOGRAPHY";
+  font-family: var(--c-f-mono);
+  font-size: 14px;
+  letter-spacing: 0.18em;
+  color: var(--c-dim);
+  opacity: 0.5;
+}
+.photo-placeholder::after {
+  content: "";
+  position: absolute;
+  inset: 24px;
+  border: 1px solid var(--c-dim);
+  opacity: 0.3;
+}
+```
+
+This signals to the downstream agent: _replace this block with a real photo when filling slides._ Without it, the gallery reads as content-light and the system's photographic voice is invisible.
+
+### Placeholder content must match the system's domain
+
+Generic `+42%` is wrong for an automotive system. Mine the source for what the system actually talks about and use that as the placeholder text. For BMW M:
+
+| Slide variant | Domain-appropriate `{{placeholder}}`                    |
+| ------------- | ------------------------------------------------------- |
+| Stat          | `{{number}}` = `523HP` / `3.2s` / `305KM/H` / `1,470KG` |
+| Spec cell     | label = `0–100 KM/H`, value = `3.2s`                    |
+| Ledger        | rows like `ENGINE → 4.4L V8 TWINPOWER`                  |
+| Quote         | `"M IS NOT A LETTER. IT'S A LANGUAGE."`                 |
+| Cover         | `BMW M.` / `THE ULTIMATE DRIVING MACHINE.`              |
+| Photo caption | `// M4 CSL · NÜRBURGRING · 2024`                        |
+
+For Vellum (scholarly journal): `{{number}}` becomes `42 folios` not `42%`. For Studio (design agency): `{{number}}` becomes `2003` (founded year) or `12 awards`. For 8-Bit Orbit (arcade): `{{number}}` becomes `999,999` or `LEVEL 7`.
+
+The skill should explicitly read the source's example copy + glossary and reuse those phrases as placeholder text in the slide gallery. Generic numbers signal that the agent didn't actually read the source.
+
+---
+
+## 14. Surface ladder — 4 tokens is the contract, but the system needs more
+
+The four sacred palette tokens (paper / canvas / chrome / accent) are the contract. But many systems — especially industrial, automotive, luxury, financial — have a _surface ladder_ of 3–5 dark/light surfaces that step up from canvas. The skill should extract these from the source and put them in `template-css` under `--c-surface-*` aliases:
+
+```css
+.ds-slide-frame {
+  --c-bg: var(--secondary); /* canvas — true black or true white */
+  --c-surface-soft: #0d0d0d; /* one notch above canvas */
+  --c-surface-card: #1a1a1a; /* card surface */
+  --c-surface-elevated: #262626; /* one more notch */
+  --c-carbon: #2b2b2b; /* domain-specific (BMW: carbon-fiber) */
+  --c-hairline: #3c3c3c; /* divider tone */
+  --c-fg: var(--primary);
+  --c-fg-2: #bbbbbb; /* body text */
+  --c-fg-3: #7e7e7e; /* muted, captions */
+  --c-accent: var(--accent);
+}
+```
+
+The four `--primary/--secondary/--tertiary/--accent` at the `:root` level stay sacred for the downstream agent's re-skinning. The surface ladder lives inside `.ds-slide-frame` as costume — the agent reading template CSS gets the full vocabulary, but a simple re-theming only needs to touch the four root tokens.
+
+**When to extract a surface ladder:** any time the source mentions more than two dark or two light surface colors (e.g., `canvas` + `surface-soft` + `surface-card` + `surface-elevated`). Industrial / luxury / automotive systems almost always have this. Editorial / scholarly systems often don't.
+
+### Body text ladder
+
+Same principle for type colors. The source likely names `ink` + `body` + `body-strong` + `muted`. Map these to `--c-fg` / `--c-fg-2` / `--c-fg-3` inside the slide frame. Use them in slide CSS:
+
+- `.headline` uses `--c-fg` (pure white/black)
+- `.lead` / body uses `--c-fg-2` (slightly muted)
+- `.caption` / metadata uses `--c-fg-3` (very muted)
+
+This is what makes slides feel hierarchically considered rather than flat.
+
+---
+
+## 15. Signature elements — find the one decorative thing
+
+Every system has one decorative element that isn't type or palette. The skill must hunt for it explicitly:
+
+- **Broadside**: nothing decorative — type IS the decoration
+- **BlockFrame**: chunky offset shadows
+- **Vellum**: drop caps + `§` marginalia glyphs
+- **Daisy Days**: hand-drawn daisy SVGs scattered as decoration
+- **8-Bit Orbit**: CRT scanlines + pixel glow
+- **Retro Windows**: bevels + title bars
+- **Sakura Chroma**: diagonal ribbon stripes
+- **Pin & Paper**: tilted safety-pin SVGs
+- **Scatterbrain**: sticky-note tilt + offset shadow
+- **BMW M**: the M tricolor stripe (`#0066b1 → #1c69d4 → #e22718`)
+
+The signature element appears in 3 places in the showcase, at minimum:
+
+1. Inside the brand mark on the nav rail (small)
+2. As a divider between major sections — replacing or accompanying the hairline `border-top` on `.section-head`
+3. As a structural marker inside relevant slide templates (e.g., a 4px stripe at the top of the cover slide, a hairline accent rule on chapter slides)
+
+For BMW M specifically: the M tricolor stripe must appear:
+
+- Below the brand mark in the nav rail
+- As a 4px-tall divider between the cover and the manifesto, and between the manifesto and palette
+- At the top of every slide in the template gallery (`.m-stripe-slide`)
+
+Without the signature element, the showcase looks like a generic dark system. With it consistently placed, the showcase reads as unmistakably BMW M.
+
+### How to encode signature elements
+
+Inline as a tiny reusable HTML pattern + CSS class. The BMW stripe is six lines of CSS and one `
` element that can be dropped anywhere: + +```css +.m-stripe { + height: 4px; + background: linear-gradient( + 90deg, + var(--m-blue-light) 0% 33%, + var(--m-blue-dark) 33% 66%, + var(--m-red) 66% 100% + ); +} +``` + +Then place it three times in the page. Cheap to add, identity-defining. + +--- + +## 16. The BMW M case — what was missing and how to fix + +Looking at a typical first-pass BMW M showcase: + +- ✅ Black canvas, white type, M red accent — correct +- ✅ Saira Condensed for display, Inter Light for body — correct +- ✅ M tricolor stripe present — correct +- ✅ Zero radius, hairline borders — correct +- ❌ Slide gallery uses generic stat (`+42%`) instead of automotive (`523HP`, `3.2s`) +- ❌ No `slide--photo-band` template even though "photography is the brand voice" +- ❌ No `slide--lineup` for the 3-up model card grid +- ❌ Surface ladder collapsed to 4 tokens; `surface-soft` / `surface-card` / `surface-elevated` missing from `template-css` +- ❌ Body color ladder collapsed; everything uses `--c-fg`, no `--c-fg-2` / `--c-fg-3` +- ❌ Heritage BMW blue (`#1c69d4`) missing — should be in `--c-bmw-blue` inside `.ds-slide-frame` +- ❌ Photo placeholders missing on cover, spec, split, and (added) photo-band templates +- ❌ Placeholder copy in templates is generic instead of automotive + +A great BMW M showcase has all six of those failures fixed. The skill should drive the agent to actively look for each. + +--- + +## 17. The four-test gauntlet — run before delivering + +Before declaring a showcase done, run these four tests in order: + +1. **Cover test** (§12.1) — can a stranger glance at the cover and describe the system in one sentence? +2. **Voice test** (§12.2) — does the manifesto sound like the system speaks, or like generic design-system copy? +3. **Squint test** (§12.3) — does the page shape alone communicate the system's character? +4. **Mine test** (§13) — has every signature component from the source become a slide variant? Has every placeholder been replaced with domain-appropriate copy? Has the signature element been placed at least three times? + +A showcase that passes 4/4 is portfolio-worthy. 3/4 is good. 2/4 or fewer is a spec sheet. diff --git a/skills/hyperframes/references/figma-to-design-html.md b/skills/hyperframes/references/figma-to-design-html.md new file mode 100644 index 000000000..a741ad3d7 --- /dev/null +++ b/skills/hyperframes/references/figma-to-design-html.md @@ -0,0 +1,272 @@ +# Figma to DESIGN.html + +Extract a design system from a Figma file and generate a bespoke DESIGN.html. This is the lossless path — exact hex values, font weights, border radii, padding, and shadow definitions come directly from the Figma API, not approximated from screenshots. + +## Prerequisites + +- Figma MCP tools available (`get_design_context`, `search_design_system`), OR +- Figma REST API access via personal access token + `curl` + +## When to use + +- User provides a Figma URL (`figma.com/design/:fileKey/...`) +- User says "use this Figma file" or "extract from Figma" +- User has a Figma design system file and wants a DESIGN.html for video composition + +## Extraction process + +### Step 1: Get the file structure + +**Via MCP:** + +``` +get_design_context(fileKey, nodeId="0:1", excludeScreenshot=true) +``` + +**Via REST API (if MCP rate-limited):** + +```bash +curl -s -H "X-Figma-Token: $TOKEN" \ + "https://api.figma.com/v1/files/$FILE_KEY?depth=1" +``` + +This returns the page list. Identify the relevant pages by name: + +| Look for | Contains | +| ------------------------------------ | ------------------------------------------ | +| "Colours", "Colors", "Palette" | Color swatches with hex values | +| "Typography", "Type", "Fonts" | Font specimens with family/weight/size | +| "Buttons", "Components" | Button components with radius/padding/fill | +| "Brand Guidelines", "Brand Overview" | Complete brand summary | +| "Grids", "Shadows", "Elevation" | Spacing system and shadow definitions | +| "Logo", "Brand Mark" | Logo treatment | +| "Icons", "Iconography" | Icon style | + +### Step 2: Extract colors + +Fetch the Colors page at depth 6-8: + +```bash +curl -s -H "X-Figma-Token: $TOKEN" \ + "https://api.figma.com/v1/files/$FILE_KEY/nodes?ids=$COLORS_PAGE_ID&depth=8" +``` + +Walk the node tree. For each node with `type: "RECTANGLE"` or `"ELLIPSE"` that has solid fills, extract: + +- The `color` RGBA values → convert to hex: `#${Math.round(r*255).toString(16)}...` +- The parent/ancestor `name` for the color role (e.g., "Primary", "Green Grass", "Info Blue") + +Group colors by their section headings (found in adjacent TEXT nodes). Map to the 4-role model: + +| Figma section | → Picker role | +| ------------------------------ | ------------- | +| Primary brand color / main CTA | `--accent` | +| Background / canvas | `--secondary` | +| Body text / ink / dark neutral | `--primary` | +| Muted / secondary text / gray | `--tertiary` | + +### Step 3: Extract typography + +Fetch the Typography page at depth 6: + +```bash +curl -s -H "X-Figma-Token: $TOKEN" \ + "https://api.figma.com/v1/files/$FILE_KEY/nodes?ids=$TYPE_PAGE_ID&depth=6" +``` + +For each TEXT node, extract from the `style` object: + +- `fontFamily` — the exact font name +- `fontWeight` — numeric weight (400, 500, 600, 700, 800, 900) +- `fontSize` — in px +- `lineHeightPx` — line height in px (compute ratio: `lineHeightPx / fontSize`) +- `letterSpacing` — in px (convert to em: `letterSpacing / fontSize`) + +Build the hierarchy table from the token names (found in adjacent TEXT nodes like "Title/XXL", "Text/M", "Tag/S"). + +**Font resolution:** Check if the extracted fonts are on Google Fonts. If yes, use directly. If proprietary, check `fonts/` directory for `.woff2` files, then fall back to the design.md's substitute recommendations. + +### Step 4: Extract surface properties + +Fetch the Buttons/Components page at depth 6: + +```bash +curl -s -H "X-Figma-Token: $TOKEN" \ + "https://api.figma.com/v1/files/$FILE_KEY/nodes?ids=$BUTTONS_PAGE_ID&depth=6" +``` + +For each COMPONENT or INSTANCE node, extract: + +- `cornerRadius` — border-radius in px +- `paddingTop/Right/Bottom/Left` — padding values +- `itemSpacing` — gap between children +- `strokeWeight` — border width +- `strokes[].color` — border color +- `fills[].color` — background color +- `effects[]` — shadow definitions (type, color, offset, radius, spread) + +Build the radius scale from unique `cornerRadius` values across all components. +Build the spacing scale from unique padding/gap values. +Identify the shadow tier(s) — often 0 (flat), 1 (subtle), or 2 (elevated). + +### Step 5: Craft the DESIGN.html + +With the extracted tokens, follow [design-showcase.md](design-showcase.md): + +1. Write the one-sentence character description +2. Place on the 6 character axes +3. Map colors to the 4-token model (`--primary`, `--secondary`, `--tertiary`, `--accent`) +4. Set the extracted fonts (display + body from the Figma type page) +5. Set surface properties (radius scale, padding, gap, shadow from components) +6. Build each section styled to the brand's character — using the EXACT values from Figma + +**Critical:** Use the extracted values verbatim. Don't round `cornerRadius: 12.0` to `12px` and then decide "that's close to 16px." The Figma API gives exact values — use them. + +### Step 6: Export components as SVG + +The Figma REST API exports any node as a lossless SVG: + +```bash +curl -s -H "X-Figma-Token: $TOKEN" \ + "https://api.figma.com/v1/images/$FILE_KEY?ids=$NODE_ID1,$NODE_ID2&format=svg" +``` + +Returns `{ "images": { "node:id": "https://...s3.amazonaws.com/..." } }`. Download each URL — the SVG contains exact shapes, colors, text-as-paths, strokes. Truly lossless vector exports. + +**What to export:** + +- Logo/brand mark — the most important asset. Always export as SVG. +- Illustrations — character art, decorative graphics, scene illustrations. These define the brand's visual personality in video frames. Export as SVG when vector; note dimensions for raster. +- Icons — line icons, filled icons, emoji sets. Export as SVG for inline use. +- Graphics & shapes — decorative elements, patterns, dividers, ornaments. Export as SVG. +- Tags/badges/pills — visual label treatments. Export as SVG. + +**Prioritize visual assets over UI components.** A brand's illustrations, icons, and decorative graphics appear directly in video frames. UI components (buttons, inputs, forms) inform the CSS vocabulary but don't appear as-is in video — there's nothing to click. + +**Always prefer SVG format.** SVG exports from Figma contain exact shapes, colors, and strokes as vector paths. They can be: + +- Inlined directly in composition HTML +- Scaled to any size without quality loss +- Color-modified via CSS (fill, stroke attributes) +- Animated via GSAP (path morphing, drawing, transforms) + +**SVG color tokenization:** Figma exports SVGs with hardcoded hex fills (`fill="#88E655"`). For the picker's palette controls to affect SVGs in slides, replace hardcoded colors with `currentColor` and set the color via CSS vars on the parent container: + +```html + + + + Label + + + +
+ + + Label + +
+``` + +For multi-color SVGs, use CSS class selectors on SVG elements: + +```css +.brand-fill { + fill: var(--tp-accent); +} +.ink-fill { + fill: var(--tp-primary); +} +.ink-stroke { + stroke: var(--tp-primary); +} +``` + +For raster-only assets (photographs, complex textures), export as PNG at 2x and note in the DESIGN.html that the asset requires a file reference rather than inline SVG. + +### Step 7: Build slides as component references + +The slides in `summary.html` are **component references for the composition agent**. Each slide shows actual Figma components at 1920×1080 scale. The agent copies the slide's HTML/SVG structure and swaps content — no guessing. + +**Inline the exported SVGs** directly in the slide HTML. The agent reads the SVG source to extract exact CSS values or uses the SVG as-is. + +| Slide | Shows | Source | +| ----- | --------------------------------------------------------------- | ------------------------------------------------------- | +| 1 | Hero frame using the brand's illustrations + headline treatment | Inline SVG illustrations from Step 6, fonts from Step 3 | +| 2 | Feature/product frame with inline SVG graphics as hero imagery | SVG graphics/shapes from Step 6 | +| 3 | Data/stats frame with numbers in the brand's display weight | Fonts + colors from extraction | +| 4 | Split layout with inline SVG icons + body content | SVG icons from Step 6, layout from Step 4 | +| 5 | Quote/testimonial frame | Fonts from Step 3 | +| 6 | Dark closing frame with logo SVG | Logo SVG from Step 6 | + +**The slides are VIDEO FRAMES, not component catalogues.** There are no buttons in video — nothing to click. The Figma's visual assets (illustrations, icons, graphics, logo) appear directly in the slides as inline SVGs. The CSS vocabulary (radius, padding, stroke, fill) styles the frame elements. The agent sees a complete video frame and reproduces its structure. + +**Asset priority for slides:** + +1. Illustrations and character art — these ARE the brand in video +2. Icons and graphic shapes — decorative elements and data visualization +3. Logo — appears in hero and closing frames +4. Color treatment — the palette applied to containers and backgrounds +5. Typography — headline scale and weight at video size + +**Via MCP (if available):** Call `get_design_context` on individual component nodes for full HTML/CSS. Adapt to vanilla HTML. + +**Via REST API:** Export as SVG for lossless vector components. Use node properties for CSS values. + +## REST API reference + +### File structure + +``` +GET /v1/files/:key?depth=1 +→ { document: { children: [{ id, name, type:"CANVAS" }] } } +``` + +### Node details + +``` +GET /v1/files/:key/nodes?ids=:id1,:id2&depth=N +→ { nodes: { ":id1": { document: { ...node tree... } } } } +``` + +### Key node properties for extraction + +| Property | What it gives you | +| ------------------------------ | ------------------------------------------------------- | +| `fills[].color.{r,g,b,a}` | Background color (0-1 floats → multiply by 255 for hex) | +| `strokes[].color` | Border color | +| `strokeWeight` | Border width | +| `cornerRadius` | Border-radius | +| `paddingTop/Right/Bottom/Left` | CSS padding | +| `itemSpacing` | CSS gap | +| `effects[].type` | `DROP_SHADOW`, `INNER_SHADOW`, `BACKGROUND_BLUR` | +| `effects[].color` | Shadow color | +| `effects[].offset.{x,y}` | Shadow offset | +| `effects[].radius` | Shadow blur | +| `effects[].spread` | Shadow spread | +| `style.fontFamily` | Font family name | +| `style.fontWeight` | Font weight (numeric) | +| `style.fontSize` | Font size in px | +| `style.lineHeightPx` | Line height in px | +| `style.letterSpacing` | Letter spacing in px | +| `characters` | Text content | + +### Authentication + +```bash +curl -H "X-Figma-Token: $PERSONAL_ACCESS_TOKEN" "https://api.figma.com/v1/..." +``` + +Generate a personal access token at: figma.com → Settings → Personal access tokens + +## Integration with HyperFrames skill + +This guide runs during Step 1 (Design system) when the user provides a Figma URL: + +1. Extract file structure → identify color/type/component pages +2. Pull each page via REST API or MCP +3. Extract tokens (colors, fonts, radii, padding, shadows) +4. Craft DESIGN.html via [design-showcase.md](design-showcase.md) using exact Figma values +5. Craft `summary.html` slides using Figma component CSS +6. Offer the picker for fine-tuning +7. Continue to Step 2 (prompt expansion) diff --git a/skills/hyperframes/references/prompt-expansion.md b/skills/hyperframes/references/prompt-expansion.md index ab504eee1..e177a3fc5 100644 --- a/skills/hyperframes/references/prompt-expansion.md +++ b/skills/hyperframes/references/prompt-expansion.md @@ -21,9 +21,9 @@ If `design.md` doesn't exist yet, run Step 1 (Design system) first. Expansion wi Even a detailed 7-scene brief lacks things only the expansion adds: -- **Atmosphere layers per scene** (required 2–5 from house-style: radial glows, ghost type, hairline rules, grain, thematic decoratives) — the user's prompt almost never lists these; expansion adds them. -- **Secondary motion for every decorative** — breath, drift, pulse, orbit. A decorative without ambient motion feels dead. -- **Micro-details that make a scene feel real** — registration marks, tick indicators, monospace coord labels, typographic accents, code snippets in the background, grid patterns. Things the user didn't think to request. +- **Atmosphere where the scene earns it** — a radial glow, ghost text, or grain texture can add depth. But a scene built around one powerful element doesn't need decoration. Add atmosphere to serve the mood, not to fill emptiness. +- **Motion that serves pacing** — ambient motion keeps scenes alive during the breathe phase. But stillness is also a tool. Not every element needs to move. +- **Micro-details sparingly** — registration marks, monospace labels, structural rules. These earn their place in data-dense or technical scenes. In a contemplative scene, they're clutter. - **Transition choreography at the object level** — not "crossfade" but "X expands outward and becomes Y". Specific duration, ease, and morph source/target. - **Pacing beats within each scene** — where tension builds, where a hold lets the viewer breathe, where the accent word lands. - **Exact hex values, typography parameters, ease choices** from design.md — no vagueness left for the scene subagent to guess. @@ -47,7 +47,7 @@ Expand into a full production prompt with these sections: 4. **Per-scene beats** — for each scene, use the beat-direction format: - **Concept** — the big idea in 2-3 sentences. What visual WORLD? What metaphor? What should the viewer FEEL? - **Mood direction** — cultural/design references, not hex codes. ("Bauhaus color studies", "cinematic title sequence", "editorial calm") - - **Depth layers** — BG (2-5 decoratives with ambient motion), MG (content), FG (accents, structural elements, micro-details). 8-10 total elements per scene per video-composition.md. + - **Depth layers** — BG (atmosphere), MG (content), FG (accents). Density matches the beat — a SLAM might have one element; a proof scene might have a data grid. Don't pad sparse scenes with decoratives just to hit a count. - **Animation choreography** — specific verbs per element. High: SLAMS, CRASHES. Medium: CASCADE, SLIDES. Low: floats, types on, counts up. Every element gets a verb. If you can't name the verb, the element is not yet designed. - **Transition out** — shader or CSS, with specific type and parameters. Not "crossfade" but "blur crossfade, 0.4s, power2.inOut." diff --git a/skills/hyperframes/references/shader-backgrounds.md b/skills/hyperframes/references/shader-backgrounds.md new file mode 100644 index 000000000..bb861b450 --- /dev/null +++ b/skills/hyperframes/references/shader-backgrounds.md @@ -0,0 +1,173 @@ +# Shader Backgrounds + +WebGL fragment shaders as living, breathing scene backgrounds. Each composition can have a contextual shader that renders behind the content. + +## Noise Library + +Include this in every shader. It provides gradient noise (no grid artifacts), FBM, and domain warping. + +```glsl +precision highp float; +uniform float u_time; +uniform vec2 u_res; + +vec2 h2(vec2 p) { + p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); + return -1.0 + 2.0 * fract(sin(p) * 43758.5453); +} + +float gn(vec2 p) { + vec2 i = floor(p), f = fract(p); + vec2 u = f*f*f*(f*(f*6.0-15.0)+10.0); // quintic smoothstep + return mix( + mix(dot(h2(i), f), dot(h2(i+vec2(1,0)), f-vec2(1,0)), u.x), + mix(dot(h2(i+vec2(0,1)), f-vec2(0,1)), dot(h2(i+vec2(1,1)), f-vec2(1,1)), u.x), + u.y + ); +} + +// FBM with rotated octaves — prevents directional banding +float fbm(vec2 p) { + float v = 0.0, a = 0.5; + mat2 r = mat2(0.8, 0.6, -0.6, 0.8); + for (int i = 0; i < 6; i++) { v += a * gn(p); p = r * p * 2.0 + vec2(1.7, 9.2); a *= 0.5; } + return v; +} + +// Lighter FBM for secondary detail +float fbm4(vec2 p) { + float v = 0.0, a = 0.5; + mat2 r = mat2(0.8, 0.6, -0.6, 0.8); + for (int i = 0; i < 4; i++) { v += a * gn(p); p = r * p * 2.0 + vec2(1.7, 9.2); a *= 0.5; } + return v; +} +``` + +## Domain Warping + +The single most important technique for natural-looking shaders. Feed noise into noise: + +```glsl +// Single warp — organic but recognizable as noise +vec2 q = vec2(fbm(uv * 2.0 + vec2(t * 0.3, t * 0.1)), + fbm(uv * 2.0 + vec2(t * 0.1, t * 0.4) + 5.0)); +float f = fbm(uv * 1.5 + q * 1.5); + +// Double warp — painterly, flowing, no visible noise pattern +vec2 q = vec2(fbm(uv * 2.0 + vec2(t * 0.3, t * 0.1)), + fbm(uv * 2.0 + vec2(t * 0.1, t * 0.4) + 5.0)); +vec2 r = vec2(fbm(uv * 3.0 + q * 3.0 + vec2(1.7, t * 0.15)), + fbm(uv * 3.0 + q * 3.0 + vec2(8.3, t * 0.2))); +float f = fbm(uv * 2.0 + r * 1.5); + +// Triple warp — truly organic, like real fluid dynamics +vec2 q = ...; // as above +vec2 r = ...; // as above +vec2 s = vec2(fbm(uv * 1.0 + r * 2.0 + vec2(3.1, t * 0.05)), + fbm(uv * 1.0 + r * 2.0 + vec2(6.7, t * 0.03))); +float f = fbm(uv * 1.5 + s * 1.5); +``` + +## Shader Patterns by Context + +### Warm / Travel / Lifestyle + +**Gradient Mesh** — flowing color blobs: + +```glsl +// 5 blobs with gaussian falloff, noise-warped edges +vec2 c1 = vec2(0.3 + sin(t*0.7)*0.15, 0.7 + cos(t*0.5)*0.12); +float w1 = exp(-pow(length(uv - c1 + warp) * 2.5, 2.0)); +// ... repeat for each blob, weighted blend of colors +col = (color1*w1 + color2*w2 + ...) / (w1+w2+...+0.001); +``` + +**Warm Caustics** — golden light on cream: + +```glsl +// Double domain warp, then map to warm palette +float c = smoothstep(0.2, 0.8, f * 0.5 + 0.5); +vec3 col = mix(cream, gold, pow(c, 2.0) * 0.3); +``` + +**Sunset Horizon** — sky gradient + clouds + water reflection. Use 5+ color stops for the sky, FBM clouds with bottom-lit coloring, water below with noise-broken sun reflection. + +### Medical / Emergency / PSA + +**Biohazard Pulse** — expanding rings from center: + +```glsl +for (int i = 0; i < 3; i++) { + float phase = fract(t * 0.3 + float(i) * 0.33); + float ring = abs(dist - phase * 0.7) * 30.0; + col += dangerColor * exp(-ring*ring) * (1.0 - phase) * 0.4; +} +``` + +**Clinical Sterile** — bright white + scan line + faint grid: + +```glsl +float scanY = fract(t * 0.04); +float scan = exp(-abs(uv.y - scanY) * 100.0) * 0.08; +float grid = max( + smoothstep(0.49, 0.5, fract(uv.x * 60.0)), + smoothstep(0.49, 0.5, fract(uv.y * 35.0)) +); +``` + +**Microscopic** — floating particles with membrane rings: + +```glsl +float cell = exp(-pd*pd/(size*size)) * 0.2; +float membrane = exp(-pow(abs(pd-size)*40.0, 2.0)) * 0.15; +``` + +**Respiratory** — breathing cycle with UV warp: + +```glsl +float breath = sin(t * 0.8) * 0.5 + 0.5; +vec2 dir = uv - center; +vec2 warped = center + dir * (1.0 + breath * 0.04 * exp(-length(dir) * 2.0)); +``` + +### Tech / Data / Product + +**Data Grid** — subtle flowing grid with accent-colored intersections. Use `fract()` for grid lines, noise for line brightness variation. + +**Terminal Glow** — dark base with green/blue accent scan. Monospace character rain (use noise thresholds to place bright dots in grid positions). + +### Industrial / Manufacturing + +**Heat Distortion** — UV warp increasing toward bottom: + +```glsl +float heat = pow(1.0 - uv.y, 1.5); +warped.x += sin(uv.y * 30.0 + t * 8.0) * 0.003 * heat; +``` + +**Warning Stripe** — diagonal hazard bands at edges: + +```glsl +float stripe = sin((uv.x + uv.y) * 40.0) > 0.3 ? 1.0 : 0.0; +float bandMask = max(smoothstep(0.88, 0.92, uv.y), smoothstep(0.12, 0.08, uv.y)); +``` + +## Integration + +### In the picker + +Each architecture's `preview_frames` can include a `` element with inline GLSL. The canvas renders behind the frame content. Use the picker's accent color as a uniform. + +### In the composition + +The design.md `## Background` section describes the shader for the composition agent to implement. Include: + +- Effect name and visual description +- Key GLSL techniques used (domain warp level, pattern type) +- Color palette mapping (which design.md colors map to which shader roles) +- Movement speed and character +- How the shader integrates with content (behind, overlay, mask) + +### In rendering + +Shaders use `u_time` as their only time input — compatible with GSAP timeline seeking. The capture engine advances `u_time` per frame for deterministic output. No `Math.random()`, no `Date.now()`. diff --git a/skills/hyperframes/references/video-composition.md b/skills/hyperframes/references/video-composition.md index e0758eba0..87197add2 100644 --- a/skills/hyperframes/references/video-composition.md +++ b/skills/hyperframes/references/video-composition.md @@ -12,15 +12,15 @@ design.md defines what the brand looks like: colors, fonts, personality, constra ## Density -A beat with 3 elements looks empty. A beat with 8-10 feels alive. +Density is a creative choice, not a minimum. A single number filling the frame can be more powerful than 10 elements competing for attention. An empty frame with one element appearing creates tension a busy frame can't. -Every scene needs: +Think in layers, not counts: -- **Background texture** — radial glow, oversized ghost type, color panel, grain, grid. Never solid flat color. -- **Midground content** — the actual message. Cards, stats, code blocks, images. -- **Foreground accents** — dividers, labels, data bars, registration marks, monospace metadata. The details that make it feel produced, not generated. +- **Background** — texture, color, atmosphere. Can be a solid color if that serves the scene. +- **Content** — the actual message. Could be one word or a data table. +- **Accents** — structural elements that guide the eye. Optional — not every scene needs them. -Aim for 8-10 visual elements per scene. Two of those should be decorative elements the user didn't ask for — you add them because empty frames look broken. +The default failure mode is too sparse (flat background, centered text, no depth). But the overcorrection — cramming decoratives into every frame — is equally bad. Match density to the scene's emotional beat: high-energy scenes earn more elements; contemplative moments earn fewer. ## Color Presence @@ -49,14 +49,14 @@ If you're writing a font-size under 24px in a video composition, justify it. If Subtle reads as static at 30fps. Err toward more movement than feels safe. -- Every decorative element should have ambient motion: breathe, drift, pulse, orbit. Static decoratives feel dead. +- Decorative elements usually need ambient motion: breathe, drift, pulse, orbit. But deliberate stillness after motion is powerful — don't animate something just because it exists. - Vary motion per scene — don't repeat the same ambient pattern. - Scene entrances should use 3+ different eases and directions. If every element enters from `y: 30, opacity: 0`, the scene has no choreography. ## Frame Composition -- **Two focal points minimum.** The eye needs somewhere to travel. -- **Fill the frame.** Hero text: 60-80% of frame width. +- **Focal hierarchy.** The eye needs to know where to land first. Sometimes that's two competing elements; sometimes it's one dominating element with nothing else. Both work — what doesn't work is everything at equal weight. +- **Use the frame.** Content can fill 80% of the width or occupy one corner — both are valid compositions. What looks broken is content floating in the center with equal margins on all sides, the default web layout. - **Anchor to edges.** Pin content to left/top or right/bottom. Centered-and-floating is a web layout pattern. - **Split frames.** Data panel left, content right. Top bar with metadata, full-width below. Zone-based layouts over centered stacks. - **Structural elements.** Rules, dividers, border panels. They create visual paths and animate well (`scaleX: 0` → `1`). diff --git a/skills/hyperframes/scripts/build-design-picker.py b/skills/hyperframes/scripts/build-design-picker.py new file mode 100644 index 000000000..a182956a5 --- /dev/null +++ b/skills/hyperframes/scripts/build-design-picker.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python3 +"""Build design-picker.html with template data, moodboard data, and optional design.md integration. + +Usage: + python3 build-design-picker.py \ + --template skills/hyperframes/templates/design-picker.html \ + --templates-dir /tmp/beautiful-html-templates/templates \ + --presentations-dir skills/hyperframes/templates/presentations \ + --output .hyperframes/pick-design.html \ + < picker-data.json + +If design.md or DESIGN.md exists in cwd, parses it and generates a +'user-design' template card that appears first in the picker grid with +auto-advance to Phase 2. + +picker-data.json must contain: + { + "architectures": [...], "palettes": [...], "typepairs": [...], + "moodboards": [...], "prompt": {...}, "prompt_text": {...}, + "prompt_desc": "..." + } +""" +import json, sys, os, re, shutil, argparse + + +def parse_design_md(path): + """Extract brand name, colors, and font substitute from a design.md file.""" + with open(path) as f: + md = f.read() + + def find_color(pattern): + m = re.search(pattern, md, re.IGNORECASE) + return m.group(1) if m else None + + primary = find_color(r'colors\.primary\}.*?(#[0-9a-fA-F]{6})') or \ + find_color(r'primary[^#]{0,40}(#[0-9a-fA-F]{6})') + canvas = find_color(r'canvas[^#]{0,40}(#[0-9a-fA-F]{6})') + ink = find_color(r'ink\}`[^#]*[—–-]\s*`?(#[0-9a-fA-F]{6})') or \ + find_color(r'\bink\b[^#]{0,40}(#[0-9a-fA-F]{6})') + muted = find_color(r'charcoal[^#]{0,40}(#[0-9a-fA-F]{6})') or \ + find_color(r'graphite[^#]{0,40}(#[0-9a-fA-F]{6})') or \ + find_color(r'muted[^#]{0,40}(#[0-9a-fA-F]{6})') + + p_text = ink or '#1a1a1a' + p_bg = canvas or '#ffffff' + p_muted = muted or '#636363' + p_accent = primary or '#024ad8' + + font_match = re.search(r'single-family.*?:\s*\*?\*?([^*(]+?)(?:\*\*|\s*\()', md, re.IGNORECASE) + font_name = font_match.group(1).strip() if font_match else None + + subs = re.findall(r'^\s*[-*]\s*\*\*([^*]+)\*\*\s+at weights', md, re.MULTILINE) + font_sub = subs[0].strip() if subs else 'Inter' + + brand_match = re.search(r'(\w+)\s+reads like', md) + if not brand_match: + brand_match = re.search(r"(\w+)'s\s+\w+\s+surfaces?\b", md) + if not brand_match: + brand_match = re.search(r'^##?\s+(.+)', md, re.MULTILINE) + brand_name = brand_match.group(1).strip() if brand_match else 'Your Design' + if brand_name.lower() in ('overview', 'colors', 'typography', 'layout'): + brand_name = 'Your Design' + + return { + 'brand': brand_name, + 'primary': p_text, + 'secondary': p_bg, + 'tertiary': p_muted, + 'accent': p_accent, + 'font': font_sub, + 'font_original': font_name, + } + + +def generate_user_design(design_md_data, output_dir, base_design_html): + """Generate template.html and design.html for the user-design virtual template.""" + d = design_md_data + os.makedirs(output_dir, exist_ok=True) + + bg = d["secondary"] + fg = d["primary"] + mt = d["tertiary"] + ac = d["accent"] + fn = d["font"] + br = d["brand"] + fnlink = fn.replace(" ", "+") + + template_html = f''' + + + + + + + + + + +
+
+
+
{br} · Design System
+

{br}.

+

Visual identity for video compositions. Palette, type, motion, and surface tokens — configured and ready to render.

+
+
Explore System
+
Export
+
+
+
+
+
+
+
Primary{fg}
+
Secondary{bg}
+
Tertiary{mt}
+
Accent{ac}
+
+
+
+
+ + +
+
+
+
+
New
+
+
+
{br} · Featured
+

The product headline goes here at scale

+

Supporting copy describes the feature, announcement, or product at comfortable reading size for video.

+
+
4KResolution
+
12hrBattery
+
1.2kgWeight
+
+
+
Learn More
+
Compare
+
+
+
+
+ + +
+
{br} · By the Numbers
+

Key metrics.

+
+
10B+
Devices shipped
Across consumer, enterprise, and education markets worldwide.
+
175
Countries served
Global operations with localized supply chains.
+
99.9%
Uptime SLA
Enterprise-grade reliability for managed fleets.
+
+
+ + +
+
+
+
03 · Composition
+

Split frame.

+

Content on left, data on right. The accent color marks exactly one focal element per frame — the stat number.

+
+
Primary
+
Secondary
+
+
+
+
10B+
Devices shipped
+
175
Countries served
+
+
+
+ + +
+
"
+

Design at the speed of decision.

+
— {br} Design System
+
+
+ + +
+
{br}
+

Ready to
build.

+

Tokens configured. Export the design system and start composing video frames.

+
+
Create DESIGN.html
+
Browse Templates
+
+
+ + +''' + + with open(os.path.join(output_dir, 'template.html'), 'w') as f: + f.write(template_html) + + # Copy design.html and summary.html only if they don't already exist. + # If the agent crafted them, don't overwrite with the generic template. + presentations_parent = os.path.dirname(os.path.dirname(base_design_html)) + dst_design = os.path.join(output_dir, 'design.html') + if not os.path.exists(dst_design): + user_design_src = os.path.join(presentations_parent, 'user-design', 'design.html') + if os.path.exists(user_design_src): + shutil.copy2(user_design_src, dst_design) + else: + shutil.copy2(base_design_html, dst_design) + dst_summary = os.path.join(output_dir, 'summary.html') + if not os.path.exists(dst_summary): + user_summary_src = os.path.join(presentations_parent, 'user-design', 'summary.html') + if os.path.exists(user_summary_src): + shutil.copy2(user_summary_src, dst_summary) + + +def main(): + parser = argparse.ArgumentParser(description='Build design-picker.html') + parser.add_argument('--template', required=True, help='Path to design-picker.html template') + parser.add_argument('--templates-dir', required=True, help='Path to beautiful-html-templates/templates') + parser.add_argument('--presentations-dir', default=None, help='Path to presentations dir with design.html files') + parser.add_argument('--output', required=True, help='Output path for built picker') + args = parser.parse_args() + + data = json.load(sys.stdin) + + index_path = os.path.join(os.path.dirname(args.templates_dir), 'index.json') + with open(index_path) as f: + index = json.load(f) + + script_dir = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, script_dir) + from importlib import util as imp_util + build_tp_path = os.path.join(script_dir, 'build-template-picker.py') + spec = imp_util.spec_from_file_location('build_tp', build_tp_path) + build_tp = imp_util.module_from_spec(spec) + spec.loader.exec_module(build_tp) + + # Parse design.md + design_md_data = None + for name in ('design.md', 'DESIGN.md'): + if os.path.exists(name): + design_md_data = parse_design_md(name) + print(f"Parsed {name}: {design_md_data['brand']} — " + f"{design_md_data['secondary']}/{design_md_data['primary']}/" + f"{design_md_data['accent']}/{design_md_data['tertiary']} — " + f"font: {design_md_data['font']}") + break + + # Generate user-design template + if design_md_data: + output_parent = os.path.dirname(os.path.abspath(args.output)) + ud_dir = os.path.join(output_parent, '..', 'templates', 'user-design') + ud_dir = os.path.normpath(ud_dir) + base_design = None + if args.presentations_dir: + base_design = os.path.join(args.presentations_dir, 'block-frame', 'design.html') + if not base_design or not os.path.exists(base_design): + base_design = os.path.join(os.path.dirname(args.template), 'presentations', 'block-frame', 'design.html') + if os.path.exists(base_design): + generate_user_design(design_md_data, ud_dir, base_design) + print(f"Generated {ud_dir}/ (template.html + design.html)") + else: + print(f"Warning: base design.html not found at {base_design}") + design_md_data = None + + # Extract template data + templates = [] + for t in index['templates']: + html_path = os.path.join(args.templates_dir, t['slug'], 'template.html') + if not os.path.exists(html_path): + continue + preview = build_tp.extract_preview(html_path, t['slug']) + templates.append({ + 'slug': t['slug'], + 'name': t['name'], + 'tagline': t['tagline'], + 'scheme': t['scheme'], + 'density': t['density'], + 'colorVars': build_tp.extract_color_vars(html_path), + 'preview_html': preview + }) + + # Prepend user-design + if design_md_data: + d = design_md_data + ud_html = os.path.join(output_parent, '..', 'templates', 'user-design', 'template.html') + ud_html = os.path.normpath(ud_html) + preview = build_tp.extract_preview(ud_html, 'user-design') if os.path.exists(ud_html) else '' + templates.insert(0, { + 'slug': 'user-design', + 'name': d['brand'] + ' Design', + 'tagline': 'Your provided design system from design.md', + 'scheme': 'light' if d['secondary'].lower() in ('#ffffff', '#fff') else 'dark', + 'density': 'normal', + 'colorVars': [], + '_provided': True, + 'preview_html': preview, + }) + + # Read template + with open(args.template) as f: + html = f.read() + + # Inject placeholders + html = html.replace('__ARCHITECTURES_JSON__', json.dumps(data['architectures'])) + html = html.replace('__PALETTES_JSON__', json.dumps(data['palettes'])) + html = html.replace('__TYPEPAIRS_JSON__', json.dumps(data['typepairs'])) + html = html.replace('__MOODBOARDS_JSON__', json.dumps(data['moodboards'])) + html = html.replace('__PROMPT_JSON__', json.dumps(data['prompt'])) + html = html.replace('__TEMPLATES_JSON__', json.dumps(templates)) + html = html.replace('__PROMPT_TEXT_JSON__', json.dumps(data['prompt_text'])) + html = html.replace('__PROMPT_DESC__', data.get('prompt_desc', '')) + + # Set base href relative to output location so template paths resolve correctly + output_dir = os.path.dirname(os.path.abspath(args.output)) + project_root = os.getcwd() + rel = os.path.relpath(project_root, output_dir) + base_href = rel.rstrip('/') + '/' if rel != '.' else './' + html = html.replace('__BASE_HREF__', base_href) + + # Inject DESIGN_MD + dm_json = json.dumps(design_md_data) if design_md_data else 'null' + html = html.replace( + 'var PROMPT_TEXT = ', + f'var DESIGN_MD = {dm_json};\n var PROMPT_TEXT = ' + ) + + os.makedirs(os.path.dirname(args.output), exist_ok=True) + with open(args.output, 'w') as f: + f.write(html) + + print(f"Written {args.output} ({len(templates)} templates, design_md={'yes' if design_md_data else 'no'})") + + +if __name__ == '__main__': + main() diff --git a/skills/hyperframes/scripts/build-design-templates.py b/skills/hyperframes/scripts/build-design-templates.py new file mode 100644 index 000000000..8d7f0debc --- /dev/null +++ b/skills/hyperframes/scripts/build-design-templates.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python3 +"""Generate design.html for each template by reading its CSS tokens. + +The template's own design system (fonts, colors, borders, shadows, spacing) +becomes the page's visual language. Same HTML structure, CSS derived from tokens. + +Usage: + python3 build-design-templates.py [templates-dir] +""" +import re, os, sys, json + + +def parse_root_vars(css): + """Extract :root CSS custom properties into a dict.""" + m = re.search(r':root\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}', css, re.DOTALL) + if not m: + return {} + block = m.group(1) + props = {} + for pm in re.finditer(r'--([\w-]+)\s*:\s*([^;]+);', block): + props[pm.group(1)] = pm.group(2).strip() + return props + + +def classify_scheme(props): + """Determine if template is dark or light based on bg color.""" + bg = props.get('tp-secondary', props.get('c-bg', '#000000')) + bg = re.search(r'#([0-9a-fA-F]{6})', bg) + if bg: + r = int(bg.group(1)[:2], 16) + return 'dark' if r < 128 else 'light' + return 'dark' + + +def extract_fonts(props): + """Extract font families from template tokens.""" + fonts = {} + for key in ['f-display', 'f-heading', 'f-head', 'f-body', 'f-mono']: + if key in props: + val = props[key] + fm = re.search(r'"([^"]+)"', val) + if fm: + fonts[key] = fm.group(1) + return fonts + + +def extract_border_style(props): + """Classify border treatment.""" + border = props.get('border', '') + shadow = props.get('shadow', props.get('shadow-sm', '')) + if '4px' in border or '3px' in border: + return 'heavy' + if '1px' in border: + return 'hairline' + if shadow and 'px' in shadow and 'blur' not in shadow.lower(): + return 'offset' + return 'subtle' + + +def extract_radius(props): + """Get corner radius.""" + r = props.get('radius', props.get('radius-sm', '0')) + m = re.search(r'(\d+)', r) + return int(m.group(1)) if m else 0 + + +def generate_page_css(scheme, fonts, border_style, radius, props): + """Generate the page design CSS from template tokens.""" + is_dark = scheme == 'dark' + heavy = border_style == 'heavy' + + disp = fonts.get('f-display', fonts.get('f-heading', fonts.get('f-head', 'system-ui'))) + body = fonts.get('f-body', 'system-ui') + mono = fonts.get('f-mono', 'monospace') + + if border_style == 'heavy': + border_rule = '4px solid var(--black,var(--secondary))' + shadow_rule = '8px 8px 0 var(--black,var(--secondary))' + shadow_sm = '4px 4px 0 var(--black,var(--secondary))' + swatch_hover = 'transform:translate(-3px,-3px);box-shadow:11px 11px 0 var(--black,var(--secondary))' + card_shadow = shadow_rule + elif border_style == 'hairline': + border_rule = '1px solid var(--hairline)' + shadow_rule = 'none' + shadow_sm = 'none' + swatch_hover = 'transform:translateY(-4px)' + card_shadow = '0 12px 36px rgba(0,0,0,.35)' + else: + border_rule = '1px solid rgba(128,128,128,.15)' + shadow_rule = '0 4px 16px rgba(0,0,0,.12)' + shadow_sm = '0 2px 8px rgba(0,0,0,.08)' + swatch_hover = 'transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,.15)' + card_shadow = shadow_rule + + r = f'{radius}px' if radius > 0 else '0' + + MINUS = '\\2212' + CHECK = '\\2713' + CROSS = '\\2717' + MDASH = '\\2014' + + css = f"""*,*::before,*::after{{box-sizing:border-box}} +html,body{{margin:0;padding:0;background:var(--secondary)}} +body{{color:var(--primary);font:400 14px/1.6 "{mono}",monospace;-webkit-font-smoothing:antialiased;overflow-x:hidden}} +::selection{{background:var(--accent);color:var(--secondary)}} + +#design-bg{{position:fixed;inset:0;width:100%;height:100%;z-index:-2;opacity:{'.55' if is_dark else '.85'};pointer-events:none}} +#bg-veil{{position:fixed;inset:0;z-index:-1;pointer-events:none;background:{'radial-gradient(ellipse at 30% 20%,transparent 0%,var(--secondary) 75%)' if is_dark else 'none'}}} + +.rail{{position:fixed;top:0;left:0;right:0;height:{'44px' if is_dark else '56px'};display:flex;align-items:center;justify-content:space-between;padding:0 var(--pad-x);background:{'color-mix(in srgb,var(--secondary) 78%,transparent)' if is_dark else 'var(--primary)'};{'backdrop-filter:blur(14px);' if is_dark else ''}border-bottom:{border_rule};{'box-shadow:' + shadow_sm + ';' if border_style == 'heavy' else ''}z-index:100;font:500 {'11' if is_dark else '12'}px/1 "{mono}",monospace;letter-spacing:.{'14' if is_dark else '06'}em;text-transform:uppercase}} +.rail .brand{{color:var(--accent);display:inline-flex;align-items:center;gap:10px;font:{'900 14px/1' if is_dark else '400 22px/1'} "{disp}",sans-serif}} +.rail .brand .dot{{width:{'9' if is_dark else '22'}px;height:{'9' if is_dark else '22'}px;background:var(--accent);{'border:3px solid var(--secondary);transform:rotate(-6deg)' if border_style == 'heavy' else ''}}} +.rail nav{{display:flex;gap:{'28' if is_dark else '6'}px}} +.rail nav a{{color:{'var(--primary)' if is_dark else 'var(--secondary)'};text-decoration:none;{'padding:6px 10px;border:2px solid transparent;' if border_style == 'heavy' else ''}transition:all .15s}} +.rail nav a:hover{{color:var(--accent){';background:var(--accent);border-color:var(--secondary)' if border_style == 'heavy' else ''}}} +@media(max-width:880px){{.rail nav{{display:none}}}} + +section{{position:relative;padding:var(--pad-y) var(--pad-x)}} +.section-head{{display:grid;grid-template-columns:{'11em' if border_style == 'heavy' else '9em'} 1fr;gap:clamp(24px,4vw,56px);align-items:{'end' if border_style == 'heavy' else 'baseline'};{'border-top:' + border_rule + ';padding-top:28px;' if border_style != 'heavy' else ''}margin-bottom:clamp(36px,5vh,64px)}} +.section-head .num{{font:{'600' if border_style == 'heavy' else '500'} 12px/1 "{mono}",monospace;letter-spacing:.{'1' if border_style == 'heavy' else '18'}em;text-transform:uppercase;color:var(--accent){';display:inline-block;border:3px solid var(--secondary);background:var(--primary);padding:6px 14px;box-shadow:' + shadow_sm + ';justify-self:start;transform:rotate(-3deg)' if border_style == 'heavy' else ''}}} +.section-head h2{{font:{'400' if border_style == 'heavy' else '900'} clamp({'48px,8vw,128px' if border_style == 'heavy' else '36px,6vw,84px'})/.{'88' if border_style == 'heavy' else '92'} "{disp}",sans-serif;letter-spacing:-.0{'1' if border_style == 'heavy' else '35'}em;margin:0;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'}}} +.section-head .lede{{grid-column:2;max-width:60ch;margin-top:18px;font-size:15px;line-height:1.6{'color:var(--primary);opacity:.65' if is_dark else ''}}} + +.cover{{{'background:var(--accent);color:var(--secondary)' if is_dark else 'background:var(--primary);color:var(--secondary)'}}};min-height:100vh;padding:{'80px' if is_dark else '110px'} var(--pad-x) {'48px' if is_dark else '60px'};display:{'grid;grid-template-rows:auto 1fr auto' if is_dark else 'flex;flex-direction:column;justify-content:center'}}} +.cover-headline{{{'align-self:end;' if is_dark else ''}margin:0;font:900 clamp({'80px,17vw,280px' if is_dark else '64px,13vw,200px'})/.{'82' if is_dark else '88'} "{disp}",sans-serif;letter-spacing:-.04em;text-transform:{'lowercase' if is_dark else 'uppercase'}}} +.cover-foot{{margin-top:{'48' if is_dark else '56'}px;padding-top:{'20' if is_dark else '28'}px;border-top:{border_rule};display:grid;grid-template-columns:repeat(4,1fr);gap:24px}} +.cover-foot .cell{{display:flex;flex-direction:column;gap:6px}} +.cover-foot .k{{font:500 {'10' if is_dark else '11'}px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;opacity:.55}} +.cover-foot .v{{font:500 13px/1.4 "{mono}",monospace}} +.cover-foot .v.big{{font:800 {'28' if is_dark else '32'}px/1 "{disp}",sans-serif;letter-spacing:-.02em}} +@media(max-width:760px){{.cover-foot{{grid-template-columns:1fr 1fr}}}} + +.manifesto{{padding:clamp(60px,9vh,120px) var(--pad-x);{'border-top:' + border_rule + ';border-bottom:' + border_rule if border_style == 'heavy' else 'border-bottom:' + border_rule}}} +.manifesto-grid{{display:grid;grid-template-columns:{'11em' if border_style == 'heavy' else '9em'} 1fr;gap:clamp(24px,4vw,56px);align-items:start;max-width:1400px;margin:0 auto}} +.manifesto-grid .num{{font:500 12px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}} +.manifesto p{{font:{'800' if border_style == 'heavy' else '700'} clamp(28px,{'4' if border_style == 'heavy' else '3.8'}vw,{'60' if border_style == 'heavy' else '56'}px)/1.05 "{disp}",sans-serif;letter-spacing:-.025em;max-width:22ch;margin:0;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'}}} +.manifesto p em{{font-style:normal;{'background:var(--secondary);color:var(--accent);padding:0 .15em' if border_style == 'heavy' else 'color:var(--accent)'}}} + +.palette{{display:grid;grid-template-columns:repeat(4,1fr);gap:{'24px' if border_style == 'heavy' else '2px'};{'background:rgba(128,128,128,.1);border:' + border_rule if border_style != 'heavy' else ''}}} +.swatch{{{'border:' + border_rule + ';box-shadow:' + shadow_rule + ';' if border_style == 'heavy' else ''}padding:28px 24px;aspect-ratio:3/4;display:flex;flex-direction:column;justify-content:space-between;border-radius:{r};transition:all .2s}} +.swatch:hover{{{swatch_hover}}} +.swatch .role{{font:500 11px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase{';padding:4px 8px;background:var(--primary);border:2px solid var(--secondary);align-self:flex-start' if border_style == 'heavy' else ''}}} +.swatch .name{{font:900 clamp(28px,3.4vw,48px)/.92 "{disp}",sans-serif;letter-spacing:-.025em;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'};margin-top:auto}} +.swatch .hex{{font:500 12px/1 "{mono}",monospace;margin-top:8px}} +.swatch .usage{{font:400 11px/1.5 "{mono}",monospace;margin-top:6px;opacity:.65}} +.swatch--primary{{background:var(--primary);color:var(--secondary)}} +.swatch--secondary{{background:var(--secondary);color:var(--primary)}} +.swatch--tertiary{{background:var(--tertiary);color:var(--primary)}} +.swatch--accent{{background:var(--accent);color:var(--secondary)}} +@media(max-width:880px){{.palette{{grid-template-columns:repeat(2,1fr)}}}} + +.specimen{{{'border:' + border_rule + ';background:var(--primary);color:var(--secondary);box-shadow:' + shadow_rule + ';' if border_style == 'heavy' else ''}max-width:1400px;margin:0 auto;display:flex;flex-direction:column}} +.spec-row{{display:grid;grid-template-columns:{'12em' if border_style == 'heavy' else '11em'} 1fr;gap:{'28' if border_style == 'heavy' else '32'}px;padding:28px {'32px' if border_style == 'heavy' else '0'};border-{'bottom:3px solid var(--secondary)' if border_style == 'heavy' else 'top:' + border_rule};align-items:baseline}} +.spec-row:{'last-child' if border_style == 'heavy' else 'first-child'}{{border:0;padding-top:0}} +.spec-row .meta{{font:500 11px/1.4 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;display:flex;flex-direction:column;gap:4px;{'color:var(--secondary);opacity:.6' if border_style == 'heavy' else 'color:var(--primary);opacity:.4'}}} +.spec-row .meta b{{color:var(--accent);font:400 22px/1 "{disp}",sans-serif}} +.spec-display{{font:900 clamp(56px,10vw,152px)/.88 "{disp}",sans-serif;letter-spacing:-.04em;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'}}} +.spec-h1{{font:800 clamp(40px,7vw,104px)/.9 "{disp}",sans-serif;letter-spacing:-.03em;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'}}} +.spec-h2{{font:700 clamp(28px,4.2vw,64px)/1 "{disp}",sans-serif;letter-spacing:-.02em}} +.spec-lead{{font:500 clamp(18px,2vw,26px)/1.45 "{disp}",sans-serif}} +.spec-body{{font:{'500' if border_style == 'heavy' else '400'} 17px/1.65 "{disp}",sans-serif;max-width:60ch}} +.spec-label{{font:{'600' if border_style == 'heavy' else '500'} 13px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase{';color:var(--accent)' if is_dark else ''}}} + +.surface-grid{{display:grid;grid-template-columns:1.1fr 1fr;gap:{'32' if border_style == 'heavy' else '48'}px;max-width:1400px;margin:0 auto}} +@media(max-width:880px){{.surface-grid{{grid-template-columns:1fr}}}} +.demo-card{{background:var(--primary);color:var(--secondary);border-radius:{r};padding:{'32' if border_style == 'heavy' else '28'}px;{'border:' + border_rule + ';' if border_style == 'heavy' else ''}box-shadow:{card_shadow};display:flex;flex-direction:column;gap:18px}} +.demo-card .tag{{align-self:flex-start;font:{'600' if border_style == 'heavy' else '500'} {'11' if border_style == 'heavy' else '10'}px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;{'border:3px solid var(--secondary);background:var(--accent);padding:6px 14px;box-shadow:' + shadow_sm if border_style == 'heavy' else 'color:var(--accent);border:1px solid var(--accent);padding:4px 8px'}}} +.demo-card h3{{margin:0;font:900 36px/1 "{disp}",sans-serif;letter-spacing:-.02em;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'}}} +.demo-card p{{margin:0;font:400 15px/1.6 "{disp}",sans-serif;opacity:.7}} +.tokens{{list-style:none;padding:0;margin:0;{'border:' + border_rule + ';background:var(--primary);color:var(--secondary);box-shadow:' + shadow_rule if border_style == 'heavy' else 'border-top:' + border_rule}}} +.tokens li{{display:grid;grid-template-columns:1fr auto auto;gap:18px;align-items:center;padding:18px {'24px' if border_style == 'heavy' else '0'};border-bottom:{'3px solid var(--secondary)' if border_style == 'heavy' else border_rule}}} +.tokens li:last-child{{border:0}} +.tokens .name{{font:{'600' if border_style == 'heavy' else '500'} 12px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase{'' if border_style == 'heavy' else ';opacity:.6'}}} +.tokens .val{{font:800 20px/1 "{disp}",sans-serif}} +.tokens .bar{{height:{'8' if border_style == 'heavy' else '6'}px;background:var(--accent){';border:2px solid var(--secondary)' if border_style == 'heavy' else ''}}} + +.two-col{{display:grid;grid-template-columns:1fr 1fr;gap:{'32' if border_style == 'heavy' else '48'}px;max-width:1400px;margin:0 auto}} +@media(max-width:880px){{.two-col{{grid-template-columns:1fr}}}} +.panel{{{'background:var(--primary);border:' + border_rule + ';box-shadow:' + shadow_rule if border_style == 'heavy' else 'border:' + border_rule + ';background:color-mix(in srgb,var(--secondary) 70%,transparent)'}}};padding:{'32' if border_style == 'heavy' else '36'}px;border-radius:{r};display:flex;flex-direction:column;gap:22px}} +.panel .label-row{{font:500 11px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;color:var(--accent){';display:inline-block;align-self:flex-start;background:var(--accent);color:var(--secondary);border:3px solid var(--secondary);padding:6px 14px;box-shadow:' + shadow_sm if border_style == 'heavy' else ''}}} +.panel h4{{margin:0;font:{'400 40px/1' if border_style == 'heavy' else '800 32px/1'} "{disp}",sans-serif;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'}}} +.panel p{{margin:0;font:{'500 14px/1.65' if border_style == 'heavy' else '400 13px/1.7'} "{disp}",sans-serif;{'max-width:42ch' if border_style == 'heavy' else 'opacity:.65;max-width:38ch'}}} + +.bg-preview{{border:{border_rule};{'box-shadow:' + shadow_rule + ';' if border_style == 'heavy' else ''}aspect-ratio:4/3;position:relative;overflow:hidden;background:var(--secondary);border-radius:{r}}} +.bg-preview canvas{{width:100%;height:100%;display:block}} +.bg-preview .badge{{position:absolute;bottom:{'14' if border_style == 'heavy' else '12'}px;left:{'14' if border_style == 'heavy' else '12'}px;font:{'600' if border_style == 'heavy' else '500'} {'11' if border_style == 'heavy' else '10'}px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;{'color:var(--secondary);background:var(--accent);border:3px solid var(--secondary);padding:6px 12px' if border_style == 'heavy' else 'background:color-mix(in srgb,var(--secondary) 75%,transparent);padding:6px 10px;backdrop-filter:blur(8px)'}}} + +details.code-block{{{'background:var(--primary);border:3px solid var(--secondary)' if border_style == 'heavy' else 'border:' + border_rule + ';background:color-mix(in srgb,var(--secondary) 60%,transparent)'}}};margin-top:12px;border-radius:{r}}} +details.code-block summary{{cursor:pointer;padding:{'12px 16px' if border_style == 'heavy' else '14px 18px'};list-style:none;font:{'600' if border_style == 'heavy' else '500'} {'12' if border_style == 'heavy' else '11'}px/1 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;{'display:flex;justify-content:space-between;background:var(--accent)' if border_style == 'heavy' else 'color:var(--accent);display:flex;justify-content:space-between'}}} +details.code-block summary::-webkit-details-marker{{display:none}} +details.code-block summary::after{{content:"+";font:900 18px/1 "{disp}",sans-serif{';color:var(--accent)' if border_style != 'heavy' else ''}}} +details.code-block[open] summary::after{{content:"{MINUS}"}} +details.code-block pre{{margin:0;padding:16px;border-top:{'3px solid var(--secondary)' if border_style == 'heavy' else border_rule};font:11px/1.6 "{mono}",monospace;{'background:var(--primary);color:var(--secondary);opacity:.7' if border_style == 'heavy' else 'color:var(--primary);opacity:.65;background:transparent'};overflow-x:auto;white-space:pre-wrap;word-break:break-word;max-height:320px;overflow-y:auto}} + +.rules-grid{{display:grid;grid-template-columns:1fr 1fr;gap:{'32' if border_style == 'heavy' else '48'}px;max-width:1400px;margin:0 auto}} +@media(max-width:880px){{.rules-grid{{grid-template-columns:1fr}}}} +.rules-col{{{'border:' + border_rule + ';padding:32px;box-shadow:' + shadow_rule if border_style == 'heavy' else ''}}} +.rules-col.do{{{'background:var(--accent);transform:rotate(-.6deg)' if border_style == 'heavy' else ''}}} +.rules-col.dont{{{'background:var(--primary);color:var(--secondary);transform:rotate(.6deg)' if border_style == 'heavy' else ''}}} +.rules-col h3{{font:{'400 64px/1' if border_style == 'heavy' else '900 56px/1'} "{disp}",sans-serif;letter-spacing:-.03em;margin:0 0 {'24' if border_style == 'heavy' else '28'}px;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'}}} +.rules-col.do h3{{color:var(--accent)}} +.rules-col.dont h3{{opacity:.4}} +.rules-col ul{{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:{'12' if border_style == 'heavy' else '0'}px}} +.rules-col li{{display:grid;grid-template-columns:2em 1fr;gap:{'12' if border_style == 'heavy' else '8'}px;padding:{'14px 16px' if border_style == 'heavy' else '16px 0'};{'background:var(--primary);border:3px solid var(--secondary);box-shadow:' + shadow_sm + ';' if border_style == 'heavy' else 'border-top:' + border_rule + ';'}font:500 {'15px/1.45' if border_style == 'heavy' else '18px/1.45'} "{disp}",sans-serif}} +.rules-col li::before{{font:{'900 22px/1' if border_style == 'heavy' else '800 22px/1.2'} "{disp}",sans-serif;align-self:{'center' if border_style == 'heavy' else 'start'};{'text-align:center;width:1.6em;height:1.6em;display:flex;align-items:center;justify-content:center;border:2px solid var(--secondary)' if border_style == 'heavy' else ''}}} +.rules-col.do li::before{{content:"{CHECK if border_style == 'heavy' else '+'}";{'background:var(--accent)' if border_style == 'heavy' else 'color:var(--accent)'}}} +.rules-col.dont li::before{{content:"{CROSS if border_style == 'heavy' else MDASH}";opacity:.4}} + +.templates-wrap{{{'background:var(--accent);border-top:' + border_rule + ';border-bottom:' + border_rule if border_style == 'heavy' else 'background:color-mix(in srgb,var(--secondary) 96%,var(--accent))'}}} +.templates-grid{{display:grid;grid-template-columns:repeat(3,1fr);gap:{'28' if border_style == 'heavy' else '24'}px;max-width:1400px;margin:0 auto}} +@media(max-width:1100px){{.templates-grid{{grid-template-columns:repeat(2,1fr)}}}} +@media(max-width:720px){{.templates-grid{{grid-template-columns:1fr}}}} +.tmpl{{background:{'var(--primary)' if border_style == 'heavy' else 'var(--secondary)'};{'border:' + border_rule + ';box-shadow:' + shadow_rule if border_style == 'heavy' else 'border:' + border_rule};overflow:hidden;border-radius:{r};transition:all .2s}} +.tmpl:hover{{{swatch_hover.replace('translate(-3px,-3px)', 'translate(-3px,-3px)') if border_style == 'heavy' else 'border-color:var(--accent);transform:translateY(-2px)'}}} +.tmpl-thumb{{width:100%;aspect-ratio:16/9;overflow:hidden;position:relative;background:{'var(--secondary)' if border_style == 'heavy' else '#050505'};border-bottom:{'3px solid var(--secondary)' if border_style == 'heavy' else border_rule}}} +.tmpl-thumb .scale-wrap{{width:1920px;height:1080px;transform-origin:top left}} +.tmpl-foot{{display:flex;justify-content:space-between;padding:{'12px 16px' if border_style == 'heavy' else '14px 18px'};font:{'600' if border_style == 'heavy' else '500'} {'12' if border_style == 'heavy' else '11'}px/1 "{mono}",monospace;letter-spacing:.{'08' if border_style == 'heavy' else '14'}em;text-transform:uppercase}} +.tmpl-foot .idx{{{'background:var(--accent);border:2px solid var(--secondary);padding:2px 8px' if border_style == 'heavy' else 'color:var(--accent)'}}} + +footer.endcap{{background:{'var(--secondary);color:var(--primary)' if border_style == 'heavy' else 'var(--accent);color:var(--secondary)'};padding:80px var(--pad-x) 60px;display:grid;grid-template-columns:1fr auto;gap:48px;align-items:end}} +footer.endcap .endmark{{font:900 clamp({'72px,14vw,220px' if border_style == 'heavy' else '60px,13vw,220px'})/.85 "{disp}",sans-serif;letter-spacing:-.04em;text-transform:{'uppercase' if border_style == 'heavy' else 'lowercase'};margin:0}} +footer.endcap .meta{{font:500 12px/2 "{mono}",monospace;letter-spacing:.1em;text-transform:uppercase;opacity:.6;text-align:right}} +footer.endcap .meta b{{opacity:1;color:var(--accent)}}""" + return css + + +def build_design_template(slug, props, scheme, fonts, border_style, radius, meta): + """Build the full design.html template with __TOKEN__ sentinels.""" + disp = fonts.get('f-display', fonts.get('f-heading', fonts.get('f-head', 'system-ui'))) + mono = fonts.get('f-mono', 'monospace') + is_dark = scheme == 'dark' + + page_css = generate_page_css(scheme, fonts, border_style, radius, props) + + font_families = set() + for f in fonts.values(): + font_families.add(f) + font_link_parts = [] + for f in sorted(font_families): + if f in ('system-ui', 'monospace', 'sans-serif', 'serif'): + continue + font_link_parts.append(f'family={f.replace(" ", "+")}:wght@300;400;500;600;700;800;900') + font_link = '&'.join(font_link_parts) + + return f''' + + + + +__NAME__ — Design System + + + + + + + + + +
+ +
+
__NAME__
+ +
+ +
+

__NAME_LINE1____NAME_LINE2__.

+
+
Template{slug}
+
Type{disp}
+
Accent__ACCENT_LABEL__
+
Doc07 sections · __SLIDE_COUNT__ templates
+
+
+ +
+
+
— Manifesto
+

{meta.get('tagline', 'design is intent.')}

+
+
+ +
+
+
01 — Palette
+

{'Four Tokens.' if border_style == 'heavy' else 'four colors.'}

+

__PALETTE_LEDE__

+
+
+
Primary
__PRIMARY_NAME__
__PRIMARY__
{'Ink on dark.' if is_dark else 'Canvas.'}
+
Secondary
__SECONDARY_NAME__
__SECONDARY__
{'Canvas.' if is_dark else 'Ink.'}
+
Tertiary
__TERTIARY_NAME__
__TERTIARY__
Muted. Borders.
+
Accent
__ACCENT_NAME__
__ACCENT__
Signal. Reserved.
+
+
+ +
+
+
02 — Typography
+

{'Big. Heavy.' if border_style == 'heavy' else 'type scale.'}

+
+
+
Display{disp}
{'HELLO WORLD.' if border_style == 'heavy' else 'hello world.'}
+
H1{disp}
{'CHAPTER ONE' if border_style == 'heavy' else 'chapter one'}
+
H2{disp}
Slide headlines.
+
Body{disp}

Body copy below 24px is forbidden in video compositions.

+
Label{mono}
// chrome · labels · metadata
+
+
+ +
+
+
03 — Surface
+

{'Borders. Shadows.' if border_style == 'heavy' else 'corners. depth.'}

+
+
+
+ // example +

{'EXAMPLE CARD.' if border_style == 'heavy' else 'example card.'}

+

Configured corners, padding, shadow.

+
+
    +
  • Corners__CORNER_RADIUS__
  • +
  • Padding__PADDING__
  • +
  • Gap__GAP__
  • +
  • Elevation__ELEVATION__
  • +
  • Density__DENSITY__
  • +
+
+
+ +
+
+
04 — Motion
+

__EASING_HEADLINE__

+

__EASING_LEDE__

+
+
+
+
// easing
+

__EASING_NAME__

+

__EASING_DESC__

+
__EASING_VALUE__
+
+
+
// duration
+

__DURATION_DEFAULT__

+

__DURATION_DESC__

+
__DURATION_VALUES__
+
+
+
+ +
+
+
05 — Background
+

__BG_HEADLINE__

+

__BG_LEDE__

+
+
+
// __BG_BADGE__
+
+
    +
  • Geometry__BG_TYPE__
  • +
  • Density__BG_DENSITY__
  • +
  • Speed__BG_SPEED__
  • +
  • Strength__BG_STRENGTH__
  • +
  • Grain__BG_GRAIN__
  • +
+
// shader config +
__SHADER_CONFIG__
+
+
// vertex shader +
__SHADER_VERTEX__
+
+
// fragment shader +
__SHADER_FRAGMENT__
+
+
+
+
+ +
+
+
06 — Guidelines
+

{"Do. Don’t." if border_style == 'heavy' else "do and don’t."}

+
+
+

{'DO.' if border_style == 'heavy' else 'do.'}

    __DOS__
+

{"DON'T." if border_style == 'heavy' else "don't."}

    __DONTS__
+
+
+ +
+
+
07 — Templates
+

__SLIDE_COUNT_WORD__
{'Frames.' if border_style == 'heavy' else 'frames.'}

+
+
+
+ + + +
+

{'THE END.' if border_style == 'heavy' else 'end.'}

+
+
__NAME__ · v1
+
{disp} / {mono}
+
__DATE__
+
+
+ + + +__SHADER_SCRIPT__ + + +''' + + +def main(): + templates_dir = sys.argv[1] if len(sys.argv) > 1 else "skills/hyperframes/templates/presentations" + index_path = os.path.join(os.path.dirname(templates_dir), "index.json") + + with open(index_path) as f: + index = json.load(f) + + for t in index['templates']: + slug = t['slug'] + summary_path = os.path.join(templates_dir, slug, 'summary.html') + design_path = os.path.join(templates_dir, slug, 'design.html') + + if not os.path.exists(summary_path): + continue + + # Skip hand-crafted designs + if os.path.exists(design_path): + size = os.path.getsize(design_path) + if size > 5000: + print(f" {slug}: SKIP (hand-crafted, {size//1024}K)") + continue + + with open(summary_path) as f: + summary = f.read() + + styles = re.findall(r']*>(.*?)', summary, re.DOTALL) + css = '\n'.join(styles) + props = parse_root_vars(css) + scheme = classify_scheme(props) + fonts = extract_fonts(props) + border_style = extract_border_style(props) + radius = extract_radius(props) + + meta = {'tagline': t.get('tagline', 'design is intent.')} + + design = build_design_template(slug, props, scheme, fonts, border_style, radius, meta) + + with open(design_path, 'w') as f: + f.write(design) + + print(f" {slug}: {scheme} / {border_style} / r{radius} / {list(fonts.values())[:2]}") + + print(f"\nGenerated design.html for {len(index['templates'])} templates") + + +if __name__ == '__main__': + main() diff --git a/skills/hyperframes/scripts/build-summaries.py b/skills/hyperframes/scripts/build-summaries.py new file mode 100644 index 000000000..56968628a --- /dev/null +++ b/skills/hyperframes/scripts/build-summaries.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +"""Build summary.html for each template — compact design system + layout skeletons. + +Output per template: +1. Minified CSS: :root tokens + base class definitions (no comments, no theme variants, + no animation keyframes, no nav/chrome engine CSS) +2. One HTML skeleton per unique slide type with {{placeholder}} content + +Usage: + python3 build-summaries.py [templates-dir] +""" +import re, os, sys, json + + +def extract_css(html, template_dir=None): + styles = re.findall(r']*>(.*?)', html, re.DOTALL) + if template_dir: + for m in re.finditer(r']*href="([^"]+\.css)"', html): + href = m.group(1) + if 'fonts.googleapis' in href: + continue + css_path = os.path.join(template_dir, href) + if os.path.exists(css_path): + with open(css_path) as f: + styles.append(f.read()) + return '\n'.join(styles) + + +def minify_css(css): + # Strip comments + css = re.sub(r'/\*.*?\*/', '', css, flags=re.DOTALL) + # Strip lines that are only whitespace + lines = [l for l in css.split('\n') if l.strip()] + css = '\n'.join(lines) + return css + + +def strip_theme_variants(css): + """Remove nested variant overrides but keep base .slide.dark/.slide.orange rules.""" + # Remove nested blocks like .orange .stat-value { ... } or .dark .muted { ... } + css = re.sub(r'\.(orange|dark|light)\s+\.[^\{]+\{[^}]*\}', '', css) + return css + + +def strip_animation_css(css): + """Remove @keyframes and animation-related rules.""" + css = re.sub(r'@keyframes\s+\w+\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}', '', css, flags=re.DOTALL) + css = re.sub(r'\[data-anim[^\]]*\]\s*\{[^}]*\}', '', css) + css = re.sub(r'\[data-delay[^\]]*\]\s*\{[^}]*\}', '', css) + css = re.sub(r'\.slide\.is-active\s+\[data-anim[^\]]*\]\s*\{[^}]*\}', '', css) + return css + + +def strip_nav_css(css): + """Remove navigation/chrome engine CSS.""" + nav_selectors = ['#nav-dots', '.nav-dot', '#slide-counter', '#deck'] + for sel in nav_selectors: + css = re.sub(re.escape(sel) + r'[^{]*\{[^}]*\}', '', css) + return css + + +def strip_engine_css(css): + """Remove slide engine mechanics (transitions, will-change).""" + # Remove generic .slide base positioning (agent writes its own) + # Keep .slide layout rules (grid-template-rows etc) + return css + + +def compress_whitespace(css): + """Collapse multiple blank lines, trim indentation.""" + # Reduce indentation to 2 spaces + lines = [] + for line in css.split('\n'): + stripped = line.strip() + if not stripped: + continue + # Detect indent level + indent = len(line) - len(line.lstrip()) + new_indent = min(indent // 4, 2) * 2 + lines.append(' ' * new_indent + stripped) + return '\n'.join(lines) + + +def extract_root_tokens(css): + """Extract :root { ... } block.""" + m = re.search(r':root\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}', css, re.DOTALL) + if m: + return ':root {\n' + m.group(1).strip() + '\n}' + return '' + + +def extract_class_definitions(css): + """Extract class definitions, excluding theme variants and animations.""" + css = re.sub(r':root\s*\{[^}]*\}', '', css, flags=re.DOTALL) + rules = [] + for m in re.finditer(r'([.#][\w-][^{]*)\{([^}]*)\}', css): + selector = m.group(1).strip() + body = m.group(2).strip() + # Skip theme variants + if re.match(r'\.(orange|dark|light)\s', selector): + continue + # Skip animation/nav + if any(k in selector for k in ['data-anim', 'data-delay', 'nav-dot', '#deck', '#nav', '#slide-counter', 'is-active']): + continue + # Skip keyframes (handled separately) + if '@keyframes' in selector: + continue + # Skip empty + if not body: + continue + rules.append(f'{selector} {{ {body} }}') + return '\n'.join(rules) + + +INNER_CONTAINERS = {'slide-body', 'slide-content', 'slide-chrome', 'slide-foot', + 'slide-counter', 'slides-container'} + + +def get_slide_type(cls): + """Extract slide type from class string.""" + for c in cls.split(): + if c.startswith('slide--'): + return c + if c.startswith('s-'): + return c + if c.startswith('slide-') and c not in INNER_CONTAINERS: + return c + for c in cls.split(): + if c.startswith('bg-'): + return c + if c not in ('slide', 'active', 'dark', 'light', 'hairlines'): + return c + return 'slide' + + +def has_slide_class(cls): + """Check if class string has 'slide' as a standalone class (not slide-body etc).""" + return 'slide' in cls.split() + + +def get_slides(html): + """Extract slide sections from any template structure.""" + html_flat = re.sub(r'\s+', ' ', html) + slides = [] + # Match
and
(standalone slide class only) + pattern = r'(<(?:section)\s+[^>]*class="([^"]+)"[^>]*>|]*class="([^"]+)"[^>]*>)' + for m in re.finditer(pattern, html_flat): + cls = m.group(2) or m.group(3) + if m.group(3) and not has_slide_class(cls): + continue + stype = get_slide_type(cls) + start = m.start() + tag = 'section' if ' 0 and pos < len(html_flat): + open_m = re.search(r'<' + tag + r'\b', html_flat[pos:]) + close_m = re.search(r'', html_flat[pos:]) + if close_m is None: + break + if open_m and open_m.start() < close_m.start(): + depth += 1 + pos += open_m.end() + else: + depth -= 1 + if depth == 0: + slides.append((stype, html_flat[start:pos + close_m.end()])) + pos += close_m.end() + return slides + + + +def strip_content(slide_html): + """Replace text content with {{placeholders}}, remove data-anim/style attrs.""" + result = re.sub(r'\s+data-anim="[^"]*"', '', slide_html) + result = re.sub(r'\s+data-delay="\d+"', '', result) + result = re.sub(r'\s+style="[^"]*"', '', result) + + def replace_text(m): + tag_open = m.group(1) + text = m.group(2).strip() + tag_close = m.group(3) + if not text or text.startswith('<') or text.startswith('{'): + return m.group(0) + cls = tag_open.lower() + if any(c in cls for c in ['display', '"h1', '"h2', '"h3', 'heading', 'title']): + return f'{tag_open}{{{{headline}}}}{tag_close}' + elif any(c in cls for c in ['lead', 'body', 'desc', 'lede', 'meta']): + return f'{tag_open}{{{{body}}}}{tag_close}' + elif any(c in cls for c in ['label', 'kicker', 'caption', 'muted', 'note', 'source']): + return f'{tag_open}{{{{label}}}}{tag_close}' + elif any(c in cls for c in ['stat-value', 'num', 'value', 'v "']): + return f'{tag_open}{{{{number}}}}{tag_close}' + elif len(text) < 30: + return f'{tag_open}{{{{text}}}}{tag_close}' + else: + return f'{tag_open}{{{{body}}}}{tag_close}' + result = re.sub(r'(<[^>]+>)([^<]+)(]+>)', replace_text, result) + # Remove HTML comments + result = re.sub(r'', '', result, flags=re.DOTALL) + # Collapse blank lines + result = re.sub(r'\n\s*\n+', '\n', result) + return result.strip() + + +def build_summary(html_path, slug): + with open(html_path) as f: + html = f.read() + + template_dir = os.path.dirname(html_path) + css = extract_css(html, template_dir) + + # Extract and compress CSS + root_tokens = extract_root_tokens(css) + clean_css = minify_css(css) + clean_css = strip_theme_variants(clean_css) + clean_css = strip_animation_css(clean_css) + clean_css = strip_nav_css(clean_css) + class_defs = extract_class_definitions(clean_css) + compressed_css = compress_whitespace(root_tokens + '\n' + class_defs) + + # Extract font links + font_links = re.findall(r']*fonts\.googleapis[^>]*>', html) + + # Extract unique slide skeletons + slides = get_slides(html) + + seen_types = set() + skeletons = [] + for stype, slide_html in slides: + if stype in seen_types: + continue + seen_types.add(stype) + skeleton = strip_content(slide_html) + skeletons.append((stype, skeleton)) + + # Build summary HTML + out = f'\n' + out += '\n\n' + for link in font_links: + out += link + '\n' + out += '\n' + for stype, skeleton in skeletons: + out += f'\n{skeleton}\n\n' + + return out + + +def main(): + templates_dir = sys.argv[1] if len(sys.argv) > 1 else "skills/hyperframes/templates/presentations" + index_path = os.path.join(os.path.dirname(templates_dir), "index.json") + + with open(index_path) as f: + index = json.load(f) + + total_saved = 0 + for t in index['templates']: + html_path = os.path.join(templates_dir, t['slug'], 'template.html') + if not os.path.exists(html_path): + continue + + summary = build_summary(html_path, t['slug']) + out_path = os.path.join(templates_dir, t['slug'], 'summary.html') + with open(out_path, 'w') as f: + f.write(summary) + + orig_size = os.path.getsize(html_path) + summ_size = len(summary) + pct = int((1 - summ_size / orig_size) * 100) + total_saved += orig_size - summ_size + print(f" {t['slug']}: {orig_size//1024}K → {summ_size//1024}K ({pct}% smaller)") + + print(f"\nTotal saved: {total_saved//1024}K across {len(index['templates'])} templates") + + +if __name__ == '__main__': + main() diff --git a/skills/hyperframes/scripts/build-template-picker.py b/skills/hyperframes/scripts/build-template-picker.py new file mode 100755 index 000000000..e1ad51f7f --- /dev/null +++ b/skills/hyperframes/scripts/build-template-picker.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Build a template picker HTML from the template and injected data. + +Usage: + python3 build-template-picker.py \ + --template skills/hyperframes/templates/template-picker.html \ + --templates-dir /path/to/beautiful-html-templates/templates \ + --output .hyperframes/template-picker.html \ + < data.json + +data.json must contain: + { "palettes": [...], "prompt_text": {...}, "prompt_desc": "..." } + +The script reads index.json from templates-dir parent, extracts CSS color vars +from each template, and injects all data into the HTML template. +""" +import json, sys, re, os, argparse + +def extract_color_vars(html_path): + with open(html_path) as f: + html = f.read() + root_match = re.search(r':root\s*\{([^}]+)\}', html) + if not root_match: + return [] + return [m[0] for m in re.findall(r'(--[\w-]+)\s*:\s*([^;]+)', root_match.group(1)) + if '#' in m[1] or 'rgb' in m[1]] + +def extract_preview(html_path, slug): + """Extract the first slide + scoped CSS as an inline preview HTML string.""" + with open(html_path) as f: + html = f.read() + + # Extract all ', html, re.DOTALL) + css = '\n'.join(styles) + + # Find the first slide + # Try: deck-stage > section, section.slide, div.slide, .slide + slide_html = "" + for pattern in [ + r'(]*class="[^"]*s-cover[^"]*"[^>]*>.*?
)', + r'(]*class="[^"]*slide[^"]*"[^>]*>.*?)', + r'(]*class="[^"]*slide[^"]*"[^>]*>.*?\s*(?=]*class="[^"]*slide|))', + ]: + m = re.search(pattern, html, re.DOTALL) + if m: + slide_html = m.group(1) + break + + if not slide_html: + # Fallback: grab the body content + body_match = re.search(r']*>(.*?)', html, re.DOTALL) + if body_match: + slide_html = body_match.group(1)[:3000] + + if not css and not slide_html: + return "" + + # Scope CSS: prefix all selectors with .tp-{slug} + scope_class = f"tp-{slug}" + scoped_css = css + # Replace :root with .tp-{slug} scope + scoped_css = scoped_css.replace(':root', f'.{scope_class}') + # Prefix other selectors (rough but effective) + # Replace top-level selectors that start with a letter, ., # or [ + lines = [] + for line in scoped_css.split('\n'): + stripped = line.strip() + # Skip @import, @font-face, @keyframes + if stripped.startswith('@') or stripped.startswith('}') or stripped.startswith('/*') or not stripped: + lines.append(line) + continue + # If line contains { and doesn't start with space (top-level selector) + if '{' in stripped and not line.startswith(' ') and not line.startswith('\t'): + # Prefix each selector before { + before_brace = stripped.split('{')[0] + after_brace = '{'.join(stripped.split('{')[1:]) + selectors = before_brace.split(',') + prefixed = ', '.join( + f'.{scope_class} {s.strip()}' if not s.strip().startswith(f'.{scope_class}') else s.strip() + for s in selectors + ) + lines.append(f' {prefixed} {{{after_brace}') + else: + lines.append(line) + scoped_css = '\n'.join(lines) + + # Strip any script tags from the slide HTML + slide_html = re.sub(r']*>.*?', '', slide_html, flags=re.DOTALL) + + # Build inline preview: scoped style + slide content + preview = ( + f'
' + f'' + f'{slide_html}' + f'
' + ) + + return preview + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--template', required=True) + parser.add_argument('--templates-dir', required=True) + parser.add_argument('--output', required=True) + args = parser.parse_args() + + data = json.load(sys.stdin) + + index_path = os.path.join(os.path.dirname(args.templates_dir), 'index.json') + with open(index_path) as f: + index = json.load(f) + + # Import structure extraction + script_dir = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, script_dir) + try: + from importlib import import_module + extract_mod = {} + exec(open(os.path.join(script_dir, 'extract-template-structure.py')).read().split('def main')[0], extract_mod) + extract_structure = extract_mod.get('extract_structure') + except Exception: + extract_structure = None + + templates = [] + for t in index['templates']: + html_path = os.path.join(args.templates_dir, t['slug'], 'template.html') + if not os.path.exists(html_path): + continue + preview = extract_preview(html_path, t['slug']) + entry = { + 'slug': t['slug'], + 'name': t['name'], + 'tagline': t['tagline'], + 'scheme': t['scheme'], + 'density': t['density'], + 'colorVars': extract_color_vars(html_path), + 'preview_html': preview + } + if extract_structure: + try: + entry['structure'] = extract_structure(html_path, t['slug']) + except Exception: + pass + templates.append(entry) + + with open(args.template) as f: + html = f.read() + + html = html.replace('__PALETTES_JSON__', json.dumps(data['palettes'])) + html = html.replace('__PROMPT_TEXT_JSON__', json.dumps(data['prompt_text'])) + html = html.replace('__TEMPLATES_JSON__', json.dumps(templates)) + html = html.replace('__MOTION_TEMPLATES_JSON__', json.dumps(data.get('motion_templates', []))) + html = html.replace('__PROMPT_DESC__', data.get('prompt_desc', '')) + + os.makedirs(os.path.dirname(args.output), exist_ok=True) + with open(args.output, 'w') as f: + f.write(html) + + print(f"Written to {args.output} ({len(templates)} templates)") + +if __name__ == '__main__': + main() diff --git a/skills/hyperframes/scripts/detokenize-summaries.py b/skills/hyperframes/scripts/detokenize-summaries.py new file mode 100644 index 000000000..f873479e9 --- /dev/null +++ b/skills/hyperframes/scripts/detokenize-summaries.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Replace {{placeholder}} tokens in summary.html with original text from template.html. + +For each template: +1. Parse template.html to extract slides and their text content +2. Parse summary.html to find skeleton slides with {{placeholder}} tokens +3. Match by slide type class (slide--cover, slide--chapter, slide-1, etc.) +4. Replace {{headline}}, {{body}}, {{text}}, {{number}}, {{label}} with original text + +Usage: + python3 detokenize-summaries.py [templates-dir] +""" +import re, os, sys, json + + +INNER_CONTAINERS = {'slide-body', 'slide-content', 'slide-chrome', 'slide-foot', + 'slide-counter', 'slides-container'} + + +def get_slide_type(cls): + """Extract slide type from class string.""" + for c in cls.split(): + if c.startswith('slide--'): + return c + if c.startswith('s-'): + return c + if c.startswith('slide-') and c not in INNER_CONTAINERS: + return c + for c in cls.split(): + if c.startswith('bg-'): + return c + if c not in ('slide', 'active', 'dark', 'light', 'hairlines'): + return c + return 'slide' + + +def has_slide_class(cls): + return 'slide' in cls.split() + + +def get_slides(html): + """Extract slide sections from any template structure.""" + html_flat = re.sub(r'\s+', ' ', html) + slides = [] + pattern = r'(<(?:section)\s+[^>]*class="([^"]+)"[^>]*>|]*class="([^"]+)"[^>]*>)' + for m in re.finditer(pattern, html_flat): + cls = m.group(2) or m.group(3) + if m.group(3) and not has_slide_class(cls): + continue + stype = get_slide_type(cls) + start = m.start() + tag = 'section' if ' 0 and pos < len(html_flat): + open_m = re.search(r'<' + tag + r'\b', html_flat[pos:]) + close_m = re.search(r'', html_flat[pos:]) + if close_m is None: + break + if open_m and open_m.start() < close_m.start(): + depth += 1 + pos += open_m.end() + else: + depth -= 1 + if depth == 0: + slides.append((stype, html_flat[start:pos + close_m.end()])) + pos += close_m.end() + return slides + + +def extract_texts(slide_html): + """Extract text content from a slide's HTML, categorized by element class. + + Returns a list of (category, text) tuples in document order. + Categories: 'headline', 'body', 'label', 'number', 'text' + """ + texts = [] + # Match elements with text content: text + for m in re.finditer(r'(<[^>]+>)([^<]+)(]+>)', slide_html): + tag_open = m.group(1) + text = m.group(2).strip() + if not text or text.startswith('{'): + continue + cls = tag_open.lower() + if any(c in cls for c in ['display', '"h1', '"h2', '"h3', 'heading', 'title']): + cat = 'headline' + elif any(c in cls for c in ['lead', 'body', 'desc', 'lede', 'meta']): + cat = 'body' + elif any(c in cls for c in ['label', 'kicker', 'caption', 'muted', 'note', 'source']): + cat = 'label' + elif any(c in cls for c in ['stat-value', 'num', 'value', 'v "']): + cat = 'number' + elif len(text) < 30: + cat = 'text' + else: + cat = 'body' + texts.append((cat, text)) + return texts + + +def replace_tokens_in_skeleton(skeleton_html, original_texts): + """Replace {{placeholder}} tokens in skeleton HTML with original text. + + Walks through the skeleton finding {{placeholder}} tokens and replaces them + with the corresponding text from the original, matched by category and order. + """ + # Build queues per category + queues = {} + for cat, text in original_texts: + queues.setdefault(cat, []).append(text) + + # Track position in each queue + pos = {cat: 0 for cat in queues} + + def replacer(m): + tag_open = m.group(1) + token = m.group(2) + tag_close = m.group(3) + + # Map token to category + token_name = token.strip('{}') + if token_name not in pos: + # No original text for this category + return m.group(0) + + idx = pos[token_name] + if idx < len(queues[token_name]): + replacement = queues[token_name][idx] + pos[token_name] = idx + 1 + return f'{tag_open}{replacement}{tag_close}' + else: + # Ran out of original texts, keep the token + return m.group(0) + + result = re.sub( + r'(<[^>]+>)(\{\{(?:headline|body|text|number|label|value|accent|meta)\}\})(]+>)', + replacer, + skeleton_html + ) + return result + + +def process_template(template_dir, slug): + """Process a single template: detokenize summary.html using template.html.""" + template_path = os.path.join(template_dir, 'template.html') + summary_path = os.path.join(template_dir, 'summary.html') + + if not os.path.exists(template_path) or not os.path.exists(summary_path): + return False, "missing files" + + with open(template_path) as f: + template_html = f.read() + with open(summary_path) as f: + summary_html = f.read() + + # Check if summary has any tokens to replace + if '{{' not in summary_html: + return False, "no tokens" + + # Extract slides from template.html (originals with real text) + template_slides = get_slides(template_html) + + # Build a map: slide_type -> list of (category, text) tuples + # Use first occurrence of each type (same as build-summaries.py) + original_texts_by_type = {} + for stype, slide_html in template_slides: + if stype not in original_texts_by_type: + original_texts_by_type[stype] = extract_texts(slide_html) + + # Find skeleton slides in summary.html and replace tokens + # Summary has slides as HTML comments followed by sections + # Pattern: \n
...
+ + lines = summary_html.split('\n') + result_lines = [] + replacements = 0 + + for i, line in enumerate(lines): + # Check if this line contains a skeleton slide with tokens + if '{{' in line: + # Find the slide type comment above this line + stype = None + for j in range(i-1, max(i-3, -1), -1): + cm = re.search(r'', lines[j]) + if cm: + stype = cm.group(1) + break + + if stype and stype in original_texts_by_type: + original = original_texts_by_type[stype] + new_line = replace_tokens_in_skeleton(line, original) + token_count_before = line.count('{{') + token_count_after = new_line.count('{{') + replacements += token_count_before - token_count_after + result_lines.append(new_line) + else: + result_lines.append(line) + else: + result_lines.append(line) + + new_summary = '\n'.join(result_lines) + + with open(summary_path, 'w') as f: + f.write(new_summary) + + remaining = new_summary.count('{{') + return True, f"{replacements} replaced, {remaining} remaining" + + +def main(): + templates_dir = sys.argv[1] if len(sys.argv) > 1 else "skills/hyperframes/templates/presentations" + index_path = os.path.join(os.path.dirname(templates_dir), "index.json") + + with open(index_path) as f: + index = json.load(f) + + total_replaced = 0 + total_remaining = 0 + + for t in index['templates']: + slug = t['slug'] + template_dir = os.path.join(templates_dir, slug) + + ok, msg = process_template(template_dir, slug) + if ok: + nums = re.findall(r'(\d+)', msg) + if len(nums) >= 2: + total_replaced += int(nums[0]) + total_remaining += int(nums[1]) + print(f" {slug}: {msg}") + + print(f"\nTotal: {total_replaced} tokens replaced, {total_remaining} remaining") + + +if __name__ == '__main__': + main() diff --git a/skills/hyperframes/scripts/extract-template-structure.py b/skills/hyperframes/scripts/extract-template-structure.py new file mode 100644 index 000000000..fc92edd6d --- /dev/null +++ b/skills/hyperframes/scripts/extract-template-structure.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +"""Extract template structure as a compact component inventory + slide archetype index. + +For each template, produces: +{ + "template": "slug", + "slides": [{ "type": "cover", "components": ["hero-headline", "overline"] }, ...], + "components": { "hero-headline": "
{{text}}
", ... }, + "decoratives": ["..."], + "chrome": { "page_number": "...", "nav_hint": "..." } +} +""" +import re, sys, os, json +from html.parser import HTMLParser + + +def get_slides(html): + """Extract top-level slide elements using a proper parser approach.""" + # Find the slides container + # Templates use:
,
, , or direct sections + slides = [] + + # Match top-level slide elements + # Patterns: section.slide, div.slide, deck-stage > section (with any class) + # First try deck-stage sections + deck_match = re.search(r']*>', html) + if deck_match: + # deck-stage templates: sections are direct children + pattern = r'<(section)\b[^>]*>' + region_start = deck_match.end() + region_end = html.find('', region_start) + if region_end == -1: + region_end = len(html) + search_html = html[region_start:region_end] + slide_starts = [(m, region_start) for m in re.finditer(pattern, search_html)] + else: + slide_starts = [(m, 0) for m in re.finditer( + r'<(section|div)\b[^>]*\bclass="[^"]*\bslide\b[^"]*"[^>]*>', + html + )] + + for i, (m, offset) in enumerate(slide_starts): + tag = m.group(1) + start = offset + m.start() + # Find the matching close tag by counting depth + depth = 1 + pos = offset + m.end() + while depth > 0 and pos < len(html): + open_m = re.search(rf'<{tag}\b', html[pos:]) + close_m = re.search(rf'', html[pos:]) + if close_m is None: + break + if open_m and open_m.start() < close_m.start(): + depth += 1 + pos += open_m.end() + else: + depth -= 1 + if depth == 0: + end = pos + close_m.end() + slides.append(html[start:end]) + pos += close_m.end() + + return slides + + +def classify_slide(slide_html, index, css_text=""): + """Classify a slide by archetype using content heuristics.""" + text = re.sub(r'<[^>]+>', ' ', slide_html) + cls_match = re.search(r'class="([^"]+)"', slide_html[:300]) + cls = cls_match.group(1) if cls_match else "" + + # Class-name hints + cls_lower = cls.lower() + if any(k in cls_lower for k in ['cover', 'hero', 'title', 'poster']): + return "cover" + if any(k in cls_lower for k in ['quote', 'manifesto', 'statement']): + return "quote" + if any(k in cls_lower for k in ['data', 'stat', 'metric', 'financial', 'chart']): + return "data" + if any(k in cls_lower for k in ['road', 'timeline', 'phase', 'process']): + return "process" + if any(k in cls_lower for k in ['close', 'cta', 'end', 'contact']): + return "cta" + if any(k in cls_lower for k in ['service', 'feature', 'pillar', 'grid', 'gallery']): + return "grid" + if any(k in cls_lower for k in ['agenda', 'toc', 'overview', 'summary']): + return "overview" + if any(k in cls_lower for k in ['team', 'people', 'about']): + return "team" + + # Content heuristics + stats = re.findall(r'\d{2,}[%+MKBk$€£]|\$[\d,.]+', text) + if len(stats) >= 2: + return "data" + if '= 3: + return "detail" + if index == 0: + return "cover" + + return "content" + + +def extract_components(slide_html, css_text=""): + """Extract component patterns from a slide.""" + components = [] + + # Large display text (hero headlines) + for m in re.finditer(r'<(?:div|h[1-3]|span)[^>]*style="[^"]*font-size:\s*(?:clamp\()?(\d+)', slide_html): + size = int(m.group(1)) + if size >= 60: + components.append("hero-headline") + elif size >= 32: + components.append("section-heading") + + # Stat blocks (large number + label pattern) + stat_blocks = re.findall(r']*>[\s]*]*style="[^"]*font-size:\s*(?:clamp\()?(\d+)[^"]*"[^>]*>\s*[\d$%+,.MKB]+', slide_html) + for _ in stat_blocks: + components.append("stat-card") + + # Lists + if ']*>(.*?)', html, re.DOTALL): + svg = m.group(0) + if len(svg) < 2000: # Skip huge SVGs + # Clean: remove IDs, simplify + svg = re.sub(r'\s+id="[^"]*"', '', svg) + decoratives.append(svg[:500]) # Cap length + + # Decorative dividers (thin lines, borders) + for m in re.finditer(r']*style="[^"]*border-(?:top|bottom):[^"]*"[^>]*/?\s*>', html): + decoratives.append(m.group(0)) + + return decoratives[:5] # Cap at 5 + + +def extract_chrome(html): + """Extract page chrome patterns (page numbers, nav hints).""" + chrome = {} + + # Page number patterns + pn = re.search(r'class="[^"]*(?:pagenum|slide-counter|page-num)[^"]*"[^>]*>(.*?)', html, re.DOTALL) + if pn: + chrome["page_number"] = re.sub(r'<[^>]+>', '', pn.group(1)).strip()[:50] + + # Nav hints + nh = re.search(r'class="[^"]*nav-hint[^"]*"[^>]*>(.*?)', html, re.DOTALL) + if nh: + chrome["nav_hint"] = re.sub(r'<[^>]+>', '', nh.group(1)).strip()[:50] + + return chrome + + +def extract_structure(html_path, slug): + """Extract full structure from a template.""" + with open(html_path) as f: + html = f.read() + + # Extract CSS + css_blocks = re.findall(r']*>(.*?)', html, re.DOTALL) + css_text = '\n'.join(css_blocks) + + slides_html = get_slides(html) + + slides = [] + all_components = set() + for i, slide in enumerate(slides_html): + slide_type = classify_slide(slide, i, css_text) + components = extract_components(slide, css_text) + slides.append({ + "type": slide_type, + "components": components + }) + all_components.update(components) + + decoratives = extract_decoratives(html) + chrome = extract_chrome(html) + + return { + "template": slug, + "slide_count": len(slides_html), + "slides": slides, + "component_types": sorted(all_components), + "decoratives": decoratives, + "chrome": chrome + } + + +def main(): + templates_dir = sys.argv[1] if len(sys.argv) > 1 else "skills/hyperframes/templates/presentations" + index_path = os.path.join(os.path.dirname(templates_dir), "index.json") + + with open(index_path) as f: + index = json.load(f) + + for t in index["templates"]: + html_path = os.path.join(templates_dir, t["slug"], "template.html") + if not os.path.exists(html_path): + continue + structure = extract_structure(html_path, t["slug"]) + print(f" {t['slug']}: {structure['slide_count']} slides, " + f"{len(structure['component_types'])} component types, " + f"{len(structure['decoratives'])} decoratives") + + # Output one example in full + if index["templates"]: + slug = "bold-poster" + path = os.path.join(templates_dir, slug, "template.html") + if os.path.exists(path): + s = extract_structure(path, slug) + print(f"\n=== Example: {slug} ===") + print(json.dumps(s, indent=2)[:2000]) + + +if __name__ == "__main__": + main() diff --git a/skills/hyperframes/scripts/inject-tp-tokens.py b/skills/hyperframes/scripts/inject-tp-tokens.py new file mode 100644 index 000000000..9b84264e6 --- /dev/null +++ b/skills/hyperframes/scripts/inject-tp-tokens.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Inject --tp-* palette tokens into template :root blocks. + +For each template: +1. Read :root CSS variables +2. Classify into 4 roles: primary (text), secondary (bg), tertiary (muted), accent (vibrant) +3. Add --tp-* declarations with template defaults +4. Rewire only the role vars to reference --tp-* with fallbacks + +Does NOT touch: variant colors, fonts, hardcoded hex in rules, or any non-:root CSS. +""" +import re, os, sys, colorsys + +def hex_to_hsl(hex_str): + hex_str = hex_str.strip().lstrip('#') + if len(hex_str) == 3: + hex_str = hex_str[0]*2 + hex_str[1]*2 + hex_str[2]*2 + r, g, b = int(hex_str[0:2], 16)/255, int(hex_str[2:4], 16)/255, int(hex_str[4:6], 16)/255 + h, l, s = colorsys.rgb_to_hls(r, g, b) + return h*360, s*100, l*100 + +def parse_root_vars(css): + root_match = re.search(r':root\s*\{([^}]+)\}', css, re.DOTALL) + if not root_match: + return [], "" + block = root_match.group(1) + vars_list = [] + for m in re.finditer(r'(--[\w-]+)\s*:\s*([^;]+);', block): + name, val = m.group(1), m.group(2).strip() + hex_match = re.search(r'#[0-9a-fA-F]{3,8}', val) + if hex_match: + vars_list.append((name, hex_match.group(), val, m.start(), m.end())) + return vars_list, root_match + +def classify_vars(vars_list, scheme): + if not vars_list: + return {} + + analyzed = [] + for name, hex_val, full_val, start, end in vars_list: + h, s, l = hex_to_hsl(hex_val) + analyzed.append({ + 'name': name, 'hex': hex_val, 'full_val': full_val, + 'h': h, 's': s, 'l': l, 'start': start, 'end': end + }) + + # Name-based hints (strongest signal) + name_hints = { + 'secondary': ['bg', 'paper', 'canvas', 'ground', 'cream', 'white', 'void', 'deep-navy', 'dark-void'], + 'primary': ['fg', 'ink', 'text', 'dark', 'black'], + 'accent': ['accent', 'sun', 'neon', 'pink', 'red', 'green', 'coral', 'blue', 'cyan', 'yellow', 'ember'], + 'tertiary': ['muted', 'gray', 'grey', 'line', 'border', 'light', 'soft', 'haze'], + } + + roles = {} + used = set() + + # First pass: strong name matches + for role, hints in name_hints.items(): + if role in roles: + continue + for v in analyzed: + if v['name'] in used: + continue + vname = v['name'].lower().replace('--', '').replace('c-', '') + # Exact or prefix match + for hint in hints: + if vname == hint or vname.startswith(hint + '-') or vname.startswith(hint): + # For bg/secondary: prefer the main background (not variants like bg-alt, bg-2) + if role == 'secondary' and any(x in vname for x in ['alt', 'deep', 'dark', '2', '3']): + continue + # For primary/fg: prefer the main text color + if role == 'primary' and any(x in vname for x in ['2', '3', 'alt']): + continue + # For accent: prefer first saturated match + if role == 'accent' and v['s'] < 20: + continue + roles[role] = v + used.add(v['name']) + break + if role in roles: + break + + # Second pass: luminance-based for missing roles + if 'secondary' not in roles: + # BG is usually darkest (dark scheme) or lightest (light scheme) + candidates = [v for v in analyzed if v['name'] not in used] + if candidates: + if scheme == 'dark': + bg = min(candidates, key=lambda v: v['l']) + else: + bg = max(candidates, key=lambda v: v['l']) + roles['secondary'] = bg + used.add(bg['name']) + + if 'primary' not in roles: + candidates = [v for v in analyzed if v['name'] not in used] + if candidates: + if scheme == 'dark': + fg = max(candidates, key=lambda v: v['l']) + else: + fg = min(candidates, key=lambda v: v['l']) + roles['primary'] = fg + used.add(fg['name']) + + if 'accent' not in roles: + candidates = [v for v in analyzed if v['name'] not in used] + if candidates: + accent = max(candidates, key=lambda v: v['s']) + if accent['s'] > 15: + roles['accent'] = accent + used.add(accent['name']) + + if 'tertiary' not in roles: + candidates = [v for v in analyzed if v['name'] not in used] + if candidates: + if 'accent' in roles: + ac_h = roles['accent']['h'] + def hue_dist(h1, h2): + d = abs(h1 - h2) + return min(d, 360 - d) + # Prefer a muted color near the accent hue + tertiary = min(candidates, key=lambda v: hue_dist(v['h'], ac_h) + v['s'] * 0.5) + else: + tertiary = min(candidates, key=lambda v: v['s']) + roles['tertiary'] = tertiary + used.add(tertiary['name']) + + return roles + +def inject_tokens_into_css(css_content, scheme): + vars_list, root_match = parse_root_vars(css_content) + if not vars_list or not root_match: + return None, None + roles = classify_vars(vars_list, scheme) + if len(roles) < 2: + return None, None + + tp_lines = ["\n /* Palette tokens — override to re-theme */"] + for role in ['primary', 'secondary', 'tertiary', 'accent']: + if role in roles: + tp_lines.append(f" --tp-{role}: {roles[role]['hex']};") + tp_lines.append(" /* Surface tokens */") + tp_lines.append(" --tp-radius: 4px;") + tp_lines.append(" --tp-padding: 20px;") + tp_lines.append(" --tp-gap: 16px;") + tp_lines.append(" --tp-shadow: none;") + tp_block = "\n".join(tp_lines) + "\n" + + root_block = root_match.group(1) + new_block = root_block + for role, v in roles.items(): + old_decl_pattern = re.compile( + re.escape(v['name']) + r'\s*:\s*' + re.escape(v['full_val']) + r'\s*;' + ) + replacement = f"{v['name']}: var(--tp-{role}, {v['full_val']});" + new_block = old_decl_pattern.sub(replacement, new_block, count=1) + + new_block = tp_block + new_block + new_css = css_content[:root_match.start(1)] + new_block + css_content[root_match.end(1):] + + mapped = ", ".join(f"{r}={roles[r]['name']}" for r in ['primary','secondary','tertiary','accent'] if r in roles) + return new_css, mapped + +def inject_tokens(html_path, scheme='dark'): + with open(html_path) as f: + html = f.read() + + # Try inline CSS first + new_css, mapped = inject_tokens_into_css(html, scheme) + if new_css: + with open(html_path, 'w') as f: + f.write(new_css) + return mapped + + # Try external styles.css in same directory + css_path = os.path.join(os.path.dirname(html_path), 'styles.css') + if os.path.exists(css_path): + with open(css_path) as f: + css = f.read() + new_css, mapped = inject_tokens_into_css(css, scheme) + if new_css: + with open(css_path, 'w') as f: + f.write(new_css) + return mapped + + return False + +def main(): + templates_dir = sys.argv[1] if len(sys.argv) > 1 else "skills/hyperframes/templates/presentations" + index_path = os.path.join(os.path.dirname(templates_dir), "index.json") + + with open(index_path) as f: + import json + index = json.load(f) + + for t in index['templates']: + html_path = os.path.join(templates_dir, t['slug'], 'template.html') + if not os.path.exists(html_path): + continue + result = inject_tokens(html_path, t.get('scheme', 'dark')) + if result: + print(f" ✓ {t['slug']}: {result}") + else: + print(f" ✗ {t['slug']}: no :root vars found") + +if __name__ == '__main__': + main() diff --git a/skills/hyperframes/scripts/lint-design-html.py b/skills/hyperframes/scripts/lint-design-html.py new file mode 100644 index 000000000..ecf0cfd0d --- /dev/null +++ b/skills/hyperframes/scripts/lint-design-html.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Lint a design.html template for hardcoded values that should use CSS custom properties. + +Checks that every container/card element uses var(--cr), var(--pad), var(--gap), +var(--shadow) instead of hardcoded border-radius, padding, gap, or box-shadow values. + +Usage: + python3 lint-design-html.py presentations/user-design/design.html +""" +import sys, re, os + +def _compile_rules(rules): + for r in rules: + if 'pattern' in r: + r['_re'] = re.compile(r['pattern']) + if 'exclude' in r: + r['_re_ex'] = re.compile(r['exclude']) + if 'check_missing' in r: + r['_re_miss'] = re.compile(r['check_missing']) + return rules + +RULES = _compile_rules([ + { + 'name': 'hardcoded-radius', + 'pattern': r'border-radius:\s*(\d+px)', + 'message': 'Hardcoded border-radius "{match}" — use var(--cr)', + 'exclude': r'var\(--cr\)|calc\(var\(--cr\)|\.bar|\.badge|\.bar\b|3px;opacity', + }, + { + 'name': 'hardcoded-shadow', + 'pattern': r'box-shadow:\s*(\d+px\s+\d+px)', + 'message': 'Hardcoded box-shadow — use var(--shadow)', + 'exclude': r'var\(--shadow\)', + }, + { + 'name': 'hardcoded-color-in-css', + 'pattern': r'(?:color|background|border-color):\s*(#[0-9a-fA-F]{3,8})\b', + 'message': 'Hardcoded color "{match}" in CSS — use var(--primary/secondary/tertiary/accent) or color-mix()', + 'exclude': r'var\(--|color-mix\(', + }, + { + 'name': 'missing-shadow-token', + 'pattern': r'\.(?:demo-card|panel|card|specimen|tmpl|rules-col|swatch|s1-sw|s3-card)\{[^}]+\}', + 'check_missing': r'var\(--shadow', + 'message': 'Container class missing var(--shadow) — depth picker won\'t affect it', + }, + { + 'name': 'missing-cr-token', + 'pattern': r'\.(?:demo-card|panel|card|specimen|tmpl|rules-col|swatch|bg-preview|s1-sw|s3-card)\{[^}]+\}', + 'check_missing': r'var\(--cr', + 'message': 'Container class missing var(--cr) — corners picker won\'t affect it', + }, + { + 'name': 'missing-pad-token', + 'pattern': r'\.(?:demo-card|panel|card|specimen|rules-col|swatch|s1-sw|s3-card)\{[^}]+\}', + 'check_missing': r'var\(--pad', + 'message': 'Container class missing var(--pad) — density picker won\'t affect it', + }, + { + 'name': 'missing-gap-token', + 'pattern': r'\.(?:surface-grid|two-col|palette|rules-grid|templates-grid|row|s3-grid|s1-swatches|s1-ctas|s6-ctas)\{[^}]+\}', + 'check_missing': r'var\(--gap', + 'message': 'Grid class missing var(--gap) — density picker won\'t affect it', + }, + { + 'name': 'inline-hardcoded-hex', + 'pattern': r'style="[^"]*(?:background|color|border):[^"]*#[0-9a-fA-F]{3,8}', + 'message': 'Inline style with hardcoded hex color — use var(--tp-primary/--tp-secondary/--tp-accent/--tp-tertiary)', + 'exclude': r'var\(--|color-mix\(', + }, + { + 'name': 'svg-hardcoded-fill', + 'pattern': r'\bfill="(#[0-9a-fA-F]{3,8})"', + 'message': 'SVG fill="{match}" hardcoded — use fill="currentColor" or fill="var(--tp-accent)" so palette switching works', + 'exclude': r'fill="none"|fill="currentColor"|fill="var\(', + }, + { + 'name': 'svg-hardcoded-stroke', + 'pattern': r'\bstroke="(#[0-9a-fA-F]{3,8})"', + 'message': 'SVG stroke="{match}" hardcoded — use stroke="currentColor" or stroke="var(--tp-primary)"', + 'exclude': r'stroke="none"|stroke="currentColor"|stroke="var\(', + }, + { + 'name': 'missing-token-sentinels', + 'check_sentinels': ['__PRIMARY__', '__SECONDARY__', '__TERTIARY__', '__ACCENT__', + '__TEMPLATE_CSS__', '__SLIDE_CARDS__', '__SHADER_SCRIPT__', + '__SHADER_VERTEX__', '__SHADER_FRAGMENT__', '__NAME__', + '__CORNER_RADIUS__', '__PADDING__', '__GAP__', '__SHADOW__', + '__EASING_NAME__', '__EASING_VALUE__'], + 'only_files': ['design.html'], + }, +]) + +def lint(path): + with open(path) as f: + content = f.read() + + errors = [] + lines = content.split('\n') + + # Extract CSS blocks (between ', content, re.DOTALL) + css = '\n'.join(css_blocks) + + basename = os.path.basename(path) + for rule in RULES: + # Sentinel check (only for design.html, not summary.html) + if 'check_sentinels' in rule: + if 'only_files' in rule and basename not in rule['only_files']: + continue + for sentinel in rule['check_sentinels']: + if sentinel not in content: + errors.append(f"MISSING SENTINEL: {sentinel} not found in template") + continue + + # Missing-property check on matched selectors (skip @media re-declarations) + if 'check_missing' in rule: + seen = set() + pat = rule.get('_re') or re.compile(rule['pattern']) + miss = rule.get('_re_miss') or re.compile(rule['check_missing']) + for m in pat.finditer(css): + block = m.group(0) + selector = block.split('{')[0].strip() + if selector in seen: + continue + seen.add(selector) + if not miss.search(block): + errors.append(f"{rule['name']}: {selector} — {rule['message']}") + continue + + # Pattern match check + pat = rule.get('_re') or re.compile(rule['pattern']) + excl = rule.get('_re_ex') + for i, line in enumerate(lines, 1): + if excl and excl.search(line): + continue + for m in pat.finditer(line): + msg = rule['message'].replace('{match}', m.group(1) if m.groups() else m.group(0)) + errors.append(f"L{i} {rule['name']}: {msg}") + + return errors + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Usage: python3 lint-design-html.py ") + sys.exit(1) + + errors = lint(sys.argv[1]) + if errors: + print(f"\n{len(errors)} issue(s) found:\n") + for e in errors: + print(f" ⚠ {e}") + sys.exit(1) + else: + print("✓ No issues found") + sys.exit(0) diff --git a/skills/hyperframes/scripts/tokenize-templates.py b/skills/hyperframes/scripts/tokenize-templates.py new file mode 100644 index 000000000..dca1063d6 --- /dev/null +++ b/skills/hyperframes/scripts/tokenize-templates.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Convert presentation templates to use a standard CSS variable contract. + +For each template: +1. Parse :root CSS variables to identify the palette +2. Classify each variable as bg/fg/accent/muted/surface/border by name+luminance +3. Replace ALL hardcoded hex colors throughout the entire HTML with var() references +4. Add a standard :root block with --tp-* variables +5. Map original variable definitions to use --tp-* tokens + +Standard contract: + --tp-bg, --tp-fg, --tp-ac, --tp-ac2, --tp-mt, --tp-surface, --tp-border + --tp-hf (headline font), --tp-bf (body font), --tp-mf (mono font) +""" +import re, os, sys, json, colorsys + +def hex_to_rgb(h): + h = h.lstrip('#') + if len(h) == 3: + h = h[0]*2 + h[1]*2 + h[2]*2 + if len(h) < 6: + return (128, 128, 128) + return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) + +def luminance(r, g, b): + return (r * 0.299 + g * 0.587 + b * 0.114) / 255 * 100 + +def saturation(r, g, b): + _, s, _ = colorsys.rgb_to_hsv(r/255, g/255, b/255) + return s * 100 + +def classify_color(hex_val, var_name=""): + """Classify a color by its role: bg, fg, ac, mt, surface, border.""" + r, g, b = hex_to_rgb(hex_val) + lum = luminance(r, g, b) + sat = saturation(r, g, b) + vl = var_name.lower() + + # Name-based classification (highest priority) + if re.search(r'accent|sun|ember|coral|pink|highlight|primary|active|brand|neon|red|orange|yellow|green|blue|violet|purple|magenta|cyan|teal', vl): + return 'ac' + if re.search(r'bg|paper|canvas|cream|bone|surface|white|offwhite|ivory', vl): + if lum > 70: + return 'bg' + return 'surface' + if re.search(r'fg|ink|text|dark|black', vl): + return 'fg' + if re.search(r'mute|gray|grey|secondary|subtle|soft|light', vl): + return 'mt' + if re.search(r'line|rule|border|divider|outline', vl): + return 'border' + + # Luminance-based fallback + if lum > 88: + return 'bg' + if lum < 15: + return 'fg' + if sat > 40: + return 'ac' + if lum > 55: + return 'mt' + return 'surface' + +def process_template(html_path): + with open(html_path) as f: + html = f.read() + + # Extract :root block + root_match = re.search(r':root\s*\{([^}]+)\}', html) + if not root_match: + return html, 0 + + root_block = root_match.group(1) + orig_vars = {} + for m in re.findall(r'(--[\w-]+)\s*:\s*([^;]+)', root_block): + orig_vars[m[0]] = m[1].strip() + + # Classify each variable + var_roles = {} + role_values = {'bg': [], 'fg': [], 'ac': [], 'mt': [], 'surface': [], 'border': []} + + for var_name, val in orig_vars.items(): + hex_match = re.search(r'#[0-9a-fA-F]{3,8}', val) + if hex_match: + role = classify_color(hex_match.group(), var_name) + var_roles[var_name] = role + role_values[role].append((var_name, hex_match.group())) + + # Pick the primary value for each role (first occurrence) + primary = {} + for role, vals in role_values.items(): + if vals: + primary[role] = vals[0][1] + + if not primary.get('bg'): + primary['bg'] = '#ffffff' + if not primary.get('fg'): + primary['fg'] = '#000000' + if not primary.get('ac'): + primary['ac'] = primary.get('fg', '#333333') + if not primary.get('mt'): + primary['mt'] = '#888888' + + # Find font variables and font-family declarations + font_vars = {} + headline_font = "system-ui" + body_font = "system-ui" + mono_font = "monospace" + for var_name, val in orig_vars.items(): + vl = var_name.lower() + if re.search(r'font|display|heading|body|mono|serif', vl) and '#' not in val: + font_vars[var_name] = val + clean_val = val.split(',')[0].strip().strip('"').strip("'") + if re.search(r'display|heading|headline', vl): + headline_font = clean_val + elif re.search(r'body|text', vl): + body_font = clean_val + elif re.search(r'mono', vl): + mono_font = clean_val + + # If no font vars found, try to extract from CSS font-family rules + if not font_vars: + font_matches = re.findall(r'font-family:\s*["\']?([^"\';\n,]+)', html) + seen = set() + for fm in font_matches: + fm = fm.strip() + if fm.lower() not in ('sans-serif', 'serif', 'monospace', 'system-ui', 'inherit', 'cursive') and fm not in seen: + seen.add(fm) + if not headline_font or headline_font == "system-ui": + headline_font = fm + elif body_font == "system-ui": + body_font = fm + + # Build the --tp-* variable block + tp_block = "\n /* Template Picker configurable tokens */\n" + tp_block += f" --tp-bg: {primary.get('bg', '#fff')};\n" + tp_block += f" --tp-fg: {primary.get('fg', '#000')};\n" + tp_block += f" --tp-ac: {primary.get('ac', '#888')};\n" + tp_block += f" --tp-mt: {primary.get('mt', '#888')};\n" + tp_block += f" --tp-surface: {primary.get('surface', primary.get('bg', '#fff'))};\n" + tp_block += f" --tp-border: {primary.get('border', primary.get('mt', '#ccc'))};\n" + tp_block += f' --tp-hf: "{headline_font}";\n' + tp_block += f' --tp-bf: "{body_font}";\n' + tp_block += f' --tp-mf: "{mono_font}";\n' + + # Rewrite original variable definitions to reference --tp-* tokens + new_root_block = root_block + for var_name, role in var_roles.items(): + orig_val = orig_vars[var_name] + tp_var = f"--tp-{role}" + # Replace the value in the :root block + pattern = re.escape(var_name) + r'\s*:\s*' + re.escape(orig_val) + replacement = f"{var_name}: var({tp_var}, {orig_val})" + new_root_block = re.sub(pattern, replacement, new_root_block, count=1) + + # Insert --tp-* block at start of :root + new_root_block = tp_block + new_root_block + + # Replace the :root block + html = html[:root_match.start(1)] + new_root_block + html[root_match.end(1):] + + # Build color map: known palette hex → var() reference + color_map = {} + for var_name, val in orig_vars.items(): + hex_match = re.search(r'#[0-9a-fA-F]{3,8}', val) + if hex_match: + hex_val = hex_match.group().lower() + if len(hex_val) == 4: + hex_val = '#' + hex_val[1]*2 + hex_val[2]*2 + hex_val[3]*2 + role = var_roles.get(var_name, 'ac') + if hex_val not in color_map: + color_map[hex_val] = f"var(--tp-{role})" + + replacements = 0 + # Split HTML at :root block boundary — only replace OUTSIDE :root + root_end = html.find('}', html.find(':root')) + if root_end > 0: + before_root = html[:root_match.start()] + root_section = html[root_match.start():root_end+1] + after_root = html[root_end+1:] + + # Replace known palette colors + for hex_val, var_ref in color_map.items(): + count = len(re.findall(re.escape(hex_val), after_root, re.IGNORECASE)) + if count > 0: + after_root = re.sub(re.escape(hex_val), var_ref, after_root, flags=re.IGNORECASE) + replacements += count + + # Find and replace remaining hardcoded hex colors not in palette + remaining_hexes = re.findall(r'(? 0: + after_root = re.sub(font_pattern, 'var(--tp-hf)', after_root) + replacements += count + + if body_font and body_font != "system-ui" and body_font != headline_font: + font_pattern = re.escape(f'"{body_font}"') + r'|' + re.escape(f"'{body_font}'") + r'|' + re.escape(body_font) + count = len(re.findall(font_pattern, after_root)) + if count > 0: + after_root = re.sub(font_pattern, 'var(--tp-bf)', after_root) + replacements += count + + html = before_root + root_section + after_root + + # Add background layer variable for shader swap + html = html.replace( + '/* Template Picker configurable tokens */', + '/* Template Picker configurable tokens */\n --tp-bg-layer: none; /* set to shader canvas or gradient */' + ) + + return html, replacements + +def main(): + templates_dir = sys.argv[1] if len(sys.argv) > 1 else "skills/hyperframes/templates/presentations" + + total = 0 + total_replacements = 0 + + for slug in sorted(os.listdir(templates_dir)): + html_path = os.path.join(templates_dir, slug, "template.html") + if not os.path.isfile(html_path): + continue + + html, replacements = process_template(html_path) + + with open(html_path, 'w') as f: + f.write(html) + + total += 1 + total_replacements += replacements + status = f" {slug}: {replacements} color replacements" + if replacements > 0: + status += " ✓" + print(status) + + print(f"\nProcessed {total} templates, {total_replacements} total color replacements") + +if __name__ == '__main__': + main() diff --git a/skills/hyperframes/templates/index.json b/skills/hyperframes/templates/index.json new file mode 100644 index 000000000..b3b4ba876 --- /dev/null +++ b/skills/hyperframes/templates/index.json @@ -0,0 +1,711 @@ +{ + "schema_version": 1, + "generated_at": "2026-05-14T23:03:11.859Z", + "template_count": 34, + "templates": [ + { + "slug": "8-bit-orbit", + "name": "8-Bit Orbit", + "tagline": "Pixel-art neon arcade aesthetic on a deep navy void.", + "mood": ["retro-tech", "playful", "cyberpunk", "energetic"], + "occasion": [ + "gaming pitch", + "hackathon demo", + "web3 / crypto deck", + "indie product launch", + "developer tools", + "synthwave brand" + ], + "tone": ["geeky", "neon", "rebellious", "sci-fi"], + "formality": "low", + "density": "medium", + "scheme": "dark", + "best_for": "Anything that should feel like a CRT screen at 2am: cyberpunk, gaming, web3, indie dev tools, hackathon demos. Just as good for a tech talk that wants to lean into nostalgic-digital craft, a synthwave brand deck, or a creative review that wants to feel like a console.", + "avoid_for": "Contexts where the dark neon palette would actively work against the message — quiet institutional finance disclosures, healthcare patient-facing materials, traditional luxury.", + "slide_count": 10 + }, + { + "slug": "biennale-yellow", + "name": "Biennale Yellow", + "tagline": "Solar yellow on warm parchment with deep indigo serif and atmospheric sun-glow gradients.", + "mood": ["editorial", "atmospheric", "warm", "cultural-institution", "poster-like"], + "occasion": [ + "exhibition or biennale", + "arts institution programme", + "design or typography conference", + "literary or curatorial publication", + "studio annual report", + "museum season announcement" + ], + "tone": ["literary", "considered", "contemplative", "warm-modern", "Dutch-editorial"], + "formality": "high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like an art-biennale poster or a museum's annual programme: exhibition decks, arts-institution announcements, design conference brochures, curatorial pitches, literary publications, studio retrospectives. Equally good for any deck wanting Dutch-editorial atmosphere with an unmistakable single-color signature.", + "avoid_for": "Decks that need visual punch or saturated multi-color energy — the warm-paper canvas and one-yellow palette are intentionally quiet and atmospheric.", + "slide_count": 8 + }, + { + "slug": "block-frame", + "name": "BlockFrame", + "tagline": "Neobrutalist deck with pastel-neon color blocks and chunky black borders.", + "mood": ["bold", "playful", "graphic", "fresh"], + "occasion": [ + "creative agency pitch", + "indie SaaS launch", + "designer portfolio", + "brand redesign", + "modern startup deck" + ], + "tone": ["confident", "graphic", "pop", "design-led"], + "formality": "medium-low", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel pop-graphic and design-led: indie SaaS launches, agency credentials, creative reviews, brand redesigns. Also a strong unexpected pick for tech, finance, or research when the speaker wants to land as confident and contemporary rather than buttoned-up.", + "avoid_for": "Contexts that require quiet institutional restraint or traditional weight (regulated disclosures, formal legal briefs).", + "slide_count": 10 + }, + { + "slug": "blue-professional", + "name": "Blue Professional", + "tagline": "Cream paper background with electric cobalt blue accents; clean modern professional.", + "mood": ["professional", "modern", "calm", "trustworthy"], + "occasion": [ + "B2B SaaS pitch", + "consulting deliverable", + "internal review", + "advisory pitch", + "investor update" + ], + "tone": ["clean", "considered", "polished", "neutral"], + "formality": "medium-high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel modern-considered and lightly authoritative: B2B SaaS pitches, consulting deliverables, advisory updates, investor reports. Also a clean, tasteful choice whenever you want to read as professional without going stiff — research synthesis, internal reviews, brand work for service businesses.", + "avoid_for": "Contexts where the deck should feel hot, playful, or intentionally informal — the cool electric-blue restraint will read as overly polished.", + "slide_count": 10 + }, + { + "slug": "bold-poster", + "name": "Bold Poster", + "tagline": "Editorial poster aesthetic with massive Shrikhand display and a single fire-engine red accent.", + "mood": ["bold", "editorial", "loud", "confident"], + "occasion": [ + "brand manifesto", + "creative-led pitch", + "magazine / editorial", + "founder vision deck", + "art / culture" + ], + "tone": ["dramatic", "graphic", "sharp", "intentional"], + "formality": "medium", + "density": "low", + "scheme": "light", + "best_for": "Anything that should land like a magazine cover: brand manifestos, founder vision decks, editorial / cultural pitches, creative reviews. Excellent any time you want a few words to feel like a poster — including unexpected fits like a tech keynote or a finance manifesto that wants to be quotable.", + "avoid_for": "Decks that need to communicate dense information per slide — the layout is built around a few large statements, not paragraphs of detail.", + "slide_count": 10 + }, + { + "slug": "broadside", + "name": "Broadside", + "tagline": "Dark editorial canvas with a single fire orange accent and bilingual Latin/Chinese type stack.", + "mood": ["editorial", "dramatic", "loud", "newspaper"], + "occasion": [ + "brand manifesto", + "founder vision deck", + "magazine / cultural pitch", + "design talk", + "bilingual EN/CN deck", + "campaign launch" + ], + "tone": ["graphic", "punchy", "literary", "considered"], + "formality": "medium-high", + "density": "medium", + "scheme": "dark", + "best_for": "Anything that should land like a broadside newspaper headline: brand manifestos, magazine and cultural pitches, design talks, bilingual EN/CN decks, founder vision statements. Also a striking pick for tech, research, or business decks that want a dramatic single-accent editorial feel.", + "avoid_for": "Decks that need to feel quiet, warm, or institutionally traditional — the dark canvas with fire-orange accent commits to drama.", + "slide_count": 20 + }, + { + "slug": "capsule", + "name": "Capsule", + "tagline": "Modular pill-shaped cards on warm bone with a full pastel-pop palette.", + "mood": ["playful", "modern", "warm", "fresh", "fun"], + "occasion": [ + "lifestyle brand", + "creator portfolio", + "DTC product launch", + "wellness or beauty pitch", + "Y2K-tinged brand work" + ], + "tone": ["upbeat", "graphic", "approachable", "cool"], + "formality": "medium-low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel modular, modern, and a little Y2K: lifestyle brands, creator portfolios, DTC launches, beauty / wellness, agency credentials. Also fun for a playful tech demo or a research deck that wants pop-art clarity instead of gravitas.", + "avoid_for": "Contexts that require traditional institutional weight — the capsule shapes and pastel pops actively soften authority.", + "slide_count": 10 + }, + { + "slug": "cartesian", + "name": "Cartesian", + "tagline": "Quiet warm-neutral palette with classical Playfair serifs; tasteful and unhurried.", + "mood": ["quiet", "considered", "elegant", "warm-minimal"], + "occasion": [ + "investment thesis", + "white paper", + "advisory deliverable", + "research report", + "book / longform pitch", + "gallery / cultural" + ], + "tone": ["classical", "literary", "restrained", "confident-quiet"], + "formality": "high", + "density": "low", + "scheme": "light", + "best_for": "Anything that should feel quiet, considered, and grown-up: investment theses, white papers, advisory work, longform research, gallery / cultural decks. Also a strong choice for editorial features, founder reflections, or any deck where restraint is the message — including across tech and finance.", + "avoid_for": "Decks that need visual heat, multiple accents, or a sense of urgency — the warm-neutral palette is intentionally low-energy.", + "slide_count": 10 + }, + { + "slug": "cobalt-grid", + "name": "Cobalt Grid", + "tagline": "Electric cobalt serifs on a graph-paper canvas, anchored by stair-stepped pixel-glitch decorations and slim hairline rules.", + "mood": ["editorial", "design-research", "studious", "modernist", "tech-print", "monochrome"], + "occasion": [ + "design trend or research report", + "studio annual or seasonal bulletin", + "creative agency capabilities deck", + "art or architecture publication", + "academic / curatorial publication", + "newsletter or zine pitch" + ], + "tone": ["considered", "literary", "studious", "quietly-modern", "editorial"], + "formality": "high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like a quietly serious design / research bulletin, art publication, or curated trend report. Strong for studio annuals, agency capabilities decks, design-research publications, architecture / art / academic decks, and any deck wanting one strict accent colour and a printed-ledger calmness rather than corporate polish.", + "avoid_for": "Decks that need warmth, multi-colour energy, or a casual / playful voice — the strict cobalt + cream + grid palette is intentionally austere.", + "slide_count": 8 + }, + { + "slug": "coral", + "name": "Coral", + "tagline": "Cream and coral on near-black, set in oversized Bebas Neue.", + "mood": ["bold", "warm", "modern", "confident"], + "occasion": [ + "fashion / beauty pitch", + "fitness brand", + "F&B brand deck", + "lifestyle launch", + "creative agency" + ], + "tone": ["graphic", "punchy", "magazine"], + "formality": "medium", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel warm-graphic and editorial: fashion, beauty, fitness, F&B, lifestyle brands, agency credentials. Just as strong for a creator portfolio, a manifesto, or a tech / research deck that wants warmth and a single bold accent instead of corporate cool.", + "avoid_for": "Contexts that should feel quiet or institutional — the coral accent and oversized Bebas Neue commit hard to a confident magazine voice.", + "slide_count": 10 + }, + { + "slug": "creative-mode", + "name": "Creative Mode", + "tagline": "Cream paper canvas with confident multi-color (green, pink, orange, yellow) accents and Archivo Black display.", + "mood": ["creative", "confident", "playful", "design-led"], + "occasion": [ + "creative agency pitch", + "design studio deck", + "ad shop credentials", + "brand creative review", + "concept presentation" + ], + "tone": ["graphic", "expressive", "modern"], + "formality": "medium", + "density": "medium-high", + "scheme": "light", + "best_for": "Anything that should feel design-led and confident: creative agency pitches, design studio decks, ad shop credentials, brand creative reviews, art-direction reviews. Also a great unexpected pick for a tech talk, research findings, or finance review when the speaker wants to lead with taste rather than convention.", + "avoid_for": "Contexts that demand institutional restraint and a quiet authority — the saturated multi-accent palette will read as expressive, not formal.", + "slide_count": 8 + }, + { + "slug": "daisy-days", + "name": "Daisy Days", + "tagline": "Cheerful pastel deck with hand-drawn daisies, stars, and rainbows. Friendly, soft, and warm.", + "mood": ["cheerful", "playful", "warm", "sunny", "wholesome"], + "occasion": [ + "education / classroom", + "kids product launch", + "wellness program", + "community workshop", + "creator portfolio (craft / illustration)", + "team kickoff", + "wedding / baby shower planning" + ], + "tone": ["friendly", "soft", "encouraging", "approachable", "lighthearted"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel friendly, soft, and joyful: educational content, kids and family, wellness programs, community workshops, creator portfolios for craft / illustration. Also lovely for an unexpected playful internal kickoff, a wedding planning deck, or any moment where warmth is the message — including across tech or business contexts.", + "avoid_for": "Contexts where the audience explicitly expects authority and precision — the hand-drawn pastel SVG decorations are the opposite of buttoned-up.", + "slide_count": 10 + }, + { + "slug": "editorial-forest", + "name": "Editorial Forest", + "tagline": "Forest green, dusty pink, and warm cream meet Source Serif 4 in a quiet, intentional quarterly-review deck.", + "mood": ["editorial", "quiet", "considered", "warm", "intentional"], + "occasion": [ + "quarterly review", + "internal readout", + "studio update", + "creative agency deck", + "research recap" + ], + "tone": ["literary", "thoughtful", "warm", "low-pressure"], + "formality": "medium", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel like a considered editorial — quarterly reviews, internal readouts, studio updates, creative-agency presentations. Equally good for any deck that wants to feel warm and unhurried rather than corporate, including research recaps, book or program announcements, and team retrospectives.", + "avoid_for": "Contexts that need to feel urgent, punchy, or sales-driven — the palette and rhythm are intentionally quiet.", + "slide_count": 8 + }, + { + "slug": "editorial-tri-tone", + "name": "Editorial Tri-Tone", + "tagline": "Three-color editorial system: dusty pink, mustard cream, and deep burgundy, set in Bricolage + Instrument Serif.", + "mood": ["editorial", "warm", "intentional", "moody"], + "occasion": [ + "editorial / magazine pitch", + "fashion brand deck", + "lifestyle media", + "literary / cultural", + "art direction review" + ], + "tone": ["literary", "warm", "considered", "stylish"], + "formality": "medium-high", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel like a fashion-magazine spread: editorial pitches, fashion brand decks, lifestyle media, art direction reviews. Equally good for any deck — including tech, research, or business — that wants tri-tone discipline and serif/sans contrast instead of the usual neutrals.", + "avoid_for": "Decks that need to read as soft or comforting — the burgundy/pink/cream tri-tone is intentionally high-contrast and styled.", + "slide_count": 8 + }, + { + "slug": "emerald-editorial", + "name": "Emerald Editorial", + "tagline": "A magazine-cover business deck: emerald + navy + paper, double-rule masthead ornaments, and a bold Bodoni-style display serif.", + "mood": ["editorial", "considered", "confident", "magazine-cover"], + "occasion": [ + "leadership presentation", + "quarterly review", + "strategy readout", + "planning office deck", + "executive briefing" + ], + "tone": ["literary", "authoritative", "warm", "designed"], + "formality": "medium-high", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel like the front of a serious magazine, including but not limited to leadership readouts, planning-office reviews, and strategy briefings. The double-rule masthead ornament gives it editorial gravitas without making it stiff — also a great unexpected pick for product launches or research recaps that want to feel considered rather than corporate.", + "avoid_for": "Contexts that need to read as quiet, neutral, or institutionally restrained — the emerald field is too saturated to disappear into the background.", + "slide_count": 8 + }, + { + "slug": "grove", + "name": "Grove", + "tagline": "Forest-green canvas with cream type, classical Playfair serifs, and a single rust accent.", + "mood": ["organic", "considered", "warm", "literary", "natural"], + "occasion": [ + "sustainability brand", + "wellness brand", + "outdoor / nature product", + "winery or restaurant", + "literary or arts deck", + "advisory deliverable", + "bilingual EN/CN deck" + ], + "tone": ["classical", "warm", "considered", "patient"], + "formality": "medium-high", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel organic, considered, and grown-up: sustainability and wellness brands, outdoor / nature products, wineries and restaurants, literary or arts decks, advisory deliverables, bilingual EN/CN reports. Also a calm, distinctive choice for tech, research, or business decks that want patience over urgency.", + "avoid_for": "Decks that need neon energy or rapid-fire pop — the forest-green canvas and Playfair serif commit to a slow, classical voice.", + "slide_count": 12 + }, + { + "slug": "long-table", + "name": "Long Table", + "tagline": "Warm cream and rust-red supper-club aesthetic with bold uppercase grotesk headlines, Fraunces serifs, and pill-shaped outlined buttons.", + "mood": ["warm", "intimate", "modern", "friendly", "small-batch", "social", "hospitality"], + "occasion": [ + "supper club or dinner series", + "event or community gathering", + "small hospitality / restaurant brand", + "creative studio open house", + "membership or subscription pitch", + "wine or food brand catalogue", + "modern lifestyle brand" + ], + "tone": ["warm", "playful", "considered", "social", "magazine-friendly", "modern-editorial"], + "formality": "medium", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like a warm, intimate, modern hospitality / community brand: supper clubs, dinner series, small restaurants, creative-studio events, membership pitches, lifestyle and wine brands. Equally good for any deck wanting a single warm accent colour, mixed-weight typography, and a social-media-aware modern-editorial voice.", + "avoid_for": "Decks that need corporate polish, technical density, or a cold / minimalist register — the rust-red palette and bold serif mix are intentionally warm and people-facing.", + "slide_count": 8 + }, + { + "slug": "mat", + "name": "Mat", + "tagline": "Dark sage canvas with bone paper and burnt-orange accent; mid-century modern with wood undertones.", + "mood": ["warm-modern", "considered", "tactile", "mid-century"], + "occasion": [ + "design studio credentials", + "architecture / interior brand", + "ceramics or craft brand", + "furniture pitch", + "advisory deliverable", + "bilingual EN/CN deck" + ], + "tone": ["warm", "design-led", "intentional", "considered"], + "formality": "medium", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel mid-century, tactile, and intentional: design studio credentials, architecture / interior brands, ceramics / craft / furniture, advisory decks. Also a warm, distinctive choice for tech, research, or business decks that want a considered analog feel instead of digital-cool.", + "avoid_for": "Contexts that need fast tech energy or institutional restraint — the muted sage and burnt-orange palette is intentionally warm and slow.", + "slide_count": 9 + }, + { + "slug": "monochrome", + "name": "Monochrome", + "tagline": "Ivory ledger paper with all-black type; Lora serif headlines, Jost body, no color at all.", + "mood": ["restrained", "literary", "archival", "ledger"], + "occasion": [ + "user research synthesis", + "white paper", + "longform report", + "academic deck", + "policy brief", + "advisory deliverable", + "bilingual EN/CN deck" + ], + "tone": ["literary", "considered", "neutral", "honest"], + "formality": "high", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel like a hand-typeset ledger: user research synthesis, white papers, longform reports, academic and policy briefs, advisory deliverables, bilingual EN/CN reports. Equally good for tech, design, or brand decks that want their words to be the only thing on the page.", + "avoid_for": "Decks that need visual personality or color-led storytelling — the all-ink palette is intentionally austere.", + "slide_count": 18 + }, + { + "slug": "neo-grid-bold", + "name": "Neo-Grid Bold", + "tagline": "Editorial neo-brutalism with a single neon yellow accent on off-white paper.", + "mood": ["confident", "punchy", "editorial", "modern"], + "occasion": [ + "product launch", + "design review", + "founder pitch", + "brand deck", + "consulting findings", + "conference talk" + ], + "tone": ["bold", "minimal", "design-led", "graphic"], + "formality": "medium", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel confident and editorial-graphic: design-led pitches, brand work, founder talks, conference keynotes. Excellent for stat-heavy slides, comparisons, and process flows. Just as strong for tech, research, or finance when the speaker wants to read as design-led rather than corporate.", + "avoid_for": "Contexts that need to feel quiet, traditional, or warm — the neon-yellow accent and uppercase display commit to a confident editorial voice.", + "slide_count": 13 + }, + { + "slug": "peoples-platform", + "name": "People's Platform (Block & Bold)", + "tagline": "Activist poster energy: blue, orange, red on cream, with Alfa Slab + Caveat Brush.", + "mood": ["activist", "loud", "graphic", "honest"], + "occasion": [ + "cultural commentary", + "manifesto", + "community / civic deck", + "design talk", + "campaign pitch", + "founder vision" + ], + "tone": ["punchy", "direct", "expressive", "warm-bold"], + "formality": "medium-low", + "density": "medium-high", + "scheme": "light", + "best_for": "Anything that should feel honest, loud, and graphic: cultural commentary, manifestos, civic and community decks, design talks, campaign pitches. Excellent for founder-vision moments, mission statements, or any deck — including across industries — that wants protest-poster energy instead of corporate polish.", + "avoid_for": "Contexts where institutional restraint is the actual goal — the saturated political-poster palette commits hard to expressive energy.", + "slide_count": 10 + }, + { + "slug": "pin-and-paper", + "name": "Pin & Paper", + "tagline": "Yellow paper with safety-pin illustrations, ink-blue handwritten Caveat, paper-grain texture.", + "mood": ["crafted", "handmade", "warm", "thoughtful", "literary"], + "occasion": [ + "research findings with personality", + "qualitative report", + "founder reflection", + "creator essay deck", + "workshop debrief" + ], + "tone": ["literary", "intimate", "warm", "grounded"], + "formality": "medium", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel hand-crafted, warm, and literary: qualitative research findings, founder reflections, longform brand stories, workshop debriefs. The signature safety-pin illustrations and paper-grain texture make it especially good for any deck — including tech or business — that wants personality and warmth over polish.", + "avoid_for": "Decks that need to feel digital-native polished or rigorously data-driven — handwritten Caveat is intentionally informal.", + "slide_count": 11 + }, + { + "slug": "pink-script", + "name": "Pink Script — After Hours", + "tagline": "Black canvas, hot pink accent, pearl-cream paper, Instrument Serif headlines: late-night editorial luxury.", + "mood": ["nocturnal", "moody", "intentional", "luxe", "expressive"], + "occasion": [ + "fashion brand deck", + "creator personal brand", + "after-hours product (nightlife / dating / spirits)", + "luxury launch", + "editorial feature" + ], + "tone": ["literary", "sultry", "considered", "magazine"], + "formality": "medium-high", + "density": "low", + "scheme": "dark", + "best_for": "Anything that should feel nocturnal, intentional, and a little luxe: fashion brand decks, creator personal brands, after-hours / nightlife / spirits launches, luxury product reveals, editorial features. Also a striking unexpected pick for a tech keynote, research synthesis, or business pitch that wants to land with magnetic confidence.", + "avoid_for": "Daytime corporate-professional and traditional B2B contexts where the dark canvas with hot-pink accent reads as too styled or too expressive.", + "slide_count": 9 + }, + { + "slug": "playful", + "name": "Playful", + "tagline": "Sun-warm peach background with Syne display: a friendly indie launch deck.", + "mood": ["warm", "approachable", "indie", "friendly"], + "occasion": [ + "creator portfolio", + "indie product launch", + "lifestyle brand", + "small-business pitch", + "newsletter / community" + ], + "tone": ["upbeat", "informal", "welcoming"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel warm, indie, and approachable: creator portfolios, indie product launches, lifestyle brands, small-business pitches, newsletter / community decks. Also welcoming for any deck — including tech or research — that wants to feel friendly and human rather than corporate.", + "avoid_for": "Contexts where institutional credibility matters more than warmth — the peach palette is intentionally informal.", + "slide_count": 10 + }, + { + "slug": "raw-grid", + "name": "Raw Grid", + "tagline": "Neo-brutalist deck with thick borders, offset shadows, and a pink/sage/ink palette.", + "mood": ["raw", "punchy", "energetic", "confident"], + "occasion": [ + "startup pitch", + "accelerator demo day", + "founder pitch", + "indie product launch", + "brand deck", + "creator portfolio" + ], + "tone": ["direct", "modern", "no-nonsense", "graphic"], + "formality": "medium-low", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel direct and graphic-confident: founder pitches, accelerator demos, brand decks, indie launches, creator portfolios. Strong for stat slides, comparison tables, and process flows. Equally good for tech, research, or finance when the speaker wants the deck to feel scrappy-confident rather than buttoned-up.", + "avoid_for": "Contexts that need to feel soft, warm, or intentionally quiet — the brutalist borders and offset shadows commit to a graphic voice.", + "slide_count": 10 + }, + { + "slug": "retro-windows", + "name": "Retro Windows", + "tagline": "Windows 95 chrome: gray title bars, MS Sans Serif, pixel typography, full nostalgia.", + "mood": ["nostalgic", "retro", "geeky", "playful"], + "occasion": [ + "retro gaming pitch", + "Y2K brand", + "creator portfolio (90s aesthetic)", + "tech-history talk", + "shitpost-but-make-it-fancy deck" + ], + "tone": ["winking", "nostalgic", "geeky", "fun"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel knowingly nostalgic: retro gaming, Y2K-aesthetic brands, creator portfolios with a 90s vibe, tech-history talks, deliberately tongue-in-cheek decks. A great choice anywhere a playful retro reference is the entire point.", + "avoid_for": "Decks that need to read as modern, elegant, or institutionally credible — the Win95 chrome will always read as a costume.", + "slide_count": 10 + }, + { + "slug": "retro-zine", + "name": "Retro Zine", + "tagline": "Beige paper with green accent and Bebas Neue + Caveat: a riso-printed zine in HTML form.", + "mood": ["crafted", "lo-fi", "underground", "warm-retro"], + "occasion": [ + "indie zine / publication", + "music or arts brand", + "creator portfolio", + "small-batch / craft launch", + "cultural / community deck" + ], + "tone": ["scrappy", "warm", "intentional", "DIY"], + "formality": "medium-low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel printed, lo-fi, and crafted: indie zines and publications, music / arts brands, creator portfolios, small-batch craft launches, community decks. Also a great underdog choice for tech, research, or business decks that want a riso-print warmth instead of digital polish.", + "avoid_for": "Contexts that demand digital-native polish or fast modern-tech energy — the layered zine aesthetic intentionally feels handmade.", + "slide_count": 10 + }, + { + "slug": "sakura-chroma", + "name": "Sakura Chroma", + "tagline": "Vintage Japanese cassette-package aesthetic: cream paper, diagonal rainbow ribbons, condensed bold type, JIS-style spec checkboxes.", + "mood": ["retro", "playful", "kawaii-tech", "warm", "tactile", "product-catalogue"], + "occasion": [ + "product launch or catalogue", + "indie hardware or analog studio brand", + "music label or release schedule", + "creative studio annual report", + "magazine or zine pitch", + "vintage-flavored brand campaign" + ], + "tone": ["playful", "confident", "warm", "tactile", "80s-Japanese-tech"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like a vintage Japanese cassette package or a TDK / Sony / Sakura Color product catalogue: indie hardware brand decks, music-label release schedules, analog studio retrospectives, zine and magazine pitches, kawaii-tech product launches, creative-studio annual reports. Equally good for any deck wanting bold colour, condensed display type, and a tactile printed-product personality.", + "avoid_for": "Decks that need restrained, corporate, or quiet typography — the bold condensed lockups, ribbon stripes, and primary-colour palette are intentionally loud and product-page-y.", + "slide_count": 8 + }, + { + "slug": "scatterbrain", + "name": "Scatterbrain", + "tagline": "Post-it inspired: pastel sticky notes, Caveat handwriting, Shrikhand and Zilla Slab type stack.", + "mood": ["playful", "creative", "warm", "messy-on-purpose", "workshop"], + "occasion": [ + "brainstorm / workshop", + "creative agency credentials", + "design-thinking session", + "ideation pitch", + "art-direction review" + ], + "tone": ["informal", "warm", "expressive", "human"], + "formality": "low", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel like a designer's whiteboard: brainstorms, workshops, creative-agency credentials, design-thinking sessions, ideation pitches, art-direction reviews. Equally fun for any deck — including tech, research, or business — that wants to read as in-progress thinking rather than polished conclusions.", + "avoid_for": "Contexts that demand precision and institutional weight — the post-it sticky-note aesthetic intentionally reads as warm and unfinished.", + "slide_count": 10 + }, + { + "slug": "signal", + "name": "Signal", + "tagline": "Deep navy canvas with bone paper and a single muted-gold accent; institutional with quiet weight.", + "mood": ["institutional", "trustworthy", "considered", "weighty"], + "occasion": [ + "investor deck", + "consulting deliverable", + "board presentation", + "legal / policy brief", + "academic deck", + "advisory pitch", + "bilingual EN/CN deck" + ], + "tone": ["sober", "polished", "established", "literary"], + "formality": "high", + "density": "high", + "scheme": "mixed", + "best_for": "Anything that should feel weighty, considered, and credibly institutional: investor decks, board presentations, consulting deliverables, legal / policy briefs, advisory pitches. Also a strong choice for tech, research, or brand work that wants to read as quietly authoritative rather than loud.", + "avoid_for": "Contexts that should feel hot, fast, or intentionally playful — the navy + gold restraint commits to a sober voice.", + "slide_count": 18 + }, + { + "slug": "soft-editorial", + "name": "Soft Editorial", + "tagline": "Cormorant Garamond serif on warm paper with sage, blush, and lemon accents.", + "mood": ["literary", "elegant", "quiet", "warm-classical"], + "occasion": [ + "editorial feature", + "longform brand story", + "gallery or museum", + "literary pitch", + "advisory deliverable", + "wedding / lifestyle media" + ], + "tone": ["literary", "considered", "warm", "magazine"], + "formality": "high", + "density": "low", + "scheme": "light", + "best_for": "Anything that should feel literary, elegant, and unhurried: editorial features, longform brand stories, gallery / museum decks, advisory deliverables, wedding / lifestyle media, founder essays. Equally good for tech, research, or business decks that want a Sunday-supplement warmth instead of corporate polish.", + "avoid_for": "Decks that need visual heat or punch — the warm-paper palette and Cormorant serif are intentionally quiet.", + "slide_count": 12 + }, + { + "slug": "stencil-tablet", + "name": "Stencil & Tablet", + "tagline": "Bone paper with stencil-cut headlines and a six-color earth palette: archaeology meets brand.", + "mood": ["archival", "earthy", "tactile", "considered", "graphic"], + "occasion": [ + "museum / cultural institution", + "art / architecture brand", + "longform research", + "heritage / craft brand", + "manifesto" + ], + "tone": ["weighty", "considered", "tactile", "literary"], + "formality": "medium-high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel archival, tactile, and weighty-graphic: museum and cultural-institution decks, art / architecture brands, longform research, heritage and craft brands, manifestos. A great choice anytime — including across tech and business — when you want the deck to feel like a field manual rather than a slide deck.", + "avoid_for": "Contexts that demand digital-native polish or playful pop — the stencil-cut display and earth-tone palette commit to a deliberate analog feel.", + "slide_count": 11 + }, + { + "slug": "studio", + "name": "Studio", + "tagline": "Black canvas with electric-yellow type; high-voltage design studio aesthetic.", + "mood": ["electric", "bold", "graphic", "design-led", "high-contrast"], + "occasion": [ + "design studio credentials", + "creative agency pitch", + "brand showcase", + "art-direction review", + "fashion / sneaker brand", + "bilingual EN/CN deck" + ], + "tone": ["graphic", "loud", "modern", "intentional"], + "formality": "medium", + "density": "medium", + "scheme": "dark", + "best_for": "Anything that should feel electric and design-led: studio credentials, creative agency pitches, brand showcases, art-direction reviews, fashion / sneaker brand work. Also a striking unexpected choice for tech, research, or business decks where the speaker wants the deck to *be* a brand statement.", + "avoid_for": "Contexts that should feel quiet or institutional — the black-and-electric-yellow palette is the loudest in the library.", + "slide_count": 12 + }, + { + "slug": "vellum", + "name": "Vellum", + "tagline": "Deep navy canvas with warm-yellow Cormorant serifs and a single dusty teal accent. A quiet, scholarly aesthetic.", + "mood": ["scholarly", "literary", "considered", "quiet", "intellectual"], + "occasion": [ + "research findings", + "white paper or longform report", + "academic or university deck", + "advisory deliverable", + "literary or editorial pitch", + "founder reflection / vision deck", + "bilingual EN/CN deck" + ], + "tone": ["literary", "considered", "patient", "intelligent"], + "formality": "high", + "density": "low", + "scheme": "dark", + "best_for": "Anything that should feel scholarly, literary, and quietly intelligent: research synthesis, white papers, academic and policy briefs, advisory deliverables, longform editorial pieces, founder reflections. Equally strong for any deck — including tech, business, or creator work — that wants a calm, considered atmosphere instead of energetic visuals.", + "avoid_for": "Contexts that need visual heat or pop — the navy + warm-yellow Cormorant aesthetic is intentionally low-tempo.", + "slide_count": 9 + } + ] +} diff --git a/skills/hyperframes/templates/presentations-index.json b/skills/hyperframes/templates/presentations-index.json new file mode 100644 index 000000000..b3b4ba876 --- /dev/null +++ b/skills/hyperframes/templates/presentations-index.json @@ -0,0 +1,711 @@ +{ + "schema_version": 1, + "generated_at": "2026-05-14T23:03:11.859Z", + "template_count": 34, + "templates": [ + { + "slug": "8-bit-orbit", + "name": "8-Bit Orbit", + "tagline": "Pixel-art neon arcade aesthetic on a deep navy void.", + "mood": ["retro-tech", "playful", "cyberpunk", "energetic"], + "occasion": [ + "gaming pitch", + "hackathon demo", + "web3 / crypto deck", + "indie product launch", + "developer tools", + "synthwave brand" + ], + "tone": ["geeky", "neon", "rebellious", "sci-fi"], + "formality": "low", + "density": "medium", + "scheme": "dark", + "best_for": "Anything that should feel like a CRT screen at 2am: cyberpunk, gaming, web3, indie dev tools, hackathon demos. Just as good for a tech talk that wants to lean into nostalgic-digital craft, a synthwave brand deck, or a creative review that wants to feel like a console.", + "avoid_for": "Contexts where the dark neon palette would actively work against the message — quiet institutional finance disclosures, healthcare patient-facing materials, traditional luxury.", + "slide_count": 10 + }, + { + "slug": "biennale-yellow", + "name": "Biennale Yellow", + "tagline": "Solar yellow on warm parchment with deep indigo serif and atmospheric sun-glow gradients.", + "mood": ["editorial", "atmospheric", "warm", "cultural-institution", "poster-like"], + "occasion": [ + "exhibition or biennale", + "arts institution programme", + "design or typography conference", + "literary or curatorial publication", + "studio annual report", + "museum season announcement" + ], + "tone": ["literary", "considered", "contemplative", "warm-modern", "Dutch-editorial"], + "formality": "high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like an art-biennale poster or a museum's annual programme: exhibition decks, arts-institution announcements, design conference brochures, curatorial pitches, literary publications, studio retrospectives. Equally good for any deck wanting Dutch-editorial atmosphere with an unmistakable single-color signature.", + "avoid_for": "Decks that need visual punch or saturated multi-color energy — the warm-paper canvas and one-yellow palette are intentionally quiet and atmospheric.", + "slide_count": 8 + }, + { + "slug": "block-frame", + "name": "BlockFrame", + "tagline": "Neobrutalist deck with pastel-neon color blocks and chunky black borders.", + "mood": ["bold", "playful", "graphic", "fresh"], + "occasion": [ + "creative agency pitch", + "indie SaaS launch", + "designer portfolio", + "brand redesign", + "modern startup deck" + ], + "tone": ["confident", "graphic", "pop", "design-led"], + "formality": "medium-low", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel pop-graphic and design-led: indie SaaS launches, agency credentials, creative reviews, brand redesigns. Also a strong unexpected pick for tech, finance, or research when the speaker wants to land as confident and contemporary rather than buttoned-up.", + "avoid_for": "Contexts that require quiet institutional restraint or traditional weight (regulated disclosures, formal legal briefs).", + "slide_count": 10 + }, + { + "slug": "blue-professional", + "name": "Blue Professional", + "tagline": "Cream paper background with electric cobalt blue accents; clean modern professional.", + "mood": ["professional", "modern", "calm", "trustworthy"], + "occasion": [ + "B2B SaaS pitch", + "consulting deliverable", + "internal review", + "advisory pitch", + "investor update" + ], + "tone": ["clean", "considered", "polished", "neutral"], + "formality": "medium-high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel modern-considered and lightly authoritative: B2B SaaS pitches, consulting deliverables, advisory updates, investor reports. Also a clean, tasteful choice whenever you want to read as professional without going stiff — research synthesis, internal reviews, brand work for service businesses.", + "avoid_for": "Contexts where the deck should feel hot, playful, or intentionally informal — the cool electric-blue restraint will read as overly polished.", + "slide_count": 10 + }, + { + "slug": "bold-poster", + "name": "Bold Poster", + "tagline": "Editorial poster aesthetic with massive Shrikhand display and a single fire-engine red accent.", + "mood": ["bold", "editorial", "loud", "confident"], + "occasion": [ + "brand manifesto", + "creative-led pitch", + "magazine / editorial", + "founder vision deck", + "art / culture" + ], + "tone": ["dramatic", "graphic", "sharp", "intentional"], + "formality": "medium", + "density": "low", + "scheme": "light", + "best_for": "Anything that should land like a magazine cover: brand manifestos, founder vision decks, editorial / cultural pitches, creative reviews. Excellent any time you want a few words to feel like a poster — including unexpected fits like a tech keynote or a finance manifesto that wants to be quotable.", + "avoid_for": "Decks that need to communicate dense information per slide — the layout is built around a few large statements, not paragraphs of detail.", + "slide_count": 10 + }, + { + "slug": "broadside", + "name": "Broadside", + "tagline": "Dark editorial canvas with a single fire orange accent and bilingual Latin/Chinese type stack.", + "mood": ["editorial", "dramatic", "loud", "newspaper"], + "occasion": [ + "brand manifesto", + "founder vision deck", + "magazine / cultural pitch", + "design talk", + "bilingual EN/CN deck", + "campaign launch" + ], + "tone": ["graphic", "punchy", "literary", "considered"], + "formality": "medium-high", + "density": "medium", + "scheme": "dark", + "best_for": "Anything that should land like a broadside newspaper headline: brand manifestos, magazine and cultural pitches, design talks, bilingual EN/CN decks, founder vision statements. Also a striking pick for tech, research, or business decks that want a dramatic single-accent editorial feel.", + "avoid_for": "Decks that need to feel quiet, warm, or institutionally traditional — the dark canvas with fire-orange accent commits to drama.", + "slide_count": 20 + }, + { + "slug": "capsule", + "name": "Capsule", + "tagline": "Modular pill-shaped cards on warm bone with a full pastel-pop palette.", + "mood": ["playful", "modern", "warm", "fresh", "fun"], + "occasion": [ + "lifestyle brand", + "creator portfolio", + "DTC product launch", + "wellness or beauty pitch", + "Y2K-tinged brand work" + ], + "tone": ["upbeat", "graphic", "approachable", "cool"], + "formality": "medium-low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel modular, modern, and a little Y2K: lifestyle brands, creator portfolios, DTC launches, beauty / wellness, agency credentials. Also fun for a playful tech demo or a research deck that wants pop-art clarity instead of gravitas.", + "avoid_for": "Contexts that require traditional institutional weight — the capsule shapes and pastel pops actively soften authority.", + "slide_count": 10 + }, + { + "slug": "cartesian", + "name": "Cartesian", + "tagline": "Quiet warm-neutral palette with classical Playfair serifs; tasteful and unhurried.", + "mood": ["quiet", "considered", "elegant", "warm-minimal"], + "occasion": [ + "investment thesis", + "white paper", + "advisory deliverable", + "research report", + "book / longform pitch", + "gallery / cultural" + ], + "tone": ["classical", "literary", "restrained", "confident-quiet"], + "formality": "high", + "density": "low", + "scheme": "light", + "best_for": "Anything that should feel quiet, considered, and grown-up: investment theses, white papers, advisory work, longform research, gallery / cultural decks. Also a strong choice for editorial features, founder reflections, or any deck where restraint is the message — including across tech and finance.", + "avoid_for": "Decks that need visual heat, multiple accents, or a sense of urgency — the warm-neutral palette is intentionally low-energy.", + "slide_count": 10 + }, + { + "slug": "cobalt-grid", + "name": "Cobalt Grid", + "tagline": "Electric cobalt serifs on a graph-paper canvas, anchored by stair-stepped pixel-glitch decorations and slim hairline rules.", + "mood": ["editorial", "design-research", "studious", "modernist", "tech-print", "monochrome"], + "occasion": [ + "design trend or research report", + "studio annual or seasonal bulletin", + "creative agency capabilities deck", + "art or architecture publication", + "academic / curatorial publication", + "newsletter or zine pitch" + ], + "tone": ["considered", "literary", "studious", "quietly-modern", "editorial"], + "formality": "high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like a quietly serious design / research bulletin, art publication, or curated trend report. Strong for studio annuals, agency capabilities decks, design-research publications, architecture / art / academic decks, and any deck wanting one strict accent colour and a printed-ledger calmness rather than corporate polish.", + "avoid_for": "Decks that need warmth, multi-colour energy, or a casual / playful voice — the strict cobalt + cream + grid palette is intentionally austere.", + "slide_count": 8 + }, + { + "slug": "coral", + "name": "Coral", + "tagline": "Cream and coral on near-black, set in oversized Bebas Neue.", + "mood": ["bold", "warm", "modern", "confident"], + "occasion": [ + "fashion / beauty pitch", + "fitness brand", + "F&B brand deck", + "lifestyle launch", + "creative agency" + ], + "tone": ["graphic", "punchy", "magazine"], + "formality": "medium", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel warm-graphic and editorial: fashion, beauty, fitness, F&B, lifestyle brands, agency credentials. Just as strong for a creator portfolio, a manifesto, or a tech / research deck that wants warmth and a single bold accent instead of corporate cool.", + "avoid_for": "Contexts that should feel quiet or institutional — the coral accent and oversized Bebas Neue commit hard to a confident magazine voice.", + "slide_count": 10 + }, + { + "slug": "creative-mode", + "name": "Creative Mode", + "tagline": "Cream paper canvas with confident multi-color (green, pink, orange, yellow) accents and Archivo Black display.", + "mood": ["creative", "confident", "playful", "design-led"], + "occasion": [ + "creative agency pitch", + "design studio deck", + "ad shop credentials", + "brand creative review", + "concept presentation" + ], + "tone": ["graphic", "expressive", "modern"], + "formality": "medium", + "density": "medium-high", + "scheme": "light", + "best_for": "Anything that should feel design-led and confident: creative agency pitches, design studio decks, ad shop credentials, brand creative reviews, art-direction reviews. Also a great unexpected pick for a tech talk, research findings, or finance review when the speaker wants to lead with taste rather than convention.", + "avoid_for": "Contexts that demand institutional restraint and a quiet authority — the saturated multi-accent palette will read as expressive, not formal.", + "slide_count": 8 + }, + { + "slug": "daisy-days", + "name": "Daisy Days", + "tagline": "Cheerful pastel deck with hand-drawn daisies, stars, and rainbows. Friendly, soft, and warm.", + "mood": ["cheerful", "playful", "warm", "sunny", "wholesome"], + "occasion": [ + "education / classroom", + "kids product launch", + "wellness program", + "community workshop", + "creator portfolio (craft / illustration)", + "team kickoff", + "wedding / baby shower planning" + ], + "tone": ["friendly", "soft", "encouraging", "approachable", "lighthearted"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel friendly, soft, and joyful: educational content, kids and family, wellness programs, community workshops, creator portfolios for craft / illustration. Also lovely for an unexpected playful internal kickoff, a wedding planning deck, or any moment where warmth is the message — including across tech or business contexts.", + "avoid_for": "Contexts where the audience explicitly expects authority and precision — the hand-drawn pastel SVG decorations are the opposite of buttoned-up.", + "slide_count": 10 + }, + { + "slug": "editorial-forest", + "name": "Editorial Forest", + "tagline": "Forest green, dusty pink, and warm cream meet Source Serif 4 in a quiet, intentional quarterly-review deck.", + "mood": ["editorial", "quiet", "considered", "warm", "intentional"], + "occasion": [ + "quarterly review", + "internal readout", + "studio update", + "creative agency deck", + "research recap" + ], + "tone": ["literary", "thoughtful", "warm", "low-pressure"], + "formality": "medium", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel like a considered editorial — quarterly reviews, internal readouts, studio updates, creative-agency presentations. Equally good for any deck that wants to feel warm and unhurried rather than corporate, including research recaps, book or program announcements, and team retrospectives.", + "avoid_for": "Contexts that need to feel urgent, punchy, or sales-driven — the palette and rhythm are intentionally quiet.", + "slide_count": 8 + }, + { + "slug": "editorial-tri-tone", + "name": "Editorial Tri-Tone", + "tagline": "Three-color editorial system: dusty pink, mustard cream, and deep burgundy, set in Bricolage + Instrument Serif.", + "mood": ["editorial", "warm", "intentional", "moody"], + "occasion": [ + "editorial / magazine pitch", + "fashion brand deck", + "lifestyle media", + "literary / cultural", + "art direction review" + ], + "tone": ["literary", "warm", "considered", "stylish"], + "formality": "medium-high", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel like a fashion-magazine spread: editorial pitches, fashion brand decks, lifestyle media, art direction reviews. Equally good for any deck — including tech, research, or business — that wants tri-tone discipline and serif/sans contrast instead of the usual neutrals.", + "avoid_for": "Decks that need to read as soft or comforting — the burgundy/pink/cream tri-tone is intentionally high-contrast and styled.", + "slide_count": 8 + }, + { + "slug": "emerald-editorial", + "name": "Emerald Editorial", + "tagline": "A magazine-cover business deck: emerald + navy + paper, double-rule masthead ornaments, and a bold Bodoni-style display serif.", + "mood": ["editorial", "considered", "confident", "magazine-cover"], + "occasion": [ + "leadership presentation", + "quarterly review", + "strategy readout", + "planning office deck", + "executive briefing" + ], + "tone": ["literary", "authoritative", "warm", "designed"], + "formality": "medium-high", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel like the front of a serious magazine, including but not limited to leadership readouts, planning-office reviews, and strategy briefings. The double-rule masthead ornament gives it editorial gravitas without making it stiff — also a great unexpected pick for product launches or research recaps that want to feel considered rather than corporate.", + "avoid_for": "Contexts that need to read as quiet, neutral, or institutionally restrained — the emerald field is too saturated to disappear into the background.", + "slide_count": 8 + }, + { + "slug": "grove", + "name": "Grove", + "tagline": "Forest-green canvas with cream type, classical Playfair serifs, and a single rust accent.", + "mood": ["organic", "considered", "warm", "literary", "natural"], + "occasion": [ + "sustainability brand", + "wellness brand", + "outdoor / nature product", + "winery or restaurant", + "literary or arts deck", + "advisory deliverable", + "bilingual EN/CN deck" + ], + "tone": ["classical", "warm", "considered", "patient"], + "formality": "medium-high", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel organic, considered, and grown-up: sustainability and wellness brands, outdoor / nature products, wineries and restaurants, literary or arts decks, advisory deliverables, bilingual EN/CN reports. Also a calm, distinctive choice for tech, research, or business decks that want patience over urgency.", + "avoid_for": "Decks that need neon energy or rapid-fire pop — the forest-green canvas and Playfair serif commit to a slow, classical voice.", + "slide_count": 12 + }, + { + "slug": "long-table", + "name": "Long Table", + "tagline": "Warm cream and rust-red supper-club aesthetic with bold uppercase grotesk headlines, Fraunces serifs, and pill-shaped outlined buttons.", + "mood": ["warm", "intimate", "modern", "friendly", "small-batch", "social", "hospitality"], + "occasion": [ + "supper club or dinner series", + "event or community gathering", + "small hospitality / restaurant brand", + "creative studio open house", + "membership or subscription pitch", + "wine or food brand catalogue", + "modern lifestyle brand" + ], + "tone": ["warm", "playful", "considered", "social", "magazine-friendly", "modern-editorial"], + "formality": "medium", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like a warm, intimate, modern hospitality / community brand: supper clubs, dinner series, small restaurants, creative-studio events, membership pitches, lifestyle and wine brands. Equally good for any deck wanting a single warm accent colour, mixed-weight typography, and a social-media-aware modern-editorial voice.", + "avoid_for": "Decks that need corporate polish, technical density, or a cold / minimalist register — the rust-red palette and bold serif mix are intentionally warm and people-facing.", + "slide_count": 8 + }, + { + "slug": "mat", + "name": "Mat", + "tagline": "Dark sage canvas with bone paper and burnt-orange accent; mid-century modern with wood undertones.", + "mood": ["warm-modern", "considered", "tactile", "mid-century"], + "occasion": [ + "design studio credentials", + "architecture / interior brand", + "ceramics or craft brand", + "furniture pitch", + "advisory deliverable", + "bilingual EN/CN deck" + ], + "tone": ["warm", "design-led", "intentional", "considered"], + "formality": "medium", + "density": "medium", + "scheme": "mixed", + "best_for": "Anything that should feel mid-century, tactile, and intentional: design studio credentials, architecture / interior brands, ceramics / craft / furniture, advisory decks. Also a warm, distinctive choice for tech, research, or business decks that want a considered analog feel instead of digital-cool.", + "avoid_for": "Contexts that need fast tech energy or institutional restraint — the muted sage and burnt-orange palette is intentionally warm and slow.", + "slide_count": 9 + }, + { + "slug": "monochrome", + "name": "Monochrome", + "tagline": "Ivory ledger paper with all-black type; Lora serif headlines, Jost body, no color at all.", + "mood": ["restrained", "literary", "archival", "ledger"], + "occasion": [ + "user research synthesis", + "white paper", + "longform report", + "academic deck", + "policy brief", + "advisory deliverable", + "bilingual EN/CN deck" + ], + "tone": ["literary", "considered", "neutral", "honest"], + "formality": "high", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel like a hand-typeset ledger: user research synthesis, white papers, longform reports, academic and policy briefs, advisory deliverables, bilingual EN/CN reports. Equally good for tech, design, or brand decks that want their words to be the only thing on the page.", + "avoid_for": "Decks that need visual personality or color-led storytelling — the all-ink palette is intentionally austere.", + "slide_count": 18 + }, + { + "slug": "neo-grid-bold", + "name": "Neo-Grid Bold", + "tagline": "Editorial neo-brutalism with a single neon yellow accent on off-white paper.", + "mood": ["confident", "punchy", "editorial", "modern"], + "occasion": [ + "product launch", + "design review", + "founder pitch", + "brand deck", + "consulting findings", + "conference talk" + ], + "tone": ["bold", "minimal", "design-led", "graphic"], + "formality": "medium", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel confident and editorial-graphic: design-led pitches, brand work, founder talks, conference keynotes. Excellent for stat-heavy slides, comparisons, and process flows. Just as strong for tech, research, or finance when the speaker wants to read as design-led rather than corporate.", + "avoid_for": "Contexts that need to feel quiet, traditional, or warm — the neon-yellow accent and uppercase display commit to a confident editorial voice.", + "slide_count": 13 + }, + { + "slug": "peoples-platform", + "name": "People's Platform (Block & Bold)", + "tagline": "Activist poster energy: blue, orange, red on cream, with Alfa Slab + Caveat Brush.", + "mood": ["activist", "loud", "graphic", "honest"], + "occasion": [ + "cultural commentary", + "manifesto", + "community / civic deck", + "design talk", + "campaign pitch", + "founder vision" + ], + "tone": ["punchy", "direct", "expressive", "warm-bold"], + "formality": "medium-low", + "density": "medium-high", + "scheme": "light", + "best_for": "Anything that should feel honest, loud, and graphic: cultural commentary, manifestos, civic and community decks, design talks, campaign pitches. Excellent for founder-vision moments, mission statements, or any deck — including across industries — that wants protest-poster energy instead of corporate polish.", + "avoid_for": "Contexts where institutional restraint is the actual goal — the saturated political-poster palette commits hard to expressive energy.", + "slide_count": 10 + }, + { + "slug": "pin-and-paper", + "name": "Pin & Paper", + "tagline": "Yellow paper with safety-pin illustrations, ink-blue handwritten Caveat, paper-grain texture.", + "mood": ["crafted", "handmade", "warm", "thoughtful", "literary"], + "occasion": [ + "research findings with personality", + "qualitative report", + "founder reflection", + "creator essay deck", + "workshop debrief" + ], + "tone": ["literary", "intimate", "warm", "grounded"], + "formality": "medium", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel hand-crafted, warm, and literary: qualitative research findings, founder reflections, longform brand stories, workshop debriefs. The signature safety-pin illustrations and paper-grain texture make it especially good for any deck — including tech or business — that wants personality and warmth over polish.", + "avoid_for": "Decks that need to feel digital-native polished or rigorously data-driven — handwritten Caveat is intentionally informal.", + "slide_count": 11 + }, + { + "slug": "pink-script", + "name": "Pink Script — After Hours", + "tagline": "Black canvas, hot pink accent, pearl-cream paper, Instrument Serif headlines: late-night editorial luxury.", + "mood": ["nocturnal", "moody", "intentional", "luxe", "expressive"], + "occasion": [ + "fashion brand deck", + "creator personal brand", + "after-hours product (nightlife / dating / spirits)", + "luxury launch", + "editorial feature" + ], + "tone": ["literary", "sultry", "considered", "magazine"], + "formality": "medium-high", + "density": "low", + "scheme": "dark", + "best_for": "Anything that should feel nocturnal, intentional, and a little luxe: fashion brand decks, creator personal brands, after-hours / nightlife / spirits launches, luxury product reveals, editorial features. Also a striking unexpected pick for a tech keynote, research synthesis, or business pitch that wants to land with magnetic confidence.", + "avoid_for": "Daytime corporate-professional and traditional B2B contexts where the dark canvas with hot-pink accent reads as too styled or too expressive.", + "slide_count": 9 + }, + { + "slug": "playful", + "name": "Playful", + "tagline": "Sun-warm peach background with Syne display: a friendly indie launch deck.", + "mood": ["warm", "approachable", "indie", "friendly"], + "occasion": [ + "creator portfolio", + "indie product launch", + "lifestyle brand", + "small-business pitch", + "newsletter / community" + ], + "tone": ["upbeat", "informal", "welcoming"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel warm, indie, and approachable: creator portfolios, indie product launches, lifestyle brands, small-business pitches, newsletter / community decks. Also welcoming for any deck — including tech or research — that wants to feel friendly and human rather than corporate.", + "avoid_for": "Contexts where institutional credibility matters more than warmth — the peach palette is intentionally informal.", + "slide_count": 10 + }, + { + "slug": "raw-grid", + "name": "Raw Grid", + "tagline": "Neo-brutalist deck with thick borders, offset shadows, and a pink/sage/ink palette.", + "mood": ["raw", "punchy", "energetic", "confident"], + "occasion": [ + "startup pitch", + "accelerator demo day", + "founder pitch", + "indie product launch", + "brand deck", + "creator portfolio" + ], + "tone": ["direct", "modern", "no-nonsense", "graphic"], + "formality": "medium-low", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel direct and graphic-confident: founder pitches, accelerator demos, brand decks, indie launches, creator portfolios. Strong for stat slides, comparison tables, and process flows. Equally good for tech, research, or finance when the speaker wants the deck to feel scrappy-confident rather than buttoned-up.", + "avoid_for": "Contexts that need to feel soft, warm, or intentionally quiet — the brutalist borders and offset shadows commit to a graphic voice.", + "slide_count": 10 + }, + { + "slug": "retro-windows", + "name": "Retro Windows", + "tagline": "Windows 95 chrome: gray title bars, MS Sans Serif, pixel typography, full nostalgia.", + "mood": ["nostalgic", "retro", "geeky", "playful"], + "occasion": [ + "retro gaming pitch", + "Y2K brand", + "creator portfolio (90s aesthetic)", + "tech-history talk", + "shitpost-but-make-it-fancy deck" + ], + "tone": ["winking", "nostalgic", "geeky", "fun"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel knowingly nostalgic: retro gaming, Y2K-aesthetic brands, creator portfolios with a 90s vibe, tech-history talks, deliberately tongue-in-cheek decks. A great choice anywhere a playful retro reference is the entire point.", + "avoid_for": "Decks that need to read as modern, elegant, or institutionally credible — the Win95 chrome will always read as a costume.", + "slide_count": 10 + }, + { + "slug": "retro-zine", + "name": "Retro Zine", + "tagline": "Beige paper with green accent and Bebas Neue + Caveat: a riso-printed zine in HTML form.", + "mood": ["crafted", "lo-fi", "underground", "warm-retro"], + "occasion": [ + "indie zine / publication", + "music or arts brand", + "creator portfolio", + "small-batch / craft launch", + "cultural / community deck" + ], + "tone": ["scrappy", "warm", "intentional", "DIY"], + "formality": "medium-low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel printed, lo-fi, and crafted: indie zines and publications, music / arts brands, creator portfolios, small-batch craft launches, community decks. Also a great underdog choice for tech, research, or business decks that want a riso-print warmth instead of digital polish.", + "avoid_for": "Contexts that demand digital-native polish or fast modern-tech energy — the layered zine aesthetic intentionally feels handmade.", + "slide_count": 10 + }, + { + "slug": "sakura-chroma", + "name": "Sakura Chroma", + "tagline": "Vintage Japanese cassette-package aesthetic: cream paper, diagonal rainbow ribbons, condensed bold type, JIS-style spec checkboxes.", + "mood": ["retro", "playful", "kawaii-tech", "warm", "tactile", "product-catalogue"], + "occasion": [ + "product launch or catalogue", + "indie hardware or analog studio brand", + "music label or release schedule", + "creative studio annual report", + "magazine or zine pitch", + "vintage-flavored brand campaign" + ], + "tone": ["playful", "confident", "warm", "tactile", "80s-Japanese-tech"], + "formality": "low", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel like a vintage Japanese cassette package or a TDK / Sony / Sakura Color product catalogue: indie hardware brand decks, music-label release schedules, analog studio retrospectives, zine and magazine pitches, kawaii-tech product launches, creative-studio annual reports. Equally good for any deck wanting bold colour, condensed display type, and a tactile printed-product personality.", + "avoid_for": "Decks that need restrained, corporate, or quiet typography — the bold condensed lockups, ribbon stripes, and primary-colour palette are intentionally loud and product-page-y.", + "slide_count": 8 + }, + { + "slug": "scatterbrain", + "name": "Scatterbrain", + "tagline": "Post-it inspired: pastel sticky notes, Caveat handwriting, Shrikhand and Zilla Slab type stack.", + "mood": ["playful", "creative", "warm", "messy-on-purpose", "workshop"], + "occasion": [ + "brainstorm / workshop", + "creative agency credentials", + "design-thinking session", + "ideation pitch", + "art-direction review" + ], + "tone": ["informal", "warm", "expressive", "human"], + "formality": "low", + "density": "high", + "scheme": "light", + "best_for": "Anything that should feel like a designer's whiteboard: brainstorms, workshops, creative-agency credentials, design-thinking sessions, ideation pitches, art-direction reviews. Equally fun for any deck — including tech, research, or business — that wants to read as in-progress thinking rather than polished conclusions.", + "avoid_for": "Contexts that demand precision and institutional weight — the post-it sticky-note aesthetic intentionally reads as warm and unfinished.", + "slide_count": 10 + }, + { + "slug": "signal", + "name": "Signal", + "tagline": "Deep navy canvas with bone paper and a single muted-gold accent; institutional with quiet weight.", + "mood": ["institutional", "trustworthy", "considered", "weighty"], + "occasion": [ + "investor deck", + "consulting deliverable", + "board presentation", + "legal / policy brief", + "academic deck", + "advisory pitch", + "bilingual EN/CN deck" + ], + "tone": ["sober", "polished", "established", "literary"], + "formality": "high", + "density": "high", + "scheme": "mixed", + "best_for": "Anything that should feel weighty, considered, and credibly institutional: investor decks, board presentations, consulting deliverables, legal / policy briefs, advisory pitches. Also a strong choice for tech, research, or brand work that wants to read as quietly authoritative rather than loud.", + "avoid_for": "Contexts that should feel hot, fast, or intentionally playful — the navy + gold restraint commits to a sober voice.", + "slide_count": 18 + }, + { + "slug": "soft-editorial", + "name": "Soft Editorial", + "tagline": "Cormorant Garamond serif on warm paper with sage, blush, and lemon accents.", + "mood": ["literary", "elegant", "quiet", "warm-classical"], + "occasion": [ + "editorial feature", + "longform brand story", + "gallery or museum", + "literary pitch", + "advisory deliverable", + "wedding / lifestyle media" + ], + "tone": ["literary", "considered", "warm", "magazine"], + "formality": "high", + "density": "low", + "scheme": "light", + "best_for": "Anything that should feel literary, elegant, and unhurried: editorial features, longform brand stories, gallery / museum decks, advisory deliverables, wedding / lifestyle media, founder essays. Equally good for tech, research, or business decks that want a Sunday-supplement warmth instead of corporate polish.", + "avoid_for": "Decks that need visual heat or punch — the warm-paper palette and Cormorant serif are intentionally quiet.", + "slide_count": 12 + }, + { + "slug": "stencil-tablet", + "name": "Stencil & Tablet", + "tagline": "Bone paper with stencil-cut headlines and a six-color earth palette: archaeology meets brand.", + "mood": ["archival", "earthy", "tactile", "considered", "graphic"], + "occasion": [ + "museum / cultural institution", + "art / architecture brand", + "longform research", + "heritage / craft brand", + "manifesto" + ], + "tone": ["weighty", "considered", "tactile", "literary"], + "formality": "medium-high", + "density": "medium", + "scheme": "light", + "best_for": "Anything that should feel archival, tactile, and weighty-graphic: museum and cultural-institution decks, art / architecture brands, longform research, heritage and craft brands, manifestos. A great choice anytime — including across tech and business — when you want the deck to feel like a field manual rather than a slide deck.", + "avoid_for": "Contexts that demand digital-native polish or playful pop — the stencil-cut display and earth-tone palette commit to a deliberate analog feel.", + "slide_count": 11 + }, + { + "slug": "studio", + "name": "Studio", + "tagline": "Black canvas with electric-yellow type; high-voltage design studio aesthetic.", + "mood": ["electric", "bold", "graphic", "design-led", "high-contrast"], + "occasion": [ + "design studio credentials", + "creative agency pitch", + "brand showcase", + "art-direction review", + "fashion / sneaker brand", + "bilingual EN/CN deck" + ], + "tone": ["graphic", "loud", "modern", "intentional"], + "formality": "medium", + "density": "medium", + "scheme": "dark", + "best_for": "Anything that should feel electric and design-led: studio credentials, creative agency pitches, brand showcases, art-direction reviews, fashion / sneaker brand work. Also a striking unexpected choice for tech, research, or business decks where the speaker wants the deck to *be* a brand statement.", + "avoid_for": "Contexts that should feel quiet or institutional — the black-and-electric-yellow palette is the loudest in the library.", + "slide_count": 12 + }, + { + "slug": "vellum", + "name": "Vellum", + "tagline": "Deep navy canvas with warm-yellow Cormorant serifs and a single dusty teal accent. A quiet, scholarly aesthetic.", + "mood": ["scholarly", "literary", "considered", "quiet", "intellectual"], + "occasion": [ + "research findings", + "white paper or longform report", + "academic or university deck", + "advisory deliverable", + "literary or editorial pitch", + "founder reflection / vision deck", + "bilingual EN/CN deck" + ], + "tone": ["literary", "considered", "patient", "intelligent"], + "formality": "high", + "density": "low", + "scheme": "dark", + "best_for": "Anything that should feel scholarly, literary, and quietly intelligent: research synthesis, white papers, academic and policy briefs, advisory deliverables, longform editorial pieces, founder reflections. Equally strong for any deck — including tech, business, or creator work — that wants a calm, considered atmosphere instead of energetic visuals.", + "avoid_for": "Contexts that need visual heat or pop — the navy + warm-yellow Cormorant aesthetic is intentionally low-tempo.", + "slide_count": 9 + } + ] +}