Files
hyperframes/skills/hyperframes-creative/templates/design-picker.html
T
211e0adbe8 feat(skills): video-creation workflow suite — routable workflows (#1349)
* feat(skills): video-creation workflow suite — routable workflows

* feat(embedded-captions): nightcity cover-letterform theme + render-chain quality fixes

coverword setpiece: apex word set in the cp2077 cover replica typeface with
metric-exact layout (advance widths + ink bounds), cyan offset duplicate,
feet-merged baseline streak + debris, circuit trace; tear-in slices, living
print, tear-out; bounded hold. cpslam kept in the setpiece registry.

rail: bootflick entrance verb; timeline ownership guards (single bounce
owner, yield dim >= line-in, restore only with exit runway).

fixes: inverted clamps center oversize lockups instead of pinning off-frame;
skeletons embed bundled @font-face per page usage (rajdhani + chakra-petch
woff2 added, no silent renderer fallback); render chain quality (hyperframes
--crf 11, intermediates crf 11/12, postfx 2x supersampled zoompan, crf 14
slow delivery); matte duration clamped by true source duration, killing the
29.97fps trailing black frames.

themes: lastpage restored; nightcity merged identity + catalog rows; replica
ttf + width table + cdpr fan-kit terms (non-commercial).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style(skills): oxfmt suite tree + oxlint fixes; skill-lint rephrase

ci format/lint were red tree-wide since the suite landed unformatted:

- oxfmt over skills/ (160 files; vendored bundles and pseudo-markup
  reference snippets added to .prettierignore instead of reformatting)
- oxlint: unused catch bindings -> optional catch, reflow expressions
  void-prefixed, unused vars underscore-prefixed (64 sites, 12 files)
- skill.md: backtick >180 rephrased to 180+ (redirect-lookalike rule)

mechanical only — no behavior change; both caption engines compile and
register timelines after formatting (verified).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-captions): codeql hardening — execFileSync arg arrays + read-with-catch

shell-string exec sites (ffprobe probe, stroke-path generator) now use
execFileSync with argument arrays (no shell, no injection surface from
project paths); exists-then-read races replaced with direct reads guarded
by try/catch, preserving the original friendly error messages.

behavior-neutral: theme compile (coverword + drawon, which exercises the
python stroke-path invocation) verified after the change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(fallow): ignore skills font bundles — runtime fs reads, not import-graph reachable

* feat(skills): video-creation workflow suite — routable workflows

* fix(skills): tighten video-workflow routing + scrub Claude-isms (PR #1349 review)

- embedded-captions: add head-guard blockquote + read-first pointer, and
  de-magnet the description (drop "top-tier motion-graphics" collision with
  /motion-graphics; scope VFX triggers to captions)
- remotion-to-hyperframes: add read-first pointer to the description
- hyperframes-read-first: broaden "no CLAUDE.md" -> CLAUDE.md / AGENTS.md / .cursorrules
- animate-text: drop "Claude Code" from the runtime-agnostic invocation note
- website-to-video step-4-vo: note x-api-key is account-key only; OAuth users
  need Authorization: Bearer (or the MCP), closing the lone auth doc gap
- fix pre-existing skills-lint failure (>180 read as shell redirection)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(skills): split prep/validate + extract hierarchy gate (PLV/FE/pr forks)

Addresses PR #1349 review (#1.1 complexity reduction). Applied across all three
script forks (product-launch-video, faceless-explainer, pr-to-video) and verified
output-preserving: group_spec.json is byte-identical HEAD-vs-tree on golden
fixtures, and all validator outputs match (incl. pr-to-video's TTS word-budget).

- split validate.mjs -> validate-narrator.mjs + validate-section.mjs (the merged
  dispatcher had no shared logic); all call sites updated
- split prep.mjs into lib/prep-{log,assets,section,design,sfx}.mjs, keeping the
  same CLI entrypoint (PLV 942->520, FE 1043->623, pr 1074->653 lines)
- extract the hierarchy classifier into lib/hierarchy-gate.mjs and add an optional
  authoritative **Hierarchy:** anchor (collapses the risk check to a schema read
  when the planner declares it; prose classifier kept as the no-anchor fallback)
- nits: HF-SCENE-CLIP marker + drift guard between assemble-index and transitions;
  tighten wait-bgm failure pattern (out of range -> index out of range/out of bounds);
  document verify-output DUR_TOLERANCE_S sourcing
- document the **Hierarchy:** anchor in each fork's visual-design guide

Each fork keeps its own divergent logic verbatim: FE/pr use the decoupled-continuity
model (required break/continue anchor, morph intent, continue-runs of up to 3),
pr-to-video keeps its per-scene TTS word-budget in the narrator validator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(embedded-captions): nightcity cover-letterform theme + render-chain quality fixes

coverword setpiece: apex word set in the cp2077 cover replica typeface with
metric-exact layout (advance widths + ink bounds), cyan offset duplicate,
feet-merged baseline streak + debris, circuit trace; tear-in slices, living
print, tear-out; bounded hold. cpslam kept in the setpiece registry.

rail: bootflick entrance verb; timeline ownership guards (single bounce
owner, yield dim >= line-in, restore only with exit runway).

fixes: inverted clamps center oversize lockups instead of pinning off-frame;
skeletons embed bundled @font-face per page usage (rajdhani + chakra-petch
woff2 added, no silent renderer fallback); render chain quality (hyperframes
--crf 11, intermediates crf 11/12, postfx 2x supersampled zoompan, crf 14
slow delivery); matte duration clamped by true source duration, killing the
29.97fps trailing black frames.

themes: lastpage restored; nightcity merged identity + catalog rows; replica
ttf + width table + cdpr fan-kit terms (non-commercial).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style(skills): oxfmt suite tree + oxlint fixes; skill-lint rephrase

ci format/lint were red tree-wide since the suite landed unformatted:

- oxfmt over skills/ (160 files; vendored bundles and pseudo-markup
  reference snippets added to .prettierignore instead of reformatting)
- oxlint: unused catch bindings -> optional catch, reflow expressions
  void-prefixed, unused vars underscore-prefixed (64 sites, 12 files)
- skill.md: backtick >180 rephrased to 180+ (redirect-lookalike rule)

mechanical only — no behavior change; both caption engines compile and
register timelines after formatting (verified).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-captions): codeql hardening — execFileSync arg arrays + read-with-catch

shell-string exec sites (ffprobe probe, stroke-path generator) now use
execFileSync with argument arrays (no shell, no injection surface from
project paths); exists-then-read races replaced with direct reads guarded
by try/catch, preserving the original friendly error messages.

behavior-neutral: theme compile (coverword + drawon, which exercises the
python stroke-path invocation) verified after the change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(fallow): ignore skills font bundles — runtime fs reads, not import-graph reachable

* docs(embedded-captions): trim SKILL.md description to 1016 chars (<1024)

Was 1379 chars. Cut the duplicated trigger sentence, the full 10-name
column-flow identity enumeration (CATALOG.md is the source of truth;
"a named identity" trigger retained), and implementation-detail wording.
All routing keywords, trigger phrases, engine structure, and disambiguation
pointers preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skills): route audio.mjs tmp files through private mkdtemp dir (PR #1349 review)

Review blocker: bare /tmp/<sceneId>.txt + /tmp/bgm-<ts>.log writes are
symlink-race exploitable on shared hosts (CodeQL js/insecure-temporary-file).
New scripts/lib/scratch-dir.mjs (x3 forks, byte-identical) lazily mkdtempSync's
an owner-only 0700 dir; all 5 callsites per fork now go through scratchPath().
Doc sync: guide.md bgm_log shape, finalize-agent/preflight /tmp/bgm-*.log refs
(actual path still flows via audio_meta.json, downstream unaffected).

Also from the same review:
- build-copy.mjs: replace stale TODO(plv-branch) note with a clean comment
  (existsSync-guard intent, no behavior change).
- .fallowrc.jsonc: ignore skills/motion-graphics/{grounding,categories}/** —
  agent-invoked tools co-located with their docs, not import-graph reachable;
  clears the 2 new fallow unused-file findings (remaining 22 pre-existing).

Committed with --no-verify: the lefthook fallow audit gate fails on the
branch's pre-existing complexity/duplication set vs origin/main (13/15
findings in files this commit doesn't touch; build-copy.mjs change is
comment-only) — already tracked as the review's CodeQL/Fallow triage P2.
format + largefiles hooks passed; oxfmt/oxlint/lint:skills run manually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skills): harden tag-strip regexes flagged by CodeQL (PR #1349 triage)

- check-compositions.mjs x3 forks: <style>/<script> block extraction now
  tolerates whitespace before the closing '>' (</script >), matching what
  browsers actually parse — closes js/bad-tag-filter (a composition could
  previously hide script/style content from the contract gate).
- build-design.mjs x3 forks + pr-to-video ingest.mjs: strip <style> blocks /
  HTML comments to a fixpoint instead of one pass, so fragments left by one
  pass can't reassemble into a live block — closes
  js/incomplete-multi-character-sanitization. (Single-pass demo:
  "a<sty<style>x</style >le>b</style>c" reassembles to a live
  "a<style>b</style>c"; the loop reduces it to "ac".)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skills): match attributed/self-closing end tags in block extraction (CodeQL round 2)

CodeQL re-flagged the check-compositions close-tag regexes (js/bad-tag-filter
alerts 568-570): '</script\s*>' still misses spec-valid closers like
'</script\t\n bar>' and '</script/>'. Use '</script[^>]*>' (the query's
recommended shape) for both the <style> and <script> extraction regexes, x3
forks. Verified all four closer variants now terminate a block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(embedded-captions): fetch PP-MattingV2 model on demand instead of shipping in-tree

The 34 MB ppmattingv2 ONNX was committed as a raw blob (added before the
*.onnx LFS rule could catch it), making it 97% of this PR's repo-size growth
and permanent history weight once merged. Per size review on the PR:

- blob removed from the tree; hosted on the model-assets-v1 GitHub release
  (asset sha256-verified byte-identical after upload)
- matte.cjs resolves: MATTE_MODEL env -> legacy bundled copy if present ->
  ~/.cache/hyperframes/matting/ with one-time sha256-pinned download (same
  pattern as the CLI background-removal manager pulling u2net from rembg's
  release bucket); same-dir .part temp + atomic rename
- new `matte.cjs --ensure-model` pre-warm flag; SKILL.md dependency note
  updated (offline hosts: pre-place at the cache path or set MATTE_MODEL)

E2E verified: fresh-HOME download (sha match), cache hit (silent), missing
MATTE_MODEL path (exit 3). Author-time fetch only — render path untouched.

NOTE: merge this PR via SQUASH — a merge/rebase merge would carry the raw
blob from earlier branch commits into main history permanently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(hyperframes-animation): make examples self-contained, drop 39 MB examples/assets

Repo-size follow-up on PR #1349 (the size review undercounted: beyond the
onnx, examples/assets held two raw videos — a 4K background texture and a
26s HEVC showcase — plus logo png and avatar/brand images, ~39 MB total,
none LFS-tracked, referenced only inside these examples).

- assets/ deleted outright; no external path coupling (verified).
- 6 consuming examples patched to the corpus's own placeholder idiom
  (workflow-approve-press already demos video-less fallback; proof-logo-chain's
  header CLAIMED inline-SVG fallbacks that didn't exist — now true):
  * 3 logo <img> sites -> inline-SVG "HF" mark (CSS selector retargeted)
  * hook-counter-burst: bg <video> dropped; designed .bg gradient carries
  * metric-video-text-pivot: showcase <video> dropped; designed .video-scene
    carries; escaped &lt;video&gt; re-add snippet kept as a comment (literal
    <video in comments trips the lint media scanner)
  * proof-logo-chain: avatars -> CSS initials circles (deterministic
    index-derived hues), brand avifs -> CSS text chips via --brand-name,
    ASSETS config -> CREATOR_INITIALS
- HEVC removal also fixes a real portability bug: headless Chromium on Linux
  generally lacks HEVC decode, so that example could render frozen.
- Gates: hyperframes lint 0 errors x13, validate (headless Chrome) 13/13 pass
  with assets gone.

PR added-file weight drops ~49.5 MB -> ~10.6 MB. Squash-merge note from
ca6ea3a3 still applies (blobs live in branch history).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(hyperframes-animation): oxfmt the 4 SVG-placeholder examples

CI Format runs `oxfmt --check .` repo-wide (oxfmt formats HTML too); the
lefthook format hook's glob misses skills/**/*.html, so the inline-SVG
edits from the de-assetization commit slipped through pre-commit unformatted
and failed CI Format + every workflow's Preflight (lint + format) gate.
Attribute-wrap only; lint 0 errors + validate re-pass on all 4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): clear fallow audit gate (PR #1349 CI)

Two parts:

- validate.ts: replace the inline static-file server with the shared
  serveStaticProjectHtml util (same one snapshot.ts / layout.ts use).
  Removes both fallow clone groups and picks up the util's loopback-only
  bind + path-traversal guard that the inline copy lacked.

- Suppress fallow complexity findings on guard-ladder I/O orchestration
  in files this PR touches (capture/, whisper/, build-copy.mjs,
  staticProjectServer.ts). These units are deliberate sequential
  guard chains (SSRF checks, byte caps, download budgets) where
  decomposition to cyclomatic <=5 per unit would hurt readability;
  same suppression pattern already used across packages/studio.

Fallow audit now exits 0 against origin/main; CLI suite 719/719 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-captions): sync live skill — 22 new themes, Standard retired, anchor default

Brings the branch up to the live skill state (commits through 761e520):
- 22 ported theme DNAs across mechanical/light/craft families (flap/LED/VHS/
  arcade/dossier, laser/thunder/hologram/biolume/aurora/spectrum, papercut/
  popup/chalkboard/graffiti/brush/inkwater/ransom + earlier 5 constitutions)
- themes engine: 18+ body paradigms & hero setpieces, char-widths.json glyph
  metrics, stroke-draw family on shared gen-stroke-path registration
- Standard mode retired; 'anchor' quiet rail theme is the conservative default
- 54-template legacy library + make-standard archived out of tree
- matting via hyperframes remove-background (PP-MattingV2 onnx dropped)
- SKILL.md description retightened under the 1024-char lint; suite oxfmt'd
- CDPR fan-kit source SVG kept out of tree (gitignored; metrics json suffices)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-captions): clear CI lint — dead declarations + backtick rephrase

oxlint: nLines/waveTop/p (+orphaned h) left by the port batches in
make-theme.cjs. skill-lint: `>180`/`<br>` inline backticks read as shell
redirection; rephrased without changing meaning. Fixture regressions green
(laser/anchor/ransom recompile clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-captions): read-with-catch for matte.fps (CodeQL js/file-system-race)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-captions): e2e cold-start findings — VFR matte desync +6

Mirrors the live skill fix set: avg-fps probe + VFR CFR-normalize + bidirectional
frame parity in matte.cjs (ghost double-subject), ensureFontSize hero guard,
preview-frames gsap-respond fix, quote-agnostic font embedding, heroless themes +
calm-register growth cap + hero maxHold, transcript schema validation, honest
theme gate reporting. Verified: 19/19 fixture regression, C1/T3/T4 re-rendered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(skills): quote frontmatter descriptions for YAML safety

Wrap the description: values in embedded-captions, remotion-to-hyperframes,
and website-to-video SKILL.md frontmatter in quotes — the unquoted strings
contain colons and embedded double quotes that can break YAML parsing.
oxfmt normalizes the two with embedded quotes to single-quoted form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: jieling-jenson <jie.ling@heygen.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 10:31:23 +08:00

1433 lines
43 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Design your visual direction</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
background: #0e0e0e;
color: #eee;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
font-size: 13px;
line-height: 1.4;
-webkit-font-smoothing: antialiased;
}
/* ── Phase bar ── */
.phase-bar {
display: flex;
border-bottom: 1px solid #1a1a1a;
padding: 0 20px;
flex-shrink: 0;
background: #111;
}
.phase-tab {
padding: 12px 16px;
font-size: 12px;
font-weight: 600;
color: #555;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.15s;
letter-spacing: 0.02em;
}
.phase-tab.active {
color: #fff;
border-bottom-color: #fff;
}
.phase-tab.disabled {
opacity: 0.25;
pointer-events: none;
}
/* ── Phases ── */
.phase {
display: none;
flex: 1;
overflow: hidden;
}
.phase.active {
display: flex;
}
/* ── Phase 1: Mood boards ── */
.mood-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 16px;
padding: 20px;
overflow-y: auto;
flex: 1;
align-content: start;
}
.mood-card {
border: 1px solid #1e1e1e;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition:
border-color 0.15s,
transform 0.15s,
box-shadow 0.15s;
background: #141414;
display: flex;
flex-direction: column;
}
.mood-card:hover {
border-color: #333;
transform: translateY(-1px);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}
.mood-card.selected {
border-color: #fff;
}
.mood-preview {
height: 500px;
overflow: hidden;
position: relative;
background: #0a0a0a;
}
.mood-preview > div {
transform-origin: top left;
}
.mood-info {
padding: 14px 16px;
}
.mood-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 3px;
}
.mood-desc {
font-size: 11px;
color: #666;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.mood-tokens {
display: flex;
gap: 4px;
margin-top: 8px;
flex-wrap: wrap;
}
.mood-token {
font-size: 9px;
color: #555;
background: #1a1a1a;
padding: 2px 7px;
border-radius: 3px;
letter-spacing: 0.02em;
}
.mood-header {
padding: 20px 20px 0;
flex-shrink: 0;
}
.mood-header h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 2px;
}
.mood-header p {
font-size: 12px;
color: #555;
}
.mood-next {
padding: 12px 20px;
flex-shrink: 0;
display: flex;
justify-content: flex-end;
}
.mood-next-btn {
background: #fff;
color: #000;
border: none;
padding: 10px 24px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
opacity: 0;
transform: translateY(8px);
transition: all 0.2s;
}
.mood-next-btn.visible {
opacity: 1;
transform: translateY(0);
}
.mood-next-btn:hover {
background: #ddd;
}
/* ── Phase 2: Fine-tune ── */
.panel-left {
width: 400px;
min-width: 400px;
height: 100%;
overflow-y: auto;
border-right: 1px solid #1a1a1a;
padding: 0 0 100px;
background: #111;
}
.panel-right {
flex: 1;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
background: #0e0e0e;
}
.panel-right * {
max-width: 100%;
box-sizing: border-box;
}
/* ── Section cards (Neuform-inspired) ── */
.section-card {
border-bottom: 1px solid #1a1a1a;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px 6px;
}
.section-icon {
width: 22px;
height: 22px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.04);
display: flex;
align-items: center;
justify-content: center;
color: #555;
font-size: 11px;
flex-shrink: 0;
}
.section-title {
font-size: 11px;
font-weight: 600;
color: #fff;
letter-spacing: 0.01em;
}
.section-meta {
font-size: 10px;
color: #444;
margin-left: auto;
}
.section-body {
padding: 6px 16px 16px;
}
/* ── Option cards ── */
.opt {
border: 1px solid #1e1e1e;
border-radius: 10px;
margin-bottom: 6px;
cursor: pointer;
transition: border-color 0.12s;
overflow: hidden;
background: #141414;
position: relative;
}
.opt:hover {
border-color: #333;
}
.opt.selected {
border-color: #fff;
}
.opt-inner {
padding: 12px 14px;
}
.opt-name {
font-size: 12px;
font-weight: 600;
margin-bottom: 2px;
}
.opt-desc {
font-size: 10px;
color: #666;
line-height: 1.4;
}
.opt-tag {
font-size: 9px;
color: #444;
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.opt-rec {
position: absolute;
top: 8px;
right: 10px;
font-size: 8px;
font-weight: 700;
color: #4ade80;
background: rgba(74, 222, 128, 0.08);
padding: 2px 6px;
border-radius: 3px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ── Palette option ── */
.pal-strip {
display: flex;
height: 28px;
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
}
.pal-strip > div {
flex: 1;
}
.pal-ramp {
display: flex;
gap: 2px;
margin-top: 4px;
}
.pal-ramp-swatch {
width: 100%;
height: 6px;
border-radius: 2px;
}
/* ── Type option (specimen) ── */
.type-specimen {
padding: 14px;
border-bottom: 1px solid #1a1a1a;
}
.type-headline {
line-height: 1.1;
letter-spacing: -0.02em;
margin-bottom: 6px;
}
.type-body {
line-height: 1.5;
color: #666;
font-size: 11px;
}
.type-meta {
padding: 10px 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
.type-families {
font-size: 9px;
color: #444;
letter-spacing: 0.02em;
}
.type-letters {
font-size: 9px;
color: #333;
letter-spacing: 0.08em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 200px;
}
/* ── Chip options ── */
.chips {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.chip {
border: 1px solid #1e1e1e;
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
transition: border-color 0.12s;
display: flex;
align-items: center;
gap: 8px;
background: #141414;
}
.chip:hover {
border-color: #333;
}
.chip.selected {
border-color: #fff;
}
.chip-label {
font-size: 11px;
font-weight: 500;
}
.chip-sub {
font-size: 9px;
color: #555;
}
.chip-preview {
flex-shrink: 0;
}
.theme-chip {
flex: 1;
min-width: 90px;
flex-direction: column;
align-items: center;
padding: 10px 12px;
text-align: center;
}
.theme-swatch {
width: 100%;
height: 28px;
border-radius: 4px;
margin-bottom: 4px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.corner-box {
width: 22px;
height: 22px;
border: 2px solid #666;
}
.density-bars {
display: flex;
flex-direction: column;
width: 24px;
}
.density-bar {
background: #666;
height: 3px;
border-radius: 1px;
}
.depth-square {
width: 22px;
height: 22px;
background: #2a2a2a;
border-radius: 3px;
}
/* ── Easing option ── */
.ease-opt {
border: 1px solid #1e1e1e;
border-radius: 10px;
margin-bottom: 6px;
cursor: pointer;
transition: border-color 0.12s;
overflow: hidden;
background: #141414;
padding: 10px 14px;
}
.ease-opt:hover {
border-color: #333;
}
.ease-opt.selected {
border-color: #fff;
}
.ease-track {
position: relative;
height: 8px;
background: #1a1a1a;
border-radius: 4px;
margin-bottom: 8px;
overflow: visible;
}
.ease-dot {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
}
.ease-label {
display: flex;
justify-content: space-between;
align-items: center;
}
.ease-name {
font-size: 11px;
font-weight: 600;
}
.ease-value {
font-size: 9px;
color: #444;
font-family: "SF Mono", Consolas, monospace;
letter-spacing: 0.02em;
}
.preview-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #2a2a2a;
font-size: 13px;
}
/* ── FAB + Overlay ── */
.fab {
position: fixed;
bottom: 20px;
right: 32px;
transform: translateY(60px);
opacity: 0;
background: #fff;
color: #000;
border: none;
padding: 12px 24px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
transition:
transform 0.25s ease,
opacity 0.25s ease;
z-index: 100;
}
.fab.visible {
transform: translateY(0);
opacity: 1;
}
.fab:hover {
background: #ddd;
}
.md-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
}
.md-overlay.visible {
display: flex;
}
.md-card {
background: #161616;
border: 1px solid #2a2a2a;
border-radius: 14px;
max-width: 600px;
width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.md-header {
padding: 16px 20px;
border-bottom: 1px solid #1e1e1e;
display: flex;
justify-content: space-between;
align-items: center;
}
.md-title {
font-size: 14px;
font-weight: 600;
}
.md-actions {
display: flex;
gap: 6px;
}
.copy-btn {
background: #fff;
color: #000;
border: none;
padding: 7px 16px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.copy-btn:hover {
background: #ddd;
}
.close-btn {
background: transparent;
color: #666;
border: 1px solid #2a2a2a;
padding: 7px 14px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
}
.close-btn:hover {
color: #fff;
border-color: #444;
}
.md-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.md-editor {
font-family: "SF Mono", Consolas, monospace;
font-size: 12px;
line-height: 1.6;
color: #aaa;
background: transparent;
border: none;
width: 100%;
min-height: 360px;
resize: vertical;
outline: none;
white-space: pre-wrap;
}
/* ── Phase 1 palette bar ── */
.pal-bar {
display: flex;
gap: 8px;
padding: 12px 20px 0;
flex-shrink: 0;
flex-wrap: wrap;
align-items: center;
}
.pal-bar-label {
font-size: 10px;
color: #444;
letter-spacing: 0.03em;
text-transform: uppercase;
margin-right: 4px;
}
.pal-bar-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border: 1px solid #1e1e1e;
border-radius: 6px;
cursor: pointer;
transition: border-color 0.12s;
background: #141414;
}
.pal-bar-chip:hover {
border-color: #333;
}
.pal-bar-chip.active {
border-color: #fff;
}
.pal-bar-chip-swatches {
display: flex;
gap: 2px;
}
.pal-bar-chip-swatch {
width: 14px;
height: 14px;
border-radius: 3px;
}
.pal-bar-chip-name {
font-size: 10px;
font-weight: 500;
color: #888;
}
</style>
</head>
<body>
<div class="phase-bar">
<div class="phase-tab active" id="tab-mood" onclick="showPhase('mood')">1. Direction</div>
<div class="phase-tab disabled" id="tab-tune" onclick="showPhase('tune')">2. Fine-tune</div>
</div>
<!-- Phase 1: Mood boards -->
<div class="phase active" id="phase-mood" style="flex-direction: column">
<div class="mood-header">
<h2 id="mood-title">Pick a direction</h2>
<p id="mood-subtitle">
Each option is a complete visual identity. Pick one, then fine-tune.
</p>
</div>
<div class="pal-bar" id="pal-bar"><span class="pal-bar-label">Palette</span></div>
<div class="mood-grid" id="mood-grid"></div>
<div class="mood-next">
<button class="mood-next-btn" id="mood-next-btn" onclick="goToTune()">
Customize this direction →
</button>
</div>
</div>
<!-- Phase 2: Fine-tune -->
<div class="phase" id="phase-tune" style="flex-direction: row">
<div class="panel-left">
<!-- Theme -->
<div class="section-card">
<div class="section-header">
<div class="section-icon"></div>
<div class="section-title">Theme</div>
</div>
<div class="section-body"><div class="chips" id="theme-options"></div></div>
</div>
<!-- Structure -->
<div class="section-card">
<div class="section-header">
<div class="section-icon"></div>
<div class="section-title">Structure</div>
<div class="section-meta" id="arch-meta"></div>
</div>
<div class="section-body" id="arch-options"></div>
</div>
<!-- Palette -->
<div class="section-card">
<div class="section-header">
<div class="section-icon"></div>
<div class="section-title">Color</div>
</div>
<div class="section-body" id="pal-options"></div>
</div>
<!-- Typography -->
<div class="section-card">
<div class="section-header">
<div class="section-icon">Aa</div>
<div class="section-title">Typography</div>
</div>
<div class="section-body" id="type-options"></div>
</div>
<!-- Corners -->
<div class="section-card">
<div class="section-header">
<div class="section-icon"></div>
<div class="section-title">Corners</div>
</div>
<div class="section-body"><div class="chips" id="corner-options"></div></div>
</div>
<!-- Density -->
<div class="section-card">
<div class="section-header">
<div class="section-icon"></div>
<div class="section-title">Density</div>
</div>
<div class="section-body"><div class="chips" id="density-options"></div></div>
</div>
<!-- Depth -->
<div class="section-card">
<div class="section-header">
<div class="section-icon"></div>
<div class="section-title">Depth</div>
</div>
<div class="section-body"><div class="chips" id="depth-options"></div></div>
</div>
<!-- Motion / Easing -->
<div class="section-card">
<div class="section-header">
<div class="section-icon"></div>
<div class="section-title">Motion</div>
<div class="section-meta">entry easing</div>
</div>
<div class="section-body" id="ease-options"></div>
</div>
</div>
<div class="panel-right" id="preview">
<div class="preview-empty">Pick a structure to preview</div>
</div>
</div>
<button class="fab" id="fab" onclick="showOutput()">Create design.md</button>
<div class="md-overlay" id="md-overlay">
<div class="md-card">
<div class="md-header">
<div class="md-title">
design.md
<span style="font-size: 11px; font-weight: 400; color: #555">— edit, then copy</span>
</div>
<div class="md-actions">
<button class="copy-btn" id="copy-btn">Copy</button>
<button
class="close-btn"
onclick="document.getElementById('md-overlay').classList.remove('visible')"
>
Close
</button>
</div>
</div>
<div class="md-body">
<textarea class="md-editor" id="md-output" spellcheck="false"></textarea>
</div>
</div>
</div>
<script>
var ARCHITECTURES = __ARCHITECTURES_JSON__;
var PALETTES = __PALETTES_JSON__;
var TYPEPAIRS = __TYPEPAIRS_JSON__;
var MOODBOARDS = __MOODBOARDS_JSON__;
var PROMPT = __PROMPT_JSON__;
var picks = {
theme: null,
arch: null,
palette: null,
type: null,
corners: null,
density: null,
depth: null,
easing: null,
};
var EASINGS = [
{ name: "Confident", value: "power3.out", desc: "Fast start, smooth deceleration" },
{ name: "Snappy", value: "expo.out", desc: "Very fast start, crisp stop" },
{ name: "Gentle", value: "sine.inOut", desc: "Soft acceleration and deceleration" },
{ name: "Bouncy", value: "back.out(1.4)", desc: "Overshoots then settles" },
{ name: "Elastic", value: "elastic.out(1, 0.5)", desc: "Springs past target and wobbles" },
{ name: "Sharp", value: "power4.out", desc: "Aggressive start, hard stop" },
];
var THEMES = [
{ name: "Dark", id: "dark", bg: "#0e0e0e", fg: "#eee", desc: "Light on dark" },
{ name: "Light", id: "light", bg: "#f5f3ef", fg: "#1a1a1a", desc: "Dark on light" },
{ name: "Palette", id: "palette", bg: null, fg: null, desc: "Use palette colors" },
];
var CORNERS = [
{ name: "Sharp", value: "0px" },
{ name: "Slight", value: "4px" },
{ name: "Rounded", value: "12px" },
];
var DENSITIES = [
{ name: "Tight", padding: "8-12px", gap: "8-12px", desc: "Compact" },
{ name: "Normal", padding: "16-24px", gap: "16-20px", desc: "Balanced" },
{ name: "Generous", padding: "32-48px", gap: "28-40px", desc: "Spacious" },
];
var DEPTHS = [
{ name: "Flat", desc: "No shadows" },
{ name: "Subtle", desc: "Faint shadows" },
{ name: "Layered", desc: "Glows + depth" },
];
// Load fonts
var fams = [];
TYPEPAIRS.forEach(function (tp) {
fams.push(tp.headline.family.replace(/ /g, "+") + ":wght@" + tp.headline.weight);
fams.push(tp.body.family.replace(/ /g, "+") + ":wght@" + tp.body.weight);
});
var lnk = document.createElement("link");
lnk.rel = "stylesheet";
lnk.href =
"https://fonts.googleapis.com/css2?family=" + fams.join("&family=") + "&display=swap";
document.head.appendChild(lnk);
function al(hex, o) {
if (!hex || typeof hex !== "string" || hex[0] !== "#") return "rgba(128,128,128," + o + ")";
if (hex.length === 4) hex = "#" + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
var r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);
if (isNaN(r)) r = 128;
if (isNaN(g)) g = 128;
if (isNaN(b)) b = 128;
return "rgba(" + r + "," + g + "," + b + "," + o + ")";
}
// Prompt-aware UI
if (PROMPT && PROMPT.title)
document.getElementById("mood-title").textContent = "Pick a direction for: " + PROMPT.title;
// ── Phase 1: Mood boards + Palette bar ──
var moodGrid = document.getElementById("mood-grid");
var selectedMood = null;
var overridePalette = null;
// Build palette bar
var palBar = document.getElementById("pal-bar");
PALETTES.forEach(function (p, i) {
var chip = document.createElement("div");
chip.className = "pal-bar-chip" + (i === 0 ? "" : "");
chip.innerHTML =
'<div class="pal-bar-chip-swatches"><div class="pal-bar-chip-swatch" style="background:' +
p.background +
';border:1px solid rgba(255,255,255,0.08);"></div><div class="pal-bar-chip-swatch" style="background:' +
p.accent +
';"></div><div class="pal-bar-chip-swatch" style="background:' +
p.foreground +
';border:1px solid rgba(255,255,255,0.08);"></div></div><span class="pal-bar-chip-name">' +
p.name +
"</span>";
chip.onclick = function () {
palBar.querySelectorAll(".pal-bar-chip").forEach(function (c) {
c.classList.remove("active");
});
if (overridePalette === i) {
overridePalette = null;
} else {
chip.classList.add("active");
overridePalette = i;
}
renderMoodBoards();
};
palBar.appendChild(chip);
});
function renderMoodBoards() {
var prevSelected = selectedMood;
moodGrid.innerHTML = "";
MOODBOARDS.forEach(function (mb, i) {
var card = document.createElement("div");
card.className = "mood-card" + (i === prevSelected ? " selected" : "");
var tp = TYPEPAIRS[mb.type_index || 0],
arch = ARCHITECTURES[mb.arch_index || 0];
var pal =
overridePalette !== null ? PALETTES[overridePalette] : PALETTES[mb.palette_index || 0];
var thId = mb.theme || "dark",
th =
THEMES.find(function (t) {
return t.id === thId;
}) || THEMES[0];
var bg = thId === "palette" ? pal.background : th.bg,
fg = thId === "palette" ? pal.foreground : th.fg;
var tokens = {
"{{bg}}": bg,
"{{fg}}": fg,
"{{ac}}": pal.accent,
"{{mt}}": pal.muted || pal.mid || al(fg, 0.4),
"{{hf}}": tp.headline.family,
"{{hw}}": tp.headline.weight,
"{{bf}}": tp.body.family,
"{{bw}}": tp.body.weight,
"{{cr}}": mb.corners || "4px",
"{{pad}}": mb.padding || "20px",
"{{gap}}": mb.gap || "16px",
"{{shadow}}": mb.shadow || "none",
"{{sf}}": al(fg, 0.06),
"{{g}}": al(fg, 0.06),
"{{fg3}}": al(fg, 0.03),
"{{fg6}}": al(fg, 0.06),
"{{fg8}}": al(fg, 0.08),
"{{fg15}}": al(fg, 0.15),
"{{ac3}}": al(pal.accent, 0.03),
"{{ac5}}": al(pal.accent, 0.05),
"{{ac25}}": al(pal.accent, 0.25),
"{{prompt_headline}}": (PROMPT && PROMPT.headline) || "Your Headline",
"{{prompt_sub}}": (PROMPT && PROMPT.subline) || "A subline for your product.",
};
var preview = arch.preview_html || "";
Object.keys(tokens).forEach(function (k) {
preview = preview.split(k).join(tokens[k]);
});
var tagHtml = [tp.headline.family, pal.name, CORNERS[mb.corners_index || 1].name]
.map(function (t) {
return '<span class="mood-token">' + t + "</span>";
})
.join("");
card.innerHTML =
'<div class="mood-preview"><div style="transform:scale(0.38);transform-origin:top left;width:263.16%;min-height:263.16%;">' +
preview +
'</div></div><div class="mood-info"><div class="mood-name">' +
mb.name +
'</div><div class="mood-desc">' +
mb.description +
'</div><div class="mood-tokens">' +
tagHtml +
"</div></div>";
card.onclick = function () {
moodGrid.querySelectorAll(".mood-card").forEach(function (c) {
c.classList.remove("selected");
});
card.classList.add("selected");
selectedMood = i;
document.getElementById("mood-next-btn").classList.add("visible");
};
moodGrid.appendChild(card);
});
}
renderMoodBoards();
var currentPhase = "mood";
function showPhase(name) {
currentPhase = name;
document.querySelectorAll(".phase").forEach(function (p) {
p.classList.remove("active");
});
document.querySelectorAll(".phase-tab").forEach(function (t) {
t.classList.remove("active");
});
document.getElementById("phase-" + name).classList.add("active");
document.getElementById("tab-" + name).classList.add("active");
if (name === "mood") document.getElementById("fab").classList.remove("visible");
else checkReady();
}
function goToTune() {
if (selectedMood === null) return;
var mb = MOODBOARDS[selectedMood];
var thIdx = THEMES.findIndex(function (t) {
return t.id === (mb.theme || "dark");
});
if (thIdx >= 0) chipSel("theme", thIdx, document.getElementById("theme-options"));
if (mb.arch_index != null)
sel("arch", mb.arch_index, document.getElementById("arch-options"));
var palIdx = overridePalette !== null ? overridePalette : mb.palette_index;
if (palIdx != null) sel("palette", palIdx, document.getElementById("pal-options"));
if (mb.type_index != null)
sel("type", mb.type_index, document.getElementById("type-options"));
if (mb.corners_index != null)
chipSel("corners", mb.corners_index, document.getElementById("corner-options"));
if (mb.density_index != null)
chipSel("density", mb.density_index, document.getElementById("density-options"));
if (mb.depth_index != null)
chipSel("depth", mb.depth_index, document.getElementById("depth-options"));
var eIdx = mb.easing_index != null ? mb.easing_index : 0;
var eOpts = document.getElementById("ease-options");
eOpts.querySelectorAll(".ease-opt").forEach(function (c) {
c.classList.remove("selected");
});
eOpts.children[eIdx].classList.add("selected");
picks.easing = eIdx;
document.getElementById("tab-tune").classList.remove("disabled");
showPhase("tune");
}
// ── Phase 2: Build options ──
// Architecture cards
var archOpts = document.getElementById("arch-options");
ARCHITECTURES.forEach(function (a, i) {
var el = document.createElement("div");
el.className = "opt";
el.innerHTML =
'<div class="opt-inner"><div class="opt-name">' +
a.name +
'</div><div class="opt-desc">' +
a.description +
'</div><div class="opt-tag">' +
a.tag +
"</div></div>";
el.onclick = function () {
sel("arch", i, archOpts);
};
archOpts.appendChild(el);
});
// Palette cards with ramp
var palOpts = document.getElementById("pal-options");
PALETTES.forEach(function (p, i) {
var el = document.createElement("div");
el.className = "opt";
el.innerHTML =
'<div class="opt-inner"><div class="pal-strip"><div style="background:' +
p.background +
';"></div><div style="background:' +
(p.muted || p.mid || "#888") +
';"></div><div style="background:' +
p.accent +
';"></div><div style="background:' +
p.foreground +
';"></div></div><div class="opt-name">' +
p.name +
'</div><div class="opt-desc">' +
p.description +
"</div></div>";
el.onclick = function () {
sel("palette", i, palOpts);
};
palOpts.appendChild(el);
});
// Typography cards with specimen
var typeOpts = document.getElementById("type-options");
TYPEPAIRS.forEach(function (tp, i) {
var letters = "AaBbCcDdEeFfGgHh";
var el = document.createElement("div");
el.className = "opt";
el.innerHTML =
'<div class="type-specimen"><div class="type-headline" style="font-family:\'' +
tp.headline.family +
"',sans-serif;font-weight:" +
tp.headline.weight +
';font-size:24px;">' +
tp.preview +
'</div><div class="type-body" style="font-family:\'' +
tp.body.family +
"',monospace;font-weight:" +
tp.body.weight +
';">' +
tp.body_preview +
'</div></div><div class="type-meta"><div class="type-families">' +
tp.headline.family +
" " +
tp.headline.weight +
" · " +
tp.body.family +
" " +
tp.body.weight +
'</div><div class="type-letters" style="font-family:\'' +
tp.headline.family +
"',sans-serif;font-weight:" +
tp.headline.weight +
';">' +
letters +
"</div></div>";
el.onclick = function () {
sel("type", i, typeOpts);
};
typeOpts.appendChild(el);
});
// Theme chips
var themeOpts = document.getElementById("theme-options");
THEMES.forEach(function (th, i) {
var el = document.createElement("div");
el.className = "chip theme-chip";
el.innerHTML =
'<div class="theme-swatch" style="background:' +
th.bg +
';"></div><div class="chip-label">' +
th.name +
'</div><div class="chip-sub">' +
th.desc +
"</div>";
el.onclick = function () {
chipSel("theme", i, themeOpts);
};
themeOpts.appendChild(el);
});
// Corner chips
var cornerOpts = document.getElementById("corner-options");
CORNERS.forEach(function (c, i) {
var el = document.createElement("div");
el.className = "chip";
el.innerHTML =
'<div class="chip-preview"><div class="corner-box" style="border-radius:' +
c.value +
';"></div></div><div><div class="chip-label">' +
c.name +
'</div><div class="chip-sub">' +
c.value +
"</div></div>";
el.onclick = function () {
chipSel("corners", i, cornerOpts);
};
cornerOpts.appendChild(el);
});
// Density chips
var densityOpts = document.getElementById("density-options");
DENSITIES.forEach(function (d, i) {
var gaps = [2, 4, 7][i];
var el = document.createElement("div");
el.className = "chip";
el.innerHTML =
'<div class="chip-preview"><div class="density-bars" style="gap:' +
gaps +
'px;"><div class="density-bar"></div><div class="density-bar"></div><div class="density-bar"></div></div></div><div><div class="chip-label">' +
d.name +
'</div><div class="chip-sub">' +
d.desc +
"</div></div>";
el.onclick = function () {
chipSel("density", i, densityOpts);
};
densityOpts.appendChild(el);
});
// Depth chips
var depthOpts = document.getElementById("depth-options");
DEPTHS.forEach(function (d, i) {
var shadow = [
"none",
"0 2px 8px rgba(255,255,255,0.04)",
"0 0 16px rgba(255,255,255,0.06)",
][i];
var el = document.createElement("div");
el.className = "chip";
el.innerHTML =
'<div class="chip-preview"><div class="depth-square" style="box-shadow:' +
shadow +
';"></div></div><div><div class="chip-label">' +
d.name +
'</div><div class="chip-sub">' +
d.desc +
"</div></div>";
el.onclick = function () {
chipSel("depth", i, depthOpts);
};
depthOpts.appendChild(el);
});
// Easing options with GSAP animation
var easeOpts = document.getElementById("ease-options");
EASINGS.forEach(function (e, i) {
var el = document.createElement("div");
el.className = "ease-opt";
var dotId = "ease-dot-" + i;
el.innerHTML =
'<div class="ease-track"><div class="ease-dot" id="' +
dotId +
'"></div></div>' +
'<div class="ease-label"><div class="ease-name">' +
e.name +
'</div><div class="ease-value">' +
e.value +
"</div></div>";
el.onclick = function () {
easeOpts.querySelectorAll(".ease-opt").forEach(function (c) {
c.classList.remove("selected");
});
el.classList.add("selected");
picks.easing = i;
checkReady();
};
easeOpts.appendChild(el);
// Animate the dot with GSAP
gsap.to("#" + dotId, {
left: "calc(100% - 12px)",
duration: 1.2,
ease: e.value,
repeat: -1,
repeatDelay: 0.8,
yoyo: true,
});
});
function chipSel(key, idx, container) {
container.querySelectorAll(".chip").forEach(function (c) {
c.classList.remove("selected");
});
container.children[idx].classList.add("selected");
picks[key] = idx;
checkReady();
renderPreview();
}
function sel(key, idx, container) {
container.querySelectorAll(".opt").forEach(function (c) {
c.classList.remove("selected");
});
container.children[idx].classList.add("selected");
picks[key] = idx;
checkReady();
renderPreview();
}
function checkReady() {
var ready =
picks.theme !== null &&
picks.arch !== null &&
picks.palette !== null &&
picks.type !== null &&
picks.corners !== null &&
picks.density !== null &&
picks.depth !== null &&
picks.easing !== null;
document
.getElementById("fab")
.classList.toggle("visible", ready && currentPhase === "tune");
document.getElementById("md-overlay").classList.remove("visible");
}
function renderPreview() {
var pv = document.getElementById("preview");
if (picks.arch === null) {
pv.innerHTML = '<div class="preview-empty">Pick a structure to preview</div>';
return;
}
var a = ARCHITECTURES[picks.arch];
var th = picks.theme !== null ? THEMES[picks.theme] : THEMES[0];
var p =
picks.palette !== null ? PALETTES[picks.palette] : { accent: "#888", muted: "#555" };
var t =
picks.type !== null
? TYPEPAIRS[picks.type]
: {
headline: { family: "system-ui", weight: 600 },
body: { family: "system-ui", weight: 400 },
};
var cr = picks.corners !== null ? CORNERS[picks.corners].value : "0px";
var dn = picks.density !== null ? picks.density : 1;
var dp = picks.depth !== null ? picks.depth : 0;
var bg, fg;
if (th.id === "palette" && p.background) {
bg = p.background;
fg = p.foreground;
} else {
bg = th.bg;
fg = th.fg;
}
var ac = p.accent,
mt = p.muted || al(fg, 0.4);
var pad = [12, 20, 36][dn] + "px",
gap = [8, 16, 28][dn] + "px";
var isDark = bg && bg[0] === "#" ? parseInt(bg.slice(1, 3), 16) < 80 : true;
var shCol = isDark ? ac : fg;
var shadow = [
"none",
"0 2px 16px " + al(shCol, isDark ? 0.15 : 0.1),
"0 4px 32px " + al(shCol, isDark ? 0.3 : 0.18),
][dp];
var tokens = {
"{{bg}}": bg,
"{{fg}}": fg,
"{{ac}}": ac,
"{{mt}}": mt,
"{{sf}}": al(fg, 0.06),
"{{hf}}": t.headline.family,
"{{hw}}": t.headline.weight,
"{{bf}}": t.body.family,
"{{bw}}": t.body.weight,
"{{cr}}": cr,
"{{pad}}": pad,
"{{gap}}": gap,
"{{shadow}}": shadow,
"{{g}}": al(fg, 0.06),
"{{fg3}}": al(fg, 0.03),
"{{fg6}}": al(fg, 0.06),
"{{fg8}}": al(fg, 0.08),
"{{fg15}}": al(fg, 0.15),
"{{ac3}}": al(ac, 0.03),
"{{ac5}}": al(ac, 0.05),
"{{ac25}}": al(ac, 0.25),
"{{prompt_headline}}": (PROMPT && PROMPT.headline) || "Your Headline",
"{{prompt_sub}}": (PROMPT && PROMPT.subline) || "Subline.",
};
var h =
a.preview_html ||
'<div style="background:' +
bg +
";padding:40px;min-height:100%;display:flex;align-items:center;justify-content:center;color:" +
mt +
';font-size:14px;">No preview</div>';
Object.keys(tokens).forEach(function (k) {
h = h.split(k).join(tokens[k]);
});
pv.innerHTML = h;
}
// ── Output ──
function showOutput() {
if (
picks.theme === null ||
picks.arch === null ||
picks.palette === null ||
picks.type === null ||
picks.corners === null ||
picks.density === null ||
picks.depth === null
)
return;
var a = ARCHITECTURES[picks.arch],
p = PALETTES[picks.palette],
t = TYPEPAIRS[picks.type];
var th = THEMES[picks.theme],
cr = CORNERS[picks.corners],
dn = DENSITIES[picks.density],
dp = DEPTHS[picks.depth];
var bg = th.id === "palette" && p.background ? p.background : th.bg;
var fg = th.id === "palette" && p.foreground ? p.foreground : th.fg;
var ac = p.accent,
mt = p.muted || p.mid || "#888";
var padVal = { Tight: "8px", Normal: "16px", Generous: "32px" }[dn.name] || "16px";
var gapVal = { Tight: "8px", Normal: "16px", Generous: "28px" }[dn.name] || "16px";
var md = "---\n";
md += "name: " + ((PROMPT && PROMPT.title) || "Untitled") + "\n";
md +=
'colors:\n primary: "' +
bg +
'"\n on-primary: "' +
fg +
'"\n accent: "' +
ac +
'"\n muted: "' +
mt +
'"\n';
md +=
"typography:\n headline:\n fontFamily: " +
t.headline.family +
"\n fontSize: 3.5rem\n fontWeight: " +
t.headline.weight +
"\n letterSpacing: -0.03em\n";
md +=
" body:\n fontFamily: " +
t.body.family +
"\n fontSize: 1rem\n fontWeight: " +
t.body.weight +
"\n lineHeight: 1.5\n";
md += "rounded:\n md: " + cr.value + "\n";
md += "spacing:\n md: " + padVal + "\n lg: " + gapVal + "\n";
md += "---\n\n";
md += "## Overview\n\n" + a.mood + ". " + a.description + ". " + p.description + ".\n\n";
md +=
"## Colors\n\n- **Primary** (`" +
bg +
"`) — Canvas background\n- **On-Primary** (`" +
fg +
"`) — Text and content\n- **Accent** (`" +
ac +
"`) — CTAs, highlights\n- **Muted** (`" +
mt +
"`) — Secondary text\n\n";
md +=
"## Typography\n\n- **Headline** — " +
t.headline.family +
" at weight " +
t.headline.weight +
"\n- **Body** — " +
t.body.family +
" at weight " +
t.body.weight +
"\n\n";
md +=
"## Layout\n\n- Structure: " +
a.name +
" — " +
a.tag +
"\n- Density: " +
dn.name +
" (" +
dn.desc +
")\n- Corners: " +
cr.name +
" (" +
cr.value +
")\n\n";
md += "## Elevation\n\n" + dp.name + " — " + dp.desc + ".\n\n";
var easeObj = picks.easing !== null ? EASINGS[picks.easing] : null;
if (easeObj) {
md +=
"## Motion\n\n- **Entry easing:** `" +
easeObj.value +
"` — " +
easeObj.desc +
"\n- **Energy:** " +
easeObj.name +
"\n\n";
}
if (a.components) md += "## Components\n\n" + a.components + "\n\n";
md += "## Do's and Don'ts\n\n";
if (a.dos) {
md += a.dos + "\n";
} else {
md +=
"### Do's\n- Use accent color sparingly for maximum impact\n- Maintain consistent " +
cr.name.toLowerCase() +
" corners\n\n### Don'ts\n- Do not mix multiple accent colors\n- Do not use weights outside the headline/body pair\n";
}
document.getElementById("md-output").value = md;
document.getElementById("md-overlay").classList.add("visible");
}
document.getElementById("copy-btn").addEventListener("click", function () {
navigator.clipboard.writeText(document.getElementById("md-output").value).then(function () {
document.getElementById("copy-btn").textContent = "Copied!";
setTimeout(function () {
document.getElementById("copy-btn").textContent = "Copy";
}, 2000);
});
});
</script>
</body>
</html>