first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: audio-jingle
|
||||
description: |
|
||||
Audio generation skill — jingles, beds, voiceover, and sound effects.
|
||||
Routes music requests to Suno V5 / Udio / Lyria, speech to MiniMax
|
||||
TTS / FishAudio / ElevenLabs V3, and SFX to ElevenLabs SFX or
|
||||
AudioCraft. Output is one MP3/WAV file saved to the project folder.
|
||||
triggers:
|
||||
- "music"
|
||||
- "jingle"
|
||||
- "bed"
|
||||
- "voiceover"
|
||||
- "tts"
|
||||
- "sound effect"
|
||||
- "音乐"
|
||||
- "配音"
|
||||
- "音效"
|
||||
od:
|
||||
mode: audio
|
||||
surface: audio
|
||||
scenario: marketing
|
||||
preview:
|
||||
type: html
|
||||
entry: example.html
|
||||
design_system:
|
||||
requires: false
|
||||
example_prompt: |
|
||||
A 30-second upbeat indie-pop jingle for a coffee shop launch — warm
|
||||
electric piano lead, brushed drums, gentle bass, a single sun-soaked
|
||||
"ahhh" choir on the chorus. No vocals. Loop-friendly tail.
|
||||
---
|
||||
|
||||
# Audio Jingle Skill
|
||||
|
||||
Three sub-modes. The active project's `audioKind` decides which one
|
||||
runs:
|
||||
|
||||
| `audioKind` | Models we route to | Plan focus |
|
||||
|---|---|---|
|
||||
| `music` | Suno V5 (default), Udio, Lyria 2 | genre + tempo + instrumentation |
|
||||
| `speech` | MiniMax TTS (default), Fish, ElevenLabs V3 | script + voice + pacing |
|
||||
| `sfx` | ElevenLabs SFX (default), AudioCraft | texture + impact + duration |
|
||||
|
||||
## Resource map
|
||||
|
||||
```
|
||||
audio-jingle/
|
||||
├── SKILL.md
|
||||
└── example.html
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 0 — Read the project metadata
|
||||
|
||||
`audioKind`, `audioModel`, `audioDuration` (seconds), and (for speech)
|
||||
`voice`. Branch by `audioKind` and use the values verbatim — no
|
||||
clarifying form unless something is marked `(unknown — ask)`.
|
||||
|
||||
Important: `voice` is provider-specific. For `minimax-tts`, `--voice`
|
||||
must be a valid MiniMax `voice_id` (for example `male-qn-qingse`), not
|
||||
a natural-language description. If you only have a prose voice brief
|
||||
("warm female narrator", "neutral Mandarin"), keep that in your plan
|
||||
but omit `--voice` so the daemon's default voice id applies, or ask the
|
||||
user to choose a specific id.
|
||||
|
||||
### Step 1 — Plan
|
||||
|
||||
**Music**
|
||||
- Genre + reference artists (1-2)
|
||||
- Tempo (BPM) + key
|
||||
- Instrumentation (3-5 instruments max)
|
||||
- Vocals: yes / no / hummed / choir
|
||||
- Mood arc (intro → chorus → outro)
|
||||
|
||||
**Speech**
|
||||
- Script (final, not draft — TTS runs verbatim)
|
||||
- Voice target + pacing
|
||||
For MiniMax this means a real `voice_id`, not prose in `--voice`
|
||||
- Pronunciation hints for proper nouns / acronyms
|
||||
|
||||
**SFX**
|
||||
- Texture (impact / whoosh / ambience / foley)
|
||||
- Duration + envelope (sharp attack vs. gentle swell)
|
||||
- Layering note (single hit vs. stacked)
|
||||
|
||||
State the plan in 2-3 sentences before dispatching.
|
||||
|
||||
### Step 2 — Compose the prompt
|
||||
|
||||
Use the format the upstream model prefers. Bind `audioDuration` to the
|
||||
API parameter directly; never put "make it 30 seconds" in prose.
|
||||
|
||||
### Step 3 — Dispatch via the media contract
|
||||
|
||||
Use the unified dispatcher — do **not** call provider APIs by hand:
|
||||
|
||||
```bash
|
||||
node "$OD_BIN" media generate \
|
||||
--project "$OD_PROJECT_ID" \
|
||||
--surface audio \
|
||||
--audio-kind "<music|speech|sfx>" \
|
||||
--model "<audioModel from metadata>" \
|
||||
--duration <audioDuration seconds> \
|
||||
[--voice "<provider voice id (speech only)>"] \
|
||||
--output "<short-slug>-<duration>s.mp3" \
|
||||
--prompt "<assembled prompt from Step 2 — for speech, the literal script>"
|
||||
```
|
||||
|
||||
The command prints one line of JSON: `{"file": {"name": "...", ...}}`.
|
||||
The bytes land in the project; the FileViewer renders the audio
|
||||
transport controls automatically.
|
||||
|
||||
### Step 4 — Hand off
|
||||
|
||||
Reply with: plan summary, the filename returned by the dispatcher, and
|
||||
one sentence on what to try if the user wants a variation (e.g. "swap
|
||||
tempo from 92 to 108 BPM" rather than "make it different").
|
||||
|
||||
## Hard rules
|
||||
|
||||
- TTS runs your script **literally**. Proof it before dispatching —
|
||||
even one stray comma changes the cadence.
|
||||
- MiniMax TTS rejects free-form voice prose in `--voice`. Use a real
|
||||
MiniMax `voice_id` (for example `male-qn-qingse`) or omit the flag
|
||||
and let the daemon's default voice apply.
|
||||
- Music: under 30s = single section; 30–90s = intro + body; 90s+ =
|
||||
full arc. Don't try to fit a 3-act song into 15 seconds.
|
||||
- SFX: prefer one well-described layer over a paragraph of "make it
|
||||
cool" — generators reward specific texture words.
|
||||
- Save the file every turn. The audio viewer shows transport controls
|
||||
the moment the file lands.
|
||||
@@ -0,0 +1,128 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Audio jingle — example</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5efe5;
|
||||
--panel: #ffffff;
|
||||
--ink: #1c1b1a;
|
||||
--muted: #8b8579;
|
||||
--accent: #c96442;
|
||||
--grid: #e6dfd1;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--ink);
|
||||
font-family: 'Iowan Old Style', 'Charter', Georgia, serif; }
|
||||
body { min-height: 100dvh; display: grid; place-items: center; padding: 32px; }
|
||||
.card {
|
||||
width: min(640px, 92vw);
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
padding: 26px 28px 22px;
|
||||
box-shadow: 0 16px 40px rgba(28,27,26,0.10), 0 1px 2px rgba(28,27,26,0.05);
|
||||
border: 1px solid rgba(28,27,26,0.06);
|
||||
}
|
||||
.row1 { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
|
||||
.icon {
|
||||
width: 44px; height: 44px; border-radius: 50%;
|
||||
background: var(--accent); color: #fff;
|
||||
display: grid; place-items: center;
|
||||
box-shadow: 0 6px 18px rgba(201, 100, 66, 0.35);
|
||||
}
|
||||
.icon svg { width: 22px; height: 22px; }
|
||||
.title { margin: 0; font-size: 20px; line-height: 1.2; }
|
||||
.sub { font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 11px; color: var(--muted); letter-spacing: 0.14em; text-transform: uppercase; margin-top: 2px; }
|
||||
|
||||
.wave {
|
||||
display: flex; align-items: end; gap: 3px;
|
||||
height: 96px; padding: 0 4px;
|
||||
border-top: 1px dashed var(--grid);
|
||||
border-bottom: 1px dashed var(--grid);
|
||||
}
|
||||
.wave span {
|
||||
flex: 1; background: linear-gradient(180deg, var(--accent), #a4502f);
|
||||
border-radius: 2px;
|
||||
animation: bob 2s ease-in-out infinite;
|
||||
animation-delay: var(--d, 0s);
|
||||
}
|
||||
@keyframes bob {
|
||||
0%, 100% { height: var(--h, 30%); }
|
||||
50% { height: calc(var(--h, 30%) * 1.6); }
|
||||
}
|
||||
|
||||
.transport {
|
||||
margin-top: 14px;
|
||||
display: grid; grid-template-columns: auto 1fr auto auto; gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.play {
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
background: var(--ink); color: #fff;
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.timeline {
|
||||
height: 4px; border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--accent) 0 32%, var(--grid) 32% 100%);
|
||||
}
|
||||
.time {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 11px; color: var(--muted);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.badge {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 10px; color: var(--accent);
|
||||
letter-spacing: 0.18em; text-transform: uppercase;
|
||||
padding: 4px 8px; border-radius: 999px;
|
||||
background: rgba(201, 100, 66, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="row1">
|
||||
<div class="icon" aria-hidden>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="title">A 30s coffee-shop launch jingle.</h1>
|
||||
<div class="sub">suno-v5 · 92 BPM · loop-friendly tail</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wave" aria-hidden>
|
||||
<span style="--h:24%;--d:0s"></span>
|
||||
<span style="--h:38%;--d:.05s"></span>
|
||||
<span style="--h:52%;--d:.1s"></span>
|
||||
<span style="--h:64%;--d:.15s"></span>
|
||||
<span style="--h:48%;--d:.2s"></span>
|
||||
<span style="--h:70%;--d:.25s"></span>
|
||||
<span style="--h:42%;--d:.3s"></span>
|
||||
<span style="--h:58%;--d:.35s"></span>
|
||||
<span style="--h:36%;--d:.4s"></span>
|
||||
<span style="--h:62%;--d:.45s"></span>
|
||||
<span style="--h:26%;--d:.5s"></span>
|
||||
<span style="--h:50%;--d:.55s"></span>
|
||||
<span style="--h:34%;--d:.6s"></span>
|
||||
<span style="--h:46%;--d:.65s"></span>
|
||||
<span style="--h:58%;--d:.7s"></span>
|
||||
<span style="--h:30%;--d:.75s"></span>
|
||||
<span style="--h:44%;--d:.8s"></span>
|
||||
<span style="--h:54%;--d:.85s"></span>
|
||||
<span style="--h:28%;--d:.9s"></span>
|
||||
<span style="--h:48%;--d:.95s"></span>
|
||||
</div>
|
||||
<div class="transport">
|
||||
<div class="play" aria-hidden>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M6 4v16l14-8z"/></svg>
|
||||
</div>
|
||||
<div class="timeline" aria-hidden></div>
|
||||
<span class="time">00:09 / 00:30</span>
|
||||
<span class="badge">MP3</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: blog-post
|
||||
description: |
|
||||
A long-form article / blog post — masthead, hero image placeholder,
|
||||
article body with figures and pull quotes, author byline, related posts.
|
||||
Use when the brief asks for "blog", "article", "post", "essay", or
|
||||
"case study".
|
||||
triggers:
|
||||
- "blog"
|
||||
- "blog post"
|
||||
- "article"
|
||||
- "essay"
|
||||
- "case study"
|
||||
- "newsletter"
|
||||
- "博客"
|
||||
- "文章"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: marketing
|
||||
featured: 11
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
---
|
||||
|
||||
# Blog Post Skill
|
||||
|
||||
Produce a single long-form article page — editorial layout, no chrome.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md** (injected above). Lean into the typography
|
||||
tokens — long-form is 70% type, 20% image, 10% chrome.
|
||||
2. **Pick the topic** from the brief and write a real article — at least 600
|
||||
words across 4–6 H2 sections. No lorem ipsum.
|
||||
3. **Sections**, in order:
|
||||
- **Masthead** — small wordmark + 4–6 nav links, plain.
|
||||
- **Article header** — category eyebrow, headline (display token, large),
|
||||
deck (1–2 sentence subhead), author name + role + date.
|
||||
- **Hero image** — a 16:9 placeholder block using a DS-tinted gradient or
|
||||
solid fill (no external images). Add a 1-line caption underneath.
|
||||
- **Body** — alternating prose paragraphs with at least:
|
||||
- 1 pull quote (large display type, accent rule on the left).
|
||||
- 1 figure (image placeholder + caption).
|
||||
- 1 list (numbered or bulleted).
|
||||
- 1 inline blockquote.
|
||||
- **Author footer** — author avatar (initials in a circle), bio paragraph.
|
||||
- **Related** — 3 cards linking to other posts. Each card: tiny image
|
||||
block, title, 1-line excerpt, date.
|
||||
4. **Write** a single HTML document:
|
||||
- `<!doctype html>` through `</html>`, CSS inline.
|
||||
- Article body uses the DS body font, centered, max-width per DS layout
|
||||
rule (typically 680–720px).
|
||||
- Drop caps (`first-letter`) only if the DS mood is editorial / serif —
|
||||
skip on tech-y DSes.
|
||||
- `data-od-id` on the headline, hero, body, pull quote, related grid.
|
||||
5. **Self-check**:
|
||||
- Type hierarchy is unambiguous — H1 is clearly the headline; H2s are
|
||||
section dividers; pull quotes do not compete with H1.
|
||||
- Line length 60–75 chars for body prose.
|
||||
- Accent appears at most twice (eyebrow + pull-quote rule, or one link).
|
||||
- The page reads like a magazine, not a marketing landing.
|
||||
|
||||
## Output contract
|
||||
|
||||
Emit between `<artifact>` tags:
|
||||
|
||||
```
|
||||
<artifact identifier="post-slug" type="text/html" title="Article Title">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact, nothing after.
|
||||
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Why we rewrote our sync engine in Rust — Filebase</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fafaf9; --fg: #1c1b1a; --muted: #6b6964; --border: #e6e4e0;
|
||||
--accent: #c96442; --surface: #ffffff;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--fg); font: 18px/1.65 Georgia, 'Iowan Old Style', serif; }
|
||||
.wrap { max-width: 680px; margin: 0 auto; padding: 56px 28px 96px; }
|
||||
nav.top { font-family: -apple-system, system-ui, sans-serif; font-size: 13px; color: var(--muted); margin-bottom: 56px; }
|
||||
nav.top a { color: inherit; text-decoration: none; }
|
||||
.eyebrow { font-family: -apple-system, system-ui, sans-serif; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); margin-bottom: 14px; }
|
||||
h1 { font-size: clamp(36px, 5vw, 52px); line-height: 1.1; letter-spacing: -0.015em; margin: 0 0 20px; }
|
||||
.byline { font-family: -apple-system, system-ui, sans-serif; font-size: 14px; color: var(--muted); margin: 0 0 40px; display: flex; align-items: center; gap: 12px; }
|
||||
.avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--accent); opacity: 0.18; }
|
||||
.lede { font-size: 22px; line-height: 1.5; color: var(--fg); margin: 0 0 40px; font-style: italic; }
|
||||
.hero-figure { aspect-ratio: 16/9; background: linear-gradient(135deg, var(--accent), #6b6964); border-radius: 8px; margin-bottom: 48px; opacity: 0.85; }
|
||||
p { margin: 24px 0; }
|
||||
p:first-of-type::first-letter { float: left; font-size: 64px; line-height: 0.9; padding: 6px 10px 0 0; font-weight: 600; color: var(--accent); }
|
||||
h2 { font-size: 28px; letter-spacing: -0.01em; margin: 56px 0 12px; line-height: 1.2; }
|
||||
blockquote { margin: 40px 0; padding: 0 32px; font-size: 24px; line-height: 1.4; color: var(--fg); border-left: 3px solid var(--accent); font-style: italic; }
|
||||
code { font-family: ui-monospace, monospace; background: var(--surface); border: 1px solid var(--border); padding: 1px 5px; border-radius: 4px; font-size: 0.85em; }
|
||||
pre { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px 18px; overflow-x: auto; font: 14px/1.55 ui-monospace, monospace; }
|
||||
figure.numbers { font-family: -apple-system, system-ui, sans-serif; display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin: 40px -24px; padding: 28px 24px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||||
figure.numbers .stat .value { font-family: Georgia, serif; font-size: 38px; letter-spacing: -0.01em; line-height: 1; }
|
||||
figure.numbers .stat .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 6px; }
|
||||
.endnote { font-family: -apple-system, system-ui, sans-serif; font-size: 13px; color: var(--muted); margin-top: 64px; padding-top: 24px; border-top: 1px solid var(--border); }
|
||||
.endnote a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="wrap" data-od-id="article">
|
||||
<nav class="top"><a href="#">← Filebase blog</a></nav>
|
||||
<div class="eyebrow">Engineering</div>
|
||||
<h1>Why we rewrote our sync engine in Rust</h1>
|
||||
<div class="byline">
|
||||
<div class="avatar"></div>
|
||||
<span>By Mira Hassan · April 22, 2026 · 8 min read</span>
|
||||
</div>
|
||||
<p class="lede">For two years our Go sync engine was good enough. Then video editors started joining the customer list, and the GC pauses we'd been politely ignoring turned into bug reports we couldn't ignore.</p>
|
||||
<div class="hero-figure" data-od-id="hero-figure"></div>
|
||||
|
||||
<p>The decision wasn't sudden. We'd been watching the GC pause distribution shift for six months before we admitted what the data was telling us. P50 latency was great. P99 was a horror movie. Customers syncing 30 GB of <code>.psd</code> files in active editing sessions were the ones writing in.</p>
|
||||
|
||||
<p>Rewriting an entire sync engine sounds like the kind of project a startup is told never to do. We did it anyway. Here's how it went, what surprised us, and the parts I'd do differently.</p>
|
||||
|
||||
<h2>The trigger: GC pauses we couldn't fix</h2>
|
||||
<p>Go's garbage collector is brilliant. It is also, fundamentally, a tradeoff. Our hot path allocated short-lived buffer slices on every block diff — and at our scale, on a heavy uploader, the collector ran often enough that the P99 pause crept past 50ms.</p>
|
||||
|
||||
<p>We tried the usual fixes: pooling buffers with <code>sync.Pool</code>, tuning <code>GOGC</code>, reducing allocations in the merge path. They each helped a little. None of them got us under 20ms, and the customers we cared about needed under 5.</p>
|
||||
|
||||
<blockquote>"We can't fix this in Go. We can fix it in something without a GC."</blockquote>
|
||||
|
||||
<p>Our staff engineer Sasha said this in a meeting in October. He was right. The question wasn't whether to leave Go. It was what to leave it for, and how much we could keep.</p>
|
||||
|
||||
<h2>What we kept; what we threw out</h2>
|
||||
<p>The CLI stayed in Go. The control plane stayed in Go. The bit that does block-level diffing in a hot loop on a customer's laptop — that became Rust. The boundary became a single FFI surface with a small, opinionated protocol.</p>
|
||||
|
||||
<figure class="numbers">
|
||||
<div class="stat"><div class="value">38ms → 4ms</div><div class="label">P99 sync latency</div></div>
|
||||
<div class="stat"><div class="value">62%</div><div class="label">Memory drop</div></div>
|
||||
<div class="stat"><div class="value">11 weeks</div><div class="label">From RFC to ship</div></div>
|
||||
</figure>
|
||||
|
||||
<p>The numbers above are real and from production. They are also misleading without context: the Rust port doesn't just remove the GC, it also removes a layer of abstraction we'd been carrying since the Go MVP.</p>
|
||||
|
||||
<h2>What I'd do differently</h2>
|
||||
<p>One thing: the FFI boundary. We chose <code>cgo</code> for symmetry — Go calling Rust feels right when you already have Go everywhere. But the binding ceremony is brittle, and we ate two production incidents from string lifetime mistakes before we wrote a wrapper layer that handled them once.</p>
|
||||
|
||||
<p>If I were starting today, I'd reach for <code>uniffi</code> or generate the bindings from a schema. The lessons isn't <em>don't use cgo</em>; it's <em>treat the boundary like an external API the moment you cross language families</em>.</p>
|
||||
|
||||
<div class="endnote">Filebase is hiring engineers who like writing this kind of post. <a href="#">See open roles →</a></div>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,258 @@
|
||||
---
|
||||
name: critique
|
||||
description: |
|
||||
Run a 5-dimension expert design review on any HTML artifact in the
|
||||
project — Philosophy / Visual hierarchy / Detail / Functionality /
|
||||
Innovation, each scored 0–10. Outputs a single self-contained HTML
|
||||
report with a radar chart, evidence-backed scores, and three lists:
|
||||
Keep / Fix / Quick-wins. Use when the brief asks for a "design
|
||||
review", "design critique", "5 维度评审", "design audit", or "what's
|
||||
wrong with my design".
|
||||
triggers:
|
||||
- "critique"
|
||||
- "design review"
|
||||
- "design audit"
|
||||
- "5 维度评审"
|
||||
- "5-dim review"
|
||||
- "audit my design"
|
||||
- "review my deck"
|
||||
- "review my landing page"
|
||||
- "评审"
|
||||
- "复盘"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: design
|
||||
upstream: "https://github.com/alchaincyf/huashu-design"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
example_prompt: "Run a 5-dimension critique on the magazine-web-ppt deck I just generated — score philosophy / hierarchy / detail / function / innovation, give me Keep / Fix / Quick-wins."
|
||||
---
|
||||
|
||||
# Critique Skill · 5 维度专家评审
|
||||
|
||||
Produce a single-file HTML "design review report" that scores any
|
||||
artifact across 5 dimensions and proposes actionable fixes. Inspired by
|
||||
the *huashu-design* expert-critique flow.
|
||||
|
||||
## When to use
|
||||
|
||||
- After the agent (or user) generates an artifact (deck / prototype /
|
||||
landing page) and the user asks "what's wrong with this?" or
|
||||
"review this"
|
||||
- As a self-check loop the agent can run on its own output **before**
|
||||
emitting it
|
||||
- For comparing two variants of the same design
|
||||
|
||||
## What you produce
|
||||
|
||||
A single self-contained `<artifact type="text/html">` review report
|
||||
including:
|
||||
|
||||
1. **Header** — what artifact was reviewed, date, reviewer ("OD ·
|
||||
Critique skill"), 1-line verdict
|
||||
2. **Radar chart** (inline SVG, no library) showing the 5 scores
|
||||
3. **Five dimension cards**, each with:
|
||||
- Score 0–10 (with band: 0–4 *Broken* · 5–6 *Functional* · 7–8 *Strong*
|
||||
· 9–10 *Exceptional*)
|
||||
- 1-paragraph evidence (cite specific elements / files / lines)
|
||||
- One Keep / Fix / Quick-win bullet
|
||||
4. **Combined action lists** at the bottom:
|
||||
- **Keep** — what's working, don't touch
|
||||
- **Fix** — P0 / P1 issues that are visually expensive
|
||||
- **Quick wins** — 5–15 minute tweaks with disproportionate impact
|
||||
|
||||
## The 5 dimensions
|
||||
|
||||
> Each dimension is independent — a deck can be 9/10 on Innovation but
|
||||
> 4/10 on Hierarchy and the report should say so plainly. Don't average
|
||||
> away interesting failures.
|
||||
|
||||
### 1. Philosophy consistency · 哲学一致性
|
||||
|
||||
> Does the artifact pick a clear *direction* and stick to it through
|
||||
> every micro-decision (chrome / kicker / spacing / accent)?
|
||||
|
||||
**Evidence to look for:**
|
||||
- Is there one declared design direction (e.g. Monocle / WIRED /
|
||||
Kinfolk) or is it three styles in a trench coat?
|
||||
- Does the chrome / kicker vocabulary stay in one register, or does
|
||||
page 3 say "Vol.04 · Spring" and page 7 say "BUT WAIT 🔥"?
|
||||
- Are accent / serif / mono used by the same rule throughout?
|
||||
|
||||
**0–4** Three styles fighting each other. **5–6** One direction but
|
||||
half the elements drift. **7–8** Coherent, occasional drift on edge
|
||||
pages. **9–10** Every element argues for the same thesis.
|
||||
|
||||
### 2. Visual hierarchy · 视觉层级
|
||||
|
||||
> Can a stranger figure out what to read first, second, third — without
|
||||
> being told?
|
||||
|
||||
**Evidence to look for:**
|
||||
- Is the largest type clearly the most important thing on each page?
|
||||
- Do mono / serif / sans roles match the information's *role* (meta /
|
||||
body / display)?
|
||||
- Lots of "loud" elements competing? Or a clear primary + secondary +
|
||||
tertiary tier?
|
||||
|
||||
**0–4** Everything shouts. **5–6** Hierarchy works on hero pages but
|
||||
breaks on body. **7–8** Clear tiers, occasional collision. **9–10** Eye
|
||||
moves with zero friction.
|
||||
|
||||
### 3. Detail execution · 细节执行
|
||||
|
||||
> The 90/10 stuff — alignment, leading, kerning at large sizes, image
|
||||
> framing, foot/chrome polish, edge-case spacing.
|
||||
|
||||
**Evidence to look for:**
|
||||
- Big-stat pages: does the number sit on a baseline, or float?
|
||||
- Left/right column tops aligned in `grid-2-7-5`?
|
||||
- `frame-img` + caption proportions consistent across pages?
|
||||
- Mono labels: same letter-spacing? same uppercase rule?
|
||||
- Any orphaned `<br>` causing 1-character lines?
|
||||
|
||||
**0–4** Visible tape and string. **5–6** Most pages clean, 1–2
|
||||
ragged. **7–8** Polished, expert eye finds 2–3 misses. **9–10**
|
||||
Magazine-grade — the kind of detail that makes printed-by-hand
|
||||
typographers nod.
|
||||
|
||||
### 4. Functionality · 功能性
|
||||
|
||||
> Does the artifact *work* for its intended use? Click targets, nav,
|
||||
> readability at presentation distance, copy-paste-ability for code
|
||||
> blocks, mobile fallback if relevant.
|
||||
|
||||
**Evidence to look for:**
|
||||
- Deck: keyboard / wheel / touch nav all working? Iframe scroll
|
||||
fallback?
|
||||
- Landing: CTA above the fold? Phone number tappable on mobile?
|
||||
- Runbook: code blocks copyable, mono font, no smart quotes?
|
||||
- Critical info readable from 4m away (large screen presentation)?
|
||||
|
||||
**0–4** Visually fine but doesn't accomplish its job. **5–6** Core
|
||||
flow works, edge cases broken. **7–8** Robust through normal use.
|
||||
**9–10** Defensively engineered — handles iframe / fullscreen / paste
|
||||
/ print without flinching.
|
||||
|
||||
### 5. Innovation · 创新性
|
||||
|
||||
> Does this push past the median? Is there one element that makes
|
||||
> people lean in?
|
||||
|
||||
**Evidence to look for:**
|
||||
- One *unexpected* layout / motion / typographic move that wasn't
|
||||
required?
|
||||
- Or 100% safe — could be any deck/landing from any agency?
|
||||
- Is the innovation *earned* (matches direction) or grafted on
|
||||
(random WebGL on a Kinfolk slow-living deck)?
|
||||
|
||||
**0–4** Generic AI-slop median. **5–6** Competent and unmemorable.
|
||||
**7–8** One memorable moment, the rest solid. **9–10** Multiple
|
||||
moves you'd steal — but each one obviously serves the thesis.
|
||||
|
||||
## Scoring discipline (read before you score)
|
||||
|
||||
- **Always cite evidence** — "scored 4 because hero page mixes
|
||||
Playfair display with Inter sans on the same line" beats "feels
|
||||
inconsistent". Numbers without evidence get rejected.
|
||||
- **Don't average up** — if Hierarchy is 5 because page 3 is broken,
|
||||
don't bump to 7 because pages 1 and 2 are fine. The score is the
|
||||
*worst sustained band*.
|
||||
- **Don't grade-inflate** — a 7 means *strong*, not *acceptable*. If
|
||||
every score is 7+, you're not reviewing critically.
|
||||
- **Innovation is allowed to be low** — 5/10 is fine for production
|
||||
deliverables. Don't punish *appropriate* conservatism.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Acquire the artifact
|
||||
|
||||
Three modes:
|
||||
|
||||
1. **Project file** — user said "review the index.html I just made":
|
||||
open it from the project folder.
|
||||
2. **Pasted HTML** — user pasted code in the chat: read it from the
|
||||
message.
|
||||
3. **Generated by you in this turn** — you just emitted an artifact
|
||||
above and want to self-critique: re-read your own `<artifact>`.
|
||||
|
||||
If multiple HTML files exist, ask which one (don't review all).
|
||||
|
||||
### Step 2 — Read enough to score
|
||||
|
||||
Skim the entire `<style>`, then read 6–8 representative content
|
||||
blocks. **Do not score from frontmatter alone.** The score depends on
|
||||
*executed* design, not declared intent.
|
||||
|
||||
### Step 3 — Score with evidence
|
||||
|
||||
For each of the 5 dimensions, write the score and a 30–80 word
|
||||
evidence paragraph that names specific elements. Use line numbers,
|
||||
class names, page numbers.
|
||||
|
||||
Example:
|
||||
```
|
||||
Dimension: Detail execution
|
||||
Score: 6 / 10
|
||||
Evidence: Stat-cards on page 3 align cleanly (grid-6, 3×2), but on
|
||||
page 8 the right column foot sits 2vh higher than the left because
|
||||
.callout has 3vh top margin while the figure doesn't. Image captions
|
||||
use mono on page 5 but sans on page 7 — pick one.
|
||||
```
|
||||
|
||||
### Step 4 — Build the action lists
|
||||
|
||||
Aggregate the 5 evidence paragraphs into:
|
||||
|
||||
- **Keep** (3–5 bullets) — concrete things working that the user must
|
||||
not break in the next iteration. Cite by class / page / element.
|
||||
- **Fix** (3–6 bullets) — must-do, ordered by *visual cost saved per
|
||||
minute spent*. Each bullet ≤ 1 sentence.
|
||||
- **Quick wins** (3–5 bullets) — 5–15 minutes each, high
|
||||
signal-to-noise (e.g. "swap `display:flex` for `grid` on page 4 to
|
||||
fix the column drift").
|
||||
|
||||
### Step 5 — Emit the report HTML
|
||||
|
||||
Build a single file:
|
||||
|
||||
- Header: artifact name + reviewer credit + date
|
||||
- Big radar chart (SVG)
|
||||
- 5 dimension cards in a 1-column or 2-column grid
|
||||
- Three action lists at the bottom with checkbox affordance
|
||||
|
||||
Use the active DESIGN.md tokens if one exists; otherwise default to a
|
||||
neutral light theme (off-white background, near-black text, one accent
|
||||
for radar fill).
|
||||
|
||||
## Output contract
|
||||
|
||||
```
|
||||
<artifact identifier="critique-<artifact-slug>" type="text/html" title="Critique · <Artifact Title>">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact ("Reviewed X across 5 dimensions, see
|
||||
report below.") and **stop after `</artifact>`** — do not paraphrase
|
||||
the report in chat; the user will read the artifact.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **5 scores, every time** — partial reports (e.g. only 3 dimensions)
|
||||
are not allowed.
|
||||
- **Evidence per score** — no "feels off" / "needs work". If you
|
||||
can't cite an element, the score is not justified.
|
||||
- **Don't grade-inflate** — overall mean above 8 is suspicious; check
|
||||
yourself.
|
||||
- **Don't review your own artifact in the same turn** — the user
|
||||
needs to see it first. Self-critique only on explicit request
|
||||
("now critique what you just made").
|
||||
- **Single-file HTML only** — no external CSS/JS. Inline everything.
|
||||
- **Radar chart is mandatory** — gives the report a recognizable
|
||||
silhouette and lets the user spot weak axes at a glance.
|
||||
@@ -0,0 +1,671 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Critique · magazine-web-ppt example deck</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f3ee;
|
||||
--paper: #ffffff;
|
||||
--ink: #1a1a1c;
|
||||
--muted: #6b6964;
|
||||
--rule: #e2dfd7;
|
||||
--accent: #c96442;
|
||||
--good: #4a7a3f;
|
||||
--warn: #c96442;
|
||||
--bad: #a83a2a;
|
||||
|
||||
--serif: 'Source Serif 4', Georgia, serif;
|
||||
--sans: 'Inter', -apple-system, system-ui, sans-serif;
|
||||
--mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a { color: var(--accent); }
|
||||
|
||||
.wrap {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 56px 40px 96px;
|
||||
}
|
||||
|
||||
/* ============ Header ============ */
|
||||
.hd {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 40px;
|
||||
padding-bottom: 28px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.hd-title {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: clamp(34px, 4.4vw, 56px);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.hd-meta {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.hd-verdict {
|
||||
font-family: var(--serif);
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
line-height: 1.45;
|
||||
color: var(--muted);
|
||||
max-width: 36ch;
|
||||
text-align: right;
|
||||
}
|
||||
.hd-verdict strong { color: var(--ink); font-style: normal; font-weight: 600; }
|
||||
|
||||
/* ============ Top row: radar + score table ============ */
|
||||
.top {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 48px;
|
||||
margin-bottom: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.top { grid-template-columns: 1fr; }
|
||||
}
|
||||
.radar-card {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 6px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.radar-card .lbl {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.radar-card svg { width: 100%; height: auto; max-width: 300px; }
|
||||
.radar-card .overall {
|
||||
font-family: var(--serif);
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-top: 18px;
|
||||
}
|
||||
.radar-card .overall .n {
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Score table */
|
||||
.scores { display: flex; flex-direction: column; gap: 14px; }
|
||||
.score-row {
|
||||
display: grid;
|
||||
grid-template-columns: 22ch 1fr 6ch 14ch;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 14px 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.score-row:first-child { border-top: 0; }
|
||||
.score-name {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
}
|
||||
.score-name .en {
|
||||
display: block;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.score-bar {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
background: var(--rule);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.score-bar-fill {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
background: var(--ink);
|
||||
}
|
||||
.score-num {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
letter-spacing: -0.02em;
|
||||
text-align: right;
|
||||
}
|
||||
.score-num .denom {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
.score-band {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
}
|
||||
.band-broken { color: var(--bad); }
|
||||
.band-functional { color: var(--muted); }
|
||||
.band-strong { color: var(--good); }
|
||||
.band-exceptional { color: var(--accent); }
|
||||
|
||||
/* ============ Dimension cards ============ */
|
||||
.section-title {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
letter-spacing: -0.005em;
|
||||
margin: 64px 0 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.section-title .en {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.dim-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.dim-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.dim {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 6px;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
.dim-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.dim-name {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
}
|
||||
.dim-name .en {
|
||||
display: block;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.dim-score {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: 26px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.dim-score .denom {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
.dim-evidence {
|
||||
font-family: var(--serif);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.65;
|
||||
color: #2d2d30;
|
||||
margin: 10px 0 16px;
|
||||
}
|
||||
.dim-evidence code {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.88em;
|
||||
background: var(--rule);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.dim-tags {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.tag-row {
|
||||
display: grid;
|
||||
grid-template-columns: 70px 1fr;
|
||||
gap: 12px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.tag {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
color: var(--paper);
|
||||
align-self: start;
|
||||
text-align: center;
|
||||
}
|
||||
.tag-keep { background: var(--good); }
|
||||
.tag-fix { background: var(--warn); }
|
||||
.tag-qw { background: #2c4d6e; }
|
||||
|
||||
/* ============ Action lists ============ */
|
||||
.lists-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.lists-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.list-card {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 6px;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
.list-head {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.26em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.list-head.keep { color: var(--good); }
|
||||
.list-head.fix { color: var(--warn); }
|
||||
.list-head.qw { color: #2c4d6e; }
|
||||
.list-head .ct {
|
||||
font-size: 16px;
|
||||
font-family: var(--serif);
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.list-card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.list-card li {
|
||||
display: grid;
|
||||
grid-template-columns: 18px 1fr;
|
||||
gap: 10px;
|
||||
font-family: var(--serif);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.list-card li::before {
|
||||
content: "";
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
border: 1.5px solid var(--rule);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.list-card li code {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.85em;
|
||||
background: var(--bg);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ============ Footer ============ */
|
||||
.ft {
|
||||
margin-top: 80px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ft .br { color: var(--ink); font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- ============ Header ============ -->
|
||||
<header class="hd">
|
||||
<div>
|
||||
<div class="hd-meta">
|
||||
<span>5-Dim Critique</span>
|
||||
<span>·</span>
|
||||
<span>2026.04.27</span>
|
||||
<span>·</span>
|
||||
<span>OD · Critique skill</span>
|
||||
</div>
|
||||
<h1 class="hd-title">magazine-web-ppt<br>example deck</h1>
|
||||
</div>
|
||||
<p class="hd-verdict">
|
||||
<strong>7.4 / 10 overall.</strong> Strong philosophical
|
||||
backbone and detail — the deck looks like one designer made
|
||||
every slide. Innovation is conservative on purpose; functionality
|
||||
loses points only because the example ships without real images.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- ============ Radar + Score table ============ -->
|
||||
<section class="top">
|
||||
<div class="radar-card">
|
||||
<div class="lbl">Score Radar</div>
|
||||
<!-- Pentagon radar, 5 axes; score grid at 0/2.5/5/7.5/10 -->
|
||||
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" aria-label="Score radar chart">
|
||||
<defs>
|
||||
<style>
|
||||
.axis { stroke: #e2dfd7; stroke-width: 1; fill: none; }
|
||||
.grid { stroke: #e8e5dd; stroke-width: 1; fill: none; }
|
||||
.grid-mid { stroke: #e2dfd7; stroke-width: 1; fill: none; }
|
||||
.area { fill: rgba(201,100,66,0.18); stroke: #c96442; stroke-width: 1.6; stroke-linejoin: round; }
|
||||
.dot { fill: #c96442; }
|
||||
.lbl { font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; fill: #6b6964; }
|
||||
.lbl-n { font-family: 'Source Serif 4', serif; font-size: 12px; font-weight: 600; fill: #1a1a1c; }
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Center 150,150. Radius 110 = 10/10. -->
|
||||
<!-- Grid rings 25/50/75/100% of 110 = 27.5 / 55 / 82.5 / 110 -->
|
||||
<!-- Pentagon angles: -90, -18, 54, 126, 198 (deg) measured from center.
|
||||
Order: top=Philosophy, top-right=Hierarchy, bottom-right=Detail,
|
||||
bottom-left=Function, top-left=Innovation -->
|
||||
<!-- Outer rings (5 sided) -->
|
||||
<polygon class="grid" points="150,40 254.66,116.05 214.69,238.95 85.31,238.95 45.34,116.05" />
|
||||
<polygon class="grid" points="150,67.5 228.47,124.54 198.51,216.71 101.49,216.71 71.53,124.54" />
|
||||
<polygon class="grid-mid" points="150,95 202.33,133.02 182.34,194.48 117.66,194.48 97.67,133.02" />
|
||||
<polygon class="grid" points="150,122.5 176.16,141.51 166.17,172.24 133.83,172.24 123.84,141.51" />
|
||||
<!-- Axes -->
|
||||
<line class="axis" x1="150" y1="150" x2="150" y2="40" />
|
||||
<line class="axis" x1="150" y1="150" x2="254.66" y2="116.05" />
|
||||
<line class="axis" x1="150" y1="150" x2="214.69" y2="238.95" />
|
||||
<line class="axis" x1="150" y1="150" x2="85.31" y2="238.95" />
|
||||
<line class="axis" x1="150" y1="150" x2="45.34" y2="116.05" />
|
||||
|
||||
<!-- Score area · Phil 8 / Hier 7 / Det 8 / Func 6 / Innov 5
|
||||
Distances from center (radius 110):
|
||||
Phil 8 → 88 : 150, 150 - 88 = 150, 62
|
||||
Hier 7 → 77 : 150 + 77*sin(72°), 150 - 77*cos(72°)
|
||||
≈ 150 + 73.24, 150 - 23.79
|
||||
= 223.24, 126.21
|
||||
Det 8 → 88 : 150 + 88*sin(144°), 150 - 88*cos(144°)
|
||||
≈ 150 + 51.72, 150 + 71.20
|
||||
= 201.72, 221.20
|
||||
Func 6 → 66 : 150 - 66*sin(36°), 150 + 66*cos(36°)
|
||||
≈ 150 - 38.79, 150 + 53.40
|
||||
= 111.21, 203.40
|
||||
Innov 5 → 55 : 150 - 55*sin(108°),150 - 55*cos(108°)
|
||||
≈ 150 - 52.32, 150 + 17.00
|
||||
= 97.68, 167.00
|
||||
Wait - cos(108°) is negative, so 150 - 55*(-0.309) = 150 + 17, that's bottom of axis. But Innov axis is top-left. Let me redo.
|
||||
Innov axis end point: 45.34, 116.05. Vector from center (150,150): (-104.66, -33.95), magnitude 110.
|
||||
At score 5, scale = 5/10 = 0.5: center + 0.5 * (-104.66, -33.95) = 150 - 52.33, 150 - 16.97 = 97.67, 133.03 -->
|
||||
<polygon class="area" points="150,62 223.24,126.21 201.72,221.20 111.21,203.40 97.67,133.03" />
|
||||
<circle class="dot" cx="150" cy="62" r="3" />
|
||||
<circle class="dot" cx="223.24" cy="126.21" r="3" />
|
||||
<circle class="dot" cx="201.72" cy="221.20" r="3" />
|
||||
<circle class="dot" cx="111.21" cy="203.40" r="3" />
|
||||
<circle class="dot" cx="97.67" cy="133.03" r="3" />
|
||||
|
||||
<!-- Axis labels -->
|
||||
<text class="lbl" x="150" y="28" text-anchor="middle">PHILOSOPHY</text>
|
||||
<text class="lbl-n" x="150" y="14" text-anchor="middle">8</text>
|
||||
<text class="lbl" x="270" y="116" text-anchor="middle">HIERARCHY</text>
|
||||
<text class="lbl-n" x="278" y="100" text-anchor="middle">7</text>
|
||||
<text class="lbl" x="220" y="259" text-anchor="middle">DETAIL</text>
|
||||
<text class="lbl-n" x="220" y="275" text-anchor="middle">8</text>
|
||||
<text class="lbl" x="80" y="259" text-anchor="middle">FUNCTION</text>
|
||||
<text class="lbl-n" x="80" y="275" text-anchor="middle">6</text>
|
||||
<text class="lbl" x="30" y="116" text-anchor="middle">INNOVATION</text>
|
||||
<text class="lbl-n" x="22" y="100" text-anchor="middle">5</text>
|
||||
</svg>
|
||||
<div class="overall">Overall · <span class="n">7.4</span> / 10 · band <em>Strong</em></div>
|
||||
</div>
|
||||
|
||||
<div class="scores" aria-label="Score breakdown">
|
||||
<div class="score-row">
|
||||
<div class="score-name">Philosophy consistency<span class="en">Phil. cons.</span></div>
|
||||
<div class="score-bar"><span class="score-bar-fill" style="width:80%"></span></div>
|
||||
<div class="score-num">8<span class="denom">/10</span></div>
|
||||
<div class="score-band band-strong">Strong</div>
|
||||
</div>
|
||||
<div class="score-row">
|
||||
<div class="score-name">Visual hierarchy<span class="en">Hier.</span></div>
|
||||
<div class="score-bar"><span class="score-bar-fill" style="width:70%"></span></div>
|
||||
<div class="score-num">7<span class="denom">/10</span></div>
|
||||
<div class="score-band band-strong">Strong</div>
|
||||
</div>
|
||||
<div class="score-row">
|
||||
<div class="score-name">Detail execution<span class="en">Detail</span></div>
|
||||
<div class="score-bar"><span class="score-bar-fill" style="width:80%"></span></div>
|
||||
<div class="score-num">8<span class="denom">/10</span></div>
|
||||
<div class="score-band band-strong">Strong</div>
|
||||
</div>
|
||||
<div class="score-row">
|
||||
<div class="score-name">Functionality<span class="en">Func.</span></div>
|
||||
<div class="score-bar"><span class="score-bar-fill" style="width:60%"></span></div>
|
||||
<div class="score-num">6<span class="denom">/10</span></div>
|
||||
<div class="score-band band-functional">Functional</div>
|
||||
</div>
|
||||
<div class="score-row">
|
||||
<div class="score-name">Innovation<span class="en">Innov.</span></div>
|
||||
<div class="score-bar"><span class="score-bar-fill" style="width:50%"></span></div>
|
||||
<div class="score-num">5<span class="denom">/10</span></div>
|
||||
<div class="score-band band-functional">Functional</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Dimension cards ============ -->
|
||||
<h2 class="section-title">Dimension reports<span class="en">Evidence per axis</span></h2>
|
||||
|
||||
<div class="dim-grid">
|
||||
<article class="dim">
|
||||
<div class="dim-head">
|
||||
<div class="dim-name">Philosophy consistency<span class="en">Phil. cons. · 哲学一致性</span></div>
|
||||
<div class="dim-score">8<span class="denom">/10</span></div>
|
||||
</div>
|
||||
<p class="dim-evidence">
|
||||
The 9-slide rhythm reads as a single direction (Monocle Editorial)
|
||||
from cover to close. <code>chrome</code> vocabulary stays in one
|
||||
register: <em>"A Talk · 2026.04.22"</em>, <em>"Act II · 04 / 09"</em>,
|
||||
<em>"Page 06 · 金句"</em>. The drift is the <code>kicker</code> on
|
||||
slide 5 — <em>"Act II"</em> is good, but the slide title <em>"折叠"</em>
|
||||
is a one-character display word that competes with the Act number for
|
||||
eyeballs. Worth tightening.
|
||||
</p>
|
||||
<div class="dim-tags">
|
||||
<div class="tag-row"><span class="tag tag-keep">Keep</span><span>The chrome / kicker / foot vocabulary across all 9 slides — it's the deck's identity.</span></div>
|
||||
<div class="tag-row"><span class="tag tag-fix">Fix</span><span>Slide 5: bump the kicker to <em>"Act II · 折叠"</em> or shrink the display title to clear the hierarchy.</span></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="dim">
|
||||
<div class="dim-head">
|
||||
<div class="dim-name">Visual hierarchy<span class="en">Hier. · 视觉层级</span></div>
|
||||
<div class="dim-score">7<span class="denom">/10</span></div>
|
||||
</div>
|
||||
<p class="dim-evidence">
|
||||
Hero pages (1, 5, 7, 9) are textbook — display serif dominates,
|
||||
<code>kicker</code> and <code>meta-row</code> recede. Body pages
|
||||
mostly hold up: stat-cards on slide 2 use <code>.stat-label</code>
|
||||
(mono small) → <code>.stat-nb</code> (serif large) → <code>.stat-note</code>
|
||||
(sans body), three tiers, no collision. The miss is slide 3's
|
||||
<code>callout</code> — its left-rule competes visually with the
|
||||
<code>.h-xl</code> heading because both sit at the same x-coord
|
||||
and similar weight. Eye doesn't know if to read heading-first or
|
||||
quote-first.
|
||||
</p>
|
||||
<div class="dim-tags">
|
||||
<div class="tag-row"><span class="tag tag-keep">Keep</span><span>Stat-card 3-tier structure on slide 2 — copy this everywhere.</span></div>
|
||||
<div class="tag-row"><span class="tag tag-fix">Fix</span><span>Slide 3: indent the <code>callout</code> by <code>2vw</code> or push it below the lead so it visibly belongs to a lower tier.</span></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="dim">
|
||||
<div class="dim-head">
|
||||
<div class="dim-name">Detail execution<span class="en">Detail · 细节执行</span></div>
|
||||
<div class="dim-score">8<span class="denom">/10</span></div>
|
||||
</div>
|
||||
<p class="dim-evidence">
|
||||
Magazine-grade in places — every <code>.foot</code> aligns
|
||||
baseline-to-baseline across all 9 slides; <code>.meta-row</code>
|
||||
uses one mono spec throughout (<code>.16em</code> tracking,
|
||||
uppercase). Pipeline on slide 4 keeps perfect grid even when
|
||||
column count drops to 3. Two real misses: (1) Slide 3 image-slot
|
||||
uses <code>aspect-ratio:16/10</code> but the placeholder text
|
||||
inside is centered which makes it look hollow at viewport widths
|
||||
≤ 1100px; (2) the dot-nav at the bottom overlaps the foot text
|
||||
on slide 5 because the hero centered grid eats vertical space.
|
||||
</p>
|
||||
<div class="dim-tags">
|
||||
<div class="tag-row"><span class="tag tag-keep">Keep</span><span>The mono <code>.foot</code> spec — it's the deck's grace note, do not change letter-spacing.</span></div>
|
||||
<div class="tag-row"><span class="tag tag-fix">Fix</span><span>Slide 5 hero grid: cap inner content at <code>min-height:78vh</code> so the foot stays clear of the dot-nav.</span></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="dim">
|
||||
<div class="dim-head">
|
||||
<div class="dim-name">Functionality<span class="en">Func. · 功能性</span></div>
|
||||
<div class="dim-score">6<span class="denom">/10</span></div>
|
||||
</div>
|
||||
<p class="dim-evidence">
|
||||
Keyboard / wheel / touch navigation works correctly inside the
|
||||
host iframe (verified: ←/→/PageUp/PageDown all advance). ESC
|
||||
opens the index overview, dot clicks register. Big miss is the
|
||||
example ships <em>without real images</em> — slide 3 shows a
|
||||
dashed <code>.img-slot</code> placeholder where a product
|
||||
screenshot belongs, which is the right call for an example file
|
||||
but means the user can't judge how the layout holds at full
|
||||
fidelity. Second miss: <code>iframe</code> sandbox is
|
||||
<code>allow-scripts</code> only in the example card, so the
|
||||
WebGL background loads but the dot-nav inside the iframe takes
|
||||
a click before keyboard nav captures focus.
|
||||
</p>
|
||||
<div class="dim-tags">
|
||||
<div class="tag-row"><span class="tag tag-keep">Keep</span><span>The 5-bug-fix nav script (real scroller detection, capture-phase listeners) — proven and stable.</span></div>
|
||||
<div class="tag-row"><span class="tag tag-fix">Fix</span><span>Add a <code>data:</code> URI placeholder image (1×1 colored gradient) in the example so slide 3's layout reads at any width.</span></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="dim">
|
||||
<div class="dim-head">
|
||||
<div class="dim-name">Innovation<span class="en">Innov. · 创新性</span></div>
|
||||
<div class="dim-score">5<span class="denom">/10</span></div>
|
||||
</div>
|
||||
<p class="dim-evidence">
|
||||
Innovation is intentionally conservative — this is a port of
|
||||
歸藏's guizang-ppt-skill, and the value proposition is
|
||||
<em>predictability</em>, not novelty. The dual WebGL background
|
||||
(Holographic Dispersion on dark, Spiral Vortex on light) is the
|
||||
one earned moment; the cross-fade on slide-theme transitions is
|
||||
subtle and well-timed. But everything else (layout vocabulary,
|
||||
chrome / foot pattern, theme presets) is faithfully replicated
|
||||
from the upstream. There is no "lean-forward" surprise that
|
||||
makes a viewer screenshot a slide. For its declared purpose
|
||||
(Monocle Editorial direction), this is appropriate. For an
|
||||
AI demo-day deck, it's a missed opportunity.
|
||||
</p>
|
||||
<div class="dim-tags">
|
||||
<div class="tag-row"><span class="tag tag-keep">Keep</span><span>The dual-shader cross-fade — it's the only "magic" the deck performs and it earns its keep.</span></div>
|
||||
<div class="tag-row"><span class="tag tag-qw">Quick win</span><span>Add one <em>typographic</em> moment per deck — e.g. an oversized italic <code>em</code> kicker that breaks the grid on the closing slide.</span></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- ============ Action lists ============ -->
|
||||
<h2 class="section-title">Action lists<span class="en">Keep · Fix · Quick wins</span></h2>
|
||||
|
||||
<div class="lists-grid">
|
||||
<section class="list-card">
|
||||
<div class="list-head keep"><span>Keep</span><span class="ct">don't break it</span></div>
|
||||
<ul>
|
||||
<li>The 9-page rhythm: <code>hero dark → light → dark → light → hero light → dark → hero dark → light → hero light</code>. It's the gold standard.</li>
|
||||
<li>Dual WebGL backdrops + the <code>1.2s</code> cross-fade between dark and light slides.</li>
|
||||
<li><code>chrome</code> / <code>kicker</code> / <code>foot</code> vocabulary — they carry the Monocle direction.</li>
|
||||
<li>3-tier <code>stat-card</code> on slide 2 (<code>label</code> → <code>nb</code> → <code>note</code>).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="list-card">
|
||||
<div class="list-head fix"><span>Fix</span><span class="ct">P0 — visually expensive</span></div>
|
||||
<ul>
|
||||
<li>Slide 3 callout indent — currently competes with <code>.h-xl</code>; push 2vw right or below the lead.</li>
|
||||
<li>Slide 5 hero centered grid — cap content height at <code>78vh</code> so foot doesn't overlap the dot nav.</li>
|
||||
<li>Slide 3 add a <code>data:</code> gradient placeholder image so the layout reads at narrow widths even without real assets.</li>
|
||||
<li>Slide 5 kicker / display: pick one to be primary — currently both fight.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="list-card">
|
||||
<div class="list-head qw"><span>Quick wins</span><span class="ct">5–15 min, high signal</span></div>
|
||||
<ul>
|
||||
<li>Inject <code>data-screen-label</code> on every slide for accessibility + grep self-checks.</li>
|
||||
<li>Add one oversized italic <em>en</em> moment on the closing slide for typographic surprise.</li>
|
||||
<li>Move the <code>#hint</code> overlay from <code>opacity:.4</code> to <code>.55</code> on hero pages — currently invisible.</li>
|
||||
<li>Add a print stylesheet (one slide per page) so PDF export carries the rhythm.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="ft">
|
||||
<span>OD · Critique skill · v0.1</span>
|
||||
<span>5 dimensions · Phil / Hier / Det / Func / Innov</span>
|
||||
<span class="br">github.com/alchaincyf/huashu-design</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: dashboard
|
||||
description: |
|
||||
Admin / analytics dashboard in a single HTML file. Fixed left sidebar,
|
||||
top bar with user/search, main grid of KPI cards and one or two charts.
|
||||
Use when the brief asks for a "dashboard", "admin", "analytics", or
|
||||
"control panel" screen.
|
||||
triggers:
|
||||
- "dashboard"
|
||||
- "admin panel"
|
||||
- "analytics"
|
||||
- "control panel"
|
||||
- "后台"
|
||||
- "管理后台"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: operations
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
---
|
||||
|
||||
# Dashboard Skill
|
||||
|
||||
Produce a single-screen admin / analytics dashboard.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md** (injected above). Colors, typography, spacing,
|
||||
component styling all come from it. Do not invent new tokens.
|
||||
2. **Classify** what the dashboard monitors (sales, traffic, usage, incidents,
|
||||
ops, etc.) from the brief. Generate specific, plausible metric names and
|
||||
values — no "Metric A / Metric B" placeholders.
|
||||
3. **Lay out** the required regions:
|
||||
- **Left sidebar** (220–260px): brand mark at top, 6–8 nav links with
|
||||
icons, active state uses the DS accent.
|
||||
- **Top bar**: page title on the left, search input + user avatar / status
|
||||
on the right.
|
||||
- **Main**:
|
||||
- Row 1: 3–4 KPI cards (label + big number + delta vs. prior period).
|
||||
- Row 2: one primary chart (full width or 2/3) — render as an inline SVG
|
||||
line / bar / area chart drawn from real-looking numbers.
|
||||
- Row 3: one secondary chart or table (recent events, top items, etc.).
|
||||
4. **Write** one self-contained HTML document:
|
||||
- `<!doctype html>` through `</html>`, CSS in one inline `<style>` block.
|
||||
- CSS Grid for the overall layout; Flexbox inside cards.
|
||||
- Semantic HTML: `<aside>`, `<header>`, `<main>`, `<section>`.
|
||||
- Tag each logical region with `data-od-id="slug"` for comment mode.
|
||||
5. **Charts**: inline SVG only, no JS libraries. A line chart is ~10 lines of
|
||||
`<polyline>` with a subtle area fill. A bar chart is N `<rect>`s with
|
||||
DS-accent fill. Label axes lightly (muted text, smaller scale).
|
||||
6. **Self-check**:
|
||||
- Every color comes from DESIGN.md tokens.
|
||||
- Accent used at most twice (sidebar active + one chart highlight).
|
||||
- Sidebar + top bar are sticky; main scrolls independently.
|
||||
- Density matches the DS mood — airy DSes get more padding, dense DSes
|
||||
(trading, crypto) tighten rows.
|
||||
|
||||
## Output contract
|
||||
|
||||
Emit between `<artifact>` tags:
|
||||
|
||||
```
|
||||
<artifact identifier="dashboard-slug" type="text/html" title="Dashboard Title">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact, nothing after.
|
||||
@@ -0,0 +1,118 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Pulse — analytics overview</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fafaf9; --fg: #1c1b1a; --muted: #6b6964; --border: #e6e4e0;
|
||||
--accent: #c96442; --surface: #ffffff; --good: #2f7d4a; --bad: #b53a2a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 -apple-system, system-ui, sans-serif; display: grid; grid-template-columns: 220px 1fr; min-height: 100vh; }
|
||||
.sidebar { background: var(--surface); border-right: 1px solid var(--border); padding: 16px; }
|
||||
.brand { font-weight: 600; padding: 8px 10px 18px; }
|
||||
.nav { display: flex; flex-direction: column; gap: 2px; }
|
||||
.nav a { padding: 7px 10px; border-radius: 6px; color: var(--fg); text-decoration: none; }
|
||||
.nav a.active { background: var(--bg); font-weight: 500; }
|
||||
.nav a:hover { background: var(--bg); }
|
||||
.nav .group-label { font-size: 11px; color: var(--muted); padding: 14px 10px 6px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
main { padding: 0 28px 56px; }
|
||||
.topbar { padding: 16px 0; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
|
||||
.topbar h1 { font-size: 20px; margin: 0; letter-spacing: -0.01em; }
|
||||
.topbar .right { display: flex; align-items: center; gap: 12px; color: var(--muted); }
|
||||
button { font: inherit; cursor: pointer; padding: 7px 13px; border-radius: 6px; }
|
||||
.btn-primary { background: var(--accent); color: white; border: 1px solid var(--accent); }
|
||||
.btn-secondary { background: transparent; color: var(--fg); border: 1px solid var(--border); }
|
||||
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
|
||||
@media (max-width: 900px) { .kpis { grid-template-columns: repeat(2, 1fr); } }
|
||||
.kpi { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px; }
|
||||
.kpi .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
|
||||
.kpi .value { font-size: 28px; letter-spacing: -0.02em; }
|
||||
.kpi .delta { font-size: 12px; margin-top: 4px; }
|
||||
.kpi .delta.up { color: var(--good); }
|
||||
.kpi .delta.down { color: var(--bad); }
|
||||
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 20px; margin-bottom: 16px; }
|
||||
.panel h3 { margin: 0 0 16px; font-size: 14px; font-weight: 500; }
|
||||
.chart { height: 240px; background: linear-gradient(180deg, rgba(201,100,66,0.06), transparent); border-bottom: 1px solid var(--border); position: relative; overflow: hidden; }
|
||||
.chart svg { width: 100%; height: 100%; display: block; }
|
||||
.panels-row { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
|
||||
@media (max-width: 900px) { .panels-row { grid-template-columns: 1fr; } }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 10px 6px; border-top: 1px solid var(--border); }
|
||||
th { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 500; }
|
||||
.pill { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); }
|
||||
.pill.good { color: var(--good); border-color: rgba(47,125,74,0.3); }
|
||||
.pill.bad { color: var(--bad); border-color: rgba(181,58,42,0.3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<aside class="sidebar" data-od-id="sidebar">
|
||||
<div class="brand">◐ Pulse</div>
|
||||
<nav class="nav">
|
||||
<a href="#" class="active">Overview</a>
|
||||
<a href="#">Funnels</a>
|
||||
<a href="#">Cohorts</a>
|
||||
<a href="#">Sessions</a>
|
||||
<span class="group-label">Workspace</span>
|
||||
<a href="#">Sources</a>
|
||||
<a href="#">Members</a>
|
||||
<a href="#">Billing</a>
|
||||
<a href="#">Settings</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main>
|
||||
<div class="topbar" data-od-id="topbar">
|
||||
<h1>Overview · April 2026</h1>
|
||||
<div class="right">
|
||||
<button class="btn-secondary">Last 30 days ▾</button>
|
||||
<button class="btn-primary">+ New report</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpis" data-od-id="kpis">
|
||||
<div class="kpi"><div class="label">MRR</div><div class="value">$48.2K</div><div class="delta up">+12.4% MoM</div></div>
|
||||
<div class="kpi"><div class="label">Active accounts</div><div class="value">3,184</div><div class="delta up">+204 this month</div></div>
|
||||
<div class="kpi"><div class="label">Churn (30d)</div><div class="value">2.1%</div><div class="delta down">+0.4 pp</div></div>
|
||||
<div class="kpi"><div class="label">P95 latency</div><div class="value">182 ms</div><div class="delta up">-23 ms</div></div>
|
||||
</div>
|
||||
|
||||
<div class="panels-row">
|
||||
<div class="panel" data-od-id="chart-panel">
|
||||
<h3>Revenue · 30 days</h3>
|
||||
<div class="chart">
|
||||
<svg viewBox="0 0 600 240" preserveAspectRatio="none">
|
||||
<polyline fill="none" stroke="#c96442" stroke-width="2" points="0,180 30,170 60,150 90,160 120,140 150,120 180,130 210,110 240,90 270,100 300,80 330,70 360,80 390,60 420,50 450,60 480,40 510,30 540,40 570,20 600,10" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" data-od-id="signups-panel">
|
||||
<h3>New accounts</h3>
|
||||
<table>
|
||||
<thead><tr><th>Account</th><th>Plan</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Linear</td><td>Team</td><td><span class="pill good">active</span></td></tr>
|
||||
<tr><td>Cursor</td><td>Pro</td><td><span class="pill good">active</span></td></tr>
|
||||
<tr><td>Notion</td><td>Team</td><td><span class="pill bad">trial</span></td></tr>
|
||||
<tr><td>Vercel</td><td>Enterprise</td><td><span class="pill good">active</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" data-od-id="recent-events">
|
||||
<h3>Recent events</h3>
|
||||
<table>
|
||||
<thead><tr><th>Time</th><th>Account</th><th>Event</th><th>Plan</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>2:14 pm</td><td>Acme Co</td><td>Upgraded to Team</td><td>Team</td></tr>
|
||||
<tr><td>1:48 pm</td><td>Northwind</td><td>Connected GitHub</td><td>Pro</td></tr>
|
||||
<tr><td>1:32 pm</td><td>Globex</td><td>Cancelled subscription</td><td>Solo</td></tr>
|
||||
<tr><td>12:51 pm</td><td>Initech</td><td>New seat invited</td><td>Team</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: dating-web
|
||||
description: |
|
||||
A consumer-feeling dating / matchmaking dashboard — left rail navigation,
|
||||
ticker bar of community signals, headline KPIs, a 30-day mutual-matches
|
||||
bar chart, and a match-rate trend block. Editorial typography, restrained
|
||||
accent. Use when the brief asks for a "dating site", "matchmaking",
|
||||
"community dashboard", "social network dashboard", or any consumer
|
||||
product where the data is the story.
|
||||
triggers:
|
||||
- "dating app"
|
||||
- "dating site"
|
||||
- "matchmaking"
|
||||
- "social dashboard"
|
||||
- "community dashboard"
|
||||
- "consumer dashboard"
|
||||
- "约会应用"
|
||||
- "婚恋"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: personal
|
||||
featured: 5
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
example_prompt: "Design ‘mutuals’ — a dating site for X posters. Daily digest dashboard with stats, mutual-matches bar chart, and a community ticker."
|
||||
---
|
||||
|
||||
# Dating Web Skill
|
||||
|
||||
Produce a single-screen consumer dashboard that feels like a Sunday-paper
|
||||
dating column rendered as software. Editorial type, single restrained
|
||||
accent, lots of negative space, *no* swipe deck or hookup tropes.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md** (injected above). Lean into a serif display
|
||||
token for the metric numerals — these screens live or die on numerals.
|
||||
2. **Pick a brand voice** — wry, observational, slightly literary. Generate
|
||||
real, specific copy. Examples: "the people who'd text back within a day",
|
||||
"manageable. two are now friends.", "your single greatest compatibility
|
||||
asset."
|
||||
3. **Layout**, in order:
|
||||
- **Top ticker** — single-row horizontal strip across the top in a
|
||||
sans-serif eyebrow style: tagline left, "NEXT TIER AT 2,080 MUTUALS"
|
||||
right, both in mono caps with letter-spacing. Thin rule below.
|
||||
- **Left rail** — 220–260px sidebar. Brand wordmark in serif italic at
|
||||
top. User card (avatar / handle / ratio / tier). Three groups of nav:
|
||||
"TODAY" (specimen, inbox, queue, notifications), "YOU" (your stats,
|
||||
mutuals & communities, blocked, settings), "ARCHIVE" (past issues,
|
||||
expired matches). Active item gets accent text + accent dot.
|
||||
- **Main content**:
|
||||
- **KPI grid** — 3 columns × 3 rows (or 9 cells). Each cell: small
|
||||
caps mono label, an oversized serif numeral (use accent or muted
|
||||
green for positive, muted red for caution), one-line italic
|
||||
footnote. Plausible specifics — "1,842 ↑ 41 this wk · healthy
|
||||
growth.", "14% above median for your cohort.", "4 / exes in your
|
||||
circle · manageable. two are now friends."
|
||||
- **Bar chart panel** — "mutuals — last 30 days". Tall thin black
|
||||
bars, last two days highlighted in accent. Caption above with
|
||||
"↑ TRENDING UP · +3 CLOSE MUTUALS THIS MONTH · TWO VIA THE SAME
|
||||
OFFSITE" in mono.
|
||||
- **Trend panel** — "match rate — last 12 weeks". One line of body
|
||||
copy below ("STEADY CLIMB FROM 8% → 14%. ATTRIBUTABLE TO ONE
|
||||
COMMUNITY JOIN…"). Footer rule.
|
||||
4. **Write** a single HTML document:
|
||||
- `<!doctype html>` through `</html>`, CSS inline.
|
||||
- Background creamy off-white, body serif, mono labels everywhere.
|
||||
- Use `font-feature-settings: 'tnum'` on the metric numerals.
|
||||
- SVG bar chart with ~30 bars, varied heights.
|
||||
- `data-od-id` on ticker, sidebar, kpi grid, chart, trend.
|
||||
5. **Self-check**:
|
||||
- Reads as restrained, editorial, slightly funny — not horny.
|
||||
- Single accent token used in 3–4 places max (one KPI, two highlight
|
||||
bars, one nav active state).
|
||||
- No swipe deck, no hearts, no fire emoji.
|
||||
|
||||
## Output contract
|
||||
|
||||
Emit between `<artifact>` tags:
|
||||
|
||||
```
|
||||
<artifact identifier="dating-slug" type="text/html" title="Dating Dashboard — Title">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact, nothing after.
|
||||
@@ -0,0 +1,265 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>mutuals · your dating life, measured by the company you keep</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Serif+Text:ital@0;1&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--paper: #f4ede0;
|
||||
--panel: #f9f3e7;
|
||||
--ink: #1f1c14;
|
||||
--muted: #7a7264;
|
||||
--rule: #d6cdb6;
|
||||
--accent: #c14a2b;
|
||||
--good: #406b3a;
|
||||
--bad: #b6422f;
|
||||
--serif-display: 'DM Serif Display', 'Iowan Old Style', Georgia, serif;
|
||||
--serif-body: 'DM Serif Text', 'Iowan Old Style', Georgia, serif;
|
||||
--mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--paper); color: var(--ink); font: 14px/1.55 var(--serif-body); }
|
||||
|
||||
.ticker {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 28px;
|
||||
border-bottom: 1px solid var(--ink);
|
||||
font: 11px/1 var(--mono);
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ticker .left { display: flex; align-items: center; gap: 18px; }
|
||||
.ticker b { color: var(--ink); font-weight: 500; }
|
||||
|
||||
.layout { display: grid; grid-template-columns: 232px 1fr; min-height: calc(100vh - 44px); }
|
||||
aside.rail {
|
||||
border-right: 1px solid var(--ink);
|
||||
padding: 22px 22px 22px 28px;
|
||||
display: flex; flex-direction: column; gap: 22px;
|
||||
}
|
||||
aside .brand { font: italic 800 30px/1 var(--serif-display); letter-spacing: -0.005em; }
|
||||
aside .brand .dot { color: var(--accent); }
|
||||
aside .user { display: flex; align-items: center; gap: 10px; }
|
||||
aside .avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--ink); color: var(--paper); display: grid; place-items: center; font: 700 12px/1 var(--mono); letter-spacing: 0.06em; }
|
||||
aside .user .meta { font: 13px/1.2 var(--mono); }
|
||||
aside .user .meta b { display: block; color: var(--ink); font-weight: 500; }
|
||||
aside .user .meta span { color: var(--muted); font-size: 11px; letter-spacing: 0.06em; }
|
||||
|
||||
aside h4 { font: 11px/1 var(--mono); color: var(--muted); letter-spacing: 0.18em; text-transform: uppercase; margin: 0 0 10px; }
|
||||
aside ul { list-style: none; padding: 0; margin: 0 0 14px; display: flex; flex-direction: column; gap: 4px; }
|
||||
aside li { display: flex; justify-content: space-between; align-items: center; padding: 5px 8px; border-radius: 4px; font: 15.5px/1.2 var(--serif-body); color: var(--ink); cursor: default; }
|
||||
aside li.active { background: rgba(193,74,43,0.10); color: var(--accent); font-weight: 600; }
|
||||
aside li.active::before { content: '●'; color: var(--accent); margin-right: 6px; font-size: 9px; }
|
||||
aside li .badge { background: var(--accent); color: var(--paper); font: 10px/1 var(--mono); padding: 3px 6px; border-radius: 999px; letter-spacing: 0.06em; }
|
||||
aside li .badge.gray { background: var(--ink); }
|
||||
|
||||
aside .status {
|
||||
margin-top: auto;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--rule);
|
||||
font: 11px/1.4 var(--mono);
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
aside .status .live::before { content: '●'; color: #2f7d4a; margin-right: 6px; }
|
||||
|
||||
main { padding: 30px 36px 44px; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: minmax(120px, auto);
|
||||
gap: 22px 36px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.stat { padding: 4px 0 14px; border-bottom: 1px solid var(--rule); }
|
||||
.stat .label { font: 11px/1.4 var(--mono); color: var(--muted); letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 6px; }
|
||||
.stat .value {
|
||||
font: 800 56px/1.05 var(--serif-display);
|
||||
letter-spacing: -0.01em;
|
||||
font-feature-settings: 'tnum';
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.stat .value.good { color: var(--good); }
|
||||
.stat .value.bad { color: var(--bad); }
|
||||
.stat .value em { font-style: italic; font-weight: 400; }
|
||||
.stat .note { font: italic 13.5px/1.4 var(--serif-body); color: var(--muted); max-width: 32ch; }
|
||||
.stat .arrow { font-style: normal; color: var(--good); font-size: 14px; }
|
||||
|
||||
.panel { padding: 18px 0 24px; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--rule); margin-bottom: 18px; }
|
||||
.panel-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; margin-bottom: 14px; }
|
||||
.panel-head h3 { margin: 0; font: italic 24px/1 var(--serif-display); letter-spacing: -0.005em; }
|
||||
.panel-head .meta { font: 11px/1.4 var(--mono); color: var(--muted); letter-spacing: 0.16em; text-transform: uppercase; max-width: 56ch; text-align: right; }
|
||||
.panel svg { width: 100%; height: 220px; display: block; }
|
||||
.panel .axis { display: flex; justify-content: space-between; font: 10px/1 var(--mono); color: var(--muted); letter-spacing: 0.1em; padding: 8px 4px 0; text-transform: uppercase; }
|
||||
|
||||
.lower-panel .lede { font: italic 15px/1.55 var(--serif-body); color: var(--muted); margin: 0; max-width: 70ch; }
|
||||
.lower-panel .lede b { color: var(--ink); font-style: normal; font-weight: 600; }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
aside.rail { border-right: none; border-bottom: 1px solid var(--ink); }
|
||||
.grid { grid-template-columns: repeat(2, 1fr); gap: 18px 28px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ticker" data-od-id="ticker">
|
||||
<div class="left">
|
||||
<span>YOUR DATING LIFE, MEASURED BY THE COMPANY YOU KEEP</span>
|
||||
<span style="opacity:0.6;">·</span>
|
||||
<span>REVIEWED WEEKLY</span>
|
||||
</div>
|
||||
<div>NEXT TIER AT <b>2,080 MUTUALS</b></div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="rail" data-od-id="rail">
|
||||
<div class="brand">mutuals<span class="dot">.</span></div>
|
||||
<div class="user">
|
||||
<div class="avatar">si</div>
|
||||
<div class="meta"><b>@signals</b><span>RATIO 22.9 · TIER III</span></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Today</h4>
|
||||
<ul>
|
||||
<li>specimen <span class="badge">3</span></li>
|
||||
<li>inbox <span class="badge">3</span></li>
|
||||
<li>queue <span style="font:11px/1 var(--mono);color:var(--muted);">6</span></li>
|
||||
<li>notifications <span class="badge gray">12</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>You</h4>
|
||||
<ul>
|
||||
<li class="active">your stats</li>
|
||||
<li>mutuals & communities</li>
|
||||
<li>blocked <span style="font:11px/1 var(--mono);color:var(--muted);">14</span></li>
|
||||
<li>settings</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Archive</h4>
|
||||
<ul>
|
||||
<li>past issues</li>
|
||||
<li>expired matches <span style="font:11px/1 var(--mono);color:var(--muted);">7</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<div class="live">online · last match 11m ago</div>
|
||||
<div style="opacity:0.7;margin-top:2px;">mutuals.v0.6.1</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main data-od-id="main">
|
||||
<section class="grid" data-od-id="kpis">
|
||||
<div class="stat">
|
||||
<div class="label">Mutuals on file</div>
|
||||
<div class="value"><em>1,842</em></div>
|
||||
<p class="note"><span class="arrow">↑</span> 41 this wk · healthy growth.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Replies in 24h</div>
|
||||
<div class="value good">47</div>
|
||||
<p class="note">the people who'd text back within a day.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Communities</div>
|
||||
<div class="value"><em>14</em></div>
|
||||
<p class="note">4 active · 7 lurking · 3 inferred.</p>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="label">Match rate</div>
|
||||
<div class="value good">14%</div>
|
||||
<p class="note">above median for your cohort.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">2nd dates</div>
|
||||
<div class="value"><em>3</em></div>
|
||||
<p class="note">of 7 first dates this year. you commit.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Exes in your circle</div>
|
||||
<div class="value bad">4</div>
|
||||
<p class="note">manageable. two are now friends.</p>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="label">Shared blocks</div>
|
||||
<div class="value"><em>214</em></div>
|
||||
<p class="note">your single greatest compatibility asset.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Avg response</div>
|
||||
<div class="value"><em>2.1<span style="font-size:32px;">h</span></em></div>
|
||||
<p class="note">too fast. wait 4–6h. they notice.</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Logged-off hrs</div>
|
||||
<div class="value bad">4</div>
|
||||
<p class="note">/ 168 this wk. we beg.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" data-od-id="bars">
|
||||
<div class="panel-head">
|
||||
<h3>mutuals — <em>last 30 days</em></h3>
|
||||
<div class="meta">↑ TRENDING UP · +3 CLOSE MUTUALS THIS MONTH · TWO VIA THE SAME OFFSITE</div>
|
||||
</div>
|
||||
<svg viewBox="0 0 720 220" preserveAspectRatio="none" aria-hidden="true">
|
||||
<g fill="#1f1c14">
|
||||
<rect x="6" y="170" width="14" height="50"></rect>
|
||||
<rect x="30" y="158" width="14" height="62"></rect>
|
||||
<rect x="54" y="146" width="14" height="74"></rect>
|
||||
<rect x="78" y="172" width="14" height="48"></rect>
|
||||
<rect x="102" y="162" width="14" height="58"></rect>
|
||||
<rect x="126" y="138" width="14" height="82"></rect>
|
||||
<rect x="150" y="120" width="14" height="100"></rect>
|
||||
<rect x="174" y="148" width="14" height="72"></rect>
|
||||
<rect x="198" y="132" width="14" height="88"></rect>
|
||||
<rect x="222" y="108" width="14" height="112"></rect>
|
||||
<rect x="246" y="118" width="14" height="102"></rect>
|
||||
<rect x="270" y="154" width="14" height="66"></rect>
|
||||
<rect x="294" y="130" width="14" height="90"></rect>
|
||||
<rect x="318" y="100" width="14" height="120"></rect>
|
||||
<rect x="342" y="86" width="14" height="134"></rect>
|
||||
<rect x="366" y="116" width="14" height="104"></rect>
|
||||
<rect x="390" y="138" width="14" height="82"></rect>
|
||||
<rect x="414" y="92" width="14" height="128"></rect>
|
||||
<rect x="438" y="74" width="14" height="146"></rect>
|
||||
<rect x="462" y="106" width="14" height="114"></rect>
|
||||
<rect x="486" y="84" width="14" height="136"></rect>
|
||||
<rect x="510" y="124" width="14" height="96"></rect>
|
||||
<rect x="534" y="98" width="14" height="122"></rect>
|
||||
<rect x="558" y="68" width="14" height="152"></rect>
|
||||
<rect x="582" y="80" width="14" height="140"></rect>
|
||||
<rect x="606" y="46" width="14" height="174" fill="#c14a2b"></rect>
|
||||
<rect x="630" y="60" width="14" height="160" fill="#c14a2b"></rect>
|
||||
<rect x="654" y="92" width="14" height="128"></rect>
|
||||
<rect x="678" y="76" width="14" height="144"></rect>
|
||||
<rect x="702" y="90" width="14" height="130"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="axis"><span>MAR 18</span><span>MAR 25</span><span>APR 1</span><span>APR 8</span><span>APR 15</span><span>TODAY</span></div>
|
||||
</section>
|
||||
|
||||
<section class="panel lower-panel" data-od-id="trend">
|
||||
<div class="panel-head">
|
||||
<h3>match rate — <em>last 12 weeks</em></h3>
|
||||
<div class="meta">STEADY CLIMB FROM 8% → 14%. ATTRIBUTABLE TO ONE COMMUNITY JOIN (FOUNDERS WHO POST, WK 4).</div>
|
||||
</div>
|
||||
<p class="lede">A real climb, not a vibe. <b>One community join</b> moved your match rate more than four months of profile edits — keep posting from that circle, ship more, tweet less.</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,252 @@
|
||||
---
|
||||
name: design-brief
|
||||
description: |
|
||||
Parse a structured design brief written in I-Lang protocol format into a
|
||||
concrete design spec. Eliminates ambiguity from vague requests like
|
||||
"make it professional" by requiring explicit dimensions: palette, typography,
|
||||
layout, mood, density, and constraints.
|
||||
Trigger keywords: "design brief", "create a design brief", "ilang brief", "structured brief".
|
||||
triggers:
|
||||
- "design brief"
|
||||
- "create a design brief"
|
||||
- "ilang brief"
|
||||
- "structured brief"
|
||||
od:
|
||||
mode: design-system
|
||||
platform: desktop
|
||||
scenario: planning
|
||||
preview:
|
||||
type: html
|
||||
entry: brief-preview.html
|
||||
reload: debounce-100
|
||||
design_system:
|
||||
requires: false
|
||||
generates: true
|
||||
sections: [visual-theme, color-palette, typography, component-stylings, layout, depth-elevation, dos-and-donts, responsive, agent-prompt-guide]
|
||||
inputs:
|
||||
- name: brief
|
||||
type: string
|
||||
required: true
|
||||
description: "I-Lang formatted design brief or natural language description"
|
||||
outputs:
|
||||
primary: DESIGN.md
|
||||
secondary: brief-preview.html
|
||||
capabilities_required:
|
||||
- file_write
|
||||
---
|
||||
|
||||
# Design Brief Skill
|
||||
|
||||
Parse a structured design brief into a concrete DESIGN.md and optional visual preview. Agent, follow this workflow exactly.
|
||||
|
||||
## Background
|
||||
|
||||
The 8 dimensions in this skill are derived from analysis of the 71 design systems bundled with Open Design. Every DESIGN.md in `design-systems/` resolves at minimum: color palette, accent, typography, display font, layout model, and component style. We distilled these into 8 orthogonal dimensions that cover the decisions a designer makes before any pixel is placed. Mood and density were added because they are the two most common sources of ambiguity in natural language briefs ("make it clean" means different things to different people).
|
||||
|
||||
Dimensions intentionally excluded from the brief level: animation timing, responsive strategy, and accessibility contrast. These are enforced at the template level by individual skills (e.g., `saas-landing` handles its own responsive logic), though the generated DESIGN.md includes sensible breakpoint defaults for downstream consumption.
|
||||
|
||||
## 1. Accept input
|
||||
|
||||
The user provides a design brief in one of two formats:
|
||||
|
||||
### Option A: I-Lang structured brief
|
||||
|
||||
```
|
||||
[PLAN:@DESIGN|type=saas_landing]
|
||||
|palette=navy_and_white|accent=coral
|
||||
|typography=inter|display=space_grotesk
|
||||
|layout=single_column|max_width=1200px
|
||||
|mood=professional_minimal
|
||||
|density=spacious|section_gap=96px
|
||||
|hero=headline+subhead+cta
|
||||
|sections=features,pricing,testimonials,footer
|
||||
|exclude=animations,parallax,gradients
|
||||
|responsive=mobile_first
|
||||
```
|
||||
|
||||
### Option B: Natural language
|
||||
|
||||
> "I need a landing page for a developer tool. Clean, minimal, dark mode. Inter font. No flashy animations."
|
||||
|
||||
If the user provides Option B, convert it to the structured format using the mapping table below, then proceed. Identify every dimension explicitly stated and flag dimensions that were left unspecified.
|
||||
|
||||
### Natural language → I-Lang mapping
|
||||
|
||||
For each sentence in the natural language input, identify dimension keywords and map to the closest structured value:
|
||||
|
||||
| Natural language phrase | Dimension | I-Lang value |
|
||||
|------------------------|-----------|-------------|
|
||||
| "dark mode", "dark theme" | palette | `monochrome_dark` |
|
||||
| "light", "white background" | palette | `light_clean` |
|
||||
| "earthy", "warm tones" | palette | `earth_tones` |
|
||||
| "pop of color", "vibrant" | accent | `electric_blue` (default) or `coral` |
|
||||
| "subtle accent" | accent | `muted_sage` (default) or `slate` |
|
||||
| "clean", "minimal", "simple" | mood | `professional_minimal` |
|
||||
| "playful", "fun", "friendly" | mood | `playful` |
|
||||
| "bold", "brutalist", "raw" | mood | `brutalist` |
|
||||
| "editorial", "magazine-like" | mood | `editorial` |
|
||||
| "spacious", "lots of whitespace" | density | `spacious` |
|
||||
| "compact", "dense", "information-rich" | density | `compact` |
|
||||
| "Inter", "system font" | typography | `inter` (default) or `system_ui` |
|
||||
| "serif", "traditional" | typography | `georgia` (default) or `playfair` |
|
||||
| "monospace", "code-like" | typography | `jetbrains_mono` |
|
||||
| "no animations", "static" | exclude | `animations` |
|
||||
| "no gradients" | exclude | `gradients` |
|
||||
| "no stock photos" | exclude | `stock_photos` |
|
||||
| "single page" | layout | `single_column` |
|
||||
| "two columns", "sidebar" | layout | `two_column` |
|
||||
| "mobile first" | responsive | `mobile_first` |
|
||||
|
||||
When a phrase maps to multiple dimensions (e.g. "clean dark landing page" → mood=professional_minimal + palette=monochrome_dark + layout=single_column), resolve each dimension independently. When multiple values are listed for a single mapping, the first is the default; the agent may select the alternative only if surrounding context strongly favors it.
|
||||
|
||||
## 2. Validate dimensions
|
||||
|
||||
Every design brief must resolve these 8 dimensions. If any are missing from the input, select sensible defaults using the rules in Section 2.2.
|
||||
|
||||
The values listed below form a closed vocabulary. Only values in this table have concrete token mappings in Section 2.1. If the user provides a value not listed here, the agent must prompt for clarification rather than guessing.
|
||||
|
||||
| # | Dimension | Key | Example values |
|
||||
|---|-----------|-----|---------------|
|
||||
| 1 | Color palette | `palette` | navy_and_white, earth_tones, monochrome_dark, light_clean |
|
||||
| 2 | Accent color | `accent` | coral, electric_blue, emerald, muted_sage |
|
||||
| 3 | Body typography | `typography` | inter, system_ui, dm_sans, georgia |
|
||||
| 4 | Display typography | `display` | space_grotesk, clash_display, same_as_body, playfair |
|
||||
| 5 | Layout model | `layout` | single_column, two_column, asymmetric |
|
||||
| 6 | Mood | `mood` | professional_minimal, playful, brutalist, editorial |
|
||||
| 7 | Density | `density` | compact, balanced, spacious |
|
||||
| 8 | Constraints | `exclude` | animations, gradients, stock_photos, carousel |
|
||||
|
||||
### 2.1 Symbolic → concrete token resolution
|
||||
|
||||
Each symbolic value maps to concrete design tokens. The agent must resolve these before writing DESIGN.md:
|
||||
|
||||
| Symbolic value | Concrete tokens |
|
||||
|---------------|----------------|
|
||||
| `palette=navy_and_white` | Background: #0F172A, Surface: #1E293B, Text: #F8FAFC, Secondary: #94A3B8 |
|
||||
| `palette=monochrome_dark` | Background: #09090B, Surface: #18181B, Text: #FAFAFA, Secondary: #A1A1AA |
|
||||
| `palette=light_clean` | Background: #FFFFFF, Surface: #F8FAFC, Text: #0F172A, Secondary: #64748B |
|
||||
| `palette=earth_tones` | Background: #FFFBEB, Surface: #FEF3C7, Text: #451A03, Secondary: #92400E |
|
||||
| `accent=coral` | Accent: #F97316, Hover: #EA580C |
|
||||
| `accent=electric_blue` | Accent: #3B82F6, Hover: #2563EB |
|
||||
| `accent=emerald` | Accent: #10B981, Hover: #059669 |
|
||||
| `accent=muted_sage` | Accent: #84A98C, Hover: #6B8F73 |
|
||||
| `accent=slate` | Accent: #64748B, Hover: #475569 |
|
||||
| `typography=inter` | Body: Inter, 400, 1rem/1.6 |
|
||||
| `typography=system_ui` | Body: system-ui, 400, 1rem/1.6 |
|
||||
| `typography=dm_sans` | Body: DM Sans, 400, 1rem/1.6 |
|
||||
| `typography=georgia` | Body: Georgia, 400, 1.125rem/1.7 |
|
||||
| `display=space_grotesk` | Display: Space Grotesk, 700, clamp(2rem, 5vw, 3.5rem) |
|
||||
| `display=clash_display` | Display: Clash Display, 700, clamp(2rem, 5vw, 3.5rem) |
|
||||
| `display=playfair` | Display: Playfair Display, 700, clamp(2rem, 5vw, 3.5rem) |
|
||||
| `display=same_as_body` | Display inherits body font family, weight 600 |
|
||||
| `density=compact` | Section spacing: 48px, Content padding: 16px/24px |
|
||||
| `density=balanced` | Section spacing: 72px, Content padding: 24px/40px |
|
||||
| `density=spacious` | Section spacing: 96px, Content padding: 24px/48px |
|
||||
|
||||
Symbolic values not in this table are not valid. If the user provides an unrecognized value (e.g., `palette=ocean_blue`), the agent must prompt for clarification: "I don't recognize `palette=ocean_blue`. Did you mean `navy_and_white`, `monochrome_dark`, `light_clean`, or `earth_tones`?"
|
||||
|
||||
### 2.2 Default resolution rules
|
||||
|
||||
When a dimension is unspecified, defaults are selected based on mood compatibility:
|
||||
|
||||
| Unspecified dimension | Default rule |
|
||||
|----------------------|-------------|
|
||||
| `palette` | If mood=editorial → `light_clean`. If mood=brutalist → `monochrome_dark`. Otherwise → `light_clean`. |
|
||||
| `accent` | If palette is dark → `coral`. If palette is light → `electric_blue`. |
|
||||
| `typography` | Always → `inter` (highest cross-platform legibility). |
|
||||
| `display` | If mood=editorial → `playfair`. If mood=brutalist → `space_grotesk`. Otherwise → `same_as_body`. |
|
||||
| `layout` | Always → `single_column` (safest responsive default). |
|
||||
| `mood` | Always → `professional_minimal` (least opinionated). |
|
||||
| `density` | Always → `balanced`. |
|
||||
| `exclude` | Always → none (no constraints unless specified). |
|
||||
|
||||
If mood is also unspecified, all defaults fall back to the safe neutral set: `palette=light_clean`, `accent=electric_blue`, `typography=inter`, `display=same_as_body`, `layout=single_column`, `mood=professional_minimal`, `density=balanced`, `exclude=none`.
|
||||
|
||||
## 3. Generate DESIGN.md
|
||||
|
||||
This skill generates a new DESIGN.md from scratch based on the resolved brief dimensions. If a DESIGN.md already exists in the working directory, the agent should ask the user whether to overwrite or skip.
|
||||
|
||||
Produce a DESIGN.md following Open Design's 9-section convention. All color hex values, font stacks, and spacing values must come from the resolved tokens in Section 2.1 — do not invent values outside the resolution table.
|
||||
|
||||
```markdown
|
||||
# [Project Name] Design System
|
||||
|
||||
## Visual Theme & Atmosphere
|
||||
- Mood: [resolved from mood]
|
||||
- Feel: [derived from mood — e.g., professional_minimal → "Clean, confident, restrained"]
|
||||
- References: [if mood=editorial → "Magazine layouts, Monocle, Cereal"; if mood=brutalist → "Exposed structure, raw typography"]
|
||||
|
||||
## Color Palette & Roles
|
||||
- Background: [resolved from palette]
|
||||
- Surface: [resolved from palette]
|
||||
- Text primary: [resolved from palette]
|
||||
- Text secondary: [resolved from palette]
|
||||
- Accent: [resolved from accent]
|
||||
- Accent hover: [resolved from accent]
|
||||
|
||||
## Typography Rules
|
||||
- Display: [resolved from display], 700, clamp(2rem, 5vw, 3.5rem)
|
||||
- Body: [resolved from typography], 400, 1rem/1.6
|
||||
- Mono: JetBrains Mono, 400, 0.875rem
|
||||
|
||||
## Component Stylings
|
||||
- Buttons: [if mood=playful → "rounded-full", otherwise → "rounded-md"], accent bg, contrast text
|
||||
- Cards: surface bg, subtle border, 12px radius
|
||||
- Inputs: [if mood=brutalist → "thick border", otherwise → "transparent bg, bottom border"]
|
||||
|
||||
## Layout Principles
|
||||
- Max width: 1200px
|
||||
- Grid: [resolved from layout]
|
||||
- Section spacing: [resolved from density]
|
||||
- Content padding: [resolved from density]
|
||||
|
||||
## Depth & Elevation
|
||||
- Shadows: [if mood=brutalist → "hard 4px offset", if mood=professional_minimal → "none", otherwise → "subtle sm"]
|
||||
- Borders: 1px solid [derived from palette, 8% opacity of text color]
|
||||
|
||||
## Do's and Don'ts
|
||||
- DO use the declared color tokens exclusively.
|
||||
- DO maintain consistent section spacing.
|
||||
- DO ensure all text meets WCAG AA contrast ratio.
|
||||
- DON'T invent colors outside the palette.
|
||||
- DON'T add decorative shadows unless Depth & Elevation allows them.
|
||||
- DON'T use more than 2 display/body typefaces (monospace is a utility face for code and data — it does not count toward this limit).
|
||||
|
||||
## Responsive Behavior
|
||||
- Breakpoints: 640px (sm), 768px (md), 1024px (lg), 1280px (xl)
|
||||
- Mobile: single column, stack all sections vertically
|
||||
- Tablet: allow 2-column feature grids
|
||||
- Desktop: full layout with max-width constraint
|
||||
- Images: fluid, max-width 100%, maintain aspect ratio
|
||||
|
||||
## Agent Prompt Guide
|
||||
- Do NOT invent colors outside this palette.
|
||||
- Do NOT add box-shadows unless specified above.
|
||||
- Accent color appears maximum 3 times per viewport.
|
||||
- All interactive elements need :focus-visible outline.
|
||||
- [if exclude contains items → list each as "Do NOT use {item}."]
|
||||
```
|
||||
|
||||
## 4. Generate brief-preview.html
|
||||
|
||||
Create a single HTML file that visually renders the resolved design tokens. The preview must contain these 4 sections in order:
|
||||
|
||||
1. **Color palette swatches** — A horizontal row of rectangles, each showing one color from the Color section. Label each with its role (Background, Surface, Text, Accent) and hex code.
|
||||
2. **Typography specimens** — Three text blocks showing Display, Body, and Mono fonts at their declared sizes. Use a sample sentence ("The quick brown fox...") for each.
|
||||
3. **Spacing ruler** — A visual ruler or stacked bars showing section spacing and content padding values, labeled with their px values.
|
||||
4. **Component preview** — Render 2–3 live components (a primary button, a card with title/body, a text input) using the resolved tokens. These should be functional HTML/CSS, not screenshots.
|
||||
|
||||
Style the preview itself with the resolved design system tokens (background color, font, spacing). The preview should look like a design system documentation page.
|
||||
|
||||
## 5. Report unspecified dimensions
|
||||
|
||||
At the end of output, list any dimensions the user did not specify and the defaults that were applied, including the rule that selected each default:
|
||||
|
||||
```
|
||||
Dimensions resolved from defaults:
|
||||
- display: set to "same_as_body" (rule: mood=professional_minimal → same_as_body)
|
||||
- density: set to "balanced" (rule: static fallback, no spacing preference given)
|
||||
- exclude: set to "none" (rule: no constraints unless specified)
|
||||
```
|
||||
|
||||
This transparency prevents silent assumptions from propagating into the final design.
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: digital-eguide
|
||||
description: |
|
||||
A two-spread digital e-guide preview — page 1 is a cover (display title,
|
||||
author, "What's inside" stats, table of contents teaser); page 2 is a
|
||||
spread (lesson body with pull-quote and a step list). Lifestyle / creator
|
||||
brand tone. Use when the brief asks for an "e-guide", "digital guide",
|
||||
"lookbook", "lead magnet", "creator guide", "playbook", "PDF guide",
|
||||
or "电子指南".
|
||||
triggers:
|
||||
- "e-guide"
|
||||
- "digital guide"
|
||||
- "lead magnet"
|
||||
- "lookbook"
|
||||
- "creator guide"
|
||||
- "playbook"
|
||||
- "pdf guide"
|
||||
- "ebook"
|
||||
- "电子指南"
|
||||
- "电子书"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: marketing
|
||||
featured: 6
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
example_prompt: "Design ‘The Creator's Style & Format Guide’ — cover page and one inside spread, lifestyle creator brand."
|
||||
---
|
||||
|
||||
# Digital E-Guide Skill
|
||||
|
||||
Produce a two-page digital guide preview side-by-side. Cover on the left,
|
||||
inside spread on the right. Lifestyle creator tone, lots of negative space,
|
||||
serif display headings, careful column rhythm.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md** (injected above). Pick a serif display
|
||||
token for the title (italic ligatures encouraged), a body serif for
|
||||
long-form, and a mono token for stats / labels.
|
||||
2. **Pick the topic + author** from the brief. Generate a real title (e.g.
|
||||
"The Creator's Style & Format Guide"), a real subtitle, and a one-line
|
||||
author byline.
|
||||
3. **Layout** — center two pages on a tinted backdrop:
|
||||
- **Page 1 — cover**:
|
||||
- Eyebrow ("STYLE & FORMAT GUIDE FOR CREATORS").
|
||||
- Display title with mixed weights and one italic flourish word
|
||||
("The Creator's Style & Format guide" — `&` and `guide` italic).
|
||||
- 3-cell stat row ("16 PRINCIPLES OF STYLE", "38 DOS & DON'TS",
|
||||
"1 BLOCK, ZERO TEMPLATES") in mono, separated by `·`.
|
||||
- "What's inside" header with a 2-column TOC (chapters + page numbers
|
||||
in mono, leader dots).
|
||||
- Footer: "FIND YOUR VOICE" + page 01 mono.
|
||||
- Subtle decorative dot or sticker (CSS) in a corner.
|
||||
- **Page 2 — spread**:
|
||||
- Eyebrow with chapter number + name ("CHAPTER 02 · TONE").
|
||||
- Display sub-title ("Write like you talk — only sharper.").
|
||||
- 2-column body: opening paragraph + a numbered 4-step list ("01 Pick
|
||||
the rule", "02 Drop the filler"…).
|
||||
- Pull-quote pinned right-side: large italic display, accent color, with
|
||||
attribution.
|
||||
- Bottom strip with "EXERCISE" callout (mono label + 1 sentence prompt
|
||||
in italic).
|
||||
- Footer: chapter title + page 18 mono.
|
||||
4. **Write** a single HTML document:
|
||||
- `<!doctype html>` through `</html>`, CSS inline.
|
||||
- Pages are 600×860 paper-tone cards with 6px shadow, slight rotation
|
||||
opposing each other (±0.6deg) for a magazine-on-desk feel.
|
||||
- `data-od-id` on cover, spread, toc, pull-quote, exercise.
|
||||
5. **Self-check**:
|
||||
- Type hierarchy is editorial — title owns page 1, sub-title owns page 2.
|
||||
- Italic accent appears once per page.
|
||||
- Mono used only for labels, stats, and TOC numbers.
|
||||
|
||||
## Output contract
|
||||
|
||||
Emit between `<artifact>` tags:
|
||||
|
||||
```
|
||||
<artifact identifier="eguide-slug" type="text/html" title="E-Guide — Title">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact, nothing after.
|
||||
@@ -0,0 +1,204 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>The Creator's Style & Format Guide — Auny</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,700;1,500;1,700&family=DM+Serif+Text:ital@0;1&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--backdrop: #d8c8c0;
|
||||
--paper: #faf3ea;
|
||||
--paper-2: #f4ecdf;
|
||||
--ink: #1f1c14;
|
||||
--muted: #837964;
|
||||
--rule: #d3c9b3;
|
||||
--accent: #c44a47;
|
||||
--accent-2: #e07d52;
|
||||
--serif: 'Cormorant Garamond', 'Iowan Old Style', Georgia, serif;
|
||||
--serif-body: 'DM Serif Text', Georgia, serif;
|
||||
--sans: -apple-system, system-ui, 'Inter', sans-serif;
|
||||
--mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% 20%, #e8d4cc, transparent 70%),
|
||||
radial-gradient(ellipse 60% 60% at 80% 90%, #c79a8e, transparent 70%),
|
||||
var(--backdrop);
|
||||
font: 14px/1.55 var(--serif-body);
|
||||
padding: 60px 40px;
|
||||
display: flex; gap: 36px; justify-content: center; align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 540px; min-height: 740px;
|
||||
background: var(--paper);
|
||||
border-radius: 4px;
|
||||
padding: 44px 44px 36px;
|
||||
box-shadow: 0 30px 60px rgba(31,28,20,0.18), 0 4px 8px rgba(31,28,20,0.06);
|
||||
position: relative;
|
||||
}
|
||||
.page.left { transform: rotate(-0.6deg); }
|
||||
.page.right { transform: rotate(0.6deg); background: var(--paper-2); }
|
||||
|
||||
.eyebrow {
|
||||
font: 10.5px/1 var(--mono);
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding-bottom: 22px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.eyebrow .left, .eyebrow .right { display: flex; align-items: center; gap: 10px; }
|
||||
.eyebrow .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
||||
|
||||
/* Cover */
|
||||
.cover h1.title {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: clamp(60px, 7.5vw, 92px);
|
||||
line-height: 0.96;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 32px 0 8px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.cover h1.title .creator { color: var(--accent); font-style: italic; }
|
||||
.cover h1.title .amp { color: var(--accent-2); font-style: italic; font-weight: 500; padding: 0 6px; }
|
||||
.cover h1.title .guide { font-style: italic; font-weight: 500; }
|
||||
.cover h1.title .format { font-style: italic; font-weight: 500; padding-right: 4px; }
|
||||
|
||||
.cover .author { font: 12px/1 var(--mono); color: var(--muted); letter-spacing: 0.18em; text-transform: uppercase; margin: 16px 0 18px; display: flex; align-items: center; gap: 10px; }
|
||||
.cover .author b { color: var(--ink); font-weight: 500; }
|
||||
|
||||
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; padding: 18px 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); margin: 22px 0 28px; }
|
||||
.stat .num { font: 700 36px/1 var(--serif); letter-spacing: -0.005em; }
|
||||
.stat .lbl { font: 10px/1.4 var(--mono); color: var(--muted); letter-spacing: 0.16em; text-transform: uppercase; margin-top: 6px; max-width: 16ch; }
|
||||
|
||||
.cover h2.inside { font: italic 700 36px/1 var(--serif); margin: 14px 0 14px; letter-spacing: -0.005em; }
|
||||
.cover h2.inside em { font-style: italic; color: var(--accent); }
|
||||
|
||||
.toc { display: grid; grid-template-columns: 1fr 1fr; gap: 20px 36px; }
|
||||
.toc .item { display: flex; align-items: baseline; gap: 6px; font: 14.5px/1.4 var(--serif-body); }
|
||||
.toc .item .name { font-style: italic; color: var(--ink); }
|
||||
.toc .item .leader { flex: 1; border-bottom: 1px dotted var(--muted); transform: translateY(-2px); margin: 0 4px; }
|
||||
.toc .item .pn { font: 11px/1 var(--mono); color: var(--muted); letter-spacing: 0.06em; }
|
||||
|
||||
.cover-footer { position: absolute; left: 44px; right: 44px; bottom: 28px; display: flex; justify-content: space-between; align-items: center; font: 10.5px/1 var(--mono); color: var(--muted); letter-spacing: 0.18em; text-transform: uppercase; padding-top: 14px; border-top: 1px solid var(--rule); }
|
||||
.sticker { position: absolute; top: 280px; right: 44px; width: 92px; height: 92px; border-radius: 50%; background: var(--accent-2); transform: rotate(8deg); display: grid; place-items: center; color: #fff; font: italic 700 14px/1.1 var(--serif); text-align: center; padding: 10px; }
|
||||
.sticker::after { content: ''; position: absolute; inset: 6px; border: 1px dashed rgba(255,255,255,0.5); border-radius: 50%; }
|
||||
|
||||
/* Spread */
|
||||
.spread h2.head { font: italic 700 44px/1 var(--serif); letter-spacing: -0.005em; margin: 32px 0 6px; max-width: 18ch; }
|
||||
.spread h2.head .accent { color: var(--accent); }
|
||||
.spread .deck { font: italic 16px/1.5 var(--serif-body); color: var(--muted); margin: 0 0 22px; max-width: 50ch; }
|
||||
|
||||
.columns { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding-top: 14px; border-top: 1px solid var(--rule); }
|
||||
.columns p { margin: 0 0 14px; font: 14.5px/1.6 var(--serif-body); color: var(--ink); }
|
||||
.columns p:first-letter { font-family: var(--serif); font-size: 38px; line-height: 0.85; padding: 4px 6px 0 0; float: left; font-weight: 700; color: var(--accent); font-style: italic; }
|
||||
.steps { display: flex; flex-direction: column; gap: 10px; }
|
||||
.steps .row { display: grid; grid-template-columns: 28px 1fr; gap: 10px; align-items: baseline; padding: 8px 0; border-bottom: 1px dashed var(--rule); }
|
||||
.steps .row .n { font: 700 12px/1 var(--mono); color: var(--accent); letter-spacing: 0.08em; }
|
||||
.steps .row .body { font: 14px/1.45 var(--serif-body); }
|
||||
.steps .row .body b { color: var(--ink); font-weight: 700; font-style: italic; }
|
||||
|
||||
.pullquote {
|
||||
position: absolute; right: -16px; top: 280px;
|
||||
width: 250px;
|
||||
padding: 18px 22px;
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 8px 18px rgba(31,28,20,0.10);
|
||||
font: italic 700 22px/1.2 var(--serif);
|
||||
color: var(--ink);
|
||||
transform: rotate(2.4deg);
|
||||
}
|
||||
.pullquote .open { font-size: 56px; line-height: 0.4; color: var(--accent); display: block; height: 24px; }
|
||||
.pullquote .by { font: 11px/1 var(--mono); color: var(--muted); letter-spacing: 0.14em; text-transform: uppercase; font-weight: 400; font-style: normal; margin-top: 14px; display: block; }
|
||||
|
||||
.exercise { margin-top: 18px; padding: 14px 16px; border: 1px solid var(--accent); border-radius: 4px; background: rgba(196,74,71,0.05); display: flex; gap: 14px; align-items: center; }
|
||||
.exercise .label { font: 10.5px/1 var(--mono); color: var(--accent); letter-spacing: 0.2em; text-transform: uppercase; padding: 6px 8px; border: 1px solid var(--accent); }
|
||||
.exercise .text { font: italic 14px/1.4 var(--serif-body); color: var(--ink); }
|
||||
|
||||
.spread-footer { position: absolute; left: 44px; right: 44px; bottom: 28px; display: flex; justify-content: space-between; align-items: center; font: 10.5px/1 var(--mono); color: var(--muted); letter-spacing: 0.18em; text-transform: uppercase; padding-top: 14px; border-top: 1px solid var(--rule); }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.pullquote { right: 16px; }
|
||||
.page { width: 92vw; max-width: 540px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="page left cover" data-od-id="cover">
|
||||
<div class="eyebrow">
|
||||
<div class="left"><span class="dot"></span>STYLE & FORMAT GUIDE FOR CREATORS</div>
|
||||
<div class="right">2026 EDITION</div>
|
||||
</div>
|
||||
|
||||
<h1 class="title">The <span class="creator">Creator's</span> Style <span class="amp">&</span> <span class="format">Format</span> <span class="guide">guide</span></h1>
|
||||
|
||||
<div class="author">— BY <b>AUNY</b> · CREATOR EDUCATOR · 18 / 04 / 2026</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="num">16</div><div class="lbl">Principles of style</div></div>
|
||||
<div class="stat"><div class="num">38</div><div class="lbl">Do's & Don'ts</div></div>
|
||||
<div class="stat"><div class="num">1</div><div class="lbl">Block, zero templates</div></div>
|
||||
</div>
|
||||
|
||||
<h2 class="inside">What's <em>inside.</em></h2>
|
||||
|
||||
<div class="toc" data-od-id="toc">
|
||||
<div class="item"><span class="name">Find your voice</span><span class="leader"></span><span class="pn">04</span></div>
|
||||
<div class="item"><span class="name">Pick a format</span><span class="leader"></span><span class="pn">12</span></div>
|
||||
<div class="item"><span class="name">Tone & tension</span><span class="leader"></span><span class="pn">18</span></div>
|
||||
<div class="item"><span class="name">Visual rhythm</span><span class="leader"></span><span class="pn">24</span></div>
|
||||
<div class="item"><span class="name">Headlines that hold</span><span class="leader"></span><span class="pn">32</span></div>
|
||||
<div class="item"><span class="name">Editing the cut</span><span class="leader"></span><span class="pn">40</span></div>
|
||||
</div>
|
||||
|
||||
<div class="sticker">FOR THE FIRST DRAFT</div>
|
||||
<div class="cover-footer"><span>FIND YOUR VOICE</span><span>01 / 64</span></div>
|
||||
</article>
|
||||
|
||||
<article class="page right spread" data-od-id="spread">
|
||||
<div class="eyebrow">
|
||||
<div class="left"><span class="dot"></span>CHAPTER 02 · TONE</div>
|
||||
<div class="right">3 — RULES, 1 — EXERCISE</div>
|
||||
</div>
|
||||
|
||||
<h2 class="head">Write like you talk —<br/><span class="accent">only sharper.</span></h2>
|
||||
<p class="deck">Your voice already exists. The work is to remove the parts that aren't you, then put what's left in the order people remember. Three small rules and one Sunday-morning exercise.</p>
|
||||
|
||||
<div class="columns">
|
||||
<p>Strong writing has the cadence of speech and the precision of editing. Most beginners pick one and stop. Read your draft aloud. The sentences that catch in your throat are the ones to cut.</p>
|
||||
<div class="steps">
|
||||
<div class="row"><span class="n">01</span><span class="body"><b>Pick the rule.</b> One idea per paragraph. If two appear, split the paragraph.</span></div>
|
||||
<div class="row"><span class="n">02</span><span class="body"><b>Drop the filler.</b> "I think", "kind of", "in my opinion" — they soften, then they erase.</span></div>
|
||||
<div class="row"><span class="n">03</span><span class="body"><b>End with a verb.</b> The last beat lands harder when it asks for an action, not an adjective.</span></div>
|
||||
<div class="row"><span class="n">04</span><span class="body"><b>Read aloud once.</b> Always. The microphone is the editor.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pullquote" data-od-id="pullquote">
|
||||
<span class="open">"</span>
|
||||
Specificity is the unlock — write what only you saw.
|
||||
<span class="by">— AUNY · CHAPTER 02</span>
|
||||
</div>
|
||||
|
||||
<div class="exercise" data-od-id="exercise">
|
||||
<span class="label">EXERCISE</span>
|
||||
<span class="text">Rewrite your last three captions without the words <em>just</em>, <em>really</em>, or <em>very</em>. Keep what survives.</span>
|
||||
</div>
|
||||
|
||||
<div class="spread-footer"><span>TONE & TENSION</span><span>18 / 64</span></div>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: docs-page
|
||||
description: |
|
||||
A documentation page — left nav, scrollable article body, right-rail
|
||||
table of contents. Use when the brief mentions "docs", "documentation",
|
||||
"guide", "API reference", or "tutorial".
|
||||
triggers:
|
||||
- "docs"
|
||||
- "documentation"
|
||||
- "guide"
|
||||
- "tutorial"
|
||||
- "api reference"
|
||||
- "文档"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: engineering
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
---
|
||||
|
||||
# Docs Page Skill
|
||||
|
||||
Produce a single, three-column documentation page in one HTML file.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md** (injected above). Use the body type token for
|
||||
prose; the mono token for code; respect line-height and max-width rules.
|
||||
2. **Pick a topic** from the brief — the page should look like real docs, not
|
||||
a generic wireframe. Concrete API names, command examples, plausible
|
||||
parameters.
|
||||
3. **Lay out** three regions:
|
||||
- **Left nav** (240–280px, sticky): grouped link list, current page bolded
|
||||
with a left-edge accent stripe. 3–5 groups of 4–8 links.
|
||||
- **Article body** (max-width ~720px, centered in the middle column):
|
||||
H1, lede paragraph, H2 sections, code blocks, callout boxes (note /
|
||||
warning), inline links, lists.
|
||||
- **Right TOC** (200–240px, sticky): "On this page" with the H2/H3
|
||||
anchors, current section highlighted as the user scrolls.
|
||||
4. **Write** a single HTML document:
|
||||
- `<!doctype html>` through `</html>`, all CSS inline.
|
||||
- CSS Grid for the three columns; sticky positioning for the rails.
|
||||
- Code blocks: monospace token, soft surface fill, copy-button affordance
|
||||
(visual only — no JS needed).
|
||||
- Anchor IDs on every H2/H3 so the TOC links work.
|
||||
- `data-od-id` on the nav, article, and TOC.
|
||||
5. **Prose**: write at least 350 words of believable docs. Include at least
|
||||
one shell command, one code snippet (5–15 lines), one callout, one table.
|
||||
6. **Self-check**:
|
||||
- Body text wraps at the DS line-length sweet spot (60–75 chars).
|
||||
- Code uses the DS mono token, not generic `monospace`.
|
||||
- Accent is restrained — used for active nav item, links, one callout
|
||||
border. Not on body text.
|
||||
- Page is readable at 1280w and collapses gracefully below 900w (TOC drops
|
||||
out, nav becomes a top drawer).
|
||||
|
||||
## Output contract
|
||||
|
||||
Emit between `<artifact>` tags:
|
||||
|
||||
```
|
||||
<artifact identifier="docs-slug" type="text/html" title="Docs — Page Title">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact, nothing after.
|
||||
@@ -0,0 +1,122 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Filebase docs — Quickstart</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fafaf9; --fg: #1c1b1a; --muted: #6b6964; --border: #e6e4e0;
|
||||
--accent: #c96442; --surface: #ffffff; --code-bg: #f4f4f2;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.6 -apple-system, system-ui, sans-serif; }
|
||||
.topbar { background: var(--surface); border-bottom: 1px solid var(--border); padding: 12px 28px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.topbar .brand { font-weight: 600; }
|
||||
.topbar input { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border); width: 280px; font: inherit; background: var(--bg); }
|
||||
.layout { display: grid; grid-template-columns: 240px minmax(0, 1fr) 220px; gap: 0; min-height: calc(100vh - 50px); }
|
||||
@media (max-width: 1024px) { .layout { grid-template-columns: 220px 1fr; } .toc { display: none; } }
|
||||
@media (max-width: 720px) { .layout { grid-template-columns: 1fr; } .sidebar { display: none; } }
|
||||
.sidebar { padding: 24px 16px; border-right: 1px solid var(--border); overflow-y: auto; font-size: 14px; }
|
||||
.sidebar .group { margin-bottom: 22px; }
|
||||
.sidebar .group-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; padding: 0 8px 8px; }
|
||||
.sidebar a { display: block; color: var(--fg); text-decoration: none; padding: 5px 8px; border-radius: 6px; }
|
||||
.sidebar a:hover { background: var(--surface); }
|
||||
.sidebar a.active { background: var(--accent); color: white; }
|
||||
article { padding: 40px 56px 80px; max-width: 760px; }
|
||||
.crumbs { color: var(--muted); font-size: 13px; margin-bottom: 12px; }
|
||||
h1 { font-size: 36px; letter-spacing: -0.02em; margin: 0 0 12px; }
|
||||
.lede { color: var(--muted); font-size: 17px; margin: 0 0 32px; }
|
||||
h2 { font-size: 22px; letter-spacing: -0.01em; margin: 40px 0 12px; }
|
||||
h3 { font-size: 16px; margin: 24px 0 8px; }
|
||||
p { margin: 12px 0; }
|
||||
code { font-family: ui-monospace, monospace; background: var(--code-bg); padding: 1px 5px; border-radius: 4px; font-size: 0.9em; }
|
||||
pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; overflow-x: auto; font-size: 13px; line-height: 1.55; }
|
||||
pre code { background: transparent; padding: 0; }
|
||||
.callout { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 8px; padding: 14px 18px; margin: 20px 0; font-size: 14px; }
|
||||
.callout .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--accent); margin-bottom: 4px; }
|
||||
.toc { padding: 40px 24px 24px; font-size: 13px; border-left: 1px solid var(--border); }
|
||||
.toc .toc-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 10px; }
|
||||
.toc a { display: block; color: var(--muted); text-decoration: none; padding: 4px 0; }
|
||||
.toc a.active { color: var(--accent); font-weight: 500; }
|
||||
.pager { display: flex; justify-content: space-between; gap: 12px; margin-top: 56px; padding-top: 24px; border-top: 1px solid var(--border); }
|
||||
.pager a { flex: 1; text-decoration: none; color: var(--fg); padding: 12px 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; }
|
||||
.pager a small { display: block; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar" data-od-id="topbar">
|
||||
<span class="brand">◰ Filebase docs</span>
|
||||
<input placeholder="Search · ⌘K" />
|
||||
</header>
|
||||
<div class="layout">
|
||||
<nav class="sidebar" data-od-id="sidebar">
|
||||
<div class="group">
|
||||
<div class="group-label">Getting started</div>
|
||||
<a href="#" class="active">Quickstart</a>
|
||||
<a href="#">Concepts</a>
|
||||
<a href="#">Authentication</a>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="group-label">Sync engine</div>
|
||||
<a href="#">Block-level deltas</a>
|
||||
<a href="#">Conflict resolution</a>
|
||||
<a href="#">Resumable uploads</a>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="group-label">CLI</div>
|
||||
<a href="#">Install</a>
|
||||
<a href="#">Configuration</a>
|
||||
<a href="#">Subcommands</a>
|
||||
</div>
|
||||
</nav>
|
||||
<article data-od-id="article">
|
||||
<div class="crumbs">Docs › Getting started › Quickstart</div>
|
||||
<h1>Quickstart</h1>
|
||||
<p class="lede">Sync your first folder in under five minutes. The CLI is the fastest path; the desktop app and the API client all wrap the same engine.</p>
|
||||
<h2 id="install">1. Install the CLI</h2>
|
||||
<p>The CLI is distributed as a single binary for macOS, Linux, and Windows.</p>
|
||||
<pre><code># macOS · Homebrew
|
||||
brew install filebase
|
||||
|
||||
# Linux · curl
|
||||
curl -fsSL https://get.filebase.dev | sh</code></pre>
|
||||
<p>Verify the install:</p>
|
||||
<pre><code>filebase --version
|
||||
# filebase 0.6.4</code></pre>
|
||||
<h2 id="auth">2. Authenticate</h2>
|
||||
<p>Sign in with your Filebase account. The token is stored in <code>~/.config/filebase/credentials</code>.</p>
|
||||
<pre><code>filebase auth login
|
||||
# → opens your browser
|
||||
# ✓ Logged in as you@example.com</code></pre>
|
||||
<div class="callout">
|
||||
<div class="label">Note</div>
|
||||
On servers without a browser, use <code>filebase auth login --device</code> for a device-code flow.
|
||||
</div>
|
||||
<h2 id="sync">3. Sync a folder</h2>
|
||||
<p>Pick a local directory and link it to a remote root. Filebase watches it for changes and pushes block-level diffs in the background.</p>
|
||||
<pre><code>cd ~/projects
|
||||
filebase init my-team
|
||||
filebase sync</code></pre>
|
||||
<h3>Excluding files</h3>
|
||||
<p>Add a <code>.filebaseignore</code> at the root of the synced folder. Same syntax as <code>.gitignore</code>:</p>
|
||||
<pre><code>node_modules/
|
||||
*.log
|
||||
build/</code></pre>
|
||||
<h2 id="next">4. Where to go next</h2>
|
||||
<p>Read <a href="#">Conflict resolution</a> to understand how Filebase merges concurrent edits, or skip to the <a href="#">CLI reference</a> for the full subcommand list.</p>
|
||||
<div class="pager">
|
||||
<a href="#"><small>← Previous</small>Concepts</a>
|
||||
<a href="#" style="text-align: right;"><small>Next →</small>Conflict resolution</a>
|
||||
</div>
|
||||
</article>
|
||||
<aside class="toc" data-od-id="toc">
|
||||
<div class="toc-label">On this page</div>
|
||||
<a href="#install" class="active">1. Install the CLI</a>
|
||||
<a href="#auth">2. Authenticate</a>
|
||||
<a href="#sync">3. Sync a folder</a>
|
||||
<a href="#next">4. Where to go next</a>
|
||||
</aside>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: email-marketing
|
||||
description: |
|
||||
A brand product-launch email — masthead with wordmark, hero image block,
|
||||
headline lockup with skewed-italic accent, body copy, primary CTA, and a
|
||||
specifications grid. Pure HTML email layout (centered single column, table
|
||||
fallback). Use when the brief asks for an "email", "newsletter blast",
|
||||
"MJML", "product launch email", or "email template".
|
||||
triggers:
|
||||
- "email"
|
||||
- "email template"
|
||||
- "newsletter"
|
||||
- "email blast"
|
||||
- "product launch email"
|
||||
- "mjml"
|
||||
- "邮件营销"
|
||||
- "邮件模板"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: marketing
|
||||
featured: 7
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
example_prompt: "Design a launch email for a sporty running shoe brand — masthead, hero, big headline lockup, specs grid, CTA."
|
||||
---
|
||||
|
||||
# Email Marketing Skill
|
||||
|
||||
Produce a single HTML email — centered, single column, no chrome around the
|
||||
email body. Treat it like a marketing artifact: one big idea, one CTA.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md** (injected above). Email leans on the display
|
||||
font more than any other surface — pick the loudest type token in the DS
|
||||
for the headline lockup.
|
||||
2. **Pick the brand + product** from the brief. Generate a real wordmark, a
|
||||
real product name, and one real benefit sentence — no placeholders.
|
||||
3. **Layout**, in order, all centered inside a 600–680px column on a tinted
|
||||
page background (so the email body looks like an email, not the page):
|
||||
- **Masthead** — wordmark on the left + 3 short nav links (SHOP, JOURNAL,
|
||||
MEMBERS) on the right. Thin underline.
|
||||
- **Hero block** — a 16:9 product image placeholder. Use a DS-tinted
|
||||
gradient or a stylized SVG silhouette of the product (shoe, bottle,
|
||||
headphones, whatever the brief implies). Add a tiny brand stamp on the
|
||||
top-left and a colorway tag on the bottom-left.
|
||||
- **Eyebrow** — small caps, accent color, separated by `·` characters
|
||||
(e.g. "NEW · MAX-CUSHION TRAINER · EMBER FLARE").
|
||||
- **Headline lockup** — 2–3 line headline using the display font, all caps,
|
||||
extra-tight tracking. Apply a slight skew (`transform: skew(-6deg)`) on
|
||||
one accent word to give it a sporty parallelogram feel.
|
||||
- **Body** — 2–3 sentence paragraph, left-aligned, body font.
|
||||
- **Primary CTA** — solid pill or block button. One only.
|
||||
- **Specs grid** — 2×2 grid of (big number + unit + label) callouts using
|
||||
the display font for the numbers.
|
||||
- **Footer** — wordmark, address line, unsubscribe + view-in-browser links.
|
||||
4. **Write** a single HTML document:
|
||||
- `<!doctype html>` through `</html>`, CSS inline.
|
||||
- Center the column with `margin: 0 auto`. Set `body { background: <tint> }`
|
||||
so the email-on-page metaphor reads.
|
||||
- No external images — use inline SVG or DS-tinted gradient blocks for the
|
||||
product photo.
|
||||
- `data-od-id` on the masthead, hero, headline, CTA, specs.
|
||||
5. **Self-check**:
|
||||
- Email reads top to bottom in 8–10 seconds.
|
||||
- One CTA. Accent appears at most twice (eyebrow + CTA, or headline word).
|
||||
- Looks legible on a 480px window (column reflows, type drops one step).
|
||||
|
||||
## Output contract
|
||||
|
||||
Emit between `<artifact>` tags:
|
||||
|
||||
```
|
||||
<artifact identifier="email-slug" type="text/html" title="Email — Subject Line">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact, nothing after.
|
||||
@@ -0,0 +1,159 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SPORT TEST — Meet the Axis Pro</title>
|
||||
<style>
|
||||
:root {
|
||||
--page: #d9d6d0;
|
||||
--paper: #f4efe7;
|
||||
--ink: #1a1816;
|
||||
--muted: #6b6964;
|
||||
--border: #d8d3c8;
|
||||
--accent: #d8482b;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--page); color: var(--ink); font: 15px/1.55 'Inter', -apple-system, system-ui, sans-serif; }
|
||||
.frame { max-width: 680px; margin: 0 auto; background: var(--paper); padding: 0; }
|
||||
.masthead { display: flex; justify-content: space-between; align-items: center; padding: 22px 32px; border-bottom: 1px solid var(--border); }
|
||||
.wordmark { display: flex; align-items: center; gap: 10px; font-family: 'Anton', 'Bebas Neue', Impact, sans-serif; font-size: 22px; letter-spacing: 0.04em; }
|
||||
.wordmark .lockup { display: flex; align-items: center; gap: 8px; }
|
||||
.wordmark .mark { width: 22px; height: 22px; background: var(--accent); transform: skew(-12deg); display: inline-block; }
|
||||
.wordmark .est { font: 11px/1 ui-monospace, monospace; color: var(--muted); padding: 4px 6px; border: 1px solid var(--border); border-radius: 3px; letter-spacing: 0.08em; }
|
||||
.nav { display: flex; gap: 28px; font-size: 12px; letter-spacing: 0.18em; color: var(--ink); }
|
||||
.nav a { color: inherit; text-decoration: none; }
|
||||
|
||||
.hero { position: relative; aspect-ratio: 4 / 3; background:
|
||||
radial-gradient(circle at 30% 20%, #ffd6b8 0%, transparent 55%),
|
||||
radial-gradient(circle at 75% 70%, #f59a6c 0%, transparent 60%),
|
||||
linear-gradient(135deg, #c9c4b8 0%, #aaa39a 100%); overflow: hidden; }
|
||||
.hero .stamp-tl { position: absolute; top: 18px; left: 22px; font: 11px/1 ui-monospace, monospace; color: rgba(26,24,22,0.78); letter-spacing: 0.18em; }
|
||||
.hero .stamp-bl { position: absolute; bottom: 18px; left: 22px; font: 11px/1 ui-monospace, monospace; color: rgba(26,24,22,0.78); letter-spacing: 0.18em; }
|
||||
.hero .stamp-br { position: absolute; bottom: 18px; right: 22px; font: 11px/1 ui-monospace, monospace; color: rgba(26,24,22,0.6); letter-spacing: 0.18em; }
|
||||
.hero svg.shoe { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 78%; height: auto; filter: drop-shadow(0 18px 26px rgba(26,24,22,0.18)); }
|
||||
|
||||
.article { padding: 44px 44px 12px; }
|
||||
.eyebrow { font: 11px/1 ui-monospace, monospace; color: var(--accent); letter-spacing: 0.22em; margin-bottom: 28px; display: flex; gap: 12px; align-items: center; }
|
||||
.eyebrow span.bar { display: inline-block; width: 22px; height: 2px; background: var(--accent); }
|
||||
h1.lockup { font-family: 'Anton', 'Bebas Neue', Impact, sans-serif; font-weight: 400; font-size: clamp(56px, 9vw, 96px); line-height: 0.95; letter-spacing: -0.005em; margin: 0 0 28px; text-transform: uppercase; }
|
||||
h1.lockup .axis { color: var(--accent); display: inline-block; transform: skew(-8deg); }
|
||||
p.body { font-size: 16px; line-height: 1.55; color: var(--ink); margin: 0 0 30px; max-width: 56ch; }
|
||||
p.body em { font-style: italic; color: var(--accent); }
|
||||
|
||||
.cta { display: inline-flex; align-items: center; gap: 14px; background: var(--ink); color: var(--paper); padding: 14px 22px; font: 12px/1 'Inter', sans-serif; letter-spacing: 0.2em; text-transform: uppercase; text-decoration: none; }
|
||||
.cta .arrow { display: inline-block; width: 22px; height: 1px; background: var(--paper); position: relative; }
|
||||
.cta .arrow::after { content: ''; position: absolute; right: 0; top: -3px; border: 4px solid transparent; border-left-color: var(--paper); }
|
||||
|
||||
.specs { padding: 56px 44px 12px; border-top: 1px solid var(--border); margin-top: 44px; }
|
||||
.specs .head { font: 11px/1 ui-monospace, monospace; color: var(--accent); letter-spacing: 0.22em; margin-bottom: 24px; display: flex; align-items: center; gap: 10px; }
|
||||
.specs .head span.bar { display: inline-block; width: 22px; height: 2px; background: var(--accent); }
|
||||
.specs-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 32px 48px; }
|
||||
.spec .num { font-family: 'Anton', 'Bebas Neue', Impact, sans-serif; font-size: 56px; line-height: 0.9; letter-spacing: -0.005em; }
|
||||
.spec .num sup { font-size: 18px; vertical-align: top; margin-left: 4px; color: var(--muted); font-family: ui-monospace, monospace; letter-spacing: 0.04em; }
|
||||
.spec .label { font: 11px/1.45 ui-monospace, monospace; color: var(--muted); letter-spacing: 0.16em; text-transform: uppercase; margin-top: 8px; max-width: 22ch; }
|
||||
|
||||
.footer { padding: 56px 44px 40px; margin-top: 32px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; }
|
||||
.footer .left { display: flex; flex-direction: column; gap: 12px; font-size: 12px; color: var(--muted); }
|
||||
.footer .marks { display: flex; align-items: center; gap: 10px; }
|
||||
.footer .right { font: 11px/1.6 ui-monospace, monospace; color: var(--muted); letter-spacing: 0.06em; text-align: right; }
|
||||
.footer a { color: var(--muted); }
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.article, .specs, .footer { padding-left: 24px; padding-right: 24px; }
|
||||
h1.lockup { font-size: 48px; }
|
||||
.nav { display: none; }
|
||||
.specs-grid { grid-template-columns: 1fr 1fr; gap: 24px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame" data-od-id="email">
|
||||
<header class="masthead" data-od-id="masthead">
|
||||
<div class="wordmark">
|
||||
<span class="mark"></span>
|
||||
<span class="lockup">SPORT TEST</span>
|
||||
<span class="est">EST · 2024</span>
|
||||
</div>
|
||||
<nav class="nav"><a href="#">SHOP</a><a href="#">JOURNAL</a><a href="#">MEMBERS</a></nav>
|
||||
</header>
|
||||
|
||||
<div class="hero" data-od-id="hero">
|
||||
<div class="stamp-tl">— SPORT TEST</div>
|
||||
<svg class="shoe" viewBox="0 0 600 280" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="upper" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ffe0c4"/>
|
||||
<stop offset="55%" stop-color="#f78c4c"/>
|
||||
<stop offset="100%" stop-color="#c8442d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="midsole" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fff8ee"/>
|
||||
<stop offset="100%" stop-color="#e7dccd"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M 60 180 C 80 130, 160 90, 230 92 C 290 94, 330 110, 380 115 C 430 120, 470 125, 500 145 C 530 165, 540 195, 520 210 L 100 210 C 80 210, 55 200, 60 180 Z" fill="url(#upper)" stroke="#7c2615" stroke-width="2"/>
|
||||
<path d="M 60 180 L 100 210 L 520 210 L 540 200 C 550 190, 545 175, 530 175 L 90 175 C 75 175, 60 175, 60 180 Z" fill="url(#midsole)" stroke="#7c2615" stroke-width="2"/>
|
||||
<path d="M 100 210 L 100 230 L 540 230 L 540 220" fill="none" stroke="#7c2615" stroke-width="3" stroke-linecap="round"/>
|
||||
<g stroke="#7c2615" stroke-width="2" fill="none" opacity="0.85">
|
||||
<path d="M 200 110 C 220 130, 230 145, 240 165"/>
|
||||
<path d="M 250 105 C 270 125, 280 140, 290 160"/>
|
||||
<path d="M 300 105 C 320 125, 330 140, 340 160"/>
|
||||
<path d="M 350 110 C 370 130, 380 145, 390 165"/>
|
||||
</g>
|
||||
<g fill="#7c2615">
|
||||
<circle cx="220" cy="160" r="3"/>
|
||||
<circle cx="270" cy="158" r="3"/>
|
||||
<circle cx="320" cy="158" r="3"/>
|
||||
<circle cx="370" cy="160" r="3"/>
|
||||
</g>
|
||||
<path d="M 405 145 Q 470 130, 500 150 Q 470 165, 410 162 Z" fill="#fffbf5" stroke="#7c2615" stroke-width="2"/>
|
||||
</svg>
|
||||
<div class="stamp-bl">— EMBER FLARE</div>
|
||||
<div class="stamp-br">DROP 04 · 04—2026</div>
|
||||
</div>
|
||||
|
||||
<section class="article" data-od-id="article">
|
||||
<div class="eyebrow"><span class="bar"></span>NEW · MAX-CUSHION TRAINER · EMBER FLARE</div>
|
||||
<h1 class="lockup" data-od-id="headline">
|
||||
Meet the<br/>
|
||||
<span class="axis">Axis Pro.</span><br/>
|
||||
A sneaker that runs.
|
||||
</h1>
|
||||
<p class="body">A plush, gel-cushioned trainer wrapped in a painterly flame-knit upper. Built for long days on the road, café runs, and everything between — softer underfoot, louder on the outside. Limited first drop in <em>Ember Flare</em>.</p>
|
||||
<a class="cta" href="#" data-od-id="cta">Shop the Axis Pro <span class="arrow"></span></a>
|
||||
</section>
|
||||
|
||||
<section class="specs" data-od-id="specs">
|
||||
<div class="head"><span class="bar"></span>SPECIFICATIONS · WOMEN'S</div>
|
||||
<div class="specs-grid">
|
||||
<div class="spec">
|
||||
<div class="num">7.4<sup>OZ</sup></div>
|
||||
<div class="label">Weight (women's US 8)</div>
|
||||
</div>
|
||||
<div class="spec">
|
||||
<div class="num">34<sup>MM</sup></div>
|
||||
<div class="label">Max-cushion stack at the heel</div>
|
||||
</div>
|
||||
<div class="spec">
|
||||
<div class="num">8<sup>MM</sup></div>
|
||||
<div class="label">Heel-to-toe drop for low-impact landing</div>
|
||||
</div>
|
||||
<div class="spec">
|
||||
<div class="num" style="font-size:42px;">Gel-02</div>
|
||||
<div class="label">Heel & forefoot gel shock pods</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer" data-od-id="footer">
|
||||
<div class="left">
|
||||
<div class="marks"><span style="display:inline-block;width:18px;height:18px;background:var(--accent);transform:skew(-12deg);"></span><span class="lockup" style="font-family:'Anton',sans-serif;font-size:18px;letter-spacing:0.04em;">SPORT TEST</span></div>
|
||||
<div>118 Stillman St · Brooklyn NY 11211</div>
|
||||
<div><a href="#">Unsubscribe</a> · <a href="#">View in browser</a></div>
|
||||
</div>
|
||||
<div class="right">© 2026 SPORT TEST<br/>ALL RIGHTS RESERVED</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: eng-runbook
|
||||
description: |
|
||||
An engineering runbook — service overview, alerts table, dashboards
|
||||
links, common procedures with copy-pasteable commands, on-call rotation,
|
||||
and an incident-response checklist. Use when the brief mentions
|
||||
"runbook", "ops doc", "on-call guide", "SRE doc", or "运维手册".
|
||||
triggers:
|
||||
- "runbook"
|
||||
- "ops doc"
|
||||
- "on-call"
|
||||
- "sre doc"
|
||||
- "service runbook"
|
||||
- "运维手册"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: engineering
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
example_prompt: "Write a runbook for our auth service — alerts, dashboards, common procedures, on-call rotation."
|
||||
---
|
||||
|
||||
# Engineering Runbook Skill
|
||||
|
||||
Produce a single-page engineering runbook.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read DESIGN.md.
|
||||
2. Identify the service from the brief.
|
||||
3. Layout:
|
||||
- Header: service name, owner team, severity tier, version.
|
||||
- Service summary paragraph + dependency list.
|
||||
- Alerts table: alert name / severity / what it means / first response.
|
||||
- Dashboards & links list.
|
||||
- Common procedures block (3–4) with code blocks (deploy, rollback, rotate keys).
|
||||
- On-call rotation table (week / primary / secondary / backup).
|
||||
- Incident response checklist (5 numbered steps).
|
||||
4. One inline `<style>`, semantic HTML, monospace for code blocks.
|
||||
|
||||
## Output contract
|
||||
|
||||
```
|
||||
<artifact identifier="runbook-name" type="text/html" title="Service Runbook">
|
||||
<!doctype html>...</artifact>
|
||||
```
|
||||
@@ -0,0 +1,250 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Auth Service · Runbook</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0c0e14;
|
||||
--paper: #14171f;
|
||||
--paper-2: #1c2030;
|
||||
--ink: #eaecf3;
|
||||
--muted: #8b94ad;
|
||||
--line: #262b3b;
|
||||
--accent: #6ee7b7;
|
||||
--accent-soft: rgba(110,231,183,0.1);
|
||||
--warn: #fbbf24;
|
||||
--danger: #f87171;
|
||||
--display: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
|
||||
--mono: ui-monospace, 'JetBrains Mono', SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--body); font-size: 14px; line-height: 1.6; }
|
||||
.page { max-width: 1100px; margin: 0 auto; padding: 32px 28px 64px; }
|
||||
|
||||
/* Header */
|
||||
.head { display: flex; justify-content: space-between; align-items: flex-end; padding-bottom: 24px; border-bottom: 1px solid var(--line); margin-bottom: 28px; }
|
||||
.head-left { display: flex; flex-direction: column; gap: 6px; }
|
||||
.crumb { font-family: var(--mono); font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
h1 { font-family: var(--display); font-size: 36px; margin: 4px 0; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.head-meta { font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
|
||||
.head-meta span { color: var(--accent); }
|
||||
.pill {
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 999px;
|
||||
font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
|
||||
}
|
||||
.pill.tier { background: var(--accent-soft); color: var(--accent); border: 1px solid rgba(110,231,183,0.3); }
|
||||
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
||||
|
||||
section { margin-top: 40px; }
|
||||
h2 { font-family: var(--display); font-size: 22px; margin: 0 0 14px; letter-spacing: -0.005em; font-weight: 700; }
|
||||
h2 .index { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-right: 12px; vertical-align: middle; }
|
||||
|
||||
/* Summary */
|
||||
.summary { display: grid; grid-template-columns: 1.4fr 1fr; gap: 14px; }
|
||||
.panel { padding: 22px 24px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; }
|
||||
.panel p { margin: 0 0 12px; }
|
||||
.panel p:last-child { margin: 0; }
|
||||
.deps h3 { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin: 0 0 10px; font-weight: 500; }
|
||||
.deps ul { padding: 0; margin: 0; list-style: none; display: flex; flex-direction: column; gap: 8px; font-family: var(--mono); font-size: 12.5px; }
|
||||
.deps li { display: flex; justify-content: space-between; padding: 8px 12px; background: var(--paper-2); border-radius: 6px; }
|
||||
.deps li .ok { color: var(--accent); }
|
||||
.deps li .warn { color: var(--warn); }
|
||||
|
||||
/* Tables */
|
||||
table { width: 100%; border-collapse: collapse; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
th, td { text-align: left; padding: 12px 16px; border-bottom: 1px solid var(--line); font-size: 13px; vertical-align: top; }
|
||||
th { font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); background: var(--paper-2); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
td.code, .panel code { font-family: var(--mono); }
|
||||
.sev { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 4px; font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
|
||||
.sev-1 { background: rgba(248,113,113,0.15); color: var(--danger); }
|
||||
.sev-2 { background: rgba(251,191,36,0.15); color: var(--warn); }
|
||||
.sev-3 { background: rgba(110,231,183,0.15); color: var(--accent); }
|
||||
|
||||
/* Procedure cards */
|
||||
.procs { display: flex; flex-direction: column; gap: 14px; }
|
||||
.proc { padding: 18px 22px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; }
|
||||
.proc-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
|
||||
.proc-head h3 { margin: 0; font-family: var(--display); font-size: 17px; }
|
||||
.proc-head .when { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
||||
pre { background: var(--paper-2); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; overflow-x: auto; font-family: var(--mono); font-size: 12.5px; line-height: 1.6; color: #cdd6f4; margin: 8px 0 0; }
|
||||
pre .cmt { color: var(--muted); }
|
||||
pre .var { color: var(--warn); }
|
||||
pre .ok { color: var(--accent); }
|
||||
|
||||
/* On-call */
|
||||
.rota { background: var(--paper); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
|
||||
/* Checklist */
|
||||
.checklist { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.step { padding: 18px 20px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; display: flex; gap: 16px; align-items: flex-start; }
|
||||
.step-num { flex: 0 0 36px; width: 36px; height: 36px; border-radius: 50%; background: var(--accent); color: var(--bg); display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-family: var(--display); font-size: 16px; }
|
||||
.step h4 { margin: 0 0 6px; font-family: var(--display); font-size: 15px; }
|
||||
.step p { margin: 0; color: var(--muted); font-size: 13px; }
|
||||
.step code { font-family: var(--mono); background: var(--paper-2); padding: 2px 6px; border-radius: 4px; font-size: 12px; color: var(--accent); }
|
||||
|
||||
footer { margin-top: 56px; padding-top: 18px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.summary, .checklist { grid-template-columns: 1fr; }
|
||||
h1 { font-size: 26px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="head">
|
||||
<div class="head-left">
|
||||
<div class="crumb">Northwind / Identity / Auth</div>
|
||||
<h1>auth-service</h1>
|
||||
<div class="head-meta">Owned by <span>@identity-platform</span> · v4.7.2 · Last reviewed 14 Oct 2025</div>
|
||||
</div>
|
||||
<span class="pill tier"><span class="dot"></span>Tier 0 · production-critical</span>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2><span class="index">01</span>Service summary</h2>
|
||||
<div class="summary">
|
||||
<div class="panel">
|
||||
<p><strong>auth-service</strong> issues, validates, and revokes session tokens for every Northwind product surface — web, mobile, and the public API. It owns the password store, the TOTP/WebAuthn enrollments, and the audit-log writer for all auth events.</p>
|
||||
<p>If <code>auth-service</code> is down, customers cannot log in or refresh sessions. Existing valid sessions continue to work for their TTL (15 minutes) but no new auth happens.</p>
|
||||
</div>
|
||||
<div class="panel deps">
|
||||
<h3>Dependencies</h3>
|
||||
<ul>
|
||||
<li><span>Postgres · auth-db</span><span class="ok">healthy</span></li>
|
||||
<li><span>Redis · session-cache</span><span class="ok">healthy</span></li>
|
||||
<li><span>KMS · auth-keyring</span><span class="ok">healthy</span></li>
|
||||
<li><span>SES · transactional</span><span class="warn">degraded</span></li>
|
||||
<li><span>Pager · oncall.northwind</span><span class="ok">healthy</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="index">02</span>Alerts you might wake up to</h2>
|
||||
<table>
|
||||
<thead><tr><th>Alert</th><th>Severity</th><th>What it means</th><th>First response</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="code">auth.login_5xx_rate > 1%</td>
|
||||
<td><span class="sev sev-1">SEV-1</span></td>
|
||||
<td>Login endpoint returning errors. Customers are locked out.</td>
|
||||
<td>Check Postgres + Redis dashboards. Roll back last deploy if < 30 min old.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="code">auth.token_refresh_lag_p95 > 800ms</td>
|
||||
<td><span class="sev sev-2">SEV-2</span></td>
|
||||
<td>Refresh path is slow. Web app starts to feel sluggish.</td>
|
||||
<td>Inspect Redis CPU + connection count. Scale read replicas if needed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="code">auth.signup_failure > 10/min</td>
|
||||
<td><span class="sev sev-2">SEV-2</span></td>
|
||||
<td>New signups are failing. Often SES bounces or SMTP auth.</td>
|
||||
<td>Check SES bounce rate. Failover transactional queue to backup region.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="code">auth.kms_signing_errors > 0</td>
|
||||
<td><span class="sev sev-1">SEV-1</span></td>
|
||||
<td>KMS can't sign session tokens. New logins fail; existing sessions OK.</td>
|
||||
<td>Page the security team. Do not roll keys without a security engineer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="code">auth.audit_writer_backlog > 5k</td>
|
||||
<td><span class="sev sev-3">SEV-3</span></td>
|
||||
<td>Audit log writer is falling behind. Compliance impact.</td>
|
||||
<td>Drain manually. Open a ticket; not a wake-up.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="index">03</span>Common procedures</h2>
|
||||
<div class="procs">
|
||||
<div class="proc">
|
||||
<div class="proc-head"><h3>Deploy a new version</h3><span class="when">Use during business hours</span></div>
|
||||
<p>Deploys are blue/green. The script waits for two consecutive healthchecks before promoting traffic.</p>
|
||||
<pre><span class="cmt"># Deploy auth-service v4.7.3 to production</span>
|
||||
$ nw deploy auth-service --tag <span class="var">v4.7.3</span> --env production
|
||||
|
||||
<span class="cmt"># Wait for two consecutive healthchecks (~90 s), then promote.</span>
|
||||
$ nw deploy promote auth-service --env production
|
||||
<span class="ok">→ traffic shifted: 10% / 50% / 100%</span></pre>
|
||||
</div>
|
||||
<div class="proc">
|
||||
<div class="proc-head"><h3>Roll back to last known good</h3><span class="when">Use when error rate > 1% post-deploy</span></div>
|
||||
<pre><span class="cmt"># Rolls back to the previously promoted version, no rebuild.</span>
|
||||
$ nw deploy rollback auth-service --env production
|
||||
<span class="ok">→ rolled back to v4.7.2 in 38 s</span></pre>
|
||||
</div>
|
||||
<div class="proc">
|
||||
<div class="proc-head"><h3>Rotate signing keys</h3><span class="when">Schedule with security; never solo</span></div>
|
||||
<pre><span class="cmt"># 1. Generate the new signing key in KMS</span>
|
||||
$ nw kms create-key --alias auth-signing-<span class="var">$(date +%Y%m%d)</span>
|
||||
|
||||
<span class="cmt"># 2. Mark the new key as the primary; old key remains valid for 24h</span>
|
||||
$ nw kms set-primary auth-signing --key <span class="var"><arn></span>
|
||||
|
||||
<span class="cmt"># 3. After 24h, schedule deletion of the previous key</span>
|
||||
$ nw kms schedule-deletion auth-signing --key <span class="var"><old-arn></span> --days 30</pre>
|
||||
</div>
|
||||
<div class="proc">
|
||||
<div class="proc-head"><h3>Drain audit-log backlog</h3><span class="when">Use when audit_writer_backlog alert fires</span></div>
|
||||
<pre>$ nw exec auth-service -- bin/audit-drain --batch <span class="var">5000</span>
|
||||
<span class="ok">→ drained 4,812 entries in 12 s; backlog now 0</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="index">04</span>On-call rotation · this month</h2>
|
||||
<table class="rota">
|
||||
<thead><tr><th>Week</th><th>Primary</th><th>Secondary</th><th>Backup (escalation)</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Oct 27 – Nov 02</td><td>Devon Park</td><td>Priya Banerjee</td><td>Sasha Lin</td></tr>
|
||||
<tr><td>Nov 03 – Nov 09</td><td>Caleb Renner</td><td>Devon Park</td><td>Sasha Lin</td></tr>
|
||||
<tr><td>Nov 10 – Nov 16</td><td>Priya Banerjee</td><td>Caleb Renner</td><td>Mira Reddy</td></tr>
|
||||
<tr><td>Nov 17 – Nov 23</td><td>Sasha Lin</td><td>Priya Banerjee</td><td>Mira Reddy</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="index">05</span>Incident response — first 30 minutes</h2>
|
||||
<div class="checklist">
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div><h4>Acknowledge the page within 5 min.</h4><p>Type <code>/ack</code> in <code>#incidents-auth</code>. The bot stops re-paging and tags the on-call.</p></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div><h4>Open the incident channel.</h4><p>Run <code>/incident open auth-service "<short title>"</code>. Slack bot creates a dedicated channel and pages the secondary.</p></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div><h4>Post a status snapshot.</h4><p>Customer-impact in one line, what you know, what you're checking next. Re-post every 10 minutes.</p></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">4</div>
|
||||
<div><h4>Mitigate before you diagnose.</h4><p>If a recent deploy is suspect, roll back. If KMS is degraded, fail open is <em>never</em> the answer for auth — escalate to security.</p></div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">5</div>
|
||||
<div><h4>Hand off or stand down.</h4><p>If you can't resolve in 30 min, hand to the secondary. When healthy, close with <code>/incident close</code>; postmortem is owed within 5 business days.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>Northwind Identity Platform · runbook v3.2</span>
|
||||
<span>Source: ops-docs/auth-service.md</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: finance-report
|
||||
description: |
|
||||
Quarterly / monthly financial report — masthead with KPIs, revenue and
|
||||
burn charts, P&L summary table, top-line highlights, and an outlook
|
||||
paragraph. Use when the brief mentions "financial report", "Q3 report",
|
||||
"MRR review", "P&L", or "财报".
|
||||
triggers:
|
||||
- "financial report"
|
||||
- "finance report"
|
||||
- "quarterly report"
|
||||
- "p&l"
|
||||
- "mrr review"
|
||||
- "财报"
|
||||
- "财务报告"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: finance
|
||||
featured: 10
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
example_prompt: "Build me a Q3 financial report for an early-stage SaaS — MRR, burn, gross margin, top accounts."
|
||||
---
|
||||
|
||||
# Finance Report Skill
|
||||
|
||||
Produce a single-screen financial report in one self-contained HTML file.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md.** Tables, KPI cards, and chart strokes use
|
||||
palette tokens — never invent new ones.
|
||||
2. **Classify** the period (monthly / quarterly / yearly) and entity
|
||||
(startup, division, project) from the brief. If unspecified, assume a
|
||||
quarterly SaaS report and pick believable numbers.
|
||||
3. **Layout** the page in this order:
|
||||
- Masthead: company / period / "Confidential — Finance" badge.
|
||||
- Headline KPI strip (4 cards): Revenue, Net new MRR, Gross margin, Cash runway.
|
||||
- Revenue trend chart (inline SVG line + area).
|
||||
- Cost breakdown chart (inline SVG bar) with a 2–3 bullet caption.
|
||||
- P&L summary table (Revenue / Gross profit / Opex / Net) with current vs prior period.
|
||||
- Top accounts table with logo placeholders, plan, ARR, status badge.
|
||||
- Outlook paragraph + footer with author + signature line.
|
||||
4. **Write** one self-contained HTML doc (CSS in one inline `<style>` block).
|
||||
5. **Self-check**: every number ties to a labelled chart or table; deltas
|
||||
show direction and percentage; accent colour used at most twice.
|
||||
|
||||
## Output contract
|
||||
|
||||
```
|
||||
<artifact identifier="finance-report-q3" type="text/html" title="Q3 Finance Report">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
@@ -0,0 +1,242 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Northwind — Q3 Financial Report</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f7f6f2;
|
||||
--paper: #ffffff;
|
||||
--ink: #11141a;
|
||||
--muted: #5f6573;
|
||||
--line: #e6e3dd;
|
||||
--line-strong: #c8c2b6;
|
||||
--accent: #1f6e8c;
|
||||
--accent-soft: #e7f0f4;
|
||||
--positive: #1f8c5c;
|
||||
--negative: #b13b3b;
|
||||
--display: 'Iowan Old Style', 'Charter', 'Iowan', Georgia, serif;
|
||||
--body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: var(--body);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.page {
|
||||
max-width: 980px;
|
||||
margin: 32px auto;
|
||||
padding: 56px 64px;
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 24px 60px rgba(28,27,26,0.06);
|
||||
}
|
||||
header.masthead { display: flex; justify-content: space-between; align-items: flex-end; padding-bottom: 18px; border-bottom: 2px solid var(--ink); margin-bottom: 28px; }
|
||||
.mast-left { display: flex; flex-direction: column; gap: 6px; }
|
||||
.mast-co { font-family: var(--display); font-size: 32px; letter-spacing: -0.01em; font-weight: 700; }
|
||||
.mast-meta { font-family: var(--mono); font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
.mast-badge {
|
||||
font-family: var(--mono); font-size: 11px; padding: 5px 10px; border-radius: 4px;
|
||||
border: 1px solid var(--ink); color: var(--ink); text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
h2 { font-family: var(--display); font-size: 19px; margin: 36px 0 14px; letter-spacing: -0.005em; font-weight: 700; }
|
||||
h2 .accent { color: var(--accent); }
|
||||
.lede { color: var(--muted); max-width: 64ch; }
|
||||
|
||||
/* KPI strip */
|
||||
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 8px 0 28px; }
|
||||
.kpi { padding: 16px 18px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; }
|
||||
.kpi .label { font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
|
||||
.kpi .value { font-family: var(--display); font-size: 28px; font-weight: 700; margin-top: 6px; line-height: 1; letter-spacing: -0.01em; }
|
||||
.kpi .delta { font-family: var(--mono); font-size: 11.5px; margin-top: 6px; }
|
||||
.delta.up { color: var(--positive); }
|
||||
.delta.down { color: var(--negative); }
|
||||
.delta.flat { color: var(--muted); }
|
||||
|
||||
/* Charts */
|
||||
.chart-row { display: grid; grid-template-columns: 1.6fr 1fr; gap: 14px; }
|
||||
.card { padding: 18px 20px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; }
|
||||
.card h3 { margin: 0 0 4px; font-size: 14px; font-weight: 600; }
|
||||
.card .sub { font-size: 12px; color: var(--muted); }
|
||||
.chart svg { width: 100%; height: 200px; display: block; margin-top: 8px; }
|
||||
.legend { display: flex; gap: 14px; font-size: 11.5px; color: var(--muted); margin-top: 6px; }
|
||||
.legend .swatch { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }
|
||||
.legend .a { background: var(--accent); }
|
||||
.legend .b { background: var(--ink); opacity: 0.6; }
|
||||
|
||||
/* Bars */
|
||||
.bars { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
|
||||
.bar-row { display: grid; grid-template-columns: 110px 1fr 60px; gap: 10px; align-items: center; font-size: 12.5px; }
|
||||
.bar-row .label { color: var(--muted); }
|
||||
.bar-track { background: var(--accent-soft); border-radius: 4px; height: 10px; position: relative; overflow: hidden; }
|
||||
.bar-fill { background: var(--accent); height: 100%; border-radius: 4px; }
|
||||
.bar-value { font-family: var(--mono); font-size: 11.5px; text-align: right; color: var(--ink); }
|
||||
|
||||
/* Tables */
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 6px; }
|
||||
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); font-size: 13px; vertical-align: middle; }
|
||||
th { font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); border-bottom: 1px solid var(--line-strong); }
|
||||
td.num, th.num { text-align: right; font-family: var(--mono); }
|
||||
tr.total td { font-weight: 700; border-top: 2px solid var(--ink); border-bottom: none; padding-top: 14px; }
|
||||
.badge { display: inline-block; padding: 2px 8px; font-size: 11px; border-radius: 999px; font-weight: 500; }
|
||||
.badge.green { background: #e7f4ee; color: var(--positive); }
|
||||
.badge.amber { background: #fbf0d6; color: #8a6912; }
|
||||
.badge.red { background: #f7e1e1; color: var(--negative); }
|
||||
.logo { display: inline-flex; width: 22px; height: 22px; border-radius: 6px; background: linear-gradient(135deg, var(--accent), #2c98c5); margin-right: 10px; vertical-align: middle; }
|
||||
|
||||
/* Outlook */
|
||||
.outlook { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 12px; }
|
||||
.outlook .quote { padding: 18px; background: var(--accent-soft); border-left: 3px solid var(--accent); border-radius: 6px; font-family: var(--display); font-size: 16px; line-height: 1.5; }
|
||||
.outlook .signoff { font-size: 13px; color: var(--muted); }
|
||||
.outlook .signoff strong { color: var(--ink); display: block; font-family: var(--display); font-size: 16px; margin-bottom: 2px; }
|
||||
footer { margin-top: 40px; padding-top: 18px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; font-family: var(--mono); font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page { padding: 32px 24px; margin: 0; border-radius: 0; }
|
||||
.kpis { grid-template-columns: 1fr 1fr; }
|
||||
.chart-row { grid-template-columns: 1fr; }
|
||||
.outlook { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<header class="masthead">
|
||||
<div class="mast-left">
|
||||
<div class="mast-meta">Northwind Trading · Q3 FY25</div>
|
||||
<div class="mast-co">Quarterly Financial Report</div>
|
||||
<div class="mast-meta">Prepared by Finance · Issued 14 October 2025</div>
|
||||
</div>
|
||||
<div class="mast-badge">Confidential</div>
|
||||
</header>
|
||||
|
||||
<p class="lede">Q3 closed ahead of plan on revenue and gross margin, with cash runway extending to 27 months on the back of a leaner cost base. Mid-market and enterprise both expanded; SMB churn remains the watch item heading into Q4.</p>
|
||||
|
||||
<h2>Headline KPIs</h2>
|
||||
<div class="kpis">
|
||||
<div class="kpi">
|
||||
<div class="label">Revenue</div>
|
||||
<div class="value">$8.42M</div>
|
||||
<div class="delta up">▲ 14.6% QoQ</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="label">Net new MRR</div>
|
||||
<div class="value">$184k</div>
|
||||
<div class="delta up">▲ 22.0% QoQ</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="label">Gross margin</div>
|
||||
<div class="value">82%</div>
|
||||
<div class="delta up">▲ 3 pp YoY</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="label">Cash runway</div>
|
||||
<div class="value">27 mo</div>
|
||||
<div class="delta up">▲ 4 mo QoQ</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Revenue & costs</h2>
|
||||
<div class="chart-row">
|
||||
<div class="card">
|
||||
<h3>Revenue · trailing 12 months</h3>
|
||||
<div class="sub">USD millions, monthly</div>
|
||||
<div class="chart">
|
||||
<svg viewBox="0 0 720 200" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id="lg" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="var(--accent)" stop-opacity="0.32"/>
|
||||
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon fill="url(#lg)" points="20,180 20,150 80,140 140,128 200,118 260,110 320,98 380,92 440,80 500,72 560,60 620,52 680,40 700,40 700,180" />
|
||||
<polyline fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"
|
||||
points="20,150 80,140 140,128 200,118 260,110 320,98 380,92 440,80 500,72 560,60 620,52 680,40" />
|
||||
<polyline fill="none" stroke="#11141a" stroke-opacity="0.45" stroke-width="1.5" stroke-dasharray="3 3"
|
||||
points="20,165 80,158 140,150 200,142 260,134 320,128 380,122 440,116 500,108 560,102 620,96 680,90" />
|
||||
<circle cx="680" cy="40" r="3.5" fill="var(--accent)"/>
|
||||
</svg>
|
||||
<div class="legend">
|
||||
<span><span class="swatch a"></span>Revenue</span>
|
||||
<span><span class="swatch b"></span>Plan</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Operating costs</h3>
|
||||
<div class="sub">USD thousands, Q3</div>
|
||||
<div class="bars">
|
||||
<div class="bar-row"><span class="label">R&D</span><div class="bar-track"><div class="bar-fill" style="width: 78%"></div></div><span class="bar-value">$1.42M</span></div>
|
||||
<div class="bar-row"><span class="label">Sales & GTM</span><div class="bar-track"><div class="bar-fill" style="width: 60%"></div></div><span class="bar-value">$1.10M</span></div>
|
||||
<div class="bar-row"><span class="label">G&A</span><div class="bar-track"><div class="bar-fill" style="width: 36%"></div></div><span class="bar-value">$660k</span></div>
|
||||
<div class="bar-row"><span class="label">Marketing</span><div class="bar-track"><div class="bar-fill" style="width: 28%"></div></div><span class="bar-value">$510k</span></div>
|
||||
<div class="bar-row"><span class="label">Infrastructure</span><div class="bar-track"><div class="bar-fill" style="width: 18%"></div></div><span class="bar-value">$330k</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>P&L summary</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Line item</th>
|
||||
<th class="num">Q3 FY25</th>
|
||||
<th class="num">Q2 FY25</th>
|
||||
<th class="num">Δ QoQ</th>
|
||||
<th class="num">Q3 FY24</th>
|
||||
<th class="num">Δ YoY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Revenue</td><td class="num">$8.42M</td><td class="num">$7.34M</td><td class="num" style="color: var(--positive);">+14.6%</td><td class="num">$5.92M</td><td class="num" style="color: var(--positive);">+42.2%</td></tr>
|
||||
<tr><td>Cost of revenue</td><td class="num">($1.51M)</td><td class="num">($1.46M)</td><td class="num" style="color: var(--negative);">+3.4%</td><td class="num">($1.18M)</td><td class="num" style="color: var(--negative);">+28.0%</td></tr>
|
||||
<tr><td>Gross profit</td><td class="num">$6.91M</td><td class="num">$5.88M</td><td class="num" style="color: var(--positive);">+17.5%</td><td class="num">$4.74M</td><td class="num" style="color: var(--positive);">+45.8%</td></tr>
|
||||
<tr><td>Operating expenses</td><td class="num">($4.02M)</td><td class="num">($4.18M)</td><td class="num" style="color: var(--positive);">−3.8%</td><td class="num">($3.66M)</td><td class="num" style="color: var(--negative);">+9.8%</td></tr>
|
||||
<tr class="total"><td>Operating income</td><td class="num">$2.89M</td><td class="num">$1.70M</td><td class="num" style="color: var(--positive);">+70.0%</td><td class="num">$1.08M</td><td class="num" style="color: var(--positive);">+167.5%</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Top accounts</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>Plan</th>
|
||||
<th>Region</th>
|
||||
<th class="num">ARR</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><span class="logo"></span>Pioneer Robotics</td><td>Enterprise</td><td>EMEA</td><td class="num">$612k</td><td><span class="badge green">Renewed</span></td></tr>
|
||||
<tr><td><span class="logo"></span>Atlas Cooperative</td><td>Enterprise</td><td>APAC</td><td class="num">$486k</td><td><span class="badge green">Expanded</span></td></tr>
|
||||
<tr><td><span class="logo"></span>Foundry Group</td><td>Team Plus</td><td>NA</td><td class="num">$320k</td><td><span class="badge amber">In renewal</span></td></tr>
|
||||
<tr><td><span class="logo"></span>Voltage Co.</td><td>Enterprise</td><td>NA</td><td class="num">$298k</td><td><span class="badge green">Renewed</span></td></tr>
|
||||
<tr><td><span class="logo"></span>Lattice Health</td><td>Team Plus</td><td>EMEA</td><td class="num">$214k</td><td><span class="badge red">At risk</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Outlook · Q4</h2>
|
||||
<div class="outlook">
|
||||
<div class="quote">"We're entering Q4 with the strongest pipeline coverage of the year — 3.4× plan — and the operating leverage to convert it without expanding the cost base."</div>
|
||||
<div class="signoff">
|
||||
<strong>Mira Okafor, CFO</strong>
|
||||
We expect revenue of $9.1–9.4M, net new MRR of $200–220k, and gross margin holding above 80%. The two open items are SMB churn (we'll publish a recovery plan with the November update) and the EMEA infra migration, which moves to GA in mid-November.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span>Northwind Trading · Q3 FY25 · Internal use only</span>
|
||||
<span>Page 1 of 1</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: gamified-app
|
||||
description: |
|
||||
A multi-frame gamified mobile-app prototype — three phone frames on a dark
|
||||
showcase stage. Frame 1: cover / poster, Frame 2: today's quests with XP
|
||||
ribbons and a level bar, Frame 3: quest detail. Vivid quest tiles, level
|
||||
ribbon, bottom tab bar. Use when the brief asks for a "gamified app",
|
||||
"habit tracker", "RPG-style life app", "level-up app", "daily quests",
|
||||
"XP / streak app", or "ELI5-style explainer app".
|
||||
triggers:
|
||||
- "gamified app"
|
||||
- "habit tracker"
|
||||
- "rpg app"
|
||||
- "level up app"
|
||||
- "daily quests"
|
||||
- "xp app"
|
||||
- "streak app"
|
||||
- "life management app"
|
||||
- "游戏化"
|
||||
- "习惯打卡"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: mobile
|
||||
scenario: personal
|
||||
featured: 12
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
example_prompt: "Design a gamified life-management app — multi-screen mobile prototype: cover poster, today's quests with XP, and a quest detail. ‘Daily quests for becoming a better human.’"
|
||||
---
|
||||
|
||||
# Gamified App Skill
|
||||
|
||||
Produce a multi-screen mobile prototype on a single dark showcase page.
|
||||
Three phone frames side-by-side, each one its own moment in the journey.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the active DESIGN.md** (injected above). For gamified apps, lean
|
||||
on bold display type for headlines and a brighter, broader palette than
|
||||
most products — quests look like quests because the colors do.
|
||||
2. **Pick the brand + value prop** from the brief. Generate real quest
|
||||
names (e.g. "Body — 20-min strength: pushups & planks", "Read — Four
|
||||
Thousand Weeks", "Listen — Huberman Lab · Sleep Architecture",
|
||||
"Nourish — Cook a high-protein lunch", "Mind — 10-min focus
|
||||
meditation", "Watch — The Bear · S3 E4").
|
||||
3. **Stage** — full-bleed dark page (near-black `#0e0d0c` or DS dark token)
|
||||
with a soft top spotlight gradient. Above the phones, a small caption
|
||||
row: "HI-FI PROTOTYPE · IPHONE" left, brand wordmark right, both in mono.
|
||||
4. **Phones** — three 360×780 phone frames in a horizontal row (wraps to
|
||||
stack on narrow viewports). Each phone:
|
||||
- 12px black bezel, 44px corner radius, dynamic-island notch.
|
||||
- Status bar (time / signal / battery).
|
||||
- Phone-specific content (below).
|
||||
- Bottom tab bar with 5 icons (Today, Library, Stats, ⊕ central CTA,
|
||||
Profile). Active tab in accent.
|
||||
5. **Phone 1 — cover poster (sales/value prop)**:
|
||||
- Status bar.
|
||||
- HI-FI PROTOTYPE · IPHONE eyebrow.
|
||||
- Big display headline ("Daily quests for becoming a better human."),
|
||||
accent on "becoming".
|
||||
- 1–2 sentence body in muted serif/sans.
|
||||
- Mono tip line ("Tap quests to open detail. Toggle [theme] in the
|
||||
toolbar to switch theme & layout.")
|
||||
- Subtle scrolling teaser of the next screen at the bottom edge.
|
||||
6. **Phone 2 — today's quests dashboard** (the hero screen):
|
||||
- Greeting "Good morning, Sam" + small XP-bell ringing.
|
||||
- Level ribbon — "LV 14 · Level 14 · 1648 / 2480 XP" with a progress
|
||||
bar inside a glassmorphic ribbon.
|
||||
- Sub-line: "8 quests waiting · earn 430 XP today".
|
||||
- 3×2 grid of quest tiles. Each tile: rounded corner, pastel accent
|
||||
color, glyph chip in top-left, title, mini-meta line, "+NN XP" pill
|
||||
in bottom-right.
|
||||
- Bottom tab bar.
|
||||
7. **Phone 3 — quest detail**:
|
||||
- Back arrow + screen title ("Quest").
|
||||
- Hero block with the quest's accent color, big serif quest title
|
||||
("Body — strength"), short narrative body, "REWARD +90 XP" stamp.
|
||||
- Steps checklist (3–4 micro-tasks, one done, two pending).
|
||||
- Big primary CTA "Start quest" pill at the bottom in accent.
|
||||
8. **Write** a single HTML document:
|
||||
- `<!doctype html>` through `</html>`, CSS inline.
|
||||
- All in CSS — no images. Use `linear-gradient` and inline SVG glyphs
|
||||
for tile chips and tab icons.
|
||||
- `data-od-id` on stage, each phone, each frame's regions.
|
||||
9. **Self-check**:
|
||||
- Three frames, each with a distinct purpose. Not three copies of the
|
||||
same screen.
|
||||
- Tile colors don't overpower — each quest tile uses a different pastel
|
||||
against the same neutral surface.
|
||||
- Reads as gamified and adult — playful, not childish.
|
||||
|
||||
## Output contract
|
||||
|
||||
Emit between `<artifact>` tags:
|
||||
|
||||
```
|
||||
<artifact identifier="game-slug" type="text/html" title="Mobile — App Name">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact, nothing after.
|
||||
@@ -0,0 +1,292 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Level — daily quests for becoming a better human</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--stage: #0e0d0c;
|
||||
--stage-2: #1a1714;
|
||||
--paper: #ffffff;
|
||||
--ink: #1a1714;
|
||||
--muted: #6c6660;
|
||||
--line: #ebe6dd;
|
||||
--accent: #e98425;
|
||||
--accent-2: #ff6b3d;
|
||||
--tile-1: #ffe9bf;
|
||||
--tile-2: #ffe1d9;
|
||||
--tile-3: #f3e6ff;
|
||||
--tile-4: #d2eecb;
|
||||
--tile-5: #d6e7ff;
|
||||
--tile-6: #ffd6f1;
|
||||
--serif: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
|
||||
--sans: 'Inter', -apple-system, system-ui, sans-serif;
|
||||
--mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 50% -10%, rgba(233,132,37,0.18), transparent 70%),
|
||||
radial-gradient(ellipse 70% 50% at 50% 110%, rgba(255,255,255,0.04), transparent 70%),
|
||||
var(--stage);
|
||||
color: #f5efe4;
|
||||
font: 14px/1.5 var(--sans);
|
||||
}
|
||||
|
||||
.stage-bar {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 24px 36px;
|
||||
font: 11px/1 var(--mono);
|
||||
color: rgba(245,239,228,0.5);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.stage-bar .word { font-family: var(--serif); font-style: italic; font-size: 22px; color: #f5efe4; letter-spacing: 0; text-transform: none; }
|
||||
|
||||
.phones {
|
||||
display: flex; gap: 28px; justify-content: center; padding: 12px 32px 56px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.phone {
|
||||
width: 360px; height: 760px;
|
||||
background: #050403;
|
||||
border-radius: 56px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 30px 60px rgba(0,0,0,0.45), inset 0 0 0 2px rgba(255,255,255,0.04);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.phone::before {
|
||||
content: '';
|
||||
position: absolute; top: 22px; left: 50%; transform: translateX(-50%);
|
||||
width: 116px; height: 30px; background: #050403; border-radius: 999px; z-index: 5;
|
||||
}
|
||||
.screen { width: 100%; height: 100%; background: var(--paper); border-radius: 44px; overflow: hidden; display: flex; flex-direction: column; color: var(--ink); }
|
||||
.status { display: flex; justify-content: space-between; align-items: center; padding: 14px 26px 6px; font: 600 14px/1 var(--sans); }
|
||||
.status .right { display: flex; gap: 6px; align-items: center; font-size: 12px; }
|
||||
|
||||
/* Phone 1 — cover */
|
||||
.cover { background: var(--ink); color: #fef9ee; height: 100%; display: flex; flex-direction: column; }
|
||||
.cover .status { color: #fef9ee; }
|
||||
.cover .body { flex: 1; padding: 40px 28px 0; display: flex; flex-direction: column; }
|
||||
.cover .eyebrow { display: inline-flex; align-items: center; gap: 6px; font: 10.5px/1 var(--mono); letter-spacing: 0.18em; color: rgba(254,249,238,0.6); padding: 6px 9px; border: 1px solid rgba(254,249,238,0.22); border-radius: 999px; align-self: flex-start; margin-bottom: 26px; }
|
||||
.cover .eyebrow .dot { width: 6px; height: 6px; background: var(--accent); border-radius: 50%; }
|
||||
.cover h1 { font: italic 800 56px/1 var(--serif); margin: 0 0 16px; letter-spacing: -0.005em; max-width: 12ch; }
|
||||
.cover h1 .accent { color: var(--accent); font-style: italic; }
|
||||
.cover p.lede { color: rgba(254,249,238,0.62); font-size: 14.5px; line-height: 1.55; margin: 0 0 18px; }
|
||||
.cover .tip { font: 11px/1.5 var(--mono); color: rgba(254,249,238,0.4); border-top: 1px dashed rgba(254,249,238,0.2); padding-top: 12px; }
|
||||
.cover .tip b { color: rgba(254,249,238,0.7); font-weight: 500; }
|
||||
.cover .next-peek { margin-top: auto; height: 92px; background: #211d18; border-top-left-radius: 26px; border-top-right-radius: 26px; padding: 14px 22px; display: flex; align-items: center; gap: 10px; color: rgba(254,249,238,0.6); font: 11px/1.4 var(--mono); letter-spacing: 0.16em; text-transform: uppercase; }
|
||||
.cover .next-peek .swatch { width: 36px; height: 36px; border-radius: 8px; background: var(--accent); flex-shrink: 0; }
|
||||
|
||||
/* Phone 2 — quests dashboard */
|
||||
.home { display: flex; flex-direction: column; height: 100%; padding: 0; }
|
||||
.home .head { padding: 14px 22px 6px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.home .head h2 { margin: 0; font: 700 18px/1.2 var(--sans); letter-spacing: -0.005em; }
|
||||
.home .head .bell { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,107,61,0.10); color: var(--accent-2); display: grid; place-items: center; font: 700 11px/1 var(--sans); }
|
||||
.level-ribbon {
|
||||
margin: 8px 14px 12px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(135deg, #1a1714 0%, #2b251f 100%);
|
||||
color: #f5efe4; border-radius: 16px;
|
||||
display: grid; grid-template-columns: 38px 1fr auto; gap: 12px; align-items: center;
|
||||
}
|
||||
.level-ribbon .lv { width: 38px; height: 38px; border-radius: 12px; background: var(--accent); display: grid; place-items: center; font: 700 14px/1 var(--mono); color: #1a1714; }
|
||||
.level-ribbon .meta .label { font: 10px/1 var(--mono); letter-spacing: 0.16em; color: rgba(245,239,228,0.5); text-transform: uppercase; }
|
||||
.level-ribbon .meta .name { font: 700 14px/1.2 var(--sans); margin-top: 4px; }
|
||||
.level-ribbon .xp { font: 600 12px/1 var(--mono); color: rgba(245,239,228,0.7); }
|
||||
.level-ribbon .bar { grid-column: 1 / -1; height: 6px; background: rgba(245,239,228,0.10); border-radius: 999px; overflow: hidden; margin-top: 8px; }
|
||||
.level-ribbon .bar > span { display: block; width: 66%; height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-2)); }
|
||||
|
||||
.home .sub { padding: 0 22px 10px; font: 12.5px/1.4 var(--sans); color: var(--muted); display: flex; align-items: center; gap: 8px; }
|
||||
.home .sub .pill { font: 10.5px/1 var(--mono); padding: 4px 8px; border-radius: 999px; background: var(--ink); color: #f5efe4; letter-spacing: 0.06em; }
|
||||
|
||||
.quests { padding: 4px 14px 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; overflow: hidden; }
|
||||
.q { border-radius: 18px; padding: 12px; min-height: 110px; position: relative; display: flex; flex-direction: column; gap: 6px; }
|
||||
.q .glyph { width: 28px; height: 28px; border-radius: 8px; background: rgba(0,0,0,0.10); color: var(--ink); display: grid; place-items: center; font: 700 13px/1 var(--sans); }
|
||||
.q .title { font: 700 13.5px/1.3 var(--sans); color: var(--ink); margin: 0; }
|
||||
.q .meta { font: 11px/1.4 var(--sans); color: var(--ink); opacity: 0.7; }
|
||||
.q .xp { position: absolute; bottom: 10px; right: 10px; font: 700 11px/1 var(--mono); padding: 4px 7px; border-radius: 999px; background: var(--ink); color: #f5efe4; letter-spacing: 0.06em; }
|
||||
.q.q1 { background: var(--tile-2); }
|
||||
.q.q1 .glyph { background: #ff7a52; color: white; }
|
||||
.q.q2 { background: var(--tile-1); }
|
||||
.q.q2 .glyph { background: #f0b54a; color: white; }
|
||||
.q.q3 { background: var(--tile-3); }
|
||||
.q.q3 .glyph { background: #b08bf2; color: white; }
|
||||
.q.q4 { background: var(--tile-4); }
|
||||
.q.q4 .glyph { background: #6cba5b; color: white; }
|
||||
.q.q5 { background: var(--tile-6); }
|
||||
.q.q5 .glyph { background: #e76aae; color: white; }
|
||||
.q.q6 { background: var(--tile-5); }
|
||||
.q.q6 .glyph { background: #4a86e9; color: white; }
|
||||
|
||||
/* Phone 3 — quest detail */
|
||||
.detail { display: flex; flex-direction: column; height: 100%; }
|
||||
.detail .topbar { display: flex; align-items: center; gap: 10px; padding: 8px 22px 6px; font: 13px/1 var(--sans); color: var(--muted); }
|
||||
.detail .topbar .back { width: 28px; height: 28px; border-radius: 50%; background: var(--line); display: grid; place-items: center; }
|
||||
.hero { margin: 8px 14px 14px; padding: 22px 20px 24px; border-radius: 24px; background: linear-gradient(160deg, #ffd2bb 0%, #ff7a52 100%); color: var(--ink); position: relative; overflow: hidden; }
|
||||
.hero .badge { display: inline-flex; align-items: center; gap: 6px; font: 10.5px/1 var(--mono); padding: 5px 8px; border-radius: 999px; background: rgba(0,0,0,0.10); letter-spacing: 0.16em; text-transform: uppercase; }
|
||||
.hero h2 { font: italic 700 30px/1.05 var(--serif); margin: 12px 0 6px; max-width: 12ch; }
|
||||
.hero p { font: 14px/1.5 var(--sans); color: rgba(26,23,20,0.75); margin: 0; max-width: 30ch; }
|
||||
.hero .stamp { position: absolute; right: 18px; top: 18px; font: 700 11px/1 var(--mono); padding: 6px 8px; background: rgba(255,255,255,0.7); border-radius: 999px; color: var(--ink); letter-spacing: 0.08em; }
|
||||
|
||||
.steps { padding: 4px 22px 12px; }
|
||||
.steps h3 { font: 700 11px/1 var(--mono); letter-spacing: 0.18em; color: var(--muted); margin: 12px 0 8px; text-transform: uppercase; }
|
||||
.step { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-top: 1px solid var(--line); }
|
||||
.step .check { width: 22px; height: 22px; border-radius: 50%; border: 1.5px solid var(--line); flex-shrink: 0; display: grid; place-items: center; font: 700 11px/1 var(--sans); color: white; }
|
||||
.step.done .check { background: var(--accent); border-color: var(--accent); }
|
||||
.step.done .check::after { content: '✓'; }
|
||||
.step.done .name { color: var(--muted); text-decoration: line-through; }
|
||||
.step .name { font: 14px/1.3 var(--sans); }
|
||||
.step .meta { font: 11px/1 var(--mono); color: var(--muted); margin-left: auto; letter-spacing: 0.06em; }
|
||||
|
||||
.detail .start {
|
||||
margin: auto 18px 12px; padding: 14px; border-radius: 999px;
|
||||
background: var(--ink); color: #f5efe4; text-align: center;
|
||||
font: 600 14px/1 var(--sans); letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
/* Tab bar shared */
|
||||
.tabbar {
|
||||
margin-top: auto; display: grid; grid-template-columns: repeat(5, 1fr);
|
||||
padding: 10px 14px 26px; border-top: 1px solid var(--line); background: var(--paper);
|
||||
}
|
||||
.tab { display: flex; flex-direction: column; align-items: center; gap: 4px; font: 9.5px/1 var(--mono); color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.tab .icon { width: 22px; height: 22px; border-radius: 6px; background: var(--line); }
|
||||
.tab.active { color: var(--accent); }
|
||||
.tab.active .icon { background: var(--accent); }
|
||||
.tab.center .icon { background: var(--ink); color: #f5efe4; display: grid; place-items: center; font: 700 16px/1 var(--sans); border-radius: 50%; }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.phones { gap: 18px; }
|
||||
.phone { width: 320px; height: 700px; }
|
||||
.cover h1 { font-size: 46px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage-bar" data-od-id="stage-bar">
|
||||
<span>HI-FI PROTOTYPE · IPHONE</span>
|
||||
<span class="word">level<span style="color:var(--accent);">.</span></span>
|
||||
<span>3 SCREENS · LIGHT MODE</span>
|
||||
</div>
|
||||
|
||||
<div class="phones" data-od-id="phones">
|
||||
|
||||
<!-- Phone 1 — cover -->
|
||||
<div class="phone" data-od-id="phone-cover">
|
||||
<div class="screen cover">
|
||||
<div class="status"><span>9:41</span><span class="right">·· 5G · 100%</span></div>
|
||||
<div class="body">
|
||||
<span class="eyebrow"><span class="dot"></span>HI-FI PROTOTYPE · IPHONE</span>
|
||||
<h1>Daily quests for <span class="accent">becoming</span> a better human.</h1>
|
||||
<p class="lede">Level turns the things you already know you should do — exercise, read, reflect, call a friend — into a daily quest log. Finish them, earn XP, watch your classes level up.</p>
|
||||
<p class="tip">Tap quests to open detail. Complete the 6th quest to trigger the level-up moment. Toggle <b>[theme]</b> in the toolbar to switch theme & layout.</p>
|
||||
<div class="next-peek"><div class="swatch"></div>NEXT — TODAY'S QUESTS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone 2 — quests dashboard -->
|
||||
<div class="phone" data-od-id="phone-home">
|
||||
<div class="screen home">
|
||||
<div class="status"><span>9:41</span><span class="right">·· 5G · 100%</span></div>
|
||||
<div class="head">
|
||||
<h2>Good morning, Sam</h2>
|
||||
<div class="bell">×3</div>
|
||||
</div>
|
||||
<div class="level-ribbon" data-od-id="level-ribbon">
|
||||
<div class="lv">14</div>
|
||||
<div class="meta"><div class="label">LEVEL</div><div class="name">Level 14</div></div>
|
||||
<div class="xp">1648 / 2480</div>
|
||||
<div class="bar"><span></span></div>
|
||||
</div>
|
||||
<div class="sub">8 quests waiting · earn <span class="pill">430 XP</span> today</div>
|
||||
|
||||
<div class="quests" data-od-id="quests">
|
||||
<div class="q q1">
|
||||
<div class="glyph">B</div>
|
||||
<p class="title">Body</p>
|
||||
<div class="meta">20-min strength: pushups & planks</div>
|
||||
<span class="xp">+90</span>
|
||||
</div>
|
||||
<div class="q q2">
|
||||
<div class="glyph">R</div>
|
||||
<p class="title">Read</p>
|
||||
<div class="meta">Four Thousand Weeks</div>
|
||||
<span class="xp">+60</span>
|
||||
</div>
|
||||
<div class="q q3">
|
||||
<div class="glyph">L</div>
|
||||
<p class="title">Listen</p>
|
||||
<div class="meta">Huberman Lab — Sleep Architecture</div>
|
||||
<span class="xp">+50</span>
|
||||
</div>
|
||||
<div class="q q4">
|
||||
<div class="glyph">N</div>
|
||||
<p class="title">Nourish</p>
|
||||
<div class="meta">Cook a high-protein lunch</div>
|
||||
<span class="xp">+70</span>
|
||||
</div>
|
||||
<div class="q q5">
|
||||
<div class="glyph">M</div>
|
||||
<p class="title">Mind</p>
|
||||
<div class="meta">10-min focus meditation</div>
|
||||
<span class="xp">+40</span>
|
||||
</div>
|
||||
<div class="q q6">
|
||||
<div class="glyph">W</div>
|
||||
<p class="title">Watch</p>
|
||||
<div class="meta">The Bear · S3 E4</div>
|
||||
<span class="xp">+30</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabbar" data-od-id="tabbar-home">
|
||||
<div class="tab active"><div class="icon"></div>Today</div>
|
||||
<div class="tab"><div class="icon"></div>Library</div>
|
||||
<div class="tab center"><div class="icon">+</div> </div>
|
||||
<div class="tab"><div class="icon"></div>Stats</div>
|
||||
<div class="tab"><div class="icon"></div>Profile</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone 3 — detail -->
|
||||
<div class="phone" data-od-id="phone-detail">
|
||||
<div class="screen detail">
|
||||
<div class="status"><span>9:41</span><span class="right">·· 5G · 100%</span></div>
|
||||
<div class="topbar"><div class="back">←</div>QUEST · 03 / 08</div>
|
||||
<div class="hero">
|
||||
<span class="stamp">+90 XP</span>
|
||||
<span class="badge">— BODY · STRENGTH</span>
|
||||
<h2>20 minutes that change Wednesday.</h2>
|
||||
<p>A short, repeatable strength block — pushups, planks, and one wildcard. No equipment. Sam, you've finished this 11 times this month.</p>
|
||||
</div>
|
||||
<div class="steps" data-od-id="steps">
|
||||
<h3>Today's micro-tasks</h3>
|
||||
<div class="step done"><div class="check"></div><div class="name">Roll out the mat</div><div class="meta">+5 XP</div></div>
|
||||
<div class="step"><div class="check"></div><div class="name">3 × 12 pushups</div><div class="meta">+30 XP</div></div>
|
||||
<div class="step"><div class="check"></div><div class="name">3 × 45s plank</div><div class="meta">+30 XP</div></div>
|
||||
<div class="step"><div class="check"></div><div class="name">Wildcard: lunges</div><div class="meta">+25 XP</div></div>
|
||||
</div>
|
||||
<div class="start">Start quest</div>
|
||||
<div class="tabbar" data-od-id="tabbar-detail">
|
||||
<div class="tab active"><div class="icon"></div>Today</div>
|
||||
<div class="tab"><div class="icon"></div>Library</div>
|
||||
<div class="tab center"><div class="icon">+</div> </div>
|
||||
<div class="tab"><div class="icon"></div>Stats</div>
|
||||
<div class="tab"><div class="icon"></div>Profile</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 op7418 (歸藏)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Magazine Web PPT · Editorial-Style Web Slide Deck Skill
|
||||
|
||||
A [Claude Code / Claude Agent Skills](https://agentskills.io/) skill that generates **single-file HTML horizontal-swipe decks** with an "**editorial magazine × electronic ink**" aesthetic — picture *Monocle* with code stitched in.
|
||||
|
||||
> Distilled by [Guizang](https://x.com/op7418) from offline talks like "One-Person Company: Organizations Folded by AI" and "A New Way of Working." Every pitfall hit during those decks is logged in `checklist.md`.
|
||||
|
||||

|
||||
|
||||
## What you get
|
||||
|
||||
- 🖋 **Three-tier type system**: serif for headlines, sans-serif for body, mono for metadata
|
||||
- 🌊 **WebGL fluid / dispersion backgrounds** — visible on hero pages, restrained on body pages
|
||||
- 📐 **Horizontal swipe navigation**: ← → arrows / scroll wheel / touch swipe / bottom dots / ESC for index
|
||||
- 🎨 **5 curated theme presets**: Ink Classic / Indigo Porcelain / Forest Ink / Kraft Paper / Dune
|
||||
- 🧩 **10 page layouts**: cover, act divider, big numbers, lead image + text, image grid, pipeline, hero question, big quote, before/after, image + text mix
|
||||
- 📄 **Single HTML file** — no build, no server, open directly in the browser
|
||||
|
||||
## Fits / Doesn't fit
|
||||
|
||||
**✅ Fits**: offline talks, industry keynotes, private salons, AI product launches, demo day, presentations with strong personal voice
|
||||
|
||||
**❌ Doesn't fit**: data-heavy tables, training decks (density too low), multi-user collaborative editing (static HTML)
|
||||
|
||||
## Install
|
||||
|
||||
### Option 1: Paste this to an AI (recommended)
|
||||
|
||||
> Install the `guizang-ppt-skill` Claude Code skill for me. Steps:
|
||||
>
|
||||
> 1. Make sure `~/.claude/skills/` exists (create if not)
|
||||
> 2. Run `git clone https://github.com/op7418/guizang-ppt-skill.git ~/.claude/skills/magazine-web-ppt`
|
||||
> 3. Verify: `ls ~/.claude/skills/magazine-web-ppt/` should show `SKILL.md`, `assets/`, `references/`
|
||||
> 4. Tell me when done. Later, saying things like "make me a magazine-style deck" will trigger this skill.
|
||||
|
||||
Paste the block above into Claude Code / Cursor / any AI agent with shell access and it handles the install.
|
||||
|
||||
### Option 2: Manual CLI
|
||||
|
||||
```bash
|
||||
git clone https://github.com/op7418/guizang-ppt-skill.git ~/.claude/skills/magazine-web-ppt
|
||||
```
|
||||
|
||||
### How to trigger it
|
||||
|
||||
Once installed, Claude Code auto-detects the skill. Trigger phrases:
|
||||
|
||||
- "Make me a magazine-style deck"
|
||||
- "Generate a horizontal swipe deck"
|
||||
- "Editorial magazine style presentation"
|
||||
- "Electronic ink slides for my talk"
|
||||
|
||||
## Workflow
|
||||
|
||||
The skill is a structured 6-step flow; Claude walks you through each:
|
||||
|
||||
1. **Clarify intent** — 6-question checklist: audience, duration, source material, images, theme, hard constraints
|
||||
2. **Copy template** — `assets/template.html` → project folder, update `<title>`, swap theme vars
|
||||
3. **Fill content** — pick from 10 layout skeletons, paste, edit copy (with class-name pre-flight + theme rhythm plan)
|
||||
4. **Self-check** — match against `references/checklist.md`; P0 issues must all pass
|
||||
5. **Preview** — open the HTML in a browser
|
||||
6. **Iterate** — use inline styles to tune font size, height, spacing
|
||||
|
||||
Full spec in [`SKILL.md`](./SKILL.md).
|
||||
|
||||
## Directory
|
||||
|
||||
```
|
||||
magazine-web-ppt/
|
||||
├── SKILL.md ← main skill file: workflow, principles, common mistakes
|
||||
├── README.md ← Chinese README
|
||||
├── README.en.md ← this file
|
||||
├── assets/
|
||||
│ └── template.html ← runnable seed HTML (CSS + WebGL + swipe JS pre-wired)
|
||||
└── references/
|
||||
├── components.md ← component catalog (type, color, grid, icons, callout, stat, pipeline)
|
||||
├── layouts.md ← 10 layout skeletons (paste-ready)
|
||||
├── themes.md ← 5 theme presets (pick, don't customize)
|
||||
└── checklist.md ← quality checklist (P0 / P1 / P2 / P3 tiers)
|
||||
```
|
||||
|
||||
## Theme presets
|
||||
|
||||
Pick from `references/themes.md`. **Custom hex values are not allowed** — protecting the aesthetic matters more than freedom of choice.
|
||||
|
||||
| Theme | Best for |
|
||||
|------|---------|
|
||||
| 🖋 Ink Classic | general default, commercial launches, when in doubt |
|
||||
| 🌊 Indigo Porcelain | tech / research / AI / technical keynotes |
|
||||
| 🌿 Forest Ink | nature / sustainability / culture / non-fiction |
|
||||
| 🍂 Kraft Paper | nostalgic / humanist / literary / indie zines |
|
||||
| 🌙 Dune | art / design / creative / gallery |
|
||||
|
||||
Switching themes only requires replacing the 6 variables at the top of `template.html`'s `:root{}` block — all other CSS flows through `var(--...)`.
|
||||
|
||||
## Core design principles
|
||||
|
||||
1. **Restraint over flash** — WebGL backgrounds only bleed through on hero pages
|
||||
2. **Structure over decoration** — information hierarchy via type size + typeface + grid whitespace, not shadows or floating cards
|
||||
3. **Images are first-class citizens** — crop only from the bottom; top and sides stay intact
|
||||
4. **Rhythm lives on hero pages** — hero / non-hero alternation keeps the eye from fatiguing
|
||||
5. **Terms stay consistent** — Skills is Skills; no mix-and-match translations
|
||||
|
||||
## Visual references
|
||||
|
||||
- [*Monocle*](https://monocle.com) magazine layouts
|
||||
- YC Garry Tan — "Thin Harness, Fat Skills"
|
||||
- Guizang's offline talk deck series
|
||||
|
||||
## Contributing
|
||||
|
||||
Bugs, layout issues, new layout requests — Issues and PRs welcome. Prioritize:
|
||||
|
||||
- Add new classes to `template.html` first; don't let `layouts.md` reference undefined classes
|
||||
- Log pitfalls into `checklist.md` at the matching P0 / P1 / P2 / P3 tier
|
||||
- New theme colors go into `themes.md` with a recommended use case
|
||||
|
||||
## License
|
||||
|
||||
MIT © 2026 [op7418](https://github.com/op7418)
|
||||
@@ -0,0 +1,120 @@
|
||||
# Magazine Web PPT · 电子杂志风网页 PPT Skill
|
||||
|
||||
> 🌏 **English version: [README.en.md](./README.en.md)**
|
||||
|
||||
一个 [Claude Code / Claude Agent Skills](https://agentskills.io/) 技能,用于生成**单文件 HTML 横向翻页 PPT**,视觉基调是"**电子杂志 × 电子墨水**"——像 *Monocle* 贴上了代码的样子。
|
||||
|
||||
> 由 [歸藏](https://x.com/op7418) 在"一人公司:被 AI 折叠的组织"、"一种新的工作方式"等线下分享中沉淀而成,踩过的每一个坑都写进了 `checklist.md`。
|
||||
|
||||

|
||||
|
||||
## 效果
|
||||
|
||||
- 🖋 **衬线大标题 + 非衬线正文 + 等宽元数据**的三级字体分工
|
||||
- 🌊 **WebGL 流体/色散背景**,hero 页可见,正文页克制
|
||||
- 📐 **横向左右翻页**:键盘 ← → / 滚轮 / 触屏滑动 / 底部圆点 / ESC 索引
|
||||
- 🎨 **5 套主题色预设**:墨水经典 / 靛蓝瓷 / 森林墨 / 牛皮纸 / 沙丘
|
||||
- 🧩 **10 种页面布局**:开场封面、章节幕封、数据大字报、左文右图、图片网格、Pipeline、悬念问题、大引用、Before/After 对比、图文混排
|
||||
- 📄 **单文件 HTML**:不需要构建、不需要服务器,浏览器直接打开
|
||||
|
||||
## 适合 / 不适合
|
||||
|
||||
**✅ 合适**:线下分享 / 行业内部讲话 / 私享会 / AI 产品发布 / demo day / 带强烈个人风格的演讲
|
||||
|
||||
**❌ 不合适**:大段表格数据 / 培训课件(信息密度不够)/ 需要多人协作编辑(静态 HTML)
|
||||
|
||||
## 安装
|
||||
|
||||
### 方式一:把下面这段话直接发给 AI(推荐)
|
||||
|
||||
> 帮我安装 `guizang-ppt-skill` 这个 Claude Code skill。请按下面步骤做:
|
||||
>
|
||||
> 1. 确保 `~/.claude/skills/` 目录存在(不存在就创建)
|
||||
> 2. 执行 `git clone https://github.com/op7418/guizang-ppt-skill.git ~/.claude/skills/magazine-web-ppt`
|
||||
> 3. 验证:`ls ~/.claude/skills/magazine-web-ppt/` 应该看到 `SKILL.md`、`assets/`、`references/` 三项
|
||||
> 4. 告诉我安装好了,之后我说"做一份杂志风 PPT"之类的话就会触发这个 skill
|
||||
|
||||
把这段话复制粘贴给 Claude Code / Cursor / 任何有 shell 权限的 AI Agent,它会自动完成安装。
|
||||
|
||||
### 方式二:手动命令行
|
||||
|
||||
```bash
|
||||
git clone https://github.com/op7418/guizang-ppt-skill.git ~/.claude/skills/magazine-web-ppt
|
||||
```
|
||||
|
||||
### 触发方式
|
||||
|
||||
装好后,Claude Code 会在对话里自动发现并调用这个 skill。触发关键词:
|
||||
|
||||
- "帮我做一份杂志风 PPT"
|
||||
- "生成一个 horizontal swipe deck"
|
||||
- "editorial magazine style presentation"
|
||||
- "electronic ink 风格演讲 slides"
|
||||
|
||||
## 使用流程
|
||||
|
||||
Skill 本身是结构化的 6 步工作流,Claude 会逐步引导:
|
||||
|
||||
1. **需求澄清** — 6 问清单:受众、时长、素材、图片、主题色、硬约束
|
||||
2. **拷贝模板** — `assets/template.html` → 项目目录,改 `<title>`,换主题色
|
||||
3. **填充内容** — 从 10 种 layout 骨架里挑、粘、改文案(先做类名预检 + 主题节奏规划)
|
||||
4. **自检** — 对照 `references/checklist.md`,P0 级问题必须全过
|
||||
5. **预览** — 浏览器直接打开
|
||||
6. **迭代** — inline style 改字号/高度/间距
|
||||
|
||||
详细说明见 [`SKILL.md`](./SKILL.md)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
magazine-web-ppt/
|
||||
├── SKILL.md ← Skill 主文件:工作流、原则、常见错误
|
||||
├── README.md ← 本文件
|
||||
├── assets/
|
||||
│ └── template.html ← 完整可运行的种子 HTML(CSS + WebGL + 翻页 JS 全配好)
|
||||
└── references/
|
||||
├── components.md ← 组件手册(字体、色、网格、图标、callout、stat、pipeline)
|
||||
├── layouts.md ← 10 种页面布局骨架(可直接粘贴)
|
||||
├── themes.md ← 5 套主题色预设(只能选不能自定义)
|
||||
└── checklist.md ← 质量检查清单(P0 / P1 / P2 / P3 分级)
|
||||
```
|
||||
|
||||
## 主题色预设
|
||||
|
||||
从 `references/themes.md` 里选一套——**不允许自定义 hex 值**,保护美学比给自由更重要。
|
||||
|
||||
| 主题 | 适合场景 |
|
||||
|------|---------|
|
||||
| 🖋 墨水经典 | 通用默认、商业发布、不知道选啥 |
|
||||
| 🌊 靛蓝瓷 | 科技 / 研究 / AI / 技术发布会 |
|
||||
| 🌿 森林墨 | 自然 / 可持续 / 文化 / 非虚构 |
|
||||
| 🍂 牛皮纸 | 怀旧 / 人文 / 文学 / 独立杂志 |
|
||||
| 🌙 沙丘 | 艺术 / 设计 / 创意 / 画廊 |
|
||||
|
||||
切换主题只需替换 `template.html` 开头 `:root{}` 里的 6 行变量,其他 CSS 全走 `var(--...)`。
|
||||
|
||||
## 核心设计原则
|
||||
|
||||
1. **克制优于炫技** — WebGL 背景只在 hero 页透出
|
||||
2. **结构优于装饰** — 信息靠字号 + 字体对比 + 网格留白,不用阴影和浮动卡片
|
||||
3. **图片是第一公民** — 只裁底部,顶部和左右完整
|
||||
4. **节奏靠 hero 页** — hero / non-hero 交替,才不累眼睛
|
||||
5. **术语统一** — Skills 就是 Skills,不中英混译
|
||||
|
||||
## 视觉参考
|
||||
|
||||
- [*Monocle*](https://monocle.com) 杂志的版式
|
||||
- YC Garry Tan "Thin Harness, Fat Skills"
|
||||
- 歸藏线下分享 PPT 系列
|
||||
|
||||
## 贡献
|
||||
|
||||
Bug、排版问题、新布局需求——欢迎开 Issue 或 PR。改动请优先:
|
||||
|
||||
- 在 `template.html` 里补类,不要让 layouts.md 使用未定义的类
|
||||
- 把踩过的坑写到 `checklist.md` 对应的 P0 / P1 / P2 / P3 级别
|
||||
- 新主题色进 `themes.md` 并给出适合的场景
|
||||
|
||||
## License
|
||||
|
||||
MIT © 2026 [op7418](https://github.com/op7418)
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
name: magazine-web-ppt
|
||||
description: 生成"电子杂志 × 电子墨水"风格的横向翻页网页 PPT(单 HTML 文件),含 WebGL 流体背景、衬线标题 + 非衬线正文、章节幕封、数据大字报、图片网格等模板。当用户需要制作分享 / 演讲 / 发布会风格的网页 PPT,或提到"杂志风 PPT"、"horizontal swipe deck"、"editorial magazine"、"e-ink presentation"时使用。
|
||||
triggers:
|
||||
- "ppt"
|
||||
- "deck"
|
||||
- "slides"
|
||||
- "presentation"
|
||||
- "magazine"
|
||||
- "杂志"
|
||||
- "杂志风 PPT"
|
||||
- "horizontal swipe"
|
||||
- "horizontal swipe deck"
|
||||
- "editorial magazine"
|
||||
- "e-ink presentation"
|
||||
- "网页 PPT"
|
||||
- "发布会"
|
||||
- "分享 PPT"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: marketing
|
||||
featured: 9
|
||||
default_for: deck
|
||||
upstream: "https://github.com/op7418/guizang-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
example_prompt: "帮我做一份杂志风的 PPT —— 关于'一人公司 · 被 AI 折叠的组织',25 分钟分享会,目标受众是设计师 + 创业者。先推荐一个方向(Monocle / WIRED / Kinfolk / Domus / Lab)让我选。"
|
||||
---
|
||||
|
||||
# Magazine Web Ppt
|
||||
|
||||
## 这个 Skill 做什么
|
||||
|
||||
生成一份**单文件 HTML**的横向翻页 PPT,视觉基调是:
|
||||
|
||||
- **电子杂志 + 电子墨水**混血风格
|
||||
- **WebGL 流体 / 等高线 / 色散背景**(hero 页可见)
|
||||
- **衬线标题(Noto Serif SC + Playfair Display)+ 非衬线正文(Noto Sans SC + Inter)+ 等宽元数据(IBM Plex Mono)**
|
||||
- **Lucide 线性图标**(不用 emoji)
|
||||
- **横向左右翻页**(键盘 ← →、滚轮、触屏滑动、底部圆点、ESC 索引)
|
||||
- **主题平滑插值**:翻到 hero 页时颜色和 shader 柔顺过渡
|
||||
|
||||
这个 skill 的美学不是"商务 PPT",也不是"消费互联网 UI"——它像 *Monocle* 杂志贴上了代码后的样子。
|
||||
|
||||
## 何时使用
|
||||
|
||||
**合适的场景**:
|
||||
- 线下分享 / 行业内部讲话 / 私享会
|
||||
- AI 新产品发布 / demo day
|
||||
- 带有强烈个人风格的演讲
|
||||
- 需要"一次做完,不用翻页工具"的网页版 slides
|
||||
|
||||
**不合适的场景**:
|
||||
- 大段表格数据、图表叠加(用常规 PPT)
|
||||
- 培训课件(信息密度不够)
|
||||
- 需要多人协作编辑(这是静态 HTML)
|
||||
|
||||
## 工作流
|
||||
|
||||
### Step 0 · 选方向(Direction · 必做的第一步)
|
||||
|
||||
**在问 6 个澄清问题之前,先让用户在 5 个 magazine 方向里挑一个**。每个方向都把"主题色 / 推荐 layout / chrome 风格 / 推荐 slide 数"打包好,挑了方向就回答掉一半澄清问题。
|
||||
|
||||
打开 `references/styles.md`,**整段拷过来**给用户看 5 个方向的 1-line summary,然后让他选:
|
||||
|
||||
```
|
||||
1. Monocle Editorial · 国际杂志风 ✦ 默认
|
||||
2. WIRED Tech · 数据 + 工程
|
||||
3. Kinfolk Slow · 慢生活 / 人文
|
||||
4. Domus Architectural · 建筑 / 空间感
|
||||
5. Lab / Reference · 学术 + 工艺手册
|
||||
```
|
||||
|
||||
如果用户说"不知道,你推荐"——**默认推 Monocle Editorial**,因为它失败概率最低。如果用户提到"AI / benchmark / 技术发布"——推 WIRED;"读书 / 私享 / 朋友圈"——推 Kinfolk;"设计 / 建筑 / portfolio"——推 Domus;"研究 / 学术 / 方法论"——推 Lab。
|
||||
|
||||
挑完方向后,在项目目录下创建或更新 `项目记录.md`,第一行写清方向 + 主题色 + 受众 + 时长(模板见 `styles.md` 末尾)。**全程不要换方向**——半路换 = 前面全废。
|
||||
|
||||
### Step 1 · 需求澄清(**动手前必做**)
|
||||
|
||||
**如果用户已经给了完整的大纲 + 图片**,可以跳过直接进 Step 2。
|
||||
|
||||
**如果用户只给了主题或一个模糊想法**,用这 6 个问题逐个对齐后再动手。不要基于猜测就开始写 slide——一旦结构定错,后期翻修代价很高:
|
||||
|
||||
#### 6 问澄清清单
|
||||
|
||||
> 第 5 题已在 Step 0 选方向时一并回答(方向→主题色)。下面的 5 题里,第 5 题留白即可。
|
||||
|
||||
| # | 问题 | 为什么要问 |
|
||||
|---|------|-----------|
|
||||
| 1 | **受众是谁?分享场景?**(行业内部 / 商业发布 / demo day / 私享会) | 决定语言风格和深度 |
|
||||
| 2 | **分享时长?** | 15 分钟 ≈ 10 页,30 分钟 ≈ 20 页,45 分钟 ≈ 25-30 页(每个方向的推荐范围见 `styles.md`) |
|
||||
| 3 | **有没有原始素材?**(文档 / 数据 / 旧 PPT / 文章链接) | 有素材就基于素材,没有就帮他搭 |
|
||||
| 4 | **有没有图片?放在哪?** | 详见下方"图片约定" |
|
||||
| 5 | ~~**想要哪套主题色?**~~ | ✓ 已在 Step 0 由方向决定 |
|
||||
| 6 | **有没有硬约束?**(必须包含 XX 数据 / 不能出现 YY) | 避免返工 |
|
||||
|
||||
#### 大纲协助(如果用户没有大纲)
|
||||
|
||||
用"叙事弧"模板搭骨架,再填内容:
|
||||
|
||||
```
|
||||
钩子(Hook) → 1 页 : 抛一个反差 / 问题 / 硬数据让人停下来
|
||||
定调(Context) → 1-2 页 : 说明背景 / 你是谁 / 为什么讲这个
|
||||
主体(Core) → 3-5 页 : 核心内容,用 Layout 4/5/6/9/10 穿插
|
||||
转折(Shift) → 1 页 : 打破预期 / 提出新观点
|
||||
收束(Takeaway) → 1-2 页 : 金句 / 悬念问题 / 行动建议
|
||||
```
|
||||
|
||||
叙事弧 + 页数规划 + 主题节奏表(见 `layouts.md`),**三张表对齐后**再进 Step 2。
|
||||
|
||||
大纲建议保存为 `项目记录.md` 或 `大纲-v1.md`,便于后续迭代。
|
||||
|
||||
#### 图片约定(告知用户)
|
||||
|
||||
在动手前向用户说清:
|
||||
|
||||
- **文件夹位置**:`项目/XXX/ppt/images/` 下(和 `index.html` 同级)
|
||||
- **命名规范**:`{页号}-{语义}.{ext}`,例如 `01-cover.jpg` / `03-figma.jpg` / `05-dashboard.png`
|
||||
- 页号补零便于排序
|
||||
- 语义用英文,短、具体、和内容对应
|
||||
- **规格建议**:
|
||||
- 单张 ≥ 1600px 宽(避免大屏模糊)
|
||||
- JPG 用于照片/截图,PNG 用于透明 UI/图表
|
||||
- 总大小控制在 10MB 内(影响翻页流畅度)
|
||||
- **如何替换**:保持**同名覆盖**最稳(HTML 里不用改路径);如果文件名变了,记得全局搜 `images/旧名` 改成新名
|
||||
- **没图怎么办**:和用户对齐,可以先用占位色块生成结构,等图片后期补;但要告知 layout 4/5/10 等图文混排页没图就没法验证视觉效果
|
||||
|
||||
### Step 2 · 拷贝模板
|
||||
|
||||
从 `assets/template.html` 拷贝一份到目标位置(通常是 `项目/XXX/ppt/index.html`),同时在同级建一个 `images/` 文件夹准备接图片。
|
||||
|
||||
```bash
|
||||
mkdir -p "项目/XXX/ppt/images"
|
||||
cp "<SKILL_ROOT>/assets/template.html" "项目/XXX/ppt/index.html"
|
||||
```
|
||||
|
||||
`template.html` 是一个**完整可运行**的文件——CSS、WebGL shader、翻页 JS、字体/图标 CDN 全已预设好,只有 `<main id="deck">` 里面是 3 个示例 slide(封面、章节幕封、空白填充页)。
|
||||
|
||||
#### 2.1 · 必改占位符(**容易漏**)
|
||||
|
||||
拷贝后立刻改掉以下占位符,否则浏览器 Tab 会显示"[必填] 替换为 PPT 标题"这种尴尬文字:
|
||||
|
||||
| 位置 | 原始 | 需改为 |
|
||||
|------|------|--------|
|
||||
| `<title>` | `[必填] 替换为 PPT 标题 · Deck Title` | 实际 deck 标题(如 `一种新的工作方式 · Luke Wroblewski`) |
|
||||
|
||||
每次拷贝完 template.html 第一件事:grep 一下"[必填]" 确认全部替换完。
|
||||
|
||||
#### 2.2 · 选定主题色(5 套预设 · 不允许自定义)
|
||||
|
||||
本 skill **只允许从 5 套精心调配的预设里选一套**,不接受用户自定义 hex 值——颜色搭配错了画面瞬间变丑,保护美学比给自由更重要。
|
||||
|
||||
| # | 主题 | 适合 |
|
||||
|---|------|------|
|
||||
| 1 | 🖋 墨水经典 | 通用 / 商业发布 / 不知道选啥的默认 |
|
||||
| 2 | 🌊 靛蓝瓷 | 科技 / 研究 / 数据 / 技术发布会 |
|
||||
| 3 | 🌿 森林墨 | 自然 / 可持续 / 文化 / 非虚构 |
|
||||
| 4 | 🍂 牛皮纸 | 怀旧 / 人文 / 文学 / 独立杂志 |
|
||||
| 5 | 🌙 沙丘 | 艺术 / 设计 / 创意 / 画廊 |
|
||||
|
||||
**操作**:
|
||||
1. 基于内容主题推荐一套,或直接问用户选哪一套
|
||||
2. 打开 `references/themes.md`,找到对应主题的 `:root` 块
|
||||
3. **整体替换** `assets/template.html`(已拷贝版本)开头 `:root{` 块里标有"主题色"注释的那几行(`--ink` / `--ink-rgb` / `--paper` / `--paper-rgb` / `--paper-tint` / `--ink-tint`)
|
||||
4. 其他 CSS 都走 `var(--...)`,无需任何其他改动
|
||||
|
||||
**硬规则**:
|
||||
- 一份 deck 只用一套主题,不要中途换色
|
||||
- 不要接受用户给的任意 hex 值——委婉拒绝并展示 5 套让选
|
||||
- 不要混搭(例如 ink 取墨水经典、paper 取沙丘)——会彻底违和
|
||||
|
||||
### Step 3 · 填充内容
|
||||
|
||||
#### 3.0 · 预检:类名必须在 template.html 里有定义(**最重要**)
|
||||
|
||||
**这是所有生成问题的源头**。layouts.md 的骨架使用了很多类名(`h-hero` / `h-xl` / `stat-card` / `pipeline` / `grid-2-7-5` 等),如果 `assets/template.html` 的 `<style>` 里没有对应定义,浏览器会 fallback 到默认样式——大标题变成非衬线、数据卡片挤成一团、pipeline 糊成一行、图片堆到页面底部。
|
||||
|
||||
**在写任何 slide 代码之前:**
|
||||
|
||||
1. **先 Read `assets/template.html`**(至少读到 `<style>` 块末尾)
|
||||
2. **对照 layouts.md 的 Pre-flight 列表**,确认你要用的每个类都在 `<style>` 里存在
|
||||
3. 如果某个类缺失:**在 template.html 的 `<style>` 里补上**,不要在每个 slide 里 inline 重写
|
||||
4. **template.html 是唯一的类名来源**——不要发明新类名,如需自定义用 `style="..."` inline
|
||||
|
||||
常见容易遗漏的类(必须预先确认存在):
|
||||
`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` / `kicker` / `meta-row` / `stat-card` / `stat-label` / `stat-nb` / `stat-unit` / `stat-note` / `pipeline-section` / `pipeline-label` / `pipeline` / `step` / `step-nb` / `step-title` / `step-desc` / `grid-2-7-5` / `grid-2-6-6` / `grid-2-8-4` / `grid-3-3` / `grid-6` / `grid-3` / `grid-4` / `frame` / `frame-img` / `img-cap` / `callout` / `callout-src` / `chrome` / `foot`
|
||||
|
||||
#### 3.0.5 · 规划主题节奏(**和类预检同等重要**)
|
||||
|
||||
**在挑布局之前**,必须先列出每一页的主题 class(`hero dark` / `hero light` / `light` / `dark`)并写到文档或草稿里对齐。详细规则看 `references/layouts.md` 开头的"主题节奏规划"一节。
|
||||
|
||||
**强制规则**:
|
||||
|
||||
- 每页 section 必须带 `light` / `dark` / `hero light` / `hero dark` 之一,不要只写 `hero`
|
||||
- 连续 3 页以上同主题 = 视觉疲劳,不允许
|
||||
- 8 页以上必须有 ≥1 个 `hero dark` + ≥1 个 `hero light`
|
||||
- 整个 deck 不能只有 `light` 正文页,必须有 `dark` 正文页制造呼吸
|
||||
- 每 3-4 页插入 1 个 hero 页(封面/幕封/问题/大引用)
|
||||
|
||||
**生成后自检**:`grep 'class="slide' index.html` 列出所有主题,人工确认节奏合理再交付。
|
||||
|
||||
#### 3.1 · 挑布局
|
||||
|
||||
**不要从零写 slide**。打开 `references/layouts.md`,里面有 10 种现成布局骨架,每种都是完整可粘贴的 `<section>` 代码块:
|
||||
|
||||
| Layout | 用途 |
|
||||
|---|---|
|
||||
| 1. 开场封面 | 第 1 页 |
|
||||
| 2. 章节幕封 | 每幕开场 |
|
||||
| 3. 数据大字报 | 抛硬数据 |
|
||||
| 4. 左文右图(Quote + Image) | 身份反差 / 故事 |
|
||||
| 5. 图片网格 | 多图对比 / 截图实证 |
|
||||
| 6. 两列流水线(Pipeline) | 工作流程 |
|
||||
| 7. 悬念收束 / 问题页 | 幕末 / 收尾 |
|
||||
| 8. 大引用页(Big Quote) | 衬线金句 / takeaway |
|
||||
| 9. 并列对比(Before / After) | 旧模式 vs 新模式 |
|
||||
| 10. 图文混排(Lead Image + Side Text) | 信息密集的图文页 |
|
||||
|
||||
选对应 layout,粘过去,改文案和图片路径即可。**务必先完成 3.0 预检**。
|
||||
|
||||
#### 3.2 · 图片比例规范
|
||||
|
||||
永远用**标准比例**,不要用原图奇葩比例(如 `2592/1798`):
|
||||
|
||||
| 场景 | 推荐比例 |
|
||||
|------|---------|
|
||||
| 左文右图 主图 | 16:10 或 4:3 + `max-height:56vh` |
|
||||
| 图片网格(多图对比) | **固定 `height:26vh`**,不用 aspect-ratio |
|
||||
| 左小图 + 右文字 | 1:1 或 3:2 |
|
||||
| 全屏主视觉 | 16:9 + `max-height:64vh` |
|
||||
| 图文混排小插图 | 3:2 或 3:4 |
|
||||
|
||||
**图片绝不使用 `align-self:end`**——会滑到 cell 底被浏览器工具栏遮挡。用 grid 容器 + `align-items:start`(template 已预设)让图片贴顶即可;左列若想贴底,用 flex column + `justify-content:space-between`。
|
||||
|
||||
组件细节(字体、颜色、网格、图标、callout、stat-card 等)在 `references/components.md`。
|
||||
|
||||
### Step 4 · 对照检查清单自检
|
||||
|
||||
生成完一定要打开 `references/checklist.md`,逐项对照。里面总结了**真实迭代过程中踩过的所有坑**,P0 级别的问题(emoji、图片撑破、标题换行、字体分工)必须全部通过。
|
||||
|
||||
特别要注意的几条:
|
||||
|
||||
1. **大标题必须是衬线字体**——如果显示成非衬线,99% 是 Step 3.0 预检没做,`h-hero` 类在 template.html 里缺失
|
||||
2. **图片网格里只用 `height:Nvh`,不用 `aspect-ratio`**(会撑破)
|
||||
3. **图片不能堆到页面底部**——不要用 `align-self:end`,用 grid + `align-items:start`(见 Step 3.2)
|
||||
4. **图片只能用标准比例**(16:10 / 4:3 / 3:2 / 1:1 / 16:9),不要复制原图的奇葩比例
|
||||
5. **中文大标题 ≤ 5 字且 `nowrap`**(避免 1 字 1 行)
|
||||
6. **用 Lucide,不用 emoji**
|
||||
7. **标题用衬线,正文用非衬线,元数据用等宽**
|
||||
|
||||
### Step 5 · 本地预览
|
||||
|
||||
直接在浏览器打开 `index.html` 就行。macOS 下:
|
||||
|
||||
```bash
|
||||
open "项目/XXX/ppt/index.html"
|
||||
```
|
||||
|
||||
不需要本地服务器。图片走相对路径 `images/xxx.png`。
|
||||
|
||||
### Step 6 · 迭代
|
||||
|
||||
根据用户反馈修改——模板的 CSS 已经高度参数化,90% 的调整都是改 inline style(字号 `font-size:Xvw` / 高度 `height:Yvh` / 间距 `gap:Zvh`)。
|
||||
|
||||
---
|
||||
|
||||
## 资源文件导览
|
||||
|
||||
```
|
||||
magazine-web-ppt/
|
||||
├── SKILL.md ← 你正在读
|
||||
├── assets/
|
||||
│ ├── template.html ← 完整的可运行模板(种子文件)
|
||||
│ └── example-slides.html ← 9 页样例 deck(用于 Examples 预览)
|
||||
└── references/
|
||||
├── styles.md ← 5 个 magazine 方向(Monocle / WIRED / Kinfolk / Domus / Lab)
|
||||
├── components.md ← 组件手册(字体、色、网格、图标、callout、stat、pipeline...)
|
||||
├── layouts.md ← 10 种页面布局骨架(可直接粘贴)
|
||||
├── themes.md ← 5 套主题色预设(只能选不能自定义)
|
||||
└── checklist.md ← 质量检查清单(P0/P1/P2/P3 分级)
|
||||
```
|
||||
|
||||
**加载顺序建议**:
|
||||
1. 先读完 `SKILL.md`(这个文件)了解整体
|
||||
2. **Step 0 选方向时,读 `styles.md`**——5 个方向各自打包好了主题色 + 推荐 layout + chrome 风格
|
||||
3. Step 1 需求澄清完成后,如果方向需要确认,再读 `themes.md` 看色板细节
|
||||
4. **动手前 Read `assets/template.html` 的 `<style>` 块**——这是类名的唯一来源,缺类会导致整页样式崩
|
||||
5. 读 `layouts.md` 挑布局(顶部有 Pre-flight 类名清单和主题节奏规划)
|
||||
6. 细节调整时读 `components.md` 查组件
|
||||
7. 生成后读 `checklist.md` 自检(顶部 P0-0 规则强制预检)
|
||||
|
||||
## 核心设计原则(哲学)
|
||||
|
||||
> 这些原则是"一人公司"分享 PPT 的 5 轮迭代总结出来的。违反其中任何一条,视觉感都会垮。
|
||||
|
||||
1. **克制优于炫技** — WebGL 背景只在 hero 页透出,普通页几乎看不见
|
||||
2. **结构优于装饰** — 不用阴影、不用浮动卡片、不用 padding box,一切信息靠**大字号 + 字体对比 + 网格留白**
|
||||
3. **内容层级由字号和字体共同定义** — 最大衬线 = 主标题,中衬线 = 副标,大非衬线 = lead,小非衬线 = body,等宽 = 元数据
|
||||
4. **图片是第一公民** — 图片只裁底部,保证顶部和左右完整;网格用 `height:Nvh` 固定,不要用 `aspect-ratio` 撑
|
||||
5. **节奏靠 hero 页** — hero 和 non-hero 交替,才不累眼睛
|
||||
6. **术语统一** — Skills 就是 Skills,不要中英混合翻译
|
||||
|
||||
## 参考作品
|
||||
|
||||
本 skill 的视觉基调参考了:
|
||||
|
||||
- 歸藏 "一人公司:被 AI 折叠的组织" 分享(2026-04-22,27 页)
|
||||
- *Monocle* 杂志的版式
|
||||
- YC 总裁 Garry Tan "Thin Harness, Fat Skills" 那篇博客的 demo
|
||||
|
||||
可以把它们当做风格锚点。
|
||||
@@ -0,0 +1,318 @@
|
||||
<!--
|
||||
Example slides for the magazine-web-ppt skill.
|
||||
|
||||
Topic: "一人公司 · The Quiet Hardware" — a fictional but realistic 64-day
|
||||
case study, mirroring the rhythm and content arc of the original 歸藏
|
||||
guizang-ppt-skill demo. Used to power the Examples preview without
|
||||
requiring real product imagery on disk — image slots stand in for what a
|
||||
real deck would show.
|
||||
|
||||
Theme rhythm: hero dark → light → dark → light → hero light → dark →
|
||||
hero dark → light → hero light. Hits all 8 layout categories.
|
||||
-->
|
||||
|
||||
<!-- Layout 1 · Hero Cover ============================================ -->
|
||||
<section class="slide hero dark">
|
||||
<div class="chrome">
|
||||
<div>A Talk · 2026.04.22</div>
|
||||
<div>Vol.01</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:4vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">私享会 · 创作者 Demo Day</div>
|
||||
<h1 class="h-hero">一人公司</h1>
|
||||
<h2 class="h-sub">被 AI 折叠的组织</h2>
|
||||
<p class="lead" style="max-width:60vw">
|
||||
一个独立创作者 —— 在 64 天里完成 11 万行代码、覆盖 9 个平台、跨过 5 个时区,<br>
|
||||
生活节奏几乎没有被打扰。
|
||||
</p>
|
||||
<div class="meta-row">
|
||||
<span>歸藏 Guizang</span><span>·</span><span>独立创作者</span><span>·</span><span>CodePilot 作者</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>一场关于 AI · 组织 · 个体的分享</div>
|
||||
<div>— 2026 —</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 2 · Big Numbers Grid ======================================= -->
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>过去 64 天 · 开发篇</div>
|
||||
<div>Act I / Dev · 02 / 09</div>
|
||||
</div>
|
||||
<div class="frame" style="padding-top:6vh">
|
||||
<div class="kicker">一个人,做了什么。</div>
|
||||
<h2 class="h-xl">过去 64 天</h2>
|
||||
<p class="lead" style="margin-bottom:5vh">从 0 到开源 CodePilot。</p>
|
||||
|
||||
<div class="grid-6" style="margin-top:6vh">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Duration</div>
|
||||
<div class="stat-nb">64 <span class="stat-unit">天</span></div>
|
||||
<div class="stat-note">从立项到现在</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Lines of Code</div>
|
||||
<div class="stat-nb">110K+</div>
|
||||
<div class="stat-note">一行一行写到 11 万+</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">GitHub Stars</div>
|
||||
<div class="stat-nb">5,166</div>
|
||||
<div class="stat-note">单仓库 · 60 天破 5K</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Downloads</div>
|
||||
<div class="stat-nb">41K+</div>
|
||||
<div class="stat-note">装进了几万台电脑里</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">AI Providers</div>
|
||||
<div class="stat-nb">19</div>
|
||||
<div class="stat-note">跨平台模型接入</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Commits</div>
|
||||
<div class="stat-nb">608+</div>
|
||||
<div class="stat-note">没有协作者</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>项目 · CodePilot | github.com/codepilot</div>
|
||||
<div>Act I · Dev Numbers</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 4 · Quote + Image ========================================== -->
|
||||
<section class="slide dark">
|
||||
<div class="chrome">
|
||||
<div>身份反差 · The Twist</div>
|
||||
<div>03 / 09</div>
|
||||
</div>
|
||||
<div class="frame grid-2-7-5" style="padding-top:6vh">
|
||||
<div style="display:flex; flex-direction:column; justify-content:space-between; gap:3vh">
|
||||
<div>
|
||||
<div class="kicker">BUT</div>
|
||||
<h2 class="h-xl" style="white-space:nowrap; font-size:7.2vw">
|
||||
我不是程序员。
|
||||
</h2>
|
||||
<p class="lead" style="margin-top:3vh">
|
||||
大学毕业之后再没写过一行生产代码。过去十年做的是 UI 设计 / AI 特效 / 自媒体内容。
|
||||
</p>
|
||||
</div>
|
||||
<div class="callout">
|
||||
“这东西在三年前,<br>
|
||||
需要一个十人团队做一年。”
|
||||
<div class="callout-src">— 一个观察者的判断</div>
|
||||
</div>
|
||||
</div>
|
||||
<figure class="img-slot r-3x2" style="aspect-ratio:16/10; max-height:56vh">
|
||||
<span class="plus">+</span>
|
||||
<span class="label">Product Screenshot · CodePilot</span>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 03 · 我不是程序员</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 6 · Pipeline =============================================== -->
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>我的工作流 · Workflow</div>
|
||||
<div>Act II · 04 / 09</div>
|
||||
</div>
|
||||
<div class="frame">
|
||||
<div class="kicker">Pipeline · 流水线</div>
|
||||
<h2 class="h-xl">两条流水线</h2>
|
||||
|
||||
<div class="pipeline-section">
|
||||
<div class="pipeline-label">文本侧 · Text Pipeline</div>
|
||||
<div class="pipeline">
|
||||
<div class="step">
|
||||
<div class="step-nb">01</div>
|
||||
<div class="step-title">Draft</div>
|
||||
<div class="step-desc">AI 帮我起草初稿</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">02</div>
|
||||
<div class="step-title">Polish</div>
|
||||
<div class="step-desc">AI 润色去 AI 味</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">03</div>
|
||||
<div class="step-title">Morph</div>
|
||||
<div class="step-desc">AI 变形成推特 / 小红书</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">04</div>
|
||||
<div class="step-title">Illustrate</div>
|
||||
<div class="step-desc">AI 生成信息图</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">05</div>
|
||||
<div class="step-title">Distribute</div>
|
||||
<div class="step-desc">一键分发 9 平台</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline-section">
|
||||
<div class="pipeline-label">视觉 · 视频侧 · Video Pipeline</div>
|
||||
<div class="pipeline" data-cols="3">
|
||||
<div class="step">
|
||||
<div class="step-nb">06</div>
|
||||
<div class="step-title">Cut</div>
|
||||
<div class="step-desc">AI 剪辑 + 自动配字幕</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">07</div>
|
||||
<div class="step-title">Wrap</div>
|
||||
<div class="step-desc">AI 包装 + 配 BGM</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">08</div>
|
||||
<div class="step-title">Cover</div>
|
||||
<div class="step-desc">AI 生成封面图</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 04 · 我的内容工厂</div>
|
||||
<div>Workflow</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 2 · Act Divider ============================================ -->
|
||||
<section class="slide hero light">
|
||||
<div class="chrome">
|
||||
<div>第二幕 · 折叠</div>
|
||||
<div>Act II · 05 / 09</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:6vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">Act II</div>
|
||||
<h1 class="h-hero" style="font-size:8.5vw">折叠</h1>
|
||||
<p class="lead" style="max-width:55vw">
|
||||
从 “一个人做内容” 到 “一个人是组织”。<br>
|
||||
AI 不是工具,是岗位的折叠器。
|
||||
</p>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>第二幕引子</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 8 · Big Quote ============================================== -->
|
||||
<section class="slide dark">
|
||||
<div class="chrome">
|
||||
<div>The Takeaway · 核心金句</div>
|
||||
<div>06 / 09</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:5vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">Quote · 金句</div>
|
||||
<blockquote style="font-family:var(--serif-zh); font-weight:700; font-size:5.6vw; line-height:1.2; letter-spacing:-.01em; max-width:78vw">
|
||||
“没有交接,<br>所有人都在构建。”
|
||||
</blockquote>
|
||||
<p class="lead" style="max-width:55vw; opacity:.65">
|
||||
Without the handoff, everyone builds.<br>
|
||||
And that makes all the difference.
|
||||
</p>
|
||||
<div class="meta-row">
|
||||
<span>— Luke Wroblewski</span><span>·</span><span>2026.04.16</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 06 · 金句</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 7 · Hero Question ========================================== -->
|
||||
<section class="slide hero dark">
|
||||
<div class="chrome">
|
||||
<div>留给你的问题</div>
|
||||
<div>07 / 09</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:8vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">The Question</div>
|
||||
<h1 class="h-hero" style="font-size:7vw; line-height:1.15">
|
||||
你的公司里,<br>
|
||||
哪些岗位本来就<br>
|
||||
不该由人来做?
|
||||
</h1>
|
||||
<p class="lead" style="max-width:50vw">
|
||||
这不是技术问题,是架构问题。
|
||||
</p>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 07 · The Question</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 9 · Before / After ========================================= -->
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>旧 vs 新 · The Shift</div>
|
||||
<div>08 / 09</div>
|
||||
</div>
|
||||
<div class="frame" style="padding-top:5vh">
|
||||
<div class="kicker">Before / After · 范式转变</div>
|
||||
<h2 class="h-xl" style="margin-bottom:4vh">从交接到共建</h2>
|
||||
|
||||
<div class="grid-2-6-6" style="gap:5vw 4vh">
|
||||
<div style="padding:3vh 2vw; border-left:3px solid currentColor; opacity:.55">
|
||||
<div class="kicker" style="opacity:.9">Before · 旧模式</div>
|
||||
<h3 class="h-md" style="margin-top:2vh">设计 → 开发 → 交接</h3>
|
||||
<ul style="margin-top:3vh; padding-left:1.2em; display:flex; flex-direction:column; gap:1.4vh; font-family:var(--sans-zh); font-size:max(14px,1.1vw); line-height:1.55">
|
||||
<li>设计师在 Figma 做稿,反复对齐像素</li>
|
||||
<li>开发盯着设计稿手动翻译</li>
|
||||
<li>反复 PR 沟通,文档遗失在 Slack</li>
|
||||
<li>非技术成员无法触碰代码</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="padding:3vh 2vw; border-left:3px solid currentColor">
|
||||
<div class="kicker" style="opacity:.9">After · 新模式</div>
|
||||
<h3 class="h-md" style="margin-top:2vh">同工具 · 并行 · 共建</h3>
|
||||
<ul style="margin-top:3vh; padding-left:1.2em; display:flex; flex-direction:column; gap:1.4vh; font-family:var(--sans-zh); font-size:max(14px,1.1vw); line-height:1.55">
|
||||
<li>三个角色同时在同一份 Intent 上工作</li>
|
||||
<li>agents.md / SKILL.md 是共享上下文</li>
|
||||
<li>代理处理对齐、冲突、动效</li>
|
||||
<li>任何人都能安全贡献代码</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 08 · 范式转变</div>
|
||||
<div>Before / After</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout 2 · Hero Close ============================================= -->
|
||||
<section class="slide hero light">
|
||||
<div class="chrome">
|
||||
<div>End · 致谢</div>
|
||||
<div>09 / 09</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:5vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">Thanks for watching</div>
|
||||
<h1 class="h-hero" style="font-size:9vw">谢谢。</h1>
|
||||
<p class="lead" style="max-width:55vw">
|
||||
Slides are a single HTML file —<br>
|
||||
open in any browser, no build, no server.
|
||||
</p>
|
||||
<div class="meta-row">
|
||||
<span>github.com/op7418/guizang-ppt-skill</span><span>·</span><span>MIT License</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Made with magazine-web-ppt skill</div>
|
||||
<div>— Fin —</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,647 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>[必填] 替换为 PPT 标题 · Deck Title</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;0,8..60,500;0,8..60,600;1,8..60,400&family=IBM+Plex+Mono:wght@300;400;500;600&family=Noto+Serif+SC:wght@300;400;500;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
/* ============ 主题色(默认:🖋 墨水经典) ============
|
||||
切换主题:从 references/themes.md 复制对应的 :root 块
|
||||
整体替换这几行(--ink / --ink-rgb / --paper / --paper-rgb)
|
||||
其他地方散落的 rgba() 都走 var(--ink-rgb) / var(--paper-rgb),无需逐处改 */
|
||||
--ink:#0a0a0b;
|
||||
--ink-rgb:10,10,11;
|
||||
--paper:#f1efea;
|
||||
--paper-rgb:241,239,234;
|
||||
--paper-tint:#e8e5de;
|
||||
--ink-tint:#18181a;
|
||||
|
||||
/* ============ 字体(跨主题固定) ============ */
|
||||
--mono:"IBM Plex Mono",ui-monospace,monospace;
|
||||
--serif-en:"Playfair Display","Source Serif 4",Georgia,serif;
|
||||
--serif-body-en:"Source Serif 4",Georgia,serif;
|
||||
--serif-zh:"Noto Serif SC",source-han-serif-sc,serif;
|
||||
--sans-zh:"Noto Sans SC",source-han-sans-sc,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:var(--ink);color:var(--paper);font-family:var(--sans-zh);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
||||
|
||||
/* ============ WebGL 双背景 ============ */
|
||||
canvas.bg{position:fixed;inset:0;width:100vw;height:100vh;z-index:0;display:block;transition:opacity 1.2s ease}
|
||||
canvas#bg-light{opacity:0}
|
||||
canvas#bg-dark{opacity:1}
|
||||
body.light-bg canvas#bg-light{opacity:1}
|
||||
body.light-bg canvas#bg-dark{opacity:0}
|
||||
|
||||
/* ============ Deck 容器 + 翻页 ============ */
|
||||
/* width: NSLIDES * 100vw,会在 JS 里动态矫正 */
|
||||
#deck{position:fixed;inset:0;width:10000vw;height:100vh;display:flex;flex-wrap:nowrap;transition:transform .9s cubic-bezier(.77,0,.175,1);z-index:10;will-change:transform}
|
||||
.slide{width:100vw;height:100vh;flex:0 0 100vw;position:relative;padding:6vh 6vw 10vh 6vw;display:flex;flex-direction:column;overflow:hidden}
|
||||
.slide.light{color:var(--ink)}
|
||||
.slide.dark{color:var(--paper)}
|
||||
|
||||
/* 默认页:遮罩较厚,保证文字可读 */
|
||||
.slide::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;transition:background .7s ease}
|
||||
.slide.light::before{background:rgba(var(--paper-rgb),.78);backdrop-filter:blur(3px)}
|
||||
.slide.dark::before{background:rgba(var(--ink-rgb),.78);backdrop-filter:blur(3px)}
|
||||
/* Hero 页:遮罩大幅降低,让 WebGL 背景明显透出 */
|
||||
.slide.hero.light::before{background:rgba(var(--paper-rgb),.16);backdrop-filter:none}
|
||||
.slide.hero.dark::before{background:rgba(var(--ink-rgb),.12);backdrop-filter:none}
|
||||
/* Hero 页顶底微弱渐隐,保证 chrome/foot 区域可读 */
|
||||
.slide.hero::after{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none}
|
||||
.slide.hero.light::after{background:linear-gradient(180deg,rgba(var(--paper-rgb),.28) 0%,rgba(var(--paper-rgb),0) 14%,rgba(var(--paper-rgb),0) 86%,rgba(var(--paper-rgb),.28) 100%)}
|
||||
.slide.hero.dark::after{background:linear-gradient(180deg,rgba(var(--ink-rgb),.32) 0%,rgba(var(--ink-rgb),0) 14%,rgba(var(--ink-rgb),0) 86%,rgba(var(--ink-rgb),.32) 100%)}
|
||||
|
||||
/* ============ Magazine chrome:顶部 meta + 底部 foot ============ */
|
||||
.chrome{display:flex;justify-content:space-between;align-items:flex-start;font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;opacity:.7}
|
||||
.chrome .left,.chrome .right{display:flex;gap:2.4em;align-items:center}
|
||||
.chrome .sep{width:40px;height:1px;background:currentColor;opacity:.4}
|
||||
.foot{margin-top:auto;display:flex;justify-content:space-between;align-items:flex-end;font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;opacity:.55}
|
||||
.foot .title{font-family:var(--serif-zh);font-weight:400;letter-spacing:.05em;text-transform:none;opacity:.75;font-size:13px}
|
||||
|
||||
.tag{display:inline-block;font-family:var(--mono);font-size:11px;letter-spacing:.24em;text-transform:uppercase;padding:6px 14px;border:1px solid currentColor;opacity:.85}
|
||||
.rule{width:100%;height:1px;background:currentColor;opacity:.25;margin:3vh 0}
|
||||
.rule.v{width:1px;height:100%;margin:0}
|
||||
|
||||
/* ============ 字体规则 ============
|
||||
· 衬线(Noto Serif SC / Playfair):大标题、重点金句、数字
|
||||
· 非衬线(Noto Sans SC):正文描述、body、补充说明
|
||||
· 等宽(IBM Plex Mono):kicker、meta 小标签、foot 右侧
|
||||
*/
|
||||
.kicker{font-family:var(--mono);font-size:12px;letter-spacing:.3em;text-transform:uppercase;opacity:.6;margin-bottom:2.6vh}
|
||||
.display{font-family:var(--serif-en);font-weight:700;font-size:11vw;line-height:.92;letter-spacing:-.025em}
|
||||
.display-zh{font-family:var(--serif-zh);font-weight:700;font-size:7.8vw;line-height:1.04;letter-spacing:-.005em}
|
||||
.h1-zh{font-family:var(--serif-zh);font-weight:700;font-size:4.6vw;line-height:1.12;letter-spacing:-.005em}
|
||||
.h2-zh{font-family:var(--serif-zh);font-weight:600;font-size:3.2vw;line-height:1.2;letter-spacing:0}
|
||||
.h3-zh{font-family:var(--serif-zh);font-weight:500;font-size:1.9vw;line-height:1.35}
|
||||
.body-zh{font-family:var(--sans-zh);font-weight:400;font-size:max(15px,1.22vw);line-height:1.75;opacity:.82;letter-spacing:.01em}
|
||||
.body-serif{font-family:var(--serif-zh);font-weight:400;font-size:max(15px,1.3vw);line-height:1.65;opacity:.88}
|
||||
.lead{font-family:var(--serif-zh);font-weight:400;font-size:1.9vw;line-height:1.4;opacity:.85}
|
||||
.meta{font-family:var(--mono);font-size:max(11px,.88vw);letter-spacing:.16em;text-transform:uppercase;opacity:.6}
|
||||
.big-num{font-family:var(--serif-en);font-weight:800;font-size:10vw;line-height:.85;letter-spacing:-.03em;font-feature-settings:"tnum"}
|
||||
.mid-num{font-family:var(--serif-en);font-weight:700;font-size:5.5vw;line-height:.88;letter-spacing:-.02em;font-feature-settings:"tnum"}
|
||||
.ghost{font-family:var(--serif-en);font-weight:900;font-size:34vw;line-height:.8;opacity:.06;letter-spacing:-.04em;position:absolute;font-feature-settings:"tnum"}
|
||||
em{font-style:italic;font-family:var(--serif-en)}
|
||||
.en{font-family:var(--serif-en);font-style:italic;font-weight:500}
|
||||
|
||||
/* ============ 布局工具 ============ */
|
||||
.col{display:flex;flex-direction:column;gap:2.4vh}
|
||||
.row{display:flex;align-items:center;gap:3vw}
|
||||
.grid-6{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:4vw 6vw;flex:1;align-content:center;padding:2vh 0}
|
||||
.grid-9{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:3vh 4vw;flex:1;align-content:center}
|
||||
.grid-4{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:4vh 6vw;flex:1;align-content:center}
|
||||
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:4vw;flex:1;align-content:center}
|
||||
.split{display:grid;grid-template-columns:1fr 1fr;gap:4vw;flex:1;align-items:center}
|
||||
.split-55{display:grid;grid-template-columns:55fr 45fr;gap:5vw;flex:1;align-items:stretch}
|
||||
.fill{flex:1}
|
||||
.center{align-items:center;justify-content:center;text-align:center}
|
||||
.bottom-left{position:absolute;left:6vw;bottom:9vh;max-width:50vw}
|
||||
.bottom-right{position:absolute;right:6vw;bottom:9vh;max-width:50vw;text-align:right}
|
||||
.top-right{position:absolute;right:6vw;top:6vh;text-align:right}
|
||||
|
||||
/* ============ Stat(数字矩阵) ============ */
|
||||
.stat{display:flex;flex-direction:column;gap:1vh;align-items:flex-start}
|
||||
.stat .n{font-family:var(--serif-en);font-weight:800;font-size:8vw;line-height:.88;letter-spacing:-.03em;font-feature-settings:"tnum"}
|
||||
.stat .l{font-family:var(--sans-zh);font-size:max(13px,1.05vw);opacity:.7;margin-top:1vh;font-weight:400;line-height:1.5}
|
||||
.stat .m{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.5;margin-bottom:.2vh}
|
||||
|
||||
/* ============ Callout(引用框) ============ */
|
||||
.callout{padding:3vh 2.4vw;border-left:3px solid currentColor;position:relative;font-family:var(--serif-zh);font-size:max(15px,1.2vw);line-height:1.55;opacity:.92}
|
||||
.slide.light .callout{background:rgba(var(--ink-rgb),.05)}
|
||||
.slide.dark .callout{background:rgba(var(--paper-rgb),.06)}
|
||||
.callout .cite{display:block;margin-top:1.6vh;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:.6}
|
||||
.callout .q-big{font-family:var(--serif-zh);font-weight:600;font-size:max(17px,1.6vw);line-height:1.42}
|
||||
|
||||
/* ============ Platform(平台卡) ============ */
|
||||
.plat{display:flex;flex-direction:column;justify-content:flex-end;padding:2vh 0;border-top:1px solid currentColor;border-color:rgba(127,127,127,.35)}
|
||||
.plat .name{font-family:var(--serif-zh);font-weight:700;font-size:1.8vw;margin-bottom:.6vh}
|
||||
.plat .nb{font-family:var(--serif-en);font-weight:700;font-size:3.2vw;letter-spacing:-.02em;line-height:1;font-feature-settings:"tnum"}
|
||||
.plat .sub{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;opacity:.55;margin-top:.6vh}
|
||||
.plat .fill{font-family:var(--sans-zh);font-weight:300;font-size:2.4vw;opacity:.28;letter-spacing:-.01em;line-height:1}
|
||||
|
||||
/* ============ Rowline(表格行) ============ */
|
||||
.rowline{display:grid;grid-template-columns:1fr 2fr 1fr;gap:2vw;padding:2.2vh 0;border-top:1px solid currentColor;align-items:center;border-color:rgba(127,127,127,.25)}
|
||||
.rowline:last-child{border-bottom:1px solid currentColor;border-color:rgba(127,127,127,.25)}
|
||||
.rowline .k{font-family:var(--serif-zh);font-weight:700;font-size:1.7vw}
|
||||
.rowline .v{font-family:var(--sans-zh);font-weight:400;font-size:max(14px,1.2vw);opacity:.85;line-height:1.55}
|
||||
.rowline .m{font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:.6;justify-self:end}
|
||||
|
||||
/* ============ Pillar(支柱卡片) ============ */
|
||||
.pillar{display:flex;flex-direction:column;gap:1.8vh}
|
||||
.pillar .ic{font-family:var(--serif-en);font-style:italic;font-size:2.6vw;opacity:.45;font-weight:400}
|
||||
.pillar .ic svg{width:2.8vw;height:2.8vw;stroke-width:1.2;opacity:.7}
|
||||
.pillar .t{font-family:var(--serif-zh);font-weight:700;font-size:2.4vw;line-height:1.1}
|
||||
.pillar .d{font-family:var(--sans-zh);font-weight:400;font-size:max(14px,1.1vw);opacity:.76;line-height:1.6}
|
||||
|
||||
/* ============ Signature / Highlight ============ */
|
||||
.sign{font-family:var(--serif-en);font-style:italic;font-weight:500;font-size:2vw;opacity:.7}
|
||||
.hi{position:relative;display:inline}
|
||||
.slide.dark .hi::after{content:"";position:absolute;left:-.1em;right:-.1em;bottom:-.05em;height:.28em;background:rgba(var(--paper-rgb),.15);z-index:-1}
|
||||
.slide.light .hi::after{content:"";position:absolute;left:-.1em;right:-.1em;bottom:-.05em;height:.28em;background:rgba(var(--ink-rgb),.08);z-index:-1}
|
||||
|
||||
/* ============ Icons(Lucide via CDN) ============ */
|
||||
.ico{width:1em;height:1em;display:inline-block;vertical-align:-.12em;stroke:currentColor;fill:none;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
|
||||
.ico-lg,.ico-md,.ico-sm{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
|
||||
.ico-lg{width:2.6vw;height:2.6vw;stroke-width:1.2;display:inline-block}
|
||||
.ico-md{width:1.8vw;height:1.8vw;stroke-width:1.3;display:inline-block;vertical-align:-.4em}
|
||||
.ico-sm{width:1.1vw;height:1.1vw;stroke-width:1.4;display:inline-block;vertical-align:-.15em;opacity:.7}
|
||||
|
||||
/* ============ 图片占位(虚线框,提示设计师位置) ============ */
|
||||
.img-slot{border:1.5px dashed rgba(127,127,127,.4);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:1vh;padding:2vh 2vw;font-family:var(--mono);font-size:10px;letter-spacing:.28em;text-transform:uppercase;opacity:.55;position:relative;aspect-ratio:16/9;width:100%;max-height:56vh;margin-inline:auto;box-sizing:border-box}
|
||||
.img-slot::before{content:"";position:absolute;inset:8px;border:1px solid currentColor;opacity:.2}
|
||||
.img-slot .plus{font-size:2vw;font-weight:300;opacity:.5;letter-spacing:0}
|
||||
.img-slot .label{position:relative;z-index:2;text-align:center}
|
||||
.img-slot.r-4x3{aspect-ratio:4/3}
|
||||
.img-slot.r-3x2{aspect-ratio:3/2}
|
||||
.img-slot.r-1x1{aspect-ratio:1/1}
|
||||
|
||||
/* ============ 图片实填框(关键:固定高度 + 只裁底部) ============
|
||||
重要约束:高度用内联 height:Nvh 精确控制,不要用 aspect-ratio(会撑破布局)
|
||||
object-position:top center 保证严禁裁剪顶部和左右,只裁剪底部
|
||||
*/
|
||||
.frame-img{overflow:hidden;position:relative;background:rgba(0,0,0,.04);box-sizing:border-box;width:100%;border-radius:4px}
|
||||
.slide.dark .frame-img{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.12)}
|
||||
.frame-img > img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block}
|
||||
.frame-cap{display:flex;justify-content:space-between;align-items:baseline;gap:1vw;margin-top:.8vh;font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.72}
|
||||
.frame-cap .pf{font-family:var(--serif-zh);font-weight:600;font-size:max(13px,1vw);letter-spacing:.04em;text-transform:none;opacity:.94}
|
||||
.frame-cap .nb{font-family:var(--serif-en);font-style:italic;font-size:max(15px,1.2vw);letter-spacing:.02em;text-transform:none;opacity:.88}
|
||||
.frame-cap .idx{font-family:var(--mono);opacity:.5}
|
||||
figure.tile{display:flex;flex-direction:column;margin:0;min-width:0}
|
||||
figure.tile > .frame-img{flex:0 0 auto}
|
||||
|
||||
/* ============ 导航 ============ */
|
||||
#nav{position:fixed;left:50%;bottom:2.6vh;transform:translateX(-50%);z-index:30;display:flex;gap:10px;padding:8px 14px;border-radius:999px;background:rgba(0,0,0,.18);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
|
||||
#nav .dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.3);cursor:pointer;transition:all .3s ease;border:0;padding:0}
|
||||
#nav .dot:hover{background:rgba(255,255,255,.5);transform:scale(1.15)}
|
||||
#nav .dot.active{background:rgba(255,255,255,.95);width:22px;border-radius:999px}
|
||||
body.light-bg #nav{background:rgba(255,255,255,.25)}
|
||||
body.light-bg #nav .dot{background:rgba(var(--ink-rgb),.25)}
|
||||
body.light-bg #nav .dot.active{background:rgba(var(--ink-rgb),.9)}
|
||||
#hint{position:fixed;bottom:3vh;right:3vw;z-index:30;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;opacity:.4;mix-blend-mode:difference;color:#aaa}
|
||||
|
||||
/* ============================================================
|
||||
============ LAYOUTS API · 面向 agent 的类(v2)============
|
||||
所有 layouts.md 中的骨架都基于下面这套命名。
|
||||
如果你在 layouts.md 里看到某个类,它必须在下面有定义。
|
||||
============================================================ */
|
||||
|
||||
/* ---------- .frame:每页主内容容器 ---------- */
|
||||
.frame{flex:1;display:flex;flex-direction:column;min-height:0}
|
||||
/* 当 .frame 同时加了 grid 类时,grid 的 display:grid 覆盖 flex */
|
||||
.frame.grid-2-7-5,
|
||||
.frame.grid-2-6-6,
|
||||
.frame.grid-2-8-4,
|
||||
.frame.grid-3-3,
|
||||
.frame.grid-6{display:grid}
|
||||
|
||||
/* ---------- 标题层级(API 名称,衬线为主) ---------- */
|
||||
.h-hero{
|
||||
font-family:var(--serif-zh);
|
||||
font-weight:900;
|
||||
font-size:10vw;
|
||||
line-height:.96;
|
||||
letter-spacing:-.02em;
|
||||
}
|
||||
.h-xl{
|
||||
font-family:var(--serif-zh);
|
||||
font-weight:700;
|
||||
font-size:6.2vw;
|
||||
line-height:1.08;
|
||||
letter-spacing:-.01em;
|
||||
}
|
||||
.h-sub{
|
||||
font-family:var(--serif-zh);
|
||||
font-weight:500;
|
||||
font-size:3.1vw;
|
||||
line-height:1.25;
|
||||
letter-spacing:0;
|
||||
opacity:.7;
|
||||
}
|
||||
.h-md{
|
||||
font-family:var(--serif-zh);
|
||||
font-weight:600;
|
||||
font-size:2.3vw;
|
||||
line-height:1.3;
|
||||
}
|
||||
/* 英文标题专用(Playfair 衬线) */
|
||||
.h-hero-en,.h-xl-en{font-family:var(--serif-en);letter-spacing:-.025em}
|
||||
|
||||
/* ---------- lead 引语 ---------- */
|
||||
.lead{
|
||||
font-family:var(--serif-zh);
|
||||
font-weight:400;
|
||||
font-size:1.75vw;
|
||||
line-height:1.5;
|
||||
opacity:.86;
|
||||
}
|
||||
|
||||
/* ---------- meta-row 底部元数据 ---------- */
|
||||
.meta-row{
|
||||
display:flex;
|
||||
gap:1.2em;
|
||||
align-items:baseline;
|
||||
flex-wrap:wrap;
|
||||
font-family:var(--mono);
|
||||
font-size:max(12px,.92vw);
|
||||
letter-spacing:.16em;
|
||||
text-transform:uppercase;
|
||||
opacity:.6;
|
||||
}
|
||||
|
||||
/* ---------- stat-card(数据大字报用) ---------- */
|
||||
.stat-card{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.8vh;
|
||||
align-items:flex-start;
|
||||
padding-top:1.6vh;
|
||||
border-top:1px solid currentColor;
|
||||
border-color:rgba(127,127,127,.3);
|
||||
}
|
||||
.stat-card .stat-label{
|
||||
font-family:var(--mono);
|
||||
font-size:max(10px,.78vw);
|
||||
letter-spacing:.24em;
|
||||
text-transform:uppercase;
|
||||
opacity:.55;
|
||||
}
|
||||
.stat-card .stat-nb{
|
||||
font-family:var(--serif-en);
|
||||
font-weight:800;
|
||||
font-size:5.8vw;
|
||||
line-height:.9;
|
||||
letter-spacing:-.03em;
|
||||
font-feature-settings:"tnum";
|
||||
margin-top:.4vh;
|
||||
}
|
||||
.stat-card .stat-nb .stat-unit{
|
||||
font-family:var(--serif-zh);
|
||||
font-weight:500;
|
||||
font-size:.38em;
|
||||
letter-spacing:0;
|
||||
opacity:.72;
|
||||
margin-left:.14em;
|
||||
}
|
||||
.stat-card .stat-note{
|
||||
font-family:var(--sans-zh);
|
||||
font-weight:400;
|
||||
font-size:max(13px,1.05vw);
|
||||
line-height:1.5;
|
||||
opacity:.72;
|
||||
margin-top:.6vh;
|
||||
}
|
||||
/* 当 stat-card 用于 grid-4(2x2),数字可以更大 */
|
||||
.grid-4 .stat-card .stat-nb{font-size:7.5vw}
|
||||
/* 当只有 3 个,字也可以稍大 */
|
||||
.grid-3 .stat-card .stat-nb{font-size:6.8vw}
|
||||
|
||||
/* ---------- pipeline(流水线) ---------- */
|
||||
.pipeline-section{
|
||||
margin-top:4.4vh;
|
||||
padding-top:2.8vh;
|
||||
border-top:1px dashed rgba(127,127,127,.32);
|
||||
}
|
||||
.pipeline-section:first-of-type{
|
||||
border-top:0;
|
||||
padding-top:0;
|
||||
margin-top:3vh;
|
||||
}
|
||||
.pipeline-label{
|
||||
font-family:var(--mono);
|
||||
font-size:max(11px,.85vw);
|
||||
letter-spacing:.24em;
|
||||
text-transform:uppercase;
|
||||
opacity:.62;
|
||||
margin-bottom:2.2vh;
|
||||
}
|
||||
.pipeline{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(5,1fr);
|
||||
gap:1.2vw;
|
||||
}
|
||||
.pipeline[data-cols="3"]{grid-template-columns:repeat(3,1fr)}
|
||||
.pipeline[data-cols="4"]{grid-template-columns:repeat(4,1fr)}
|
||||
.pipeline[data-cols="6"]{grid-template-columns:repeat(6,1fr)}
|
||||
.step{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.8vh;
|
||||
padding-top:1.4vh;
|
||||
border-top:1px solid currentColor;
|
||||
border-color:rgba(127,127,127,.35);
|
||||
}
|
||||
.step-nb{
|
||||
font-family:var(--serif-en);
|
||||
font-style:italic;
|
||||
font-weight:500;
|
||||
font-size:1.15vw;
|
||||
opacity:.45;
|
||||
}
|
||||
.step-title{
|
||||
font-family:var(--sans-zh);
|
||||
font-weight:700;
|
||||
font-size:1.55vw;
|
||||
letter-spacing:.01em;
|
||||
line-height:1.2;
|
||||
}
|
||||
.step-desc{
|
||||
font-family:var(--sans-zh);
|
||||
font-weight:400;
|
||||
font-size:max(12px,.95vw);
|
||||
line-height:1.45;
|
||||
opacity:.72;
|
||||
}
|
||||
|
||||
/* ---------- 网格(layouts.md 所用) ---------- */
|
||||
/* 这些类独立挂到任何容器上都能生效,不依赖 .frame 复合选择器 */
|
||||
.grid-2-7-5{display:grid;grid-template-columns:7fr 5fr;gap:3vw 4vh;align-items:start}
|
||||
.grid-2-6-6{display:grid;grid-template-columns:1fr 1fr;gap:3vw 4vh;align-items:start}
|
||||
.grid-2-8-4{display:grid;grid-template-columns:8fr 4fr;gap:3vw 4vh;align-items:start}
|
||||
.grid-3-3{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3,1fr);
|
||||
grid-auto-rows:minmax(0,1fr);
|
||||
gap:2.4vh 2vw;
|
||||
}
|
||||
/* grid-6 已在旧样式里定义为 3x2,这里仅补 align */
|
||||
|
||||
/* ---------- 图片 frame-img(layouts.md 主命名) ---------- */
|
||||
/* 在旧样式里已定义,这里补 img-cap 命名别名与增强 */
|
||||
figure.frame-img{margin:0;display:flex;flex-direction:column;min-width:0}
|
||||
.img-cap{
|
||||
display:block;
|
||||
margin-top:.8vh;
|
||||
font-family:var(--mono);
|
||||
font-size:max(10px,.8vw);
|
||||
letter-spacing:.22em;
|
||||
text-transform:uppercase;
|
||||
opacity:.6;
|
||||
}
|
||||
/* callout src 命名别名 */
|
||||
.callout-src{
|
||||
display:block;
|
||||
margin-top:1.6vh;
|
||||
font-family:var(--mono);
|
||||
font-size:11px;
|
||||
letter-spacing:.2em;
|
||||
text-transform:uppercase;
|
||||
opacity:.6;
|
||||
}
|
||||
|
||||
/* ---------- chrome & foot 补位(layouts.md 简单写法) ---------- */
|
||||
.chrome{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62}
|
||||
.foot{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5}
|
||||
|
||||
/* ---------- 响应式降级 ---------- */
|
||||
@media (max-width:900px){
|
||||
.display{font-size:16vw}
|
||||
.display-zh{font-size:12vw}
|
||||
.h1-zh{font-size:7vw}
|
||||
.h-hero{font-size:14vw}
|
||||
.h-xl{font-size:9vw}
|
||||
.pipeline{grid-template-columns:repeat(2,1fr)}
|
||||
.grid-2-7-5,.grid-2-6-6,.grid-2-8-4{grid-template-columns:1fr}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="bg-dark" class="bg"></canvas>
|
||||
<canvas id="bg-light" class="bg"></canvas>
|
||||
<div id="hint">← → 翻页 · ESC 索引</div>
|
||||
|
||||
<div id="deck">
|
||||
|
||||
<!-- ============================================================
|
||||
SLIDES 插入区 · 在此处填充所有 <section class="slide ..."> 页面
|
||||
每页模板参考 references/page-patterns.md
|
||||
页面组件参考 references/components.md
|
||||
============================================================ -->
|
||||
|
||||
<!-- SLIDES_HERE -->
|
||||
|
||||
</div>
|
||||
|
||||
<div id="nav"></div>
|
||||
|
||||
<script>
|
||||
/* =============== WebGL 双背景 ===============
|
||||
深色页:Holographic Dispersion(全息色散 · 钛金暗流)—— 彩虹微扰、鼠标径向涟漪
|
||||
浅色页:Spiral Vortex(旋转涡流 · 银色珍珠)—— domain-warp 流动、无中心
|
||||
修改风格请参考 references/webgl-backgrounds.md
|
||||
*/
|
||||
const VS = `attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}`;
|
||||
|
||||
const FS_DARK = `precision highp float;
|
||||
uniform vec2 u_resolution;uniform float u_time;uniform vec2 u_mouse;
|
||||
vec3 palette(float t,vec3 a,vec3 b,vec3 c,vec3 d){return a+b*cos(6.28318*(c*t+d));}
|
||||
void main(){
|
||||
vec2 uv=gl_FragCoord.xy/u_resolution.xy;
|
||||
vec2 p=uv*2.0-1.0;p.x*=u_resolution.x/u_resolution.y;
|
||||
vec2 m=u_mouse*2.0-1.0;m.x*=u_resolution.x/u_resolution.y;
|
||||
float md=length(p-m);
|
||||
float mr=sin(md*15.0-u_time*4.0)*exp(-md*3.0);p+=mr*0.08;
|
||||
vec2 p0=p;
|
||||
for(float i=1.0;i<4.0;i++){
|
||||
p.x+=0.1/i*sin(i*3.0*p.y+u_time*0.4)+0.05;
|
||||
p.y+=0.1/i*cos(i*2.0*p.x+u_time*0.3)-0.05;
|
||||
}
|
||||
float r=length(p);float ang=atan(p.y,p.x);
|
||||
vec3 a=vec3(0.12,0.12,0.13);
|
||||
vec3 b=vec3(0.03,0.04,0.05);
|
||||
vec3 c=vec3(1.0,1.0,1.0);
|
||||
vec3 d=vec3(0.1,0.2,0.4);
|
||||
vec3 col=palette(r*1.5+p0.x*0.5+u_time*0.1,a,b,c,d);
|
||||
float disp=sin(r*25.0-u_time*1.5+ang*2.0)*0.5+0.5;
|
||||
col+=vec3(disp*0.015,disp*0.01,disp*0.02);
|
||||
float hi=pow(sin(p.x*4.0+p.y*3.0+u_time)*0.5+0.5,8.0);
|
||||
col+=hi*0.08;
|
||||
vec3 base=vec3(0.05,0.05,0.06);
|
||||
col=mix(base,col,0.85);
|
||||
gl_FragColor=vec4(col,1.0);
|
||||
}`;
|
||||
|
||||
const FS_LIGHT = `precision highp float;
|
||||
uniform vec2 u_resolution;uniform float u_time;uniform vec2 u_mouse;
|
||||
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
|
||||
float noise(vec2 p){
|
||||
vec2 i=floor(p),f=fract(p);
|
||||
float a=hash(i),b=hash(i+vec2(1,0));
|
||||
float c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));
|
||||
vec2 u=f*f*(3.0-2.0*f);
|
||||
return mix(a,b,u.x)+(c-a)*u.y*(1.0-u.x)+(d-b)*u.x*u.y;
|
||||
}
|
||||
float fbm(vec2 p){
|
||||
float v=0.0,a=0.5;
|
||||
mat2 m=mat2(0.80,0.60,-0.60,0.80);
|
||||
for(int i=0;i<5;i++){v+=a*noise(p);p=m*p*2.02;a*=0.5;}
|
||||
return v;
|
||||
}
|
||||
void main(){
|
||||
vec2 uv=gl_FragCoord.xy/u_resolution.xy;
|
||||
vec2 p=uv;p.x*=u_resolution.x/u_resolution.y;
|
||||
vec2 m=u_mouse;m.x*=u_resolution.x/u_resolution.y;
|
||||
vec2 md=p-m;float dl=length(md);
|
||||
p+=normalize(md+vec2(0.0001))*exp(-dl*5.0)*0.03;
|
||||
vec2 q=vec2(fbm(p*1.8+u_time*0.07),fbm(p*1.8+vec2(5.2,1.3)+u_time*0.06));
|
||||
vec2 r=vec2(fbm(p*2.0+q*1.3+vec2(1.7,9.2)+u_time*0.05),
|
||||
fbm(p*2.0+q*1.3+vec2(8.3,2.8)+u_time*0.04));
|
||||
float f=fbm(p*2.2+r*1.5);
|
||||
vec3 silverDark=vec3(0.86,0.85,0.84);
|
||||
vec3 paper=vec3(0.955,0.945,0.925);
|
||||
vec3 col=mix(silverDark,paper,f);
|
||||
float ph=r.x*2.2+u_time*0.35;
|
||||
col+=vec3(0.78,0.62,0.92)*sin(ph)*0.055;
|
||||
col+=vec3(0.55,0.72,0.95)*sin(ph*0.8+2.0)*0.05;
|
||||
float hl=smoothstep(0.48,0.92,f);
|
||||
col+=hl*0.06;
|
||||
gl_FragColor=vec4(col,1.0);
|
||||
}`;
|
||||
|
||||
const mouse={x:0.5,y:0.5};
|
||||
addEventListener('mousemove',e=>{mouse.x=e.clientX/innerWidth;mouse.y=e.clientY/innerHeight});
|
||||
|
||||
function bootGL(canvasId, fsSrc){
|
||||
const canvas=document.getElementById(canvasId);
|
||||
const gl=canvas.getContext('webgl',{alpha:false,antialias:true});
|
||||
if(!gl) return ()=>false;
|
||||
const mk=(t,s)=>{const sh=gl.createShader(t);gl.shaderSource(sh,s);gl.compileShader(sh);return sh};
|
||||
const prog=gl.createProgram();
|
||||
gl.attachShader(prog,mk(gl.VERTEX_SHADER,VS));
|
||||
gl.attachShader(prog,mk(gl.FRAGMENT_SHADER,fsSrc));
|
||||
gl.linkProgram(prog);gl.useProgram(prog);
|
||||
const buf=gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER,buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);
|
||||
const pos=gl.getAttribLocation(prog,'position');
|
||||
gl.enableVertexAttribArray(pos);gl.vertexAttribPointer(pos,2,gl.FLOAT,false,0,0);
|
||||
const lRes=gl.getUniformLocation(prog,'u_resolution');
|
||||
const lT=gl.getUniformLocation(prog,'u_time');
|
||||
const lM=gl.getUniformLocation(prog,'u_mouse');
|
||||
const resize=()=>{
|
||||
const d=Math.min(window.devicePixelRatio||1,2);
|
||||
canvas.width=innerWidth*d;canvas.height=innerHeight*d;
|
||||
gl.viewport(0,0,canvas.width,canvas.height);
|
||||
};
|
||||
addEventListener('resize',resize);resize();
|
||||
return (tSec)=>{
|
||||
gl.uniform2f(lRes,canvas.width,canvas.height);
|
||||
gl.uniform1f(lT,tSec);
|
||||
gl.uniform2f(lM,mouse.x,1-mouse.y);
|
||||
gl.drawArrays(gl.TRIANGLES,0,6);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
const drawDark=bootGL('bg-dark',FS_DARK);
|
||||
const drawLight=bootGL('bg-light',FS_LIGHT);
|
||||
const t0=Date.now();
|
||||
(function loop(){
|
||||
const t=(Date.now()-t0)/1000;
|
||||
drawDark(t);drawLight(t);
|
||||
requestAnimationFrame(loop);
|
||||
})();
|
||||
|
||||
// =============== 导航(翻页 / 圆点 / 键盘 / 滚轮 / 触屏) ===============
|
||||
const deck=document.getElementById('deck');
|
||||
const slides=deck.querySelectorAll('.slide');
|
||||
const nav=document.getElementById('nav');
|
||||
let idx=0,total=slides.length,lock=false;
|
||||
|
||||
// 关键:矫正 deck 宽度为 total * 100vw,否则翻页会错位
|
||||
deck.style.width=(total*100)+'vw';
|
||||
|
||||
slides.forEach((s,i)=>{
|
||||
const b=document.createElement('button');
|
||||
b.className='dot';b.dataset.i=i;b.setAttribute('aria-label','Page '+(i+1));
|
||||
b.onclick=()=>go(i);
|
||||
nav.appendChild(b);
|
||||
});
|
||||
|
||||
function go(n){
|
||||
if(lock)return;
|
||||
idx=Math.max(0,Math.min(total-1,n));
|
||||
deck.style.transform=`translateX(${-idx*100}vw)`;
|
||||
/* load-bearing: .slide.active is read by Open Design's host bridge
|
||||
(src/runtime/srcdoc.ts findActiveByClass) to drive the slide counter.
|
||||
No CSS targets it — do not remove. */
|
||||
slides.forEach((s,i)=>s.classList.toggle('active',i===idx));
|
||||
nav.querySelectorAll('.dot').forEach((d,i)=>d.classList.toggle('active',i===idx));
|
||||
/* 主题切换:优先读 data-theme,其次从 class(light/dark)推断 */
|
||||
const el=slides[idx];
|
||||
const th=el.dataset.theme || (el.classList.contains('light')?'light':(el.classList.contains('dark')?'dark':'dark'));
|
||||
document.body.classList.toggle('light-bg',th==='light');
|
||||
lock=true;setTimeout(()=>lock=false,700);
|
||||
}
|
||||
|
||||
/* =============== ESC 索引视图 =============== */
|
||||
let overviewOn=false;
|
||||
const ov=document.createElement('div');
|
||||
ov.id='overview';
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:100;background:rgba(var(--ink-rgb),.92);backdrop-filter:blur(12px);display:none;overflow-y:auto;padding:4vh 4vw';
|
||||
document.body.appendChild(ov);
|
||||
|
||||
function buildOverview(){
|
||||
ov.innerHTML='';
|
||||
const grid=document.createElement('div');
|
||||
grid.style.cssText='display:grid;grid-template-columns:repeat(4,1fr);gap:2vh 1.6vw;max-width:90vw;margin:0 auto';
|
||||
slides.forEach((s,i)=>{
|
||||
const card=document.createElement('div');
|
||||
card.style.cssText='cursor:pointer;border-radius:6px;overflow:hidden;border:2px solid '+(i===idx?'rgba(var(--paper-rgb),.8)':'rgba(var(--paper-rgb),.15)')+';transition:border-color .2s';
|
||||
card.onmouseenter=()=>card.style.borderColor='rgba(var(--paper-rgb),.6)';
|
||||
card.onmouseleave=()=>card.style.borderColor=i===idx?'rgba(var(--paper-rgb),.8)':'rgba(var(--paper-rgb),.15)';
|
||||
const wrap=document.createElement('div');
|
||||
wrap.style.cssText='width:100%;aspect-ratio:16/9;overflow:hidden;position:relative;pointer-events:none;background:'+(s.classList.contains('light')?'var(--paper)':'var(--ink)');
|
||||
const clone=s.cloneNode(true);
|
||||
clone.style.cssText='width:100vw;height:100vh;transform:scale('+(1/4.5)+');transform-origin:top left;position:absolute;top:0;left:0;pointer-events:none';
|
||||
wrap.appendChild(clone);
|
||||
const label=document.createElement('div');
|
||||
label.style.cssText='padding:6px 10px;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--paper);opacity:.7';
|
||||
label.textContent=(i+1)+' / '+total;
|
||||
card.appendChild(wrap);
|
||||
card.appendChild(label);
|
||||
card.onclick=()=>{toggleOverview();go(i)};
|
||||
grid.appendChild(card);
|
||||
});
|
||||
ov.appendChild(grid);
|
||||
}
|
||||
|
||||
function toggleOverview(){
|
||||
overviewOn=!overviewOn;
|
||||
if(overviewOn){buildOverview();ov.style.display='block';}
|
||||
else{ov.style.display='none';}
|
||||
}
|
||||
|
||||
addEventListener('keydown',e=>{
|
||||
if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
|
||||
if(overviewOn)return;
|
||||
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown')go(idx+1);
|
||||
if(e.key==='ArrowLeft'||e.key==='PageUp'||e.key==='ArrowUp')go(idx-1);
|
||||
if(e.key==='Home')go(0);
|
||||
if(e.key==='End')go(total-1);
|
||||
});
|
||||
|
||||
let wheelTO=null,wheelAcc=0;
|
||||
addEventListener('wheel',e=>{
|
||||
wheelAcc+=e.deltaY+e.deltaX;
|
||||
if(Math.abs(wheelAcc)>50){go(idx+(wheelAcc>0?1:-1));wheelAcc=0;}
|
||||
clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
|
||||
},{passive:true});
|
||||
|
||||
let tx=0,ty=0;
|
||||
addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
|
||||
addEventListener('touchend',e=>{
|
||||
const dx=(e.changedTouches[0].clientX-tx);
|
||||
const dy=(e.changedTouches[0].clientY-ty);
|
||||
if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy))go(idx+(dx<0?1:-1));
|
||||
},{passive:true});
|
||||
|
||||
go(0);
|
||||
</script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,265 @@
|
||||
# 质量检查清单(Checklist)
|
||||
|
||||
这个清单来自"一人公司"分享 PPT 的真实迭代过程。每一条都是踩过坑之后总结的,按重要性排序。
|
||||
|
||||
生成 PPT 前,先通读一遍;生成后,逐项自检。
|
||||
|
||||
---
|
||||
|
||||
## 🔴 P0 · 一定不能犯的错
|
||||
|
||||
### 0. 生成前必须通过的类名校验(最重要)
|
||||
|
||||
**现象**:直接把 layouts.md 的骨架粘到新 HTML,结果样式全部丢失——大标题变成非衬线、数据大字报字体小得像正文、pipeline 多页糊成一坨、图片堆到浏览器底部。
|
||||
|
||||
**根因**:如果 `template.html` 的 `<style>` 里没有这些类的定义,浏览器就 fallback 到默认样式。
|
||||
|
||||
**做法**:
|
||||
- **生成 PPT 前,必须先 `Read` `assets/template.html`**,确认 layouts.md 里用到的类都已定义
|
||||
- 最常见遗漏的类:`h-hero / h-xl / h-sub / h-md / lead / meta-row / stat-card / stat-label / stat-nb / stat-unit / stat-note / pipeline-section / pipeline-label / pipeline / step / step-nb / step-title / step-desc / grid-2-7-5 / grid-2-6-6 / grid-2-8-4 / grid-3-3 / frame / img-cap / callout-src`
|
||||
- 如果某个类确实缺了,**在 template.html 的 `<style>` 里补上**,不要在每页 inline 重写
|
||||
- 生成后打开浏览器,如果看到"大标题是非衬线"或"pipeline 步骤挤在一行",几乎 100% 是这个问题
|
||||
|
||||
### 1. 不要用 emoji 作图标
|
||||
|
||||
**现象**:在中式杂志风格里用 emoji(🎯 💡 ✅)会立刻破坏格调。
|
||||
|
||||
**做法**:用 Lucide 图标库,CDN 方式引用:
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
...
|
||||
<i data-lucide="target" class="ico-md"></i>
|
||||
...
|
||||
<script>lucide.createIcons();</script>
|
||||
```
|
||||
|
||||
常用图标名:`target / palette / search-check / compass / share-2 / crown / check-circle / x-circle / plus / arrow-right / grid-2x2 / network`
|
||||
|
||||
### 2. 图片只允许裁底部,左右和顶部绝对不能切
|
||||
|
||||
**现象**:用 `aspect-ratio` 撑图,网格会在父容器不足时堆叠或切掉图片关键信息(比如截图上部的标题栏)。
|
||||
|
||||
**做法**:图片容器用**固定 height + overflow hidden**,图片走 `object-fit:cover + object-position:top`:
|
||||
|
||||
```html
|
||||
<figure class="frame-img" style="height:26vh">
|
||||
<img src="screenshot.png">
|
||||
</figure>
|
||||
```
|
||||
|
||||
CSS 里 `.frame-img img` 已经预设 `object-position:top`,只裁底。
|
||||
|
||||
**绝不用这种写法**(会在网格中撑破容器):
|
||||
|
||||
```html
|
||||
<!-- 坏例 -->
|
||||
<figure class="frame-img" style="aspect-ratio: 16/9">...</figure>
|
||||
```
|
||||
|
||||
**例外**:单张主视觉(非网格内)可以用 `aspect-ratio + max-height`,因为父容器会兜底。
|
||||
|
||||
### 2b. 亮页面配暗 WebGL = 灰蒙蒙(主题切换没生效)
|
||||
|
||||
**现象**:所有 light 页面背景都像蒙了一层灰,甚至 hero light 也灰。
|
||||
|
||||
**根因**:JS 根据 slide 的主题切换两张 canvas 的 opacity。如果整个 deck 开场是 hero dark,而没有任何机制能把 bg 切到 light,body 永远不加 `light-bg` 类,`canvas#bg-dark` 一直在上面。
|
||||
|
||||
**做法**:
|
||||
- 模板里 `go()` 函数已改为从 `classList` 推断主题(`light` / `dark`),所以 **slide 必须明确带 `light` 或 `dark` 类**。不要漏写,更不要用其他自定义主题名
|
||||
- hero 页用 `hero light` / `hero dark`,正文页用 `light` / `dark`。只写 `hero` 不带主题色是坏的
|
||||
- 一个 deck 里必须至少有一个 **非 hero 的 light 页**,确保 body 有机会加 `light-bg`
|
||||
|
||||
### 2b-2. 整个 deck 全是 light,没有节奏
|
||||
|
||||
**现象**:除封面 `hero dark` 外,其余所有页面默认写 `light`——视觉平淡,没有呼吸感,白花花一片。
|
||||
|
||||
**根因**:layouts.md 的骨架默认全写 `light`,如果只是粘贴骨架不调整主题,就会全亮。
|
||||
|
||||
**做法**:
|
||||
- **生成前画"主题节奏表"**:每一页写清 `hero dark` / `hero light` / `light` / `dark` 中的哪一个,对齐后再写代码
|
||||
- **硬规则**:连续 3 页以上同主题 = 不允许;8 页以上必须有 ≥1 `hero dark` + ≥1 `hero light`;不能全是 `light` 正文页——必须有 `dark` 正文页
|
||||
- **按布局选主题**(详见 layouts.md 开头"主题节奏规划"):
|
||||
- 左文右图(Layout 4)、大引用(Layout 8)、图文混排(Layout 10)→ **`light` / `dark` 交替**
|
||||
- 大字报、图片网格、Pipeline、对比页 → `light`(截图/数字/流程需要亮底)
|
||||
- 封面、问题页 → `hero dark`
|
||||
- 章节幕封 → `hero dark` 与 `hero light` 交替
|
||||
- **生成后自检**:`grep 'class="slide' index.html`,目视确认节奏有交错
|
||||
|
||||
### 2c. chrome 和 kicker 不要写同一句话
|
||||
|
||||
**现象**:左上角 `.chrome` 写"Design First · 设计先行",同一页里 `.kicker` 又写"Phase 01 · 设计阶段"——同义翻译,AI 味浓。
|
||||
|
||||
**做法**:
|
||||
- **chrome = 杂志页眉 / 导航标签**:跨多页可相同(如 "Act II · Workflow"、"Data · Result"、"lukew.com · 2026.04")
|
||||
- **kicker = 本页独一份的引导句**:短、有钩子、是大标题的"小前缀"(如 "BUT"、"一个人,做了什么。"、"The Question")
|
||||
- 一个描述栏目,一个描述这一页——绝不互相翻译
|
||||
|
||||
### 3. 大标题字号不能超过屏宽 / 单字数
|
||||
|
||||
**现象**:中文大标题字号设太大(比如 13vw),结果每行只容 1 个字,强制换行非常难看。
|
||||
|
||||
**做法**:
|
||||
- `h-hero`(最大):10vw,**且标题长度 ≤ 5 字**
|
||||
- `h-xl`(次大):6vw-7vw
|
||||
- 长标题用 `<br>` 手工断行,不要依赖自动换行
|
||||
- 必要时加 `white-space:nowrap`
|
||||
|
||||
**示例**:`我不是程序员。`(6 字)用 `h-xl` 7.2vw + nowrap,一行排完。
|
||||
|
||||
### 4. 字体分工:标题衬线、正文非衬线
|
||||
|
||||
**做法**:
|
||||
- 大标题、重点 quote、数字大字 → **衬线字体**(Noto Serif SC + Playfair Display + Source Serif)
|
||||
- 正文、描述、pipeline 步骤名 → **非衬线字体**(Noto Sans SC + Inter)
|
||||
- 元数据、代码、标签 → **等宽字体**(IBM Plex Mono + JetBrains Mono)
|
||||
|
||||
所有字体用 Google Fonts CDN 引入,模板里已预设。
|
||||
|
||||
### 4b. 图片不要用 `align-self:end` 贴底
|
||||
|
||||
**现象**:左文右图布局里,为了让右列图片和左列 callout 底部对齐,在 `<figure>` 上加 `align-self:end`。结果:
|
||||
- 如果父容器不是 grid(比如类名没定义),`align-self` 完全失效,图片掉到文档流最下面被浏览器底栏遮挡
|
||||
- 即使是 grid,图片会在 cell 里贴底,低分屏上仍然被 `.foot` 和 `#nav` 圆点遮挡
|
||||
|
||||
**做法**:
|
||||
- 图文混排**必须用 `.frame.grid-2-7-5`**(或 `.grid-2-6-6`/`.grid-2-8-4`)
|
||||
- 右列 `<figure class="frame-img">` 用 **标准比例 16/10 或 4/3 + max-height:56vh**,自然贴顶即可
|
||||
- 要让左列 callout 看起来"贴底",给**左列**加 flex column + `justify-content:space-between`,不要动右列
|
||||
|
||||
### 4c. 图片不要用原图奇葩比例
|
||||
|
||||
**现象**:`aspect-ratio: 2592/1798` 这种从原图复制的比例,在不同屏幕下撑出奇怪的空白或溢出。
|
||||
|
||||
**做法**:无论原图什么比例,占位器固定用标准比例 **16/10 / 4/3 / 3/2 / 1/1 / 16/9**。图片自动 `object-fit:cover + object-position:top`,顶部不裁,底部裁掉一点无伤大雅。
|
||||
|
||||
### 5. 不要给图片加厚边框 / 阴影
|
||||
|
||||
**现象**:为了"高级感"加了强阴影或黑框,瞬间变成商务 PPT。
|
||||
|
||||
**做法**:最多 1-4px 的微圆角 + **极淡的底噪**(已在模板里)。不要加 `box-shadow`,不要加 `border`(除非 1px 极淡的灰)。
|
||||
|
||||
---
|
||||
|
||||
## 🟡 P1 · 排版节奏
|
||||
|
||||
### 6. Hero 页和非 hero 页要交替
|
||||
|
||||
**推荐节奏**(25-30 页):
|
||||
```
|
||||
Hero Cover → Act Divider (hero) → 3-4 pages non-hero → Act Divider (hero)
|
||||
→ 4-5 pages non-hero → Hero Question → ... → Hero Close
|
||||
```
|
||||
|
||||
连续 2 页以上 hero 会让人疲劳,连续 4 页以上 non-hero 会让节奏死。
|
||||
|
||||
### 7. 大字报页和密集页要交替
|
||||
|
||||
大字报(big numbers / hero question)和密集页(pipeline / image grid)交替出现,听众眼睛才不累。
|
||||
|
||||
### 8. 同一概念的英文/中文用法要统一
|
||||
|
||||
**现象**:一会儿写 "Skills",一会儿写 "技能",一会儿写 "薄承载厚技能",全篇不一致。
|
||||
|
||||
**做法**:
|
||||
- 术语优先用**英文单词**(Skills / Harness / Pipeline / Workflow),这些都是圈内熟悉词
|
||||
- **别硬翻译**,硬翻译反而生硬
|
||||
- 整个 deck 里同一个词 1 个写法
|
||||
|
||||
### 9. 底部 chrome 的页码要一致
|
||||
|
||||
用 `XX / 总页数` 的格式(比如 `05 / 27`)。**不要在右上角加动态页码**(会和 `.chrome` 重复)。
|
||||
|
||||
---
|
||||
|
||||
## 🟢 P2 · 视觉打磨
|
||||
|
||||
### 10. WebGL 背景的遮罩透明度
|
||||
|
||||
**dark hero**:遮罩 12-15%(WebGL 明显透出)
|
||||
**light hero**:遮罩 16-20%(WebGL 隐约可见,不抢字)
|
||||
**普通 light/dark 页**:遮罩 92-95%(几乎不透)
|
||||
|
||||
如果页面文字非常少(hero question),遮罩可以再薄些;如果正文密集,必须加厚遮罩确保可读。
|
||||
|
||||
### 11. Light hero 的 shader 不能有强中心点
|
||||
|
||||
**现象**:Spiral Vortex、径向涟漪在 light 主题下太显眼,像 Windows 98 屏保。
|
||||
|
||||
**做法**:light hero 用 FBM 域扭曲驱动的无中心流动,底色保持银/纸色(接近 #F0F0F0 / #FBF8F3),彩虹偏色 subtle(0.05 以下)。
|
||||
|
||||
### 12. Dark hero 允许更多视觉冲击
|
||||
|
||||
Dark hero 可以用 Holographic Dispersion(钛金色散)等带中心结构的 shader,因为黑底能容纳更多视觉信息。
|
||||
|
||||
### 13. 左文右图的对齐
|
||||
|
||||
- 左列的文字组 `justify-content:space-between`:标题贴顶,引用框贴底
|
||||
- 右列图片 `align-self:end`:和左列的底部元素对齐
|
||||
- 网格整体 `align-items:start`(不是 `center` / `end`)
|
||||
|
||||
### 14. 图片的微弱圆角
|
||||
|
||||
所有 `.frame-img` 和 `.frame-img img` 都加 `border-radius:4px`,视觉上"柔和"但不软。**不要超过 8px**,否则像消费 app UI。
|
||||
|
||||
---
|
||||
|
||||
## 🔵 P3 · 操作细节
|
||||
|
||||
### 15. 图片路径用相对路径
|
||||
|
||||
图片放在 `images/` 文件夹下,HTML 里用相对路径 `images/xxx.png`,不要用绝对路径。
|
||||
|
||||
### 16. 页码在 `.chrome` 里写死
|
||||
|
||||
JS 会动态算总页数并扩展底部翻页圆点,但 `.chrome` 里的 `XX / N` 是写死的。加页/删页时要手工改 N。
|
||||
|
||||
### 17. 翻页导航要保留
|
||||
|
||||
模板默认支持:← → / 滚轮 / 触屏滑动 / 底部圆点 / Home·End。不要删 JS 里的导航逻辑。
|
||||
|
||||
### 18. 不要用 `height:100vh` 硬设,用 `min-height:80vh`
|
||||
|
||||
`100vh` 会让内容刚好卡满屏幕,但浏览器工具栏、标签栏会吃掉一部分高度,导致内容溢出。用 `min-height:80vh + align-content:center` 更稳。
|
||||
|
||||
---
|
||||
|
||||
## 🧪 最终自检清单
|
||||
|
||||
生成完 PPT 后,逐项对照这个清单(勾一下):
|
||||
|
||||
```
|
||||
预检(生成前)
|
||||
□ 已读过 template.html 的 <style>,确认所需类都存在
|
||||
□ 已决定每页用哪个 Layout(1-10)
|
||||
□ 已画出"主题节奏表":每页明确 hero dark / hero light / light / dark
|
||||
□ 节奏表满足硬规则:无连续 3 页同主题 / 有 ≥1 hero dark + ≥1 hero light(8 页以上) / 至少有 1 个 dark 正文页
|
||||
□ `<title>` 已改为实际 deck 标题(grep "[必填]" 应无结果)
|
||||
|
||||
内容
|
||||
□ 每一幕的页数比例合理(不会头重脚轻)
|
||||
□ 没有使用 emoji 作图标
|
||||
□ Skills / Harness 等术语用法统一
|
||||
□ 每页的 kicker + 标题 + 正文 三级信息清晰
|
||||
|
||||
排版
|
||||
□ 所有大标题没有出现 1 字 1 行的换行
|
||||
□ 图片网格用 height:Nvh 而非 aspect-ratio
|
||||
□ 图片只裁底部,顶部和左右完整
|
||||
□ 衬线/非衬线字体分工符合模板
|
||||
□ Pipeline 多组之间有明显分隔
|
||||
|
||||
视觉
|
||||
□ hero 页和 non-hero 页交替
|
||||
□ WebGL 背景在 hero 页可见
|
||||
□ 图片有微弱圆角
|
||||
□ 没有沉重的阴影和边框
|
||||
|
||||
交互
|
||||
□ ← → 翻页正常
|
||||
□ 底部圆点数量与总页数匹配
|
||||
□ chrome 里的页码和实际页号一致
|
||||
□ ESC 键触发索引视图(如果保留)
|
||||
```
|
||||
|
||||
全勾完,才是合格的 PPT。
|
||||
@@ -0,0 +1,363 @@
|
||||
# 组件参考 · Components
|
||||
|
||||
这是 `magazine-web-ppt` skill 的组件手册。template.html 已经定义好了所有样式,这里只写"这个组件长什么样、怎么用"。
|
||||
|
||||
## 目录
|
||||
|
||||
- [基础 Slide 外壳](#基础-slide-外壳)
|
||||
- [字体 Typography](#字体-typography)
|
||||
- [Chrome & Foot](#chrome--foot)
|
||||
- [Callout 引用框](#callout-引用框)
|
||||
- [Stat 数字矩阵](#stat-数字矩阵)
|
||||
- [Platform 平台卡](#platform-平台卡)
|
||||
- [Rowline 表格行](#rowline-表格行)
|
||||
- [Pillar 支柱卡](#pillar-支柱卡)
|
||||
- [Tag & Kicker](#tag--kicker)
|
||||
- [Figure 图片框](#figure-图片框)
|
||||
- [Icons 图标](#icons-图标)
|
||||
- [Ghost 巨型背景字](#ghost-巨型背景字)
|
||||
- [Highlight 荧光标记](#highlight-荧光标记)
|
||||
|
||||
---
|
||||
|
||||
## 基础 Slide 外壳
|
||||
|
||||
每一页都是一个 `<section class="slide ...">`。必须包含 `data-theme` 属性(`light` 或 `dark`),JS 翻页时会根据这个属性切换背景。
|
||||
|
||||
```html
|
||||
<section class="slide light" data-theme="light"> <!-- 浅色页 -->
|
||||
<section class="slide dark" data-theme="dark"> <!-- 深色页 -->
|
||||
<section class="slide light hero" data-theme="light"> <!-- Hero 页:浅色 + 薄遮罩透出 WebGL -->
|
||||
<section class="slide dark hero" data-theme="dark"> <!-- Hero 页:深色 + 薄遮罩 -->
|
||||
```
|
||||
|
||||
**light vs dark 的使用:交替使用**,每 2-3 页切换一次主题,避免连续超过 3 页同色。翻页时 WebGL 背景会自动在两个 shader 之间渐变过渡。
|
||||
|
||||
**hero 类的使用**:只给视觉主导的页面加(封面、金句页、章节过渡、结尾)。加 `hero` 后遮罩降到 12-16%,WebGL 背景会大幅透出,所以不要在 hero 页上放太多文字。
|
||||
|
||||
---
|
||||
|
||||
## 字体 Typography
|
||||
|
||||
字体分工是本模板最重要的规则,严禁混用。
|
||||
|
||||
| Class | 用途 | 字体 |
|
||||
|---|---|---|
|
||||
| `.display` | 超大号英文(Hero 页) | Playfair Display 700, 11vw |
|
||||
| `.display-zh` | 超大号中文标题 | Noto Serif SC 700, 7.8vw |
|
||||
| `.h1-zh` | 页面主标题 | Noto Serif SC 700, 4.6vw |
|
||||
| `.h2-zh` | 副标题 | Noto Serif SC 600, 3.2vw |
|
||||
| `.h3-zh` | 流水线步骤标题 | Noto Serif SC 500, 1.9vw |
|
||||
| `.lead` | 引导段(比 body 大) | Noto Serif SC 400, 1.9vw |
|
||||
| `.body-zh` | **正文/描述(非衬线)** | Noto Sans SC 400, 1.22vw |
|
||||
| `.body-serif` | 正文(衬线) | Noto Serif SC 400, 1.3vw |
|
||||
| `.kicker` | 小节提示(标题上方) | IBM Plex Mono, 12px uppercase |
|
||||
| `.meta` | 元信息标签 | IBM Plex Mono, 0.88vw uppercase |
|
||||
| `.big-num` | 巨型数字 | Playfair Display 800, 10vw |
|
||||
| `.mid-num` | 中号数字 | Playfair Display 700, 5.5vw |
|
||||
|
||||
**核心规则**:
|
||||
- **衬线**(`serif-zh` / `serif-en`):标题、重点金句、数字 —— 用于"视觉重音"
|
||||
- **非衬线**(`sans-zh`):正文描述、大段阅读内容 —— 用于"信息密度"
|
||||
- **等宽**(`mono`):kicker、meta、foot 的英文标签 —— 用于"装饰节奏"
|
||||
|
||||
**强调技巧**:
|
||||
- `<em class="en">英文词</em>` —— 把英文词渲染成 Playfair Display 斜体(很好看)
|
||||
- `<em style="opacity:.65">短语</em>` —— 让标题后半段淡出,制造节奏
|
||||
|
||||
---
|
||||
|
||||
## Chrome & Foot
|
||||
|
||||
每一页的顶部和底部的元信息条。几乎所有页都应该有。
|
||||
|
||||
```html
|
||||
<div class="chrome">
|
||||
<div class="left">
|
||||
<span>第一幕 · 硬数据</span>
|
||||
<span class="sep"></span>
|
||||
<span>Act I</span>
|
||||
</div>
|
||||
<div class="right"><span>02 / 27</span></div>
|
||||
</div>
|
||||
|
||||
<!-- ... 页面主体 ... -->
|
||||
|
||||
<div class="foot">
|
||||
<div class="title">项目名 · CodePilot | github.com/codepilot</div>
|
||||
<div>Act I · Dev Numbers</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**规则**:
|
||||
- `chrome.right` 总是放页码 `NN / TOTAL` (TOTAL 为总页数)
|
||||
- `foot.title` 是中文说明,`foot.right` 是英文 act 标记
|
||||
- chrome 和 foot 共同构成杂志感的"页眉页脚"
|
||||
|
||||
---
|
||||
|
||||
## Callout 引用框
|
||||
|
||||
展示金句 / 关键观点 / 他人引言。
|
||||
|
||||
```html
|
||||
<div class="callout" style="max-width:80vw">
|
||||
<div class="q-big">"这东西在三年前,<br>需要一个十人团队做一年。"</div>
|
||||
<span class="cite">— 一个观察者的判断</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
变体:
|
||||
- 不带 cite:去掉 `<span class="cite">` 即可
|
||||
- 带英文金句:`<em class="en">"Thin Harness, Fat Skills."</em>`
|
||||
- 在 hero 页使用:外层加 `style="position:relative;z-index:2"`(避免被背景遮罩盖住)
|
||||
|
||||
---
|
||||
|
||||
## Stat 数字矩阵
|
||||
|
||||
展示数据指标,常与 `.grid-6` / `.grid-4` 配合。
|
||||
|
||||
```html
|
||||
<div class="grid-6">
|
||||
<div class="stat">
|
||||
<span class="m">Duration</span>
|
||||
<span class="n">64<em style="font-size:.4em;opacity:.5;font-style:normal"> 天</em></span>
|
||||
<span class="l">从 0 到现在</span>
|
||||
</div>
|
||||
<!-- ... 更多 stat ... -->
|
||||
</div>
|
||||
```
|
||||
|
||||
三段式结构:`.m` 等宽小标签 → `.n` 巨型数字 → `.l` 描述说明。数字后的单位用 `<em>` 缩小到 0.4em,opacity 0.5。
|
||||
|
||||
**常用布局容器**:
|
||||
- `.grid-6` — 3×2 网格(最常用,6 个 stat)
|
||||
- `.grid-4` — 2×2 网格(4 个 stat)
|
||||
- `.grid-3` — 3 等分单行(3 个 stat / pillar)
|
||||
|
||||
---
|
||||
|
||||
## Platform 平台卡
|
||||
|
||||
展示社交平台 / 渠道 + 粉丝数。
|
||||
|
||||
```html
|
||||
<div class="plat">
|
||||
<div class="sub">Weibo</div>
|
||||
<div class="name">微博</div>
|
||||
<div class="nb">289K</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
可选第四行(补充说明):
|
||||
```html
|
||||
<div class="body-zh" style="font-size:max(11px,.8vw);opacity:.5;margin-top:.6vh">
|
||||
含小绿书同步
|
||||
</div>
|
||||
```
|
||||
|
||||
**"Also On" 变体**(补充平台):
|
||||
```html
|
||||
<div class="plat" style="border-top-style:dashed;opacity:.72">
|
||||
<div class="sub">Also On</div>
|
||||
<div class="body-zh" style="font-weight:600;margin-top:.8vh">
|
||||
B 站 · 知乎
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rowline 表格行
|
||||
|
||||
列表式内容,每行一个条目。
|
||||
|
||||
```html
|
||||
<div class="rowline">
|
||||
<div class="k">CLAUDE.md</div>
|
||||
<div class="v">你该怎么做事 —— 行为规则 + 工作偏好 + 禁止事项</div>
|
||||
<div class="m">EMPLOYEE · HANDBOOK</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
三列结构:`.k` 衬线关键词 · `.v` 正文描述 · `.m` 等宽标签(右对齐)。第一个和最后一个 rowline 自动加上下边框。
|
||||
|
||||
**变体:2 列**:`style="grid-template-columns:1fr 3fr"` 去掉 `.m` 列。
|
||||
|
||||
---
|
||||
|
||||
## Pillar 支柱卡
|
||||
|
||||
三支柱结构,常用于"概念并列"类型页面。
|
||||
|
||||
```html
|
||||
<div class="grid-3">
|
||||
<div class="pillar">
|
||||
<div class="ic">01</div>
|
||||
<div class="t">三层<br>文档体系</div>
|
||||
<div class="d">CLAUDE.md<br>+ 项目知识库<br>+ 护栏文件</div>
|
||||
</div>
|
||||
<!-- ... 更多 pillar ... -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**带图标的 pillar(用于强调性页面)**:
|
||||
```html
|
||||
<div class="pillar" style="padding:4vh 2vw;border:1px solid currentColor;border-color:rgba(10,10,11,.2)">
|
||||
<div class="ic"><i data-lucide="compass" class="ico-lg"></i></div>
|
||||
<div class="t">判断力</div>
|
||||
<div class="d">决策和方向的权威。<br>取舍、品味、方向感。</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
`.ic` 可以是序号(`01 / 02 / 03` 或 `A. / B. / C.`),也可以是 Lucide 图标。
|
||||
|
||||
---
|
||||
|
||||
## Tag & Kicker
|
||||
|
||||
**Kicker** 是标题上方的小提示文字(等宽、全大写、小字号):
|
||||
```html
|
||||
<div class="kicker">过去 64 天 · 开发篇</div>
|
||||
<div class="h1-zh">一个人,做了什么。</div>
|
||||
```
|
||||
|
||||
**Tag** 是独立的标签胶囊(带边框):
|
||||
```html
|
||||
<div style="display:flex;gap:1.6vw;flex-wrap:wrap">
|
||||
<div class="tag">早上 10 点起床</div>
|
||||
<div class="tag">周二 / 四下午健身</div>
|
||||
<div class="tag">晚上照样看剧 · 玩游戏</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Figure 图片框
|
||||
|
||||
**这是本模板最容易踩坑的组件,务必遵守以下规则**。
|
||||
|
||||
### 基础结构
|
||||
|
||||
```html
|
||||
<figure class="tile">
|
||||
<div class="frame-img" style="height:26vh">
|
||||
<img src="图片素材/xxx.png" alt="说明">
|
||||
</div>
|
||||
<figcaption class="frame-cap">
|
||||
<span class="pf">推特 · Twitter</span>
|
||||
<span class="nb">137K</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
```
|
||||
|
||||
### 关键约束(血泪经验,不要违反)
|
||||
|
||||
1. **必须用 `height:Nvh` 固定高度**,不要用 `aspect-ratio`。
|
||||
- 原因:用 aspect-ratio 在网格里会撑破父容器,导致图片堆叠。
|
||||
- 推荐尺寸:`height:18vh` (紧凑条形) / `22vh` (标准网格) / `26vh` (突出展示) / `28vh` (大图)。
|
||||
|
||||
2. **`object-position:top center`(已在 CSS 里设好)**,只允许裁掉底部。
|
||||
- 严禁裁剪左右和顶部 —— 这是图片的核心身份信息区。
|
||||
|
||||
3. **网格里多张图时,用内联 grid 而不是 `grid-3`**:
|
||||
```html
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1vh 1.2vw">
|
||||
<figure class="tile">...</figure>
|
||||
<figure class="tile">...</figure>
|
||||
<figure class="tile">...</figure>
|
||||
</div>
|
||||
```
|
||||
|
||||
4. **图片与布局其他部分对齐**:figure 单独加 `align-self:end` 让图片贴底。
|
||||
|
||||
### Frame Caption 变体
|
||||
|
||||
```html
|
||||
<!-- 标准:左 figure 名,右数字 -->
|
||||
<figcaption class="frame-cap">
|
||||
<span class="pf">推特 · Twitter</span>
|
||||
<span class="nb">137K</span>
|
||||
</figcaption>
|
||||
|
||||
<!-- 带编号 -->
|
||||
<figcaption class="frame-cap">
|
||||
<span class="idx">01</span>
|
||||
<span class="pf">AI 润色</span>
|
||||
<span>Polish</span>
|
||||
</figcaption>
|
||||
```
|
||||
|
||||
### 图片占位(设计阶段占位符)
|
||||
|
||||
图片还没有就位时,用虚线框占位:
|
||||
```html
|
||||
<div class="img-slot r-4x3"> <!-- r-4x3 / r-16x9(default) / r-3x2 / r-1x1 -->
|
||||
<span class="plus">+</span>
|
||||
<span class="label">GitHub 截图位置</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icons 图标
|
||||
|
||||
**严禁使用 emoji**。用 Lucide via CDN(template.html 已引入)。
|
||||
|
||||
```html
|
||||
<i data-lucide="compass" class="ico-lg"></i> <!-- 大图标(pillar 用) -->
|
||||
<i data-lucide="target" class="ico-md"></i> <!-- 中图标(列表项用) -->
|
||||
<i data-lucide="check-circle" class="ico-sm"></i> <!-- 小图标(inline 用) -->
|
||||
```
|
||||
|
||||
**常用 Lucide 图标名**(按含义分组):
|
||||
|
||||
- 判断类:`compass`, `target`, `crosshair`, `search-check`
|
||||
- 关系类:`share-2`, `users`, `network`, `link`, `handshake`
|
||||
- 品牌类:`crown`, `gem`, `award`, `star`, `badge-check`
|
||||
- 流程类:`workflow`, `route`, `arrow-right-left`, `repeat`
|
||||
- 数据类:`grid-2x2`, `bar-chart-3`, `trending-up`, `activity`
|
||||
- 审美类:`palette`, `brush`, `eye`, `sparkles`
|
||||
- 对错类:`check-circle`, `x-circle`, `check`, `x`
|
||||
- 方向类:`arrow-right`, `arrow-up-right`, `corner-down-right`
|
||||
|
||||
**图标与文字 inline 组合**:
|
||||
```html
|
||||
<div class="h3-zh" style="display:flex;align-items:center;gap:.8em">
|
||||
<i data-lucide="target" class="ico-md"></i>
|
||||
判断 — 什么值得写
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ghost 巨型背景字
|
||||
|
||||
用作"装饰性背景字",极低透明度,营造杂志感。
|
||||
|
||||
```html
|
||||
<div class="ghost" style="right:-6vw;top:-8vh">BUT</div>
|
||||
<div class="ghost" style="left:-8vw;bottom:-18vh;font-style:italic">Harness</div>
|
||||
```
|
||||
|
||||
- 字号 34vw,opacity 0.06
|
||||
- 常用定位:`right:-6vw;top:-8vh`(右上超出)/ `left:-8vw;bottom:-18vh`(左下超出)
|
||||
- 内容:英文单词或数字(章节序号 01/02/03、关键词 BUT/NOW/HERE)
|
||||
|
||||
**注意**:使用 ghost 的页面里,其他内容要加 `position:relative;z-index:2` 避免被压到下面。
|
||||
|
||||
---
|
||||
|
||||
## Highlight 荧光标记
|
||||
|
||||
行内短语的"荧光笔"效果:
|
||||
|
||||
```html
|
||||
<span class="hi">不是</span>
|
||||
<span class="hi">一次性爆发</span>
|
||||
```
|
||||
|
||||
在文字底部生成一条半透明高亮条。深色主题用亮条,浅色主题用暗条(CSS 已处理)。
|
||||
|
||||
**适合场景**:只对关键 1-3 个词使用,不要大面积用。
|
||||
@@ -0,0 +1,630 @@
|
||||
# 页面布局库(Layouts)
|
||||
|
||||
本文档收录 10 种最常用的页面布局骨架。每种都是一个完整可粘贴的 `<section class="slide ...">...</section>` 代码块,直接替换文案/图片即可使用。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 生成前必读(Pre-flight)
|
||||
|
||||
### A. 类名必须来自 template.html
|
||||
|
||||
layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` / `meta-row` / `stat-card` / `stat-label` / `stat-nb` / `stat-unit` / `stat-note` / `pipeline-section` / `pipeline-label` / `pipeline` / `step` / `step-nb` / `step-title` / `step-desc` / `grid-2-7-5` / `grid-2-6-6` / `grid-2-8-4` / `grid-3-3` / `grid-6` / `grid-3` / `grid-4` / `frame` / `frame-img` / `img-cap` / `callout` / `callout-src` / `kicker`)都在 `assets/template.html` 的 `<style>` 块里预定义。
|
||||
|
||||
**不要发明新类名**。如果必须自定义,用 `style="..."` inline 写。生成前若不确定某个类是否存在,grep template.html 确认。
|
||||
|
||||
### B. 图片比例规范(非常重要)
|
||||
|
||||
**永远用标准比例**,不要用原图 `aspect-ratio: 2592/1798` 这种奇葩比例:
|
||||
|
||||
| 场景 | 推荐比例 | 写法 |
|
||||
|------|---------|------|
|
||||
| 左文右图 主图 | 16:10 或 4:3 | `aspect-ratio:16/10; max-height:54vh` |
|
||||
| 图片网格(多图对比) | 统一 | **固定 `height:26vh`,不用 aspect-ratio** |
|
||||
| 左小图 + 右文字 | 1:1 或 3:2 | `aspect-ratio:1/1; max-width:40vw` |
|
||||
| 全屏主视觉 | 16:9 | `aspect-ratio:16/9; max-height:64vh` |
|
||||
| 图文混排小插图 | 3:2 | `aspect-ratio:3/2; max-width:30vw` |
|
||||
|
||||
图片必须包在 `<figure class="frame-img">` 里,里面的 `<img>` 会自动 `object-fit:cover + object-position:top center`,只裁底部,不裁顶/左/右。
|
||||
|
||||
### C. 图片定位准则(避免图片堆到页面最底部、被浏览器工具栏遮挡)
|
||||
|
||||
**错误做法**(已踩坑,不要再犯):
|
||||
- 在非 grid 容器里用 `align-self:end`:`align-self` 在 flex/grid 之外完全无效,图片会掉到文档流末尾堆底
|
||||
- 用 `position:absolute + bottom:0` 把图"固定"到底:会被底部 `.foot` 和 `#nav` 圆点遮挡
|
||||
- 单张图片只写 `height:N vh` 不限 `max-height`:在低分屏会撑出视口
|
||||
|
||||
**正确做法**:
|
||||
- 图文混排**必须用 `.frame.grid-2-7-5`**(或 `.grid-2-6-6` / `.grid-2-8-4`)的 grid 结构
|
||||
- grid 容器默认 `align-items:start`(已在 template 中设置),图片自然贴到 cell 顶端
|
||||
- 如果需要"图片底对齐左列 callout":**左列用 flex column + `justify-content:space-between`**(让 callout 自己贴左列底),**右列 figure 直接保持 align-items:start 即可**,不要加 `align-self:end`
|
||||
- 所有 grid 父容器建议加 inline `style="padding-top:6vh"`,给标题区留呼吸空间
|
||||
|
||||
### D. 主题色与主题节奏
|
||||
|
||||
- 主题色从 `references/themes.md` 的 5 套预设里选一套,不允许自定义 hex 值
|
||||
- 主题节奏(每页用 light / dark / hero light / hero dark 哪一个)在下文"主题节奏规划"一节有硬规则,生成前必读
|
||||
- 两件事都要在挑布局之前决定,避免返工
|
||||
|
||||
---
|
||||
|
||||
## 0. 基础结构(所有 slide 都一样)
|
||||
|
||||
```html
|
||||
<section class="slide [light|dark|hero light|hero dark]">
|
||||
<div class="chrome">
|
||||
<div>上下文标签 · 子标签</div>
|
||||
<div>ACT · 页号 / 总页数</div>
|
||||
</div>
|
||||
<!-- 主内容 -->
|
||||
<div class="foot">
|
||||
<div>页码说明 · Page Description</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
- 非 hero 页建议加 `light` 或 `dark` 主题;hero 页加 `hero light` 或 `hero dark`(参与 WebGL 主题插值)
|
||||
- `chrome` 和 `foot` 是可选但推荐保留的上下左右四角元数据
|
||||
- **hero 页用于章节封面/开场/收束/转场**,非 hero 页用于正文
|
||||
|
||||
### ⚠️ chrome 和 kicker 不要写同一句话
|
||||
|
||||
这是最常见的内容重复问题。两者在语义上完全不同的维度:
|
||||
|
||||
| 位置 | 角色 | 内容性质 | 例子 |
|
||||
|------|------|---------|------|
|
||||
| `.chrome` 左上 | **杂志页眉 / 导航元数据** | 稳定的"栏目名"或"章节分类",跨多页可以相同 | "Act II · Workflow" / "Data · Result" / "lukew.com · 2026.04" |
|
||||
| `.chrome` 右上 | **页号 + 幕号** | 固定格式 | "Act II · 15 / 25" |
|
||||
| `.kicker` | **这一页独一份的引导句** | 是大标题的"小前缀",像杂志大标题上方的一行话,每页都应不同 | "BUT" / "一个人,做了什么。" / "Phase 01 · 设计阶段" |
|
||||
|
||||
**反例**(已踩坑):chrome 写"设计先行 · Design First",kicker 又写"Phase 01 · 设计阶段"——意思重复,读者一眼就觉得 AI 生成的。
|
||||
|
||||
**正确做法**:chrome 是**栏目标签**(稳定、跨页可复用),kicker 是**本页钩子**(短句、有戏剧性),两者互为补充,不互相翻译。
|
||||
|
||||
### ⚠️ 主题节奏规划(必读 · 生成前必做)
|
||||
|
||||
**核心机制**:每页 `<section>` 必须带 `light` / `dark` / `hero light` / `hero dark` 之一。JS 根据 class 推断主题,决定 body 加不加 `light-bg`,从而切换暗/亮两张 WebGL canvas 哪张在前。不带主题或写自定义名 = fallback 出错。
|
||||
|
||||
#### 按布局的主题默认值
|
||||
|
||||
| Layout | 默认主题 | 原因 |
|
||||
|---|---|---|
|
||||
| 1. 开场封面 | `hero dark` | 开场仪式感,暗底强冲击 |
|
||||
| 2. 章节幕封 | `hero dark` 与 `hero light` **必须交替** | 呼吸节奏 |
|
||||
| 3. 大字报(数据) | `light` | 数字需纸白底;多幕连发时可偶插 `dark` |
|
||||
| 4. 左文右图 | **`light` / `dark` 交替** | 正文节奏主力 |
|
||||
| 5. 图片网格 | `light` | 截图需亮底 |
|
||||
| 6. Pipeline | `light` | 流程图需清晰 |
|
||||
| 7. 问题页 | `hero dark` | 强视觉冲击默认 |
|
||||
| 8. 大引用 | **`dark` 优先**,偶用 `light` | 金句仪式感靠暗底 |
|
||||
| 9. 对比页 | `light` | 双列需清晰 |
|
||||
| 10. 图文混排 | **`light` / `dark` 交替** | 节奏 |
|
||||
|
||||
#### 节奏硬规则(生成后 grep 自检)
|
||||
|
||||
- ❌ **禁止**连续 3 页以上相同主题(包括 light 堆叠和 dark 堆叠)
|
||||
- ❌ **禁止**8 页以上的 deck 没有至少 1 个 `hero dark` + 1 个 `hero light`
|
||||
- ❌ **禁止**整个 deck 只有 `light` 正文页没有任何 `dark` 正文页——会显得平淡、没呼吸
|
||||
- ✅ **推荐**每 3-4 页插入 1 个 hero(封面/幕封/问题/大引用)
|
||||
|
||||
#### 8 页节奏模板(可直接套用)
|
||||
|
||||
| 页 | 主题 | 布局 | 备注 |
|
||||
|---|---|---|---|
|
||||
| 1 | `hero dark` | 封面 | 开场 |
|
||||
| 2 | `light` | 大字报 | 数据抛出 |
|
||||
| 3 | `dark` | 左文右图 | 对比/故事 |
|
||||
| 4 | `light` | Pipeline | 流程 |
|
||||
| 5 | `hero light` | 章节幕封 | 呼吸 |
|
||||
| 6 | `dark` | 左文右图 or 大引用 | |
|
||||
| 7 | `hero dark` | 问题页 | 悬念收束 |
|
||||
| 8 | `light` | 大引用/结尾 | 收尾 |
|
||||
|
||||
**先画这张表对齐,再动手写 slide**。跳过规划直接粘骨架 = 全是 light。
|
||||
|
||||
---
|
||||
|
||||
## Layout 1: 开场封面(Hero Cover)
|
||||
|
||||
```html
|
||||
<section class="slide hero dark">
|
||||
<div class="chrome">
|
||||
<div>A Talk · 2026.04.22</div>
|
||||
<div>Vol.01</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:4vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">私享会 · 李继刚</div>
|
||||
<h1 class="h-hero">一人公司</h1>
|
||||
<h2 class="h-sub">被 AI 折叠的组织</h2>
|
||||
<p class="lead" style="max-width:60vw">
|
||||
一个 AI 创作者 —— 在 64 天里做了 11 万行代码、在 9 个平台上持续输出,生活节奏几乎没有被改变。
|
||||
</p>
|
||||
<div class="meta-row">
|
||||
<span>歸藏 Guizang</span><span>·</span><span>独立创作者 / CodePilot 作者</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>一场关于 AI · 组织 · 个体的分享</div>
|
||||
<div>— 2026 —</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 用 `hero dark` 让 WebGL 背景在大部分区域透出
|
||||
- `h-hero` 是最大字号(10vw),这里作标题主视觉
|
||||
- 用 `min-height:80vh + align-content:center` 让内容整体垂直居中
|
||||
- 不需要 `.chrome` 里写页码,封面页自成一体
|
||||
|
||||
---
|
||||
|
||||
## Layout 2: 章节幕封(Act Divider)
|
||||
|
||||
```html
|
||||
<section class="slide hero light">
|
||||
<div class="chrome">
|
||||
<div>第一幕 · 硬数据</div>
|
||||
<div>Act I · 01 / 25</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:6vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">Act I</div>
|
||||
<h1 class="h-hero" style="font-size:8.5vw">硬数据</h1>
|
||||
<p class="lead" style="max-width:55vw">
|
||||
先看数字,再谈方法。
|
||||
</p>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>第一幕引子</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 极简,只需要 kicker + 大标题 + 一行引语
|
||||
- 两个幕的封面可以交替 `hero light` / `hero dark`,制造节奏
|
||||
- `h-hero` 字号可以从 10vw 调到 8.5vw 适配长短
|
||||
|
||||
---
|
||||
|
||||
## Layout 3: 数据大字报(Big Numbers Grid)
|
||||
|
||||
```html
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>过去 64 天 · 开发篇</div>
|
||||
<div>Act I / Dev · 02 / 25</div>
|
||||
</div>
|
||||
<div class="frame" style="padding-top:6vh">
|
||||
<div class="kicker">一个人,做了什么。</div>
|
||||
<h2 class="h-xl">过去 64 天</h2>
|
||||
<p class="lead" style="margin-bottom:5vh">从 0 到开源 CodePilot。</p>
|
||||
|
||||
<div class="grid-6" style="margin-top:6vh">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Duration</div>
|
||||
<div class="stat-nb">64 <span class="stat-unit">天</span></div>
|
||||
<div class="stat-note">从 0 到现在</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Lines of Code</div>
|
||||
<div class="stat-nb">110K+</div>
|
||||
<div class="stat-note">一行行写到 11 万+</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">GitHub Stars</div>
|
||||
<div class="stat-nb">5,166</div>
|
||||
<div class="stat-note">一个开源仓库</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Downloads</div>
|
||||
<div class="stat-nb">41K+</div>
|
||||
<div class="stat-note">装到了几万台电脑里</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">AI Providers</div>
|
||||
<div class="stat-nb">19</div>
|
||||
<div class="stat-note">跨平台接入</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Commits</div>
|
||||
<div class="stat-nb">608+</div>
|
||||
<div class="stat-note">没有协作者</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>项目 · CodePilot | github.com/codepilot</div>
|
||||
<div>Act I · Dev Numbers</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 3×2 或 4×2 网格最稳(见 `.grid-6`)
|
||||
- 每个 `stat-card` 结构固定:label(英文小字)→ nb(大字数字)→ note(注释)
|
||||
- 数字建议 2-3 位字符(太长会溢出),用 K / M 简写
|
||||
- 留 5vh 以上的上方缓冲,让标题区先抢眼球
|
||||
|
||||
---
|
||||
|
||||
## Layout 4: 左文右图(Quote + Image)
|
||||
|
||||
```html
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>身份反差 · The Twist</div>
|
||||
<div>03 / 25</div>
|
||||
</div>
|
||||
<div class="frame grid-2-7-5" style="padding-top:6vh">
|
||||
<!-- 左列:标题 + 正文 + callout,flex column 让 callout 贴列底 -->
|
||||
<div style="display:flex; flex-direction:column; justify-content:space-between; gap:3vh">
|
||||
<div>
|
||||
<div class="kicker">BUT</div>
|
||||
<h2 class="h-xl" style="white-space:nowrap; font-size:7.2vw">
|
||||
我不是程序员。
|
||||
</h2>
|
||||
<p class="lead" style="margin-top:3vh">
|
||||
大学毕业之后再也没写过一行代码。过去十年做的是 UI 设计和 AI 特效。
|
||||
</p>
|
||||
</div>
|
||||
<div class="callout">
|
||||
"这东西在三年前,<br>
|
||||
需要一个十人团队做一年。"
|
||||
<div class="callout-src">— 一个观察者的判断</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右列:图片用标准 16/10 比例 + max-height,不要 align-self:end -->
|
||||
<figure class="frame-img" style="aspect-ratio:16/10; max-height:56vh">
|
||||
<img src="images/codepilot.png" alt="CodePilot 产品截图">
|
||||
<figcaption class="img-cap">CodePilot · 产品截图</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 03 · 我不是程序员</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 用 `grid-2-7-5`(左 7 份、右 5 份),`align-items:start` 已在 template 预设
|
||||
- **左列**用 flex column + `justify-content:space-between`:标题贴顶,callout 自然贴底
|
||||
- **右列图片** **不要加 `align-self:end`**。会让图片滑到 cell 底部,低分屏下被浏览器工具栏遮挡
|
||||
- 图片必须用 **标准比例 16/10 或 4/3 + `max-height:56vh`**,不要用原图奇葩比例(`2592/1798` 这种)
|
||||
|
||||
---
|
||||
|
||||
## Layout 5: 图片网格(多图对比)
|
||||
|
||||
```html
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>平台粉丝实证</div>
|
||||
<div>Act I / Ops · 05 / 27</div>
|
||||
</div>
|
||||
<div class="frame" style="padding-top:5vh">
|
||||
<div class="kicker">Proof · 粉丝实证</div>
|
||||
<h2 class="h-xl">10 个平台 · 6 张截图</h2>
|
||||
|
||||
<div class="grid-3-3" style="margin-top:4vh">
|
||||
<figure class="frame-img" style="height:26vh">
|
||||
<img src="images/weibo.png" alt="微博 289K">
|
||||
<figcaption class="img-cap">微博 · 289K</figcaption>
|
||||
</figure>
|
||||
<figure class="frame-img" style="height:26vh">
|
||||
<img src="images/twitter.png" alt="推特 137K">
|
||||
<figcaption class="img-cap">推特 · 137K</figcaption>
|
||||
</figure>
|
||||
<figure class="frame-img" style="height:26vh">
|
||||
<img src="images/wechat.png" alt="公众号 96K">
|
||||
<figcaption class="img-cap">公众号 · 96K</figcaption>
|
||||
</figure>
|
||||
<figure class="frame-img" style="height:26vh">
|
||||
<img src="images/jike.png" alt="即刻 26K">
|
||||
<figcaption class="img-cap">即刻 · 26K</figcaption>
|
||||
</figure>
|
||||
<figure class="frame-img" style="height:26vh">
|
||||
<img src="images/xhs.png" alt="小红书 19K">
|
||||
<figcaption class="img-cap">小红书 · 19K</figcaption>
|
||||
</figure>
|
||||
<figure class="frame-img" style="height:26vh">
|
||||
<img src="images/douyin.png" alt="抖音 10K">
|
||||
<figcaption class="img-cap">抖音 · 10K</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>截图时间 · 2026.04</div>
|
||||
<div>Page 05 · 粉丝实证</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 关键:每个 `frame-img` 必须写死 `height:NNvh`(不要用 `aspect-ratio`),否则网格会撑破
|
||||
- 图片会自动 `object-fit:cover + object-position:top`,只裁底部
|
||||
- 用 `.grid-3-3`(3×2)或 `.grid-3`(3×1)承载
|
||||
|
||||
---
|
||||
|
||||
## Layout 6: 两列流水线(Pipeline)
|
||||
|
||||
```html
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>我的工作流 · Workflow</div>
|
||||
<div>Act II · 15 / 27</div>
|
||||
</div>
|
||||
<div class="frame">
|
||||
<div class="kicker">Pipeline · 流水线</div>
|
||||
<h2 class="h-xl">两条流水线</h2>
|
||||
|
||||
<!-- 第一组:文本侧 -->
|
||||
<div class="pipeline-section">
|
||||
<div class="pipeline-label">文本侧 · Text Pipeline</div>
|
||||
<div class="pipeline">
|
||||
<div class="step">
|
||||
<div class="step-nb">01</div>
|
||||
<div class="step-title">Draft</div>
|
||||
<div class="step-desc">AI 帮我起草初稿</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">02</div>
|
||||
<div class="step-title">Polish</div>
|
||||
<div class="step-desc">AI 润色去 AI 味</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">03</div>
|
||||
<div class="step-title">Morph</div>
|
||||
<div class="step-desc">AI 变形成推特 / 小红书</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">04</div>
|
||||
<div class="step-title">Illustrate</div>
|
||||
<div class="step-desc">AI 生成信息图</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">05</div>
|
||||
<div class="step-title">Distribute</div>
|
||||
<div class="step-desc">一键分发 9 平台</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二组:视频侧 -->
|
||||
<div class="pipeline-section">
|
||||
<div class="pipeline-label">视觉 · 视频侧 · Video Pipeline</div>
|
||||
<div class="pipeline">
|
||||
<div class="step">
|
||||
<div class="step-nb">06</div>
|
||||
<div class="step-title">Cut</div>
|
||||
<div class="step-desc">AI 帮我剪辑</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">07</div>
|
||||
<div class="step-title">Wrap</div>
|
||||
<div class="step-desc">AI 帮我包装</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-nb">08</div>
|
||||
<div class="step-title">Cover</div>
|
||||
<div class="step-desc">AI 生成封面</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 15 · 我的内容工厂</div>
|
||||
<div>Workflow</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 用 `.pipeline-section` 分组 + `.pipeline-label` 作组标题
|
||||
- 两组之间用 3.6vh 的间距 + 顶部细分隔线(已在 CSS 中预设)
|
||||
- 每个 step 是固定的 nb → title → desc 结构
|
||||
- 步骤数不限但单行最好 ≤5 个,否则换到第二 pipeline
|
||||
|
||||
---
|
||||
|
||||
## Layout 7: 悬念收束 / 问题页(Hero Question)
|
||||
|
||||
```html
|
||||
<section class="slide hero dark">
|
||||
<div class="chrome">
|
||||
<div>留给你的问题</div>
|
||||
<div>24 / 27</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:8vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">The Question</div>
|
||||
<h1 class="h-hero" style="font-size:7vw; line-height:1.15">
|
||||
你的公司里,<br>
|
||||
哪些岗位本来就<br>
|
||||
不该由人来做?
|
||||
</h1>
|
||||
<p class="lead" style="max-width:50vw">
|
||||
这个问题,不是技术问题,是架构问题。
|
||||
</p>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 24 · The Question</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- Hero 页留白越多越好,只放一个问题
|
||||
- `h-hero` 字号视长度调整(7vw 适合 3 行,10vw 适合 1 行)
|
||||
- 用 `<br>` 手工断行,确保断点在语义处
|
||||
- 尾巴可以再给一行 `lead` 作为点破
|
||||
|
||||
---
|
||||
|
||||
## Layout 8: 大引用页(Big Quote · 衬线金句)
|
||||
|
||||
```html
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>The Takeaway · 核心金句</div>
|
||||
<div>18 / 25</div>
|
||||
</div>
|
||||
<div class="frame" style="display:grid; gap:5vh; align-content:center; min-height:80vh">
|
||||
<div class="kicker">Quote · 金句</div>
|
||||
<blockquote style="font-family:var(--serif-zh); font-weight:700; font-size:5.8vw; line-height:1.2; letter-spacing:-.01em; max-width:72vw">
|
||||
"没有交接,<br>所有人都在构建。"
|
||||
</blockquote>
|
||||
<p class="lead" style="max-width:55vw; opacity:.65">
|
||||
Without the handoff, everyone builds.<br>
|
||||
And that makes all the difference.
|
||||
</p>
|
||||
<div class="meta-row">
|
||||
<span>— Luke Wroblewski</span><span>·</span><span>2026.04.16</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 18 · 金句</div>
|
||||
<div>— · —</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 整页留白,只放一个大引用 + 出处
|
||||
- `<blockquote>` 用 inline style 单独放大(5-6vw),不要用 `h-hero`(那是页面主标题的命名)
|
||||
- 下面跟随英文原文(lead · opacity:.65)制造层级
|
||||
- 配 `meta-row` 写出处 · 日期
|
||||
|
||||
---
|
||||
|
||||
## Layout 9: 并列对比(A vs B · 旧 vs 新)
|
||||
|
||||
```html
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>旧 vs 新 · The Shift</div>
|
||||
<div>12 / 25</div>
|
||||
</div>
|
||||
<div class="frame" style="padding-top:5vh">
|
||||
<div class="kicker">Before / After · 范式转变</div>
|
||||
<h2 class="h-xl" style="margin-bottom:4vh">从交接到共建</h2>
|
||||
|
||||
<div class="grid-2-6-6" style="gap:5vw 4vh">
|
||||
<!-- 左列:旧 -->
|
||||
<div style="padding:3vh 2vw; border-left:3px solid currentColor; opacity:.55">
|
||||
<div class="kicker" style="opacity:.9">Before · 旧模式</div>
|
||||
<h3 class="h-md" style="margin-top:2vh">设计 → 开发 → 交接</h3>
|
||||
<ul style="margin-top:3vh; padding-left:1.2em; display:flex; flex-direction:column; gap:1.4vh; font-family:var(--sans-zh); font-size:max(14px,1.1vw); line-height:1.55">
|
||||
<li>设计师在 Figma 做稿</li>
|
||||
<li>开发者盯着文件翻译像素</li>
|
||||
<li>反复 PR 沟通对齐</li>
|
||||
<li>非技术人员无法触碰代码</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 右列:新 -->
|
||||
<div style="padding:3vh 2vw; border-left:3px solid currentColor">
|
||||
<div class="kicker" style="opacity:.9">After · 新模式</div>
|
||||
<h3 class="h-md" style="margin-top:2vh">同工具 · 并行 · 共建</h3>
|
||||
<ul style="margin-top:3vh; padding-left:1.2em; display:flex; flex-direction:column; gap:1.4vh; font-family:var(--sans-zh); font-size:max(14px,1.1vw); line-height:1.55">
|
||||
<li>三个角色同时在 Intent 工作</li>
|
||||
<li>agents.md 作为共享上下文</li>
|
||||
<li>代理处理对齐 / 冲突 / 动画</li>
|
||||
<li>任何人都能安全贡献代码</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 12 · 范式转变</div>
|
||||
<div>Before / After</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 用 `.grid-2-6-6`(1:1)左右分半
|
||||
- 左列 `opacity:.55` 做"旧"的视觉弱化,右列满亮度做"新"的突出
|
||||
- 两列都用 `border-left:3px solid` + `padding-left` 做引用块感
|
||||
- 每列结构统一:`kicker` → `h-md` → `<ul>` 要点,节奏一致
|
||||
|
||||
---
|
||||
|
||||
## Layout 10: 图文混排(Lead Image + Side Text)
|
||||
|
||||
```html
|
||||
<section class="slide light">
|
||||
<div class="chrome">
|
||||
<div>Design First · 设计先行</div>
|
||||
<div>08 / 16</div>
|
||||
</div>
|
||||
<div class="frame grid-2-8-4" style="padding-top:6vh">
|
||||
<!-- 左列:大段正文 + 引用 -->
|
||||
<div>
|
||||
<div class="kicker">Phase 01 · 设计阶段</div>
|
||||
<h2 class="h-xl" style="margin-top:1vh; margin-bottom:3vh">设计先行 · 2 周</h2>
|
||||
|
||||
<p class="lead" style="margin-bottom:3vh">
|
||||
在 Figma 中完成视觉探索与设计系统,网格 / 排版 / 颜色变量 / 可复用组件,桌面和移动端稿件几轮反馈迭代。
|
||||
</p>
|
||||
|
||||
<p style="font-family:var(--sans-zh); font-size:max(14px,1.15vw); line-height:1.75; opacity:.78; margin-bottom:2.4vh">
|
||||
两周之内,视觉风格、粗略结构、方向性内容全部稳定。这是扎实的传统设计流程——在这里还没什么新鲜事。
|
||||
</p>
|
||||
|
||||
<div class="callout" style="margin-top:3vh">
|
||||
"This phase was pretty standard.<br>Just a solid Web design process."
|
||||
<div class="callout-src">— Luke Wroblewski</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右列:辅助图 · 竖版或方形 -->
|
||||
<figure class="frame-img" style="aspect-ratio:3/4; max-height:60vh">
|
||||
<img src="images/figma.png" alt="Figma design system">
|
||||
<figcaption class="img-cap">Figma · Design System</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div>Page 08 · Design First</div>
|
||||
<div>约 2 周</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- `.grid-2-8-4`(8:4) 让正文占主导,图片作辅助
|
||||
- 左列包含多种信息层级:kicker → 大标题 → lead → 正文段落 → callout(引用)
|
||||
- 右列图片用 **竖版 3:4** 或方形 1:1,避免和左列文本竞争注意力
|
||||
- 这种布局适合**页面信息量偏大**的场景(不像 Layout 4 只有一句金句)
|
||||
|
||||
---
|
||||
|
||||
## 附录:常用网格模板
|
||||
|
||||
| 类名 | 配比 | 用途 |
|
||||
|---|---|---|
|
||||
| `.grid-2-6-6` | 6:6(1:1) | 对半分 |
|
||||
| `.grid-2-7-5` | 7:5 | 文字为主 + 辅助图 |
|
||||
| `.grid-2-8-4` | 8:4(2:1) | 大段文字 + 小图/数据 |
|
||||
| `.grid-3` | 1:1:1 | 3 项并列(案例/截图) |
|
||||
| `.grid-3-3` | 3×2 | 6 图矩阵 |
|
||||
| `.grid-6` | 3×2 | 6 个数据卡片 |
|
||||
|
||||
所有网格都预留 `gap: 3vw 4vh`(水平 3vw、竖直 4vh),可以单独覆写。
|
||||
|
||||
---
|
||||
|
||||
## 页面节奏建议
|
||||
|
||||
一场 25-30 页的分享,推荐以下节奏:
|
||||
|
||||
1. **Hero Cover**(第 1 页)
|
||||
2. **Act Divider**(第一幕开场,hero light 或 hero dark)
|
||||
3. **Big Numbers**(抛硬数据制造冲击)
|
||||
4. **Quote + Image**(讲身份反差/挂钩)
|
||||
5. **Image Grid**(证据支撑)
|
||||
6. **Hero Question**(幕收束,留悬念)
|
||||
7. ... 第二幕、第三幕同样节奏 ...
|
||||
8. **Hero Close**(最后一页,问题或致谢)
|
||||
|
||||
hero 页与 non-hero 页应该 **2-3 : 1 比例交错**,不要连续超过 3 页 non-hero,也不要连续超过 2 页 hero。
|
||||
@@ -0,0 +1,195 @@
|
||||
# 杂志风方向(Magazine Directions)
|
||||
|
||||
5 个**预设方向**,每个方向都把"用哪套主题色 / 哪些 layout / 多少 slide / 怎么写 chrome 文案"打包好,避免你在 6 问澄清里给出 5 个不相关的选项。
|
||||
|
||||
> 灵感来源:[alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design) 的 "20 design philosophies × 5 streams" — 我们把它压缩到 5 个 magazine-flavored 的方向,每个都对应到 `themes.md` 的某一套 + `layouts.md` 的某些组合。
|
||||
|
||||
---
|
||||
|
||||
## 何时用这份文档
|
||||
|
||||
在 SKILL.md `Step 1 · 需求澄清` 的开头:**先让用户在这 5 个方向里挑一个**,再去问主题色 / 时长 / 受众 / 大纲。流程是:
|
||||
|
||||
```
|
||||
1. 用户讲一句"想做个分享 PPT"
|
||||
2. 你(agent)介绍 5 个方向(拷贝下面的 1-line summary)
|
||||
3. 用户挑一个方向(或说"不知道, 你推荐")
|
||||
4. 你按所选方向回答了"主题色"和"slide 数量"两个问题, 再问剩下的 4 个
|
||||
```
|
||||
|
||||
**硬规则**:方向只能从下面 5 个里选,不能混搭。混搭 = 走 huashu-design 验证过的失败路径(品牌资产协议 v1)。如果用户对 5 个都不满意,委婉劝他选最接近的,然后允许在 `chrome` / `kicker` 里轻微定制语气,**绝不调色**。
|
||||
|
||||
---
|
||||
|
||||
## 1. Monocle Editorial · 国际杂志风 ✦ 默认推荐
|
||||
|
||||
**关键词**:克制、知识感、跨国、有 *taste*
|
||||
|
||||
| 配方 | 选择 |
|
||||
|---|---|
|
||||
| 主题色 | 🖋 墨水经典 |
|
||||
| 推荐 slide 数 | 18–24 页(60% non-hero / 40% hero) |
|
||||
| 主力 layouts | **1 封面 / 2 章节幕 / 4 左文右图 / 8 大引用 / 10 图文混排** |
|
||||
| Chrome 文案 | `Vol.04 · Spring 2026` / `Act II · 12 / 24` / `lukew.com · 2026.04` |
|
||||
| Kicker 风格 | 短英文 + 中点:`THE TWIST` / `BUT` / `DEC.` |
|
||||
| Foot 文案 | `Page 12 · 一种新的工作方式` |
|
||||
|
||||
**适合**:商业发布、行业内部讲话、产品宣发、个人品牌沉淀分享。**默认就选这个**,跑不出大错。
|
||||
|
||||
**反例**:技术深度报告(密度太低),表格数据很多的 ops 复盘(没有合适的 layout)。
|
||||
|
||||
**视觉锚点**:*Monocle* / *Apricot Magazine* / *A Book Apart* / *Apartamento*。
|
||||
|
||||
---
|
||||
|
||||
## 2. WIRED Tech · 数据 + 工程
|
||||
|
||||
**关键词**:硬数据、流水线、对比、未来感
|
||||
|
||||
| 配方 | 选择 |
|
||||
|---|---|
|
||||
| 主题色 | 🌊 靛蓝瓷 |
|
||||
| 推荐 slide 数 | 14–18 页(轻巧、数据密) |
|
||||
| 主力 layouts | **1 封面 / 3 数据大字报 / 6 Pipeline / 7 问题页 / 9 Before/After** |
|
||||
| Chrome 文案 | `Q2 / 2026 · Field Report` / `Data · 03` / `Eng Notes` |
|
||||
| Kicker 风格 | 全大写 + 数字:`38× FASTER` / `RUNTIME 04` / `CASE 02` |
|
||||
| Foot 文案 | `Page 03 · benchmark` / `methodology footnote` |
|
||||
|
||||
**适合**:技术发布会、研究分享、benchmark 报告、工程团队对内沟通、AI 产品 demo day。
|
||||
|
||||
**反例**:人文类金句分享(太冷)、艺术品牌(不够温度)。
|
||||
|
||||
**视觉锚点**:*WIRED* 长文版 / *MIT Technology Review* / *The Pudding* / *Stripe Press*。
|
||||
|
||||
**特殊建议**:每个 stat-card 的 `stat-label` 用英文等宽(这是 WIRED 风的核心),数字别加千分位逗号(不够工程),用 `K` / `M` / `×` 简写。
|
||||
|
||||
---
|
||||
|
||||
## 3. Kinfolk Slow · 慢生活 / 人文
|
||||
|
||||
**关键词**:留白、衬线、温度、私享会
|
||||
|
||||
| 配方 | 选择 |
|
||||
|---|---|
|
||||
| 主题色 | 🍂 牛皮纸 |
|
||||
| 推荐 slide 数 | 9–12 页(慢、放空、低密度) |
|
||||
| 主力 layouts | **1 封面 / 4 左文右图 / 8 大引用 / 10 图文混排 / 2 章节幕** |
|
||||
| Chrome 文案 | `Vol.07 · Autumn` / `一封信 · 03` / `Notes from Kyoto` |
|
||||
| Kicker 风格 | 中文短语 + 标点:"给一个朋友。" / "晚秋。" / "Letter Three" |
|
||||
| Foot 文案 | `Page 03 · Letter Three` / `2026 · Spring Issue` |
|
||||
|
||||
**适合**:私享会、读书分享、人物访谈复盘、生活方式品牌、个人随笔。
|
||||
|
||||
**反例**:产品发布(太慢)、技术分享(太软)、严肃数据(信息密度不够)。
|
||||
|
||||
**视觉锚点**:*Kinfolk* / *The Gentlewoman* / *Cereal* / *Drift Magazine*。
|
||||
|
||||
**特殊建议**:
|
||||
- **故意把 slide 数压到 10 页以下**——Kinfolk 的核心是"少即是多",不要塞满
|
||||
- 大量使用 Layout 8(大引用)和 Layout 10(图文混排)
|
||||
- 不要用 Layout 3(数据大字报)——和气质冲突
|
||||
- `<title>` 文字、章节名、kicker 全部用衬线 + 中文短句
|
||||
|
||||
---
|
||||
|
||||
## 4. Domus Architectural · 建筑 / 空间感
|
||||
|
||||
**关键词**:尺度、几何、不对称、克制的炫耀
|
||||
|
||||
| 配方 | 选择 |
|
||||
|---|---|
|
||||
| 主题色 | 🌙 沙丘 |
|
||||
| 推荐 slide 数 | 12–18 页(中密度,强视觉) |
|
||||
| 主力 layouts | **1 封面 / 2 章节幕 / 5 图片网格 / 9 Before/After / 10 图文混排** |
|
||||
| Chrome 文案 | `Spazio 09 · Project File` / `Plan · 03` / `Fig.4` |
|
||||
| Kicker 风格 | 数字 + 类别:`PROJECT 04` / `SECTION B` / `FIGURE 12` |
|
||||
| Foot 文案 | `Page 09 · West Wing` / `1:200 scale` |
|
||||
|
||||
**适合**:设计 / 建筑案例分享、产品设计 review、品牌视觉发布、画廊式 portfolio 展示。
|
||||
|
||||
**反例**:金句分享(太硬)、技术 deep dive(不擅长流水线)。
|
||||
|
||||
**视觉锚点**:*Domus* / *Apartamento* / *Mark Magazine* / *Pin-Up*。
|
||||
|
||||
**特殊建议**:
|
||||
- **每个 hero 页都要"留 60% 空"** — 不要塞满,建筑感来自呼吸
|
||||
- 大量使用 Layout 5(图片网格)但**只放 4 张大图**,不要放 6 张小图
|
||||
- `chrome` 文案保持冷峻,全用英文 + 数字
|
||||
|
||||
---
|
||||
|
||||
## 5. Lab / Reference · 学术 + 工艺手册
|
||||
|
||||
**关键词**:克制、有图有表、可复现、工程师爱看
|
||||
|
||||
| 配方 | 选择 |
|
||||
|---|---|
|
||||
| 主题色 | 🌿 森林墨 |
|
||||
| 推荐 slide 数 | 16–24 页(密度高、有图表) |
|
||||
| 主力 layouts | **1 封面 / 2 章节幕 / 3 数据大字报 / 6 Pipeline / 9 Before/After** |
|
||||
| Chrome 文案 | `Field Notes · Vol.II` / `Section 3.2 · Method` / `Reference 04` |
|
||||
| Kicker 风格 | 编号:`§ 3.2` / `Ref. 04` / `Method 01` |
|
||||
| Foot 文案 | `Page 12 · 3.2 Calibration` / `appendix A` |
|
||||
|
||||
**适合**:学术分享、内部研究复盘、可持续 / 自然主题、长期产品复盘、有方法论的工艺型分享(咖啡 / 香水 / 茶)。
|
||||
|
||||
**反例**:商业发布(太冷静)、营销活动(不够 catchy)。
|
||||
|
||||
**视觉锚点**:*National Geographic*(旧版)/ *Hand-Eye Magazine* / *Nautilus* / *MIT Press* book layouts。
|
||||
|
||||
**特殊建议**:
|
||||
- 大量 `meta-row` 标注来源、方法、引用
|
||||
- 比其他方向**更频繁地用 `<figcaption class="img-cap">`** 给每张图标编号
|
||||
- `kicker` 用 § 章节编号,不用感叹句
|
||||
|
||||
---
|
||||
|
||||
## 推荐速查(如果用户描述了一个意图,你应该选哪个)
|
||||
|
||||
| 用户说的话 | 推荐方向 |
|
||||
|---|---|
|
||||
| "通用分享" / "不知道选啥" | **1. Monocle** |
|
||||
| "一人公司 / AI 折叠 / 创业 demo day" | **1. Monocle**(默认)或 **2. WIRED**(如果偏技术) |
|
||||
| "AI / benchmark / 模型评测" | **2. WIRED** |
|
||||
| "产品发布会 / 工程团队分享" | **2. WIRED** |
|
||||
| "读书分享 / 人物访谈 / 一个人的故事" | **3. Kinfolk** |
|
||||
| "私享会 / 朋友间分享 / 周末闲聊式" | **3. Kinfolk** |
|
||||
| "设计案例 / 品牌发布 / portfolio 展示" | **4. Domus** |
|
||||
| "建筑 / 空间 / 装置" | **4. Domus** |
|
||||
| "学术 / 研究 / 方法论 / 教程" | **5. Lab** |
|
||||
| "可持续 / 环保 / 自然主题" | **5. Lab** |
|
||||
|
||||
---
|
||||
|
||||
## 决策记录(生成前必做)
|
||||
|
||||
挑完方向后,**在项目目录下生成或更新 `项目记录.md`**(或 `大纲-v1.md`),第一行写清:
|
||||
|
||||
```markdown
|
||||
# [演讲标题] · 项目记录
|
||||
|
||||
- 方向(Direction):**Monocle Editorial** (from `references/styles.md`)
|
||||
- 主题色(Theme):🖋 墨水经典
|
||||
- 受众:内部团队(产品 + 设计)
|
||||
- 时长:25 min · 约 18 slides
|
||||
- Chrome 风格:Vol.04 / Act II / 12 of 18
|
||||
- Kicker 风格:短英文 + 中点
|
||||
```
|
||||
|
||||
后续迭代每次调整方向都更新这一节。**不要中途换方向**——5 个方向之间的"语气"差异比想象的大,混着写就会撕裂。
|
||||
|
||||
---
|
||||
|
||||
## ❌ 不要做的事
|
||||
|
||||
- ❌ 把 5 个方向的 layout 选择混着用(例如 Monocle 配 Layout 6 Pipeline 多页 + Kinfolk 风的 chrome)—— 杂乱
|
||||
- ❌ 自己造第 6 个方向("我想做'科技 + 文艺'风")—— 委婉劝他选最近的,告诉他混搭历史失败率超高
|
||||
- ❌ 中途换方向,例如做到第 8 页突然觉得"换 Kinfolk 更好"——前 7 页就废了,要么全推倒重来,要么坚持原方向到底
|
||||
- ❌ 在不属于该方向的 layout 上花时间(例如 Kinfolk 写 4 页 Layout 6 Pipeline)—— 信号是用错方向了
|
||||
|
||||
## ✅ 应当做的事
|
||||
|
||||
- ✅ 只在 5 个方向里挑,挑完用方向去回答其他 5 个澄清问题
|
||||
- ✅ 在 `项目记录.md` 第一行明确方向,全程不变
|
||||
- ✅ 让 chrome / kicker / foot 三个文字位为方向"代言"——它们承担了一半的方向辨识度
|
||||
- ✅ 如果不确定,**默认选 Monocle Editorial**——它是 5 个方向里失败概率最低的兜底
|
||||
@@ -0,0 +1,122 @@
|
||||
# 主题色预设(Themes)
|
||||
|
||||
5 套精心调配的主题色板,保证"电子杂志 × 电子墨水"的美学不垮。**不允许用户自定义颜色——色彩搭配错了画面瞬间变丑**,只从以下预设中挑选。
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 问用户选哪套(或基于内容推荐一套)
|
||||
2. 打开 `assets/template.html` 的 `<style>` 块
|
||||
3. 找到开头的 `:root{` 块
|
||||
4. **整体替换**标有"主题色"注释的那几行 `--ink` / `--ink-rgb` / `--paper` / `--paper-rgb` / `--paper-tint` / `--ink-tint`
|
||||
5. 其他 CSS 都走 `var(--...)`,无需任何其他改动
|
||||
|
||||
---
|
||||
|
||||
## 🖋 墨水经典 (Monocle 默认)
|
||||
|
||||
**适合**:通用分享、商业发布、科技产品、任何场景都安全的默认选择。
|
||||
**调性**:纯墨黑 + 暖米白,杂志感最强,Monocle / Apricot / A Book Apart 风。
|
||||
|
||||
```css
|
||||
--ink:#0a0a0b;
|
||||
--ink-rgb:10,10,11;
|
||||
--paper:#f1efea;
|
||||
--paper-rgb:241,239,234;
|
||||
--paper-tint:#e8e5de;
|
||||
--ink-tint:#18181a;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌊 靛蓝瓷 (Indigo Porcelain)
|
||||
|
||||
**适合**:科技/研究/数据分享、工程师文化、深度内容、技术发布会。
|
||||
**调性**:深靛蓝 + 瓷白,冷静、理性、有深度,像学术期刊或蓝印花瓷器。
|
||||
|
||||
```css
|
||||
--ink:#0a1f3d;
|
||||
--ink-rgb:10,31,61;
|
||||
--paper:#f1f3f5;
|
||||
--paper-rgb:241,243,245;
|
||||
--paper-tint:#e4e8ec;
|
||||
--ink-tint:#152a4a;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌿 森林墨 (Forest Ink)
|
||||
|
||||
**适合**:自然/可持续/文化/非虚构内容、户外品牌、环保主题。
|
||||
**调性**:深森林绿 + 象牙,沉稳、有呼吸感,像旧版《国家地理》。
|
||||
|
||||
```css
|
||||
--ink:#1a2e1f;
|
||||
--ink-rgb:26,46,31;
|
||||
--paper:#f5f1e8;
|
||||
--paper-rgb:245,241,232;
|
||||
--paper-tint:#ece7da;
|
||||
--ink-tint:#253d2c;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🍂 牛皮纸 (Kraft Paper)
|
||||
|
||||
**适合**:怀旧/人文/阅读/历史/文学分享、独立杂志、手作品牌。
|
||||
**调性**:深棕 + 暖米,像牛皮信封或老笔记本,温暖、有年代感。
|
||||
|
||||
```css
|
||||
--ink:#2a1e13;
|
||||
--ink-rgb:42,30,19;
|
||||
--paper:#eedfc7;
|
||||
--paper-rgb:238,223,199;
|
||||
--paper-tint:#e0d0b6;
|
||||
--ink-tint:#3a2a1d;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌙 沙丘 (Dune)
|
||||
|
||||
**适合**:艺术/设计/创意/时尚分享、画廊手册、审美优先的私享会。
|
||||
**调性**:炭灰 + 沙色,克制、高级、中性,像沙漠黄昏或建筑设计图册。
|
||||
|
||||
```css
|
||||
--ink:#1f1a14;
|
||||
--ink-rgb:31,26,20;
|
||||
--paper:#f0e6d2;
|
||||
--paper-rgb:240,230,210;
|
||||
--paper-tint:#e3d7bf;
|
||||
--ink-tint:#2d2620;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 推荐选择参考
|
||||
|
||||
| 如果是... | 推荐主题 |
|
||||
|---|---|
|
||||
| 不知道选啥 / 第一次用 | 🖋 墨水经典 |
|
||||
| AI / 技术 / 产品发布 | 🌊 靛蓝瓷 |
|
||||
| 内容 / 行业观察 / 文化 | 🌿 森林墨 |
|
||||
| 书评 / 生活方式 / 人文 | 🍂 牛皮纸 |
|
||||
| 设计 / 艺术 / 品牌 | 🌙 沙丘 |
|
||||
|
||||
---
|
||||
|
||||
## 切换原则
|
||||
|
||||
- **一份 deck 只用一套主题**,不要中途换色
|
||||
- WebGL shader 的默认主色(钛金色散 / 银色流动)适配所有 5 套(经测试可接受)
|
||||
- `currentColor` 驱动的 border / icon 会跟随 section 的 text color 自动适配,无需额外调整
|
||||
- 选定主题后,`<title>` 文字和 `chrome` 文案可以强化该主题的语义(例如牛皮纸配"Vol.03 · 秋"这种)
|
||||
|
||||
## ❌ 不要做的事
|
||||
|
||||
- ❌ **不允许混搭**(例如 ink 取墨水经典的,paper 取沙丘的)——会彻底违和
|
||||
- ❌ **不允许用户随便给一个 hex 值**——需委婉拒绝并展示 5 套预设让选
|
||||
- ❌ **不要直接修改 template.html 其他地方的颜色**——所有散落 rgba 都走 var,改 :root 一处即可
|
||||
|
||||
选定主题后在 skill 对话中告诉用户:"用 🖋 墨水经典 / 🌊 靛蓝瓷 ..."并在 deck 项目记录里备注,方便后续迭代时保持一致。
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf of
|
||||
any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don\'t include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,56 @@
|
||||
# hatch-pet (vendored)
|
||||
|
||||
This directory is a **vendored copy** of the Codex `hatch-pet` skill. It is
|
||||
checked into the Open Design repo (rather than pulled in as a Git submodule
|
||||
or an npm package) so that:
|
||||
|
||||
- Any Open Design agent can run the skill end-to-end without a network
|
||||
fetch, an extra install step, or an out-of-tree clone.
|
||||
- The packaged desktop build can ship the skill as inert static assets
|
||||
alongside the rest of `skills/`.
|
||||
- Reviews of changes that touch pet generation can see the skill source in
|
||||
the same diff as the daemon / web wiring that consumes it.
|
||||
|
||||
The vendoring trade-off is: this copy will not auto-track upstream
|
||||
revisions. If the upstream skill changes (atlas geometry, manifest shape,
|
||||
script CLIs), this copy must be re-synced by hand. Treat it as a frozen
|
||||
snapshot, not a live dependency.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Skill: `hatch-pet`
|
||||
- Pinned upstream reference (declared in `SKILL.md` frontmatter): see the
|
||||
`upstream:` field — at vendoring time this pointed to the Codex curated
|
||||
`skills/.curated/hatch-pet` tree. That URL was not publicly resolvable
|
||||
at the time this README was written; treat the vendored snapshot in this
|
||||
directory as the authoritative source-of-truth for Open Design and
|
||||
re-confirm the upstream pointer the next time a re-sync is performed.
|
||||
- License: Apache License 2.0 (`LICENSE.txt` next to this README). The
|
||||
copyright line in the bundled `LICENSE.txt` is left unfilled because no
|
||||
separate copyright holder was identified at vendoring time. If a future
|
||||
re-sync confirms the upstream copyright holder, populate the standard
|
||||
Apache `Copyright [yyyy] [name of copyright owner]` line and add a
|
||||
`NOTICE` file mirroring upstream attribution.
|
||||
|
||||
## Re-syncing this skill
|
||||
|
||||
When the upstream skill changes:
|
||||
|
||||
1. Locate the upstream source (Codex `skills/.curated/hatch-pet` or the
|
||||
superseding location).
|
||||
2. Replace the contents of this directory with the upstream snapshot,
|
||||
preserving only this `README.md` and any Open-Design-specific notes
|
||||
inside `SKILL.md`'s `> **Open Design integration.**` blockquote.
|
||||
3. Update the `upstream:` field in `SKILL.md` frontmatter with the exact
|
||||
commit SHA / tag of the snapshot.
|
||||
4. Update `LICENSE.txt` and add a `NOTICE` file if upstream now ships
|
||||
attribution metadata.
|
||||
|
||||
## Where outputs land
|
||||
|
||||
The skill packages each pet under
|
||||
`${CODEX_HOME:-$HOME/.codex}/pets/<pet-id>/` with `pet.json` and
|
||||
`spritesheet.{webp,png,gif}`. The daemon scans that directory in
|
||||
`apps/daemon/src/codex-pets.ts`; the web pet settings list and one-click
|
||||
adopt pets from there. See `docs/codex-pets.md` for the end-user setup
|
||||
flow (including how Open Design behaves when Codex is not installed).
|
||||
@@ -0,0 +1,355 @@
|
||||
---
|
||||
name: hatch-pet
|
||||
description: Create, repair, validate, preview, and package Codex-compatible animated pet spritesheets from character art, screenshots, generated images, or visual references. Use when a user wants to hatch a Codex pet, create a custom animated pet, or build a built-in pet asset with an 8x9 atlas, transparent unused cells, row-by-row animation prompts, QA contact sheets, preview videos, and pet.json packaging. This skill composes the installed $imagegen system skill for visual generation and uses bundled scripts for deterministic spritesheet assembly.
|
||||
triggers:
|
||||
- "hatch a pet"
|
||||
- "hatch pet"
|
||||
- "codex pet"
|
||||
- "spritesheet pet"
|
||||
- "animated pet"
|
||||
- "孵化宠物"
|
||||
- "电子宠物"
|
||||
od:
|
||||
mode: image
|
||||
surface: image
|
||||
scenario: personal
|
||||
featured: 11
|
||||
preview:
|
||||
type: image
|
||||
entry: final/spritesheet.png
|
||||
design_system:
|
||||
requires: false
|
||||
outputs:
|
||||
primary: final/spritesheet.png
|
||||
secondary:
|
||||
- final/spritesheet.webp
|
||||
- pet.json
|
||||
- qa/contact-sheet.png
|
||||
example_prompt: "Hatch me a tiny pixel-art shiba pet — friendly, sitting upright, with a small pomegranate prop. Use the hatch-pet skill end-to-end."
|
||||
upstream: "https://github.com/openai/skills/tree/main/skills/.curated/hatch-pet"
|
||||
---
|
||||
|
||||
# Hatch Pet
|
||||
|
||||
> **Open Design integration.** This is the unmodified Codex `hatch-pet` skill,
|
||||
> vendored under `skills/hatch-pet/` so any Open Design agent can run it. After
|
||||
> the skill finishes packaging, the resulting `spritesheet.webp` (under
|
||||
> `${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>/`) can be imported into the
|
||||
> floating pet companion via **Settings → Pets → Import Codex sprite**. The
|
||||
> import flow auto-detects the 8×9 / `192×208` atlas and lets the user pick
|
||||
> which animation row to play (idle, running-right, waving, …).
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
Create a Codex-compatible animated pet from a concept, one or more reference images, or both. This skill owns pet-specific prompt planning, animation rows, frame extraction, atlas geometry, QA, previews, and packaging. It delegates visual generation to `$imagegen`.
|
||||
|
||||
User-facing inputs are optional. If the user omits a pet name, infer one from the concept or reference filenames; if that is not possible, choose a short appropriate name. If the user omits a description, infer one from the concept or references. If the user omits reference images, generate the base pet from text first, then use that base as the canonical reference for every animation row.
|
||||
|
||||
## Generation Delegation
|
||||
|
||||
Use `$imagegen` for all normal visual generation.
|
||||
|
||||
Before generating base art, row strips, or repair rows, load and follow the installed image generation skill:
|
||||
|
||||
```text
|
||||
${CODEX_HOME:-$HOME/.codex}/skills/.system/imagegen/SKILL.md
|
||||
```
|
||||
|
||||
Do not call the Image API directly for the normal path. Let `$imagegen` choose its own built-in-first path and its own CLI fallback rules. If `$imagegen` says a fallback requires confirmation, ask the user before continuing.
|
||||
|
||||
When invoking `$imagegen` from this skill, pass the generated pet prompt as the authoritative visual spec. Do not wrap it in the generic `$imagegen` shared prompt schema and do not add extra polish, hero-art, photo, product, or illustration-style augmentation. Pet prompts should stay terse, sprite-specific, and digital-pet oriented; only add role labels for input images and any essential user constraint.
|
||||
|
||||
Use this skill's scripts for deterministic work only: preparing prompts and manifests, ingesting selected `$imagegen` outputs, extracting frames, validating rows, composing the final atlas, creating QA media, and packaging.
|
||||
|
||||
Hard boundary: do not create, draw, tile, warp, mirror, or synthesize pet visuals with local Python/Pillow scripts, SVG, canvas, HTML/CSS, or other code-native art as a substitute for `$imagegen`. For a normal pet run, expect up to 10 visual generation jobs: 1 base pet plus 9 row-strip jobs. The only exception is `running-left`, which may be derived by mirroring `running-right` only after `running-right` has been generated, visually inspected, and explicitly approved as safe to mirror. If mirroring is not appropriate, generate `running-left` as a normal grounded `$imagegen` row. If those calls are too expensive, blocked, or unavailable, stop and explain the blocker instead of fabricating row strips locally.
|
||||
|
||||
Do not mark visual jobs complete by editing `imagegen-jobs.json`, copying files into `decoded/`, or writing helper scripts that populate row outputs. Use `record_imagegen_result.py` for selected built-in `$imagegen` outputs, or `generate_pet_images.py` only for the documented secondary fallback. The deterministic scripts may only process already-generated visual outputs.
|
||||
|
||||
Only the base job may be prompt-only. Every row-strip job generated through `$imagegen` must use the input images listed in `imagegen-jobs.json`, including the canonical base reference created after the base job is recorded. Treat any row generation without attached grounding images as invalid.
|
||||
|
||||
## Codex Digital Pet Style
|
||||
|
||||
Default pet art should match the Codex app's built-in digital pets: small pixel-art-adjacent mascots with compact chibi proportions, chunky readable silhouettes, thick dark 1-2 px outlines, visible stepped/pixel edges, limited palettes, flat cel shading, simple expressive faces, and tiny limbs. Even if the reference art is more detailed, complex or realistic, the generated pet should be simplified into this style.
|
||||
|
||||
Do NOT generate polished illustration, painterly rendering, anime key art, 3D rendering, glossy app-icon treatment, realistic fur or material texture, soft gradients, high-detail antialiasing, and complex tiny accessories. References that are more detailed than this should be simplified into the house style before row generation.
|
||||
|
||||
## Transparency And Effects
|
||||
|
||||
Pet rows are processed into transparent 192x208 cells, so every generated pixel must either belong to the pet sprite or be cleanly removable chroma-key background. Prefer pose, expression, and silhouette changes over decorative effects.
|
||||
|
||||
Allowed effects must satisfy all of these conditions:
|
||||
|
||||
- The effect is state-relevant and helps explain the animation.
|
||||
- The effect is physically attached to, touching, or overlapping the pet silhouette, not floating nearby.
|
||||
- The effect is inside the same frame slot as the pet and does not create a separate sprite component.
|
||||
- The effect is opaque, hard-edged, pixel-style, and uses non-chroma-key colors.
|
||||
- The effect is small enough to remain readable at 192x208 without clutter.
|
||||
|
||||
Examples of allowed effects: a tear touching the face, a small smoke puff touching the box or head, or tiny stars overlapping the pet during a failed/dizzy reaction.
|
||||
|
||||
Avoid these by default because they usually break transparent-background cleanup or component extraction:
|
||||
|
||||
- wave marks, motion arcs, speed lines, action streaks, afterimages, blur, or smears
|
||||
- detached stars, loose sparkles, floating punctuation, floating icons, falling tear drops, separated smoke clouds, or loose dust
|
||||
- cast shadows, contact shadows, drop shadows, oval floor shadows, floor patches, landing marks, impact bursts, glow, halo, aura, or soft transparent effects
|
||||
- text, labels, frame numbers, visible grids, guide marks, speech bubbles, thought bubbles, UI panels, code snippets, checkerboard transparency, white backgrounds, black backgrounds, or scenery
|
||||
- chroma-key-adjacent colors in the pet, prop, effects, highlights, or shadows
|
||||
- stray pixels, disconnected outline bits, speckle/noise, cropped body parts, overlapping poses, or any pose that crosses into a neighboring frame slot
|
||||
|
||||
State-specific guidance:
|
||||
|
||||
- `waving`: show the wave through paw pose only. Do not draw wave marks, motion arcs, lines, sparkles, or symbols around the paw.
|
||||
- `jumping`: show vertical motion through body position only. Do not draw shadows, dust, landing marks, impact bursts, bounce pads, or floor cues.
|
||||
- `failed`: tears, attached smoke puffs, or attached stars are allowed if they obey the allowed-effects rules; do not use red X marks, floating symbols, detached smoke, detached stars, or separate tear droplets.
|
||||
- `review`: show focus through lean, blink, eyes, head tilt, or paw position. Do not add magnifying glasses, papers, code, UI, punctuation, or symbols unless that prop already exists in the base pet identity.
|
||||
- `running-right`, `running-left`, and `running`: show locomotion through body, limb, and prop movement only. Do not draw speed lines, dust clouds, floor shadows, or motion trails.
|
||||
|
||||
## Pet Naming
|
||||
|
||||
Ask the user for a pet name when they have not provided one and only if the conversation naturally allows it. If asking would slow down a direct execution request, choose a short appropriate name from the pet concept, reference image, or personality, then use that name consistently as the display name and as the source for the package folder slug.
|
||||
|
||||
Good built-in style examples:
|
||||
|
||||
- Codex - The original Codex companion.
|
||||
- Dewey - A tidy duck for calm workspace days.
|
||||
- Fireball - Hot path energy for fast iteration.
|
||||
- Rocky - A steady rock when the diff gets large.
|
||||
- Seedy - Small green shoots for new ideas.
|
||||
- Stacky - A balanced stack for deep work.
|
||||
- BSOD - A tiny blue-screen gremlin.
|
||||
- Null Signal - Quiet signal from the void.
|
||||
|
||||
## Visible Progress Plan
|
||||
|
||||
For every pet run, keep a visible checklist so the user can see where the work is up to. Create the checklist before starting, keep one step active at a time, and update it as each step finishes.
|
||||
|
||||
Before creating the checklist, establish the pet name when possible. Use the user-provided name when available; otherwise infer a short appropriate name from the concept or references. If the name is too long, not settled, or not appropriate for a friendly checklist, use `your pet` instead.
|
||||
|
||||
Use this checklist for a normal pet run, replacing `<Pet>` with the pet's name or `your pet`:
|
||||
|
||||
1. Getting `<Pet>` ready.
|
||||
2. Imagining `<Pet>`'s main look.
|
||||
3. Picturing `<Pet>`'s poses.
|
||||
4. Hatching `<Pet>`.
|
||||
|
||||
What each step means:
|
||||
|
||||
- `Getting <Pet> ready.` Choose or confirm the pet name, description, source images, and working folder.
|
||||
- `Imagining <Pet>'s main look.` Generate the pet's main reference image. This is required for new pets, even when the user does not provide an image, because it becomes the visual source of truth.
|
||||
- `Picturing <Pet>'s poses.` Create the pose rows, starting with `idle` and `running-right` to confirm the pet still looks consistent. Only mirror `running-left` if `running-right` clearly works when flipped.
|
||||
- `Hatching <Pet>.` Turn the approved poses into the final pet files, review the contact sheet, previews, and validation results, fix any broken parts, save `pet.json` and `spritesheet.webp` into the pet folder, then tell the user where the pet and QA files were saved.
|
||||
|
||||
Only mark a step complete when the real file, image, or decision exists. If this is just a repair run, start from the first relevant step instead of restarting the whole checklist.
|
||||
|
||||
## Default Workflow
|
||||
|
||||
1. Prepare a pet run folder and imagegen job manifest:
|
||||
|
||||
```bash
|
||||
SKILL_DIR="${CODEX_HOME:-$HOME/.codex}/skills/hatch-pet"
|
||||
python "$SKILL_DIR/scripts/prepare_pet_run.py" \
|
||||
--pet-name "<Name>" \
|
||||
--description "<one sentence>" \
|
||||
--reference /absolute/path/to/reference.png \
|
||||
--output-dir /absolute/path/to/run \
|
||||
--pet-notes "<stable pet description>" \
|
||||
--style-notes "<style notes>" \
|
||||
--force
|
||||
```
|
||||
|
||||
All arguments above are optional except any flags needed to express user constraints. For text-only requests, pass the concept through `--pet-notes` and omit `--reference`; `prepare_pet_run.py` will infer a name, description, chroma key, and output directory as needed.
|
||||
|
||||
2. Inspect the next ready `$imagegen` jobs:
|
||||
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/pet_job_status.py" --run-dir /absolute/path/to/run
|
||||
```
|
||||
|
||||
3. For each ready job, invoke `$imagegen` with:
|
||||
|
||||
- the prompt file listed in `imagegen-jobs.json`
|
||||
- every input image listed for the job, with its role label
|
||||
- the default built-in `image_gen` path unless `$imagegen` itself routes otherwise
|
||||
|
||||
The base job must complete first. If user references exist, the base job uses them. If no references exist, the base job may be prompt-only. After recording the base, `record_imagegen_result.py` writes `decoded/base.png` and `references/canonical-base.png`; all row jobs use the original references if present plus those canonical base images.
|
||||
|
||||
`prepare_pet_run.py` also creates 9 row-specific layout guide images under `references/layout-guides/`, one per animation state. Row jobs attach the matching guide as a layout-only input so the model can follow the correct frame count, spacing, centering, and safe padding. Treat these guides as invisible construction references: the generated row strip must not include visible boxes, borders, center marks, labels, guide colors, or the guide background.
|
||||
|
||||
When generating row strips, keep the identity lock in the row prompt authoritative: do not redesign the pet, and preserve the same head shape, face, markings, palette, prop, outline weight, body proportions, and silhouette. A row that looks like a related but different pet is failed even if the deterministic geometry QA passes.
|
||||
|
||||
Generate and record `running-right` before deciding how to complete `running-left`. Inspect `running-right` against the base and references. If the pet is visually symmetric enough that a horizontal mirror preserves identity, prop placement, handedness, markings, lighting, text-free details, and direction semantics, derive `running-left` with:
|
||||
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/derive_running_left_from_running_right.py" \
|
||||
--run-dir /absolute/path/to/run \
|
||||
--confirm-appropriate-mirror \
|
||||
--decision-note "<why mirroring preserves this pet's identity>"
|
||||
```
|
||||
|
||||
If there is any asymmetric side-specific marking, readable text, non-mirrored logo, handed prop, one-sided accessory, lighting cue, or direction-specific pose that would become wrong when flipped, do not mirror. Generate `running-left` with `$imagegen` using its row prompt and all listed grounding images, including `decoded/running-right.png` as a gait reference.
|
||||
|
||||
For the built-in path, record the selected source image from `$CODEX_HOME/generated_images/.../ig_*.png`. Do not record files from the run directory, `tmp/`, hand-made fixtures, deterministic row folders, or post-processed copies as visual job sources.
|
||||
|
||||
4. After selecting a generated output for a job, ingest it:
|
||||
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/record_imagegen_result.py" \
|
||||
--run-dir /absolute/path/to/run \
|
||||
--job-id <job-id> \
|
||||
--source /absolute/path/to/generated-output.png
|
||||
```
|
||||
|
||||
This copies the image to the exact decoded path expected by the deterministic pipeline and records source metadata in `imagegen-jobs.json`.
|
||||
|
||||
5. When all jobs are complete, finalize:
|
||||
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/finalize_pet_run.py" \
|
||||
--run-dir /absolute/path/to/run
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
run/
|
||||
pet_request.json
|
||||
imagegen-jobs.json
|
||||
prompts/
|
||||
decoded/
|
||||
frames/frames-manifest.json
|
||||
final/spritesheet.png
|
||||
final/spritesheet.webp
|
||||
final/validation.json
|
||||
qa/contact-sheet.png
|
||||
qa/review.json
|
||||
qa/run-summary.json
|
||||
qa/videos/*.mp4
|
||||
```
|
||||
|
||||
Package output is written outside the run directory by default. If `CODEX_HOME` is set, use it; otherwise use `$HOME/.codex`.
|
||||
|
||||
```text
|
||||
${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>/
|
||||
pet.json
|
||||
spritesheet.webp
|
||||
```
|
||||
|
||||
Review `qa/contact-sheet.png`, `qa/review.json`, `final/validation.json`, and `qa/videos/` before accepting the pet.
|
||||
|
||||
Deterministic validation is necessary but not sufficient. Before calling the pet done, visually inspect the contact sheet for identity consistency. Block acceptance if any row changes species/body type, face, markings, palette, prop design, prop side unexpectedly, or overall silhouette.
|
||||
|
||||
## Subagent Row Generation
|
||||
|
||||
After the base job has been recorded and `references/canonical-base.png` exists, row-strip visual generation must use subagents unless the user explicitly says not to use subagents for this session. Before row generation, state that subagents are being used and which row jobs are being delegated. If subagents cannot be spawned because the current environment or tool policy blocks them, stop before row-strip generation, explain the blocker, and ask for explicit user direction before continuing sequentially.
|
||||
|
||||
The parent agent must own the manifest and package writes.
|
||||
|
||||
Default flow:
|
||||
|
||||
1. Parent runs `prepare_pet_run.py`.
|
||||
2. Parent generates and records `base`.
|
||||
3. Parent runs `pet_job_status.py`.
|
||||
4. Parent spawns subagents for `idle` and `running-right` first as identity and gait checks.
|
||||
5. Parent records the selected `idle` and `running-right` results returned by subagents.
|
||||
6. Parent decides whether `running-left` is safe to derive by mirror; if not, parent treats it as a normal grounded row job delegated to a subagent.
|
||||
7. Parent spawns subagents for every remaining non-derived row image-generation job.
|
||||
8. Each subagent receives the row prompt and every listed input image path, invokes `$imagegen`, and returns only the selected `$CODEX_HOME/generated_images/.../ig_*.png` source path.
|
||||
9. Parent alone runs `record_imagegen_result.py`, `derive_running_left_from_running_right.py`, repair queueing, finalization, QA, and packaging.
|
||||
|
||||
Subagent write boundary: do not let subagents edit `imagegen-jobs.json`, copy files into `decoded/`, run `record_imagegen_result.py`, run `derive_running_left_from_running_right.py`, run `finalize_pet_run.py`, or package the pet. This avoids manifest races and keeps provenance checks centralized.
|
||||
|
||||
Subagent handoff contract:
|
||||
|
||||
- Give each subagent exactly one row job unless you are intentionally batching adjacent simple rows.
|
||||
- Include the row id, the absolute prompt file path, the full prompt text or an instruction to read that exact prompt file, and every input image path with its role label from `imagegen-jobs.json`.
|
||||
- Explicitly remind the subagent that the prompt's transparency and effects rules are mandatory: no detached effects, no wave marks for `waving`, no speed lines or dust for running rows, and only attached opaque sprite-like tears/smoke/stars when allowed by the state prompt.
|
||||
- Tell the subagent to inspect the generated candidate for frame count, identity consistency, clean flat chroma-key background, safe spacing, and forbidden detached effects before returning it.
|
||||
- Tell the subagent to return only the selected original `$CODEX_HOME/generated_images/.../ig_*.png` source path plus a one-sentence QA note. The parent decides whether to record or repair it.
|
||||
|
||||
Use this template for each subagent:
|
||||
|
||||
```text
|
||||
Generate the `<row-id>` row for this hatch-pet run.
|
||||
|
||||
Run dir: <absolute run dir>
|
||||
Prompt file: <absolute prompt file>
|
||||
Input images:
|
||||
- <absolute path> — <role>
|
||||
- <absolute path> — <role>
|
||||
|
||||
Read and follow the row prompt exactly, including the Transparency and artifact rules. Use `$imagegen` only; do not use local scripts to draw, tile, edit, or synthesize sprites.
|
||||
|
||||
Before returning, visually check:
|
||||
- exact requested frame count
|
||||
- same pet identity as the canonical base
|
||||
- clean flat chroma-key background
|
||||
- complete, separated, unclipped poses
|
||||
- no forbidden detached effects or slot-crossing artifacts
|
||||
|
||||
Do not edit manifests, copy into decoded, record results, mirror rows, finalize, repair, or package. Return only:
|
||||
selected_source=/absolute/path/to/$CODEX_HOME/generated_images/.../ig_*.png
|
||||
qa_note=<one sentence>
|
||||
```
|
||||
|
||||
No silent sequential fallback: if subagents cannot be used for row-strip visual generation, stop and ask for explicit user direction before continuing without them. Only an explicit user instruction such as "do not use subagents" or "run this sequentially" authorizes a normal sequential row-generation path. The final answer must report which row jobs were delegated to subagents and which, if any, were mirrored or repaired by the parent.
|
||||
|
||||
## Repair Workflow
|
||||
|
||||
If finalization stops because row QA failed, queue targeted repair jobs:
|
||||
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/queue_pet_repairs.py" \
|
||||
--run-dir /absolute/path/to/run
|
||||
```
|
||||
|
||||
Then repeat the `$imagegen` generation and `record_imagegen_result.py` ingest loop for each reopened row job. Regenerate the smallest failing scope: the failed row, not the whole sheet.
|
||||
|
||||
For identity repairs, use the canonical base image, original references, contact sheet, and exact row failure note as grounding context. Repair only the failed row while preserving the canonical pet identity.
|
||||
|
||||
## Secondary Image Generation Fallback
|
||||
|
||||
`scripts/generate_pet_images.py` is a secondary fallback for this skill.
|
||||
|
||||
Use it only when the installed `$imagegen` system skill is unavailable or cannot be invoked in the current environment. Normal pet creation should delegate visual generation to `$imagegen`, because `$imagegen` owns the built-in-first image generation policy and its own CLI fallback behavior.
|
||||
|
||||
Run the secondary fallback only after explaining why `$imagegen` cannot be used:
|
||||
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/generate_pet_images.py" \
|
||||
--run-dir /absolute/path/to/run \
|
||||
--model gpt-image-2 \
|
||||
--states all
|
||||
```
|
||||
|
||||
The secondary fallback requires `OPENAI_API_KEY`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep `$imagegen` as the primary generation layer.
|
||||
- Keep reference images attached/visible for `$imagegen` whenever the chosen path supports references.
|
||||
- Attach the row's `references/layout-guides/<state>.png` image to every row-strip job as a layout-only guide, and do not accept outputs that copy guide pixels.
|
||||
- Use subagents for row-strip visual generation after the parent records the base image. The parent may generate the base, but row-strip jobs belong to subagents unless the user explicitly says not to use subagents for this session.
|
||||
- Generate every normal visual job with `$imagegen`: base plus all row strips that are not explicitly approved `running-left` mirror derivations.
|
||||
- Treat only the base job as eligible for prompt-only generation; every row job must attach its listed grounding images.
|
||||
- Delegate `running-right` first, then mirror `running-left` only when visual inspection confirms a mirror preserves identity and semantics; otherwise delegate `running-left` as a normal grounded `$imagegen` row.
|
||||
- Never substitute locally drawn, tiled, transformed, or code-generated row strips for missing `$imagegen` outputs.
|
||||
- Never manually mutate `imagegen-jobs.json` to claim a visual job completed.
|
||||
- Do not rely on generated images for exact atlas geometry; use this skill's deterministic scripts.
|
||||
- Use the chroma key stored in `pet_request.json`; do not force a fixed green screen.
|
||||
- Keep the pet's silhouette, face, materials, palette, and props consistent across all rows.
|
||||
- Enforce the transparency and effects rules above in every base, row, and repair prompt.
|
||||
- Treat visual identity drift as a blocker even when `qa/review.json` and `final/validation.json` have no errors.
|
||||
- Treat a contact sheet that shows cropped references, repeated tiles, white cell backgrounds, or non-sprite fragments as failed.
|
||||
- Treat forbidden detached effects, chroma-key-adjacent artifacts, shadows, glows, smears, dust, landing marks, wave marks, speed lines, or motion trails as failed rows.
|
||||
- Treat `qa/review.json` errors as blockers. Warnings require visual review.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Final atlas is PNG or WebP, `1536x1872`, transparent-capable, and based on `192x208` cells.
|
||||
- Used cells are non-empty and unused cells are fully transparent.
|
||||
- Atlas follows the row/frame counts in `references/animation-rows.md`.
|
||||
- Contact sheet and preview videos have been produced unless explicitly skipped.
|
||||
- `qa/review.json` has no errors.
|
||||
- Row-by-row review confirms the animation cycles are complete enough for the Codex app.
|
||||
- `${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>/pet.json` and `${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>/spritesheet.webp` are staged together for custom pets.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Hatch Pet"
|
||||
short_description: "Hatch Codex-compatible animated pet spritesheets"
|
||||
default_prompt: "Hatch a Codex-compatible animated pet from a concept, reference images, or both. Infer missing names/descriptions, use $imagegen for the base and grounded row strips, generate running-right before deciding whether running-left can be safely mirrored, then use this skill's deterministic scripts to ingest outputs, validate frames, assemble the spritesheet, and package the pet under ${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>/."
|
||||
@@ -0,0 +1,29 @@
|
||||
# Animation Rows
|
||||
|
||||
The Codex app reads one fixed atlas: 8 columns, 9 rows, 192x208 pixels per cell.
|
||||
|
||||
| Row | State | Used columns | Durations |
|
||||
| --- | --- | ---: | --- |
|
||||
| 0 | idle | 0-5 | 280, 110, 110, 140, 140, 320 ms |
|
||||
| 1 | running-right | 0-7 | 120 ms each, final 220 ms |
|
||||
| 2 | running-left | 0-7 | 120 ms each, final 220 ms |
|
||||
| 3 | waving | 0-3 | 140 ms each, final 280 ms |
|
||||
| 4 | jumping | 0-4 | 140 ms each, final 280 ms |
|
||||
| 5 | failed | 0-7 | 140 ms each, final 240 ms |
|
||||
| 6 | waiting | 0-5 | 150 ms each, final 260 ms |
|
||||
| 7 | running | 0-5 | 120 ms each, final 220 ms |
|
||||
| 8 | review | 0-5 | 150 ms each, final 280 ms |
|
||||
|
||||
Unused cells after each row's final used column must be fully transparent.
|
||||
|
||||
## Row Purposes
|
||||
|
||||
- `idle`: neutral breathing/blinking loop; use as the reduced-motion first frame.
|
||||
- `running-right`: locomotion to the right; 8-frame loop should read directionally.
|
||||
- `running-left`: mirrored or redrawn locomotion to the left; do not simply reuse right-facing frames unless the design is symmetric.
|
||||
- `waving`: greeting or attention gesture; clear start, raised gesture, return.
|
||||
- `jumping`: anticipation, lift, peak, descent, settle.
|
||||
- `failed`: error/sad/deflated reaction; readable but not visually noisy.
|
||||
- `waiting`: patient idle variant; glance, small bounce, or prop motion.
|
||||
- `running`: generic/front-facing or in-place run loop.
|
||||
- `review`: focused/inspecting/thinking loop suitable for review state.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Codex Pet Contract
|
||||
|
||||
## Sprite Atlas
|
||||
|
||||
- Format: PNG or WebP.
|
||||
- Dimensions: `1536x1872`.
|
||||
- Grid: 8 columns x 9 rows.
|
||||
- Cell: `192x208`.
|
||||
- Background: transparent.
|
||||
- Unused cells: fully transparent.
|
||||
|
||||
The webview animation uses CSS background positions from the fixed row and column counts. Do not add labels, gutters, borders, grid lines, shadows outside the cell, or extra frames.
|
||||
|
||||
## Local Custom Pet Package
|
||||
|
||||
Place files under:
|
||||
|
||||
```text
|
||||
${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>/
|
||||
├── pet.json
|
||||
└── spritesheet.webp
|
||||
```
|
||||
|
||||
Manifest shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "pet-name",
|
||||
"displayName": "Pet Name",
|
||||
"description": "One short sentence.",
|
||||
"spritesheetPath": "spritesheet.webp"
|
||||
}
|
||||
```
|
||||
|
||||
The app loads custom pets from the folder name under `${CODEX_HOME:-$HOME/.codex}/pets/`.
|
||||
@@ -0,0 +1,60 @@
|
||||
# QA Rubric
|
||||
|
||||
Do not accept an atlas until all checks pass.
|
||||
|
||||
## Geometry
|
||||
|
||||
- Exact `1536x1872` dimensions.
|
||||
- 8 columns x 9 rows.
|
||||
- Each frame fits inside its `192x208` cell.
|
||||
- Unused cells are transparent.
|
||||
- `qa/review.json` has no errors.
|
||||
- `frames/frames-manifest.json` records component extraction for production rows, unless slot extraction was intentionally accepted after visual inspection.
|
||||
|
||||
## Character Consistency
|
||||
|
||||
- Same silhouette and proportions across every row.
|
||||
- Same face and expression language.
|
||||
- Same material, palette, lighting, and prop design.
|
||||
- No frame introduces a new unintended character or object.
|
||||
|
||||
## Sprite Style
|
||||
|
||||
- Art reads as a Codex digital pet sprite, not a polished illustration or glossy app icon.
|
||||
- Silhouette is compact and chunky enough to read inside a `192x208` cell.
|
||||
- Outlines are dark and simple, with visible stepped/pixel-style edges.
|
||||
- Palette is limited, with flat cel shading and minimal highlights or shadow steps.
|
||||
- No painterly texture, realistic fur/material detail, soft gradients, high-detail antialiasing, or tiny accessories that disappear at pet size.
|
||||
|
||||
## Animation Completeness
|
||||
|
||||
- Each row uses the exact expected number of frames.
|
||||
- The first and last frames can loop without an obvious pop.
|
||||
- Directional rows read as the intended direction.
|
||||
- State-specific actions are recognizable at pet size.
|
||||
- Poses are generated animation variants, not repeated copies of the same source image.
|
||||
|
||||
## App Fitness
|
||||
|
||||
- First idle frame works as a static reduced-motion pet.
|
||||
- No important detail is too small to read.
|
||||
- No frame is clipped by the cell.
|
||||
- Failed/review/waiting states are distinct from ordinary idle.
|
||||
- Contact sheets must show whole sprite poses inside cells, not cropped tiles from a larger reference image.
|
||||
- Contact sheets must not be accepted if every used frame is just the reference image with small geometric transforms.
|
||||
- Used cells must not have white or opaque rectangular backgrounds unless the pet intentionally fills the whole cell and the user accepts that tradeoff.
|
||||
- The chroma key must be visually absent from the character. If extraction removes character regions, choose a different key and regenerate the affected base/rows.
|
||||
- Contact sheets must not show edge slivers or partial neighboring sprites inside cells.
|
||||
- Contact sheets must not show darker/lighter versions of the chroma key as shadows, dust, smears, glows, landing marks, or motion effects. These are background extraction failures and should trigger row repair.
|
||||
- If `qa/review.json` reports edge pixels, sparse frames, size outliers, or slot-extraction fallback, inspect the row visually and repair it when the issue is visible.
|
||||
- If `qa/review.json` reports chroma-adjacent non-transparent pixels, repair the row unless those pixels are an intentional character color and the selected key was manually accepted.
|
||||
|
||||
## Repair Policy
|
||||
|
||||
Repair the smallest failing scope first:
|
||||
|
||||
1. Single bad frame.
|
||||
2. One row.
|
||||
3. Full atlas regeneration only when identity or layout is broadly broken.
|
||||
|
||||
The normal production path should queue targeted repair jobs for failing rows. Manual repair should preserve the same run directory and regenerate only the affected row prompt/image unless the base character is wrong.
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compose or normalize a Codex pet spritesheet atlas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
COLUMNS = 8
|
||||
ROWS = 9
|
||||
CELL_WIDTH = 192
|
||||
CELL_HEIGHT = 208
|
||||
ATLAS_WIDTH = COLUMNS * CELL_WIDTH
|
||||
ATLAS_HEIGHT = ROWS * CELL_HEIGHT
|
||||
ATLAS_ASPECT_RATIO = ATLAS_WIDTH / ATLAS_HEIGHT
|
||||
ROW_SPECS = [
|
||||
("idle", 0, 6),
|
||||
("running-right", 1, 8),
|
||||
("running-left", 2, 8),
|
||||
("waving", 3, 4),
|
||||
("jumping", 4, 5),
|
||||
("failed", 5, 8),
|
||||
("waiting", 6, 6),
|
||||
("running", 7, 6),
|
||||
("review", 8, 6),
|
||||
]
|
||||
IMAGE_SUFFIXES = {".png", ".webp", ".jpg", ".jpeg"}
|
||||
|
||||
|
||||
def image_files(path: Path) -> list[Path]:
|
||||
return sorted(p for p in path.iterdir() if p.suffix.lower() in IMAGE_SUFFIXES)
|
||||
|
||||
|
||||
def find_row_frames(root: Path, state: str, row_index: int) -> list[Path]:
|
||||
candidates = [
|
||||
root / state,
|
||||
root / f"row-{row_index}",
|
||||
root / f"row{row_index}",
|
||||
root / f"{row_index}-{state}",
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate.is_dir():
|
||||
files = image_files(candidate)
|
||||
if files:
|
||||
return files
|
||||
globs = [
|
||||
f"{state}_*",
|
||||
f"{state}-*",
|
||||
f"row{row_index}_*",
|
||||
f"row-{row_index}-*",
|
||||
]
|
||||
files: list[Path] = []
|
||||
for pattern in globs:
|
||||
files.extend(p for p in root.glob(pattern) if p.suffix.lower() in IMAGE_SUFFIXES)
|
||||
return sorted(set(files))
|
||||
|
||||
|
||||
def paste_centered(atlas: Image.Image, source: Image.Image, row: int, column: int) -> None:
|
||||
frame = source.convert("RGBA")
|
||||
if frame.size != (CELL_WIDTH, CELL_HEIGHT):
|
||||
frame.thumbnail((CELL_WIDTH, CELL_HEIGHT), Image.Resampling.LANCZOS)
|
||||
left = column * CELL_WIDTH + (CELL_WIDTH - frame.width) // 2
|
||||
top = row * CELL_HEIGHT + (CELL_HEIGHT - frame.height) // 2
|
||||
atlas.alpha_composite(frame, (left, top))
|
||||
|
||||
|
||||
def compose_from_source_atlas(path: Path, resize_source: bool) -> Image.Image:
|
||||
with Image.open(path) as opened:
|
||||
source = opened.convert("RGBA")
|
||||
if source.size != (ATLAS_WIDTH, ATLAS_HEIGHT):
|
||||
if not resize_source:
|
||||
raise SystemExit(
|
||||
f"source atlas must be {ATLAS_WIDTH}x{ATLAS_HEIGHT}; got {source.width}x{source.height}"
|
||||
)
|
||||
source_ratio = source.width / source.height
|
||||
if abs(source_ratio - ATLAS_ASPECT_RATIO) > 0.02:
|
||||
raise SystemExit(
|
||||
"refusing to resize source atlas because its aspect ratio does not match "
|
||||
f"the Codex atlas ratio {ATLAS_ASPECT_RATIO:.3f}; got {source_ratio:.3f}. "
|
||||
"Generate exact atlas dimensions or use --frames-root."
|
||||
)
|
||||
source = source.resize((ATLAS_WIDTH, ATLAS_HEIGHT), Image.Resampling.LANCZOS)
|
||||
|
||||
atlas = Image.new("RGBA", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))
|
||||
for _state, row, frame_count in ROW_SPECS:
|
||||
for column in range(frame_count):
|
||||
left = column * CELL_WIDTH
|
||||
top = row * CELL_HEIGHT
|
||||
cell = source.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))
|
||||
atlas.alpha_composite(cell, (left, top))
|
||||
return atlas
|
||||
|
||||
|
||||
def compose_from_frames(root: Path) -> Image.Image:
|
||||
atlas = Image.new("RGBA", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))
|
||||
for state, row, frame_count in ROW_SPECS:
|
||||
files = find_row_frames(root, state, row)
|
||||
if len(files) < frame_count:
|
||||
raise SystemExit(
|
||||
f"{state} row needs {frame_count} frames, found {len(files)} under {root}"
|
||||
)
|
||||
for column, frame_path in enumerate(files[:frame_count]):
|
||||
with Image.open(frame_path) as frame:
|
||||
paste_centered(atlas, frame, row, column)
|
||||
return atlas
|
||||
|
||||
|
||||
def save_outputs(atlas: Image.Image, output: Path, webp_output: Path | None) -> None:
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
atlas.save(output)
|
||||
if webp_output is not None:
|
||||
webp_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
atlas.save(webp_output, format="WEBP", lossless=True, quality=100, method=6)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
source = parser.add_mutually_exclusive_group(required=True)
|
||||
source.add_argument("--source-atlas")
|
||||
source.add_argument("--frames-root")
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--webp-output")
|
||||
parser.add_argument(
|
||||
"--resize-source",
|
||||
action="store_true",
|
||||
help="Resize a lower-resolution source atlas only when it already has the Codex atlas aspect ratio.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.source_atlas:
|
||||
atlas = compose_from_source_atlas(
|
||||
Path(args.source_atlas).expanduser().resolve(), args.resize_source
|
||||
)
|
||||
else:
|
||||
atlas = compose_from_frames(Path(args.frames_root).expanduser().resolve())
|
||||
|
||||
save_outputs(
|
||||
atlas,
|
||||
Path(args.output).expanduser().resolve(),
|
||||
Path(args.webp_output).expanduser().resolve() if args.webp_output else None,
|
||||
)
|
||||
print(f"wrote {Path(args.output).expanduser().resolve()}")
|
||||
if args.webp_output:
|
||||
print(f"wrote {Path(args.webp_output).expanduser().resolve()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Conditionally derive running-left by mirroring the approved running-right strip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
|
||||
def load_manifest(run_dir: Path) -> dict[str, object]:
|
||||
path = run_dir / "imagegen-jobs.json"
|
||||
if not path.exists():
|
||||
raise SystemExit(f"job manifest not found: {path}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
|
||||
jobs = manifest.get("jobs")
|
||||
if not isinstance(jobs, list):
|
||||
raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
|
||||
return [job for job in jobs if isinstance(job, dict)]
|
||||
|
||||
|
||||
def find_job(manifest: dict[str, object], job_id: str) -> dict[str, object]:
|
||||
for job in job_list(manifest):
|
||||
if job.get("id") == job_id:
|
||||
return job
|
||||
raise SystemExit(f"unknown job id: {job_id}")
|
||||
|
||||
|
||||
def file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as file:
|
||||
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def image_metadata(path: Path) -> dict[str, object]:
|
||||
with Image.open(path) as image:
|
||||
image.verify()
|
||||
with Image.open(path) as image:
|
||||
return {
|
||||
"width": image.width,
|
||||
"height": image.height,
|
||||
"mode": image.mode,
|
||||
"format": image.format,
|
||||
}
|
||||
|
||||
|
||||
def manifest_relative(path: Path, run_dir: Path) -> str:
|
||||
return str(path.resolve().relative_to(run_dir.resolve()))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument(
|
||||
"--confirm-appropriate-mirror",
|
||||
action="store_true",
|
||||
help="Required after visually confirming the rightward strip can be mirrored without identity/prop issues.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--decision-note",
|
||||
required=True,
|
||||
help="Short note explaining why mirroring is acceptable for this pet.",
|
||||
)
|
||||
parser.add_argument("--force", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.confirm_appropriate_mirror:
|
||||
raise SystemExit("refusing to mirror without --confirm-appropriate-mirror")
|
||||
if not args.decision_note.strip():
|
||||
raise SystemExit("--decision-note must explain why mirroring is appropriate")
|
||||
|
||||
run_dir = Path(args.run_dir).expanduser().resolve()
|
||||
manifest_path = run_dir / "imagegen-jobs.json"
|
||||
manifest = load_manifest(run_dir)
|
||||
right_job = find_job(manifest, "running-right")
|
||||
left_job = find_job(manifest, "running-left")
|
||||
|
||||
if right_job.get("status") != "complete":
|
||||
raise SystemExit("running-right must be complete before deriving running-left")
|
||||
mirror_policy = left_job.get("mirror_policy")
|
||||
if not isinstance(mirror_policy, dict) or mirror_policy.get("may_derive_from") != "running-right":
|
||||
raise SystemExit("running-left is not configured for conditional mirroring")
|
||||
|
||||
source = run_dir / "decoded" / "running-right.png"
|
||||
output = run_dir / "decoded" / "running-left.png"
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"running-right decoded strip not found: {source}")
|
||||
if output.exists() and not args.force:
|
||||
raise SystemExit(f"{output} already exists; pass --force to replace it")
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with Image.open(source) as image:
|
||||
mirrored = ImageOps.mirror(image.convert("RGBA"))
|
||||
mirrored.save(output)
|
||||
|
||||
left_job["status"] = "complete"
|
||||
left_job["source_path"] = manifest_relative(source, run_dir)
|
||||
left_job["source_provenance"] = "deterministic-mirror"
|
||||
left_job["derived_from"] = "running-right"
|
||||
left_job["source_sha256"] = file_sha256(source)
|
||||
left_job["output_sha256"] = file_sha256(output)
|
||||
left_job["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
left_job["metadata"] = image_metadata(output)
|
||||
left_job["mirror_decision"] = {
|
||||
"approved": True,
|
||||
"approved_at": left_job["completed_at"],
|
||||
"note": args.decision_note.strip(),
|
||||
}
|
||||
for key in [
|
||||
"last_error",
|
||||
"secondary_fallback",
|
||||
"synthetic_test_source",
|
||||
"repair_reason",
|
||||
"queued_at",
|
||||
]:
|
||||
left_job.pop(key, None)
|
||||
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"job_id": "running-left",
|
||||
"derived_from": "running-right",
|
||||
"output": str(output),
|
||||
"decision_note": args.decision_note.strip(),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract generated horizontal row strips into 192x208 sprite frames."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
CELL_WIDTH = 192
|
||||
CELL_HEIGHT = 208
|
||||
ROW_FRAME_COUNTS = {
|
||||
"idle": 6,
|
||||
"running-right": 8,
|
||||
"running-left": 8,
|
||||
"waving": 4,
|
||||
"jumping": 5,
|
||||
"failed": 8,
|
||||
"waiting": 6,
|
||||
"running": 6,
|
||||
"review": 6,
|
||||
}
|
||||
|
||||
|
||||
def parse_states(raw: str) -> list[str]:
|
||||
if raw.strip().lower() == "all":
|
||||
return list(ROW_FRAME_COUNTS)
|
||||
states = [item.strip() for item in raw.split(",") if item.strip()]
|
||||
unknown = sorted(set(states) - set(ROW_FRAME_COUNTS))
|
||||
if unknown:
|
||||
raise SystemExit(f"unknown state(s): {', '.join(unknown)}")
|
||||
return states
|
||||
|
||||
|
||||
def parse_hex_color(value: str) -> tuple[int, int, int]:
|
||||
if not re.fullmatch(r"#[0-9a-fA-F]{6}", value):
|
||||
raise SystemExit(f"invalid chroma key color: {value}; expected #RRGGBB")
|
||||
return tuple(int(value[index : index + 2], 16) for index in (1, 3, 5))
|
||||
|
||||
|
||||
def load_chroma_key(decoded_dir: Path, override: str | None) -> tuple[int, int, int]:
|
||||
if override:
|
||||
return parse_hex_color(override)
|
||||
request_path = decoded_dir.parent / "pet_request.json"
|
||||
if request_path.is_file():
|
||||
request = json.loads(request_path.read_text(encoding="utf-8"))
|
||||
chroma_key = request.get("chroma_key")
|
||||
if isinstance(chroma_key, dict) and isinstance(chroma_key.get("hex"), str):
|
||||
return parse_hex_color(chroma_key["hex"])
|
||||
return parse_hex_color("#00FF00")
|
||||
|
||||
|
||||
def color_distance(
|
||||
red: int,
|
||||
green: int,
|
||||
blue: int,
|
||||
key: tuple[int, int, int],
|
||||
) -> float:
|
||||
return math.sqrt((red - key[0]) ** 2 + (green - key[1]) ** 2 + (blue - key[2]) ** 2)
|
||||
|
||||
|
||||
def remove_chroma_background(
|
||||
image: Image.Image,
|
||||
chroma_key: tuple[int, int, int],
|
||||
threshold: float,
|
||||
) -> Image.Image:
|
||||
rgba = image.convert("RGBA")
|
||||
pixels = rgba.load()
|
||||
for y in range(rgba.height):
|
||||
for x in range(rgba.width):
|
||||
red, green, blue, alpha = pixels[x, y]
|
||||
if color_distance(red, green, blue, chroma_key) <= threshold:
|
||||
pixels[x, y] = (red, green, blue, 0)
|
||||
return rgba
|
||||
|
||||
|
||||
def fit_to_cell(image: Image.Image) -> Image.Image:
|
||||
bbox = image.getbbox()
|
||||
target = Image.new("RGBA", (CELL_WIDTH, CELL_HEIGHT), (0, 0, 0, 0))
|
||||
if bbox is None:
|
||||
return target
|
||||
|
||||
sprite = image.crop(bbox)
|
||||
max_width = CELL_WIDTH - 10
|
||||
max_height = CELL_HEIGHT - 10
|
||||
scale = min(max_width / sprite.width, max_height / sprite.height, 1.0)
|
||||
if scale != 1.0:
|
||||
sprite = sprite.resize(
|
||||
(max(1, round(sprite.width * scale)), max(1, round(sprite.height * scale))),
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
left = (CELL_WIDTH - sprite.width) // 2
|
||||
top = (CELL_HEIGHT - sprite.height) // 2
|
||||
target.alpha_composite(sprite, (left, top))
|
||||
return target
|
||||
|
||||
|
||||
def connected_components(image: Image.Image) -> list[dict[str, object]]:
|
||||
alpha = image.getchannel("A")
|
||||
width, height = image.size
|
||||
data = alpha.tobytes()
|
||||
visited = bytearray(width * height)
|
||||
components: list[dict[str, object]] = []
|
||||
|
||||
for start, alpha_value in enumerate(data):
|
||||
if alpha_value <= 16 or visited[start]:
|
||||
continue
|
||||
|
||||
stack = [start]
|
||||
visited[start] = 1
|
||||
pixels: list[int] = []
|
||||
min_x = width
|
||||
min_y = height
|
||||
max_x = 0
|
||||
max_y = 0
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
pixels.append(current)
|
||||
x = current % width
|
||||
y = current // width
|
||||
min_x = min(min_x, x)
|
||||
min_y = min(min_y, y)
|
||||
max_x = max(max_x, x)
|
||||
max_y = max(max_y, y)
|
||||
|
||||
if x > 0:
|
||||
neighbor = current - 1
|
||||
if not visited[neighbor] and data[neighbor] > 16:
|
||||
visited[neighbor] = 1
|
||||
stack.append(neighbor)
|
||||
if x + 1 < width:
|
||||
neighbor = current + 1
|
||||
if not visited[neighbor] and data[neighbor] > 16:
|
||||
visited[neighbor] = 1
|
||||
stack.append(neighbor)
|
||||
if y > 0:
|
||||
neighbor = current - width
|
||||
if not visited[neighbor] and data[neighbor] > 16:
|
||||
visited[neighbor] = 1
|
||||
stack.append(neighbor)
|
||||
if y + 1 < height:
|
||||
neighbor = current + width
|
||||
if not visited[neighbor] and data[neighbor] > 16:
|
||||
visited[neighbor] = 1
|
||||
stack.append(neighbor)
|
||||
|
||||
components.append(
|
||||
{
|
||||
"pixels": pixels,
|
||||
"area": len(pixels),
|
||||
"bbox": (min_x, min_y, max_x + 1, max_y + 1),
|
||||
"center_x": (min_x + max_x + 1) / 2,
|
||||
}
|
||||
)
|
||||
|
||||
return components
|
||||
|
||||
|
||||
def component_group_image(
|
||||
source: Image.Image,
|
||||
components: list[dict[str, object]],
|
||||
padding: int = 4,
|
||||
) -> Image.Image:
|
||||
width, height = source.size
|
||||
min_x = max(0, min(component["bbox"][0] for component in components) - padding)
|
||||
min_y = max(0, min(component["bbox"][1] for component in components) - padding)
|
||||
max_x = min(width, max(component["bbox"][2] for component in components) + padding)
|
||||
max_y = min(height, max(component["bbox"][3] for component in components) + padding)
|
||||
|
||||
output = Image.new("RGBA", (max_x - min_x, max_y - min_y), (0, 0, 0, 0))
|
||||
source_pixels = source.load()
|
||||
output_pixels = output.load()
|
||||
for component in components:
|
||||
for pixel_index in component["pixels"]:
|
||||
x = pixel_index % width
|
||||
y = pixel_index // width
|
||||
output_pixels[x - min_x, y - min_y] = source_pixels[x, y]
|
||||
return output
|
||||
|
||||
|
||||
def extract_component_frames(strip: Image.Image, frame_count: int) -> list[Image.Image] | None:
|
||||
components = connected_components(strip)
|
||||
if not components:
|
||||
return None
|
||||
|
||||
largest_area = max(component["area"] for component in components)
|
||||
seed_threshold = max(120, largest_area * 0.20)
|
||||
seeds = [component for component in components if component["area"] >= seed_threshold]
|
||||
if len(seeds) < frame_count:
|
||||
seeds = sorted(components, key=lambda component: component["area"], reverse=True)[
|
||||
:frame_count
|
||||
]
|
||||
if len(seeds) < frame_count:
|
||||
return None
|
||||
|
||||
seeds = sorted(
|
||||
sorted(seeds, key=lambda component: component["area"], reverse=True)[:frame_count],
|
||||
key=lambda component: component["center_x"],
|
||||
)
|
||||
seed_ids = {id(seed) for seed in seeds}
|
||||
groups: list[list[dict[str, object]]] = [[seed] for seed in seeds]
|
||||
noise_threshold = max(12, largest_area * 0.002)
|
||||
|
||||
for component in components:
|
||||
if id(component) in seed_ids or component["area"] < noise_threshold:
|
||||
continue
|
||||
nearest_index = min(
|
||||
range(len(seeds)),
|
||||
key=lambda index: abs(seeds[index]["center_x"] - component["center_x"]),
|
||||
)
|
||||
groups[nearest_index].append(component)
|
||||
|
||||
return [fit_to_cell(component_group_image(strip, group)) for group in groups]
|
||||
|
||||
|
||||
def extract_slot_frames(strip: Image.Image, frame_count: int) -> list[Image.Image]:
|
||||
slot_width = strip.width / frame_count
|
||||
frames = []
|
||||
for index in range(frame_count):
|
||||
left = round(index * slot_width)
|
||||
right = round((index + 1) * slot_width)
|
||||
crop = strip.crop((left, 0, right, strip.height))
|
||||
frames.append(fit_to_cell(crop))
|
||||
return frames
|
||||
|
||||
|
||||
def extract_state(
|
||||
strip_path: Path,
|
||||
state: str,
|
||||
output_root: Path,
|
||||
chroma_key: tuple[int, int, int],
|
||||
threshold: float,
|
||||
method: str,
|
||||
) -> dict[str, object]:
|
||||
frame_count = ROW_FRAME_COUNTS[state]
|
||||
with Image.open(strip_path) as opened:
|
||||
strip = remove_chroma_background(opened, chroma_key, threshold)
|
||||
|
||||
state_dir = output_root / state
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
frames = None
|
||||
used_method = method
|
||||
if method in {"auto", "components"}:
|
||||
frames = extract_component_frames(strip, frame_count)
|
||||
if frames is None and method == "components":
|
||||
raise SystemExit(f"could not find {frame_count} sprite components in {strip_path}")
|
||||
if frames is not None:
|
||||
used_method = "components"
|
||||
|
||||
if frames is None:
|
||||
frames = extract_slot_frames(strip, frame_count)
|
||||
used_method = "slots"
|
||||
|
||||
outputs = []
|
||||
for index, frame in enumerate(frames):
|
||||
output = state_dir / f"{index:02d}.png"
|
||||
frame.save(output)
|
||||
outputs.append(str(output))
|
||||
return {"state": state, "frames": outputs, "method": used_method}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--decoded-dir", required=True)
|
||||
parser.add_argument("--output-dir", required=True)
|
||||
parser.add_argument("--states", default="all")
|
||||
parser.add_argument("--chroma-key", help="Override chroma key as #RRGGBB.")
|
||||
parser.add_argument("--key-threshold", type=float, default=96.0)
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
choices=("auto", "components", "slots"),
|
||||
default="auto",
|
||||
help="Use connected sprite components when possible, or fixed equal slots.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
decoded_dir = Path(args.decoded_dir).expanduser().resolve()
|
||||
output_dir = Path(args.output_dir).expanduser().resolve()
|
||||
chroma_key = load_chroma_key(decoded_dir, args.chroma_key)
|
||||
states = parse_states(args.states)
|
||||
manifest = []
|
||||
for state in states:
|
||||
strip_path = decoded_dir / f"{state}.png"
|
||||
if not strip_path.is_file():
|
||||
raise SystemExit(f"missing generated strip for {state}: {strip_path}")
|
||||
manifest.append(
|
||||
extract_state(
|
||||
strip_path,
|
||||
state,
|
||||
output_dir,
|
||||
chroma_key,
|
||||
args.key_threshold,
|
||||
args.method,
|
||||
)
|
||||
)
|
||||
|
||||
(output_dir / "frames-manifest.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"chroma_key": {
|
||||
"hex": f"#{chroma_key[0]:02X}{chroma_key[1]:02X}{chroma_key[2]:02X}",
|
||||
"rgb": list(chroma_key),
|
||||
"threshold": args.key_threshold,
|
||||
},
|
||||
"rows": manifest,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(json.dumps({"ok": True, "frames_root": str(output_dir), "states": states}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Finalize a Codex pet run after all imagegen jobs are complete."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
|
||||
def run(command: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
print("+ " + " ".join(command))
|
||||
return subprocess.run(command, check=check, text=True)
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, object]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as file:
|
||||
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def is_relative_to(path: Path, root: Path) -> bool:
|
||||
try:
|
||||
path.relative_to(root)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def default_generated_images_root() -> Path:
|
||||
return default_codex_home() / "generated_images"
|
||||
|
||||
|
||||
def default_codex_home() -> Path:
|
||||
return Path(os.environ.get("CODEX_HOME") or "~/.codex").expanduser().resolve()
|
||||
|
||||
|
||||
def manifest_path(raw: object, *, run_dir: Path, field: str, job_id: str) -> Path:
|
||||
if not isinstance(raw, str) or not raw:
|
||||
raise SystemExit(f"job {job_id} has no {field}")
|
||||
path = Path(raw).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = run_dir / path
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def validate_hash(job: dict[str, object], *, source: Path, output: Path, job_id: str) -> None:
|
||||
expected_hash = job.get("source_sha256")
|
||||
if not isinstance(expected_hash, str) or not expected_hash:
|
||||
raise SystemExit(
|
||||
f"job {job_id} is missing source_sha256; ingest visual outputs with "
|
||||
"record_imagegen_result.py instead of editing imagegen-jobs.json"
|
||||
)
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"job {job_id} source image no longer exists: {source}")
|
||||
if not output.is_file():
|
||||
raise SystemExit(f"job {job_id} decoded output is missing: {output}")
|
||||
source_hash = file_sha256(source)
|
||||
output_hash = file_sha256(output)
|
||||
if source_hash != expected_hash:
|
||||
raise SystemExit(f"job {job_id} source image hash does not match imagegen-jobs.json")
|
||||
if output_hash != expected_hash:
|
||||
raise SystemExit(
|
||||
f"job {job_id} decoded output does not match its recorded source image; "
|
||||
"do not rewrite decoded visual outputs locally"
|
||||
)
|
||||
|
||||
|
||||
def validate_mirror_hash(job: dict[str, object], *, source: Path, output: Path, job_id: str) -> None:
|
||||
if job_id != "running-left":
|
||||
raise SystemExit(f"job {job_id} may not use deterministic mirror provenance")
|
||||
if job.get("derived_from") != "running-right":
|
||||
raise SystemExit("running-left mirror job must derive from running-right")
|
||||
decision = job.get("mirror_decision")
|
||||
if not isinstance(decision, dict) or decision.get("approved") is not True:
|
||||
raise SystemExit(
|
||||
"running-left mirror job is missing an approved mirror_decision; "
|
||||
"use derive_running_left_from_running_right.py after visual review"
|
||||
)
|
||||
|
||||
expected_source_hash = job.get("source_sha256")
|
||||
expected_output_hash = job.get("output_sha256")
|
||||
if not isinstance(expected_source_hash, str) or not expected_source_hash:
|
||||
raise SystemExit("running-left mirror job is missing source_sha256")
|
||||
if not isinstance(expected_output_hash, str) or not expected_output_hash:
|
||||
raise SystemExit("running-left mirror job is missing output_sha256")
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"running-left mirror source image no longer exists: {source}")
|
||||
if not output.is_file():
|
||||
raise SystemExit(f"running-left mirrored output is missing: {output}")
|
||||
if source.name != "running-right.png" or source.parent.name != "decoded":
|
||||
raise SystemExit("running-left mirror source must be decoded/running-right.png")
|
||||
if output.name != "running-left.png" or output.parent.name != "decoded":
|
||||
raise SystemExit("running-left mirror output must be decoded/running-left.png")
|
||||
if file_sha256(source) != expected_source_hash:
|
||||
raise SystemExit("running-left mirror source hash does not match imagegen-jobs.json")
|
||||
if file_sha256(output) != expected_output_hash:
|
||||
raise SystemExit(
|
||||
"running-left mirrored output hash does not match imagegen-jobs.json; "
|
||||
"rerun derive_running_left_from_running_right.py"
|
||||
)
|
||||
with Image.open(source) as source_image, Image.open(output) as output_image:
|
||||
expected = ImageOps.mirror(source_image.convert("RGBA"))
|
||||
actual = output_image.convert("RGBA")
|
||||
if expected.size != actual.size or expected.tobytes() != actual.tobytes():
|
||||
raise SystemExit(
|
||||
"running-left mirrored output is not an exact horizontal mirror of running-right"
|
||||
)
|
||||
|
||||
|
||||
def validate_completed_job_source(
|
||||
job: dict[str, object],
|
||||
*,
|
||||
run_dir: Path,
|
||||
allow_synthetic_test_sources: bool,
|
||||
) -> None:
|
||||
job_id = str(job.get("id") or "")
|
||||
source = manifest_path(job.get("source_path"), run_dir=run_dir, field="source_path", job_id=job_id)
|
||||
output = manifest_path(job.get("output_path"), run_dir=run_dir, field="output_path", job_id=job_id)
|
||||
|
||||
blocked_flags = [
|
||||
flag
|
||||
for flag in ("deterministic_pet_row", "cute_raster_row", "local_raster_row")
|
||||
if job.get(flag)
|
||||
]
|
||||
if blocked_flags:
|
||||
raise SystemExit(
|
||||
f"job {job_id} was marked as a local/synthetic row ({', '.join(blocked_flags)}); "
|
||||
"regenerate it with $imagegen"
|
||||
)
|
||||
|
||||
if job.get("synthetic_test_source"):
|
||||
if not allow_synthetic_test_sources:
|
||||
raise SystemExit(
|
||||
f"job {job_id} uses a synthetic test source; rerun with real $imagegen output"
|
||||
)
|
||||
validate_hash(job, source=source, output=output, job_id=job_id)
|
||||
return
|
||||
|
||||
if job.get("secondary_fallback"):
|
||||
if job.get("source_provenance") != "secondary-fallback-image-api":
|
||||
raise SystemExit(f"job {job_id} has invalid secondary fallback provenance")
|
||||
validate_hash(job, source=source, output=output, job_id=job_id)
|
||||
return
|
||||
|
||||
if job.get("source_provenance") == "deterministic-mirror":
|
||||
validate_mirror_hash(job, source=source, output=output, job_id=job_id)
|
||||
return
|
||||
|
||||
if job.get("source_provenance") != "built-in-imagegen":
|
||||
raise SystemExit(
|
||||
f"job {job_id} was not recorded as a built-in $imagegen output; "
|
||||
"use record_imagegen_result.py with the selected $CODEX_HOME/generated_images/.../ig_*.png file"
|
||||
)
|
||||
if is_relative_to(source, run_dir):
|
||||
raise SystemExit(
|
||||
f"job {job_id} source image is inside the pet run directory; "
|
||||
"do not use locally generated row artifacts as visual sources"
|
||||
)
|
||||
generated_root = default_generated_images_root()
|
||||
if not is_relative_to(source, generated_root) or not source.name.startswith("ig_"):
|
||||
raise SystemExit(
|
||||
f"job {job_id} source image is not a built-in $imagegen output under "
|
||||
f"{generated_root}/.../ig_*.png"
|
||||
)
|
||||
validate_hash(job, source=source, output=output, job_id=job_id)
|
||||
|
||||
|
||||
def require_complete_jobs(run_dir: Path, *, allow_synthetic_test_sources: bool) -> None:
|
||||
manifest_path = run_dir / "imagegen-jobs.json"
|
||||
manifest = load_json(manifest_path)
|
||||
jobs = manifest.get("jobs")
|
||||
if not isinstance(jobs, list):
|
||||
raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
|
||||
incomplete = [
|
||||
str(job.get("id"))
|
||||
for job in jobs
|
||||
if isinstance(job, dict) and job.get("status", "pending") != "complete"
|
||||
]
|
||||
if incomplete:
|
||||
raise SystemExit(
|
||||
"imagegen jobs are not complete; run pet_job_status.py and finish: "
|
||||
+ ", ".join(incomplete)
|
||||
)
|
||||
for job in jobs:
|
||||
if isinstance(job, dict):
|
||||
validate_completed_job_source(
|
||||
job,
|
||||
run_dir=run_dir,
|
||||
allow_synthetic_test_sources=allow_synthetic_test_sources,
|
||||
)
|
||||
|
||||
|
||||
def review_failures(review: dict[str, object]) -> list[str]:
|
||||
rows = review.get("rows")
|
||||
if not isinstance(rows, list):
|
||||
return ["review did not contain row-level results"]
|
||||
failures = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
errors = row.get("errors")
|
||||
if isinstance(errors, list) and errors:
|
||||
failures.append(f"{row.get('state')}: {'; '.join(str(error) for error in errors)}")
|
||||
return failures
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument("--allow-slot-extraction", action="store_true")
|
||||
parser.add_argument("--skip-videos", action="store_true")
|
||||
parser.add_argument("--skip-package", action="store_true")
|
||||
parser.add_argument(
|
||||
"--package-dir",
|
||||
default="",
|
||||
help="Exact pet package directory. Defaults to ${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>.",
|
||||
)
|
||||
parser.add_argument("--ffmpeg", default="")
|
||||
parser.add_argument("--allow-synthetic-test-sources", action="store_true", help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
|
||||
scripts_dir = Path(__file__).resolve().parent
|
||||
run_dir = Path(args.run_dir).expanduser().resolve()
|
||||
request = load_json(run_dir / "pet_request.json")
|
||||
pet_id = str(request.get("pet_id") or "")
|
||||
display_name = str(request.get("display_name") or "")
|
||||
description = str(request.get("description") or "")
|
||||
if not pet_id or not display_name or not description:
|
||||
raise SystemExit("pet_request.json is missing pet_id, display_name, or description")
|
||||
|
||||
require_complete_jobs(
|
||||
run_dir,
|
||||
allow_synthetic_test_sources=args.allow_synthetic_test_sources,
|
||||
)
|
||||
|
||||
final_dir = run_dir / "final"
|
||||
qa_dir = run_dir / "qa"
|
||||
final_dir.mkdir(parents=True, exist_ok=True)
|
||||
qa_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
str(scripts_dir / "extract_strip_frames.py"),
|
||||
"--decoded-dir",
|
||||
str(run_dir / "decoded"),
|
||||
"--output-dir",
|
||||
str(run_dir / "frames"),
|
||||
"--states",
|
||||
"all",
|
||||
"--method",
|
||||
"auto",
|
||||
]
|
||||
)
|
||||
|
||||
review_path = qa_dir / "review.json"
|
||||
inspect_command = [
|
||||
sys.executable,
|
||||
str(scripts_dir / "inspect_frames.py"),
|
||||
"--frames-root",
|
||||
str(run_dir / "frames"),
|
||||
"--json-out",
|
||||
str(review_path),
|
||||
]
|
||||
if not args.allow_slot_extraction:
|
||||
inspect_command.append("--require-components")
|
||||
run(inspect_command, check=False)
|
||||
review = load_json(review_path)
|
||||
if not review.get("ok"):
|
||||
failures = review_failures(review)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": False,
|
||||
"review": str(review_path),
|
||||
"repair_hint": "Run queue_pet_repairs.py, regenerate the reopened row jobs with $imagegen, then finalize again.",
|
||||
"failures": failures,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
str(scripts_dir / "compose_atlas.py"),
|
||||
"--frames-root",
|
||||
str(run_dir / "frames"),
|
||||
"--output",
|
||||
str(final_dir / "spritesheet.png"),
|
||||
"--webp-output",
|
||||
str(final_dir / "spritesheet.webp"),
|
||||
]
|
||||
)
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
str(scripts_dir / "validate_atlas.py"),
|
||||
str(final_dir / "spritesheet.webp"),
|
||||
"--json-out",
|
||||
str(final_dir / "validation.json"),
|
||||
]
|
||||
)
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
str(scripts_dir / "make_contact_sheet.py"),
|
||||
str(final_dir / "spritesheet.webp"),
|
||||
"--output",
|
||||
str(qa_dir / "contact-sheet.png"),
|
||||
]
|
||||
)
|
||||
|
||||
if not args.skip_videos:
|
||||
video_command = [
|
||||
sys.executable,
|
||||
str(scripts_dir / "render_animation_videos.py"),
|
||||
str(final_dir / "spritesheet.webp"),
|
||||
"--output-dir",
|
||||
str(qa_dir / "videos"),
|
||||
]
|
||||
if args.ffmpeg:
|
||||
video_command.extend(["--ffmpeg", args.ffmpeg])
|
||||
run(video_command)
|
||||
|
||||
if not args.skip_package:
|
||||
package_command = [
|
||||
sys.executable,
|
||||
str(scripts_dir / "package_custom_pet.py"),
|
||||
"--pet-name",
|
||||
pet_id,
|
||||
"--display-name",
|
||||
display_name,
|
||||
"--description",
|
||||
description,
|
||||
"--spritesheet",
|
||||
str(final_dir / "spritesheet.webp"),
|
||||
"--force",
|
||||
]
|
||||
if args.package_dir:
|
||||
package_command.extend(["--output-dir", str(Path(args.package_dir).expanduser().resolve())])
|
||||
run(package_command)
|
||||
|
||||
package_dir = None
|
||||
if not args.skip_package:
|
||||
package_dir = (
|
||||
Path(args.package_dir).expanduser().resolve()
|
||||
if args.package_dir
|
||||
else default_codex_home() / "pets" / pet_id
|
||||
)
|
||||
|
||||
summary = {
|
||||
"ok": True,
|
||||
"run_dir": str(run_dir),
|
||||
"spritesheet": str(final_dir / "spritesheet.webp"),
|
||||
"validation": str(final_dir / "validation.json"),
|
||||
"contact_sheet": str(qa_dir / "contact-sheet.png"),
|
||||
"review": str(review_path),
|
||||
"videos": None if args.skip_videos else str(qa_dir / "videos"),
|
||||
"package": None if package_dir is None else str(package_dir),
|
||||
}
|
||||
summary_path = qa_dir / "run-summary.json"
|
||||
summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(summary, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Secondary image generation fallback for Codex pet base art and row strips."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ALL_STATES = [
|
||||
"idle",
|
||||
"running-right",
|
||||
"running-left",
|
||||
"waving",
|
||||
"jumping",
|
||||
"failed",
|
||||
"waiting",
|
||||
"running",
|
||||
"review",
|
||||
]
|
||||
CANONICAL_BASE_PATH = "references/canonical-base.png"
|
||||
|
||||
|
||||
def parse_states(raw: str) -> list[str]:
|
||||
if raw.strip().lower() == "all":
|
||||
return ALL_STATES
|
||||
states = [item.strip() for item in raw.split(",") if item.strip()]
|
||||
unknown = sorted(set(states) - set(ALL_STATES))
|
||||
if unknown:
|
||||
raise SystemExit(f"unknown state(s): {', '.join(unknown)}")
|
||||
return states
|
||||
|
||||
|
||||
def load_manifest(run_dir: Path) -> dict[str, object]:
|
||||
path = run_dir / "imagegen-jobs.json"
|
||||
if not path.exists():
|
||||
raise SystemExit(f"job manifest not found: {path}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def manifest_jobs(manifest: dict[str, object]) -> list[dict[str, object]]:
|
||||
jobs = manifest.get("jobs")
|
||||
if not isinstance(jobs, list):
|
||||
raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
|
||||
return [job for job in jobs if isinstance(job, dict)]
|
||||
|
||||
|
||||
def select_jobs(
|
||||
manifest: dict[str, object],
|
||||
*,
|
||||
states: list[str],
|
||||
skip_base: bool,
|
||||
job_ids: list[str],
|
||||
) -> list[dict[str, object]]:
|
||||
selected_ids = set(job_ids)
|
||||
if not selected_ids:
|
||||
if not skip_base:
|
||||
selected_ids.add("base")
|
||||
selected_ids.update(states)
|
||||
selected = [job for job in manifest_jobs(manifest) if job.get("id") in selected_ids]
|
||||
missing = selected_ids - {str(job.get("id")) for job in selected}
|
||||
if missing:
|
||||
raise SystemExit(f"unknown job id(s): {', '.join(sorted(missing))}")
|
||||
return selected
|
||||
|
||||
|
||||
def run_image_edit(
|
||||
*,
|
||||
model: str,
|
||||
prompt_file: Path,
|
||||
image_paths: list[Path],
|
||||
output_json: Path,
|
||||
size: str,
|
||||
api_key: str,
|
||||
) -> dict[str, object]:
|
||||
output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [
|
||||
"curl",
|
||||
"-sS",
|
||||
"-X",
|
||||
"POST",
|
||||
"https://api.openai.com/v1/images/edits",
|
||||
"-H",
|
||||
f"Authorization: Bearer {api_key}",
|
||||
"-F",
|
||||
f"model={model}",
|
||||
]
|
||||
for image_path in image_paths:
|
||||
command.extend(["-F", f"image[]=@{image_path}"])
|
||||
command.extend(
|
||||
[
|
||||
"-F",
|
||||
f"prompt=<{prompt_file}",
|
||||
"-F",
|
||||
f"size={size}",
|
||||
"-F",
|
||||
"output_format=png",
|
||||
"-o",
|
||||
str(output_json),
|
||||
]
|
||||
)
|
||||
subprocess.run(command, check=True)
|
||||
response = json.loads(output_json.read_text(encoding="utf-8"))
|
||||
if response.get("error"):
|
||||
raise SystemExit(json.dumps(response["error"], indent=2))
|
||||
return response
|
||||
|
||||
|
||||
def run_image_generation(
|
||||
*,
|
||||
model: str,
|
||||
prompt_file: Path,
|
||||
output_json: Path,
|
||||
size: str,
|
||||
api_key: str,
|
||||
) -> dict[str, object]:
|
||||
output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [
|
||||
"curl",
|
||||
"-sS",
|
||||
"-X",
|
||||
"POST",
|
||||
"https://api.openai.com/v1/images/generations",
|
||||
"-H",
|
||||
f"Authorization: Bearer {api_key}",
|
||||
"-F",
|
||||
f"model={model}",
|
||||
"-F",
|
||||
f"prompt=<{prompt_file}",
|
||||
"-F",
|
||||
f"size={size}",
|
||||
"-F",
|
||||
"output_format=png",
|
||||
"-o",
|
||||
str(output_json),
|
||||
]
|
||||
subprocess.run(command, check=True)
|
||||
response = json.loads(output_json.read_text(encoding="utf-8"))
|
||||
if response.get("error"):
|
||||
raise SystemExit(json.dumps(response["error"], indent=2))
|
||||
return response
|
||||
|
||||
|
||||
def decode_response(response: dict[str, object], output_image: Path) -> None:
|
||||
data = response.get("data")
|
||||
if not isinstance(data, list) or not data:
|
||||
raise SystemExit("image API response did not contain data[0]")
|
||||
first = data[0]
|
||||
if not isinstance(first, dict) or not isinstance(first.get("b64_json"), str):
|
||||
raise SystemExit("image API response did not contain data[0].b64_json")
|
||||
output_image.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_image.write_bytes(base64.b64decode(first["b64_json"]))
|
||||
|
||||
|
||||
def file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as file:
|
||||
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def complete_job(job: dict[str, object], output_path: Path) -> None:
|
||||
job["status"] = "complete"
|
||||
job["source_path"] = str(output_path)
|
||||
job["source_provenance"] = "secondary-fallback-image-api"
|
||||
job["source_sha256"] = file_sha256(output_path)
|
||||
job["output_sha256"] = file_sha256(output_path)
|
||||
job["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
job["secondary_fallback"] = True
|
||||
for key in [
|
||||
"last_error",
|
||||
"synthetic_test_source",
|
||||
"derived_from",
|
||||
"mirror_decision",
|
||||
"repair_reason",
|
||||
"queued_at",
|
||||
]:
|
||||
job.pop(key, None)
|
||||
|
||||
|
||||
def write_canonical_base(
|
||||
run_dir: Path, manifest: dict[str, object], output_image: Path
|
||||
) -> None:
|
||||
canonical = run_dir / CANONICAL_BASE_PATH
|
||||
canonical.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(output_image, canonical)
|
||||
reference = {
|
||||
"path": CANONICAL_BASE_PATH,
|
||||
"source_job": "base",
|
||||
"sha256": file_sha256(canonical),
|
||||
}
|
||||
manifest["canonical_identity_reference"] = reference
|
||||
request_path = run_dir / "pet_request.json"
|
||||
if request_path.exists():
|
||||
request = json.loads(request_path.read_text(encoding="utf-8"))
|
||||
request["canonical_identity_reference"] = reference
|
||||
request_path.write_text(json.dumps(request, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def path_list(run_dir: Path, job: dict[str, object]) -> list[Path]:
|
||||
inputs = job.get("input_images")
|
||||
if not isinstance(inputs, list):
|
||||
raise SystemExit(f"job {job.get('id')} has invalid input_images")
|
||||
paths = []
|
||||
for item in inputs:
|
||||
if not isinstance(item, dict) or not isinstance(item.get("path"), str):
|
||||
raise SystemExit(f"job {job.get('id')} has invalid input image entry")
|
||||
path = run_dir / item["path"]
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"input image for job {job.get('id')} not found: {path}")
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument("--model", default="gpt-image-2")
|
||||
parser.add_argument("--size", default="1024x1024")
|
||||
parser.add_argument("--states", default="all")
|
||||
parser.add_argument("--job-id", action="append", default=[])
|
||||
parser.add_argument("--skip-base", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
api_key = os.environ.get("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
raise SystemExit("OPENAI_API_KEY is not set")
|
||||
|
||||
run_dir = Path(args.run_dir).expanduser().resolve()
|
||||
manifest_path = run_dir / "imagegen-jobs.json"
|
||||
manifest = load_manifest(run_dir)
|
||||
jobs = select_jobs(
|
||||
manifest,
|
||||
states=parse_states(args.states),
|
||||
skip_base=args.skip_base,
|
||||
job_ids=args.job_id,
|
||||
)
|
||||
raw_dir = run_dir / "raw"
|
||||
|
||||
completed = []
|
||||
for job in jobs:
|
||||
job_id = str(job.get("id"))
|
||||
prompt_raw = job.get("prompt_file")
|
||||
output_raw = job.get("output_path")
|
||||
if not isinstance(prompt_raw, str) or not isinstance(output_raw, str):
|
||||
raise SystemExit(f"job {job_id} is missing prompt_file or output_path")
|
||||
prompt_file = run_dir / prompt_raw
|
||||
output_image = run_dir / output_raw
|
||||
print(f"Generating {job_id} with secondary fallback")
|
||||
image_paths = path_list(run_dir, job)
|
||||
if image_paths:
|
||||
response = run_image_edit(
|
||||
model=args.model,
|
||||
prompt_file=prompt_file,
|
||||
image_paths=image_paths,
|
||||
output_json=raw_dir / f"{job_id}.response.json",
|
||||
size=args.size,
|
||||
api_key=api_key,
|
||||
)
|
||||
else:
|
||||
response = run_image_generation(
|
||||
model=args.model,
|
||||
prompt_file=prompt_file,
|
||||
output_json=raw_dir / f"{job_id}.response.json",
|
||||
size=args.size,
|
||||
api_key=api_key,
|
||||
)
|
||||
decode_response(response, output_image)
|
||||
complete_job(job, output_image)
|
||||
if job_id == "base":
|
||||
job["canonical_reference_path"] = CANONICAL_BASE_PATH
|
||||
write_canonical_base(run_dir, manifest, output_image)
|
||||
completed.append({"job_id": job_id, "output": str(output_image)})
|
||||
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"ok": True, "completed": completed}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inspect extracted Codex pet frames before atlas composition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from statistics import median
|
||||
|
||||
from PIL import Image
|
||||
|
||||
CELL_WIDTH = 192
|
||||
CELL_HEIGHT = 208
|
||||
ROW_FRAME_COUNTS = {
|
||||
"idle": 6,
|
||||
"running-right": 8,
|
||||
"running-left": 8,
|
||||
"waving": 4,
|
||||
"jumping": 5,
|
||||
"failed": 8,
|
||||
"waiting": 6,
|
||||
"running": 6,
|
||||
"review": 6,
|
||||
}
|
||||
IMAGE_SUFFIXES = {".png", ".webp", ".jpg", ".jpeg"}
|
||||
|
||||
|
||||
def alpha_nonzero_count(image: Image.Image) -> int:
|
||||
alpha = image if image.mode == "L" else image.getchannel("A")
|
||||
return sum(alpha.histogram()[1:])
|
||||
|
||||
|
||||
def edge_alpha_count(image: Image.Image, margin: int) -> int:
|
||||
alpha = image.getchannel("A")
|
||||
width, height = alpha.size
|
||||
total = 0
|
||||
for box in (
|
||||
(0, 0, width, margin),
|
||||
(0, height - margin, width, height),
|
||||
(0, 0, margin, height),
|
||||
(width - margin, 0, width, height),
|
||||
):
|
||||
total += alpha_nonzero_count(alpha.crop(box))
|
||||
return total
|
||||
|
||||
|
||||
def color_distance(left: tuple[int, int, int], right: tuple[int, int, int]) -> float:
|
||||
return math.sqrt(sum((left[index] - right[index]) ** 2 for index in range(3)))
|
||||
|
||||
|
||||
def chroma_adjacent_count(
|
||||
image: Image.Image,
|
||||
chroma_key: tuple[int, int, int] | None,
|
||||
threshold: float,
|
||||
) -> int:
|
||||
if chroma_key is None:
|
||||
return 0
|
||||
rgba = image.convert("RGBA")
|
||||
data = rgba.tobytes()
|
||||
count = 0
|
||||
for index in range(0, len(data), 4):
|
||||
red, green, blue, alpha = data[index : index + 4]
|
||||
if alpha > 16 and color_distance((red, green, blue), chroma_key) <= threshold:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def frame_files(state_dir: Path) -> list[Path]:
|
||||
if not state_dir.is_dir():
|
||||
return []
|
||||
return sorted(path for path in state_dir.iterdir() if path.suffix.lower() in IMAGE_SUFFIXES)
|
||||
|
||||
|
||||
def load_manifest(frames_root: Path) -> dict[str, dict[str, object]]:
|
||||
manifest_path = frames_root / "frames-manifest.json"
|
||||
if not manifest_path.is_file():
|
||||
return {}
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
rows = manifest.get("rows", [])
|
||||
if not isinstance(rows, list):
|
||||
return {}
|
||||
return {
|
||||
row["state"]: row
|
||||
for row in rows
|
||||
if isinstance(row, dict) and isinstance(row.get("state"), str)
|
||||
}
|
||||
|
||||
|
||||
def load_chroma_key(frames_root: Path) -> tuple[int, int, int] | None:
|
||||
manifest_path = frames_root / "frames-manifest.json"
|
||||
if not manifest_path.is_file():
|
||||
return None
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
chroma_key = manifest.get("chroma_key")
|
||||
if not isinstance(chroma_key, dict):
|
||||
return None
|
||||
rgb = chroma_key.get("rgb")
|
||||
if (
|
||||
not isinstance(rgb, list)
|
||||
or len(rgb) != 3
|
||||
or not all(isinstance(value, int) for value in rgb)
|
||||
):
|
||||
return None
|
||||
return (rgb[0], rgb[1], rgb[2])
|
||||
|
||||
|
||||
def inspect_state(
|
||||
frames_root: Path,
|
||||
state: str,
|
||||
expected_count: int,
|
||||
manifest_rows: dict[str, dict[str, object]],
|
||||
chroma_key: tuple[int, int, int] | None,
|
||||
args: argparse.Namespace,
|
||||
) -> dict[str, object]:
|
||||
state_dir = frames_root / state
|
||||
files = frame_files(state_dir)
|
||||
row_errors: list[str] = []
|
||||
row_warnings: list[str] = []
|
||||
frames: list[dict[str, object]] = []
|
||||
areas: list[int] = []
|
||||
manifest_row = manifest_rows.get(state, {})
|
||||
method = manifest_row.get("method")
|
||||
|
||||
if len(files) != expected_count:
|
||||
row_errors.append(f"expected {expected_count} frame files for {state}, found {len(files)}")
|
||||
|
||||
if args.require_components and method and method != "components":
|
||||
row_errors.append(
|
||||
f"{state} used extraction method {method}; regenerate the row or inspect slot slicing"
|
||||
)
|
||||
elif method and method != "components":
|
||||
row_warnings.append(
|
||||
f"{state} used extraction method {method}; component extraction is preferred"
|
||||
)
|
||||
|
||||
for index, frame_path in enumerate(files[:expected_count]):
|
||||
with Image.open(frame_path) as opened:
|
||||
frame = opened.convert("RGBA")
|
||||
nontransparent = alpha_nonzero_count(frame)
|
||||
bbox = frame.getbbox()
|
||||
edge_pixels = edge_alpha_count(frame, args.edge_margin)
|
||||
chroma_adjacent_pixels = chroma_adjacent_count(
|
||||
frame,
|
||||
chroma_key,
|
||||
args.chroma_adjacent_threshold,
|
||||
)
|
||||
info = {
|
||||
"index": index,
|
||||
"file": str(frame_path),
|
||||
"width": frame.width,
|
||||
"height": frame.height,
|
||||
"nontransparent_pixels": nontransparent,
|
||||
"bbox": list(bbox) if bbox else None,
|
||||
"edge_pixels": edge_pixels,
|
||||
"chroma_adjacent_pixels": chroma_adjacent_pixels,
|
||||
}
|
||||
frames.append(info)
|
||||
areas.append(nontransparent)
|
||||
|
||||
if frame.size != (CELL_WIDTH, CELL_HEIGHT):
|
||||
row_errors.append(
|
||||
f"{state} frame {index:02d} is {frame.width}x{frame.height}; expected {CELL_WIDTH}x{CELL_HEIGHT}"
|
||||
)
|
||||
if nontransparent < args.min_used_pixels:
|
||||
row_errors.append(
|
||||
f"{state} frame {index:02d} is empty or too sparse ({nontransparent} pixels)"
|
||||
)
|
||||
if edge_pixels > args.edge_pixel_threshold:
|
||||
row_warnings.append(
|
||||
f"{state} frame {index:02d} has {edge_pixels} non-transparent pixels near the cell edge"
|
||||
)
|
||||
if chroma_adjacent_pixels > args.chroma_adjacent_pixel_threshold:
|
||||
row_errors.append(
|
||||
f"{state} frame {index:02d} has {chroma_adjacent_pixels} non-transparent pixels close to the chroma key"
|
||||
)
|
||||
|
||||
if areas:
|
||||
row_median = median(areas)
|
||||
for index, area in enumerate(areas[:expected_count]):
|
||||
if row_median > 0 and area < row_median * args.small_outlier_ratio:
|
||||
row_warnings.append(
|
||||
f"{state} frame {index:02d} is much smaller than the row median ({area} vs {row_median:.0f})"
|
||||
)
|
||||
if row_median > 0 and area > row_median * args.large_outlier_ratio:
|
||||
row_warnings.append(
|
||||
f"{state} frame {index:02d} is much larger than the row median ({area} vs {row_median:.0f})"
|
||||
)
|
||||
|
||||
return {
|
||||
"state": state,
|
||||
"expected_frames": expected_count,
|
||||
"actual_frames": len(files),
|
||||
"extraction_method": method,
|
||||
"ok": not row_errors,
|
||||
"errors": row_errors,
|
||||
"warnings": row_warnings,
|
||||
"frames": frames,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--frames-root", required=True)
|
||||
parser.add_argument("--json-out", required=True)
|
||||
parser.add_argument("--min-used-pixels", type=int, default=400)
|
||||
parser.add_argument("--edge-margin", type=int, default=2)
|
||||
parser.add_argument("--edge-pixel-threshold", type=int, default=24)
|
||||
parser.add_argument("--chroma-adjacent-threshold", type=float, default=150.0)
|
||||
parser.add_argument("--chroma-adjacent-pixel-threshold", type=int, default=800)
|
||||
parser.add_argument("--small-outlier-ratio", type=float, default=0.35)
|
||||
parser.add_argument("--large-outlier-ratio", type=float, default=2.75)
|
||||
parser.add_argument(
|
||||
"--require-components",
|
||||
action="store_true",
|
||||
help="Fail rows that fell back to equal-slot extraction.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
frames_root = Path(args.frames_root).expanduser().resolve()
|
||||
manifest_rows = load_manifest(frames_root)
|
||||
chroma_key = load_chroma_key(frames_root)
|
||||
rows = [
|
||||
inspect_state(frames_root, state, count, manifest_rows, chroma_key, args)
|
||||
for state, count in ROW_FRAME_COUNTS.items()
|
||||
]
|
||||
errors = [error for row in rows for error in row["errors"]]
|
||||
warnings = [warning for row in rows for warning in row["warnings"]]
|
||||
result = {
|
||||
"ok": not errors,
|
||||
"frames_root": str(frames_root),
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
json_out = Path(args.json_out).expanduser().resolve()
|
||||
json_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
json_out.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({k: v for k, v in result.items() if k != "rows"}, indent=2))
|
||||
raise SystemExit(0 if result["ok"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a labeled contact sheet from a Codex pet atlas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
COLUMNS = 8
|
||||
ROWS = 9
|
||||
CELL_WIDTH = 192
|
||||
CELL_HEIGHT = 208
|
||||
LABEL_HEIGHT = 22
|
||||
ROW_NAMES = [
|
||||
"idle",
|
||||
"running-right",
|
||||
"running-left",
|
||||
"waving",
|
||||
"jumping",
|
||||
"failed",
|
||||
"waiting",
|
||||
"running",
|
||||
"review",
|
||||
]
|
||||
USED_COUNTS = [6, 8, 8, 4, 5, 8, 6, 6, 6]
|
||||
|
||||
|
||||
def checker(size: tuple[int, int], square: int = 16) -> Image.Image:
|
||||
image = Image.new("RGB", size, "#ffffff")
|
||||
draw = ImageDraw.Draw(image)
|
||||
for y in range(0, size[1], square):
|
||||
for x in range(0, size[0], square):
|
||||
if (x // square + y // square) % 2:
|
||||
draw.rectangle((x, y, x + square - 1, y + square - 1), fill="#e8e8e8")
|
||||
return image
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("atlas")
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--scale", type=float, default=0.5)
|
||||
args = parser.parse_args()
|
||||
|
||||
with Image.open(Path(args.atlas).expanduser().resolve()) as opened:
|
||||
atlas = opened.convert("RGBA")
|
||||
|
||||
cell_w = max(1, round(CELL_WIDTH * args.scale))
|
||||
cell_h = max(1, round(CELL_HEIGHT * args.scale))
|
||||
width = COLUMNS * cell_w
|
||||
height = ROWS * (cell_h + LABEL_HEIGHT)
|
||||
sheet = Image.new("RGB", (width, height), "#f7f7f7")
|
||||
draw = ImageDraw.Draw(sheet)
|
||||
font = ImageFont.load_default()
|
||||
|
||||
for row in range(ROWS):
|
||||
y = row * (cell_h + LABEL_HEIGHT)
|
||||
draw.rectangle((0, y, width, y + LABEL_HEIGHT - 1), fill="#111111")
|
||||
draw.text((6, y + 5), f"row {row}: {ROW_NAMES[row]}", fill="#ffffff", font=font)
|
||||
draw.text(
|
||||
(width - 92, y + 5),
|
||||
f"{USED_COUNTS[row]} frames",
|
||||
fill="#ffffff",
|
||||
font=font,
|
||||
)
|
||||
for column in range(COLUMNS):
|
||||
crop = atlas.crop(
|
||||
(
|
||||
column * CELL_WIDTH,
|
||||
row * CELL_HEIGHT,
|
||||
(column + 1) * CELL_WIDTH,
|
||||
(row + 1) * CELL_HEIGHT,
|
||||
)
|
||||
)
|
||||
crop = crop.resize((cell_w, cell_h), Image.Resampling.LANCZOS)
|
||||
bg = checker((cell_w, cell_h))
|
||||
bg.paste(crop, (0, 0), crop)
|
||||
x = column * cell_w
|
||||
sheet.paste(bg, (x, y + LABEL_HEIGHT))
|
||||
outline = "#18a058" if column < USED_COUNTS[row] else "#cc3344"
|
||||
draw.rectangle(
|
||||
(x, y + LABEL_HEIGHT, x + cell_w - 1, y + LABEL_HEIGHT + cell_h - 1),
|
||||
outline=outline,
|
||||
)
|
||||
draw.text((x + 4, y + LABEL_HEIGHT + 4), str(column), fill="#111111", font=font)
|
||||
|
||||
output = Path(args.output).expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
sheet.save(output)
|
||||
print(f"wrote {output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Package a validated atlas as a local Codex pet."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ATLAS_SIZE = (1536, 1872)
|
||||
|
||||
|
||||
def default_codex_home() -> Path:
|
||||
return Path(os.environ.get("CODEX_HOME") or "~/.codex").expanduser().resolve()
|
||||
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
value = value.strip().lower()
|
||||
value = re.sub(r"[^a-z0-9]+", "-", value)
|
||||
value = re.sub(r"-{2,}", "-", value)
|
||||
return value.strip("-")
|
||||
|
||||
|
||||
def validate_spritesheet(path: Path) -> str:
|
||||
with Image.open(path) as image:
|
||||
if image.size != ATLAS_SIZE:
|
||||
raise SystemExit(
|
||||
f"expected {ATLAS_SIZE[0]}x{ATLAS_SIZE[1]}, got {image.width}x{image.height}"
|
||||
)
|
||||
if image.format not in {"PNG", "WEBP"}:
|
||||
raise SystemExit(f"expected PNG or WebP, got {image.format}")
|
||||
return str(image.format)
|
||||
|
||||
|
||||
def write_webp_spritesheet(source: Path, target: Path, source_format: str) -> None:
|
||||
if source_format == "WEBP":
|
||||
shutil.copy2(source, target)
|
||||
return
|
||||
with Image.open(source) as image:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.convert("RGBA").save(
|
||||
target,
|
||||
format="WEBP",
|
||||
lossless=True,
|
||||
quality=100,
|
||||
method=6,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--pet-name", default="")
|
||||
parser.add_argument("--display-name", default="")
|
||||
parser.add_argument("--description", required=True)
|
||||
parser.add_argument("--spritesheet", required=True)
|
||||
parser.add_argument("--codex-home", default=str(default_codex_home()))
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
help="Exact pet package directory. Defaults to ${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>.",
|
||||
)
|
||||
parser.add_argument("--force", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
raw_pet_name = (args.pet_name or args.display_name).strip()
|
||||
if not raw_pet_name:
|
||||
raise SystemExit("pet name is required")
|
||||
pet_id = slugify(raw_pet_name)
|
||||
if not pet_id:
|
||||
raise SystemExit("pet name must contain at least one letter or digit")
|
||||
display_name = (args.display_name or raw_pet_name).strip()
|
||||
|
||||
source = Path(args.spritesheet).expanduser().resolve()
|
||||
source_format = validate_spritesheet(source)
|
||||
target_dir = (
|
||||
Path(args.output_dir).expanduser().resolve()
|
||||
if args.output_dir
|
||||
else Path(args.codex_home).expanduser().resolve() / "pets" / pet_id
|
||||
)
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
target_sheet = target_dir / "spritesheet.webp"
|
||||
manifest_path = target_dir / "pet.json"
|
||||
if not args.force and (target_sheet.exists() or manifest_path.exists()):
|
||||
raise SystemExit(f"{target_dir} already contains pet files; pass --force to overwrite")
|
||||
|
||||
write_webp_spritesheet(source, target_sheet, source_format)
|
||||
manifest = {
|
||||
"id": pet_id,
|
||||
"displayName": display_name,
|
||||
"description": args.description,
|
||||
"spritesheetPath": target_sheet.name,
|
||||
}
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{"ok": True, "pet_dir": str(target_dir), "manifest": str(manifest_path)}, indent=2
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Show ready and pending $imagegen jobs for a Codex pet run."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_manifest(run_dir: Path) -> dict[str, object]:
|
||||
path = run_dir / "imagegen-jobs.json"
|
||||
if not path.exists():
|
||||
raise SystemExit(f"job manifest not found: {path}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def jobs(manifest: dict[str, object]) -> list[dict[str, object]]:
|
||||
raw = manifest.get("jobs")
|
||||
if not isinstance(raw, list):
|
||||
raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
|
||||
return [job for job in raw if isinstance(job, dict)]
|
||||
|
||||
|
||||
def completed_ids(manifest: dict[str, object]) -> set[str]:
|
||||
return {
|
||||
str(job["id"])
|
||||
for job in jobs(manifest)
|
||||
if job.get("status") == "complete" and isinstance(job.get("id"), str)
|
||||
}
|
||||
|
||||
|
||||
def missing_deps(job: dict[str, object], completed: set[str]) -> list[str]:
|
||||
deps = job.get("depends_on", [])
|
||||
if not isinstance(deps, list):
|
||||
return []
|
||||
return [dep for dep in deps if isinstance(dep, str) and dep not in completed]
|
||||
|
||||
|
||||
def job_view(
|
||||
job: dict[str, object], run_dir: Path, completed: set[str]
|
||||
) -> dict[str, object]:
|
||||
prompt_file = job.get("prompt_file")
|
||||
output_path = job.get("output_path")
|
||||
inputs = (
|
||||
job.get("input_images") if isinstance(job.get("input_images"), list) else []
|
||||
)
|
||||
input_images = []
|
||||
for item in inputs:
|
||||
path = (
|
||||
run_dir / item["path"]
|
||||
if isinstance(item, dict) and isinstance(item.get("path"), str)
|
||||
else None
|
||||
)
|
||||
input_images.append(
|
||||
{
|
||||
"path": str(path) if path else None,
|
||||
"role": item.get("role") if isinstance(item, dict) else None,
|
||||
"exists": path.is_file() if path else False,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"id": job.get("id"),
|
||||
"kind": job.get("kind"),
|
||||
"status": job.get("status", "pending"),
|
||||
"prompt_file": str(run_dir / prompt_file)
|
||||
if isinstance(prompt_file, str)
|
||||
else None,
|
||||
"input_images": input_images,
|
||||
"output_path": str(run_dir / output_path)
|
||||
if isinstance(output_path, str)
|
||||
else None,
|
||||
"missing_dependencies": missing_deps(job, completed),
|
||||
"repair_attempt": job.get("repair_attempt", 0),
|
||||
"generation_skill": job.get("generation_skill"),
|
||||
"requires_grounded_generation": job.get("requires_grounded_generation", False),
|
||||
"allow_prompt_only_generation": job.get("allow_prompt_only_generation", False),
|
||||
"identity_reference_paths": job.get("identity_reference_paths", []),
|
||||
"mirror_policy": job.get("mirror_policy", {}),
|
||||
"derived_from": job.get("derived_from"),
|
||||
"source_provenance": job.get("source_provenance"),
|
||||
"mirror_decision": job.get("mirror_decision"),
|
||||
"recording_owner": job.get("recording_owner", "parent"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
run_dir = Path(args.run_dir).expanduser().resolve()
|
||||
manifest = load_manifest(run_dir)
|
||||
completed = completed_ids(manifest)
|
||||
pending = [
|
||||
job for job in jobs(manifest) if job.get("status", "pending") != "complete"
|
||||
]
|
||||
ready = [job for job in pending if not missing_deps(job, completed)]
|
||||
blocked = [job for job in pending if missing_deps(job, completed)]
|
||||
|
||||
result = {
|
||||
"ok": True,
|
||||
"run_dir": str(run_dir),
|
||||
"counts": {
|
||||
"total": len(jobs(manifest)),
|
||||
"complete": len(completed),
|
||||
"ready": len(ready),
|
||||
"blocked": len(blocked),
|
||||
},
|
||||
"ready_jobs": [job_view(job, run_dir, completed) for job in ready],
|
||||
"blocked_jobs": [job_view(job, run_dir, completed) for job in blocked],
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a Codex pet run folder, prompts, and imagegen job manifest."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
|
||||
ATLAS = {"columns": 8, "rows": 9, "cell_width": 192, "cell_height": 208}
|
||||
ATLAS["width"] = ATLAS["columns"] * ATLAS["cell_width"]
|
||||
ATLAS["height"] = ATLAS["rows"] * ATLAS["cell_height"]
|
||||
|
||||
ROWS = [
|
||||
("idle", 0, 6, "neutral breathing/blinking loop"),
|
||||
("running-right", 1, 8, "rightward locomotion loop"),
|
||||
("running-left", 2, 8, "leftward locomotion loop"),
|
||||
("waving", 3, 4, "greeting gesture with raised wave and return"),
|
||||
("jumping", 4, 5, "anticipation, lift, peak, descent, settle"),
|
||||
("failed", 5, 8, "sad, failed, or deflated reaction"),
|
||||
("waiting", 6, 6, "patient waiting loop with small motion"),
|
||||
("running", 7, 6, "generic in-place running loop"),
|
||||
("review", 8, 6, "focused inspecting or review loop"),
|
||||
]
|
||||
|
||||
TRANSPARENCY_ARTIFACT_RULES = [
|
||||
"Prefer pose, expression, and silhouette changes over decorative effects.",
|
||||
"Effects are allowed only when they are state-relevant, opaque, hard-edged, pixel-style, fully inside the same frame slot, and physically touching or overlapping the pet silhouette.",
|
||||
"Allowed attached effects can include a tear touching the face, a small smoke puff touching the pet or prop, or tiny stars overlapping the pet during a failed/dizzy reaction.",
|
||||
"Do not draw detached effects: floating stars, loose sparkles, floating punctuation, floating icons, falling tear drops, separated smoke clouds, loose dust, disconnected outline bits, or stray pixels.",
|
||||
"Do not draw wave marks, motion arcs, speed lines, action streaks, afterimages, blur, smears, halos, glows, auras, floor patches, cast shadows, contact shadows, drop shadows, oval floor shadows, landing marks, or impact bursts.",
|
||||
"Do not include text, labels, frame numbers, visible grids, guide marks, speech bubbles, thought bubbles, UI panels, code snippets, scenery, checkerboard transparency, white backgrounds, or black backgrounds.",
|
||||
"Do not use the chroma-key color or chroma-key-adjacent colors in the pet, prop, effects, highlights, shadows, or outlines.",
|
||||
"Reject any pose that is cropped, overlaps another pose, crosses into a neighboring frame slot, or creates a separate disconnected component that is not attached to the pet.",
|
||||
]
|
||||
|
||||
STATE_REQUIREMENTS = {
|
||||
"waving": [
|
||||
"Show the greeting through paw pose only: paw down, paw raised, paw tilted, paw returning.",
|
||||
"Do not draw wave marks, motion arcs, lines, sparkles, symbols, or floating effects around the paw.",
|
||||
],
|
||||
"jumping": [
|
||||
"Show the jump through pose and vertical body position only: anticipation, lift, airborne peak, descent, settle.",
|
||||
"Do not draw ground shadows, contact shadows, drop shadows, oval shadows, landing marks, dust, smears, bounce pads, or motion marks under the pet.",
|
||||
"Keep the background outside the pet perfectly flat chroma key with no darker key-colored patches.",
|
||||
],
|
||||
"failed": [
|
||||
"Show failure through slumped pose, drooping ears/limbs, closed or sad eyes, and lower body position.",
|
||||
"Tears, small smoke puffs, or tiny stars are allowed only if attached to or overlapping the pet silhouette and kept inside the same frame slot.",
|
||||
"Do not draw red X marks, floating symbols, detached stars, separated smoke clouds, falling tear drops, dust, or other loose effects.",
|
||||
],
|
||||
"review": [
|
||||
"Show review through lean, blink, narrowed eyes, head tilt, or paw position.",
|
||||
"Do not add magnifying glasses, papers, code, UI, punctuation, symbols, or other new props unless they already exist in the base pet identity.",
|
||||
],
|
||||
"running-right": [
|
||||
"Show locomotion through body, limb, and prop movement only.",
|
||||
"Do not draw speed lines, dust clouds, floor shadows, motion trails, or detached motion effects.",
|
||||
],
|
||||
"running-left": [
|
||||
"Show locomotion through body, limb, and prop movement only.",
|
||||
"Do not draw speed lines, dust clouds, floor shadows, motion trails, or detached motion effects.",
|
||||
],
|
||||
"running": [
|
||||
"Show in-place running through body, limb, and prop movement only.",
|
||||
"Do not draw speed lines, dust clouds, floor shadows, motion trails, or detached motion effects.",
|
||||
],
|
||||
}
|
||||
|
||||
DIGITAL_PET_STYLE = (
|
||||
"Codex digital pet sprite style: pixel-art-adjacent low-resolution mascot sprite, "
|
||||
"compact chibi proportions, chunky whole-body silhouette, thick dark 1-2 px outline, "
|
||||
"visible stepped/pixel edges, limited palette, flat cel shading with at most one "
|
||||
"small highlight and one shadow step, simple readable face, tiny limbs, and no "
|
||||
"detail that disappears at 192x208. Avoid polished illustration, painterly "
|
||||
"rendering, anime key art, 3D render, vector app-icon polish, glossy lighting, "
|
||||
"soft gradients, realistic fur or material texture, anti-aliased high-detail "
|
||||
"edges, and complex tiny accessories."
|
||||
)
|
||||
|
||||
CHROMA_KEY_CANDIDATES = [
|
||||
("magenta", "#FF00FF"),
|
||||
("cyan", "#00FFFF"),
|
||||
("yellow", "#FFFF00"),
|
||||
("blue", "#0000FF"),
|
||||
("orange", "#FF7F00"),
|
||||
("green", "#00FF00"),
|
||||
]
|
||||
|
||||
DEFAULT_PET_NAME = "Sprout"
|
||||
CANONICAL_BASE_PATH = "references/canonical-base.png"
|
||||
LAYOUT_GUIDE_DIR = "references/layout-guides"
|
||||
LAYOUT_GUIDE_SAFE_MARGIN_X = 18
|
||||
LAYOUT_GUIDE_SAFE_MARGIN_Y = 16
|
||||
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
value = value.strip().lower()
|
||||
value = re.sub(r"[^a-z0-9]+", "-", value)
|
||||
value = re.sub(r"-{2,}", "-", value)
|
||||
return value.strip("-")
|
||||
|
||||
|
||||
def display_from_slug(value: str) -> str:
|
||||
words = [word for word in re.split(r"[^a-zA-Z0-9]+", value.strip()) if word]
|
||||
return " ".join(word.capitalize() for word in words)
|
||||
|
||||
|
||||
def concept_words(value: str) -> list[str]:
|
||||
stop_words = {
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"app",
|
||||
"based",
|
||||
"codex",
|
||||
"compact",
|
||||
"digital",
|
||||
"for",
|
||||
"from",
|
||||
"in",
|
||||
"of",
|
||||
"on",
|
||||
"pet",
|
||||
"ready",
|
||||
"small",
|
||||
"the",
|
||||
"to",
|
||||
"with",
|
||||
}
|
||||
words = [
|
||||
word.lower()
|
||||
for word in re.findall(r"[a-zA-Z0-9]+", value)
|
||||
if word.lower() not in stop_words
|
||||
]
|
||||
return words
|
||||
|
||||
|
||||
def infer_name(args: argparse.Namespace, reference_paths: list[Path]) -> str:
|
||||
for raw_value in [args.display_name, args.pet_name]:
|
||||
value = raw_value.strip()
|
||||
if value:
|
||||
return value
|
||||
|
||||
if args.pet_id.strip():
|
||||
display = display_from_slug(args.pet_id)
|
||||
if display:
|
||||
return display
|
||||
|
||||
for raw_value in [args.pet_notes, args.description]:
|
||||
words = concept_words(raw_value)
|
||||
if words:
|
||||
return words[0].capitalize()
|
||||
|
||||
for path in reference_paths:
|
||||
display = display_from_slug(path.stem)
|
||||
if display:
|
||||
return display
|
||||
|
||||
return DEFAULT_PET_NAME
|
||||
|
||||
|
||||
def sentence(value: str) -> str:
|
||||
value = " ".join(value.strip().split())
|
||||
if not value:
|
||||
return value
|
||||
if value[-1] not in ".!?":
|
||||
value += "."
|
||||
return value
|
||||
|
||||
|
||||
def infer_description(args: argparse.Namespace, reference_paths: list[Path]) -> str:
|
||||
if args.description.strip():
|
||||
return sentence(args.description)
|
||||
if args.pet_notes.strip():
|
||||
return sentence(f"A compact Codex digital pet: {args.pet_notes}")
|
||||
if reference_paths:
|
||||
return "A compact Codex digital pet based on the provided reference image."
|
||||
return "A compact original Codex digital pet ready for animation."
|
||||
|
||||
|
||||
def infer_pet_notes(args: argparse.Namespace, reference_paths: list[Path]) -> str:
|
||||
if args.pet_notes.strip():
|
||||
return args.pet_notes.strip()
|
||||
if args.description.strip():
|
||||
return args.description.strip().rstrip(".")
|
||||
if reference_paths:
|
||||
return "the pet shown in the reference image(s)"
|
||||
return "a compact original Codex digital pet"
|
||||
|
||||
|
||||
def default_output_dir(pet_id: str) -> Path:
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
return Path.cwd() / "output" / "hatch-pet" / f"{pet_id}-{timestamp}"
|
||||
|
||||
|
||||
def rel(path: Path, root: Path) -> str:
|
||||
return str(path.resolve().relative_to(root.resolve()))
|
||||
|
||||
|
||||
def image_metadata(path: Path) -> dict[str, object]:
|
||||
with Image.open(path) as image:
|
||||
return {
|
||||
"path": str(path),
|
||||
"width": image.width,
|
||||
"height": image.height,
|
||||
"mode": image.mode,
|
||||
"format": image.format,
|
||||
}
|
||||
|
||||
|
||||
def draw_dashed_line(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
start: tuple[int, int],
|
||||
end: tuple[int, int],
|
||||
*,
|
||||
fill: str,
|
||||
dash: int = 8,
|
||||
gap: int = 6,
|
||||
) -> None:
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
if x1 == x2:
|
||||
step = dash + gap
|
||||
for y in range(min(y1, y2), max(y1, y2), step):
|
||||
draw.line((x1, y, x2, min(y + dash, max(y1, y2))), fill=fill)
|
||||
return
|
||||
if y1 == y2:
|
||||
step = dash + gap
|
||||
for x in range(min(x1, x2), max(x1, x2), step):
|
||||
draw.line((x, y1, min(x + dash, max(x1, x2)), y2), fill=fill)
|
||||
return
|
||||
raise ValueError("draw_dashed_line only supports horizontal or vertical lines")
|
||||
|
||||
|
||||
def create_layout_guide(path: Path, state: str, frames: int) -> dict[str, object]:
|
||||
width = frames * ATLAS["cell_width"]
|
||||
height = ATLAS["cell_height"]
|
||||
cell_width = ATLAS["cell_width"]
|
||||
image = Image.new("RGB", (width, height), "#f7f7f7")
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
for index in range(frames):
|
||||
left = index * cell_width
|
||||
right = left + cell_width - 1
|
||||
draw.rectangle((left, 0, right, height - 1), outline="#111111", width=2)
|
||||
|
||||
safe_left = left + LAYOUT_GUIDE_SAFE_MARGIN_X
|
||||
safe_top = LAYOUT_GUIDE_SAFE_MARGIN_Y
|
||||
safe_right = right - LAYOUT_GUIDE_SAFE_MARGIN_X
|
||||
safe_bottom = height - 1 - LAYOUT_GUIDE_SAFE_MARGIN_Y
|
||||
draw.rectangle(
|
||||
(safe_left, safe_top, safe_right, safe_bottom),
|
||||
outline="#2f80ed",
|
||||
width=2,
|
||||
)
|
||||
|
||||
center_x = left + cell_width // 2
|
||||
center_y = height // 2
|
||||
draw_dashed_line(
|
||||
draw,
|
||||
(center_x, safe_top),
|
||||
(center_x, safe_bottom),
|
||||
fill="#b8b8b8",
|
||||
)
|
||||
draw_dashed_line(
|
||||
draw,
|
||||
(safe_left, center_y),
|
||||
(safe_right, center_y),
|
||||
fill="#b8b8b8",
|
||||
)
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.save(path)
|
||||
return {
|
||||
"state": state,
|
||||
"path": str(path),
|
||||
"width": width,
|
||||
"height": height,
|
||||
"frames": frames,
|
||||
"cell_width": ATLAS["cell_width"],
|
||||
"cell_height": ATLAS["cell_height"],
|
||||
"safe_margin_x": LAYOUT_GUIDE_SAFE_MARGIN_X,
|
||||
"safe_margin_y": LAYOUT_GUIDE_SAFE_MARGIN_Y,
|
||||
"usage": "layout guide input only; do not copy visible guide lines into generated sprite strips",
|
||||
}
|
||||
|
||||
|
||||
def create_layout_guides(run_dir: Path) -> list[dict[str, object]]:
|
||||
guide_dir = run_dir / LAYOUT_GUIDE_DIR
|
||||
return [
|
||||
create_layout_guide(guide_dir / f"{state}.png", state, frames)
|
||||
for state, _row, frames, _purpose in ROWS
|
||||
]
|
||||
|
||||
|
||||
def parse_hex_color(value: str) -> tuple[int, int, int]:
|
||||
if not re.fullmatch(r"#[0-9a-fA-F]{6}", value):
|
||||
raise SystemExit(f"invalid chroma key color: {value}; expected #RRGGBB")
|
||||
return tuple(int(value[index : index + 2], 16) for index in (1, 3, 5))
|
||||
|
||||
|
||||
def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
||||
return f"#{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}"
|
||||
|
||||
|
||||
def color_distance(left: tuple[int, int, int], right: tuple[int, int, int]) -> float:
|
||||
return math.sqrt(sum((left[index] - right[index]) ** 2 for index in range(3)))
|
||||
|
||||
|
||||
def sampled_reference_pixels(paths: list[Path]) -> list[tuple[int, int, int]]:
|
||||
pixels: list[tuple[int, int, int]] = []
|
||||
for path in paths:
|
||||
with Image.open(path) as opened:
|
||||
image = opened.convert("RGBA")
|
||||
image.thumbnail((128, 128), Image.Resampling.LANCZOS)
|
||||
data = image.tobytes()
|
||||
for index in range(0, len(data), 4):
|
||||
red, green, blue, alpha = data[index : index + 4]
|
||||
if alpha <= 16:
|
||||
continue
|
||||
pixels.append((red, green, blue))
|
||||
|
||||
non_background = [
|
||||
pixel
|
||||
for pixel in pixels
|
||||
if not (pixel[0] > 244 and pixel[1] > 244 and pixel[2] > 244)
|
||||
]
|
||||
return non_background or pixels
|
||||
|
||||
|
||||
def choose_chroma_key(reference_paths: list[Path], requested: str) -> dict[str, object]:
|
||||
if requested.lower() != "auto":
|
||||
rgb = parse_hex_color(requested)
|
||||
return {
|
||||
"hex": rgb_to_hex(rgb),
|
||||
"rgb": list(rgb),
|
||||
"name": "user-selected",
|
||||
"selection": "manual",
|
||||
}
|
||||
|
||||
pixels = sampled_reference_pixels(reference_paths)
|
||||
if not pixels:
|
||||
rgb = parse_hex_color("#FF00FF")
|
||||
return {
|
||||
"hex": "#FF00FF",
|
||||
"rgb": list(rgb),
|
||||
"name": "magenta",
|
||||
"selection": "fallback",
|
||||
}
|
||||
|
||||
scored: list[tuple[float, int, str, tuple[int, int, int]]] = []
|
||||
for preference_index, (name, hex_color) in enumerate(CHROMA_KEY_CANDIDATES):
|
||||
rgb = parse_hex_color(hex_color)
|
||||
distances = sorted(color_distance(rgb, pixel) for pixel in pixels)
|
||||
percentile_index = max(0, min(len(distances) - 1, int(len(distances) * 0.01)))
|
||||
scored.append((distances[percentile_index], -preference_index, name, rgb))
|
||||
|
||||
score, _preference, name, rgb = max(scored)
|
||||
return {
|
||||
"hex": rgb_to_hex(rgb),
|
||||
"rgb": list(rgb),
|
||||
"name": name,
|
||||
"selection": "auto",
|
||||
"score": round(score, 2),
|
||||
}
|
||||
|
||||
|
||||
def write_text(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text.rstrip() + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def resolved_style_notes(raw_style_notes: str) -> str:
|
||||
raw_style_notes = raw_style_notes.strip()
|
||||
if not raw_style_notes:
|
||||
return DIGITAL_PET_STYLE
|
||||
return f"{DIGITAL_PET_STYLE} Additional user style notes: {raw_style_notes}."
|
||||
|
||||
|
||||
def base_pet_prompt(args: argparse.Namespace) -> str:
|
||||
pet_notes = args.pet_notes or "the pet shown in the reference image(s)"
|
||||
style_notes = resolved_style_notes(args.style_notes)
|
||||
chroma_key = args.chroma_key["hex"]
|
||||
chroma_name = args.chroma_key["name"]
|
||||
return f"""Create a single clean reference sprite for a Codex app digital pet named {args.display_name}.
|
||||
|
||||
Pet: {pet_notes}.
|
||||
Style contract: {style_notes}
|
||||
|
||||
Use this prompt as an authoritative sprite-production spec. Do not expand it into a polished illustration, painterly character image, anime key art, 3D render, vector mascot, glossy app icon, realistic animal portrait, or marketing artwork.
|
||||
|
||||
Output one centered full-body pet sprite pose only, on a perfectly flat pure {chroma_name} {chroma_key} chroma-key background. The pet must be fully visible, readable as a tiny digital pet, and suitable for animation into a 192x208 sprite cell. Do not include scenery, text, labels, borders, checkerboard transparency, detached effects, shadows, glows, or extra props not present in the reference unless explicitly requested. Do not use {chroma_key}, pure {chroma_name}, or colors close to that chroma key in the pet, prop, highlights, or effects."""
|
||||
|
||||
|
||||
def row_prompt(
|
||||
args: argparse.Namespace, state: str, row: int, frames: int, purpose: str
|
||||
) -> str:
|
||||
pet_notes = args.pet_notes or "the same pet from the approved base reference"
|
||||
style_notes = resolved_style_notes(args.style_notes)
|
||||
chroma_key = args.chroma_key["hex"]
|
||||
chroma_name = args.chroma_key["name"]
|
||||
state_requirements = STATE_REQUIREMENTS.get(state, [])
|
||||
state_requirement_text = ""
|
||||
if state_requirements:
|
||||
state_requirement_text = "\n\nState-specific requirements:\n" + "\n".join(
|
||||
f"- {requirement}" for requirement in state_requirements
|
||||
)
|
||||
transparency_artifact_text = "\n".join(
|
||||
f"- {requirement}" for requirement in TRANSPARENCY_ARTIFACT_RULES
|
||||
)
|
||||
return f"""Create a single horizontal sprite strip for the Codex app digital pet `{args.pet_id}` in the state `{state}`.
|
||||
|
||||
Use the attached reference image(s) for pet identity and the attached base pet image as the canonical design. Use the attached layout guide image only for frame count, slot spacing, centering, and safe padding. Simplify any high-resolution reference details into the Codex digital pet sprite style. Do not simply copy the still reference pose. Generate distinct animation poses that create a readable cycle.
|
||||
|
||||
Identity lock:
|
||||
- Do not redesign the pet. Only change pose/action for the `{state}` animation.
|
||||
- Preserve the exact head shape, ear/horn/limb shape, face design, markings, palette, outline weight, body proportions, prop design, and overall silhouette from the canonical base pet.
|
||||
- Keep every frame recognizably the same individual pet, not a related variant.
|
||||
- If the pet has a prop or accessory, preserve its size, side, palette, and attachment style unless the row action requires a small pose-only adjustment.
|
||||
- Prefer a subtler animation over any change that mutates the pet identity.
|
||||
|
||||
Output exactly {frames} separate animation frames arranged left-to-right in one single row. Each frame must show the same pet: {pet_notes}.
|
||||
|
||||
Style contract: {style_notes}
|
||||
|
||||
Use this prompt as an authoritative sprite-production spec. Do not expand it into a polished illustration, painterly character image, anime key art, 3D render, vector mascot, glossy app icon, realistic animal portrait, or marketing artwork.
|
||||
|
||||
Animation action: {purpose}.
|
||||
{state_requirement_text}
|
||||
|
||||
Transparency and artifact rules:
|
||||
{transparency_artifact_text}
|
||||
|
||||
Layout requirements:
|
||||
- Exactly {frames} full-body frames, left to right, in one horizontal row.
|
||||
- The attached layout guide shows the {frames} frame boxes and inner safe area for this row. Follow its slot count, spacing, centering, and padding.
|
||||
- Do not reproduce the layout guide itself: no visible boxes, guide lines, center marks, labels, guide colors, or guide background may appear in the output.
|
||||
- Treat the image as {frames} equal-width invisible frame slots. Fill every slot: each requested slot must contain exactly one complete full-body pose.
|
||||
- Spread the {frames} poses evenly across the whole image width. Do not leave any requested slot blank or create large empty gaps between poses.
|
||||
- Center one complete pose in each slot. No pose may cross into the neighboring slot.
|
||||
- Use a perfectly flat pure {chroma_name} {chroma_key} chroma-key background across the whole image.
|
||||
- Do not draw visible grid lines, borders, labels, numbers, text, watermarks, or checkerboard transparency.
|
||||
- Do not include scenery or a background environment.
|
||||
- Keep the rendering sprite-like: chunky silhouette, dark pixel-style outline, limited palette, flat shading, minimal tiny detail.
|
||||
- Do not use {chroma_key}, pure {chroma_name}, or colors close to that chroma key in the pet, props, highlights, shadows, motion marks, dust, landing marks, or effects.
|
||||
- Do not draw shadows, glows, smears, dust, or landing marks using darker/lighter versions of the chroma-key color.
|
||||
- Keep every frame self-contained with safe padding. No pet body part should be clipped by the frame slot.
|
||||
- Avoid motion blur. Use clear pose changes readable at 192x208.
|
||||
- Preserve the same silhouette, face, proportions, palette, material, and props across every frame."""
|
||||
|
||||
|
||||
def make_jobs(
|
||||
run_dir: Path, copied_refs: list[dict[str, object]]
|
||||
) -> list[dict[str, object]]:
|
||||
reference_inputs = [
|
||||
{"path": rel(Path(str(ref["copied_path"])), run_dir), "role": "pet reference"}
|
||||
for ref in copied_refs
|
||||
]
|
||||
identity_reference_paths = [CANONICAL_BASE_PATH, "decoded/base.png"]
|
||||
jobs: list[dict[str, object]] = [
|
||||
{
|
||||
"id": "base",
|
||||
"kind": "base-pet",
|
||||
"status": "pending",
|
||||
"prompt_file": "prompts/base-pet.md",
|
||||
"input_images": reference_inputs,
|
||||
"output_path": "decoded/base.png",
|
||||
"depends_on": [],
|
||||
"generation_skill": "$imagegen",
|
||||
"requires_grounded_generation": bool(reference_inputs),
|
||||
"allow_prompt_only_generation": not reference_inputs,
|
||||
"recording_owner": "parent",
|
||||
}
|
||||
]
|
||||
for state, _row, frames, _purpose in ROWS:
|
||||
depends_on = ["base"]
|
||||
extra_inputs: list[dict[str, str]] = []
|
||||
mirror_policy: dict[str, object] = {}
|
||||
if state == "running-left":
|
||||
depends_on.append("running-right")
|
||||
extra_inputs.append(
|
||||
{
|
||||
"path": "decoded/running-right.png",
|
||||
"role": "rightward gait reference for leftward row decision",
|
||||
}
|
||||
)
|
||||
mirror_policy = {
|
||||
"may_derive_from": "running-right",
|
||||
"derivation": "horizontal-mirror",
|
||||
"requires_explicit_approval": True,
|
||||
"fallback_generation_skill": "$imagegen",
|
||||
}
|
||||
jobs.append(
|
||||
{
|
||||
"id": state,
|
||||
"kind": "row-strip",
|
||||
"status": "pending",
|
||||
"prompt_file": f"prompts/rows/{state}.md",
|
||||
"input_images": [
|
||||
*reference_inputs,
|
||||
{
|
||||
"path": f"{LAYOUT_GUIDE_DIR}/{state}.png",
|
||||
"role": f"layout guide for {frames} frame slots; use for spacing only, do not copy guide lines",
|
||||
},
|
||||
{
|
||||
"path": CANONICAL_BASE_PATH,
|
||||
"role": "canonical identity reference",
|
||||
},
|
||||
{"path": "decoded/base.png", "role": "approved base pet"},
|
||||
*extra_inputs,
|
||||
],
|
||||
"output_path": f"decoded/{state}.png",
|
||||
"depends_on": depends_on,
|
||||
"generation_skill": "$imagegen",
|
||||
"requires_grounded_generation": True,
|
||||
"allow_prompt_only_generation": False,
|
||||
"identity_reference_paths": identity_reference_paths,
|
||||
"parallelizable_after": depends_on,
|
||||
"mirror_policy": mirror_policy,
|
||||
"recording_owner": "parent",
|
||||
}
|
||||
)
|
||||
return jobs
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--pet-name",
|
||||
default="",
|
||||
help="User-facing pet name. Ask the user for this when practical; otherwise choose a short appropriate name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pet-id",
|
||||
default="",
|
||||
help="Stable pet folder/id slug. Defaults to the slugified pet name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--display-name",
|
||||
default="",
|
||||
help="Display label. Defaults to the pet name.",
|
||||
)
|
||||
parser.add_argument("--description", default="")
|
||||
parser.add_argument("--reference", action="append", default=[])
|
||||
parser.add_argument("--output-dir", default="")
|
||||
parser.add_argument("--pet-notes", default="")
|
||||
parser.add_argument("--style-notes", default="")
|
||||
parser.add_argument(
|
||||
"--chroma-key",
|
||||
default="auto",
|
||||
help="Chroma key as #RRGGBB, or auto to choose a safe key from reference colors.",
|
||||
)
|
||||
parser.add_argument("--force", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
raw_reference_paths = [
|
||||
Path(raw_path).expanduser().resolve() for raw_path in args.reference
|
||||
]
|
||||
|
||||
args.display_name = infer_name(args, raw_reference_paths)
|
||||
args.pet_name = (args.pet_name or args.display_name).strip()
|
||||
args.description = infer_description(args, raw_reference_paths)
|
||||
args.pet_notes = infer_pet_notes(args, raw_reference_paths)
|
||||
args.pet_id = slugify(args.pet_id or args.pet_name or args.display_name)
|
||||
if not args.pet_id:
|
||||
raise SystemExit("pet id must contain at least one letter or digit")
|
||||
|
||||
run_dir = (
|
||||
Path(args.output_dir).expanduser().resolve()
|
||||
if args.output_dir
|
||||
else default_output_dir(args.pet_id).resolve()
|
||||
)
|
||||
if run_dir.exists() and any(run_dir.iterdir()) and not args.force:
|
||||
raise SystemExit(
|
||||
f"{run_dir} already exists and is not empty; pass --force to reuse it"
|
||||
)
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ref_dir = run_dir / "references"
|
||||
prompt_dir = run_dir / "prompts"
|
||||
row_prompt_dir = prompt_dir / "rows"
|
||||
for directory in [
|
||||
ref_dir,
|
||||
prompt_dir,
|
||||
row_prompt_dir,
|
||||
run_dir / "decoded",
|
||||
run_dir / "qa",
|
||||
]:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied_refs: list[dict[str, object]] = []
|
||||
copied_ref_paths: list[Path] = []
|
||||
for index, source in enumerate(raw_reference_paths, start=1):
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"reference not found: {source}")
|
||||
suffix = source.suffix.lower() or ".png"
|
||||
copied = ref_dir / f"reference-{index:02d}{suffix}"
|
||||
shutil.copy2(source, copied)
|
||||
meta = image_metadata(copied)
|
||||
meta["source_path"] = str(source)
|
||||
meta["copied_path"] = str(copied)
|
||||
copied_refs.append(meta)
|
||||
copied_ref_paths.append(copied)
|
||||
|
||||
args.chroma_key = choose_chroma_key(copied_ref_paths, args.chroma_key)
|
||||
layout_guides = create_layout_guides(run_dir)
|
||||
|
||||
request = {
|
||||
"pet_id": args.pet_id,
|
||||
"display_name": args.display_name,
|
||||
"description": args.description,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"atlas": ATLAS,
|
||||
"rows": [
|
||||
{"state": state, "row": row, "frames": frames, "purpose": purpose}
|
||||
for state, row, frames, purpose in ROWS
|
||||
],
|
||||
"layout_guides": [
|
||||
{**guide, "path": rel(Path(str(guide["path"])), run_dir)}
|
||||
for guide in layout_guides
|
||||
],
|
||||
"references": copied_refs,
|
||||
"chroma_key": args.chroma_key,
|
||||
"pet_notes": args.pet_notes,
|
||||
"style_notes": args.style_notes,
|
||||
"house_style": DIGITAL_PET_STYLE,
|
||||
"primary_generation_skill": "$imagegen",
|
||||
}
|
||||
(run_dir / "pet_request.json").write_text(
|
||||
json.dumps(request, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
write_text(prompt_dir / "base-pet.md", base_pet_prompt(args))
|
||||
for state, row, frames, purpose in ROWS:
|
||||
write_text(
|
||||
row_prompt_dir / f"{state}.md",
|
||||
row_prompt(args, state, row, frames, purpose),
|
||||
)
|
||||
|
||||
jobs = {
|
||||
"schema_version": 1,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"run_dir": str(run_dir),
|
||||
"primary_generation_skill": "$imagegen",
|
||||
"jobs": make_jobs(run_dir, copied_refs),
|
||||
}
|
||||
(run_dir / "imagegen-jobs.json").write_text(
|
||||
json.dumps(jobs, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"run_dir": str(run_dir),
|
||||
"request": str(run_dir / "pet_request.json"),
|
||||
"jobs": str(run_dir / "imagegen-jobs.json"),
|
||||
"ready_jobs": ["base"],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reopen failed Codex pet row jobs after frame QA."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, object]:
|
||||
if not path.exists():
|
||||
raise SystemExit(f"file not found: {path}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def rows_to_repair(
|
||||
review: dict[str, object], *, repair_on_warnings: bool
|
||||
) -> list[dict[str, object]]:
|
||||
rows = review.get("rows")
|
||||
if not isinstance(rows, list):
|
||||
raise SystemExit("review does not contain row-level results")
|
||||
|
||||
repairs: list[dict[str, object]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict) or not isinstance(row.get("state"), str):
|
||||
continue
|
||||
errors = row.get("errors") if isinstance(row.get("errors"), list) else []
|
||||
warnings = row.get("warnings") if isinstance(row.get("warnings"), list) else []
|
||||
if errors or (repair_on_warnings and warnings):
|
||||
repairs.append(
|
||||
{
|
||||
"state": row["state"],
|
||||
"reason": "; ".join(str(item) for item in [*errors, *warnings])
|
||||
or "the row did not pass visual QA",
|
||||
}
|
||||
)
|
||||
return repairs
|
||||
|
||||
|
||||
def append_repair_note(run_dir: Path, state: str, attempt: int, reason: str) -> None:
|
||||
prompt_path = run_dir / "prompts" / "rows" / f"{state}.md"
|
||||
if not prompt_path.exists():
|
||||
raise SystemExit(f"row prompt not found: {prompt_path}")
|
||||
existing = prompt_path.read_text(encoding="utf-8")
|
||||
note = f"""
|
||||
|
||||
Repair attempt {attempt}:
|
||||
- The previous `{state}` strip failed QA: {reason}
|
||||
- Regenerate the entire row, not just one pose.
|
||||
- Fill every requested frame slot with one complete centered full-body pet pose.
|
||||
- Keep large gaps of pure chroma key only between slots; do not leave a requested slot empty.
|
||||
- Avoid pose overlap, clipping, edge slivers, extra partial sprites, and detached fragments from neighboring poses.
|
||||
- Use the canonical base image and any original references listed in `imagegen-jobs.json` as grounding inputs.
|
||||
- Do not redesign the pet. Keep the exact same head shape, face design, markings, body proportions, palette, outline weight, materials, and props as the approved base pet.
|
||||
- If the contact sheet shows identity drift, repair only this row while preserving the canonical base identity.
|
||||
"""
|
||||
prompt_path.write_text(existing.rstrip() + note.rstrip() + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
|
||||
jobs = manifest.get("jobs")
|
||||
if not isinstance(jobs, list):
|
||||
raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
|
||||
return [job for job in jobs if isinstance(job, dict)]
|
||||
|
||||
|
||||
def next_archive_path(archive_dir: Path, state: str, attempt: int, suffix: str) -> Path:
|
||||
candidate = archive_dir / f"{state}-attempt-{attempt}-previous{suffix}"
|
||||
if not candidate.exists():
|
||||
return candidate
|
||||
counter = 2
|
||||
while True:
|
||||
candidate = archive_dir / f"{state}-attempt-{attempt}-previous-{counter}{suffix}"
|
||||
if not candidate.exists():
|
||||
return candidate
|
||||
counter += 1
|
||||
|
||||
|
||||
def archive_decoded_output(run_dir: Path, job: dict[str, object], state: str, attempt: int) -> str | None:
|
||||
output_raw = job.get("output_path")
|
||||
output = (
|
||||
run_dir / output_raw
|
||||
if isinstance(output_raw, str) and output_raw
|
||||
else run_dir / "decoded" / f"{state}.png"
|
||||
)
|
||||
if not output.exists():
|
||||
return None
|
||||
archive_dir = run_dir / "decoded" / "repair-archive"
|
||||
archive_dir.mkdir(parents=True, exist_ok=True)
|
||||
archived = next_archive_path(archive_dir, state, attempt, output.suffix or ".png")
|
||||
shutil.move(str(output), archived)
|
||||
return str(archived.relative_to(run_dir))
|
||||
|
||||
|
||||
def queue_repair(manifest: dict[str, object], run_dir: Path, state: str, reason: str) -> dict[str, object]:
|
||||
for job in job_list(manifest):
|
||||
if job.get("id") != state:
|
||||
continue
|
||||
attempt = int(job.get("repair_attempt", 0)) + 1
|
||||
archived_output = archive_decoded_output(run_dir, job, state, attempt)
|
||||
job["status"] = "pending"
|
||||
job["repair_attempt"] = attempt
|
||||
job["repair_reason"] = reason
|
||||
job["queued_at"] = datetime.now(timezone.utc).isoformat()
|
||||
if archived_output is not None:
|
||||
previous_outputs = job.setdefault("previous_outputs", [])
|
||||
if not isinstance(previous_outputs, list):
|
||||
previous_outputs = []
|
||||
job["previous_outputs"] = previous_outputs
|
||||
previous_outputs.append(
|
||||
{
|
||||
"attempt": attempt,
|
||||
"path": archived_output,
|
||||
"archived_at": job["queued_at"],
|
||||
}
|
||||
)
|
||||
for key in [
|
||||
"source_path",
|
||||
"source_provenance",
|
||||
"source_sha256",
|
||||
"output_sha256",
|
||||
"completed_at",
|
||||
"metadata",
|
||||
"synthetic_test_source",
|
||||
"secondary_fallback",
|
||||
"derived_from",
|
||||
"mirror_decision",
|
||||
]:
|
||||
job.pop(key, None)
|
||||
result: dict[str, object] = {"attempt": attempt}
|
||||
if archived_output is not None:
|
||||
result["archived_output"] = archived_output
|
||||
return result
|
||||
raise SystemExit(f"unknown row job id: {state}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument("--review", default="")
|
||||
parser.add_argument("--repair-on-warnings", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_dir = Path(args.run_dir).expanduser().resolve()
|
||||
review_path = (
|
||||
Path(args.review).expanduser().resolve()
|
||||
if args.review
|
||||
else run_dir / "qa" / "review.json"
|
||||
)
|
||||
manifest_path = run_dir / "imagegen-jobs.json"
|
||||
review = load_json(review_path)
|
||||
manifest = load_json(manifest_path)
|
||||
|
||||
repairs = rows_to_repair(review, repair_on_warnings=args.repair_on_warnings)
|
||||
queued: list[dict[str, object]] = []
|
||||
for repair in repairs:
|
||||
state = str(repair["state"])
|
||||
reason = str(repair["reason"])
|
||||
queued_repair = queue_repair(manifest, run_dir, state, reason)
|
||||
attempt = int(queued_repair["attempt"])
|
||||
append_repair_note(run_dir, state, attempt, reason)
|
||||
queued.append({"state": state, "reason": reason, **queued_repair})
|
||||
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"ok": True, "queued": queued}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record a selected $imagegen output for a Codex pet generation job."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
CANONICAL_BASE_PATH = "references/canonical-base.png"
|
||||
|
||||
|
||||
def load_jobs(path: Path) -> dict[str, object]:
|
||||
if not path.exists():
|
||||
raise SystemExit(f"job manifest not found: {path}")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
|
||||
jobs = manifest.get("jobs")
|
||||
if not isinstance(jobs, list):
|
||||
raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
|
||||
return [job for job in jobs if isinstance(job, dict)]
|
||||
|
||||
|
||||
def find_job(manifest: dict[str, object], job_id: str) -> dict[str, object]:
|
||||
for job in job_list(manifest):
|
||||
if job.get("id") == job_id:
|
||||
return job
|
||||
raise SystemExit(f"unknown job id: {job_id}")
|
||||
|
||||
|
||||
def image_metadata(path: Path) -> dict[str, object]:
|
||||
with Image.open(path) as image:
|
||||
image.verify()
|
||||
with Image.open(path) as image:
|
||||
return {
|
||||
"width": image.width,
|
||||
"height": image.height,
|
||||
"mode": image.mode,
|
||||
"format": image.format,
|
||||
}
|
||||
|
||||
|
||||
def file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as file:
|
||||
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def manifest_relative(path: Path, run_dir: Path) -> str:
|
||||
return str(path.resolve().relative_to(run_dir.resolve()))
|
||||
|
||||
|
||||
def completed_job_ids(manifest: dict[str, object]) -> set[str]:
|
||||
return {
|
||||
str(job["id"])
|
||||
for job in job_list(manifest)
|
||||
if job.get("status") == "complete" and isinstance(job.get("id"), str)
|
||||
}
|
||||
|
||||
|
||||
def is_relative_to(path: Path, root: Path) -> bool:
|
||||
try:
|
||||
path.relative_to(root)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def default_generated_images_root() -> Path:
|
||||
codex_home = Path(os.environ.get("CODEX_HOME") or "~/.codex").expanduser().resolve()
|
||||
return codex_home / "generated_images"
|
||||
|
||||
|
||||
def validate_source_path(
|
||||
*,
|
||||
source: Path,
|
||||
run_dir: Path,
|
||||
allow_synthetic_test_source: bool,
|
||||
) -> str:
|
||||
if allow_synthetic_test_source:
|
||||
return "synthetic-test"
|
||||
if is_relative_to(source, run_dir):
|
||||
raise SystemExit(
|
||||
"source image is inside the pet run directory; record the original "
|
||||
"$imagegen output from $CODEX_HOME/generated_images/.../ig_*.png instead"
|
||||
)
|
||||
generated_root = default_generated_images_root()
|
||||
if not is_relative_to(source, generated_root) or not source.name.startswith("ig_"):
|
||||
raise SystemExit(
|
||||
"source image does not look like a built-in $imagegen output; expected "
|
||||
f"{generated_root}/.../ig_*.png. Do not ingest locally drawn or "
|
||||
"post-processed row strips as visual job outputs."
|
||||
)
|
||||
return "built-in-imagegen"
|
||||
|
||||
|
||||
def validate_required_grounding(job: dict[str, object], run_dir: Path) -> None:
|
||||
if job.get("allow_prompt_only_generation") is not False:
|
||||
return
|
||||
inputs = job.get("input_images")
|
||||
if not isinstance(inputs, list) or not inputs:
|
||||
raise SystemExit(
|
||||
f"job {job.get('id')} does not list input_images; grounded row jobs must attach references"
|
||||
)
|
||||
missing = []
|
||||
for item in inputs:
|
||||
if not isinstance(item, dict) or not isinstance(item.get("path"), str):
|
||||
raise SystemExit(f"job {job.get('id')} has an invalid input image entry")
|
||||
path = run_dir / item["path"]
|
||||
if not path.is_file():
|
||||
missing.append(str(path))
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
f"job {job.get('id')} is missing required grounding image(s): "
|
||||
+ ", ".join(missing)
|
||||
)
|
||||
|
||||
|
||||
def update_base_canonical_reference(
|
||||
*,
|
||||
run_dir: Path,
|
||||
output: Path,
|
||||
manifest: dict[str, object],
|
||||
job: dict[str, object],
|
||||
metadata: dict[str, object],
|
||||
) -> None:
|
||||
if job.get("id") != "base":
|
||||
return
|
||||
|
||||
canonical = run_dir / CANONICAL_BASE_PATH
|
||||
canonical.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(output, canonical)
|
||||
canonical_sha = file_sha256(canonical)
|
||||
reference = {
|
||||
"path": manifest_relative(canonical, run_dir),
|
||||
"source_job": "base",
|
||||
"sha256": canonical_sha,
|
||||
"metadata": metadata,
|
||||
}
|
||||
job["canonical_reference_path"] = reference["path"]
|
||||
manifest["canonical_identity_reference"] = reference
|
||||
|
||||
request_path = run_dir / "pet_request.json"
|
||||
if request_path.exists():
|
||||
request = json.loads(request_path.read_text(encoding="utf-8"))
|
||||
request["canonical_identity_reference"] = reference
|
||||
request_path.write_text(json.dumps(request, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument("--job-id", required=True)
|
||||
parser.add_argument("--source", required=True)
|
||||
parser.add_argument("--force", action="store_true")
|
||||
parser.add_argument(
|
||||
"--allow-synthetic-test-source", action="store_true", help=argparse.SUPPRESS
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
run_dir = Path(args.run_dir).expanduser().resolve()
|
||||
source = Path(args.source).expanduser().resolve()
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"source image not found: {source}")
|
||||
source_provenance = validate_source_path(
|
||||
source=source,
|
||||
run_dir=run_dir,
|
||||
allow_synthetic_test_source=args.allow_synthetic_test_source,
|
||||
)
|
||||
|
||||
manifest_path = run_dir / "imagegen-jobs.json"
|
||||
manifest = load_jobs(manifest_path)
|
||||
job = find_job(manifest, args.job_id)
|
||||
|
||||
missing_deps = [
|
||||
dep
|
||||
for dep in job.get("depends_on", [])
|
||||
if isinstance(dep, str) and dep not in completed_job_ids(manifest)
|
||||
]
|
||||
if missing_deps:
|
||||
raise SystemExit(
|
||||
f"job {args.job_id} is not ready; missing dependency result(s): {', '.join(missing_deps)}"
|
||||
)
|
||||
validate_required_grounding(job, run_dir)
|
||||
|
||||
output_raw = job.get("output_path")
|
||||
if not isinstance(output_raw, str):
|
||||
raise SystemExit(f"job {args.job_id} has no output_path")
|
||||
output = run_dir / output_raw
|
||||
if output.exists() and not args.force:
|
||||
raise SystemExit(f"{output} already exists; pass --force to replace it")
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, output)
|
||||
metadata = image_metadata(output)
|
||||
|
||||
job["status"] = "complete"
|
||||
job["source_path"] = str(source)
|
||||
job["source_provenance"] = source_provenance
|
||||
job["source_sha256"] = file_sha256(source)
|
||||
job["output_sha256"] = file_sha256(output)
|
||||
if source_provenance == "synthetic-test":
|
||||
job["synthetic_test_source"] = True
|
||||
else:
|
||||
job.pop("synthetic_test_source", None)
|
||||
job["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
job["metadata"] = metadata
|
||||
for key in [
|
||||
"last_error",
|
||||
"secondary_fallback",
|
||||
"derived_from",
|
||||
"mirror_decision",
|
||||
"repair_reason",
|
||||
"queued_at",
|
||||
]:
|
||||
job.pop(key, None)
|
||||
update_base_canonical_reference(
|
||||
run_dir=run_dir,
|
||||
output=output,
|
||||
manifest=manifest,
|
||||
job=job,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"job_id": args.job_id,
|
||||
"output": str(output),
|
||||
"metadata": metadata,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render Codex pet state videos from an atlas using ffmpeg."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
CELL_WIDTH = 192
|
||||
CELL_HEIGHT = 208
|
||||
STATES = {
|
||||
"idle": (0, [280, 110, 110, 140, 140, 320]),
|
||||
"running-right": (1, [120, 120, 120, 120, 120, 120, 120, 220]),
|
||||
"running-left": (2, [120, 120, 120, 120, 120, 120, 120, 220]),
|
||||
"waving": (3, [140, 140, 140, 280]),
|
||||
"jumping": (4, [140, 140, 140, 140, 280]),
|
||||
"failed": (5, [140, 140, 140, 140, 140, 140, 140, 240]),
|
||||
"waiting": (6, [150, 150, 150, 150, 150, 260]),
|
||||
"running": (7, [120, 120, 120, 120, 120, 220]),
|
||||
"review": (8, [150, 150, 150, 150, 150, 280]),
|
||||
}
|
||||
|
||||
|
||||
def checker(size: tuple[int, int], square: int = 16) -> Image.Image:
|
||||
image = Image.new("RGB", size, "#ffffff")
|
||||
draw = ImageDraw.Draw(image)
|
||||
for y in range(0, size[1], square):
|
||||
for x in range(0, size[0], square):
|
||||
if (x // square + y // square) % 2:
|
||||
draw.rectangle((x, y, x + square - 1, y + square - 1), fill="#e8e8e8")
|
||||
return image
|
||||
|
||||
|
||||
def shell_quote_for_concat(path: Path) -> str:
|
||||
return "'" + str(path).replace("'", "'\\''") + "'"
|
||||
|
||||
|
||||
def render_state(
|
||||
atlas: Image.Image,
|
||||
state: str,
|
||||
row: int,
|
||||
durations: list[int],
|
||||
output_dir: Path,
|
||||
loops: int,
|
||||
scale: int,
|
||||
ffmpeg: str,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix=f"codex-pet-{state}-") as temp_raw:
|
||||
temp = Path(temp_raw)
|
||||
frame_paths: list[Path] = []
|
||||
for column in range(len(durations)):
|
||||
crop = atlas.crop(
|
||||
(
|
||||
column * CELL_WIDTH,
|
||||
row * CELL_HEIGHT,
|
||||
(column + 1) * CELL_WIDTH,
|
||||
(row + 1) * CELL_HEIGHT,
|
||||
)
|
||||
).convert("RGBA")
|
||||
bg = checker((CELL_WIDTH, CELL_HEIGHT))
|
||||
bg.paste(crop, (0, 0), crop)
|
||||
frame_path = temp / f"{state}-{column:02d}.png"
|
||||
bg.save(frame_path)
|
||||
frame_paths.append(frame_path)
|
||||
|
||||
concat_path = temp / f"{state}.ffconcat"
|
||||
lines = ["ffconcat version 1.0"]
|
||||
sequence: list[tuple[Path, int]] = []
|
||||
for _ in range(loops):
|
||||
sequence.extend(zip(frame_paths, durations, strict=True))
|
||||
for frame_path, duration_ms in sequence:
|
||||
lines.append(f"file {shell_quote_for_concat(frame_path)}")
|
||||
lines.append(f"duration {duration_ms / 1000:.3f}")
|
||||
lines.append(f"file {shell_quote_for_concat(sequence[-1][0])}")
|
||||
concat_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
output = output_dir / f"{state}.mp4"
|
||||
command = [
|
||||
ffmpeg,
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
str(concat_path),
|
||||
"-vf",
|
||||
f"scale={CELL_WIDTH * scale}:{CELL_HEIGHT * scale}:flags=lanczos,format=yuv420p",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
str(output),
|
||||
]
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("atlas")
|
||||
parser.add_argument("--output-dir", required=True)
|
||||
parser.add_argument("--loops", type=int, default=4)
|
||||
parser.add_argument("--scale", type=int, default=2)
|
||||
parser.add_argument("--ffmpeg", default=shutil.which("ffmpeg") or "ffmpeg")
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = Path(args.output_dir).expanduser().resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with Image.open(Path(args.atlas).expanduser().resolve()) as opened:
|
||||
atlas = opened.convert("RGBA")
|
||||
|
||||
for state, (row, durations) in STATES.items():
|
||||
render_state(
|
||||
atlas,
|
||||
state,
|
||||
row,
|
||||
durations,
|
||||
output_dir,
|
||||
args.loops,
|
||||
args.scale,
|
||||
args.ffmpeg,
|
||||
)
|
||||
print(f"wrote videos to {output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
python3 "$SCRIPT_DIR/render_animation_videos.py" "$@"
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a Codex pet spritesheet atlas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
COLUMNS = 8
|
||||
ROWS = 9
|
||||
CELL_WIDTH = 192
|
||||
CELL_HEIGHT = 208
|
||||
ATLAS_WIDTH = COLUMNS * CELL_WIDTH
|
||||
ATLAS_HEIGHT = ROWS * CELL_HEIGHT
|
||||
ROW_BY_INDEX = {
|
||||
0: ("idle", 6),
|
||||
1: ("running-right", 8),
|
||||
2: ("running-left", 8),
|
||||
3: ("waving", 4),
|
||||
4: ("jumping", 5),
|
||||
5: ("failed", 8),
|
||||
6: ("waiting", 6),
|
||||
7: ("running", 6),
|
||||
8: ("review", 6),
|
||||
}
|
||||
|
||||
|
||||
def alpha_nonzero_count(image: Image.Image) -> int:
|
||||
alpha = image.getchannel("A")
|
||||
return sum(alpha.histogram()[1:])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("atlas")
|
||||
parser.add_argument("--json-out")
|
||||
parser.add_argument("--min-used-pixels", type=int, default=50)
|
||||
parser.add_argument("--near-opaque-threshold", type=float, default=0.95)
|
||||
parser.add_argument("--allow-opaque", action="store_true")
|
||||
parser.add_argument("--allow-near-opaque-used-cells", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
atlas_path = Path(args.atlas).expanduser().resolve()
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
near_opaque_used_cells: dict[str, list[int]] = defaultdict(list)
|
||||
cells: list[dict[str, object]] = []
|
||||
|
||||
try:
|
||||
with Image.open(atlas_path) as opened:
|
||||
source_mode = opened.mode
|
||||
source_format = opened.format
|
||||
image = opened.convert("RGBA")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
result = {"ok": False, "errors": [f"could not open atlas: {exc}"], "warnings": []}
|
||||
print(json.dumps(result, indent=2))
|
||||
raise SystemExit(1)
|
||||
|
||||
if image.size != (ATLAS_WIDTH, ATLAS_HEIGHT):
|
||||
errors.append(f"expected {ATLAS_WIDTH}x{ATLAS_HEIGHT}, got {image.width}x{image.height}")
|
||||
|
||||
if source_format not in {"PNG", "WEBP"}:
|
||||
errors.append(f"expected PNG or WebP, got {source_format}")
|
||||
|
||||
if "A" not in source_mode and not args.allow_opaque:
|
||||
errors.append("atlas does not have an alpha channel")
|
||||
|
||||
for row_index in range(ROWS):
|
||||
state, frame_count = ROW_BY_INDEX[row_index]
|
||||
for column_index in range(COLUMNS):
|
||||
left = column_index * CELL_WIDTH
|
||||
top = row_index * CELL_HEIGHT
|
||||
cell = image.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))
|
||||
nontransparent = alpha_nonzero_count(cell)
|
||||
used = column_index < frame_count
|
||||
cell_info = {
|
||||
"state": state,
|
||||
"row": row_index,
|
||||
"column": column_index,
|
||||
"used": used,
|
||||
"nontransparent_pixels": nontransparent,
|
||||
}
|
||||
cells.append(cell_info)
|
||||
if used and nontransparent < args.min_used_pixels:
|
||||
errors.append(
|
||||
f"{state} row {row_index} column {column_index} is empty or too sparse ({nontransparent} pixels)"
|
||||
)
|
||||
if used and nontransparent > CELL_WIDTH * CELL_HEIGHT * args.near_opaque_threshold:
|
||||
near_opaque_used_cells[f"{state} row {row_index}"].append(column_index)
|
||||
if not used and nontransparent != 0:
|
||||
errors.append(
|
||||
f"{state} row {row_index} unused column {column_index} is not transparent ({nontransparent} pixels)"
|
||||
)
|
||||
|
||||
for row_label, columns in near_opaque_used_cells.items():
|
||||
message = (
|
||||
f"{row_label} has {len(columns)} nearly opaque used cells; "
|
||||
"this usually means the sprite has a non-transparent background"
|
||||
)
|
||||
if args.allow_near_opaque_used_cells:
|
||||
warnings.append(message)
|
||||
else:
|
||||
errors.append(message)
|
||||
|
||||
alpha_count = alpha_nonzero_count(image)
|
||||
if alpha_count == ATLAS_WIDTH * ATLAS_HEIGHT:
|
||||
message = "atlas is fully opaque; custom pets require a transparent sprite background"
|
||||
if args.allow_opaque:
|
||||
warnings.append(message)
|
||||
else:
|
||||
errors.append(message)
|
||||
|
||||
result = {
|
||||
"ok": not errors,
|
||||
"file": str(atlas_path),
|
||||
"format": source_format,
|
||||
"mode": source_mode,
|
||||
"width": image.width,
|
||||
"height": image.height,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"cells": cells,
|
||||
}
|
||||
|
||||
if args.json_out:
|
||||
Path(args.json_out).expanduser().resolve().write_text(
|
||||
json.dumps(result, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
print(json.dumps({k: v for k, v in result.items() if k != "cells"}, indent=2))
|
||||
raise SystemExit(0 if result["ok"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: hr-onboarding
|
||||
description: |
|
||||
A new-hire onboarding plan as a single page — first week schedule,
|
||||
buddy + manager intro, learning track, equipment checklist, and "you're
|
||||
set when…" outcomes. Use when the brief mentions "onboarding",
|
||||
"new hire", "first week plan", or "入职".
|
||||
triggers:
|
||||
- "onboarding"
|
||||
- "new hire"
|
||||
- "first week"
|
||||
- "入职"
|
||||
- "新员工"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: hr
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: true
|
||||
sections: [color, typography, layout, components]
|
||||
example_prompt: "Build a 30-day onboarding plan for a new product designer joining a 40-person startup."
|
||||
---
|
||||
|
||||
# HR Onboarding Skill
|
||||
|
||||
Produce a single-screen onboarding plan in HTML.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the active DESIGN.md.
|
||||
2. Identify the role + tenure expectations from the brief. Default to a
|
||||
30/60/90-day shape if unspecified.
|
||||
3. Layout:
|
||||
- Cover banner: name placeholder, role, start date, manager + buddy.
|
||||
- "Day 1" panel with the literal schedule (kickoff time, lunch, 1:1 slot).
|
||||
- First-week timeline (Mon → Fri, two activities per day).
|
||||
- 30 / 60 / 90 day milestone cards with three concrete outcomes each.
|
||||
- Resource list: handbook, Slack channels, key dashboards, payroll setup.
|
||||
- "You're set when…" checklist — five outcomes with checkboxes.
|
||||
4. Single inline `<style>`, semantic HTML.
|
||||
|
||||
## Output contract
|
||||
|
||||
```
|
||||
<artifact identifier="onboarding-plan" type="text/html" title="Onboarding Plan">
|
||||
<!doctype html>...</artifact>
|
||||
```
|
||||
@@ -0,0 +1,219 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Welcome to Northwind — Maya's Onboarding Plan</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fbf9f4;
|
||||
--paper: #ffffff;
|
||||
--ink: #14110e;
|
||||
--muted: #6b6760;
|
||||
--line: #ece6d8;
|
||||
--accent: #c2521a;
|
||||
--accent-soft: #fbe6d6;
|
||||
--positive: #2c8a4f;
|
||||
--display: 'Georgia', 'Times New Roman', serif;
|
||||
--body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--body); font-size: 14.5px; line-height: 1.55; }
|
||||
.wrap { max-width: 1080px; margin: 28px auto; padding: 0 32px 64px; }
|
||||
|
||||
/* Cover */
|
||||
.cover { padding: 36px 40px; background: var(--ink); color: var(--paper); border-radius: 16px; display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: center; }
|
||||
.cover .eyebrow { font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-soft); }
|
||||
.cover h1 { font-family: var(--display); font-size: 38px; line-height: 1.05; letter-spacing: -0.01em; margin: 8px 0 12px; }
|
||||
.cover .meta { display: flex; gap: 28px; font-size: 13px; color: rgba(255,255,255,0.74); }
|
||||
.cover .meta strong { color: var(--paper); display: block; font-weight: 600; font-size: 14px; }
|
||||
.cover-art { width: 130px; height: 130px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), #ec8b5b); display: flex; align-items: center; justify-content: center; font-family: var(--display); font-size: 56px; color: var(--paper); }
|
||||
|
||||
section { margin-top: 44px; }
|
||||
h2 { font-family: var(--display); font-size: 22px; margin: 0 0 6px; letter-spacing: -0.005em; }
|
||||
.section-sub { color: var(--muted); margin: 0 0 18px; font-size: 13.5px; }
|
||||
|
||||
/* Day 1 */
|
||||
.day-one { padding: 24px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; }
|
||||
.schedule { display: grid; grid-template-columns: 110px 1fr; gap: 0; }
|
||||
.schedule-row { display: contents; }
|
||||
.schedule-row .time { padding: 12px 0; border-top: 1px solid var(--line); font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
||||
.schedule-row .item { padding: 12px 0; border-top: 1px solid var(--line); }
|
||||
.schedule-row:first-child .time, .schedule-row:first-child .item { border-top: none; }
|
||||
.schedule-row .item strong { display: block; font-weight: 600; }
|
||||
.schedule-row .item span { color: var(--muted); font-size: 13px; }
|
||||
|
||||
/* Week timeline */
|
||||
.week { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
|
||||
.day { padding: 16px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; display: flex; flex-direction: column; gap: 12px; min-height: 200px; }
|
||||
.day-head { display: flex; justify-content: space-between; align-items: baseline; }
|
||||
.day-name { font-family: var(--display); font-size: 16px; font-weight: 700; }
|
||||
.day-date { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
||||
.activity { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; }
|
||||
.activity .dot { flex: 0 0 8px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); margin-top: 6px; }
|
||||
.activity small { display: block; color: var(--muted); margin-top: 2px; font-size: 11.5px; }
|
||||
|
||||
/* 30/60/90 */
|
||||
.milestones { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||
.milestone { padding: 22px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; }
|
||||
.milestone .badge { display: inline-block; font-family: var(--mono); font-size: 11px; padding: 3px 10px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); letter-spacing: 0.06em; margin-bottom: 10px; }
|
||||
.milestone h3 { font-family: var(--display); font-size: 18px; margin: 0 0 12px; }
|
||||
.milestone ul { padding-left: 18px; margin: 0; display: flex; flex-direction: column; gap: 8px; font-size: 13.5px; }
|
||||
.milestone li::marker { color: var(--accent); }
|
||||
|
||||
/* Resources & checklist */
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.panel { padding: 22px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px; }
|
||||
.panel h3 { font-family: var(--display); font-size: 17px; margin: 0 0 12px; }
|
||||
.resource { display: grid; grid-template-columns: 28px 1fr auto; gap: 10px; padding: 10px 0; border-top: 1px solid var(--line); align-items: center; font-size: 13.5px; }
|
||||
.resource:first-of-type { border-top: none; padding-top: 0; }
|
||||
.resource .icon { width: 28px; height: 28px; background: var(--accent-soft); border-radius: 7px; color: var(--accent); display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; }
|
||||
.resource .meta { color: var(--muted); font-family: var(--mono); font-size: 11px; }
|
||||
.check { display: flex; align-items: flex-start; gap: 12px; padding: 12px 0; border-top: 1px dashed var(--line); }
|
||||
.check:first-of-type { border-top: none; padding-top: 0; }
|
||||
.check .box { flex: 0 0 18px; width: 18px; height: 18px; border-radius: 5px; border: 1.5px solid var(--ink); display: inline-flex; align-items: center; justify-content: center; font-weight: 700; color: transparent; }
|
||||
.check.done .box { background: var(--positive); border-color: var(--positive); color: var(--paper); }
|
||||
.check strong { display: block; font-weight: 600; }
|
||||
.check span { color: var(--muted); font-size: 12.5px; }
|
||||
|
||||
footer { margin-top: 56px; padding-top: 18px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.cover { grid-template-columns: 1fr; text-align: center; }
|
||||
.cover-art { margin: 0 auto; }
|
||||
.week { grid-template-columns: 1fr 1fr; }
|
||||
.milestones { grid-template-columns: 1fr; }
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="cover">
|
||||
<div>
|
||||
<div class="eyebrow">Onboarding plan · 30/60/90</div>
|
||||
<h1>Welcome, Maya. Let's make your first 90 days feel deliberate.</h1>
|
||||
<div class="meta">
|
||||
<div><strong>Role</strong>Product Designer · Growth squad</div>
|
||||
<div><strong>Start date</strong>Mon, 4 November 2025</div>
|
||||
<div><strong>Manager</strong>Alvaro Méndez</div>
|
||||
<div><strong>Onboarding buddy</strong>Sasha Lin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cover-art">M</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Day 1 · Monday</h2>
|
||||
<p class="section-sub">A grounded day. Coffee with the team, a working laptop, and one shipped commit on the docs site by 5pm.</p>
|
||||
<div class="day-one">
|
||||
<div class="schedule">
|
||||
<div class="schedule-row"><div class="time">09:00</div><div class="item"><strong>Kickoff with Alvaro</strong><span>Welcome, week-one walkthrough, expectations chat. Office Room 3 (or Zoom).</span></div></div>
|
||||
<div class="schedule-row"><div class="time">10:00</div><div class="item"><strong>IT setup with Devon</strong><span>Laptop, badge, SSO, Slack, Figma, Linear, GitHub. Bring two photo IDs.</span></div></div>
|
||||
<div class="schedule-row"><div class="time">11:30</div><div class="item"><strong>Coffee with Sasha (buddy)</strong><span>The unwritten rules, who-to-ask map, where the good lunch spots are.</span></div></div>
|
||||
<div class="schedule-row"><div class="time">12:30</div><div class="item"><strong>Team lunch · Northwind cafeteria</strong><span>Whole Growth squad joins. No agenda.</span></div></div>
|
||||
<div class="schedule-row"><div class="time">14:00</div><div class="item"><strong>Read & explore</strong><span>Handbook, last quarter's design crit recordings, Figma library.</span></div></div>
|
||||
<div class="schedule-row"><div class="time">16:00</div><div class="item"><strong>Ship "I exist" PR</strong><span>Add yourself to the team page on the docs site. Counts as your first commit.</span></div></div>
|
||||
<div class="schedule-row"><div class="time">17:00</div><div class="item"><strong>End-of-day check-in with Alvaro</strong><span>15 min. What was confusing, what wasn't. Repeat tomorrow if useful.</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>First week timeline</h2>
|
||||
<p class="section-sub">Two activities per day. Anything else is bonus.</p>
|
||||
<div class="week">
|
||||
<div class="day">
|
||||
<div class="day-head"><div class="day-name">Mon</div><div class="day-date">Nov 4</div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Kickoff + setup</strong><small>Alvaro · 09:00</small></div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Ship team-page PR</strong><small>Sasha can review</small></div></div>
|
||||
</div>
|
||||
<div class="day">
|
||||
<div class="day-head"><div class="day-name">Tue</div><div class="day-date">Nov 5</div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Design system tour</strong><small>Yuko · 10:00</small></div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Shadow user research call</strong><small>11:00 with Sam</small></div></div>
|
||||
</div>
|
||||
<div class="day">
|
||||
<div class="day-head"><div class="day-name">Wed</div><div class="day-date">Nov 6</div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Squad weekly</strong><small>09:30</small></div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Pick a starter ticket</strong><small>From the "good first issues" lane</small></div></div>
|
||||
</div>
|
||||
<div class="day">
|
||||
<div class="day-head"><div class="day-name">Thu</div><div class="day-date">Nov 7</div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Design crit attendance</strong><small>14:00. Just listen.</small></div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>1:1 with skip-level</strong><small>Avi · 16:00</small></div></div>
|
||||
</div>
|
||||
<div class="day">
|
||||
<div class="day-head"><div class="day-name">Fri</div><div class="day-date">Nov 8</div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>End-of-week retro</strong><small>15-min note to Alvaro</small></div></div>
|
||||
<div class="activity"><span class="dot"></span><div><strong>Optional: All-hands demo</strong><small>17:00 · drinks after</small></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>30 · 60 · 90 day milestones</h2>
|
||||
<p class="section-sub">Three outcomes per checkpoint. We'll review each at the matching 1:1 with Alvaro.</p>
|
||||
<div class="milestones">
|
||||
<div class="milestone">
|
||||
<span class="badge">Day 30</span>
|
||||
<h3>Find your footing</h3>
|
||||
<ul>
|
||||
<li>Shipped one small, end-to-end design change to production.</li>
|
||||
<li>Mapped every recurring meeting and why it exists.</li>
|
||||
<li>Met with each cross-functional partner (eng, PM, research, marketing).</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="milestone">
|
||||
<span class="badge">Day 60</span>
|
||||
<h3>Own a feature</h3>
|
||||
<ul>
|
||||
<li>Driving design on the new onboarding redesign — own the spec.</li>
|
||||
<li>Ran your first design crit as the presenter.</li>
|
||||
<li>Drafted one process improvement and posted it for the team.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="milestone">
|
||||
<span class="badge">Day 90</span>
|
||||
<h3>Move the team forward</h3>
|
||||
<ul>
|
||||
<li>Shipped a feature you led from research → launch.</li>
|
||||
<li>Mentored someone — even informally.</li>
|
||||
<li>Shared one hot take in all-hands and lived to tell.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Things to bookmark</h2>
|
||||
<p class="section-sub">Open these, save them in your browser, then forget about this page.</p>
|
||||
<div class="grid-2">
|
||||
<div class="panel">
|
||||
<h3>Resources</h3>
|
||||
<div class="resource"><div class="icon">📘</div><div><strong>Northwind Handbook</strong></div><div class="meta">handbook.nw</div></div>
|
||||
<div class="resource"><div class="icon">💬</div><div><strong>#growth-squad</strong></div><div class="meta">Slack</div></div>
|
||||
<div class="resource"><div class="icon">🎨</div><div><strong>Design Library v3.4</strong></div><div class="meta">Figma</div></div>
|
||||
<div class="resource"><div class="icon">📊</div><div><strong>Growth dashboard</strong></div><div class="meta">grafana.nw</div></div>
|
||||
<div class="resource"><div class="icon">💸</div><div><strong>Payroll & benefits</strong></div><div class="meta">Rippling</div></div>
|
||||
<div class="resource"><div class="icon">📅</div><div><strong>Onboarding calendar</strong></div><div class="meta">cal.nw/onboard</div></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>You're set when…</h3>
|
||||
<div class="check done"><div class="box">✓</div><div><strong>Laptop, SSO, and badge work end-to-end.</strong><span>Includes Slack, Figma, Linear, GitHub, 1Password.</span></div></div>
|
||||
<div class="check done"><div class="box">✓</div><div><strong>You've met everyone on the squad.</strong><span>Coffee, walk, or 15-min Zoom — your call.</span></div></div>
|
||||
<div class="check"><div class="box"></div><div><strong>You've shipped your first PR.</strong><span>Even tiny ones count. Sasha will help.</span></div></div>
|
||||
<div class="check"><div class="box"></div><div><strong>You can find any meeting on the calendar.</strong><span>And know which ones you can decline.</span></div></div>
|
||||
<div class="check"><div class="box"></div><div><strong>You feel comfortable asking dumb questions.</strong><span>This is the most important one. We mean it.</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>Northwind People Ops · Onboarding plan template v3.1</span>
|
||||
<span>Updated October 2025</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: html-ppt-course-module
|
||||
description: Online-course / workshop module deck — warm paper background + Playfair serif, persistent left sidebar of learning objectives, MCQ self-check page. Use for teaching modules, training materials, workshop slides.
|
||||
triggers:
|
||||
- "course module"
|
||||
- "course slides"
|
||||
- "workshop"
|
||||
- "training deck"
|
||||
- "lesson"
|
||||
- "教学"
|
||||
- "课件"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: education
|
||||
featured: 25
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "Use the html-ppt-course-module template to build a 7-slide module deck. Confirm: module title, 3-5 learning objectives (these stick on the left rail), and the MCQ self-check question. Then assemble the deck with serif headings on warm paper."
|
||||
---
|
||||
# HTML PPT · Course Module
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`course-module`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `course-module` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/course-module/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-course-module` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-course-module` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,542 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Module 04 · Recursion · CS101</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* course-module — academic but friendly */
|
||||
.tpl-course-module{
|
||||
--bg:#fbfaf6;--bg-soft:#f4f1e8;--surface:#ffffff;--surface-2:#f6f3ea;
|
||||
--border:rgba(60,45,20,.12);--border-strong:rgba(60,45,20,.24);
|
||||
--text-1:#2a2418;--text-2:#5a5140;--text-3:#8a7f68;
|
||||
--accent:#2d7d6e;--accent-2:#d88a3a;--accent-3:#c4593f;
|
||||
--grad:linear-gradient(135deg,#2d7d6e,#4ea893);
|
||||
--radius:14px;--radius-lg:20px;
|
||||
--shadow:0 12px 30px rgba(60,45,20,.07);
|
||||
font-family:'Inter','Noto Sans SC',sans-serif;
|
||||
}
|
||||
.tpl-course-module .slide{padding:64px 80px;background:var(--bg);display:grid;grid-template-columns:260px 1fr;gap:56px;align-content:start}
|
||||
.tpl-course-module .slide.full{grid-template-columns:1fr;display:flex;flex-direction:column;justify-content:center}
|
||||
.tpl-course-module .sidebar{border-right:1px solid var(--border);padding-right:32px;position:relative}
|
||||
.tpl-course-module .sidebar .brand{font-family:'Playfair Display',serif;font-size:22px;font-weight:700;color:var(--accent)}
|
||||
.tpl-course-module .sidebar .brand::before{content:"✦ ";color:var(--accent-2)}
|
||||
.tpl-course-module .sidebar h5{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:var(--text-3);margin:32px 0 12px}
|
||||
.tpl-course-module .obj-list{list-style:none;padding:0;margin:0;font-size:13px;color:var(--text-2);line-height:1.5}
|
||||
.tpl-course-module .obj-list li{padding:8px 0 8px 22px;position:relative;border-bottom:1px dashed var(--border)}
|
||||
.tpl-course-module .obj-list li::before{content:"○";position:absolute;left:0;top:8px;color:var(--accent)}
|
||||
.tpl-course-module .obj-list li.done::before{content:"●";color:var(--accent)}
|
||||
.tpl-course-module .obj-list li.current{color:var(--text-1);font-weight:700}
|
||||
.tpl-course-module .obj-list li.current::before{content:"▸";color:var(--accent-2)}
|
||||
.tpl-course-module .main{min-width:0}
|
||||
.tpl-course-module .h1{font-family:'Playfair Display',serif;font-size:72px;line-height:1.02;font-weight:800;letter-spacing:-.02em;color:var(--text-1)}
|
||||
.tpl-course-module .h2{font-family:'Playfair Display',serif;font-size:48px;line-height:1.1;font-weight:700;letter-spacing:-.015em;color:var(--text-1)}
|
||||
.tpl-course-module h3,.tpl-course-module h4{color:var(--text-1)}
|
||||
.tpl-course-module .kicker{color:var(--accent-2);font-size:12px;font-weight:700;letter-spacing:.14em}
|
||||
.tpl-course-module .lede{font-size:20px;color:var(--text-2);line-height:1.7}
|
||||
.tpl-course-module .callout{border-left:4px solid var(--accent-2);background:var(--surface-2);padding:20px 24px;border-radius:0 var(--radius) var(--radius) 0;margin-top:24px}
|
||||
.tpl-course-module .callout b{color:var(--accent-2)}
|
||||
.tpl-course-module .concept-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px 26px;box-shadow:var(--shadow)}
|
||||
.tpl-course-module .concept-box h4{margin-top:0;color:var(--accent)}
|
||||
.tpl-course-module .exercise{background:#fff8ed;border:1.5px dashed var(--accent-2);border-radius:var(--radius);padding:24px 28px}
|
||||
.tpl-course-module .exercise::before{content:"✎ Exercise";display:block;font-size:12px;font-weight:700;letter-spacing:.12em;color:var(--accent-2);margin-bottom:10px;text-transform:uppercase}
|
||||
.tpl-course-module .code{background:#2a2418;color:#f4f1e8;border-radius:var(--radius);padding:20px 24px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.7;overflow:auto}
|
||||
.tpl-course-module .code .cmt{color:#8a7f68;font-style:italic}
|
||||
.tpl-course-module .code .kw{color:#e8a770}
|
||||
.tpl-course-module .code .str{color:#8ec6b2}
|
||||
.tpl-course-module .mcq{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 22px;margin-bottom:10px;display:flex;gap:14px;align-items:flex-start;cursor:pointer}
|
||||
.tpl-course-module .mcq .letter{flex:none;width:28px;height:28px;border-radius:50%;border:2px solid var(--text-3);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;color:var(--text-2)}
|
||||
.tpl-course-module .mcq.correct{border-color:var(--accent);background:rgba(45,125,110,.06)}
|
||||
.tpl-course-module .mcq.correct .letter{border-color:var(--accent);background:var(--accent);color:#fff}
|
||||
.tpl-course-module .pill-academic{display:inline-block;padding:4px 12px;border-radius:4px;background:var(--surface-2);border:1px solid var(--border);font-size:12px;color:var(--text-2);font-family:'JetBrains Mono',monospace}
|
||||
.tpl-course-module .slide.full .h1{font-size:88px}
|
||||
.tpl-course-module .deck-footer{color:var(--text-3)}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-course-module">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover -->
|
||||
<section class="slide full" data-title="Cover">
|
||||
<p class="kicker">CS 101 · MODULE 04</p>
|
||||
<h1 class="h1 mt-s">Recursion: solving<br>problems by <em>calling yourself</em>.</h1>
|
||||
<p class="lede mt-l" style="max-width:62ch">In this module you'll learn why a function that calls itself is not a trick, but the most natural way to describe problems that contain smaller copies of themselves.</p>
|
||||
<div class="row mt-l" style="gap:16px">
|
||||
<span class="pill-academic">~ 45 min read</span>
|
||||
<span class="pill-academic">prereq · functions, if/else</span>
|
||||
<span class="pill-academic">lang · Python</span>
|
||||
</div>
|
||||
<div class="deck-footer"><span>Dr. A. Rivera · Spring 2026</span><span class="slide-number" data-current="1" data-total="7"></span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Objectives -->
|
||||
<section class="slide" data-title="Objectives">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="current">Define recursion</li>
|
||||
<li>Identify a base case</li>
|
||||
<li>Trace a recursive call</li>
|
||||
<li>Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Module progress</h5>
|
||||
<p class="dim" style="font-size:13px">Page 2 of 7 · ~5 min in</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">OBJECTIVES</p>
|
||||
<h2 class="h2 mt-s">By the end, you will be able to…</h2>
|
||||
<div class="stack mt-l">
|
||||
<div class="concept-box"><h4>① Explain recursion in one sentence.</h4><p class="dim">"A function that solves a problem by calling itself on a smaller version of that problem."</p></div>
|
||||
<div class="concept-box"><h4>② Write a base case that always terminates.</h4><p class="dim">Every recursive function must have an exit door, or it runs forever.</p></div>
|
||||
<div class="concept-box"><h4>③ Trace a call stack on paper.</h4><p class="dim">Given <code>fact(4)</code>, draw the stack frames top-to-bottom.</p></div>
|
||||
<div class="concept-box"><h4>④ Convert a while-loop to a recursive equivalent.</h4><p class="dim">And explain when one is clearer than the other.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Concept -->
|
||||
<section class="slide" data-title="Concept">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="current">Identify a base case</li>
|
||||
<li>Trace a recursive call</li>
|
||||
<li>Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Key terms</h5>
|
||||
<p class="dim" style="font-size:13px">base case · recursive case · call stack · tail call</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">CORE CONCEPT</p>
|
||||
<h2 class="h2 mt-s">Two parts, always.</h2>
|
||||
<p class="lede mt-m">A recursive function has exactly two things inside it: a <b>base case</b> (when to stop) and a <b>recursive case</b> (how to shrink the problem before calling yourself).</p>
|
||||
<div class="callout">
|
||||
<b>Rule of thumb.</b> If you can't name the base case out loud, don't write the recursion yet. Draw it on paper first.
|
||||
</div>
|
||||
<div class="grid g2 mt-l">
|
||||
<div class="concept-box"><h4>Base case</h4><p class="dim">The smallest possible input — one the function answers directly, without calling itself.</p><p class="pill-academic">e.g. <b>n == 0</b></p></div>
|
||||
<div class="concept-box"><h4>Recursive case</h4><p class="dim">Every other input — delegate to a smaller version of the same problem.</p><p class="pill-academic">e.g. <b>n × fact(n-1)</b></p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Example -->
|
||||
<section class="slide" data-title="Example">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="done">Identify a base case</li>
|
||||
<li class="current">Trace a recursive call</li>
|
||||
<li>Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Try it yourself</h5>
|
||||
<p class="dim" style="font-size:13px">Open repl.it and run the code on the right. Then try <code>fact(10)</code>.</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">WORKED EXAMPLE</p>
|
||||
<h2 class="h2 mt-s">Factorial, 7 lines.</h2>
|
||||
<div class="code mt-m"><pre style="margin:0"><span class="cmt"># fact(n) = n × (n-1) × … × 1, and fact(0) = 1</span>
|
||||
<span class="kw">def</span> fact(n):
|
||||
<span class="kw">if</span> n == <span class="str">0</span>: <span class="cmt"># base case</span>
|
||||
<span class="kw">return</span> <span class="str">1</span>
|
||||
<span class="kw">return</span> n * fact(n - <span class="str">1</span>) <span class="cmt"># recursive case</span>
|
||||
|
||||
<span class="kw">print</span>(fact(<span class="str">4</span>)) <span class="cmt"># → 24</span></pre></div>
|
||||
<div class="callout">
|
||||
<b>Trace fact(4).</b> 4 × fact(3) → 4 × (3 × fact(2)) → 4 × 3 × (2 × fact(1)) → 4 × 3 × 2 × 1 × fact(0) → 4 × 3 × 2 × 1 × 1 = <b>24</b>.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Exercise -->
|
||||
<section class="slide" data-title="Exercise">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="done">Identify a base case</li>
|
||||
<li class="done">Trace a recursive call</li>
|
||||
<li class="current">Convert loop ↔ recursion</li>
|
||||
<li>Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Time</h5>
|
||||
<p class="dim" style="font-size:13px">~10 minutes · solo</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">EXERCISE 4.1</p>
|
||||
<h2 class="h2 mt-s">Write <em>sum_to(n)</em>.</h2>
|
||||
<p class="lede mt-m">Return <code>1 + 2 + … + n</code> using recursion — no loops allowed.</p>
|
||||
<div class="exercise mt-l">
|
||||
<p style="margin:0;font-size:18px;color:var(--text-1)"><b>Your task</b></p>
|
||||
<ol style="color:var(--text-2);line-height:1.8;margin:10px 0 0">
|
||||
<li>Write the base case. What does <code>sum_to(0)</code> return?</li>
|
||||
<li>Write the recursive case in terms of <code>sum_to(n - 1)</code>.</li>
|
||||
<li>Test it: <code>sum_to(5) == 15</code>, <code>sum_to(10) == 55</code>.</li>
|
||||
<li>Bonus: what happens if you call <code>sum_to(-3)</code>? Fix it.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p class="dim mt-m" style="font-size:14px">Stuck? Remember: a base case is the smallest input you already know the answer to.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Check understanding -->
|
||||
<section class="slide" data-title="Check">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">CS 101 · M04</div>
|
||||
<h5>Learning objectives</h5>
|
||||
<ul class="obj-list">
|
||||
<li class="done">Define recursion</li>
|
||||
<li class="done">Identify a base case</li>
|
||||
<li class="done">Trace a recursive call</li>
|
||||
<li class="done">Convert loop ↔ recursion</li>
|
||||
<li class="current">Recognize when recursion helps</li>
|
||||
</ul>
|
||||
<h5>Self-assess</h5>
|
||||
<p class="dim" style="font-size:13px">You should get 3/3.</p>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<p class="kicker">CHECK YOUR UNDERSTANDING</p>
|
||||
<h2 class="h2 mt-s">Which function will recurse forever?</h2>
|
||||
<div class="mt-l">
|
||||
<div class="mcq"><div class="letter">A</div><div><b>def f(n): return 1 if n == 0 else n * f(n - 1)</b><p class="dim" style="font-size:13px;margin:4px 0 0">Base case <code>n == 0</code>, shrinks toward it. Terminates.</p></div></div>
|
||||
<div class="mcq correct"><div class="letter">B</div><div><b>def f(n): return n + f(n + 1)</b><p class="dim" style="font-size:13px;margin:4px 0 0"><b style="color:var(--accent)">✓ Correct.</b> No base case, and <code>n</code> grows — infinite recursion.</p></div></div>
|
||||
<div class="mcq"><div class="letter">C</div><div><b>def f(n): return n if n < 2 else f(n - 1) + f(n - 2)</b><p class="dim" style="font-size:13px;margin:4px 0 0">Classic Fibonacci. Base case on <code>n < 2</code>. Terminates.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. Summary -->
|
||||
<section class="slide full" data-title="Summary">
|
||||
<p class="kicker">SUMMARY · MODULE 04</p>
|
||||
<h1 class="h1 mt-s">You can now…</h1>
|
||||
<div class="grid g2 mt-l">
|
||||
<div class="concept-box"><h4>✓ Define recursion</h4><p class="dim">A function that calls itself on a smaller input.</p></div>
|
||||
<div class="concept-box"><h4>✓ Write a safe base case</h4><p class="dim">Every recursion needs an exit door.</p></div>
|
||||
<div class="concept-box"><h4>✓ Trace a call stack</h4><p class="dim">You can unwind <code>fact(4)</code> by hand.</p></div>
|
||||
<div class="concept-box"><h4>✓ Judge when to use it</h4><p class="dim">Trees and self-similar problems → recursion. Flat iteration → loop.</p></div>
|
||||
</div>
|
||||
<div class="callout mt-l">
|
||||
<b>Up next · Module 05.</b> Divide & conquer: merge sort. We'll use everything you just learned — but on lists, not numbers.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-dir-key-nav-minimal
|
||||
description: 8 页极简方向键 keynote — 每页一个独立单色背景(靛 / 奶 / 绛 / 翠 / 灰 / 紫 / 白 / 炭),各自配色,160px display 标题 + 4px 短粗 accent 线分隔、箭头 → 前缀的 Mono 列表、左下 ← → kbd 提示 + 右下页码、巨大呼吸留白。适合"有话要说但没什么可看"的 keynote、launch、公开演讲。
|
||||
triggers:
|
||||
- "minimal keynote"
|
||||
- "极简"
|
||||
- "mono color"
|
||||
- "one idea per slide"
|
||||
- "public talk"
|
||||
- "launch keynote"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: personal
|
||||
featured: 34
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-dir-key-nav-minimal 模板做一份 8 页极简 keynote。每页一个单色背景 + 一句 160px 大标题 + 几条箭头列表。先告诉我演讲主题,然后帮我把 8 个核心观点拍成 8 页(每页一个 idea)。"
|
||||
---
|
||||
# HTML PPT · 8 色极简方向键
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`dir-key-nav-minimal`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `dir-key-nav-minimal` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/dir-key-nav-minimal/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-dir-key-nav-minimal` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-dir-key-nav-minimal` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,366 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dir-Key Nav Minimal</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* dir-key-nav-minimal — 方向键极简 · 8 种 mono-background 切换 */
|
||||
.tpl-dir-key-nav-minimal{
|
||||
--dk-font:'Inter','Noto Sans SC','PingFang SC',-apple-system,sans-serif;
|
||||
--dk-mono:'JetBrains Mono',monospace;
|
||||
background:#000;
|
||||
color:#fff;
|
||||
font-family:var(--dk-font);
|
||||
}
|
||||
.tpl-dir-key-nav-minimal .slide{padding:80px 104px;overflow:hidden;position:absolute;inset:0}
|
||||
/* 8 background themes */
|
||||
.tpl-dir-key-nav-minimal .t-indigo{background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 100%);color:#fff}
|
||||
.tpl-dir-key-nav-minimal .t-cream{background:#F5F0E8;color:#1a1a1a}
|
||||
.tpl-dir-key-nav-minimal .t-crimson{background:linear-gradient(135deg,#7f1d1d 0%,#991b1b 100%);color:#fff}
|
||||
.tpl-dir-key-nav-minimal .t-emerald{background:linear-gradient(135deg,#052e16 0%,#064e3b 100%);color:#ecfdf5}
|
||||
.tpl-dir-key-nav-minimal .t-slate{background:linear-gradient(135deg,#0f1923 0%,#1a2942 100%);color:#e6edf3}
|
||||
.tpl-dir-key-nav-minimal .t-violet{background:linear-gradient(135deg,#1e0a2e 0%,#2e1065 100%);color:#f5f3ff}
|
||||
.tpl-dir-key-nav-minimal .t-white{background:#ffffff;color:#111216}
|
||||
.tpl-dir-key-nav-minimal .t-charcoal{background:linear-gradient(135deg,#111827 0%,#1f2937 100%);color:#f3f4f6}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-snum{position:absolute;top:30px;right:48px;font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;font-family:var(--dk-mono)}
|
||||
.tpl-dir-key-nav-minimal .t-cream .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-white .dk-snum{color:#999}
|
||||
.tpl-dir-key-nav-minimal .t-indigo .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-crimson .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-emerald .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-slate .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-violet .dk-snum,
|
||||
.tpl-dir-key-nav-minimal .t-charcoal .dk-snum{color:rgba(255,255,255,.38)}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-eyebrow{font-size:12px;font-weight:700;letter-spacing:3.5px;text-transform:uppercase;opacity:.55;margin-bottom:22px;display:flex;align-items:center;gap:14px}
|
||||
.tpl-dir-key-nav-minimal .dk-eyebrow::after{content:'';flex:1;max-width:120px;height:1px;background:currentColor;opacity:.3}
|
||||
.tpl-dir-key-nav-minimal .dk-h0{font-size:160px;font-weight:900;line-height:.9;letter-spacing:-5px;margin:0 0 20px}
|
||||
.tpl-dir-key-nav-minimal .dk-h1{font-size:100px;font-weight:900;line-height:.98;letter-spacing:-3px;margin:0 0 18px}
|
||||
.tpl-dir-key-nav-minimal .dk-h2{font-size:72px;font-weight:800;line-height:1.05;letter-spacing:-2px;margin:0 0 16px}
|
||||
.tpl-dir-key-nav-minimal .dk-lede{font-size:26px;line-height:1.45;opacity:.72;max-width:900px;font-weight:300}
|
||||
.tpl-dir-key-nav-minimal .dk-lede strong{font-weight:700;opacity:1}
|
||||
.tpl-dir-key-nav-minimal .dk-big{font-family:var(--dk-mono);font-size:240px;font-weight:800;line-height:.9;letter-spacing:-10px}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-line{display:block;width:90px;height:4px;background:currentColor;margin:30px 0;opacity:.85}
|
||||
.tpl-dir-key-nav-minimal .t-indigo .dk-accent{color:#a5b4fc}
|
||||
.tpl-dir-key-nav-minimal .t-cream .dk-accent{color:#B5392A}
|
||||
.tpl-dir-key-nav-minimal .t-crimson .dk-accent{color:#fecaca}
|
||||
.tpl-dir-key-nav-minimal .t-emerald .dk-accent{color:#6ee7b7}
|
||||
.tpl-dir-key-nav-minimal .t-slate .dk-accent{color:#7dd3fc}
|
||||
.tpl-dir-key-nav-minimal .t-violet .dk-accent{color:#c4b5fd}
|
||||
.tpl-dir-key-nav-minimal .t-white .dk-accent{color:#6366f1}
|
||||
.tpl-dir-key-nav-minimal .t-charcoal .dk-accent{color:#fbbf24}
|
||||
|
||||
.tpl-dir-key-nav-minimal .dk-list{list-style:none;padding:0;margin:28px 0 0;font-family:var(--dk-mono);font-size:22px;line-height:2}
|
||||
.tpl-dir-key-nav-minimal .dk-list li{padding-left:30px;position:relative;font-weight:400;opacity:.85}
|
||||
.tpl-dir-key-nav-minimal .dk-list li::before{content:'→';position:absolute;left:0;opacity:.5}
|
||||
.tpl-dir-key-nav-minimal .dk-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:56px;margin-top:36px}
|
||||
.tpl-dir-key-nav-minimal .dk-col h3{font-size:28px;font-weight:700;margin-bottom:10px}
|
||||
.tpl-dir-key-nav-minimal .dk-col p{font-size:19px;line-height:1.55;opacity:.72;font-weight:300}
|
||||
.tpl-dir-key-nav-minimal .dk-code{font-family:var(--dk-mono);font-size:16px;line-height:1.9;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:24px 28px;margin-top:24px;white-space:pre}
|
||||
.tpl-dir-key-nav-minimal .t-cream .dk-code,
|
||||
.tpl-dir-key-nav-minimal .t-white .dk-code{background:rgba(0,0,0,.05);border-color:rgba(0,0,0,.1)}
|
||||
.tpl-dir-key-nav-minimal .dk-keyhint{position:absolute;bottom:34px;left:104px;font-family:var(--dk-mono);font-size:12px;letter-spacing:2px;text-transform:uppercase;opacity:.45}
|
||||
.tpl-dir-key-nav-minimal .dk-keyhint kbd{display:inline-block;padding:2px 10px;margin:0 3px;border:1px solid currentColor;border-radius:4px;font-size:12px}
|
||||
.tpl-dir-key-nav-minimal .dk-page{position:absolute;bottom:34px;right:48px;font-family:var(--dk-mono);font-size:12px;letter-spacing:2px;opacity:.45}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-dir-key-nav-minimal">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER · indigo -->
|
||||
<section class="slide t-indigo is-active">
|
||||
<div class="dk-snum">01 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Karpathy LLM Wiki</div>
|
||||
<h1 class="dk-h0">为什么笔记<br>治不了 <span class="dk-accent">LLM</span></h1>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">8 种背景、8 张幻灯,一个关于如何把 AI 变成「长期记忆外挂」的最短陈述。<strong>按 → 继续。</strong></p>
|
||||
</div>
|
||||
<div class="dk-keyhint">nav · <kbd>←</kbd> <kbd>→</kbd> · <kbd>space</kbd></div>
|
||||
<div class="dk-page">cover</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION · cream -->
|
||||
<section class="slide t-cream">
|
||||
<div class="dk-snum">02 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Chapter 01</div>
|
||||
<h1 class="dk-h0">The <span class="dk-accent">Problem</span>.</h1>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">Token 上限是一个物理事实。你每次和 LLM 说话,它都是一个失忆症患者。</p>
|
||||
</div>
|
||||
<div class="dk-keyhint">chapter · 01 / 04</div>
|
||||
<div class="dk-page">section</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT · crimson -->
|
||||
<section class="slide t-crimson">
|
||||
<div class="dk-snum">03 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Symptoms</div>
|
||||
<h2 class="dk-h1">四种你已经<br>受够的<br><span class="dk-accent">遗忘</span>。</h2>
|
||||
<ul class="dk-list">
|
||||
<li>昨天聊过的项目,今天重新解释一遍</li>
|
||||
<li>上下文窗口一到,它开始「编造记忆」</li>
|
||||
<li>不同 session 之间毫无关联,就像第一次见</li>
|
||||
<li>你的真正偏好从未被记住,每次都要 re-prompt</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dk-keyhint">content · list</div>
|
||||
<div class="dk-page">03</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CONTENT · emerald -->
|
||||
<section class="slide t-emerald">
|
||||
<div class="dk-snum">04 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">The Fix</div>
|
||||
<h2 class="dk-h1">答案不是<br><span class="dk-accent">更大</span> 的窗口。</h2>
|
||||
<p class="dk-lede" style="margin-top:10px">而是:把你的知识、偏好、历史都<strong>写进文件系统</strong>。<br>让 LLM 每次对话前,先去读那个系统。</p>
|
||||
<div class="dk-grid-2">
|
||||
<div class="dk-col"><h3>× 窗口 stuffing</h3><p>把所有东西塞 prompt,贵、慢、最终溢出。</p></div>
|
||||
<div class="dk-col"><h3>✓ 文件 + 检索</h3><p>按需加载,永远不溢出,结构化可 diff。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dk-keyhint">content · compare</div>
|
||||
<div class="dk-page">04</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CODE · slate -->
|
||||
<section class="slide t-slate">
|
||||
<div class="dk-snum">05 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Minimal Setup</div>
|
||||
<h2 class="dk-h2"><span class="dk-accent">4 行</span> YAML<br>就能开始。</h2>
|
||||
<pre class="dk-code">memory:
|
||||
root: ~/.llm-wiki
|
||||
format: markdown
|
||||
retrieval: hybrid # embedding + bm25</pre>
|
||||
<p class="dk-lede" style="margin-top:16px;font-size:20px">你现在拥有一个会随时间增长的 <strong>第二大脑</strong>。每次对话它都会被读、被更新。</p>
|
||||
</div>
|
||||
<div class="dk-keyhint">content · code</div>
|
||||
<div class="dk-page">05</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. CHART · violet — big number with bar -->
|
||||
<section class="slide t-violet">
|
||||
<div class="dk-snum">06 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">30-day result</div>
|
||||
<div class="dk-big dk-accent">87%</div>
|
||||
<p class="dk-lede" style="margin-top:14px;font-size:26px">的 re-explain 被消除。平均每次对话节省 <strong>4.2 分钟</strong> 的 re-context。</p>
|
||||
<svg viewBox="0 0 900 80" style="width:100%;max-width:900px;margin-top:30px">
|
||||
<rect x="0" y="30" width="900" height="22" rx="11" fill="rgba(255,255,255,.12)"/>
|
||||
<rect x="0" y="30" width="783" height="22" rx="11" fill="#c4b5fd"/>
|
||||
<text x="792" y="47" font-family="JetBrains Mono" font-size="16" fill="#c4b5fd" font-weight="700">87%</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dk-keyhint">chart · big-num</div>
|
||||
<div class="dk-page">06</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA · white -->
|
||||
<section class="slide t-white">
|
||||
<div class="dk-snum">07 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">Start tonight</div>
|
||||
<h2 class="dk-h1">开始<br>你的 <span class="dk-accent">wiki</span>。</h2>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">不是装又一个插件。是决定:从今晚起,<strong>你的所有 AI 对话都要有一个共同的 vault</strong>。</p>
|
||||
<pre class="dk-code" style="font-size:18px">$ mkdir ~/llm-wiki && cd ~/llm-wiki
|
||||
$ git init
|
||||
$ echo "# my brain" > README.md</pre>
|
||||
</div>
|
||||
<div class="dk-keyhint">cta · three-commands</div>
|
||||
<div class="dk-page">07</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS · charcoal -->
|
||||
<section class="slide t-charcoal">
|
||||
<div class="dk-snum">08 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="dk-eyebrow">End · thanks for staying</div>
|
||||
<h1 class="dk-h0"><span class="dk-accent">謝謝</span>。</h1>
|
||||
<span class="dk-line"></span>
|
||||
<p class="dk-lede">Karpathy 的原始 thread + 我的 vault 结构都在 <strong>github.com/lewis/llm-wiki</strong>。欢迎按 ← 再看一遍。</p>
|
||||
</div>
|
||||
<div class="dk-keyhint">press <kbd>←</kbd> to rewind · <kbd>F</kbd> for fullscreen</div>
|
||||
<div class="dk-page">fin</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-graphify-dark-graph
|
||||
description: 暗底知识图谱 deck — #06060c→#0e1020 深夜渐变 + 漂浮 blur orbs、封面 SVG 力导向图谱、彩虹渐变标题、JetBrains Mono 命令行高亮、glass-morphism 卡片。适合 dev-tool / CLI / 知识图谱 / 数据可视化的发布会,"AI-native + 科幻 + 暖色" 调子。
|
||||
triggers:
|
||||
- "知识图谱"
|
||||
- "graph deck"
|
||||
- "dark graph"
|
||||
- "dev tool launch"
|
||||
- "cli launch"
|
||||
- "data viz launch"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: engineering
|
||||
featured: 28
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-graphify-dark-graph 模板做一份 dev-tool 发布会 PPT。深夜渐变背景 + 力导向图谱封面 + 彩虹标题 + JetBrains Mono 命令行。先确认:工具名、核心能力、demo 步骤;要不要现场敲 CLI。"
|
||||
---
|
||||
# HTML PPT · 暗底知识图谱
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`graphify-dark-graph`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `graphify-dark-graph` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/graphify-dark-graph/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-graphify-dark-graph` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-graphify-dark-graph` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,402 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Graphify Dark Graph</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* graphify-dark-graph — 暗底玻璃 + 力导向知识图谱 */
|
||||
.tpl-graphify-dark-graph{
|
||||
--gd-bg:#06060c;
|
||||
--gd-bg2:#0e1020;
|
||||
--gd-text:#f0ece4;
|
||||
--gd-text2:#b0a99e;
|
||||
--gd-text3:#7a746c;
|
||||
--gd-warm:#e8a87c;
|
||||
--gd-blue:#7eb8da;
|
||||
--gd-green:#7ed3a4;
|
||||
--gd-rose:#d4a0b9;
|
||||
--gd-purple:#b8a4d6;
|
||||
--gd-danger:#e07070;
|
||||
background:var(--gd-bg);
|
||||
color:var(--gd-text);
|
||||
font-family:'Inter','Noto Sans SC',-apple-system,sans-serif;
|
||||
letter-spacing:-.01em;
|
||||
}
|
||||
.tpl-graphify-dark-graph .slide{background:linear-gradient(160deg,#08080f,#0e1020 50%,#08080f);color:var(--gd-text);padding:64px 88px;overflow:hidden}
|
||||
.tpl-graphify-dark-graph .gd-ambient{position:absolute;inset:0;pointer-events:none;z-index:0;overflow:hidden}
|
||||
.tpl-graphify-dark-graph .gd-orb{position:absolute;border-radius:50%;filter:blur(110px);opacity:.35;animation:gdDrift 22s ease-in-out infinite alternate}
|
||||
.tpl-graphify-dark-graph .gd-orb-1{width:520px;height:520px;background:radial-gradient(circle,rgba(126,184,218,.55),transparent 70%);top:-12%;left:-6%}
|
||||
.tpl-graphify-dark-graph .gd-orb-2{width:460px;height:460px;background:radial-gradient(circle,rgba(232,168,124,.45),transparent 70%);top:55%;right:-8%;animation-delay:-6s}
|
||||
.tpl-graphify-dark-graph .gd-orb-3{width:420px;height:420px;background:radial-gradient(circle,rgba(184,164,214,.4),transparent 70%);bottom:-8%;left:30%;animation-delay:-11s}
|
||||
@keyframes gdDrift{0%{transform:translate(0,0) scale(1)}100%{transform:translate(25px,-20px) scale(1.08)}}
|
||||
.tpl-graphify-dark-graph .slide > *{position:relative;z-index:2}
|
||||
.tpl-graphify-dark-graph .gd-snum{position:absolute;top:28px;right:40px;font-size:12px;letter-spacing:.25em;color:var(--gd-text3);z-index:3}
|
||||
.tpl-graphify-dark-graph .gd-eyebrow{font-size:13px;letter-spacing:.2em;text-transform:uppercase;color:var(--gd-text3);font-weight:500}
|
||||
.tpl-graphify-dark-graph .gd-h1{font-size:74px;font-weight:800;line-height:1.08;letter-spacing:-.02em;margin:16px 0 10px;color:var(--gd-text)}
|
||||
.tpl-graphify-dark-graph .gd-h2{font-size:52px;font-weight:700;line-height:1.12;margin:0 0 14px}
|
||||
.tpl-graphify-dark-graph .gd-lede{font-size:22px;line-height:1.65;font-weight:300;color:var(--gd-text2);max-width:850px}
|
||||
.tpl-graphify-dark-graph .gd-rainbow{background:linear-gradient(90deg,#ff0080,#ff4d00,#ff9900,#ffe600,#00c853,#0091ea,#6200ea,#ff0080);background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:gdRainbow 4s linear infinite}
|
||||
@keyframes gdRainbow{0%{background-position:0% center}100%{background-position:200% center}}
|
||||
.tpl-graphify-dark-graph .gd-grad{background:linear-gradient(135deg,var(--gd-warm),var(--gd-rose),var(--gd-purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.tpl-graphify-dark-graph .gd-accent{color:var(--gd-warm);font-weight:500}
|
||||
.tpl-graphify-dark-graph .gd-green{color:var(--gd-green)}
|
||||
.tpl-graphify-dark-graph .gd-blue{color:var(--gd-blue)}
|
||||
.tpl-graphify-dark-graph .gd-dim{color:var(--gd-text2)}
|
||||
.tpl-graphify-dark-graph .gd-mono{font-family:'JetBrains Mono',monospace}
|
||||
.tpl-graphify-dark-graph .gd-glass{position:relative;overflow:hidden;border-radius:20px;padding:22px 26px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.1);backdrop-filter:blur(20px) saturate(160%);box-shadow:0 8px 32px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.08)}
|
||||
.tpl-graphify-dark-graph .gd-glass::before{content:'';position:absolute;top:0;left:0;right:0;height:50%;background:linear-gradient(180deg,rgba(255,255,255,.05),transparent);pointer-events:none}
|
||||
.tpl-graphify-dark-graph .gd-glass-warm{background:rgba(232,168,124,.06);border-color:rgba(232,168,124,.2)}
|
||||
.tpl-graphify-dark-graph .gd-glass-green{background:rgba(126,211,164,.06);border-color:rgba(126,211,164,.2)}
|
||||
.tpl-graphify-dark-graph .gd-glass-blue{background:rgba(126,184,218,.06);border-color:rgba(126,184,218,.2)}
|
||||
.tpl-graphify-dark-graph .gd-grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:24px}
|
||||
.tpl-graphify-dark-graph .gd-grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:24px}
|
||||
.tpl-graphify-dark-graph .gd-tag{display:inline-block;border-radius:999px;padding:5px 14px;font-size:12px;font-weight:500;margin:2px 4px 2px 0;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:var(--gd-text2)}
|
||||
.tpl-graphify-dark-graph .gd-cmd{font-family:'JetBrains Mono',monospace;font-size:32px;font-weight:700;color:var(--gd-green);text-shadow:0 0 30px rgba(126,211,164,.45),0 0 60px rgba(126,211,164,.15);letter-spacing:-.01em}
|
||||
.tpl-graphify-dark-graph .gd-big{font-size:120px;font-weight:900;letter-spacing:-.04em;line-height:1}
|
||||
.tpl-graphify-dark-graph .gd-codebox{background:rgba(0,0,0,.55);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.8;color:#d8d4c8}
|
||||
.tpl-graphify-dark-graph .gd-codebox .cm{color:#6b6a62}
|
||||
.tpl-graphify-dark-graph .gd-codebox .kw{color:var(--gd-warm)}
|
||||
.tpl-graphify-dark-graph .gd-codebox .st{color:var(--gd-green)}
|
||||
.tpl-graphify-dark-graph .gd-codebox .fn{color:var(--gd-blue)}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-graphify-dark-graph">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div><div class="gd-orb gd-orb-2"></div><div class="gd-orb gd-orb-3"></div></div>
|
||||
<!-- live force-directed graph bg -->
|
||||
<svg viewBox="0 0 1600 900" style="position:absolute;inset:0;width:100%;height:100%;opacity:.38;z-index:1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="#7eb8da" stroke-width="1" stroke-opacity=".5" fill="none">
|
||||
<line x1="300" y1="200" x2="520" y2="340"/>
|
||||
<line x1="520" y1="340" x2="780" y2="260"/>
|
||||
<line x1="780" y1="260" x2="1040" y2="420"/>
|
||||
<line x1="520" y1="340" x2="640" y2="560"/>
|
||||
<line x1="640" y1="560" x2="900" y2="620"/>
|
||||
<line x1="900" y1="620" x2="1040" y2="420"/>
|
||||
<line x1="1040" y1="420" x2="1260" y2="300"/>
|
||||
<line x1="1260" y1="300" x2="1380" y2="500"/>
|
||||
<line x1="900" y1="620" x2="1120" y2="720"/>
|
||||
<line x1="300" y1="200" x2="200" y2="420"/>
|
||||
<line x1="200" y1="420" x2="360" y2="640"/>
|
||||
<line x1="360" y1="640" x2="640" y2="560"/>
|
||||
</g>
|
||||
<g>
|
||||
<circle cx="300" cy="200" r="10" fill="#e8a87c"/>
|
||||
<circle cx="520" cy="340" r="14" fill="#7eb8da"/>
|
||||
<circle cx="780" cy="260" r="9" fill="#7ed3a4"/>
|
||||
<circle cx="1040" cy="420" r="18" fill="#b8a4d6"/>
|
||||
<circle cx="640" cy="560" r="11" fill="#d4a0b9"/>
|
||||
<circle cx="900" cy="620" r="12" fill="#e8a87c"/>
|
||||
<circle cx="1260" cy="300" r="8" fill="#7ed3a4"/>
|
||||
<circle cx="1380" cy="500" r="10" fill="#7eb8da"/>
|
||||
<circle cx="1120" cy="720" r="9" fill="#d4a0b9"/>
|
||||
<circle cx="200" cy="420" r="8" fill="#b8a4d6"/>
|
||||
<circle cx="360" cy="640" r="11" fill="#7eb8da"/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="gd-snum">01 / 08</div>
|
||||
<div style="margin-top:auto">
|
||||
<p class="gd-eyebrow">Tech Sharing · 纯干货</p>
|
||||
<h1 class="gd-h1" style="font-size:88px"><span class="gd-rainbow">手把手用 Graphify<br>搭建个人知识图谱</span></h1>
|
||||
<p class="gd-lede" style="margin-top:20px">一行命令 · 全多模态 · 诚实审计 —— <span class="gd-accent">把任何文件夹变成可导航的知识网络。</span></p>
|
||||
<p class="gd-eyebrow" style="margin-top:26px">↑ 背景就是 Graphify 真实跑出来的知识图谱</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION DIVIDER -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div><div class="gd-orb gd-orb-2"></div></div>
|
||||
<div class="gd-snum">02 / 08</div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="gd-eyebrow">Part 01</div>
|
||||
<h1 class="gd-h1" style="font-size:120px">Why <span class="gd-grad">Graph</span>?</h1>
|
||||
<p class="gd-lede">folder → tree → graph,人类认知的下一步</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT — plugin grid -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-2"></div><div class="gd-orb gd-orb-3"></div></div>
|
||||
<div class="gd-snum">03 / 08</div>
|
||||
<p class="gd-eyebrow">Feature Map</p>
|
||||
<h2 class="gd-h2">一个工具,<span class="gd-grad">四件事</span></h2>
|
||||
<div class="gd-grid-4">
|
||||
<div class="gd-glass gd-glass-warm"><div style="font-size:30px">📂</div><h4 style="margin:10px 0 6px">Folder Ingest</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">递归扫描任意路径,支持 md / pdf / 代码 / 图片</p></div>
|
||||
<div class="gd-glass gd-glass-blue"><div style="font-size:30px">🧠</div><h4 style="margin:10px 0 6px">Entity Extract</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">用 LLM 抽概念、人物、事件、关系</p></div>
|
||||
<div class="gd-glass gd-glass-green"><div style="font-size:30px">🕸️</div><h4 style="margin:10px 0 6px">Force Graph</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">D3 力导向,点击即跳转原文</p></div>
|
||||
<div class="gd-glass"><div style="font-size:30px">🔍</div><h4 style="margin:10px 0 6px">Audit Trail</h4><p class="gd-dim" style="font-size:13px;line-height:1.55">每条边都能追溯到 source span</p></div>
|
||||
</div>
|
||||
<div class="gd-glass gd-glass-warm" style="margin-top:24px"><p style="font-size:18px;line-height:1.6">它不是「又一个 RAG」—— 它是 <span class="gd-accent">把检索结果画出来,让你一眼就知道信息长什么样</span>。</p></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CODE -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div></div>
|
||||
<div class="gd-snum">04 / 08</div>
|
||||
<p class="gd-eyebrow">One command</p>
|
||||
<h2 class="gd-h2">从 0 到图谱,<span class="gd-grad">大概 90 秒</span></h2>
|
||||
<p class="gd-cmd" style="margin:16px 0 22px">$ graphify ~/notes --out ./graph</p>
|
||||
<pre class="gd-codebox"><span class="cm"># graphify.config.yaml</span>
|
||||
<span class="kw">ingest</span>:
|
||||
paths: [<span class="st">~/notes</span>, <span class="st">~/code/docs</span>]
|
||||
include: [<span class="st">"*.md"</span>, <span class="st">"*.pdf"</span>, <span class="st">"*.py"</span>]
|
||||
|
||||
<span class="kw">extract</span>:
|
||||
model: <span class="st">claude-opus-4-6</span>
|
||||
schema: [<span class="st">concept</span>, <span class="st">person</span>, <span class="st">event</span>, <span class="st">relation</span>]
|
||||
|
||||
<span class="kw">render</span>:
|
||||
engine: <span class="st">d3-force</span>
|
||||
audit: <span class="fn">true</span> <span class="cm"># 每条边带 source span</span></pre>
|
||||
</section>
|
||||
|
||||
<!-- 5. CHART — race diagram -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-3"></div></div>
|
||||
<div class="gd-snum">05 / 08</div>
|
||||
<p class="gd-eyebrow">Efficiency Race</p>
|
||||
<h2 class="gd-h2">没有知识库 vs 有知识库</h2>
|
||||
<div style="max-width:900px;margin-top:30px">
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
|
||||
<div style="width:110px;text-align:right;font-weight:700;color:var(--gd-danger)">没有<br>知识库</div>
|
||||
<div style="flex:1;position:relative;height:70px;background:rgba(224,112,112,.06);border:1px solid rgba(224,112,112,.2);border-radius:16px">
|
||||
<div style="position:absolute;left:16px;top:50%;transform:translateY(-50%);font-size:32px">🛵</div>
|
||||
<div style="position:absolute;left:72px;top:50%;transform:translateY(-50%);color:var(--gd-danger);font-size:14px">反复喂信息…整理…又忘了…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px">
|
||||
<div style="width:110px;text-align:right;font-weight:700;color:var(--gd-green)">有<br>知识库</div>
|
||||
<div style="flex:1;position:relative;height:70px;background:rgba(126,211,164,.06);border:1px solid rgba(126,211,164,.25);border-radius:16px">
|
||||
<div style="position:absolute;right:16px;top:50%;transform:translateY(-50%);font-size:32px">🏎️</div>
|
||||
<div style="position:absolute;right:72px;top:50%;transform:translateY(-50%);color:var(--gd-green);font-size:14px">AI 自己找 → 确认 → 干活!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gd-grid-3" style="margin-top:36px">
|
||||
<div class="gd-glass gd-glass-warm"><div class="gd-big gd-grad">5×</div><p class="gd-dim" style="margin-top:6px">速度提升</p></div>
|
||||
<div class="gd-glass gd-glass-green"><div class="gd-big gd-grad">-80%</div><p class="gd-dim" style="margin-top:6px">重复喂信息</p></div>
|
||||
<div class="gd-glass gd-glass-blue"><div class="gd-big gd-grad">∞</div><p class="gd-dim" style="margin-top:6px">记忆持久化</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. PIPELINE -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-2"></div></div>
|
||||
<div class="gd-snum">06 / 08</div>
|
||||
<p class="gd-eyebrow">Pipeline</p>
|
||||
<h2 class="gd-h2">端到端 <span class="gd-grad">4 步走</span></h2>
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-top:36px">
|
||||
<div class="gd-glass" style="flex:1;text-align:center"><div style="font-size:34px">📂</div><div style="font-weight:600;margin-top:8px">Scan</div><div class="gd-dim" style="font-size:13px">递归读文件</div></div>
|
||||
<div style="color:var(--gd-text3);font-size:24px">→</div>
|
||||
<div class="gd-glass gd-glass-blue" style="flex:1;text-align:center"><div style="font-size:34px">🔬</div><div style="font-weight:600;margin-top:8px">Extract</div><div class="gd-dim" style="font-size:13px">LLM 抽实体</div></div>
|
||||
<div style="color:var(--gd-text3);font-size:24px">→</div>
|
||||
<div class="gd-glass gd-glass-green" style="flex:1;text-align:center"><div style="font-size:34px">🕸️</div><div style="font-weight:600;margin-top:8px">Build</div><div class="gd-dim" style="font-size:13px">构图 + 去重</div></div>
|
||||
<div style="color:var(--gd-text3);font-size:24px">→</div>
|
||||
<div class="gd-glass gd-glass-warm" style="flex:1;text-align:center"><div style="font-size:34px">🎨</div><div style="font-weight:600;margin-top:8px">Render</div><div class="gd-dim" style="font-size:13px">D3 交互图</div></div>
|
||||
</div>
|
||||
<div class="gd-glass" style="margin-top:32px"><p style="font-size:16px;line-height:1.6;color:var(--gd-text2)">每一步都有 audit log:你永远知道某个节点为什么存在、它来自哪个文件的哪一行。</p></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-1"></div><div class="gd-orb gd-orb-3"></div></div>
|
||||
<div class="gd-snum">07 / 08</div>
|
||||
<p class="gd-eyebrow">Try it tonight</p>
|
||||
<h2 class="gd-h1" style="font-size:80px">Graphify <span class="gd-grad">your folders</span></h2>
|
||||
<p class="gd-cmd" style="margin-top:22px">$ npm i -g @lewis/graphify</p>
|
||||
<p class="gd-cmd" style="margin-top:10px;color:var(--gd-warm);text-shadow:0 0 30px rgba(232,168,124,.45)">$ graphify ~/obsidian-vault</p>
|
||||
<div style="margin-top:32px">
|
||||
<span class="gd-tag">#knowledge-graph</span>
|
||||
<span class="gd-tag">#open-source</span>
|
||||
<span class="gd-tag">#claude-agent</span>
|
||||
<span class="gd-tag">#obsidian</span>
|
||||
<span class="gd-tag">#d3-force</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="gd-ambient"><div class="gd-orb gd-orb-2"></div></div>
|
||||
<div class="gd-snum">08 / 08</div>
|
||||
<div style="margin:auto 0;text-align:center">
|
||||
<div class="gd-big gd-rainbow" style="font-size:180px">Thanks.</div>
|
||||
<p class="gd-lede" style="margin:28px auto 0">github.com/lewis/graphify · 欢迎 star / issue / PR</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-hermes-cyber-terminal
|
||||
description: 暗终端 honest-review deck — #0a0c10 黑底 + 56px 赛博网格 + CRT 暗角 + 扫描线、窗口红绿灯 chrome、`$ prompt` 命令行标题、薄荷绿 #7ed3a4 大字、JetBrains Mono、stroke-only 柱状图、blinking 光标、琥珀/绿/红三档 tag、暗色代码块。适合 CLI / agent / dev tool 测评(含 trace、diff、benchmark)。
|
||||
triggers:
|
||||
- "terminal review"
|
||||
- "cli review"
|
||||
- "agent review"
|
||||
- "honest review"
|
||||
- "dev tool review"
|
||||
- "测评"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: engineering
|
||||
featured: 30
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-hermes-cyber-terminal 模板做一份 CLI / agent 测评 PPT。深色终端风 + scanlines + 命令行标题 + benchmark 柱状图。先确认:被测评对象、3-5 个对比维度、benchmark 数据。"
|
||||
---
|
||||
# HTML PPT · 暗终端测评
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`hermes-cyber-terminal`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `hermes-cyber-terminal` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/hermes-cyber-terminal/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-hermes-cyber-terminal` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-hermes-cyber-terminal` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,422 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Hermes Cyber Terminal</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* hermes-cyber-terminal — 暗终端 + 霓虹绿青 + 扫描线 */
|
||||
.tpl-hermes-cyber-terminal{
|
||||
--hc-bg:#0a0c10;
|
||||
--hc-bg2:#15151b;
|
||||
--hc-surface:#12141a;
|
||||
--hc-border:rgba(126,211,164,.18);
|
||||
--hc-ink:#e4e2d8;
|
||||
--hc-ink2:#8a8892;
|
||||
--hc-green:#7ed3a4;
|
||||
--hc-cyan:#64dfdf;
|
||||
--hc-amber:#e9c58a;
|
||||
--hc-rose:#d4a0b9;
|
||||
--hc-red:#ff6b6b;
|
||||
background:var(--hc-bg);
|
||||
color:var(--hc-ink);
|
||||
font-family:'JetBrains Mono','SF Mono','Inter','Noto Sans SC',monospace;
|
||||
}
|
||||
.tpl-hermes-cyber-terminal .slide{background:var(--hc-bg);color:var(--hc-ink);padding:60px 84px;overflow:hidden}
|
||||
.tpl-hermes-cyber-terminal .hc-scanlines{position:absolute;inset:0;pointer-events:none;z-index:3;background:repeating-linear-gradient(180deg,transparent 0,transparent 3px,rgba(126,211,164,.025) 3px,rgba(126,211,164,.025) 4px);mix-blend-mode:screen}
|
||||
.tpl-hermes-cyber-terminal .hc-grid{position:absolute;inset:0;pointer-events:none;opacity:.35;background-image:linear-gradient(rgba(126,211,164,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(126,211,164,.08) 1px,transparent 1px);background-size:56px 56px;mask-image:radial-gradient(ellipse at 50% 50%,black 30%,transparent 80%)}
|
||||
.tpl-hermes-cyber-terminal .hc-vignette{position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse at 50% 50%,transparent 50%,rgba(0,0,0,.6) 100%)}
|
||||
.tpl-hermes-cyber-terminal .slide > *{position:relative;z-index:2}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;font-size:11px;color:var(--hc-ink2);letter-spacing:.18em;text-transform:uppercase}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots{display:flex;gap:8px}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span{width:11px;height:11px;border-radius:50%;background:#2a2d33}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span:nth-child(1){background:#ff5f57}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span:nth-child(2){background:#febc2e}
|
||||
.tpl-hermes-cyber-terminal .hc-chrome .dots span:nth-child(3){background:var(--hc-green)}
|
||||
.tpl-hermes-cyber-terminal .hc-prompt{color:var(--hc-green);font-weight:500}
|
||||
.tpl-hermes-cyber-terminal .hc-prompt::before{content:'$ ';color:var(--hc-cyan)}
|
||||
.tpl-hermes-cyber-terminal .hc-h1{font-family:'JetBrains Mono',monospace;font-size:72px;font-weight:700;line-height:1.05;letter-spacing:-.02em;color:var(--hc-green);text-shadow:0 0 30px rgba(126,211,164,.35),0 0 60px rgba(126,211,164,.1);margin:14px 0 12px}
|
||||
.tpl-hermes-cyber-terminal .hc-h2{font-size:46px;font-weight:600;color:var(--hc-ink);margin:0 0 10px;letter-spacing:-.015em}
|
||||
.tpl-hermes-cyber-terminal .hc-h3{font-size:22px;font-weight:600;color:var(--hc-amber);margin:0 0 10px}
|
||||
.tpl-hermes-cyber-terminal .hc-lede{font-size:18px;line-height:1.7;color:var(--hc-ink2);max-width:780px;font-family:'Inter','Noto Sans SC',sans-serif}
|
||||
.tpl-hermes-cyber-terminal .hc-cursor{display:inline-block;width:12px;height:1em;background:var(--hc-green);vertical-align:middle;margin-left:6px;animation:hcBlink 1s steps(2) infinite}
|
||||
@keyframes hcBlink{50%{opacity:0}}
|
||||
.tpl-hermes-cyber-terminal .hc-card{background:var(--hc-surface);border:1px solid var(--hc-border);border-radius:10px;padding:20px 24px;position:relative}
|
||||
.tpl-hermes-cyber-terminal .hc-card::before{content:'';position:absolute;top:-1px;left:12px;right:12px;height:2px;background:linear-gradient(90deg,transparent,var(--hc-green),transparent)}
|
||||
.tpl-hermes-cyber-terminal .hc-card .lbl{font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--hc-ink2);margin-bottom:8px}
|
||||
.tpl-hermes-cyber-terminal .hc-card .val{font-size:22px;font-weight:700;color:var(--hc-green);font-family:'JetBrains Mono',monospace}
|
||||
.tpl-hermes-cyber-terminal .hc-card .desc{font-size:13px;color:var(--hc-ink2);margin-top:10px;line-height:1.55;font-family:'Inter',sans-serif}
|
||||
.tpl-hermes-cyber-terminal .hc-grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-top:24px}
|
||||
.tpl-hermes-cyber-terminal .hc-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:24px}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox{background:#0c0d12;border:1px solid var(--hc-border);border-radius:10px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.85;color:#d8d4c8;box-shadow:inset 0 0 60px rgba(126,211,164,.04)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .cm{color:#5a6068}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .kw{color:var(--hc-amber)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .st{color:var(--hc-green)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .fn{color:var(--hc-cyan)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .var{color:var(--hc-rose)}
|
||||
.tpl-hermes-cyber-terminal .hc-codebox .hl{color:#fff;background:rgba(126,211,164,.15);padding:0 4px;border-radius:3px}
|
||||
.tpl-hermes-cyber-terminal .hc-tag{display:inline-block;font-family:'JetBrains Mono',monospace;font-size:11px;padding:3px 10px;border:1px solid var(--hc-border);border-radius:4px;color:var(--hc-green);background:rgba(126,211,164,.04);margin:2px 6px 2px 0;text-transform:uppercase;letter-spacing:.1em}
|
||||
.tpl-hermes-cyber-terminal .hc-tag.amber{color:var(--hc-amber);border-color:rgba(233,197,138,.2);background:rgba(233,197,138,.04)}
|
||||
.tpl-hermes-cyber-terminal .hc-tag.red{color:var(--hc-red);border-color:rgba(255,107,107,.25);background:rgba(255,107,107,.05)}
|
||||
.tpl-hermes-cyber-terminal .hc-big{font-family:'JetBrains Mono',monospace;font-size:140px;font-weight:700;line-height:1;color:var(--hc-green);text-shadow:0 0 40px rgba(126,211,164,.4),0 0 80px rgba(126,211,164,.15);letter-spacing:-.04em}
|
||||
.tpl-hermes-cyber-terminal .hc-footer{position:absolute;left:84px;right:84px;bottom:32px;display:flex;justify-content:space-between;font-size:10px;color:var(--hc-ink2);letter-spacing:.2em;text-transform:uppercase;border-top:1px solid rgba(126,211,164,.1);padding-top:14px}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-hermes-cyber-terminal">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-vignette"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>~/hermes · zsh · 118x42 · 01:37:04</div></div>
|
||||
<div style="margin:auto 0">
|
||||
<p class="hc-prompt">whoami --hermes</p>
|
||||
<h1 class="hc-h1">HERMES<br>AGENT / v0.9.2<span class="hc-cursor"></span></h1>
|
||||
<p class="hc-lede">一个号称能「自主跑完整软件工程任务」的命令行 agent。<br>真的好用?还是又一轮营销?—— 我连续跑了 72 小时,告诉你答案。</p>
|
||||
<div style="margin-top:26px">
|
||||
<span class="hc-tag">rust-core</span>
|
||||
<span class="hc-tag">mcp-native</span>
|
||||
<span class="hc-tag amber">72h-benchmark</span>
|
||||
<span class="hc-tag red">honest-review</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hc-footer"><span>hermes-review · lewis · 2026</span><span>01 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION DIVIDER -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>section · 01/04</div></div>
|
||||
<div style="margin:auto 0">
|
||||
<p class="hc-prompt">cat chapter_01.md</p>
|
||||
<h1 class="hc-h1" style="font-size:110px">// Setup</h1>
|
||||
<p class="hc-lede">从 <code style="color:var(--hc-amber)">brew install hermes</code> 到第一次 prompt —— 一共 4 分 22 秒。</p>
|
||||
</div>
|
||||
<div class="hc-footer"><span>section · setup</span><span>02 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT — spec cards -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>benchmark · cold-start</div></div>
|
||||
<h2 class="hc-h2">开箱数据</h2>
|
||||
<p class="hc-lede">cold start → first-successful-task 三次平均</p>
|
||||
<div class="hc-grid-3">
|
||||
<div class="hc-card"><div class="lbl">install time</div><div class="val">42s</div><div class="desc">单 binary,无 docker,无 python env。</div></div>
|
||||
<div class="hc-card"><div class="lbl">first token</div><div class="val">1.8s</div><div class="desc">接入 claude-opus-4-6,无预热。</div></div>
|
||||
<div class="hc-card"><div class="lbl">first PR merged</div><div class="val">4m22s</div><div class="desc">跑的是 fix-a-typo 级别的低难度任务。</div></div>
|
||||
</div>
|
||||
<div class="hc-grid-2">
|
||||
<div class="hc-card"><div class="lbl">// verdict +</div><div class="val" style="color:var(--hc-green);font-size:18px">冷启动是真的快</div><div class="desc">和 OpenClaw 的 docker + pip 流程比,快不止一个数量级。</div></div>
|
||||
<div class="hc-card"><div class="lbl">// verdict -</div><div class="val" style="color:var(--hc-red);font-size:18px">MCP 服务器配置不够友好</div><div class="desc">env 变量需要手动塞进 ~/.hermes/env,文档几乎没写。</div></div>
|
||||
</div>
|
||||
<div class="hc-footer"><span>data · verified 3 runs</span><span>03 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CODE -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>trace · hermes run</div></div>
|
||||
<p class="hc-prompt">hermes run "refactor auth module to use pkce"</p>
|
||||
<h3 class="hc-h3" style="margin-top:12px">↓ 真实 trace (节选)</h3>
|
||||
<pre class="hc-codebox" style="margin-top:10px"><span class="cm"># hermes v0.9.2 · session 42a1</span>
|
||||
[<span class="fn">plan</span>] <span class="st">"分析 src/auth/*.ts → 找 oauth flow → 抽成 pkce"</span>
|
||||
[<span class="fn">read</span>] src/auth/oauth.ts <span class="cm">// 214 lines</span>
|
||||
[<span class="fn">read</span>] src/auth/token.ts <span class="cm">// 88 lines</span>
|
||||
[<span class="kw">think</span>] <span class="st">"发现 implicit flow,改为 code+pkce,需新 state param"</span>
|
||||
[<span class="fn">edit</span>] src/auth/oauth.ts <span class="hl">+43 -17</span>
|
||||
[<span class="fn">edit</span>] src/auth/token.ts <span class="hl">+12 -4</span>
|
||||
[<span class="fn">test</span>] pnpm vitest auth <span class="st">PASS 18/18</span>
|
||||
[<span class="fn">commit</span>] <span class="var">"feat(auth): migrate to oauth2 code+pkce"</span>
|
||||
[<span class="fn">push</span>] origin feat/pkce-auth <span class="st">ok</span>
|
||||
|
||||
<span class="cm"># 总耗时 3m 14s · 14k tokens · $0.21</span></pre>
|
||||
<div class="hc-footer"><span>trace · live</span><span>04 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CHART -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>benchmark · hermes vs openclaw</div></div>
|
||||
<h2 class="hc-h2">72 小时对比</h2>
|
||||
<p class="hc-lede">同一组 48 个 GitHub issue,两个 agent 各跑一遍</p>
|
||||
<svg viewBox="0 0 1000 380" style="width:100%;max-width:1040px;margin-top:24px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g font-family="JetBrains Mono, monospace" font-size="13" fill="#8a8892">
|
||||
<!-- axis -->
|
||||
<line x1="80" y1="40" x2="80" y2="320" stroke="rgba(126,211,164,.2)"/>
|
||||
<line x1="80" y1="320" x2="960" y2="320" stroke="rgba(126,211,164,.2)"/>
|
||||
<!-- y labels -->
|
||||
<text x="70" y="46" text-anchor="end">100%</text>
|
||||
<text x="70" y="116" text-anchor="end">75%</text>
|
||||
<text x="70" y="186" text-anchor="end">50%</text>
|
||||
<text x="70" y="256" text-anchor="end">25%</text>
|
||||
<text x="70" y="324" text-anchor="end">0</text>
|
||||
<!-- bars: hermes -->
|
||||
<g>
|
||||
<rect x="130" y="80" width="80" height="240" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="170" y="76" text-anchor="middle" fill="#7ed3a4" font-weight="700">82%</text>
|
||||
<text x="170" y="345" text-anchor="middle">resolved</text>
|
||||
<rect x="240" y="146" width="80" height="174" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="280" y="142" text-anchor="middle" fill="#7ed3a4" font-weight="700">58%</text>
|
||||
<text x="280" y="345" text-anchor="middle">one-shot</text>
|
||||
<rect x="350" y="60" width="80" height="260" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="390" y="56" text-anchor="middle" fill="#7ed3a4" font-weight="700">89%</text>
|
||||
<text x="390" y="345" text-anchor="middle">test-pass</text>
|
||||
<rect x="460" y="110" width="80" height="210" fill="rgba(126,211,164,.15)" stroke="#7ed3a4" stroke-width="1.5"/>
|
||||
<text x="500" y="106" text-anchor="middle" fill="#7ed3a4" font-weight="700">71%</text>
|
||||
<text x="500" y="345" text-anchor="middle">pr-merged</text>
|
||||
</g>
|
||||
<!-- bars: openclaw -->
|
||||
<g>
|
||||
<rect x="570" y="150" width="80" height="170" fill="rgba(233,197,138,.12)" stroke="#e9c58a" stroke-width="1.5"/>
|
||||
<text x="610" y="146" text-anchor="middle" fill="#e9c58a" font-weight="700">60%</text>
|
||||
<text x="610" y="345" text-anchor="middle">resolved</text>
|
||||
<rect x="680" y="212" width="80" height="108" fill="rgba(233,197,138,.12)" stroke="#e9c58a" stroke-width="1.5"/>
|
||||
<text x="720" y="208" text-anchor="middle" fill="#e9c58a" font-weight="700">38%</text>
|
||||
<text x="720" y="345" text-anchor="middle">one-shot</text>
|
||||
<rect x="790" y="130" width="80" height="190" fill="rgba(233,197,138,.12)" stroke="#e9c58a" stroke-width="1.5"/>
|
||||
<text x="830" y="126" text-anchor="middle" fill="#e9c58a" font-weight="700">67%</text>
|
||||
<text x="830" y="345" text-anchor="middle">test-pass</text>
|
||||
</g>
|
||||
<!-- legend -->
|
||||
<g transform="translate(820,50)">
|
||||
<rect x="0" y="0" width="14" height="14" fill="rgba(126,211,164,.15)" stroke="#7ed3a4"/>
|
||||
<text x="22" y="12" fill="#7ed3a4">hermes 0.9.2</text>
|
||||
<rect x="0" y="22" width="14" height="14" fill="rgba(233,197,138,.12)" stroke="#e9c58a"/>
|
||||
<text x="22" y="34" fill="#e9c58a">openclaw 2.1</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="hc-footer"><span>benchmark · n=48</span><span>05 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. STATS -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>tldr</div></div>
|
||||
<p class="hc-prompt">echo $VERDICT</p>
|
||||
<div class="hc-big">7.8<span style="font-size:60px;color:var(--hc-ink2)">/ 10</span></div>
|
||||
<p class="hc-lede" style="margin-top:14px">值得装,还不值得完全依赖。</p>
|
||||
<div class="hc-grid-2" style="margin-top:24px">
|
||||
<div class="hc-card"><div class="lbl">+ strong points</div><div class="desc">• rust 本体冷启快<br>• trace 可读性极强<br>• diff 审核友好,commit message 也写得合格</div></div>
|
||||
<div class="hc-card"><div class="lbl">- weak points</div><div class="desc">• plan 阶段偶尔跳步<br>• 超 50k LoC 仓库会 OOM<br>• MCP 配置需要手动塞 env</div></div>
|
||||
</div>
|
||||
<div class="hc-footer"><span>verdict · honest</span><span>06 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>install</div></div>
|
||||
<h2 class="hc-h2">想自己跑一遍?</h2>
|
||||
<p class="hc-lede">三条命令,不到 5 分钟就能看见它干第一件事。</p>
|
||||
<pre class="hc-codebox" style="margin-top:22px"><span class="cm"># 1. install</span>
|
||||
<span class="kw">$</span> brew install hermes-agent/tap/hermes
|
||||
|
||||
<span class="cm"># 2. auth (先准备好 anthropic api key)</span>
|
||||
<span class="kw">$</span> hermes auth login
|
||||
|
||||
<span class="cm"># 3. first task</span>
|
||||
<span class="kw">$</span> cd ~/your-repo && hermes run <span class="st">"add a CHANGELOG.md from git log"</span></pre>
|
||||
<div style="margin-top:26px">
|
||||
<span class="hc-tag">brew-ready</span>
|
||||
<span class="hc-tag">opus-4.6</span>
|
||||
<span class="hc-tag amber">needs-api-key</span>
|
||||
</div>
|
||||
<div class="hc-footer"><span>try-it-now</span><span>07 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="hc-grid"></div>
|
||||
<div class="hc-scanlines"></div>
|
||||
<div class="hc-chrome"><div class="dots"><span></span><span></span><span></span></div><div>EOF</div></div>
|
||||
<div style="margin:auto 0">
|
||||
<p class="hc-prompt">exit 0</p>
|
||||
<h1 class="hc-h1" style="font-size:120px">// thanks<span class="hc-cursor"></span></h1>
|
||||
<p class="hc-lede">完整 trace、48 个任务的 PR 列表、benchmark 脚本都在 <span style="color:var(--hc-amber)">github.com/lewis/hermes-review</span></p>
|
||||
</div>
|
||||
<div class="hc-footer"><span>session closed</span><span>08 / 08</span></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-knowledge-arch-blueprint
|
||||
description: 奶油蓝图架构 deck — 奶油纸 #F0EAE0 底色 + 单一锈红 #B5392A 高亮、48px 蓝图网格 mask、2px 黑边硬卡片、pipeline 步骤盒(其中一个抬高)、右侧锈红 insight callout、Playfair 衬线大字、SVG 虚线反馈环。零渐变零软阴影,认真且印刷友好。
|
||||
triggers:
|
||||
- "architecture"
|
||||
- "blueprint"
|
||||
- "system design"
|
||||
- "架构图"
|
||||
- "data flow"
|
||||
- "engineering whitepaper"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: engineering
|
||||
featured: 29
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-knowledge-arch-blueprint 模板做一份系统架构介绍 PPT。奶油纸底 + 锈红高亮 + 蓝图网格 + pipeline 抬高一格 + 衬线大字。先告诉我系统名 + 5-7 个核心模块 + 数据流方向,再写 8-10 页。"
|
||||
---
|
||||
# HTML PPT · 奶油蓝图架构
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`knowledge-arch-blueprint`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `knowledge-arch-blueprint` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/knowledge-arch-blueprint/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-knowledge-arch-blueprint` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-knowledge-arch-blueprint` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,407 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Knowledge Arch Blueprint</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* knowledge-arch-blueprint — 奶油纸 + 建筑蓝图风 */
|
||||
.tpl-knowledge-arch-blueprint{
|
||||
--kb-bg:#F0EAE0;
|
||||
--kb-ink:#1a1a1a;
|
||||
--kb-ink2:#555;
|
||||
--kb-ink3:#aaa;
|
||||
--kb-rust:#B5392A;
|
||||
--kb-rust-soft:rgba(181,57,42,.08);
|
||||
--kb-line:#cec8be;
|
||||
background:var(--kb-bg);
|
||||
color:var(--kb-ink);
|
||||
font-family:'Inter','Noto Sans SC',-apple-system,sans-serif;
|
||||
}
|
||||
.tpl-knowledge-arch-blueprint .slide{background:var(--kb-bg);color:var(--kb-ink);padding:64px 80px}
|
||||
.tpl-knowledge-arch-blueprint .kb-grid-bg{position:absolute;inset:0;pointer-events:none;opacity:.35;background-image:linear-gradient(rgba(26,26,26,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(26,26,26,.06) 1px,transparent 1px);background-size:48px 48px;mask-image:radial-gradient(ellipse at center,black 40%,transparent 85%)}
|
||||
.tpl-knowledge-arch-blueprint .slide > *{position:relative;z-index:2}
|
||||
.tpl-knowledge-arch-blueprint .kb-kicker{font-size:13px;font-weight:800;letter-spacing:4px;text-transform:uppercase;color:var(--kb-rust);margin-bottom:12px}
|
||||
.tpl-knowledge-arch-blueprint .kb-h1{font-size:66px;font-weight:900;line-height:1.08;color:#111;margin:0 0 14px;letter-spacing:-.02em}
|
||||
.tpl-knowledge-arch-blueprint .kb-h1 span.rust{color:var(--kb-rust)}
|
||||
.tpl-knowledge-arch-blueprint .kb-sub{font-size:20px;color:#666;line-height:1.55;max-width:780px}
|
||||
.tpl-knowledge-arch-blueprint .kb-insight{display:inline-block;background:var(--kb-rust);color:#fff;border-radius:10px;padding:16px 22px;font-size:14px;font-weight:700;line-height:1.5;max-width:340px;box-shadow:0 8px 24px rgba(181,57,42,.22)}
|
||||
.tpl-knowledge-arch-blueprint .kb-insight .kk{font-size:10px;letter-spacing:2px;opacity:.7;display:block;margin-bottom:6px;font-weight:800}
|
||||
.tpl-knowledge-arch-blueprint .kb-section-label{font-size:11px;font-weight:800;letter-spacing:3.5px;text-transform:uppercase;color:#aaa;margin:30px 0 12px;display:flex;align-items:center;gap:14px}
|
||||
.tpl-knowledge-arch-blueprint .kb-section-label::after{content:'';flex:1;height:1px;background:var(--kb-line)}
|
||||
.tpl-knowledge-arch-blueprint .kb-pipeline{display:flex;align-items:stretch;gap:14px;margin-top:24px}
|
||||
.tpl-knowledge-arch-blueprint .kb-step{flex:1;border:2px solid #1a1a1a;border-radius:12px;padding:22px 18px;background:#fff;position:relative;min-height:200px;display:flex;flex-direction:column}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero{background:var(--kb-rust);border-color:var(--kb-rust);color:#fff;flex:1.25;box-shadow:0 10px 32px rgba(181,57,42,.28);transform:translateY(-10px)}
|
||||
.tpl-knowledge-arch-blueprint .kb-step-num{font-size:10px;font-weight:800;letter-spacing:2.5px;color:#bbb;margin-bottom:8px;text-transform:uppercase}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero .kb-step-num{color:rgba(255,255,255,.6)}
|
||||
.tpl-knowledge-arch-blueprint .kb-step-title{font-size:22px;font-weight:900;line-height:1.15;color:#111;margin-bottom:8px}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero .kb-step-title{color:#fff}
|
||||
.tpl-knowledge-arch-blueprint .kb-step-body{font-size:13px;line-height:1.55;color:#555;margin-top:auto}
|
||||
.tpl-knowledge-arch-blueprint .kb-step.hero .kb-step-body{color:rgba(255,255,255,.88)}
|
||||
.tpl-knowledge-arch-blueprint .kb-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:24px}
|
||||
.tpl-knowledge-arch-blueprint .kb-card{background:#fff;border:2px solid #1a1a1a;border-radius:12px;padding:22px 24px}
|
||||
.tpl-knowledge-arch-blueprint .kb-card h4{font-size:20px;font-weight:900;margin-bottom:6px}
|
||||
.tpl-knowledge-arch-blueprint .kb-card p{font-size:14px;color:#555;line-height:1.55}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend{display:flex;gap:18px;flex-wrap:wrap;margin-top:22px;font-size:12px;color:#666}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend .d{display:flex;align-items:center;gap:8px}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend .b{width:14px;height:14px;border:2px solid #1a1a1a;border-radius:3px}
|
||||
.tpl-knowledge-arch-blueprint .kb-legend .b.rust{background:var(--kb-rust);border-color:var(--kb-rust)}
|
||||
.tpl-knowledge-arch-blueprint .kb-footer{position:absolute;left:80px;right:80px;bottom:36px;display:flex;justify-content:space-between;font-size:11px;color:#999;letter-spacing:.15em;text-transform:uppercase;border-top:1px solid var(--kb-line);padding-top:16px}
|
||||
.tpl-knowledge-arch-blueprint .kb-mono{font-family:'JetBrains Mono',monospace}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox{background:#1a1a1a;color:#f0eae0;border-radius:12px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.8;margin-top:20px;border:2px solid #1a1a1a}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .cm{color:#7a766c}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .kw{color:#e8a87c}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .st{color:#b3d1bc}
|
||||
.tpl-knowledge-arch-blueprint .kb-codebox .hl{color:var(--kb-rust);background:rgba(255,255,255,.08);padding:0 4px;border-radius:3px}
|
||||
.tpl-knowledge-arch-blueprint .kb-big-num{font-family:'Playfair Display',Georgia,serif;font-size:200px;font-weight:900;line-height:.9;color:var(--kb-rust);letter-spacing:-.04em}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-knowledge-arch-blueprint">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:40px;margin-bottom:44px">
|
||||
<div>
|
||||
<div class="kb-kicker">Karpathy Stack · 架构图 v2</div>
|
||||
<h1 class="kb-h1">LLM <span class="rust">知识库</span> 的<br>工程化蓝图</h1>
|
||||
<p class="kb-sub">从「乱贴笔记」到「可审计、可纠错、可复用」的第二大脑 —— 这是我读完 Karpathy 的分享后画的一张系统图。</p>
|
||||
</div>
|
||||
<div class="kb-insight"><span class="kk">KEY INSIGHT</span>Karpathy 原版缺一块:<br>反馈闭环让错误能回流纠正。</div>
|
||||
</div>
|
||||
<div class="kb-section-label">Pipeline · End-to-end</div>
|
||||
<div class="kb-pipeline">
|
||||
<div class="kb-step"><div class="kb-step-num">STEP 01</div><div class="kb-step-title">采集</div><div class="kb-step-body">浏览器剪藏、PDF、Podcast 转写、聊天记录</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">STEP 02</div><div class="kb-step-title">去噪</div><div class="kb-step-body">清洗导航栏、广告、重复段落、低信噪素材</div></div>
|
||||
<div class="kb-step hero"><div class="kb-step-num">STEP 03 · CORE</div><div class="kb-step-title">Wiki 化</div><div class="kb-step-body">结构化成双链笔记,实体、关系、属性全在一起</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">STEP 04</div><div class="kb-step-title">使用</div><div class="kb-step-body">Agent 随时检索、回答、再写入</div></div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Blueprint · v2 · 2026.04</span><span>01 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION DIVIDER -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="kb-kicker">Chapter One</div>
|
||||
<h1 class="kb-h1" style="font-size:120px">为什么 <span class="rust">笔记</span><br>不够用了</h1>
|
||||
<p class="kb-sub" style="font-size:24px;margin-top:20px">当你的知识量超过记忆容量,<br>你需要的不是更多文件,而是一张<b>可导航的图</b>。</p>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Section · Chapter 1</span><span>02 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT 2-col -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">Problem · Solution</div>
|
||||
<h1 class="kb-h1" style="font-size:48px">原版 vs <span class="rust">升级版</span></h1>
|
||||
<div class="kb-grid-2">
|
||||
<div class="kb-card">
|
||||
<div class="kb-kicker" style="color:#888">原版 Karpathy</div>
|
||||
<h4>一次性写入</h4>
|
||||
<p>采集 → 转写 → 存档,错了就错了。没有回路,没有修正机制,笔记越多越混乱。</p>
|
||||
</div>
|
||||
<div class="kb-card" style="background:var(--kb-rust-soft);border-color:var(--kb-rust)">
|
||||
<div class="kb-kicker">升级 v2</div>
|
||||
<h4>反馈闭环</h4>
|
||||
<p>AI 使用知识库时记录每次 miss / 幻觉 / 过期事实,自动回灌到源文件,让笔记会自我修正。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kb-legend">
|
||||
<div class="d"><span class="b"></span>普通节点</div>
|
||||
<div class="d"><span class="b rust"></span>核心节点 · 反馈回路入口</div>
|
||||
<div class="d">—— 数据流 ┈┈ 反馈回路</div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Content · Compare</span><span>03 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CODE -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">Implementation · Skill Manifest</div>
|
||||
<h1 class="kb-h1" style="font-size:48px">反馈回路 <span class="rust">怎么实现</span></h1>
|
||||
<p class="kb-sub">一个 100 行的 Agent Skill,把「AI 用得顺不顺」回写成 vault 的一条条修订记录。</p>
|
||||
<pre class="kb-codebox"><span class="cm"># skills/wiki-feedback/SKILL.md</span>
|
||||
<span class="kw">name</span>: wiki-feedback
|
||||
<span class="kw">trigger</span>: <span class="st">"after every retrieval"</span>
|
||||
|
||||
<span class="kw">on_hit</span>: record(<span class="st">query, path, used=true</span>)
|
||||
<span class="kw">on_miss</span>: record(<span class="st">query, reason=</span><span class="hl">"not-in-vault"</span>)
|
||||
<span class="kw">on_wrong</span>: record(<span class="st">query, correction, path</span>)
|
||||
|
||||
<span class="kw">nightly</span>:
|
||||
- <span class="st">aggregate misses → suggest new notes</span>
|
||||
- <span class="st">aggregate wrongs → diff-patch old notes</span>
|
||||
- <span class="st">commit to git, open PR for human review</span></pre>
|
||||
<div class="kb-footer"><span>Content · Code</span><span>04 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. DIAGRAM - SVG feedback loop -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">System Diagram</div>
|
||||
<h1 class="kb-h1" style="font-size:44px">反馈回路全貌</h1>
|
||||
<svg viewBox="0 0 1200 520" style="width:100%;max-width:1200px;margin-top:20px" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#1a1a1a"/>
|
||||
</marker>
|
||||
<marker id="arrow-r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#B5392A"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<g font-family="Inter, sans-serif" font-size="16" font-weight="700">
|
||||
<!-- boxes -->
|
||||
<rect x="40" y="180" width="200" height="120" rx="10" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
|
||||
<text x="140" y="220" text-anchor="middle">Sources</text>
|
||||
<text x="140" y="250" text-anchor="middle" font-size="12" font-weight="400" fill="#666">web · pdf · chat</text>
|
||||
|
||||
<rect x="300" y="180" width="200" height="120" rx="10" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
|
||||
<text x="400" y="220" text-anchor="middle">Clean + Split</text>
|
||||
<text x="400" y="250" text-anchor="middle" font-size="12" font-weight="400" fill="#666">defuddle / chunker</text>
|
||||
|
||||
<rect x="560" y="160" width="220" height="160" rx="10" fill="#B5392A" stroke="#B5392A" stroke-width="2"/>
|
||||
<text x="670" y="210" text-anchor="middle" fill="#fff" font-size="20">Vault (Wiki)</text>
|
||||
<text x="670" y="240" text-anchor="middle" font-size="12" font-weight="400" fill="rgba(255,255,255,.8)">markdown · links</text>
|
||||
<text x="670" y="262" text-anchor="middle" font-size="12" font-weight="400" fill="rgba(255,255,255,.8)">bases · canvas</text>
|
||||
|
||||
<rect x="840" y="180" width="200" height="120" rx="10" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
|
||||
<text x="940" y="220" text-anchor="middle">Agent Use</text>
|
||||
<text x="940" y="250" text-anchor="middle" font-size="12" font-weight="400" fill="#666">retrieve / answer</text>
|
||||
|
||||
<!-- forward arrows -->
|
||||
<line x1="245" y1="240" x2="295" y2="240" stroke="#1a1a1a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<line x1="505" y1="240" x2="555" y2="240" stroke="#1a1a1a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<line x1="785" y1="240" x2="835" y2="240" stroke="#1a1a1a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- feedback dashed -->
|
||||
<path d="M 940 180 Q 940 80, 670 80 Q 400 80, 400 180" fill="none" stroke="#B5392A" stroke-width="2" stroke-dasharray="6 6" marker-end="url(#arrow-r)"/>
|
||||
<rect x="580" y="58" width="180" height="30" rx="6" fill="#F0EAE0" stroke="#B5392A" stroke-width="1"/>
|
||||
<text x="670" y="78" text-anchor="middle" fill="#B5392A" font-size="12">FEEDBACK · wrong / miss</text>
|
||||
|
||||
<!-- bottom feedback to sources -->
|
||||
<path d="M 940 300 Q 940 420, 670 420 Q 140 420, 140 300" fill="none" stroke="#B5392A" stroke-width="2" stroke-dasharray="6 6" marker-end="url(#arrow-r)"/>
|
||||
<rect x="560" y="400" width="220" height="30" rx="6" fill="#F0EAE0" stroke="#B5392A" stroke-width="1"/>
|
||||
<text x="670" y="420" text-anchor="middle" fill="#B5392A" font-size="12">NIGHTLY · suggest new sources</text>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="kb-footer"><span>Diagram · Feedback Loop</span><span>05 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. STATS -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">After 6 Months</div>
|
||||
<h1 class="kb-h1" style="font-size:44px">升级版 <span class="rust">跑了半年</span> 的数据</h1>
|
||||
<div style="display:grid;grid-template-columns:1.3fr 1fr 1fr;gap:24px;margin-top:28px;align-items:center">
|
||||
<div style="text-align:center"><div class="kb-big-num">13</div><p style="font-size:14px;color:#666;margin-top:6px;letter-spacing:.1em;text-transform:uppercase">关键优化项 · 全部落地</p></div>
|
||||
<div class="kb-card"><h4 style="color:var(--kb-rust)">-62%</h4><p>幻觉率(相比无反馈回路版本)</p></div>
|
||||
<div class="kb-card"><h4 style="color:var(--kb-rust)">+4.1×</h4><p>单次检索命中率</p></div>
|
||||
</div>
|
||||
<div class="kb-grid-2" style="margin-top:18px">
|
||||
<div class="kb-card"><h4>自动修订 227 条</h4><p>其中 189 条被人工批准合并,38 条被拒绝(数据已归档)。</p></div>
|
||||
<div class="kb-card"><h4>新增笔记 412 篇</h4><p>从 miss 日志聚类而来,每篇都有来源追溯。</p></div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>Content · Stats</span><span>06 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div class="kb-kicker">Next Step</div>
|
||||
<h1 class="kb-h1" style="font-size:60px">开始你的 <span class="rust">Wiki v2</span></h1>
|
||||
<p class="kb-sub">不用重写所有笔记。先接一条回路,让 AI 的每次使用都在「改好」你的 vault。</p>
|
||||
<div class="kb-pipeline">
|
||||
<div class="kb-step"><div class="kb-step-num">TONIGHT</div><div class="kb-step-title">装 Skill</div><div class="kb-step-body">pnpm i -g @lewis/wiki-feedback</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">DAY 2</div><div class="kb-step-title">跑 7 天</div><div class="kb-step-body">观察 miss log 自动累积</div></div>
|
||||
<div class="kb-step hero"><div class="kb-step-num">DAY 8 · CORE</div><div class="kb-step-title">第一次审 PR</div><div class="kb-step-body">花 15 分钟 review 自动生成的修订</div></div>
|
||||
<div class="kb-step"><div class="kb-step-num">MONTH 1</div><div class="kb-step-title">开始信它</div><div class="kb-step-body">你的 vault 会变成活的</div></div>
|
||||
</div>
|
||||
<div class="kb-footer"><span>CTA</span><span>07 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="kb-grid-bg"></div>
|
||||
<div style="margin:auto 0;text-align:center">
|
||||
<div class="kb-kicker">END · blueprint v2</div>
|
||||
<h1 class="kb-h1" style="font-size:140px;margin-top:24px">谢谢 <span class="rust">·</span> thanks</h1>
|
||||
<p class="kb-sub" style="margin:0 auto;font-size:22px">图纸、Skill、笔记模板都在 <b>github.com/lewis/karpathy-wiki-v2</b></p>
|
||||
</div>
|
||||
<div class="kb-footer"><span>End of deck</span><span>08 / 08</span></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-obsidian-claude-gradient
|
||||
description: GitHub 暗紫渐变 deck — GitHub-dark #0d1117 + 紫蓝 radial 环境光 + 60px 网格 mask、居中布局、紫色 pill 标签、三色渐变标题(#a855f7→#60a5fa→#34d399)、GitHub 风代码 palette、紫色左边框高亮块。适合开发者工作流 / MCP / Agent / dev tool 教程,类似 GitHub Blog / Linear Changelog。
|
||||
triggers:
|
||||
- "github dark"
|
||||
- "developer tutorial"
|
||||
- "mcp tutorial"
|
||||
- "agent tutorial"
|
||||
- "dev workflow"
|
||||
- "changelog deck"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: engineering
|
||||
featured: 31
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-obsidian-claude-gradient 模板做一份开发者教程 PPT。GitHub 暗紫渐变 + 居中布局 + 紫色 pill + 三色渐变标题 + 配置/步骤代码块。先确认:教什么、目标受众、要不要 MCP/Agent 配置示例。"
|
||||
---
|
||||
# HTML PPT · GitHub 暗紫渐变
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`obsidian-claude-gradient`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `obsidian-claude-gradient` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/obsidian-claude-gradient/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-obsidian-claude-gradient` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-obsidian-claude-gradient` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,371 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Obsidian × Claude Gradient</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* obsidian-claude-gradient — 紫色暗底 + GitHub-ish 渐变卡 */
|
||||
.tpl-obsidian-claude-gradient{
|
||||
--oc-bg:#0d1117;
|
||||
--oc-surface:#161b22;
|
||||
--oc-surface2:#21262d;
|
||||
--oc-border:#30363d;
|
||||
--oc-accent:#7c3aed;
|
||||
--oc-accent2:#a855f7;
|
||||
--oc-accent3:#c084fc;
|
||||
--oc-green:#3fb950;
|
||||
--oc-blue:#58a6ff;
|
||||
--oc-orange:#f97316;
|
||||
--oc-yellow:#fbbf24;
|
||||
--oc-red:#f87171;
|
||||
--oc-text:#e6edf3;
|
||||
--oc-dim:#8b949e;
|
||||
--oc-dimmer:#484f58;
|
||||
background:var(--oc-bg);
|
||||
color:var(--oc-text);
|
||||
font-family:'Inter','Noto Sans SC','PingFang SC',-apple-system,sans-serif;
|
||||
}
|
||||
.tpl-obsidian-claude-gradient .slide{background:var(--oc-bg);color:var(--oc-text);padding:64px 88px;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;overflow:hidden}
|
||||
.tpl-obsidian-claude-gradient .oc-cbg{position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse at 28% 38%,rgba(124,58,237,.25) 0%,transparent 60%),radial-gradient(ellipse at 72% 62%,rgba(88,166,255,.18) 0%,transparent 60%)}
|
||||
.tpl-obsidian-claude-gradient .oc-cgrid{position:absolute;inset:0;pointer-events:none;background-image:linear-gradient(rgba(48,54,61,.4) 1px,transparent 1px),linear-gradient(90deg,rgba(48,54,61,.4) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse at center,black 35%,transparent 80%)}
|
||||
.tpl-obsidian-claude-gradient .slide > *{position:relative;z-index:2}
|
||||
.tpl-obsidian-claude-gradient .oc-snum{position:absolute;top:24px;right:36px;color:var(--oc-dimmer);font-size:12px;letter-spacing:.1em;z-index:3}
|
||||
.tpl-obsidian-claude-gradient .oc-tag{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--oc-accent3);background:rgba(124,58,237,.14);border:1px solid rgba(168,85,247,.3);padding:5px 16px;border-radius:999px;margin-bottom:22px}
|
||||
.tpl-obsidian-claude-gradient .oc-h1{font-size:72px;font-weight:800;line-height:1.08;letter-spacing:-.02em;margin:0 0 10px;color:var(--oc-text)}
|
||||
.tpl-obsidian-claude-gradient .oc-h2{font-size:44px;font-weight:700;line-height:1.18;letter-spacing:-.015em;margin:0 0 14px}
|
||||
.tpl-obsidian-claude-gradient .oc-sub{font-size:19px;color:var(--oc-dim);line-height:1.65;max-width:720px;margin-top:14px}
|
||||
.tpl-obsidian-claude-gradient .oc-g{background:linear-gradient(135deg,#a855f7 0%,#60a5fa 55%,#34d399 100%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.tpl-obsidian-claude-gradient .oc-card{background:var(--oc-surface);border:1px solid var(--oc-border);border-radius:14px;padding:22px 26px;text-align:left;position:relative;overflow:hidden}
|
||||
.tpl-obsidian-claude-gradient .oc-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(168,85,247,.4),transparent)}
|
||||
.tpl-obsidian-claude-gradient .oc-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:18px;width:100%;max-width:1000px;margin-top:24px}
|
||||
.tpl-obsidian-claude-gradient .oc-grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;width:100%;max-width:1080px;margin-top:24px}
|
||||
.tpl-obsidian-claude-gradient .oc-badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 11px;border-radius:999px;margin-bottom:10px}
|
||||
.tpl-obsidian-claude-gradient .oc-bp{background:rgba(168,85,247,.15);color:var(--oc-accent3)}
|
||||
.tpl-obsidian-claude-gradient .oc-bb{background:rgba(88,166,255,.15);color:var(--oc-blue)}
|
||||
.tpl-obsidian-claude-gradient .oc-bg{background:rgba(63,185,80,.15);color:var(--oc-green)}
|
||||
.tpl-obsidian-claude-gradient .oc-bo{background:rgba(249,115,22,.15);color:var(--oc-orange)}
|
||||
.tpl-obsidian-claude-gradient .oc-code{background:#010409;border:1px solid var(--oc-border);border-radius:12px;padding:20px 24px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.85;width:100%;max-width:860px;text-align:left;color:#e6edf3}
|
||||
.tpl-obsidian-claude-gradient .oc-code .cp{color:var(--oc-green)}
|
||||
.tpl-obsidian-claude-gradient .oc-code .cc{color:var(--oc-blue)}
|
||||
.tpl-obsidian-claude-gradient .oc-code .ca{color:var(--oc-accent3)}
|
||||
.tpl-obsidian-claude-gradient .oc-code .cm{color:var(--oc-dimmer)}
|
||||
.tpl-obsidian-claude-gradient .oc-code .cs{color:var(--oc-orange)}
|
||||
.tpl-obsidian-claude-gradient .oc-hl{background:rgba(124,58,237,.1);border:1px solid rgba(168,85,247,.3);border-left:4px solid var(--oc-accent2);border-radius:0 12px 12px 0;padding:16px 22px;font-size:16px;line-height:1.7;max-width:860px;text-align:left}
|
||||
.tpl-obsidian-claude-gradient .oc-steps{display:flex;flex-direction:column;gap:0;width:100%;max-width:820px;text-align:left}
|
||||
.tpl-obsidian-claude-gradient .oc-step{display:flex;gap:20px;align-items:flex-start;padding:18px 0;border-bottom:1px solid var(--oc-border)}
|
||||
.tpl-obsidian-claude-gradient .oc-step:last-child{border-bottom:none}
|
||||
.tpl-obsidian-claude-gradient .oc-sn{width:36px;height:36px;flex-shrink:0;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px;background:linear-gradient(135deg,var(--oc-accent),var(--oc-blue));color:#fff}
|
||||
.tpl-obsidian-claude-gradient .oc-sc h4{font-size:17px;font-weight:600;margin-bottom:6px;color:var(--oc-text)}
|
||||
.tpl-obsidian-claude-gradient .oc-sc p{font-size:14px;color:var(--oc-dim);line-height:1.6}
|
||||
.tpl-obsidian-claude-gradient .oc-pill{display:inline-flex;align-items:center;gap:8px;background:var(--oc-surface2);border:1px solid var(--oc-border);border-radius:999px;padding:7px 18px;font-size:14px;font-weight:500;color:var(--oc-text);margin:4px 6px 4px 0}
|
||||
.tpl-obsidian-claude-gradient .oc-quote{max-width:800px}
|
||||
.tpl-obsidian-claude-gradient .oc-quote blockquote{font-size:26px;font-weight:500;line-height:1.6;position:relative;padding:0 36px;margin:0;color:var(--oc-text)}
|
||||
.tpl-obsidian-claude-gradient .oc-quote blockquote::before{content:'"';position:absolute;left:-6px;top:-22px;font-size:78px;color:var(--oc-accent);opacity:.4;font-family:Georgia,serif;line-height:1}
|
||||
.tpl-obsidian-claude-gradient .oc-quote .attr{margin-top:20px;font-size:13px;color:var(--oc-dim)}
|
||||
.tpl-obsidian-claude-gradient .oc-big{font-size:140px;font-weight:900;line-height:.95;letter-spacing:-.04em}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-obsidian-claude-gradient">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">01 / 08</div>
|
||||
<div class="oc-tag">● OBSIDIAN × CLAUDE · 第二大脑</div>
|
||||
<h1 class="oc-h1">把 Obsidian 和 Claude<br>拧成 <span class="oc-g">一条神经</span></h1>
|
||||
<p class="oc-sub">不是又一个 AI 笔记插件 —— 是让 Claude 真正理解你 vault 的结构、链接、双向引用,<br>然后在你想写东西之前就把资料准备好。</p>
|
||||
<div style="margin-top:32px">
|
||||
<span class="oc-pill">🧠 Markdown-native</span>
|
||||
<span class="oc-pill">⚡ MCP-ready</span>
|
||||
<span class="oc-pill">🔗 双链理解</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION -->
|
||||
<section class="slide">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">02 / 08</div>
|
||||
<div class="oc-tag">● CHAPTER 01</div>
|
||||
<h1 class="oc-h1" style="font-size:110px">Why <span class="oc-g">not</span> Notion?</h1>
|
||||
<p class="oc-sub">当你的知识多到会互相引用时,<br>「文件夹」就不够了,「数据库」也不是答案。</p>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT — compare cards -->
|
||||
<section class="slide">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">03 / 08</div>
|
||||
<div class="oc-tag">● COMPARE</div>
|
||||
<h2 class="oc-h2">Notion vs <span class="oc-g">Obsidian</span> · 对 AI 友好度</h2>
|
||||
<div class="oc-grid-2">
|
||||
<div class="oc-card">
|
||||
<span class="oc-badge oc-bb">NOTION</span>
|
||||
<h4 style="font-size:20px;margin-bottom:10px">数据库原生</h4>
|
||||
<p style="color:var(--oc-dim);font-size:14px;line-height:1.65">适合结构化任务、团队协作,但是——<br>• AI 要走 API,拿不到实时全文<br>• 嵌套块结构复杂,token 成本高<br>• 本地化差,没法当长期记忆</p>
|
||||
</div>
|
||||
<div class="oc-card" style="border-color:rgba(168,85,247,.35);background:rgba(124,58,237,.05)">
|
||||
<span class="oc-badge oc-bp">OBSIDIAN</span>
|
||||
<h4 style="font-size:20px;margin-bottom:10px">纯 Markdown + 双链</h4>
|
||||
<p style="color:var(--oc-dim);font-size:14px;line-height:1.65">对 AI 天生友好 ——<br>• 所有东西就是文件,Claude 直接读<br>• 双链 = 天然 graph,抽实体几乎零成本<br>• 离线、可 git、可 diff、可回滚</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oc-hl" style="margin-top:26px">💡 <b>关键洞察:</b>AI 不需要「更聪明的数据库」,它需要「能被它自己读懂的文件系统」。</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. STEPS -->
|
||||
<section class="slide">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">04 / 08</div>
|
||||
<div class="oc-tag">● SETUP · 4 STEPS</div>
|
||||
<h2 class="oc-h2">从 0 到第一次「AI 写笔记」</h2>
|
||||
<div class="oc-steps">
|
||||
<div class="oc-step"><div class="oc-sn">1</div><div class="oc-sc"><h4>装 Obsidian + 开 Local REST API 插件</h4><p>社区插件,一个勾就开。它让外部进程能 read/write 你的 vault。</p></div></div>
|
||||
<div class="oc-step"><div class="oc-sn">2</div><div class="oc-sc"><h4>接 Claude Desktop + obsidian-mcp server</h4><p>MCP 一个配置文件就能接,token 填 vault 的 api key。</p></div></div>
|
||||
<div class="oc-step"><div class="oc-sn">3</div><div class="oc-sc"><h4>装 5 个 obsidian-skills</h4><p>markdown / bases / canvas / cli / defuddle —— 让 Claude 知道怎么正确使用 Obsidian。</p></div></div>
|
||||
<div class="oc-step"><div class="oc-sn">4</div><div class="oc-sc"><h4>让 Claude 自己整理一次</h4><p>「帮我把最近 10 篇笔记里的重复概念合并,生成一张新的 MOC」—— 90 秒出结果。</p></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CODE -->
|
||||
<section class="slide">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">05 / 08</div>
|
||||
<div class="oc-tag">● MCP CONFIG</div>
|
||||
<h2 class="oc-h2">claude_desktop_config.json</h2>
|
||||
<pre class="oc-code"><span class="cm">// ~/Library/Application Support/Claude/claude_desktop_config.json</span>
|
||||
{
|
||||
<span class="cc">"mcpServers"</span>: {
|
||||
<span class="cc">"obsidian"</span>: {
|
||||
<span class="cc">"command"</span>: <span class="cs">"npx"</span>,
|
||||
<span class="cc">"args"</span>: [<span class="cs">"-y"</span>, <span class="cs">"@modelcontextprotocol/server-obsidian"</span>],
|
||||
<span class="cc">"env"</span>: {
|
||||
<span class="cc">"OBSIDIAN_API_KEY"</span>: <span class="cs">"xxxxxxxxxxxxxxxx"</span>,
|
||||
<span class="cc">"OBSIDIAN_HOST"</span>: <span class="cs">"http://127.0.0.1:27123"</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
}</pre>
|
||||
<p class="oc-sub" style="margin-top:18px">重启 Claude Desktop,输入 <b style="color:var(--oc-accent3)">/mcp</b>,你会看到 obsidian 已连。</p>
|
||||
</section>
|
||||
|
||||
<!-- 6. STATS -->
|
||||
<section class="slide">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">06 / 08</div>
|
||||
<div class="oc-tag">● 3 MONTHS IN</div>
|
||||
<h2 class="oc-h2">跑了 90 天,我的 <span class="oc-g">vault 数据</span></h2>
|
||||
<div class="oc-grid-3" style="margin-top:28px">
|
||||
<div class="oc-card" style="text-align:center"><div class="oc-big oc-g" style="font-size:80px">1,842</div><p style="color:var(--oc-dim);margin-top:8px;font-size:13px">notes in vault</p></div>
|
||||
<div class="oc-card" style="text-align:center"><div class="oc-big oc-g" style="font-size:80px">6.3k</div><p style="color:var(--oc-dim);margin-top:8px;font-size:13px">backlinks (由 AI 自动补)</p></div>
|
||||
<div class="oc-card" style="text-align:center"><div class="oc-big oc-g" style="font-size:80px">-74%</div><p style="color:var(--oc-dim);margin-top:8px;font-size:13px">找资料平均耗时</p></div>
|
||||
</div>
|
||||
<div class="oc-hl" style="margin-top:26px">最大收益不是「AI 帮我写」,而是「AI 帮我把旧笔记重新连起来」—— 每周 30 分钟,vault 就会主动生长。</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. QUOTE / CTA -->
|
||||
<section class="slide">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">07 / 08</div>
|
||||
<div class="oc-tag">● CTA · 今晚可以做</div>
|
||||
<div class="oc-quote">
|
||||
<blockquote>不要再找「AI 笔记应用」了。<br>你要的是一个 <span class="oc-g">文件夹 + 一条神经</span>。</blockquote>
|
||||
<div class="attr">— 我自己,用了 90 天后</div>
|
||||
</div>
|
||||
<div style="margin-top:36px">
|
||||
<span class="oc-pill">⬇ obsidian.md</span>
|
||||
<span class="oc-pill">⬇ Claude Desktop</span>
|
||||
<span class="oc-pill">⬇ obsidian-mcp</span>
|
||||
<span class="oc-pill">⬇ obsidian-skills × 5</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="oc-cbg"></div>
|
||||
<div class="oc-cgrid"></div>
|
||||
<div class="oc-snum">08 / 08</div>
|
||||
<div class="oc-big oc-g">Thanks.</div>
|
||||
<p class="oc-sub" style="margin-top:26px">配置模板、skill manifest、我的 vault 结构图都在 <b style="color:var(--oc-accent3)">github.com/lewis/obsidian-claude</b></p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: html-ppt-pitch-deck
|
||||
description: Investor-ready 10-slide HTML pitch deck — white + blue→purple gradient hero, big numbers, traction bar chart, $4.5M-style ask page. Use when the user wants a fundraising deck, seed-round pitch, or VC meeting slides.
|
||||
triggers:
|
||||
- "pitch deck"
|
||||
- "pitch"
|
||||
- "fundraising"
|
||||
- "seed round"
|
||||
- "investor deck"
|
||||
- "vc deck"
|
||||
- "pitch slides"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: finance
|
||||
featured: 20
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "Build a 10-slide pitch deck in HTML for my seed round. Use the html-ppt-pitch-deck full-deck template (white + blue→purple gradient, traction bars, $X.XM ask). Confirm three things first: (1) name + one-line pitch, (2) key traction numbers, (3) ask + use of funds."
|
||||
---
|
||||
# HTML PPT · Pitch Deck
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`pitch-deck`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `pitch-deck` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/pitch-deck/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-pitch-deck` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-pitch-deck` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,495 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Lumen · Pitch Deck</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* pitch-deck — classic YC/VC pitch */
|
||||
.tpl-pitch-deck{
|
||||
--bg:#ffffff;--bg-soft:#f6f7fb;--surface:#ffffff;--surface-2:#f2f4fa;
|
||||
--border:rgba(20,25,60,.08);--border-strong:rgba(20,25,60,.18);
|
||||
--text-1:#0d1130;--text-2:#4a5070;--text-3:#8a90ad;
|
||||
--accent:#3b5bff;--accent-2:#7a46ff;--accent-3:#d94cff;
|
||||
--grad:linear-gradient(135deg,#3b5bff 0%,#7a46ff 55%,#d94cff 100%);
|
||||
--grad-soft:linear-gradient(135deg,#eef1ff,#f4edff 55%,#fbedff);
|
||||
--radius:20px;--radius-lg:28px;
|
||||
--shadow:0 14px 40px rgba(20,25,60,.08),0 2px 8px rgba(20,25,60,.04);
|
||||
font-family:'Inter','Noto Sans SC',sans-serif;
|
||||
}
|
||||
.tpl-pitch-deck .slide{padding:88px 112px}
|
||||
.tpl-pitch-deck .kicker{color:var(--accent);font-weight:700}
|
||||
.tpl-pitch-deck .h1{font-size:86px;line-height:1.02;font-weight:900;letter-spacing:-.035em}
|
||||
.tpl-pitch-deck .h2{font-size:62px;font-weight:800;letter-spacing:-.03em}
|
||||
.tpl-pitch-deck .mega{font-size:180px;font-weight:900;line-height:.95;letter-spacing:-.05em;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
.tpl-pitch-deck .mega-sub{font-size:28px;color:var(--text-2);margin-top:18px}
|
||||
.tpl-pitch-deck .cover-bg{position:absolute;inset:0;background:var(--grad-soft);z-index:-1}
|
||||
.tpl-pitch-deck .cover-blob{position:absolute;right:-140px;top:-140px;width:560px;height:560px;border-radius:50%;background:var(--grad);filter:blur(8px);opacity:.35;z-index:-1}
|
||||
.tpl-pitch-deck .brand-dot{display:inline-block;width:14px;height:14px;border-radius:50%;background:var(--grad);margin-right:10px;vertical-align:middle}
|
||||
.tpl-pitch-deck .brand{font-weight:800;font-size:22px;letter-spacing:-.02em}
|
||||
.tpl-pitch-deck .card{border-radius:var(--radius)}
|
||||
.tpl-pitch-deck .num-tag{font-family:'Inter',sans-serif;font-size:14px;font-weight:700;color:var(--accent);letter-spacing:.12em}
|
||||
.tpl-pitch-deck .big-q{font-family:'Playfair Display',serif;font-size:56px;line-height:1.15;font-weight:700;letter-spacing:-.02em;max-width:22ch}
|
||||
.tpl-pitch-deck .metric{display:flex;flex-direction:column;gap:6px}
|
||||
.tpl-pitch-deck .metric .n{font-size:72px;font-weight:900;letter-spacing:-.035em;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent;line-height:1}
|
||||
.tpl-pitch-deck .metric .l{color:var(--text-2);font-size:16px}
|
||||
.tpl-pitch-deck .team-card{text-align:center;padding:32px 20px}
|
||||
.tpl-pitch-deck .avatar{width:96px;height:96px;border-radius:50%;margin:0 auto 14px;background:var(--grad);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:32px}
|
||||
.tpl-pitch-deck .ask-box{background:var(--grad);color:#fff;padding:56px 64px;border-radius:var(--radius-lg);box-shadow:0 30px 70px rgba(59,91,255,.35)}
|
||||
.tpl-pitch-deck .ask-box .h2{color:#fff}
|
||||
.tpl-pitch-deck .ask-box .dim{color:rgba(255,255,255,.85)}
|
||||
.tpl-pitch-deck .traction-bar{display:flex;align-items:flex-end;gap:14px;height:240px;margin-top:24px}
|
||||
.tpl-pitch-deck .traction-bar .bar{flex:1;background:var(--grad);border-radius:8px 8px 0 0;position:relative;min-height:20px}
|
||||
.tpl-pitch-deck .traction-bar .bar span{position:absolute;bottom:-28px;left:0;right:0;text-align:center;font-size:13px;color:var(--text-3)}
|
||||
.tpl-pitch-deck .traction-bar .bar em{position:absolute;top:-28px;left:0;right:0;text-align:center;font-size:14px;font-weight:700;font-style:normal;color:var(--text-1)}
|
||||
.tpl-pitch-deck .section-num{font-size:220px;font-weight:900;line-height:.9;color:var(--surface-2);position:absolute;right:72px;bottom:40px;z-index:0;letter-spacing:-.05em}
|
||||
.tpl-pitch-deck .slide > *{position:relative;z-index:1}
|
||||
.tpl-pitch-deck .deck-footer{color:var(--text-3)}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-pitch-deck">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover -->
|
||||
<section class="slide" data-title="Cover">
|
||||
<div class="cover-bg"></div>
|
||||
<div class="cover-blob"></div>
|
||||
<div style="position:absolute;top:56px;left:112px"><span class="brand-dot"></span><span class="brand">Lumen</span></div>
|
||||
<p class="kicker">Seed round · 2026</p>
|
||||
<h1 class="h1 anim-fade-up" data-anim="fade-up">The operating system<br>for <span class="gradient-text">solo founders</span>.</h1>
|
||||
<p class="lede mt-m">One workspace for billing, CRM, contracts and taxes — built for the 70M people running a business of one.</p>
|
||||
<div class="deck-footer"><span>Maya Chen · CEO</span><span class="slide-number" data-current="1" data-total="10"></span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Problem -->
|
||||
<section class="slide" data-title="Problem">
|
||||
<span class="section-num">01</span>
|
||||
<p class="num-tag">PROBLEM</p>
|
||||
<h2 class="h2 mt-s">Solo founders duct-tape<br>7+ tools to stay alive.</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="card"><h4>Fragmentation</h4><p class="dim">Stripe, QuickBooks, HubSpot, DocuSign, Notion, Gusto, a spreadsheet. Nothing talks.</p></div>
|
||||
<div class="card"><h4>$480/mo wasted</h4><p class="dim">Average solo founder pays for 9 SaaS seats they only half-use.</p></div>
|
||||
<div class="card"><h4>14 hrs / week lost</h4><p class="dim">Copy-pasting between tools instead of selling.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Solution -->
|
||||
<section class="slide" data-title="Solution">
|
||||
<span class="section-num">02</span>
|
||||
<p class="num-tag">SOLUTION</p>
|
||||
<h2 class="h2 mt-s">Lumen is <span class="gradient-text">one spine</span><br>for the business of one.</h2>
|
||||
<p class="lede mt-m">Invoice a client → the payment lands → the tax is reserved → the contract is filed → your dashboard updates. In one app. Without plumbing.</p>
|
||||
<div class="row mt-l">
|
||||
<span class="pill pill-accent">Billing</span>
|
||||
<span class="pill pill-accent">CRM</span>
|
||||
<span class="pill pill-accent">Contracts</span>
|
||||
<span class="pill pill-accent">Taxes</span>
|
||||
<span class="pill pill-accent">Banking</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Product -->
|
||||
<section class="slide" data-title="Product">
|
||||
<span class="section-num">03</span>
|
||||
<p class="num-tag">PRODUCT</p>
|
||||
<h2 class="h2 mt-s">Built around "jobs to be done".</h2>
|
||||
<div class="grid g2 mt-l">
|
||||
<div class="card card-hover"><h4>Get paid</h4><p class="dim">Invoices, subscriptions and Stripe/Wise payouts with a single click. ACH, card, wire, crypto.</p></div>
|
||||
<div class="card card-hover"><h4>Stay legal</h4><p class="dim">E-sign contracts from templates. Auto-file 1099s and quarterly estimates.</p></div>
|
||||
<div class="card card-hover"><h4>Sell smarter</h4><p class="dim">Lead inbox, pipeline, email sequences. No separate CRM.</p></div>
|
||||
<div class="card card-hover"><h4>See the business</h4><p class="dim">Live P&L, runway, top customers, churn. One dashboard, zero spreadsheets.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Market -->
|
||||
<section class="slide" data-title="Market">
|
||||
<span class="section-num">04</span>
|
||||
<p class="num-tag">MARKET</p>
|
||||
<h2 class="h2 mt-s">A very big small business.</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="metric"><div class="n">73M</div><div class="l">solo businesses in the US + EU</div></div>
|
||||
<div class="metric"><div class="n">$186B</div><div class="l">TAM · horizontal SaaS spend</div></div>
|
||||
<div class="metric"><div class="n">9.4%</div><div class="l">CAGR through 2030</div></div>
|
||||
</div>
|
||||
<p class="lede mt-l">Creators, consultants, indie devs, coaches, freelancers — the fastest-growing segment of the workforce, and the most under-served by tooling.</p>
|
||||
</section>
|
||||
|
||||
<!-- 6. Business model -->
|
||||
<section class="slide" data-title="Business Model">
|
||||
<span class="section-num">05</span>
|
||||
<p class="num-tag">BUSINESS MODEL</p>
|
||||
<h2 class="h2 mt-s">Flat SaaS + payment rake.</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="card"><h4>Starter</h4><div class="metric mt-s"><div class="n" style="font-size:56px">$29</div><div class="l">/ month · core billing + CRM</div></div></div>
|
||||
<div class="card card-accent"><h4>Pro</h4><div class="metric mt-s"><div class="n" style="font-size:56px">$79</div><div class="l">/ month · contracts, taxes, banking</div></div></div>
|
||||
<div class="card"><h4>+ Payments</h4><div class="metric mt-s"><div class="n" style="font-size:56px">0.4%</div><div class="l">interchange rake on processed volume</div></div></div>
|
||||
</div>
|
||||
<p class="dim mt-l">Blended LTV $1,920 · CAC payback 5 months at current funnel.</p>
|
||||
</section>
|
||||
|
||||
<!-- 7. Traction -->
|
||||
<section class="slide" data-title="Traction">
|
||||
<span class="section-num">06</span>
|
||||
<p class="num-tag">TRACTION</p>
|
||||
<h2 class="h2 mt-s">6 months, growing 38% MoM.</h2>
|
||||
<div class="traction-bar mt-l">
|
||||
<div class="bar" style="height:18%"><em>$6k</em><span>Oct</span></div>
|
||||
<div class="bar" style="height:30%"><em>$11k</em><span>Nov</span></div>
|
||||
<div class="bar" style="height:44%"><em>$17k</em><span>Dec</span></div>
|
||||
<div class="bar" style="height:62%"><em>$26k</em><span>Jan</span></div>
|
||||
<div class="bar" style="height:82%"><em>$38k</em><span>Feb</span></div>
|
||||
<div class="bar" style="height:100%"><em>$54k</em><span>Mar</span></div>
|
||||
</div>
|
||||
<p class="dim mt-l" style="margin-top:48px">2,140 paying customers · NPS 72 · Net retention 118%</p>
|
||||
</section>
|
||||
|
||||
<!-- 8. Team -->
|
||||
<section class="slide" data-title="Team">
|
||||
<span class="section-num">07</span>
|
||||
<p class="num-tag">TEAM</p>
|
||||
<h2 class="h2 mt-s">Shipped at scale before.</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="card team-card"><div class="avatar">MC</div><h4>Maya Chen</h4><p class="dim">CEO · ex-Stripe billing lead. 8 yrs in payments.</p></div>
|
||||
<div class="card team-card"><div class="avatar">RP</div><h4>Raj Patel</h4><p class="dim">CTO · ex-Linear. Built multiplayer sync at 10M users.</p></div>
|
||||
<div class="card team-card"><div class="avatar">EK</div><h4>Elena Kim</h4><p class="dim">Head of Design · ex-Notion. Shipped the mobile relaunch.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 9. Ask -->
|
||||
<section class="slide" data-title="The Ask">
|
||||
<p class="num-tag">THE ASK</p>
|
||||
<div class="ask-box mt-m">
|
||||
<h2 class="h2">Raising $4.5M seed.</h2>
|
||||
<p class="lede" style="color:rgba(255,255,255,.9);max-width:60ch">18 months of runway to reach $3M ARR. 40% engineering, 35% growth, 15% compliance/banking licenses, 10% runway buffer.</p>
|
||||
<div class="row mt-l" style="gap:40px">
|
||||
<div><div style="font-size:44px;font-weight:900">$4.5M</div><div class="dim">SAFE · post-money cap $28M</div></div>
|
||||
<div><div style="font-size:44px;font-weight:900">18 mo</div><div class="dim">runway to Series A</div></div>
|
||||
<div><div style="font-size:44px;font-weight:900">$3M</div><div class="dim">ARR target by close</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 10. Thanks -->
|
||||
<section class="slide center tc" data-title="Thanks">
|
||||
<div class="cover-bg"></div>
|
||||
<div>
|
||||
<div class="mega">Thanks.</div>
|
||||
<p class="mega-sub">maya@lumen.app · lumen.app/investors</p>
|
||||
<div class="row mt-l" style="justify-content:center;gap:24px">
|
||||
<span class="pill pill-accent">Let's talk</span>
|
||||
<span class="pill">Deck v4.2 · Apr 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: html-ppt-presenter-mode
|
||||
description: 演讲者模式专用 deck — tokyo-night 默认主题,5 套主题 T 键切换,每页带 150-300 字逐字稿示例(<aside class="notes">),按 S 打开 popup(CURRENT / NEXT / SCRIPT / TIMER 四张磁吸卡片)。用于技术分享、公开演讲、课程讲解,怕忘词或要提词器的场景。
|
||||
triggers:
|
||||
- "presenter mode"
|
||||
- "演讲者模式"
|
||||
- "逐字稿"
|
||||
- "speaker notes"
|
||||
- "提词器"
|
||||
- "presenter view"
|
||||
- "演讲"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: engineering
|
||||
featured: 26
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-presenter-mode 模板做一份带逐字稿的演讲 PPT。先确认:演讲主题、时长(每页 2-3 分钟)、目标听众。然后帮我每页写 150-300 字的口语化逐字稿(不是讲稿,是提示信号),按 S 能打开 presenter 弹窗。"
|
||||
---
|
||||
# HTML PPT · Presenter Mode (演讲者模式)
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`presenter-mode-reveal`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `presenter-mode-reveal` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/presenter-mode-reveal/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-presenter-mode-reveal` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-presenter-mode-reveal` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,725 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-themes="tokyo-night,dracula,catppuccin-mocha,nord,corporate-clean">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>演讲者模式示例 · Presenter Mode Deck</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style data-theme="tokyo-night">/* theme: tokyo-night */
|
||||
:root{
|
||||
--bg:#1a1b26;--bg-soft:#16161e;--surface:#24283b;--surface-2:#2f334d;
|
||||
--border:rgba(192,202,245,.12);--border-strong:rgba(192,202,245,.24);
|
||||
--text-1:#c0caf5;--text-2:#a9b1d6;--text-3:#565f89;
|
||||
--accent:#7aa2f7;--accent-2:#bb9af7;--accent-3:#7dcfff;
|
||||
--good:#9ece6a;--warn:#e0af68;--bad:#f7768e;
|
||||
--grad:linear-gradient(135deg,#7aa2f7,#bb9af7 55%,#f7768e);
|
||||
--grad-soft:linear-gradient(135deg,#24283b,#2f334d);
|
||||
--radius:12px;--radius-sm:8px;--radius-lg:20px;
|
||||
--shadow:0 10px 30px rgba(0,0,0,.45);
|
||||
--shadow-lg:0 24px 62px rgba(0,0,0,.6);
|
||||
--font-sans:'Inter','Noto Sans SC',sans-serif;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* tpl-presenter-mode-reveal · scoped styles
|
||||
* Presenter-mode demo deck. Inherits tokens from active theme.
|
||||
* Minimal overrides — focus is on content + notes structure.
|
||||
*/
|
||||
|
||||
.tpl-presenter-mode-reveal .slide {
|
||||
padding: 72px 96px;
|
||||
}
|
||||
|
||||
.tpl-presenter-mode-reveal .kicker {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 13px;
|
||||
color: var(--text-3);
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 18px 0;
|
||||
}
|
||||
|
||||
.tpl-presenter-mode-reveal .h1 {
|
||||
font-size: clamp(44px, 5.6vw, 76px);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.tpl-presenter-mode-reveal .h2 {
|
||||
font-size: clamp(32px, 3.6vw, 48px);
|
||||
line-height: 1.22;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 28px 0;
|
||||
}
|
||||
|
||||
.tpl-presenter-mode-reveal .lede {
|
||||
font-size: 20px;
|
||||
line-height: 1.55;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.tpl-presenter-mode-reveal .mono {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.9em;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: var(--accent, #58a6ff);
|
||||
}
|
||||
|
||||
.tpl-presenter-mode-reveal .accent {
|
||||
color: var(--accent, #f0883e);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tpl-presenter-mode-reveal .speaker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .speaker .av {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--accent, #58a6ff), #bc8cff);
|
||||
}
|
||||
.tpl-presenter-mode-reveal .speaker b {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .speaker span {
|
||||
font-size: 13px;
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
/* Agenda rows */
|
||||
.tpl-presenter-mode-reveal .agenda-row {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid var(--border, rgba(255,255,255,0.1));
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--surface, rgba(255,255,255,0.03));
|
||||
}
|
||||
.tpl-presenter-mode-reveal .agenda-row .num {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 14px;
|
||||
color: var(--accent, #58a6ff);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .agenda-row .t {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: var(--text-1);
|
||||
}
|
||||
.tpl-presenter-mode-reveal .agenda-row .d {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 12px;
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.tpl-presenter-mode-reveal .card {
|
||||
background: var(--surface, rgba(255,255,255,0.03));
|
||||
border: 1px solid var(--border, rgba(255,255,255,0.1));
|
||||
border-radius: 12px;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .card-accent {
|
||||
border-top: 3px solid var(--accent, #58a6ff);
|
||||
}
|
||||
.tpl-presenter-mode-reveal .card h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
color: var(--text-1);
|
||||
}
|
||||
.tpl-presenter-mode-reveal .card .dim {
|
||||
color: var(--text-2);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Feature rows (presenter view features) */
|
||||
.tpl-presenter-mode-reveal .feature-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
|
||||
}
|
||||
.tpl-presenter-mode-reveal .feature-row:last-child { border-bottom: none; }
|
||||
.tpl-presenter-mode-reveal .feature-row .num {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .feature-row b {
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-1);
|
||||
}
|
||||
.tpl-presenter-mode-reveal .feature-row .dim {
|
||||
font-size: 14px;
|
||||
color: var(--text-2);
|
||||
line-height: 1.55;
|
||||
margin: 0;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .blue { color: #58a6ff; }
|
||||
.tpl-presenter-mode-reveal .green { color: #3fb950; }
|
||||
.tpl-presenter-mode-reveal .orange { color: #f0883e; }
|
||||
.tpl-presenter-mode-reveal .purple { color: #bc8cff; }
|
||||
.tpl-presenter-mode-reveal .red { color: #f85149; }
|
||||
|
||||
/* Rule rows (3 铁律) */
|
||||
.tpl-presenter-mode-reveal .rule-row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
padding: 18px 22px;
|
||||
border: 1px solid var(--border, rgba(255,255,255,0.1));
|
||||
border-radius: 12px;
|
||||
margin-bottom: 14px;
|
||||
background: var(--surface, rgba(255,255,255,0.03));
|
||||
}
|
||||
.tpl-presenter-mode-reveal .rule-row .num {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-mono, monospace);
|
||||
line-height: 1;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .rule-row b {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-1);
|
||||
}
|
||||
.tpl-presenter-mode-reveal .rule-row .dim {
|
||||
font-size: 15px;
|
||||
color: var(--text-2);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
.tpl-presenter-mode-reveal .code-block {
|
||||
background: #0d1117;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px 26px;
|
||||
font-family: var(--font-mono, "SF Mono", monospace);
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: #e6edf3;
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
}
|
||||
.tpl-presenter-mode-reveal .code-block .comment { color: #8b949e; }
|
||||
.tpl-presenter-mode-reveal .code-block .cmd { color: #3fb950; font-weight: 600; }
|
||||
.tpl-presenter-mode-reveal .code-block .flag { color: #f0883e; }
|
||||
|
||||
/* Stack helper */
|
||||
.tpl-presenter-mode-reveal .stack > * + * { margin-top: 0; }
|
||||
|
||||
/* Grid helpers */
|
||||
.tpl-presenter-mode-reveal .grid { display: grid; gap: 20px; }
|
||||
.tpl-presenter-mode-reveal .grid.g2 { grid-template-columns: 1fr 1fr; }
|
||||
.tpl-presenter-mode-reveal .grid.g3 { grid-template-columns: repeat(3, 1fr); }
|
||||
|
||||
.tpl-presenter-mode-reveal .mt-m { margin-top: 20px; }
|
||||
.tpl-presenter-mode-reveal .mt-l { margin-top: 32px; }
|
||||
.tpl-presenter-mode-reveal .mt-s { margin-top: 10px; }
|
||||
.tpl-presenter-mode-reveal .tc { text-align: center; }
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-presenter-mode-reveal">
|
||||
<div class="deck">
|
||||
|
||||
<!-- ============ 1. COVER ============ -->
|
||||
<section class="slide" data-title="Cover">
|
||||
<p class="kicker">presenter-mode / demo</p>
|
||||
<h1 class="h1 anim-fade-up" data-anim="fade-up">如何做一场<br><span style="background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent">有逐字稿</span>的技术分享</h1>
|
||||
<p class="lede mt-m">按 <span class="mono">S</span> 进入演讲者视图 · <span class="mono">T</span> 切换主题 · <span class="mono">← →</span> 翻页</p>
|
||||
<div class="speaker">
|
||||
<div class="av"></div>
|
||||
<div><b>@lewis</b><span>sharing talk · 30 min</span></div>
|
||||
</div>
|
||||
<div class="deck-footer">
|
||||
<span class="mono">#presenter #逐字稿 #tech-talk</span>
|
||||
<span class="slide-number" data-current="1" data-total="6"></span>
|
||||
</div>
|
||||
<aside class="notes">
|
||||
<p>大家好,欢迎来到今天的技术分享。我是 lewis,今天想跟大家聊一个很多人忽略但其实非常影响演讲效果的话题——<strong>如何让一场技术分享既有深度,又讲得不卡壳</strong>。</p>
|
||||
<p>在正式开始之前,先跟大家介绍一下这份 deck 本身:这是一个支持<em>演讲者模式</em>的 HTML 幻灯片模板。现在你们看到的是观众视图,但我自己的屏幕上看到的是完全不一样的东西——当前页、下一页、完整逐字稿、计时器,全在一块屏幕上。</p>
|
||||
<p>为什么我要专门做这个?因为我发现自己做技术分享时最大的痛点不是 PPT 不够好看,而是<strong>讲到某一页突然不知道该说什么,或者忘了过渡怎么接</strong>。今天这份分享既是内容本身,也是个演示——我会一直开着演讲者模式讲,你们可以观察我讲得有多流畅。</p>
|
||||
<p>今天分享大概 30 分钟,分 5 个部分。有问题随时打断。Let's go.</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- ============ 2. AGENDA ============ -->
|
||||
<section class="slide" data-title="Agenda">
|
||||
<p class="kicker">agenda</p>
|
||||
<h2 class="h2">今天要讲的 5 件事</h2>
|
||||
<div class="stack mt-l">
|
||||
<div class="agenda-row"><span class="num">01</span><span class="t">为什么 PPT 本身做得好还不够</span><span class="d">~5min</span></div>
|
||||
<div class="agenda-row"><span class="num">02</span><span class="t">演讲者模式到底该有哪些信息</span><span class="d">~6min</span></div>
|
||||
<div class="agenda-row"><span class="num">03</span><span class="t">逐字稿怎么写才不像念稿</span><span class="d">~8min</span></div>
|
||||
<div class="agenda-row"><span class="num">04</span><span class="t">Live demo · html-ppt skill 怎么用</span><span class="d">~8min</span></div>
|
||||
<div class="agenda-row"><span class="num">05</span><span class="t">Takeaways + Q&A</span><span class="d">~3min</span></div>
|
||||
</div>
|
||||
<aside class="notes">
|
||||
<p>先过一下今天的议程。</p>
|
||||
<p>第一部分我想先说服你们<strong>"PPT 做得漂亮≠讲得好"</strong>。我见过太多很精致的 deck,但讲的人一上去就开始 "嗯…这个…就是…"。</p>
|
||||
<p>第二部分聊演讲者视图。业界的产品其实差别蛮大的,Keynote、PowerPoint、reveal.js 都有各自的方案,但真正好用的设计逻辑是什么,我会给出我的答案。</p>
|
||||
<p>第三部分是今天的<em>核心</em>——逐字稿。很多人以为逐字稿就是把要说的话一字不差写下来,错。逐字稿的目的是让你<strong>"看一眼就接得上"</strong>,写法完全不一样。</p>
|
||||
<p>第四部分会现场 demo 我自己用的 html-ppt skill,展示如何 30 分钟出一份带逐字稿的 deck。</p>
|
||||
<p>最后收尾 + 答疑。</p>
|
||||
<p>OK,进入第一部分。</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- ============ 3. PROBLEM ============ -->
|
||||
<section class="slide" data-title="Problem">
|
||||
<p class="kicker">// part 01 · problem</p>
|
||||
<h2 class="h2">做 PPT 和讲 PPT,<br>是<span class="accent">两件事</span>。</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="card card-accent">
|
||||
<h4>✅ PPT 做得好</h4>
|
||||
<p class="dim">主题统一、排版干净、图表清晰、动效克制。这些是"静态作品"的质量。</p>
|
||||
</div>
|
||||
<div class="card card-accent">
|
||||
<h4>❌ 讲得好</h4>
|
||||
<p class="dim">逻辑连贯、语速稳定、不 "嗯啊"、能接住问题、能当场调整节奏。</p>
|
||||
</div>
|
||||
<div class="card card-accent">
|
||||
<h4>💡 差别在哪</h4>
|
||||
<p class="dim">前者是<strong>纸上功夫</strong>,后者需要你<strong>"看一眼幻灯片就知道下句话说什么"</strong>。</p>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="notes">
|
||||
<p>我先抛一个可能有争议的观点——<strong>做 PPT 和讲 PPT 是两件完全不同的事</strong>。</p>
|
||||
<p>大家看左边这张卡片,"PPT 做得好" 意味着什么?主题统一、排版干净、图表清晰、动效克制——这些都是<em>静态作品</em>的质量标准,可以离线评判。</p>
|
||||
<p>但中间这张卡片就不一样了:"讲得好" 意味着逻辑连贯、语速稳定、不卡壳、能接住提问、能根据现场反应调整节奏——这些是<strong>临场能力</strong>,跟 PPT 好不好看基本没关系。</p>
|
||||
<p>最关键的是右边这句话——讲得好的人,本质上是"<strong>看一眼幻灯片就知道下句话说什么</strong>"。这个能力靠什么?不是背稿,也不是即兴发挥,而是靠<em>合理设计的提词器系统</em>。</p>
|
||||
<p>今天接下来 25 分钟,我就是围绕这个核心问题展开的。</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- ============ 4. SOLUTION ============ -->
|
||||
<section class="slide" data-title="Presenter View">
|
||||
<p class="kicker">// part 02 · presenter view</p>
|
||||
<h2 class="h2">演讲者视图应该有<span class="accent">四块信息</span></h2>
|
||||
<div class="grid g2 mt-l">
|
||||
<div>
|
||||
<div class="feature-row"><span class="num blue">①</span><div><b>当前页大图</b><p class="dim">占视图一半以上,保证你能扫一眼就知道观众现在看到什么。</p></div></div>
|
||||
<div class="feature-row"><span class="num green">②</span><div><b>下一页预览</b><p class="dim">帮你提前准备过渡句,避免"下一页我忘了讲什么了"。</p></div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="feature-row"><span class="num orange">③</span><div><b>逐字稿区域</b><p class="dim">大字号、高对比度、支持滚动,这才是演讲者真正在看的东西。</p></div></div>
|
||||
<div class="feature-row"><span class="num purple">④</span><div><b>计时器 + 页码</b><p class="dim">知道自己讲了多久、还剩几页,节奏全凭这个。</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="notes">
|
||||
<p>演讲者模式应该给你四块信息。我按重要性排序。</p>
|
||||
<p>第一块,<strong>当前页大图</strong>。这个必须占据视图一半以上空间,因为它是你跟观众的"同步锚"——观众看到什么,你脑子里也得是什么。</p>
|
||||
<p>第二块,<strong>下一页预览</strong>。这个很多人不理解为什么要放,我解释一下:演讲最卡的瞬间不是讲某一页,而是<em>翻到下一页的那 2 秒</em>。如果你提前看到下一页长什么样,过渡句自然就有了。</p>
|
||||
<p>第三块,<strong>逐字稿区域</strong>——这是今天的重点,下一部分我会专门讲。这里先说一个硬性要求:字号必须大、对比度必须高、必须能滚动。因为你讲的时候<em>只有余光瞄一下</em>,字小了根本来不及读。</p>
|
||||
<p>第四块,<strong>计时器和页码</strong>。知道自己讲了多久、还剩几页——节奏感全靠它。Keynote 做得最好,reveal.js 默认不够清楚。</p>
|
||||
<p>这四块缺一不可。今天这个 deck 我把这四块都做出来了,按 S 大家可以试试。</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- ============ 5. SCRIPT ============ -->
|
||||
<section class="slide" data-title="Script">
|
||||
<p class="kicker">// part 03 · script</p>
|
||||
<h2 class="h2">逐字稿的<span class="accent">3 条铁律</span></h2>
|
||||
<div class="stack mt-l">
|
||||
<div class="rule-row">
|
||||
<span class="num red">01</span>
|
||||
<div>
|
||||
<b>不是一字不差的讲稿,是<span class="accent">"提示信号"</span></b>
|
||||
<p class="dim">把要讲的核心点加粗,把过渡句单独成段,把数据和名字列清楚——<em>让你看一眼就接得上</em>。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<span class="num red">02</span>
|
||||
<div>
|
||||
<b>每页 <span class="accent">150–300 字</span>,不多不少</b>
|
||||
<p class="dim">少于 150 字提示不够,多于 300 字你没时间读。按 2–3 分钟/页的节奏控制。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-row">
|
||||
<span class="num red">03</span>
|
||||
<div>
|
||||
<b>用<span class="accent">口语</span>写,不用书面语</b>
|
||||
<p class="dim">"因此" → "所以";"该方案" → "这个方案"。写的时候读一遍,听起来像说话才对。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="notes">
|
||||
<p>进入最核心的一部分——逐字稿怎么写。我总结了 3 条铁律。</p>
|
||||
<p><strong>第一条,逐字稿不是讲稿</strong>。很多人一听"逐字稿"就以为要把每句话一字不差写下来。错。如果你照着稿念,观众会立刻看出来,信任感瞬间崩塌。</p>
|
||||
<p>逐字稿的真实作用是<em>"提示信号"</em>——把核心要点加粗,把过渡句单独成段,把数据和专有名词列清楚。这样你讲的时候<strong>瞄一眼就能接得上</strong>,但说出来的还是你自己的话。</p>
|
||||
<p><strong>第二条,每页控制在 150 到 300 字</strong>。这个是我做了十几场分享摸出来的经验值。少于 150 字提示不够,讲到一半卡住;多于 300 字你根本来不及扫完。按一页讲 2 到 3 分钟算,这个字数刚好。</p>
|
||||
<p><strong>第三条,用口语写</strong>。这条最多人栽跟头。你写"因此",讲出来会变成"所以";你写"该方案",讲出来会变成"这个方案"。<em>写的时候读一遍</em>,不拗口才对。</p>
|
||||
<p>这三条配合起来,你会发现讲 PPT 突然变成了一件很舒服的事。</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- ============ 6. DEMO + CLOSING ============ -->
|
||||
<section class="slide" data-title="Demo & Close">
|
||||
<p class="kicker">// part 04-05 · demo + close</p>
|
||||
<h2 class="h2">现在<span class="accent">你也能做到</span></h2>
|
||||
<div class="code-block mt-m">
|
||||
<span class="comment"># 安装 html-ppt skill</span>
|
||||
<span class="cmd">npx</span> skills add <span class="flag">https://github.com/lewislulu/html-ppt-skill</span>
|
||||
|
||||
<span class="comment"># 复制演讲者模式模板</span>
|
||||
<span class="cmd">cp -r</span> templates/full-decks/presenter-mode-reveal examples/my-talk
|
||||
<span class="cmd">open</span> examples/my-talk/index.html
|
||||
|
||||
<span class="comment"># 键盘操作</span>
|
||||
<span class="flag">S</span> <span class="comment">→ 进入演讲者视图</span>
|
||||
<span class="flag">T</span> <span class="comment">→ 切换主题(5 种预设)</span>
|
||||
<span class="flag">← →</span> <span class="comment">→ 翻页</span>
|
||||
<span class="flag">R</span> <span class="comment">→ 重置计时器</span>
|
||||
</div>
|
||||
<p class="lede mt-m tc">关键是:<strong>每一页 <aside class="notes"> 里写 150–300 字逐字稿</strong>。</p>
|
||||
<div class="deck-footer">
|
||||
<span class="mono">#thanks · Q&A</span>
|
||||
<span class="slide-number" data-current="6" data-total="6"></span>
|
||||
</div>
|
||||
<aside class="notes">
|
||||
<p>最后我演示一下这个 skill 怎么用,给大家省点时间自己摸索。</p>
|
||||
<p>第一步,装 html-ppt skill,一行命令。第二步,把我这个 <code>presenter-mode-reveal</code> 模板复制到你自己的 examples 目录。第三步,打开 html,按 S。</p>
|
||||
<p>键盘操作我列在这里了。<strong>S 进入演讲者视图、T 切换主题、左右键翻页、R 重置计时器</strong>。主题默认带 5 个——tokyo-night、dracula、catppuccin-mocha、nord、corporate-clean——基本覆盖了深色技术分享、浅色商务汇报两种常见场景。</p>
|
||||
<p>最关键的一步——<em>每一页底部的 <code><aside class="notes"></code> 里,老老实实写 150 到 300 字的逐字稿</em>。这是整个方法论的交付物。AI 可以帮你写初稿,但你一定要自己过一遍,读出来听听是不是你会说的话。</p>
|
||||
<p>好,我今天就讲到这里。如果你做下一场分享的时候想起了这个"演讲者视图 + 逐字稿"的组合,并且觉得讲得比以前顺——那就是我最大的收获。谢谢大家,有问题现在开始。</p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="position:fixed;bottom:12px;left:12px;font-size:11px;color:#484f5866;z-index:100;pointer-events:none">
|
||||
S 演讲者视图 · T 切换主题 · ← → 翻页 · F 全屏 · O 总览 · R 重置计时
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-product-launch
|
||||
description: Launch keynote deck — dark hero + light content, warm orange→peach accent, feature cards, pricing tiers, CTA. Use when announcing a product, launching a feature, or doing a keynote-style reveal.
|
||||
triggers:
|
||||
- "product launch"
|
||||
- "keynote"
|
||||
- "launch deck"
|
||||
- "feature reveal"
|
||||
- "launch slides"
|
||||
- "发布会"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: marketing
|
||||
featured: 21
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "Make a product-launch keynote deck in HTML using the html-ppt-product-launch full-deck template (dark hero, warm orange accent, feature cards, pricing tiers). Confirm: product name + tagline, the 3 key features, and pricing tiers — then write the deck."
|
||||
---
|
||||
# HTML PPT · Product Launch
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`product-launch`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `product-launch` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/product-launch/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-product-launch` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-product-launch` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,467 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Halo v2 · Launch</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* product-launch — modern announcement deck */
|
||||
.tpl-product-launch{
|
||||
--bg:#ffffff;--bg-soft:#f5f5f7;--surface:#ffffff;--surface-2:#f2f2f6;
|
||||
--ink:#0a0a12;--ink-2:#3a3a44;
|
||||
--border:rgba(10,10,18,.08);--border-strong:rgba(10,10,18,.18);
|
||||
--text-1:#0a0a12;--text-2:#4a4a58;--text-3:#8a8a96;
|
||||
--accent:#ff5a36;--accent-2:#ff8c5a;--accent-3:#ffb36b;
|
||||
--grad:linear-gradient(120deg,#ff5a36 0%,#ff8c5a 60%,#ffb36b 100%);
|
||||
--radius:22px;--radius-lg:32px;
|
||||
--shadow:0 20px 60px rgba(10,10,18,.1);
|
||||
font-family:'Inter','Noto Sans SC',sans-serif;
|
||||
}
|
||||
.tpl-product-launch .slide{padding:80px 112px}
|
||||
.tpl-product-launch .slide.dark{background:#0a0a12;color:#f5f5f7}
|
||||
.tpl-product-launch .slide.dark .h1,.tpl-product-launch .slide.dark .h2,.tpl-product-launch .slide.dark h3,.tpl-product-launch .slide.dark h4{color:#fff}
|
||||
.tpl-product-launch .slide.dark .lede,.tpl-product-launch .slide.dark .dim{color:rgba(245,245,247,.72)}
|
||||
.tpl-product-launch .slide.dark .card{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.12);box-shadow:none;backdrop-filter:blur(20px)}
|
||||
.tpl-product-launch .slide.dark .kicker{color:var(--accent-2)}
|
||||
.tpl-product-launch .h1{font-size:96px;line-height:.98;font-weight:900;letter-spacing:-.045em}
|
||||
.tpl-product-launch .h2{font-size:64px;font-weight:800;letter-spacing:-.035em}
|
||||
.tpl-product-launch .hero-shot{position:absolute;right:-60px;top:50%;transform:translateY(-50%);width:640px;height:640px;border-radius:50%;background:var(--grad);filter:blur(2px);opacity:.85}
|
||||
.tpl-product-launch .hero-shot::after{content:"";position:absolute;inset:80px;border-radius:40px;background:linear-gradient(160deg,rgba(255,255,255,.3),transparent 60%),#1a1a28;box-shadow:inset 0 2px 0 rgba(255,255,255,.2)}
|
||||
.tpl-product-launch .hero-shot::before{content:"Halo v2";position:absolute;inset:80px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:44px;font-weight:900;letter-spacing:-.02em;z-index:2;border-radius:40px}
|
||||
.tpl-product-launch .brand{font-size:18px;font-weight:800;letter-spacing:-.02em}
|
||||
.tpl-product-launch .feature-card{padding:40px 36px;border-radius:var(--radius-lg);background:var(--surface);border:1px solid var(--border);position:relative;overflow:hidden}
|
||||
.tpl-product-launch .feature-card .icon{width:60px;height:60px;border-radius:18px;background:var(--grad);display:flex;align-items:center;justify-content:center;color:#fff;font-size:28px;font-weight:900;margin-bottom:20px}
|
||||
.tpl-product-launch .step{display:flex;gap:24px;align-items:flex-start}
|
||||
.tpl-product-launch .step .n{flex:none;width:56px;height:56px;border-radius:50%;background:var(--grad);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:22px}
|
||||
.tpl-product-launch .price-card{padding:40px 32px;border-radius:var(--radius-lg);border:1.5px solid var(--border);background:var(--surface);text-align:left}
|
||||
.tpl-product-launch .price-card.pro{background:#0a0a12;color:#fff;border-color:#0a0a12;transform:scale(1.04);box-shadow:0 30px 80px rgba(255,90,54,.25)}
|
||||
.tpl-product-launch .price-card.pro .dim{color:rgba(255,255,255,.7)}
|
||||
.tpl-product-launch .price-card h4{font-size:16px;text-transform:uppercase;letter-spacing:.1em;color:var(--accent)}
|
||||
.tpl-product-launch .price-card.pro h4{color:var(--accent-2)}
|
||||
.tpl-product-launch .price-card .amount{font-size:64px;font-weight:900;letter-spacing:-.035em;margin:14px 0}
|
||||
.tpl-product-launch .price-card ul{list-style:none;padding:0;margin:20px 0 0}
|
||||
.tpl-product-launch .price-card li{padding:8px 0;font-size:15px;color:var(--text-2);border-top:1px solid var(--border)}
|
||||
.tpl-product-launch .price-card.pro li{color:rgba(255,255,255,.8);border-color:rgba(255,255,255,.12)}
|
||||
.tpl-product-launch .cta-btn{display:inline-block;padding:20px 40px;border-radius:999px;background:var(--grad);color:#fff;font-weight:700;font-size:20px;box-shadow:0 20px 50px rgba(255,90,54,.4)}
|
||||
.tpl-product-launch .testimonial{max-width:44ch;font-family:'Playfair Display',serif;font-size:44px;line-height:1.25;font-weight:500;letter-spacing:-.01em}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-product-launch">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover / hero -->
|
||||
<section class="slide dark" data-title="Cover">
|
||||
<div class="hero-shot"></div>
|
||||
<div style="position:absolute;top:56px;left:112px" class="brand">◎ Halo</div>
|
||||
<p class="kicker">Launch · April 2026</p>
|
||||
<h1 class="h1 anim-fade-up" data-anim="fade-up">Meet Halo v2.<br>Your ears,<br><span style="background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent">rewritten.</span></h1>
|
||||
<p class="lede mt-m" style="max-width:42ch">Studio-grade spatial audio in the lightest open-ear earbuds ever made.</p>
|
||||
<div class="deck-footer"><span>halo.audio</span><span class="slide-number" data-current="1" data-total="8"></span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Introducing -->
|
||||
<section class="slide center tc" data-title="Introducing">
|
||||
<div>
|
||||
<p class="kicker">Introducing</p>
|
||||
<h1 class="h1" style="font-size:140px">Halo v2</h1>
|
||||
<p class="lede" style="margin:24px auto;max-width:56ch">Four years of research. Three generations of silicon. One product you'll forget you're wearing.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Feature 1 -->
|
||||
<section class="slide" data-title="Sound">
|
||||
<p class="kicker">01 · The sound</p>
|
||||
<h2 class="h2">Hear the room<br>around the music.</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="feature-card"><div class="icon">♪</div><h4>Open-ear spatial</h4><p class="dim">16mm titanium drivers angled into the ear canal. You hear the song and the world at once.</p></div>
|
||||
<div class="feature-card"><div class="icon">◈</div><h4>Lossless 24-bit</h4><p class="dim">aptX Lossless and Hi-Res LDAC over Bluetooth 5.4. No dongles, no compromises.</p></div>
|
||||
<div class="feature-card"><div class="icon">◐</div><h4>Adaptive EQ</h4><p class="dim">Tunes itself to the shape of your ear every 120 seconds.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Feature 2 -->
|
||||
<section class="slide dark" data-title="Fit">
|
||||
<p class="kicker">02 · The fit</p>
|
||||
<h2 class="h2">4.9 grams.<br>All-day forgettable.</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="card"><h4>Liquid-silicone hook</h4><p>Wraps behind the ear like a glasses arm. Never falls out on a run.</p></div>
|
||||
<div class="card"><h4>IP57 sweat + rain</h4><p>Take them in the ocean. Rinse them under the tap. We dare you.</p></div>
|
||||
<div class="card"><h4>14h + 42h case</h4><p>A full workweek of commutes on one charge of the case.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Feature 3 -->
|
||||
<section class="slide" data-title="Intelligence">
|
||||
<p class="kicker">03 · The intelligence</p>
|
||||
<h2 class="h2">An AI that listens<br>so you don't have to.</h2>
|
||||
<div class="grid g2 mt-l">
|
||||
<div class="feature-card"><div class="icon">✦</div><h4>Live translate</h4><p class="dim">Real-time translation in 41 languages. Whispered directly into your ear, with a 380ms lag.</p></div>
|
||||
<div class="feature-card"><div class="icon">✧</div><h4>Meeting recap</h4><p class="dim">Double-tap to record. Walk away with a summary, action items, and a searchable transcript.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. How it works -->
|
||||
<section class="slide" data-title="How it works">
|
||||
<p class="kicker">How it works</p>
|
||||
<h2 class="h2">Three taps. You're in.</h2>
|
||||
<div class="stack mt-l" style="max-width:900px">
|
||||
<div class="step"><div class="n">1</div><div><h4>Open the case near your phone</h4><p class="dim">iOS and Android pair automatically over Bluetooth LE. No app downloads required.</p></div></div>
|
||||
<div class="step"><div class="n">2</div><div><h4>Pick your profile</h4><p class="dim">Commute, Focus, Workout, Cinema. Each is a complete audio + transparency recipe.</p></div></div>
|
||||
<div class="step"><div class="n">3</div><div><h4>Just listen</h4><p class="dim">Halo adapts to your ear shape, your environment, and your hearing profile — continuously.</p></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. Pricing -->
|
||||
<section class="slide" data-title="Pricing">
|
||||
<p class="kicker">Pricing</p>
|
||||
<h2 class="h2">Pick your Halo.</h2>
|
||||
<div class="grid g3 mt-l" style="align-items:start">
|
||||
<div class="price-card">
|
||||
<h4>Halo Lite</h4>
|
||||
<div class="amount">$179</div>
|
||||
<p class="dim">Open-ear audio, IP57, 12h battery.</p>
|
||||
<ul><li>AAC + SBC</li><li>Single-tap controls</li><li>USB-C charging</li></ul>
|
||||
</div>
|
||||
<div class="price-card pro">
|
||||
<h4>Halo v2 · Pro</h4>
|
||||
<div class="amount">$279</div>
|
||||
<p class="dim">Everything, in its best form.</p>
|
||||
<ul><li>Hi-Res Lossless</li><li>Live translate · 41 lang</li><li>Wireless + MagSafe charging</li><li>Adaptive EQ</li></ul>
|
||||
</div>
|
||||
<div class="price-card">
|
||||
<h4>Halo Studio</h4>
|
||||
<div class="amount">$399</div>
|
||||
<p class="dim">For creators and field recorders.</p>
|
||||
<ul><li>32-bit binaural capture</li><li>XLR dongle included</li><li>Lifetime firmware</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. Testimonial + CTA combined? Task says 8 slides w/ testimonial + CTA as separate. Keep 8: testimonial on 7, but we've used 7 already. Re-plan: cover(1) intro(2) f1(3) f2(4) f3(5) how(6) pricing(7) testimonial+CTA(8) -->
|
||||
<section class="slide dark" data-title="Ship">
|
||||
<p class="kicker">One more thing</p>
|
||||
<div class="row" style="gap:80px;align-items:center">
|
||||
<div style="flex:1">
|
||||
<p class="testimonial">"I forgot I was wearing them. Then I remembered, and I didn't want to take them off."</p>
|
||||
<p class="dim mt-m">— Marques Lin, The Verge · early review</p>
|
||||
</div>
|
||||
<div style="flex:0 0 auto;text-align:center">
|
||||
<p class="dim mb-m">Ships May 14 · from</p>
|
||||
<div style="font-size:96px;font-weight:900;letter-spacing:-.04em">$279</div>
|
||||
<a class="cta-btn mt-l" href="#">Pre-order Halo v2 →</a>
|
||||
<p class="dim mt-m" style="font-size:13px">Free shipping · 45-day return · 2-year warranty</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: html-ppt-taste-brutalist
|
||||
description: 16:9 HTML deck in tactical-telemetry / CRT-terminal taste. Deactivated-CRT charcoal slides, white-phosphor monospace, hazard-red accent, scanline overlay, ASCII syntax, density over decoration. Distilled from Leonxlnx/taste-skill `brutalist-skill` (Tactical Telemetry mode).
|
||||
---
|
||||
|
||||
# HTML PPT — Tactical Telemetry / CRT Terminal
|
||||
|
||||
A 16:9 deck for project debriefs, security reviews, infra incident write-ups, ops walkthroughs, and any "we are not selling, we are reporting" presentation. Reads like a declassified mission packet, not a pitch deck.
|
||||
|
||||
This skill commits to ONE substrate (dark CRT) — never mix with the light Swiss-print mode in the same artifact.
|
||||
|
||||
## Source
|
||||
|
||||
Distilled from [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) — `skills/brutalist-skill/SKILL.md` §2.2 (Tactical Telemetry & CRT Terminal). Deck system follows the project's existing `html-ppt` convention (16:9 slides, vertical-stack fallback when opened directly).
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Substrate:** deactivated-CRT charcoal `#0A0A0A` / `#121212`. Never pure black.
|
||||
- **Foreground:** white phosphor `#EAEAEA`. Secondary `#9A9A98`.
|
||||
- **Accent:** ONE color — hazard red `#E61919`. Used on alerts, classifications, the latest data point. Never as a slide background fill.
|
||||
- **Optional:** terminal green `#4AF626` for ONE specific UI element across the entire deck (e.g. a single status indicator). Omit if it doesn't earn its place.
|
||||
- **Type:** monospace dominates. JetBrains Mono / IBM Plex Mono for body and meta. Heavy condensed grotesque (Archivo Black / Inter Black) only for slide titles.
|
||||
- **Title scale:** `clamp(56px, 7vw, 96px)`, uppercase, tracking `-0.04em`, leading `0.9`.
|
||||
- **Geometry:** `border-radius: 0`. Visible 1px hairlines (`#2A2A28` on charcoal). Use `display: grid; gap: 1px` over a hairline-colored background to render perfect cells.
|
||||
- **Scanline overlay:** subtle `repeating-linear-gradient` at `2px / 4px` cycle, opacity ≤ `0.08`, applied as a fixed pointer-events-none layer.
|
||||
- **Phosphor noise:** optional SVG-grain pseudo-element, opacity ≤ `0.06`.
|
||||
- **Slide chrome:** every slide carries top register strip — classification, slide ID, timestamp, coordinates — and a bottom bar with serial number + page.
|
||||
|
||||
## Banned
|
||||
|
||||
- `border-radius` above 0.
|
||||
- Drop shadows, gradients, glassmorphism, glow.
|
||||
- Color other than charcoal, phosphor, hazard red, and at most one terminal-green element.
|
||||
- Sans-serif body fonts. Monospace is the body.
|
||||
- Pitch-deck "delight" — emoji, illustration, stock photography, friendly icons.
|
||||
- Light-mode slides anywhere in the same deck.
|
||||
- Slide transitions other than instant cuts.
|
||||
|
||||
## Required slide archetypes (10–14 total)
|
||||
|
||||
1. **Classification cover** — giant numeral or call-sign on the left, redaction bar above the title, mono meta column on the right.
|
||||
2. **Briefing strip** — eight-cell mono register with mission ID, dates, principals, classification.
|
||||
3. **Numbered objectives** — three to five hairline-separated theses, each with `>>>` marker.
|
||||
4. **Telemetry grid** — `display: grid; gap: 1px` of mono key-value cells; red highlight on the variant that breaks the trend.
|
||||
5. **Threat / risk register** — hairline table with severity column in red.
|
||||
6. **Sequence / timeline** — vertical mono list, 2-px vertical rule on the left, hazard markers on critical events.
|
||||
7. **Diagram / wiring** — pure-CSS box-and-line schematic; rectangles with hairlines, ASCII arrows.
|
||||
8. **Specimen** — single mono character or word at viewport-bleeding scale, used as a visual fulcrum.
|
||||
9. **Alert** — diagonal hazard-stripe block (`repeating-linear-gradient(135deg, ...)`) with the most important sentence in the deck.
|
||||
10. **Audit log** — append-only mono entries with timestamp + actor + event.
|
||||
11. **Closing colophon** — operator, system, build, classification, sign-off line.
|
||||
|
||||
## Motion
|
||||
|
||||
This aesthetic is mechanical and instant.
|
||||
- Cuts between slides — no fades. Optionally a 60ms flicker (`opacity: 0.85 → 1`).
|
||||
- A blinking caret on the cover (`▌`) and a single pulse on the live status dot. Nothing else moves.
|
||||
|
||||
## Pre-flight
|
||||
|
||||
- [ ] Substrate is charcoal, foreground is phosphor, only accent is hazard red
|
||||
- [ ] All `border-radius` is 0; all corners are 90°
|
||||
- [ ] Title slide includes classification + serial + timestamp + coordinates
|
||||
- [ ] At least one `display: grid; gap: 1px` telemetry module
|
||||
- [ ] Scanline overlay applied as fixed pointer-events-none element, opacity ≤ 0.08
|
||||
- [ ] At least one diagonal hazard-stripe alert block
|
||||
- [ ] ASCII syntax decoration (`[ ... ]`, `>>>`, `///`) appears at least four times across the deck
|
||||
- [ ] Numeric data uses tabular-nums + monospace
|
||||
- [ ] No emojis, no curves, no gradients, no shadow effects
|
||||
- [ ] Terminal green appears on zero or one element only — never as text body color
|
||||
@@ -0,0 +1,774 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>OPN-04 // INCIDENT TELEMETRY · CLASSIFIED INTERNAL</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=JetBrains+Mono:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--crt: #0B0B0B;
|
||||
--crt-2: #131312;
|
||||
--phos: #ECECEA;
|
||||
--phos-soft: #9F9F9C;
|
||||
--phos-mute: #6A6A67;
|
||||
--rule: #2A2A27;
|
||||
--rule-strong: #3A3A36;
|
||||
--hazard: #E61919;
|
||||
--hazard-soft: rgba(230,25,25,0.12);
|
||||
--green: #4AF626;
|
||||
--display: 'Archivo Black', 'Inter', sans-serif;
|
||||
--mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--crt);
|
||||
color: var(--phos);
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-feature-settings: "tnum";
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Fixed CRT scanlines + phosphor noise overlay */
|
||||
body::before {
|
||||
content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 200;
|
||||
background: repeating-linear-gradient(0deg, transparent 0 2px, rgba(0,0,0,0.18) 2px 3px);
|
||||
mix-blend-mode: multiply;
|
||||
opacity: 0.55;
|
||||
}
|
||||
body::after {
|
||||
content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 199;
|
||||
background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.45) 100%);
|
||||
}
|
||||
|
||||
/* ==== Deck system ==== */
|
||||
.deck { position: relative; width: 100vw; }
|
||||
.slide {
|
||||
position: relative;
|
||||
width: 100vw; height: 100vh; min-height: 720px;
|
||||
padding: 56px 64px 72px;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
page-break-after: always;
|
||||
background: var(--crt);
|
||||
}
|
||||
.slide + .slide { border-top: 1px solid var(--rule); }
|
||||
|
||||
/* ==== Slide chrome ==== */
|
||||
.topbar {
|
||||
position: absolute; top: 0; left: 0; right: 0;
|
||||
padding: 10px 24px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr 1fr 1fr 1fr 1fr;
|
||||
gap: 22px;
|
||||
font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase;
|
||||
color: var(--phos-soft);
|
||||
background: var(--crt-2);
|
||||
}
|
||||
.topbar b { color: var(--phos); font-weight: 500; }
|
||||
.topbar .red { color: var(--hazard); }
|
||||
.topbar .blink { animation: blink 1.4s steps(1) infinite; }
|
||||
@keyframes blink { 50% { opacity: 0.25; } }
|
||||
|
||||
.botbar {
|
||||
position: absolute; bottom: 0; left: 0; right: 0;
|
||||
padding: 10px 24px;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase;
|
||||
color: var(--phos-soft);
|
||||
background: var(--crt-2);
|
||||
}
|
||||
.botbar .red { color: var(--hazard); }
|
||||
.botbar .seg { display: inline-flex; gap: 14px; }
|
||||
.botbar .seg b { color: var(--phos); font-weight: 500; }
|
||||
|
||||
.ascii-frame {
|
||||
display: inline-flex; align-items: center; gap: 10px;
|
||||
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--hazard);
|
||||
}
|
||||
.ascii-frame::before { content: '['; }
|
||||
.ascii-frame::after { content: ']'; }
|
||||
|
||||
.redact {
|
||||
display: inline-block;
|
||||
background: var(--hazard); color: var(--crt);
|
||||
padding: 3px 10px;
|
||||
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.22em; text-transform: uppercase; font-weight: 600;
|
||||
}
|
||||
.stamp {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--hazard); color: var(--hazard);
|
||||
padding: 4px 10px;
|
||||
font-family: var(--mono); font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ==== Headline ==== */
|
||||
h1.title, h2.title {
|
||||
font-family: var(--display);
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.9;
|
||||
margin: 0;
|
||||
color: var(--phos);
|
||||
}
|
||||
h1.title { font-size: clamp(64px, 7.6vw, 112px); max-width: 18ch; }
|
||||
h2.title { font-size: clamp(48px, 5.6vw, 80px); max-width: 18ch; }
|
||||
h2.title .red { color: var(--hazard); }
|
||||
.lede { font-family: var(--mono); font-size: 14px; line-height: 1.65; color: var(--phos-soft); max-width: 78ch; margin: 0; }
|
||||
.label { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--hazard); }
|
||||
|
||||
/* ==== 01 · COVER ==== */
|
||||
.cover { justify-content: center; }
|
||||
.cover .grid {
|
||||
display: grid; grid-template-columns: 1.2fr 1fr;
|
||||
align-items: center;
|
||||
gap: 64px;
|
||||
}
|
||||
.cover .num {
|
||||
font-family: var(--display);
|
||||
font-size: clamp(220px, 32vw, 460px);
|
||||
line-height: 0.78;
|
||||
letter-spacing: -0.07em;
|
||||
color: var(--phos);
|
||||
position: relative;
|
||||
}
|
||||
.cover .num .caret { color: var(--hazard); animation: blink 1s steps(1) infinite; }
|
||||
.cover .meta-col { display: flex; flex-direction: column; gap: 22px; }
|
||||
.cover .meta-col h1 { margin: 0; }
|
||||
.cover .meta-stamps { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.cover dl {
|
||||
display: grid; grid-template-columns: 16ch 1fr; gap: 8px 16px;
|
||||
margin: 0; font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase;
|
||||
border-top: 1px solid var(--rule); padding-top: 18px;
|
||||
}
|
||||
.cover dl dt { color: var(--hazard); }
|
||||
.cover dl dd { margin: 0; color: var(--phos); }
|
||||
|
||||
/* ==== 02 · BRIEFING STRIP ==== */
|
||||
.briefing { justify-content: flex-start; padding-top: 96px; }
|
||||
.briefing h2 { margin-bottom: 28px; }
|
||||
.briefing .strip {
|
||||
margin-top: 32px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1px;
|
||||
background: var(--rule);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
.briefing .cell { background: var(--crt); padding: 22px 22px 26px; }
|
||||
.briefing .cell .k { color: var(--hazard); font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase; margin-bottom: 8px; display: block; }
|
||||
.briefing .cell .v { font-family: var(--display); font-size: 28px; line-height: 1; letter-spacing: -0.03em; text-transform: uppercase; color: var(--phos); }
|
||||
.briefing .cell .v small { display: block; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em; color: var(--phos-soft); margin-top: 6px; font-weight: 400; }
|
||||
.briefing .lede { margin-top: 28px; }
|
||||
|
||||
/* ==== 03 · OBJECTIVES ==== */
|
||||
.objectives { padding-top: 96px; }
|
||||
.objectives h2 { margin-bottom: 28px; }
|
||||
.objectives .list {
|
||||
border-top: 1px solid var(--rule);
|
||||
margin-top: 28px;
|
||||
}
|
||||
.objectives .item {
|
||||
display: grid;
|
||||
grid-template-columns: 6ch 1fr 14ch;
|
||||
align-items: baseline;
|
||||
gap: 28px;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.objectives .item .n {
|
||||
font-family: var(--display); font-size: 32px; line-height: 1; letter-spacing: -0.03em; color: var(--phos);
|
||||
}
|
||||
.objectives .item .arrow { color: var(--hazard); margin-right: 10px; letter-spacing: 0.1em; }
|
||||
.objectives .item h4 {
|
||||
font-family: var(--display); font-size: 22px; line-height: 1.1; letter-spacing: -0.02em; text-transform: uppercase;
|
||||
margin: 0 0 6px; font-weight: 400;
|
||||
}
|
||||
.objectives .item p { font-size: 13px; color: var(--phos-soft); line-height: 1.6; max-width: 72ch; margin: 0; }
|
||||
.objectives .item .tag {
|
||||
text-align: right; font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--hazard);
|
||||
}
|
||||
|
||||
/* ==== 04 · TELEMETRY GRID ==== */
|
||||
.telemetry { padding-top: 96px; }
|
||||
.telemetry h2 { margin-bottom: 24px; }
|
||||
.telemetry .grid {
|
||||
margin-top: 32px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-auto-rows: 130px;
|
||||
gap: 1px;
|
||||
background: var(--rule);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
.telemetry .tcell { background: var(--crt); padding: 18px 20px; display: flex; flex-direction: column; justify-content: space-between; }
|
||||
.telemetry .tcell .k { font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--phos-mute); }
|
||||
.telemetry .tcell .v { font-family: var(--display); font-size: 38px; line-height: 1; letter-spacing: -0.04em; color: var(--phos); }
|
||||
.telemetry .tcell .v small { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; color: var(--phos-soft); margin-left: 4px; font-weight: 400; }
|
||||
.telemetry .tcell.alert { background: var(--hazard-soft); }
|
||||
.telemetry .tcell.alert .k { color: var(--hazard); }
|
||||
.telemetry .tcell.alert .v { color: var(--hazard); }
|
||||
.telemetry .tcell .delta { font-size: 11px; letter-spacing: 0.06em; color: var(--hazard); margin-top: 4px; }
|
||||
.telemetry .tcell .delta.ok { color: var(--phos-soft); }
|
||||
.telemetry .tcell.live { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 12px; letter-spacing: 0.18em; }
|
||||
.telemetry .live-dot {
|
||||
width: 8px; height: 8px; background: var(--green);
|
||||
box-shadow: 0 0 10px rgba(74,246,38,0.55);
|
||||
animation: pulse 1.6s steps(1) infinite;
|
||||
}
|
||||
@keyframes pulse { 50% { opacity: 0.3; box-shadow: 0 0 0 rgba(74,246,38,0); } }
|
||||
.span-2 { grid-column: span 2; }
|
||||
.span-3 { grid-column: span 3; }
|
||||
.row-2 { grid-row: span 2; }
|
||||
|
||||
/* ==== 05 · RISK REGISTER ==== */
|
||||
.risk { padding-top: 96px; }
|
||||
.risk h2 { margin-bottom: 28px; }
|
||||
.risk .table {
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
grid-template-columns: 4ch 1.6fr 1fr 8ch 8ch 1fr;
|
||||
gap: 1px;
|
||||
background: var(--rule);
|
||||
border: 1px solid var(--rule);
|
||||
font-size: 12.5px; letter-spacing: 0.04em;
|
||||
}
|
||||
.risk .table > div { background: var(--crt); padding: 12px 14px; }
|
||||
.risk .table .h {
|
||||
background: var(--phos); color: var(--crt);
|
||||
font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 600;
|
||||
}
|
||||
.risk .table .right { text-align: right; }
|
||||
.risk .table .sev-hi { color: var(--hazard); font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; }
|
||||
.risk .table .sev-md { color: #E0A819; text-transform: uppercase; letter-spacing: 0.1em; }
|
||||
.risk .table .sev-lo { color: var(--phos-soft); text-transform: uppercase; letter-spacing: 0.1em; }
|
||||
|
||||
/* ==== 06 · TIMELINE ==== */
|
||||
.timeline { padding-top: 96px; }
|
||||
.timeline h2 { margin-bottom: 24px; }
|
||||
.timeline .seq {
|
||||
margin-top: 28px;
|
||||
border-left: 2px solid var(--phos);
|
||||
padding-left: 28px;
|
||||
display: flex; flex-direction: column; gap: 18px;
|
||||
}
|
||||
.timeline .ev {
|
||||
display: grid; grid-template-columns: 14ch 1fr 12ch; gap: 22px; align-items: baseline;
|
||||
position: relative;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.timeline .ev::before {
|
||||
content: ''; position: absolute; left: -36px; top: 8px; width: 14px; height: 1px; background: var(--phos);
|
||||
}
|
||||
.timeline .ev.crit::before { background: var(--hazard); height: 2px; top: 7px; }
|
||||
.timeline .ev .ts { color: var(--phos-soft); font-size: 12px; letter-spacing: 0.06em; }
|
||||
.timeline .ev .body { font-size: 13.5px; color: var(--phos); line-height: 1.5; max-width: 64ch; }
|
||||
.timeline .ev.crit .body { color: var(--phos); }
|
||||
.timeline .ev .actor { font-size: 11px; text-align: right; letter-spacing: 0.14em; text-transform: uppercase; color: var(--phos-soft); }
|
||||
.timeline .ev.crit .actor { color: var(--hazard); }
|
||||
|
||||
/* ==== 07 · DIAGRAM ==== */
|
||||
.diagram { padding-top: 96px; }
|
||||
.diagram h2 { margin-bottom: 28px; }
|
||||
.diagram .schematic {
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: 100px 64px 100px;
|
||||
gap: 0;
|
||||
align-items: stretch;
|
||||
font-size: 11.5px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.diagram .box {
|
||||
border: 1px solid var(--phos);
|
||||
padding: 10px 14px;
|
||||
display: flex; flex-direction: column; justify-content: center;
|
||||
color: var(--phos);
|
||||
font-size: 11.5px; letter-spacing: 0.16em;
|
||||
}
|
||||
.diagram .box b { display: block; font-family: var(--display); font-size: 16px; letter-spacing: -0.02em; text-transform: uppercase; color: var(--phos); margin-bottom: 4px; }
|
||||
.diagram .box.alert { border-color: var(--hazard); color: var(--hazard); }
|
||||
.diagram .box.alert b { color: var(--hazard); }
|
||||
.diagram .arrow {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--hazard);
|
||||
font-family: var(--mono); font-size: 18px; letter-spacing: 0.1em;
|
||||
}
|
||||
.diagram .vbar {
|
||||
display: flex; justify-content: center; align-items: stretch;
|
||||
}
|
||||
.diagram .vbar::before { content: ''; width: 1px; background: var(--phos); }
|
||||
|
||||
/* ==== 08 · SPECIMEN ==== */
|
||||
.specimen { justify-content: center; align-items: flex-start; padding-top: 110px; }
|
||||
.specimen .display {
|
||||
font-family: var(--display);
|
||||
font-size: clamp(140px, 22vw, 360px);
|
||||
line-height: 0.82;
|
||||
letter-spacing: -0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--phos);
|
||||
max-width: 100%;
|
||||
}
|
||||
.specimen .display .red { color: var(--hazard); }
|
||||
.specimen .footnote {
|
||||
margin-top: 28px;
|
||||
font-size: 12px; letter-spacing: 0.06em;
|
||||
color: var(--phos-soft); max-width: 64ch;
|
||||
}
|
||||
|
||||
/* ==== 09 · ALERT ==== */
|
||||
.alert-slide { justify-content: center; }
|
||||
.alert-slide .alertbox {
|
||||
margin-top: 12px;
|
||||
padding: 56px 48px;
|
||||
border: 2px solid var(--hazard);
|
||||
background:
|
||||
repeating-linear-gradient(135deg, transparent 0 18px, rgba(230,25,25,0.08) 18px 36px);
|
||||
display: grid; grid-template-columns: 22ch 1fr; gap: 40px;
|
||||
align-items: start;
|
||||
}
|
||||
.alert-slide .alertbox .glyph {
|
||||
font-family: var(--display); font-size: clamp(100px, 14vw, 220px); line-height: 0.86; letter-spacing: -0.06em;
|
||||
color: var(--hazard); text-transform: uppercase;
|
||||
}
|
||||
.alert-slide .alertbox h3 {
|
||||
font-family: var(--display); font-size: clamp(28px, 3.2vw, 44px); line-height: 1.1; letter-spacing: -0.025em;
|
||||
text-transform: uppercase; margin: 0 0 18px; color: var(--phos);
|
||||
}
|
||||
.alert-slide .alertbox p { font-size: 14px; line-height: 1.65; color: var(--phos); margin: 0 0 14px; max-width: 56ch; }
|
||||
.alert-slide .alertbox p strong { background: var(--hazard); color: var(--crt); padding: 1px 6px; font-weight: 500; }
|
||||
|
||||
/* ==== 10 · AUDIT LOG ==== */
|
||||
.audit { padding-top: 96px; }
|
||||
.audit h2 { margin-bottom: 24px; }
|
||||
.audit .log {
|
||||
margin-top: 32px;
|
||||
border: 1px solid var(--rule);
|
||||
background: #050505;
|
||||
padding: 22px 26px;
|
||||
font-size: 12.5px; line-height: 1.7;
|
||||
}
|
||||
.audit .log .row { display: grid; grid-template-columns: 14ch 12ch 1fr 14ch; gap: 16px; padding: 4px 0; border-top: 1px dashed var(--rule); }
|
||||
.audit .log .row:first-child { border-top: none; }
|
||||
.audit .log .ts { color: var(--phos-mute); }
|
||||
.audit .log .actor { color: var(--phos); }
|
||||
.audit .log .ev { color: var(--phos-soft); }
|
||||
.audit .log .ev.crit { color: var(--hazard); }
|
||||
.audit .log .sig { color: var(--phos-mute); text-align: right; letter-spacing: 0.06em; }
|
||||
|
||||
/* ==== 11 · COLOPHON ==== */
|
||||
.colophon-slide { padding-top: 96px; }
|
||||
.colophon-slide h2 { margin-bottom: 24px; }
|
||||
.colophon-slide .grid { margin-top: 32px; display: grid; grid-template-columns: 1fr 1fr; gap: 64px; }
|
||||
.colophon-slide dl {
|
||||
margin: 0; display: grid; grid-template-columns: 16ch 1fr; gap: 10px 18px;
|
||||
font-size: 12px; letter-spacing: 0.06em;
|
||||
}
|
||||
.colophon-slide dt { color: var(--hazard); text-transform: uppercase; letter-spacing: 0.18em; font-size: 11px; }
|
||||
.colophon-slide dd { margin: 0; color: var(--phos); }
|
||||
.colophon-slide .signoff {
|
||||
margin-top: 56px; padding-top: 24px; border-top: 1px solid var(--rule);
|
||||
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--phos-soft);
|
||||
display: flex; justify-content: space-between;
|
||||
}
|
||||
|
||||
/* responsive nicety for narrow screens */
|
||||
@media (max-width: 880px) {
|
||||
.slide { padding: 56px 22px 72px; }
|
||||
.topbar { grid-template-columns: 1.4fr 1fr 1fr; gap: 12px; }
|
||||
.topbar > *:nth-child(n+4) { display: none; }
|
||||
.cover .grid { grid-template-columns: 1fr; }
|
||||
.cover .num { font-size: 36vw; }
|
||||
.briefing .strip { grid-template-columns: 1fr 1fr; }
|
||||
.telemetry .grid { grid-template-columns: 1fr 1fr; grid-auto-rows: 100px; }
|
||||
.span-2, .span-3, .row-2 { grid-column: span 1; grid-row: auto; }
|
||||
.risk .table { grid-template-columns: 4ch 1fr 1fr; font-size: 11px; }
|
||||
.risk .table > div:nth-child(6n+4),
|
||||
.risk .table > div:nth-child(6n+5),
|
||||
.risk .table > div:nth-child(6n) { display: none; }
|
||||
.diagram .schematic { grid-template-columns: 1fr; grid-template-rows: auto; }
|
||||
.diagram .arrow, .diagram .vbar { display: none; }
|
||||
.alert-slide .alertbox { grid-template-columns: 1fr; padding: 32px 22px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="deck">
|
||||
|
||||
<!-- 01 · COVER -->
|
||||
<section class="slide cover">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · INCIDENT TELEMETRY</div>
|
||||
<div>SLIDE <b>01 / 11</b></div>
|
||||
<div>2026.05.14 · 04:12Z</div>
|
||||
<div>44.8404°N · −0.5805°W</div>
|
||||
<div class="red">⬤ <span class="blink">CLASSIFIED · INTERNAL</span></div>
|
||||
<div>OPERATOR · Q.ALBRECHT</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="num">04<span class="caret">▌</span></div>
|
||||
<div class="meta-col">
|
||||
<div class="meta-stamps">
|
||||
<span class="redact">▮▮▮▮ DECL ▮▮▮▮</span>
|
||||
<span class="stamp">SEV · CRITICAL</span>
|
||||
<span class="stamp">PKT · 04 / 11</span>
|
||||
</div>
|
||||
<h1 class="title">Incident Telemetry — <span style="color: var(--hazard);">Operation Halcyon</span></h1>
|
||||
<p class="lede">Field debrief for the runtime outage on 2026.05.13 — 03:18Z to 06:41Z. Eleven slides. No friendly icons. Read top to bottom.</p>
|
||||
<dl>
|
||||
<dt>Mission</dt><dd>OPN-04 / OPERATION HALCYON</dd>
|
||||
<dt>Operator</dt><dd>Q. Albrecht · Incident Commander</dd>
|
||||
<dt>System</dt><dd>halcyon-runtime · v 2026.05.06</dd>
|
||||
<dt>Cell</dt><dd>EU-WEST-3 · BORDEAUX-A</dd>
|
||||
<dt>Distribution</dt><dd>internal · oncall · founders</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0731</b></span><span>VOL <b>04</b></span><span>ISS <b>2026.05</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>01 / 11</b></span><span class="red">⬤ TRANSMITTING</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 02 · BRIEFING STRIP -->
|
||||
<section class="slide briefing">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · BRIEFING</div>
|
||||
<div>SLIDE <b>02 / 11</b></div>
|
||||
<div>STAGE · 01</div>
|
||||
<div>SECTION · OVERVIEW</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">02 / mission briefing</span>
|
||||
<h2 class="title">Three hours, twenty-three minutes, sixty-four percent of <span class="red">tier-3</span> traffic.</h2>
|
||||
<div class="strip">
|
||||
<div class="cell"><span class="k">window</span><span class="v">3h 23m<small>03:18Z → 06:41Z</small></span></div>
|
||||
<div class="cell"><span class="k">tier hit</span><span class="v" style="color: var(--hazard);">tier-3<small>research-agent</small></span></div>
|
||||
<div class="cell"><span class="k">tasks failed</span><span class="v">14,820<small>17.3% of window</small></span></div>
|
||||
<div class="cell"><span class="k">refunded</span><span class="v">€ 4,840<small>auto · within 24h</small></span></div>
|
||||
<div class="cell"><span class="k">root cause</span><span class="v">DNS cache<small>upstream provider 04</small></span></div>
|
||||
<div class="cell"><span class="k">resolved at</span><span class="v">06:41Z<small>by Q. Albrecht</small></span></div>
|
||||
<div class="cell"><span class="k">postmortem</span><span class="v">CIRC-04<small>filed 2026.05.14</small></span></div>
|
||||
<div class="cell"><span class="k">action items</span><span class="v" style="color: var(--hazard);">07 open<small>03 critical · 04 medium</small></span></div>
|
||||
</div>
|
||||
<p class="lede" style="margin-top: 28px;">A regional DNS provider returned stale records for 3h 23m. Halcyon's resolver pinned to one of three upstream providers; the failover threshold was set too high. Tier-3 (research) clients with aggressive retry policies amplified failure into customer-visible errors. Customers on tier-1 (transactional) saw degradation but no failure.</p>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0732</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>02 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 03 · OBJECTIVES -->
|
||||
<section class="slide objectives">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · OBJECTIVES</div>
|
||||
<div>SLIDE <b>03 / 11</b></div>
|
||||
<div>STAGE · 02</div>
|
||||
<div>SECTION · DEBRIEF</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">03 / debrief objectives</span>
|
||||
<h2 class="title">Five lines we will <span class="red">defend</span> in writing this week.</h2>
|
||||
<div class="list">
|
||||
<div class="item">
|
||||
<div class="n">01</div>
|
||||
<div>
|
||||
<h4><span class="arrow">>>></span>Resolver failover threshold drops from 600 ms to 180 ms.</h4>
|
||||
<p>Currently we wait until the upstream provider misses six hundred milliseconds of probes before failing over to provider 02. The new threshold ratifies a single missed probe at 180 ms.</p>
|
||||
</div>
|
||||
<div class="tag">CRIT · 14d</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="n">02</div>
|
||||
<div>
|
||||
<h4><span class="arrow">>>></span>Three independent DNS providers, weighted equally.</h4>
|
||||
<p>The pin to provider 04 was a vestige from the 2025 cost review. We move to a three-way Anycast resolver, weighted equally, with provider failure quarantined for 30 minutes after a missed probe.</p>
|
||||
</div>
|
||||
<div class="tag">CRIT · 21d</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="n">03</div>
|
||||
<div>
|
||||
<h4><span class="arrow">>>></span>Tier-3 clients get retry budgets, not retry loops.</h4>
|
||||
<p>Research-agent clients amplified failure 4.6× by retrying inside the failure window. We expose a budget — N retries per 60s — and refuse beyond it with an explicit, customer-readable error.</p>
|
||||
</div>
|
||||
<div class="tag">CRIT · 30d</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="n">04</div>
|
||||
<div>
|
||||
<h4><span class="arrow">>>></span>Refunds are automated, not gestured.</h4>
|
||||
<p>The 4,840 € refund cycle was hand-cranked by two engineers between 04:30 and 09:00. We codify a refund pipeline keyed to tier × failure-class × duration, with an audit log and a postmortem hook.</p>
|
||||
</div>
|
||||
<div class="tag">MED · 45d</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="n">05</div>
|
||||
<div>
|
||||
<h4><span class="arrow">>>></span>Status page reads like a sentence, not a heatmap.</h4>
|
||||
<p>During the window, the status page showed eight green pills and one yellow chevron. The customer's experience was "everything is on fire." We replace the dashboard with a one-paragraph human summary, updated every 10 minutes.</p>
|
||||
</div>
|
||||
<div class="tag">MED · 30d</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0733</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>03 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 04 · TELEMETRY GRID -->
|
||||
<section class="slide telemetry">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · TELEMETRY</div>
|
||||
<div>SLIDE <b>04 / 11</b></div>
|
||||
<div>STAGE · 03</div>
|
||||
<div>SECTION · METRICS</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">04 / telemetry · 24h window</span>
|
||||
<h2 class="title">Numbers from the <span class="red">window</span>.</h2>
|
||||
<div class="grid">
|
||||
<div class="tcell row-2 alert">
|
||||
<span class="k">tier-3 · failure rate</span>
|
||||
<span class="v">17.3<small>%</small></span>
|
||||
<span class="delta">▲ +14.6 pp vs baseline · CRIT</span>
|
||||
</div>
|
||||
<div class="tcell"><span class="k">tier-1 · failure</span><span class="v">0.04<small>%</small></span><span class="delta ok">▲ +0.02 pp · within slo</span></div>
|
||||
<div class="tcell"><span class="k">tier-2 · failure</span><span class="v">0.61<small>%</small></span><span class="delta ok">▲ +0.4 pp · within slo</span></div>
|
||||
<div class="tcell"><span class="k">p99 · resolver</span><span class="v">3,180<small>ms</small></span><span class="delta">▲ x 41 vs baseline</span></div>
|
||||
<div class="tcell"><span class="k">retries · 24h</span><span class="v">68k<small>·×4.6</small></span><span class="delta">▲ amplification</span></div>
|
||||
<div class="tcell"><span class="k">refunds</span><span class="v">€4,840</span><span class="delta ok">manual · 04:30 → 09:00</span></div>
|
||||
<div class="tcell"><span class="k">paged engineers</span><span class="v">04<small>oncall</small></span><span class="delta ok">3 ack < 5min · 1 < 12min</span></div>
|
||||
<div class="tcell"><span class="k">customer tickets</span><span class="v">37</span><span class="delta">▲ x 11 vs baseline</span></div>
|
||||
<div class="tcell live"><span class="live-dot"></span><span style="color: var(--green);">resolver healthy · 7d 14h</span></div>
|
||||
<div class="tcell"><span class="k">tasks dropped</span><span class="v">14,820</span><span class="delta">▲ refunded auto-12h</span></div>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0734</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>04 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 05 · RISK REGISTER -->
|
||||
<section class="slide risk">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · RISK REGISTER</div>
|
||||
<div>SLIDE <b>05 / 11</b></div>
|
||||
<div>STAGE · 04</div>
|
||||
<div>SECTION · POSTURE</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">05 / open risks · halcyon runtime</span>
|
||||
<h2 class="title">Open risks, scored against the <span class="red">runtime</span>.</h2>
|
||||
<div class="table">
|
||||
<div class="h">№</div><div class="h">risk</div><div class="h">vector</div><div class="h">sev</div><div class="h">prob</div><div class="h">owner / due</div>
|
||||
|
||||
<div>R-01</div><div>Single-provider DNS resolver pin</div><div>infra · routing</div><div class="sev-hi">crit</div><div class="right">0.42</div><div>Q.ALB · 2026.05.28</div>
|
||||
<div>R-02</div><div>Tier-3 retry amplification (no budget)</div><div>client · sdk</div><div class="sev-hi">crit</div><div class="right">0.31</div><div>H.NAI · 2026.06.10</div>
|
||||
<div>R-03</div><div>Refund pipeline manual</div><div>finance · ops</div><div class="sev-md">med</div><div class="right">0.55</div><div>P.NWA · 2026.06.20</div>
|
||||
<div>R-04</div><div>Status page is a heatmap, not a sentence</div><div>comms</div><div class="sev-md">med</div><div class="right">0.61</div><div>L.ARR · 2026.06.20</div>
|
||||
<div>R-05</div><div>Audit log not subpoena-grade</div><div>legal</div><div class="sev-md">med</div><div class="right">0.18</div><div>P.NWA · 2026.07.01</div>
|
||||
<div>R-06</div><div>EU-WEST-3 single-cell deployment</div><div>infra · region</div><div class="sev-lo">lo</div><div class="right">0.06</div><div>Q.ALB · 2026.Q4</div>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0735</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>05 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 06 · TIMELINE -->
|
||||
<section class="slide timeline">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · SEQUENCE</div>
|
||||
<div>SLIDE <b>06 / 11</b></div>
|
||||
<div>STAGE · 05</div>
|
||||
<div>SECTION · TIMELINE</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">06 / event sequence · 03:18Z → 06:41Z</span>
|
||||
<h2 class="title">Sequence of <span class="red">events</span>.</h2>
|
||||
<div class="seq">
|
||||
<div class="ev"><span class="ts">03:18:04Z</span><div class="body">Upstream provider 04 begins returning stale A records for runtime.halcyon.io.</div><div class="actor">— PROVIDER-04</div></div>
|
||||
<div class="ev"><span class="ts">03:19:11Z</span><div class="body">Resolver retries against pinned provider 04. p99 climbs to 1,840 ms within sixty-seven seconds.</div><div class="actor">— RESOLVER</div></div>
|
||||
<div class="ev crit"><span class="ts">03:21:48Z</span><div class="body">Tier-3 (research-agent) clients begin retry storm. Failure rate breaches the 5% page threshold; oncall pages four engineers.</div><div class="actor">— PAGER · CRIT</div></div>
|
||||
<div class="ev"><span class="ts">03:24:02Z</span><div class="body">Q. Albrecht acks the page from Bordeaux. H. Naitō from Munich at 03:24:18Z. Two more engineers within nine minutes.</div><div class="actor">— Q.ALB · H.NAI</div></div>
|
||||
<div class="ev crit"><span class="ts">03:38:00Z</span><div class="body">First public status update posted: "We are investigating elevated errors on the runtime." Status page does not yet reflect the severity.</div><div class="actor">— STATUS · CRIT</div></div>
|
||||
<div class="ev"><span class="ts">04:01:22Z</span><div class="body">Root cause narrowed to provider 04 DNS. Manual failover to provider 02 begins.</div><div class="actor">— Q.ALB</div></div>
|
||||
<div class="ev"><span class="ts">04:30:00Z</span><div class="body">Refund triage begins. Hand-rolled SQL against the audit log identifies 14,820 dropped tasks across 312 customers.</div><div class="actor">— P.NWA</div></div>
|
||||
<div class="ev crit"><span class="ts">06:41:09Z</span><div class="body">Failover complete. Failure rate returns to baseline. Public status updated. Postmortem CIRC-04 opened.</div><div class="actor">— ALL · CLEAR</div></div>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0736</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>06 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 07 · DIAGRAM -->
|
||||
<section class="slide diagram">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · WIRING</div>
|
||||
<div>SLIDE <b>07 / 11</b></div>
|
||||
<div>STAGE · 06</div>
|
||||
<div>SECTION · DIAGRAM</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">07 / resolver · before / after</span>
|
||||
<h2 class="title">Resolver — <span class="red">before</span> & after.</h2>
|
||||
<div class="schematic">
|
||||
<div class="box"><b>SDK</b>tier-1 client</div>
|
||||
<div class="arrow">━━▶</div>
|
||||
<div class="box"><b>RESOLVER</b>halcyon · pinned</div>
|
||||
<div class="arrow">━━▶</div>
|
||||
<div class="box alert"><b>PROVIDER 04</b>upstream · STALE</div>
|
||||
|
||||
<div class="vbar"></div>
|
||||
<div></div>
|
||||
<div class="vbar"></div>
|
||||
<div></div>
|
||||
<div class="vbar"></div>
|
||||
|
||||
<div class="box"><b>SDK</b>tier-3 retry storm</div>
|
||||
<div class="arrow">━━▶</div>
|
||||
<div class="box alert"><b>RESOLVER</b>p99 · 3,180 ms</div>
|
||||
<div class="arrow">━━▶</div>
|
||||
<div class="box alert"><b>14,820 TASKS</b>dropped · 17.3%</div>
|
||||
</div>
|
||||
<p class="lede" style="margin-top: 32px;">After: resolver is unpinned and weighted across providers 02 / 04 / 07. Failover threshold drops to 180 ms. Tier-3 retry budget caps amplification at ×1.4. The bottom row of this diagram never gets drawn again.</p>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0737</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>07 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 08 · SPECIMEN -->
|
||||
<section class="slide specimen">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · SPECIMEN</div>
|
||||
<div>SLIDE <b>08 / 11</b></div>
|
||||
<div>STAGE · 07</div>
|
||||
<div>SECTION · TYPOGRAPHY</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">08 / single specimen · cause</span>
|
||||
<div class="display">DNS<span class="red">.</span></div>
|
||||
<p class="footnote">A three-letter root cause for an eleven-slide debrief — set in Archivo Black at clamp(140px, 22vw, 360px), tracking −0.06em, leading 0.82. The hazard period is the only part of this slide that is not phosphor white.</p>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0738</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>08 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 09 · ALERT -->
|
||||
<section class="slide alert-slide">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · ALERT</div>
|
||||
<div>SLIDE <b>09 / 11</b></div>
|
||||
<div>STAGE · 08</div>
|
||||
<div>SECTION · DECISION</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">09 / single decision · ratify</span>
|
||||
<div class="alertbox">
|
||||
<div class="glyph">!!<br>RTFY</div>
|
||||
<div>
|
||||
<h3>Ratify the resolver redesign at <span style="color: var(--hazard);">close of business 2026.05.16</span>.</h3>
|
||||
<p>If we delay the resolver redesign past Friday close, we re-enter the failure window with the same posture we left it in. The new policy is one ticket. The redesign is a fourteen-day commitment from Q.ALB & H.NAI. <strong>This deck is the ratification artefact.</strong></p>
|
||||
<p>Sign-off lines below. Anything not signed by 16.05.2026 17:00Z is escalated to the founders' weekly.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0739</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>09 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 10 · AUDIT LOG -->
|
||||
<section class="slide audit">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · AUDIT</div>
|
||||
<div>SLIDE <b>10 / 11</b></div>
|
||||
<div>STAGE · 09</div>
|
||||
<div>SECTION · LOG</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">10 / audit log · CIRC-04 (excerpt)</span>
|
||||
<h2 class="title">Audit log, <span class="red">verbatim</span>.</h2>
|
||||
<div class="log">
|
||||
<div class="row"><span class="ts">2026.05.13 03:21Z</span><span class="actor">PAGER</span><span class="ev crit">tier-3 failure rate > 5% · 4 engineers paged</span><span class="sig">sha · 9f3a…b218</span></div>
|
||||
<div class="row"><span class="ts">2026.05.13 03:24Z</span><span class="actor">Q.ALBRECHT</span><span class="ev">ack page · joined #incident-04</span><span class="sig">sha · 14ab…a022</span></div>
|
||||
<div class="row"><span class="ts">2026.05.13 03:38Z</span><span class="actor">Q.ALBRECHT</span><span class="ev crit">status page · "investigating elevated errors"</span><span class="sig">sha · 56cf…d971</span></div>
|
||||
<div class="row"><span class="ts">2026.05.13 04:01Z</span><span class="actor">H.NAITO</span><span class="ev">root cause narrowed · provider-04 DNS stale</span><span class="sig">sha · 04bb…f110</span></div>
|
||||
<div class="row"><span class="ts">2026.05.13 04:12Z</span><span class="actor">Q.ALBRECHT</span><span class="ev">manual failover provider-04 → provider-02 initiated</span><span class="sig">sha · 17ee…0ad4</span></div>
|
||||
<div class="row"><span class="ts">2026.05.13 04:30Z</span><span class="actor">P.NWACHUKWU</span><span class="ev">refund triage opened · 14,820 tasks queued</span><span class="sig">sha · ab21…8312</span></div>
|
||||
<div class="row"><span class="ts">2026.05.13 06:41Z</span><span class="actor">Q.ALBRECHT</span><span class="ev crit">all-clear posted · CIRC-04 opened</span><span class="sig">sha · cc09…b745</span></div>
|
||||
<div class="row"><span class="ts">2026.05.14 09:00Z</span><span class="actor">P.NWACHUKWU</span><span class="ev">refund pipeline complete · €4,840 across 312 customers</span><span class="sig">sha · 1a37…ee08</span></div>
|
||||
<div class="row"><span class="ts">2026.05.14 14:22Z</span><span class="actor">Q.ALBRECHT</span><span class="ev">postmortem CIRC-04 published · 11 action items</span><span class="sig">sha · 4f12…c399</span></div>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0740</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>10 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 11 · COLOPHON -->
|
||||
<section class="slide colophon-slide">
|
||||
<div class="topbar">
|
||||
<div><b>OPN-04</b> · COLOPHON</div>
|
||||
<div>SLIDE <b>11 / 11</b></div>
|
||||
<div>STAGE · 10</div>
|
||||
<div>SECTION · SIGN-OFF</div>
|
||||
<div class="red">CLASSIFIED · INTERNAL</div>
|
||||
<div>Q.ALBRECHT</div>
|
||||
</div>
|
||||
<span class="ascii-frame">11 / colophon & sign-off</span>
|
||||
<h2 class="title">Eleven slides, <span class="red">three names</span>, one decision.</h2>
|
||||
<div class="grid">
|
||||
<dl>
|
||||
<dt>Operator</dt><dd>Q. Albrecht · Incident Commander · Bordeaux, FR</dd>
|
||||
<dt>Witness</dt><dd>H. Naitō · Resolver Owner · Munich, DE</dd>
|
||||
<dt>Counsel</dt><dd>P. Nwachukwu · Customer Refund Pipeline · Lagos, NG</dd>
|
||||
<dt>Distribution</dt><dd>Internal · oncall · founders · CIRC-04</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>System</dt><dd>halcyon-runtime · build 2026.05.06</dd>
|
||||
<dt>Set in</dt><dd>Archivo Black · JetBrains Mono · IBM Plex Mono</dd>
|
||||
<dt>Press</dt><dd>Internal — 11pp · 16:9 · 2026.05.14 14:22Z</dd>
|
||||
<dt>Classification</dt><dd>INTERNAL — do not redistribute outside oncall</dd>
|
||||
<dt>Hash</dt><dd>OPN-04 · sha-256 · 4f12c399ab21d971...</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="signoff">
|
||||
<span>SIGNED Q.ALBRECHT · 2026.05.14 14:22Z</span>
|
||||
<span class="red">END OF TRANSMISSION ///</span>
|
||||
</div>
|
||||
<div class="botbar">
|
||||
<div class="seg"><span>SERIAL <b>OPN-04 / 0741</b></span></div>
|
||||
<div class="seg"><span>PAGE <b>11 / 11</b></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: html-ppt-taste-editorial
|
||||
description: 16:9 HTML deck in editorial-minimalist taste. Warm cream slides, serif display + grotesque body, hairline rules, monospace meta, generous macro-whitespace, one accent. Distilled from Leonxlnx/taste-skill `minimalist-skill`.
|
||||
---
|
||||
|
||||
# HTML PPT — Editorial Minimalism
|
||||
|
||||
A 16:9 deck for the briefs that hate neon: investor updates, design reviews, internal manifestos, lecture decks. Reads like a print supplement, not a SaaS landing.
|
||||
|
||||
## Source
|
||||
|
||||
Distilled from [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) — `skills/minimalist-skill/SKILL.md`. The deck system follows the existing project convention from `skills/html-ppt-pitch-deck/example.html` (each `.slide` is a `100vw × 100vh` section; opened directly, slides stack vertically). See `example.html` in this directory.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Substrate:** warm off-white `#FBFBFA` / `#F7F6F3`. Foreground off-black `#1A1A19`. Never pure white or pure black.
|
||||
- **Type pairing:** display in **serif** (Instrument Serif / Newsreader / Lyon), body in **grotesque** (Inter Tight / Switzer), meta in **mono** (JetBrains Mono).
|
||||
- **Display scale per slide:** title `clamp(56px, 6.5vw, 96px)` italic-capable serif, line-height `1.05`, tracking `-0.025em`.
|
||||
- **Hairline only:** `1px solid #EAEAEA` — borders, dividers, table cells. No drop shadows.
|
||||
- **One accent color** chosen from the muted-pastel pairs (e.g. sage `#346538` on `#EDF3EC`, or red `#9F2F2D` on `#FDEBEC`). Used sparingly — eyebrow dot, chart fill, call-out chip. Never as a slide background.
|
||||
- **Slide padding:** generous (`72px 96px` minimum). Title at most 14ch wide.
|
||||
- **Eyebrow:** every slide opens with a mono uppercase eyebrow `letter-spacing: 0.18em` and a section number `01 / 09`.
|
||||
- **Page numbers:** mono, bottom-right corner.
|
||||
|
||||
## Banned
|
||||
|
||||
- Inter (use Inter *Tight* if you must, but prefer Switzer / SF Pro). No Roboto, Open Sans.
|
||||
- Heavy drop shadows. Glow. Gradient text.
|
||||
- 3-equal-card feature rows. Use uneven hairline-divided columns instead.
|
||||
- Emojis in text or as bullet markers — use `—` or no marker.
|
||||
- Full-bleed photography on every slide. Use one or two image slides; reserve them.
|
||||
- AI-cliché copy ("Elevate", "Unleash", "Seamless", "Next-Gen").
|
||||
- Slide transitions noisier than fade-in.
|
||||
|
||||
## Required slide archetypes (10–12 total recommended)
|
||||
|
||||
1. **Cover** — serif title, italic mid-sentence accent, mono meta footer.
|
||||
2. **Eyebrow + thesis** — single sentence of body lede on the left; mono numbered TOC on the right.
|
||||
3. **Numbered manifesto** — three or four hairline-separated theses.
|
||||
4. **Bento data slide** — uneven 6-col grid with hairline gaps; one stat in serif, supporting in mono.
|
||||
5. **Quote / pull-out** — single sentence at large serif, attribution mono, hairline above and below.
|
||||
6. **Comparison** — two columns separated by a vertical hairline; "Doesn't / Does" or "Before / After".
|
||||
7. **Table or index** — `display: grid; gap: 1px` on hairline color.
|
||||
8. **Chart or breakdown** — flat horizontal bar chart with mono labels, accent fill only on the latest bar.
|
||||
9. **Team / colophon** — mono key-value list, no avatars.
|
||||
10. **Closing** — serif final line italic; CTA as ghost button; signature in mono.
|
||||
|
||||
## Motion
|
||||
|
||||
- Static-preview fallback: keep every slide visible (already wired by the deck base). When run as a real deck, fade-in at `400ms cubic-bezier(0.16, 1, 0.3, 1)` is plenty.
|
||||
- No translate, no blur, no auto-advance.
|
||||
|
||||
## Pre-flight
|
||||
|
||||
- [ ] Substrate is warm off-white; foreground is off-black; never pure black/white
|
||||
- [ ] Serif used on titles, grotesque on body, mono on meta — three families, three jobs
|
||||
- [ ] One accent color, used at most three times in the whole deck
|
||||
- [ ] Every slide has eyebrow + section number + page number
|
||||
- [ ] At least one hairline-grid table or comparison module
|
||||
- [ ] No drop shadows, no gradients, no emojis, no banned fonts
|
||||
@@ -0,0 +1,490 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Quartz · 2026 Series A Memo</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter+Tight:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--paper: #FBFBFA;
|
||||
--paper-2: #F4F3F0;
|
||||
--ink: #1A1A19;
|
||||
--ink-soft: #555452;
|
||||
--muted: #828079;
|
||||
--hairline: #E5E3DE;
|
||||
--hairline-soft: #EFEDE8;
|
||||
--accent: #346538;
|
||||
--accent-bg: #EDF3EC;
|
||||
--accent-2: #9F2F2D;
|
||||
--accent-2-bg: #FDEBEC;
|
||||
--display: 'Instrument Serif', 'Newsreader', Georgia, serif;
|
||||
--sans: 'Inter Tight', 'Switzer', 'SF Pro Display', system-ui, sans-serif;
|
||||
--mono: 'JetBrains Mono', 'Geist Mono', ui-monospace, monospace;
|
||||
--ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Deck system (matches html-ppt convention) */
|
||||
.deck { position: relative; width: 100vw; }
|
||||
.slide {
|
||||
position: relative;
|
||||
width: 100vw; height: 100vh; min-height: 720px;
|
||||
padding: 72px 96px;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
page-break-after: always;
|
||||
}
|
||||
.slide + .slide { border-top: 1px solid var(--hairline); }
|
||||
|
||||
/* Slide chrome */
|
||||
.meta-row {
|
||||
position: absolute; top: 32px; left: 96px; right: 96px;
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
font-family: var(--mono); font-size: 11px;
|
||||
letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted);
|
||||
}
|
||||
.meta-row .left { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.meta-row .dot { width: 6px; height: 6px; border-radius: 999px; background: var(--accent); }
|
||||
.meta-row .num { color: var(--ink); }
|
||||
.pagenum {
|
||||
position: absolute; bottom: 32px; right: 96px;
|
||||
font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted);
|
||||
}
|
||||
.footrule {
|
||||
position: absolute; bottom: 28px; left: 96px; right: 96px;
|
||||
border: 0; border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.signature {
|
||||
position: absolute; bottom: 32px; left: 96px;
|
||||
font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted);
|
||||
}
|
||||
|
||||
/* Display headings */
|
||||
h1.cover {
|
||||
font-family: var(--display);
|
||||
font-size: clamp(72px, 9vw, 132px);
|
||||
font-weight: 400;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.028em;
|
||||
margin: 0;
|
||||
max-width: 16ch;
|
||||
}
|
||||
h1.cover em { font-style: italic; color: var(--ink-soft); }
|
||||
h2.title {
|
||||
font-family: var(--display);
|
||||
font-size: clamp(54px, 6.5vw, 92px);
|
||||
font-weight: 400;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.025em;
|
||||
margin: 0;
|
||||
max-width: 14ch;
|
||||
}
|
||||
h2.title em { font-style: italic; color: var(--ink-soft); }
|
||||
h3.sub {
|
||||
font-family: var(--display); font-style: italic; font-weight: 400;
|
||||
font-size: 32px; line-height: 1.15; letter-spacing: -0.02em;
|
||||
color: var(--ink-soft); margin: 12px 0 0;
|
||||
}
|
||||
.lede {
|
||||
font-size: 21px; line-height: 1.5; color: var(--ink-soft);
|
||||
max-width: 56ch; margin: 0;
|
||||
}
|
||||
|
||||
/* Cover slide */
|
||||
.cover-slide { justify-content: center; }
|
||||
.cover-slide .lede { margin-top: 28px; max-width: 50ch; }
|
||||
.cover-slide .stamp {
|
||||
display: inline-block;
|
||||
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.22em; text-transform: uppercase;
|
||||
color: var(--accent); background: var(--accent-bg);
|
||||
padding: 4px 10px; border-radius: 999px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
/* TOC slide */
|
||||
.toc-slide { justify-content: center; }
|
||||
.toc-slide .columns { display: grid; grid-template-columns: 1fr 1fr; gap: 96px; align-items: start; padding-top: 48px; }
|
||||
.toc {
|
||||
list-style: none; padding: 0; margin: 0;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.toc li {
|
||||
display: grid; grid-template-columns: 4ch 1fr 4ch;
|
||||
align-items: baseline; gap: 18px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
font-size: 17px;
|
||||
}
|
||||
.toc li .n { font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; color: var(--muted); }
|
||||
.toc li .pg { font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; color: var(--muted); text-align: right; }
|
||||
.toc li .t { font-family: var(--display); font-size: 22px; letter-spacing: -0.012em; line-height: 1.2; }
|
||||
.toc li .t em { font-style: italic; color: var(--ink-soft); }
|
||||
|
||||
/* Manifesto slide */
|
||||
.manifesto .body {
|
||||
margin-top: 48px;
|
||||
display: grid; grid-template-columns: 1fr;
|
||||
}
|
||||
.manifesto .item {
|
||||
display: grid; grid-template-columns: 6ch 1fr 14ch;
|
||||
gap: 32px;
|
||||
padding: 22px 0;
|
||||
border-top: 1px solid var(--hairline);
|
||||
align-items: baseline;
|
||||
}
|
||||
.manifesto .item:last-child { border-bottom: 1px solid var(--hairline); }
|
||||
.manifesto .item .n {
|
||||
font-family: var(--display); font-size: 44px; line-height: 0.95; letter-spacing: -0.03em;
|
||||
}
|
||||
.manifesto .item h4 {
|
||||
font-family: var(--display); font-size: 26px; letter-spacing: -0.015em; line-height: 1.2;
|
||||
margin: 0 0 6px; font-weight: 400; max-width: 32ch;
|
||||
}
|
||||
.manifesto .item p { margin: 0; font-size: 14.5px; color: var(--ink-soft); max-width: 56ch; }
|
||||
.manifesto .item .tag {
|
||||
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase;
|
||||
color: var(--muted); text-align: right;
|
||||
}
|
||||
|
||||
/* Bento data slide */
|
||||
.bento-slide .grid {
|
||||
margin-top: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-auto-rows: minmax(180px, auto);
|
||||
gap: 0;
|
||||
border: 1px solid var(--hairline);
|
||||
background: var(--hairline);
|
||||
}
|
||||
.bento-slide .cell { background: var(--paper); padding: 28px 32px; }
|
||||
.cell-meta { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); margin-bottom: 14px; display: block; }
|
||||
.cell .stat { font-family: var(--display); font-size: 64px; line-height: 1; letter-spacing: -0.03em; font-weight: 400; }
|
||||
.cell .stat small { font-family: var(--sans); font-size: 14px; color: var(--muted); margin-left: 6px; }
|
||||
.cell h4 { font-family: var(--display); font-size: 22px; line-height: 1.2; letter-spacing: -0.015em; font-weight: 400; margin: 0 0 8px; }
|
||||
.cell p { font-size: 13.5px; color: var(--ink-soft); margin: 0; max-width: 32ch; line-height: 1.55; }
|
||||
.span-3 { grid-column: span 3; }
|
||||
.span-2 { grid-column: span 2; }
|
||||
.span-4 { grid-column: span 4; }
|
||||
.row-2 { grid-row: span 2; }
|
||||
|
||||
.delta { display: inline-block; font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; padding: 2px 7px; border-radius: 999px; background: var(--accent-bg); color: var(--accent); margin-top: 14px; }
|
||||
.delta.down { background: var(--accent-2-bg); color: var(--accent-2); }
|
||||
|
||||
/* Quote slide */
|
||||
.quote-slide { justify-content: center; text-align: left; }
|
||||
.quote-slide blockquote {
|
||||
margin: 0; padding: 38px 0;
|
||||
border-top: 1px solid var(--hairline);
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
font-family: var(--display); font-size: clamp(40px, 4.4vw, 64px); font-weight: 400;
|
||||
letter-spacing: -0.02em; line-height: 1.12; color: var(--ink);
|
||||
max-width: 26ch;
|
||||
}
|
||||
.quote-slide blockquote em { color: var(--ink-soft); font-style: italic; }
|
||||
.quote-slide cite {
|
||||
display: block; margin-top: 22px;
|
||||
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Comparison slide */
|
||||
.compare-slide .columns {
|
||||
margin-top: 56px;
|
||||
display: grid; grid-template-columns: 1fr 1fr;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.compare-slide .col { padding: 28px 0; }
|
||||
.compare-slide .col + .col { border-left: 1px solid var(--hairline); padding-left: 32px; }
|
||||
.compare-slide .col h5 {
|
||||
font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
|
||||
color: var(--muted); margin: 0 0 22px;
|
||||
}
|
||||
.compare-slide .col.against h5 { color: var(--accent-2); }
|
||||
.compare-slide .col.for h5 { color: var(--accent); }
|
||||
.compare-slide .col p {
|
||||
font-family: var(--display); font-weight: 400; font-size: 24px; line-height: 1.3; letter-spacing: -0.01em;
|
||||
margin: 0 0 18px; padding-bottom: 18px; border-bottom: 1px solid var(--hairline);
|
||||
max-width: 28ch;
|
||||
}
|
||||
.compare-slide .col p:last-child { border-bottom: none; }
|
||||
.compare-slide .col.against p { color: var(--ink-soft); text-decoration: line-through; text-decoration-thickness: 1px; }
|
||||
|
||||
/* Table slide */
|
||||
.table-slide .data {
|
||||
margin-top: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: 4ch 1.4fr 1fr 1fr 1fr 0.9fr;
|
||||
gap: 1px;
|
||||
background: var(--hairline);
|
||||
font-size: 14px;
|
||||
}
|
||||
.table-slide .data > div { background: var(--paper); padding: 14px 18px; }
|
||||
.table-slide .data .head {
|
||||
background: var(--ink); color: var(--paper);
|
||||
font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
|
||||
}
|
||||
.table-slide .data .right { text-align: right; }
|
||||
.table-slide .data .num { font-family: var(--mono); }
|
||||
.table-slide .data .pos { color: var(--accent); }
|
||||
.table-slide .data .neg { color: var(--accent-2); }
|
||||
|
||||
/* Chart slide */
|
||||
.chart-slide .chart {
|
||||
margin-top: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: 12ch 1fr 8ch;
|
||||
gap: 18px 24px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.chart-slide .chart .label { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); }
|
||||
.chart-slide .chart .bar {
|
||||
height: 20px; background: var(--paper-2); position: relative;
|
||||
border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
.chart-slide .chart .bar::after {
|
||||
content: ''; position: absolute; left: 0; top: 0; bottom: 0;
|
||||
width: var(--w, 0%); background: var(--ink-soft);
|
||||
}
|
||||
.chart-slide .chart .bar.accent::after { background: var(--accent); }
|
||||
.chart-slide .chart .v { font-family: var(--mono); font-size: 13px; text-align: right; letter-spacing: 0.02em; }
|
||||
|
||||
/* Colophon */
|
||||
.colophon-slide .grid { margin-top: 56px; display: grid; grid-template-columns: 1fr 1fr; gap: 64px; }
|
||||
.colophon-slide dl {
|
||||
margin: 0; display: grid; grid-template-columns: 14ch 1fr; gap: 12px 18px;
|
||||
font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.06em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.colophon-slide dt { color: var(--muted); text-transform: uppercase; letter-spacing: 0.16em; font-size: 11px; }
|
||||
.colophon-slide dd { margin: 0; color: var(--ink); }
|
||||
.colophon-slide dd em { color: var(--ink-soft); font-style: normal; }
|
||||
.colophon-slide dl + dl { border-top: 1px solid var(--hairline); padding-top: 22px; }
|
||||
|
||||
/* Closing */
|
||||
.closing-slide { justify-content: center; text-align: left; }
|
||||
.closing-slide h2 {
|
||||
font-family: var(--display); font-style: italic; font-weight: 400;
|
||||
font-size: clamp(64px, 8vw, 120px); line-height: 1;
|
||||
letter-spacing: -0.025em; margin: 0; max-width: 18ch;
|
||||
}
|
||||
.closing-slide h2 b { font-weight: 400; font-style: normal; color: var(--ink); }
|
||||
.closing-slide .row { display: flex; gap: 14px; margin-top: 38px; align-items: center; }
|
||||
.ghost-cta {
|
||||
font-family: var(--sans); font-weight: 500; font-size: 14px;
|
||||
padding: 12px 22px; border: 1px solid var(--hairline);
|
||||
border-radius: 8px; background: transparent; color: var(--ink); cursor: pointer;
|
||||
transition: background 200ms var(--ease);
|
||||
}
|
||||
.ghost-cta.solid { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
||||
.ghost-cta:hover { background: var(--paper-2); }
|
||||
.ghost-cta.solid:hover { background: #2A2A28; }
|
||||
|
||||
/* Print / preview */
|
||||
@media print {
|
||||
.slide { height: auto; min-height: 100vh; page-break-after: always; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="deck">
|
||||
|
||||
<!-- 01 · Cover -->
|
||||
<section class="slide cover-slide" data-title="Cover">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">QUARTZ · MEMO 04 · 2026</span></span><span>SERIES A · CONFIDENTIAL</span></div>
|
||||
<span class="stamp">— filed 14 may 2026</span>
|
||||
<h1 class="cover">A quiet workspace, <em>handed</em> to the people who write the manuals.</h1>
|
||||
<h3 class="sub">Series A memo — for an audience of three.</h3>
|
||||
<p class="lede" style="margin-top: 28px;">We are raising six and a half million euros to spend the next eighteen months making documentation feel like writing again. This deck is the short version. The longer version lives in the manual.</p>
|
||||
<span class="signature">Q. Albrecht · CEO · q@quartz.press</span>
|
||||
<span class="pagenum">01 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 02 · Thesis & TOC -->
|
||||
<section class="slide toc-slide" data-title="Thesis & TOC">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">02 · thesis & agenda</span></span><span>q. albrecht</span></div>
|
||||
<div class="columns">
|
||||
<div>
|
||||
<h2 class="title">The world has enough <em>note-taking</em> apps. It has too few <em>writing</em> ones.</h2>
|
||||
<p class="lede" style="margin-top: 28px;">Quartz is a workspace for technical writers — the people who keep the manuals, the changelogs, the runbooks, the policy docs. They write the longest documents in any company and use the worst tools to do it.</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="toc">
|
||||
<li><span class="n">01</span><span class="t">Cover</span><span class="pg">p. 01</span></li>
|
||||
<li><span class="n">02</span><span class="t">Thesis & agenda</span><span class="pg">p. 02</span></li>
|
||||
<li><span class="n">03</span><span class="t">Six theses on a <em>quieter</em> doc tool</span><span class="pg">p. 03</span></li>
|
||||
<li><span class="n">04</span><span class="t">Where we are, in numbers</span><span class="pg">p. 04</span></li>
|
||||
<li><span class="n">05</span><span class="t">A line we believe in</span><span class="pg">p. 05</span></li>
|
||||
<li><span class="n">06</span><span class="t">What it isn't / what it is</span><span class="pg">p. 06</span></li>
|
||||
<li><span class="n">07</span><span class="t">Customers, in a row</span><span class="pg">p. 07</span></li>
|
||||
<li><span class="n">08</span><span class="t">ARR — the long way</span><span class="pg">p. 08</span></li>
|
||||
<li><span class="n">09</span><span class="t">Colophon</span><span class="pg">p. 09</span></li>
|
||||
<li><span class="n">10</span><span class="t">The ask</span><span class="pg">p. 10</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">02 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 03 · Manifesto -->
|
||||
<section class="slide manifesto" data-title="Six theses">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">03 · manifest</span></span><span>section · product</span></div>
|
||||
<h2 class="title">Six theses on a <em>quieter</em> doc tool.</h2>
|
||||
<div class="body">
|
||||
<div class="item"><div class="n">01</div><div><h4>The page is the unit, not the block.</h4><p>If the editor reads like a database, the doc reads like one. Quartz writes Markdown to disk and treats the page as a single object — not 142 nested toggles.</p></div><div class="tag">— editor</div></div>
|
||||
<div class="item"><div class="n">02</div><div><h4>Review belongs inline. Not in Slack.</h4><p>Comments anchor to a line, resolve to an audit log, never spawn a thread that nobody can find two weeks later.</p></div><div class="tag">— review</div></div>
|
||||
<div class="item"><div class="n">03</div><div><h4>History is a feature, not a tab.</h4><p>Every save is a commit. Diff two revisions in three keystrokes. Roll back without writing a support ticket.</p></div><div class="tag">— history</div></div>
|
||||
<div class="item"><div class="n">04</div><div><h4>Publishing is not a separate product.</h4><p>The same page is a draft, a review, and a published doc — by changing one field, not by exporting to a third-party site builder.</p></div><div class="tag">— publish</div></div>
|
||||
</div>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">03 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 04 · Bento data slide -->
|
||||
<section class="slide bento-slide" data-title="Where we are">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">04 · in numbers</span></span><span>section · traction</span></div>
|
||||
<h2 class="title">Where we are, in <em>numbers</em>.</h2>
|
||||
<div class="grid">
|
||||
<div class="cell span-3 row-2">
|
||||
<span class="cell-meta">paying teams · live</span>
|
||||
<div class="stat">147<small>teams</small></div>
|
||||
<span class="delta">▲ +38 this quarter · +35%</span>
|
||||
<p style="margin-top: 18px;">Including engineering and policy teams at four publicly-listed European companies. Median seat count is twelve; the long tail is solo writers paying out of pocket.</p>
|
||||
</div>
|
||||
<div class="cell span-3"><span class="cell-meta">arr · annual run rate</span><div class="stat">€842k<small>ARR</small></div><span class="delta">▲ +29% qoq</span></div>
|
||||
<div class="cell span-2"><span class="cell-meta">net retention</span><div class="stat">131<small>%</small></div></div>
|
||||
<div class="cell span-2"><span class="cell-meta">gross margin</span><div class="stat">88<small>%</small></div></div>
|
||||
<div class="cell span-2"><span class="cell-meta">churn · monthly logo</span><div class="stat">0.7<small>%</small></div><span class="delta down">▼ from 1.4% in Jan</span></div>
|
||||
</div>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">04 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 05 · Quote -->
|
||||
<section class="slide quote-slide" data-title="Quote">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">05 · pull quote</span></span><span>customer · pilot 04</span></div>
|
||||
<blockquote>
|
||||
We replaced four tools with Quartz. Confluence for the docs, Notion for the drafts, Google Docs for the review, and Slack for the panic <em>that the doc nobody had read was wrong.</em>
|
||||
</blockquote>
|
||||
<cite>— Hester Naitō, principal engineer · Pilot 04 · Munich</cite>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">05 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 06 · Comparison -->
|
||||
<section class="slide compare-slide" data-title="What it isn't">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">06 · positioning</span></span><span>section · product</span></div>
|
||||
<h2 class="title">What it <em>isn't</em>. What it <em>is</em>.</h2>
|
||||
<div class="columns">
|
||||
<div class="col against">
|
||||
<h5>— Quartz isn't</h5>
|
||||
<p>A wiki you have to maintain a sidebar for.</p>
|
||||
<p>A knowledge graph nobody asked for.</p>
|
||||
<p>An AI that suggests the third callout on the page.</p>
|
||||
<p>A static-site generator with a CMS bolted on.</p>
|
||||
</div>
|
||||
<div class="col for">
|
||||
<h5>— Quartz is</h5>
|
||||
<p>A page. A title. A column measure that the writer can trust.</p>
|
||||
<p>A history of every save and a diff between any two of them.</p>
|
||||
<p>A line of comments anchored to the line they critique, archived when resolved.</p>
|
||||
<p>Markdown out, Markdown in. Always.</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">06 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 07 · Table -->
|
||||
<section class="slide table-slide" data-title="Customers">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">07 · customer index</span></span><span>section · gtm</span></div>
|
||||
<h2 class="title">Customers, in <em>a row</em>.</h2>
|
||||
<div class="data">
|
||||
<div class="head">№</div><div class="head">team</div><div class="head">country</div><div class="head">seats</div><div class="head right">arr</div><div class="head right">since</div>
|
||||
|
||||
<div>01</div><div>Albrecht Press</div><div>FR</div><div>4</div><div class="right num">€ 4,720</div><div class="right num">22.04</div>
|
||||
<div>02</div><div>Naitō Robotics — Eng. docs</div><div>JP / DE</div><div>18</div><div class="right num pos">€ 41,280</div><div class="right num">23.10</div>
|
||||
<div>03</div><div>Andrejević Atelier</div><div>HR</div><div>3</div><div class="right num">€ 3,180</div><div class="right num">24.01</div>
|
||||
<div>04</div><div>Nwachukwu Ltd. — Policy</div><div>NG / UK</div><div>11</div><div class="right num">€ 18,640</div><div class="right num">24.06</div>
|
||||
<div>05</div><div>Arroyave & Bros</div><div>CO</div><div>7</div><div class="right num pos">€ 9,840</div><div class="right num">25.02</div>
|
||||
<div>06</div><div>Quentin Veterinary, S.A.</div><div>FR</div><div>2</div><div class="right num neg">€ 1,920</div><div class="right num">25.04</div>
|
||||
</div>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">07 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 08 · Chart -->
|
||||
<section class="slide chart-slide" data-title="ARR">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">08 · arr · the long way</span></span><span>section · finance</span></div>
|
||||
<h2 class="title">ARR — <em>the long way</em>.</h2>
|
||||
<div class="chart">
|
||||
<span class="label">Q1 / 2024</span><div class="bar" style="--w: 9%;"></div><span class="v">€ 78k</span>
|
||||
<span class="label">Q2 / 2024</span><div class="bar" style="--w: 14%;"></div><span class="v">€ 121k</span>
|
||||
<span class="label">Q3 / 2024</span><div class="bar" style="--w: 22%;"></div><span class="v">€ 187k</span>
|
||||
<span class="label">Q4 / 2024</span><div class="bar" style="--w: 32%;"></div><span class="v">€ 274k</span>
|
||||
<span class="label">Q1 / 2025</span><div class="bar" style="--w: 44%;"></div><span class="v">€ 372k</span>
|
||||
<span class="label">Q2 / 2025</span><div class="bar" style="--w: 58%;"></div><span class="v">€ 491k</span>
|
||||
<span class="label">Q3 / 2025</span><div class="bar" style="--w: 71%;"></div><span class="v">€ 603k</span>
|
||||
<span class="label">Q4 / 2025</span><div class="bar" style="--w: 83%;"></div><span class="v">€ 706k</span>
|
||||
<span class="label">Q1 / 2026</span><div class="bar accent" style="--w: 99%;"></div><span class="v">€ 842k</span>
|
||||
</div>
|
||||
<p class="lede" style="margin-top: 56px; max-width: 60ch; font-size: 15px;">
|
||||
No paid acquisition since Q3 2024. Growth comes from word-of-mouth between docs teams; ARR adds an Albrecht Press every five working days.
|
||||
</p>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">08 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 09 · Colophon -->
|
||||
<section class="slide colophon-slide" data-title="Colophon">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">09 · colophon</span></span><span>section · team</span></div>
|
||||
<h2 class="title">A small press. A long manual.</h2>
|
||||
<div class="grid">
|
||||
<dl>
|
||||
<dt>founder</dt><dd>Quentin Albrecht <em>· prev. lead writer, IETF working group on documentation tooling</em></dd>
|
||||
<dt>co-founder</dt><dd>Hester Naitō <em>· prev. principal engineer, Naitō Robotics</em></dd>
|
||||
<dt>head of design</dt><dd>Margerit Andrejević <em>· prev. typographer, Atelier Nord-Ouest</em></dd>
|
||||
<dt>policy / legal</dt><dd>Pemberton Nwachukwu <em>· prev. counsel, IBM Africa</em></dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>set in</dt><dd>Instrument Serif · Inter Tight · JetBrains Mono</dd>
|
||||
<dt>press</dt><dd>Atelier Nord-Ouest · Bordeaux</dd>
|
||||
<dt>edition</dt><dd>04 · v 2026.05 · 2,400 numbered</dd>
|
||||
<dt>contact</dt><dd>q@quartz.press · +33 (0)5 56 21 47 88</dd>
|
||||
<dt>auditor</dt><dd>BDO France — Q1 2026 review complete</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">09 / 10</span>
|
||||
</section>
|
||||
|
||||
<!-- 10 · Closing -->
|
||||
<section class="slide closing-slide" data-title="The ask">
|
||||
<div class="meta-row"><span class="left"><span class="dot"></span><span class="num">10 · the ask</span></span><span>series a · 2026</span></div>
|
||||
<h2><b>Six and a half million euros</b>, <em>to spend the next eighteen months making documentation feel like writing again.</em></h2>
|
||||
<p class="lede" style="margin-top: 32px; max-width: 64ch;">€2.4M to ship the publishing pipeline. €1.8M to grow a sales team of four. €1.4M to extend runway to thirty months. €0.9M to ratify our SOC 2 and translate the manual.</p>
|
||||
<div class="row">
|
||||
<button class="ghost-cta solid">Open the manual</button>
|
||||
<button class="ghost-cta">q@quartz.press</button>
|
||||
<span style="font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); margin-left: auto;">— end · 2026.05.14</span>
|
||||
</div>
|
||||
<hr class="footrule">
|
||||
<span class="pagenum">10 / 10</span>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-tech-sharing
|
||||
description: Conference / internal tech-talk deck — GitHub-dark, JetBrains Mono, terminal code blocks, agenda + Q&A pages. Use for engineering presentations, internal sharing sessions, conference talks, and code-heavy walkthroughs.
|
||||
triggers:
|
||||
- "tech sharing"
|
||||
- "tech talk"
|
||||
- "技术分享"
|
||||
- "engineering talk"
|
||||
- "conference talk"
|
||||
- "dev talk"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: engineering
|
||||
featured: 22
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "帮我用 html-ppt-tech-sharing 模板做一份 8 页的技术分享 PPT。先确认:分享主题、目标听众(同事 / 社区 / 客户)、要不要包含代码片段和 benchmark。GitHub 暗色主题 + JetBrains Mono,agenda + Q&A 页备好。"
|
||||
---
|
||||
# HTML PPT · Tech Sharing
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`tech-sharing`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `tech-sharing` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/tech-sharing/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-tech-sharing` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-tech-sharing` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,512 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Rust 异步运行时内部机制 · Tech Sharing</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* tech-sharing — 技术分享 dark, code-forward */
|
||||
.tpl-tech-sharing{
|
||||
--bg:#0d1117;--bg-soft:#161b22;--surface:#161b22;--surface-2:#1c2230;
|
||||
--border:rgba(139,148,158,.22);--border-strong:rgba(139,148,158,.4);
|
||||
--text-1:#e6edf3;--text-2:#8b949e;--text-3:#6e7681;
|
||||
--accent:#7ee787;--accent-2:#79c0ff;--accent-3:#ff7b72;
|
||||
--grad:linear-gradient(120deg,#7ee787 0%,#79c0ff 60%,#d2a8ff 100%);
|
||||
--radius:14px;--radius-lg:20px;
|
||||
--shadow:0 20px 60px rgba(0,0,0,.5);
|
||||
font-family:'Inter','Noto Sans SC',sans-serif;
|
||||
}
|
||||
.tpl-tech-sharing{background:#0d1117;color:var(--text-1)}
|
||||
.tpl-tech-sharing .slide{padding:72px 96px;background:#0d1117;color:var(--text-1)}
|
||||
.tpl-tech-sharing .slide::before{content:"";position:absolute;inset:0;background:
|
||||
radial-gradient(60% 50% at 90% 10%,rgba(121,192,255,.12),transparent 60%),
|
||||
radial-gradient(50% 50% at 10% 90%,rgba(126,231,135,.08),transparent 60%);
|
||||
pointer-events:none;z-index:0}
|
||||
.tpl-tech-sharing .slide>*{position:relative;z-index:1}
|
||||
.tpl-tech-sharing .h1{font-size:78px;line-height:1.03;font-weight:800;letter-spacing:-.03em;color:#fff}
|
||||
.tpl-tech-sharing .h2{font-size:54px;font-weight:700;letter-spacing:-.025em;color:#fff}
|
||||
.tpl-tech-sharing h3,.tpl-tech-sharing h4{color:#fff}
|
||||
.tpl-tech-sharing .kicker{color:var(--accent);font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:600;text-transform:none;letter-spacing:.02em}
|
||||
.tpl-tech-sharing .kicker::before{content:"> "}
|
||||
.tpl-tech-sharing .mono{font-family:'JetBrains Mono','IBM Plex Mono',monospace}
|
||||
.tpl-tech-sharing .terminal{background:#010409;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.6);font-family:'JetBrains Mono',monospace;font-size:15px;line-height:1.65}
|
||||
.tpl-tech-sharing .terminal .bar{display:flex;align-items:center;gap:8px;padding:12px 16px;background:#161b22;border-bottom:1px solid var(--border);font-size:12px;color:var(--text-3)}
|
||||
.tpl-tech-sharing .terminal .dot{width:12px;height:12px;border-radius:50%;background:#ff5f56}
|
||||
.tpl-tech-sharing .terminal .dot:nth-child(2){background:#ffbd2e}
|
||||
.tpl-tech-sharing .terminal .dot:nth-child(3){background:#27c93f}
|
||||
.tpl-tech-sharing .terminal pre{margin:0;padding:24px 28px;color:#e6edf3;overflow:auto;max-height:440px}
|
||||
.tpl-tech-sharing .kw{color:#ff7b72}
|
||||
.tpl-tech-sharing .fn{color:#d2a8ff}
|
||||
.tpl-tech-sharing .str{color:#a5d6ff}
|
||||
.tpl-tech-sharing .cmt{color:#8b949e;font-style:italic}
|
||||
.tpl-tech-sharing .num{color:#79c0ff}
|
||||
.tpl-tech-sharing .card{background:var(--surface);border:1px solid var(--border);box-shadow:none}
|
||||
.tpl-tech-sharing .card-accent{border-top:3px solid var(--accent)}
|
||||
.tpl-tech-sharing .pill{background:var(--surface-2);color:var(--text-2);border-color:var(--border)}
|
||||
.tpl-tech-sharing .pill-accent{background:rgba(126,231,135,.12);color:var(--accent);border-color:rgba(126,231,135,.35)}
|
||||
.tpl-tech-sharing .tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--surface-2);border:1px solid var(--border);color:var(--text-2)}
|
||||
.tpl-tech-sharing .agenda-row{display:flex;align-items:baseline;gap:24px;padding:18px 0;border-bottom:1px dashed var(--border);font-family:'JetBrains Mono',monospace}
|
||||
.tpl-tech-sharing .agenda-row .num{color:var(--accent);flex:none;width:48px}
|
||||
.tpl-tech-sharing .agenda-row .t{color:#fff;font-size:24px;flex:1;font-family:'Inter',sans-serif;font-weight:600}
|
||||
.tpl-tech-sharing .agenda-row .d{color:var(--text-3);font-size:13px}
|
||||
.tpl-tech-sharing .speaker{display:flex;align-items:center;gap:14px;margin-top:28px}
|
||||
.tpl-tech-sharing .speaker .av{width:56px;height:56px;border-radius:50%;background:var(--grad)}
|
||||
.tpl-tech-sharing .speaker b{display:block;color:#fff;font-size:18px}
|
||||
.tpl-tech-sharing .speaker span{color:var(--text-3);font-size:13px;font-family:'JetBrains Mono',monospace}
|
||||
.tpl-tech-sharing .lede{color:var(--text-2)}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-tech-sharing">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover -->
|
||||
<section class="slide" data-title="Cover">
|
||||
<p class="kicker">tech-sharing / 2026-04-15</p>
|
||||
<h1 class="h1 anim-fade-up" data-anim="fade-up">Rust 异步运行时<br>到底在<span style="background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent">调度什么</span>?</h1>
|
||||
<p class="lede mt-m">从 <span class="mono">Future::poll</span> 到 tokio 的 work-stealing,一次讲清楚。</p>
|
||||
<div class="speaker"><div class="av"></div><div><b>@lewis</b><span>platform infra · 45 min + Q&A</span></div></div>
|
||||
<div class="deck-footer"><span class="mono">#async #rust #tokio</span><span class="slide-number" data-current="1" data-total="8"></span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Agenda -->
|
||||
<section class="slide" data-title="Agenda">
|
||||
<p class="kicker">agenda.toml</p>
|
||||
<h2 class="h2">今天的路线图</h2>
|
||||
<div class="stack mt-l">
|
||||
<div class="agenda-row"><span class="num">01</span><span class="t">Context: 为什么需要 async</span><span class="d">~5min</span></div>
|
||||
<div class="agenda-row"><span class="num">02</span><span class="t">Deep dive 1: Future & Waker</span><span class="d">~12min</span></div>
|
||||
<div class="agenda-row"><span class="num">03</span><span class="t">Deep dive 2: Tokio scheduler</span><span class="d">~15min</span></div>
|
||||
<div class="agenda-row"><span class="num">04</span><span class="t">Code: 手写一个 mini-runtime</span><span class="d">~8min</span></div>
|
||||
<div class="agenda-row"><span class="num">05</span><span class="t">Takeaways + Q&A</span><span class="d">~5min</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Context -->
|
||||
<section class="slide" data-title="Context">
|
||||
<p class="kicker">// context</p>
|
||||
<h2 class="h2">问题:一个线程一个连接,<br>撑不住 10 万并发。</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="card card-accent"><h4>Thread-per-conn</h4><p class="dim">每条连接一根 OS 线程,栈 2–8MB。10 万连接 = 几百 GB RAM。</p><span class="tag mt-s">❌ 不现实</span></div>
|
||||
<div class="card card-accent"><h4>Event loop (C)</h4><p class="dim">epoll/kqueue + 回调地狱。快,但写起来痛苦且容易出 bug。</p><span class="tag mt-s">😩 callback hell</span></div>
|
||||
<div class="card card-accent"><h4>Async / await</h4><p class="dim">看起来像同步代码,编译成状态机。一根线程跑几千任务。</p><span class="tag mt-s">✅ Rust 选这个</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Deep dive 1 -->
|
||||
<section class="slide" data-title="Deep Dive 1">
|
||||
<p class="kicker">deep-dive · 1 / 2</p>
|
||||
<h2 class="h2">Future 其实只有一个方法。</h2>
|
||||
<div class="grid g2 mt-l" style="align-items:start">
|
||||
<div>
|
||||
<p class="lede">编译器把 <span class="mono">async fn</span> 变成一个实现了 <span class="mono">Future</span> trait 的匿名状态机。运行时只做一件事:反复 <span class="mono">poll</span> 它,直到返回 <span class="mono">Ready</span>。</p>
|
||||
<div class="mt-l">
|
||||
<span class="tag">Pending</span> <span class="tag">Ready(T)</span> <span class="tag">Waker.wake()</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal">
|
||||
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span>future.rs</span></div>
|
||||
<pre><span class="kw">pub trait</span> <span class="fn">Future</span> {
|
||||
<span class="kw">type</span> Output;
|
||||
<span class="kw">fn</span> <span class="fn">poll</span>(
|
||||
<span class="kw">self</span>: Pin<&<span class="kw">mut Self</span>>,
|
||||
cx: &<span class="kw">mut</span> Context<<span class="str">'_</span>>,
|
||||
) -> Poll<<span class="kw">Self</span>::Output>;
|
||||
}
|
||||
|
||||
<span class="cmt">// Poll::Pending → 挂起,等 waker 唤醒</span>
|
||||
<span class="cmt">// Poll::Ready(v) → 完成,产出 v</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Deep dive 2 -->
|
||||
<section class="slide" data-title="Deep Dive 2">
|
||||
<p class="kicker">deep-dive · 2 / 2</p>
|
||||
<h2 class="h2">Tokio 是一个偷任务的小工。</h2>
|
||||
<div class="grid g2 mt-l" style="align-items:start">
|
||||
<div>
|
||||
<p class="lede">Multi-thread runtime = N 个 worker,每个 worker 有自己的本地队列。空闲的 worker 会去别人队列里"偷"任务。</p>
|
||||
<div class="stack mt-m">
|
||||
<div class="tag">✦ local queue · 256 slots</div>
|
||||
<div class="tag">✦ global injection queue</div>
|
||||
<div class="tag">✦ work-stealing @ 50% steal ratio</div>
|
||||
<div class="tag">✦ LIFO slot for cache locality</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="padding:32px">
|
||||
<h4 class="mono" style="color:var(--accent-2)">scheduler tick loop</h4>
|
||||
<div class="stack mt-m" style="font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.9;color:var(--text-2)">
|
||||
<div><span style="color:var(--accent)">1.</span> pop from LIFO slot</div>
|
||||
<div><span style="color:var(--accent)">2.</span> else pop from local queue</div>
|
||||
<div><span style="color:var(--accent)">3.</span> else drain global queue (every 61 ticks)</div>
|
||||
<div><span style="color:var(--accent)">4.</span> else steal from random victim</div>
|
||||
<div><span style="color:var(--accent)">5.</span> else park the thread</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Code example -->
|
||||
<section class="slide" data-title="Code">
|
||||
<p class="kicker">mini-runtime.rs · ~40 LOC</p>
|
||||
<h2 class="h2">手写一个最小 runtime。</h2>
|
||||
<div class="terminal mt-m">
|
||||
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span>src/main.rs</span></div>
|
||||
<pre><span class="kw">use</span> std::collections::VecDeque;
|
||||
<span class="kw">use</span> std::sync::{Arc, Mutex};
|
||||
<span class="kw">use</span> std::task::{Context, Poll, Wake, Waker};
|
||||
|
||||
<span class="kw">struct</span> Task(Mutex<Pin<Box<<span class="kw">dyn</span> Future<Output = ()> + Send>>>);
|
||||
|
||||
<span class="kw">impl</span> Wake <span class="kw">for</span> Task {
|
||||
<span class="kw">fn</span> <span class="fn">wake</span>(<span class="kw">self</span>: Arc<<span class="kw">Self</span>>) { QUEUE.lock().unwrap().push_back(<span class="kw">self</span>); }
|
||||
}
|
||||
|
||||
<span class="kw">fn</span> <span class="fn">block_on</span><F: Future<Output = ()> + Send + <span class="str">'static</span>>(fut: F) {
|
||||
<span class="fn">spawn</span>(fut);
|
||||
<span class="kw">while let Some</span>(task) = QUEUE.lock().unwrap().pop_front() {
|
||||
<span class="kw">let</span> waker = Waker::from(task.clone());
|
||||
<span class="kw">let mut</span> cx = Context::from_waker(&waker);
|
||||
<span class="kw">let mut</span> fut = task.<span class="num">0</span>.lock().unwrap();
|
||||
<span class="kw">let</span> _ = fut.as_mut().<span class="fn">poll</span>(&<span class="kw">mut</span> cx); <span class="cmt">// 就是这一行</span>
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. Takeaways -->
|
||||
<section class="slide" data-title="Takeaways">
|
||||
<p class="kicker">// takeaways</p>
|
||||
<h2 class="h2">三件事带回去。</h2>
|
||||
<div class="grid g3 mt-l">
|
||||
<div class="card card-accent"><h4>1 · async 是零成本抽象</h4><p class="dim">编译成状态机,没有运行时虚表,没有 GC。</p></div>
|
||||
<div class="card card-accent"><h4>2 · Waker 是脉搏</h4><p class="dim">Future 不主动做事,运行时靠 waker 决定"什么时候再 poll"。</p></div>
|
||||
<div class="card card-accent"><h4>3 · 别在 async 里阻塞</h4><p class="dim">一行 <span class="mono">std::fs::read</span> 能让整个 worker 停摆。用 <span class="mono">spawn_blocking</span>。</p></div>
|
||||
</div>
|
||||
<p class="lede mt-l">延伸阅读:<span class="mono">tokio.rs/blog/2019-10-scheduler</span> · <span class="mono">rust-lang.github.io/async-book</span></p>
|
||||
</section>
|
||||
|
||||
<!-- 8. Q&A -->
|
||||
<section class="slide center tc" data-title="Q and A">
|
||||
<div>
|
||||
<div class="mono" style="font-size:120px;color:var(--accent);font-weight:800;letter-spacing:-.04em">?</div>
|
||||
<h2 class="h2">Questions?</h2>
|
||||
<p class="lede" style="margin:14px auto">github.com/lewis · @lewis on slack</p>
|
||||
<div class="row mt-l" style="justify-content:center">
|
||||
<span class="tag">slides: git.co/rt-deck</span>
|
||||
<span class="tag">code: git.co/mini-rt</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: html-ppt-testing-safety-alert
|
||||
description: 红琥珀警示 deck — 顶/底 45° 红黑 hazard 条纹、红色删除线否定标题、L1/L2/L3 绿/琥珀/红 tier 卡片、圆点状态 alert box、policy-yaml 代码块(红左边框 + bad 关键词高亮)、红绿 checklist、Q1 事故堆叠柱状图。适合安全 / 风险 / 事故复盘 / 红队 / 上线前 AI 评审 / policy-as-code。
|
||||
triggers:
|
||||
- "safety alert"
|
||||
- "incident"
|
||||
- "red team"
|
||||
- "risk review"
|
||||
- "事故复盘"
|
||||
- "安全评审"
|
||||
- "policy as code"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: engineering
|
||||
featured: 32
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-testing-safety-alert 模板做一份事故复盘 / 安全评审 PPT。红黑 hazard 条 + 红色删除线 + L1/L2/L3 tier 卡片 + policy-yaml 代码块。先告诉我事件时间线、根因、影响范围。"
|
||||
---
|
||||
# HTML PPT · 红琥珀警示
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`testing-safety-alert`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `testing-safety-alert` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/testing-safety-alert/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-testing-safety-alert` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-testing-safety-alert` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,413 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Testing Safety Alert</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* testing-safety-alert — 红/琥珀 警示风 · 白底高对比 */
|
||||
.tpl-testing-safety-alert{
|
||||
--ts-bg:#fffaf7;
|
||||
--ts-ink:#14141a;
|
||||
--ts-ink2:#4a4955;
|
||||
--ts-muted:#8a8892;
|
||||
--ts-line:rgba(20,20,26,.08);
|
||||
--ts-red:#e0314a;
|
||||
--ts-red-soft:#ffecee;
|
||||
--ts-amber:#d97706;
|
||||
--ts-amber-soft:#fff5e6;
|
||||
--ts-green:#067647;
|
||||
--ts-green-soft:#e8f8ee;
|
||||
background:var(--ts-bg);
|
||||
color:var(--ts-ink);
|
||||
font-family:'Inter','Noto Sans SC','PingFang SC',-apple-system,sans-serif;
|
||||
}
|
||||
.tpl-testing-safety-alert .slide{background:var(--ts-bg);color:var(--ts-ink);padding:64px 84px}
|
||||
.tpl-testing-safety-alert .ts-stripe{position:absolute;top:0;left:0;right:0;height:14px;background:repeating-linear-gradient(45deg,var(--ts-red) 0 18px,#111318 18px 36px)}
|
||||
.tpl-testing-safety-alert .ts-stripe-b{position:absolute;bottom:0;left:0;right:0;height:6px;background:repeating-linear-gradient(45deg,var(--ts-red) 0 10px,#111318 10px 20px);opacity:.6}
|
||||
.tpl-testing-safety-alert .ts-chrome{display:flex;justify-content:space-between;align-items:center;margin:22px 0 16px}
|
||||
.tpl-testing-safety-alert .ts-alert-tag{display:inline-flex;align-items:center;gap:10px;padding:8px 18px;border-radius:10px;font-size:13px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;background:var(--ts-red);color:#fff;box-shadow:0 6px 18px rgba(224,49,74,.28)}
|
||||
.tpl-testing-safety-alert .ts-alert-tag::before{content:'⚠';font-size:16px}
|
||||
.tpl-testing-safety-alert .ts-alert-tag.amber{background:var(--ts-amber);box-shadow:0 6px 18px rgba(217,119,6,.25)}
|
||||
.tpl-testing-safety-alert .ts-alert-tag.green{background:var(--ts-green);box-shadow:0 6px 18px rgba(6,118,71,.22)}
|
||||
.tpl-testing-safety-alert .ts-alert-tag.green::before{content:'✓'}
|
||||
.tpl-testing-safety-alert .ts-page{font-size:13px;color:var(--ts-muted);letter-spacing:.15em;font-weight:700}
|
||||
.tpl-testing-safety-alert .ts-kicker{font-size:15px;font-weight:700;color:var(--ts-red);letter-spacing:.06em;margin-bottom:10px;text-transform:uppercase}
|
||||
.tpl-testing-safety-alert .ts-h1{font-size:88px;font-weight:900;line-height:1.04;letter-spacing:-2px;margin:10px 0 16px;color:var(--ts-ink)}
|
||||
.tpl-testing-safety-alert .ts-h1 .red{color:var(--ts-red)}
|
||||
.tpl-testing-safety-alert .ts-h1 .strike{position:relative;display:inline-block}
|
||||
.tpl-testing-safety-alert .ts-h1 .strike::after{content:'';position:absolute;left:-4%;right:-4%;top:50%;height:10px;background:var(--ts-red);transform:skewX(-12deg);opacity:.85}
|
||||
.tpl-testing-safety-alert .ts-h2{font-size:54px;font-weight:900;line-height:1.1;letter-spacing:-1px;margin:0 0 14px}
|
||||
.tpl-testing-safety-alert .ts-sub{font-size:22px;line-height:1.5;color:var(--ts-ink2);max-width:880px;margin-top:10px}
|
||||
.tpl-testing-safety-alert .ts-highlight-red{display:inline-block;padding:4px 14px;background:var(--ts-red);color:#fff;border-radius:8px;font-weight:800}
|
||||
.tpl-testing-safety-alert .ts-highlight-amber{display:inline-block;padding:4px 14px;background:var(--ts-amber-soft);color:var(--ts-amber);border-radius:8px;font-weight:800;border:1px solid rgba(217,119,6,.2)}
|
||||
.tpl-testing-safety-alert .ts-highlight-green{display:inline-block;padding:4px 14px;background:var(--ts-green-soft);color:var(--ts-green);border-radius:8px;font-weight:800;border:1px solid rgba(6,118,71,.2)}
|
||||
.tpl-testing-safety-alert .ts-alert-box{border:2px solid var(--ts-red);border-radius:18px;padding:26px 30px;background:linear-gradient(180deg,#fff 0%,var(--ts-red-soft) 100%);box-shadow:0 14px 36px rgba(224,49,74,.14);margin-top:24px;position:relative}
|
||||
.tpl-testing-safety-alert .ts-alert-box::before{content:'';position:absolute;top:-11px;left:24px;width:22px;height:22px;background:var(--ts-red);border-radius:50%;box-shadow:0 0 0 6px rgba(224,49,74,.2)}
|
||||
.tpl-testing-safety-alert .ts-alert-box.amber{border-color:var(--ts-amber);background:linear-gradient(180deg,#fff 0%,var(--ts-amber-soft) 100%);box-shadow:0 14px 36px rgba(217,119,6,.14)}
|
||||
.tpl-testing-safety-alert .ts-alert-box.amber::before{background:var(--ts-amber);box-shadow:0 0 0 6px rgba(217,119,6,.2)}
|
||||
.tpl-testing-safety-alert .ts-alert-box.green{border-color:var(--ts-green);background:linear-gradient(180deg,#fff 0%,var(--ts-green-soft) 100%);box-shadow:0 14px 36px rgba(6,118,71,.14)}
|
||||
.tpl-testing-safety-alert .ts-alert-box.green::before{background:var(--ts-green);box-shadow:0 0 0 6px rgba(6,118,71,.2)}
|
||||
.tpl-testing-safety-alert .ts-alert-box h3{font-size:34px;font-weight:900;margin:0 0 10px}
|
||||
.tpl-testing-safety-alert .ts-alert-box p{font-size:17px;line-height:1.6;color:var(--ts-ink2);margin:0}
|
||||
.tpl-testing-safety-alert .ts-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px}
|
||||
.tpl-testing-safety-alert .ts-grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-top:20px}
|
||||
.tpl-testing-safety-alert .ts-card{border:1px solid var(--ts-line);border-radius:16px;padding:22px 24px;background:#fff;box-shadow:0 6px 20px rgba(17,19,24,.04)}
|
||||
.tpl-testing-safety-alert .ts-card .lbl{font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--ts-muted);margin-bottom:8px}
|
||||
.tpl-testing-safety-alert .ts-card h4{font-size:26px;font-weight:900;line-height:1.2;margin-bottom:8px}
|
||||
.tpl-testing-safety-alert .ts-card p{font-size:14px;color:var(--ts-ink2);line-height:1.55}
|
||||
.tpl-testing-safety-alert .ts-checklist{display:flex;flex-direction:column;gap:12px;margin-top:20px;max-width:880px}
|
||||
.tpl-testing-safety-alert .ts-check{display:flex;gap:16px;align-items:flex-start;padding:16px 20px;border:1px solid var(--ts-line);border-radius:14px;background:#fff}
|
||||
.tpl-testing-safety-alert .ts-check .box{flex:0 0 32px;height:32px;border-radius:8px;border:2px solid var(--ts-red);display:grid;place-items:center;font-weight:900;color:var(--ts-red);background:var(--ts-red-soft)}
|
||||
.tpl-testing-safety-alert .ts-check.ok .box{border-color:var(--ts-green);color:var(--ts-green);background:var(--ts-green-soft)}
|
||||
.tpl-testing-safety-alert .ts-check .txt{font-size:18px;line-height:1.5;font-weight:600}
|
||||
.tpl-testing-safety-alert .ts-codebox{background:#141418;color:#fff5ea;border-radius:14px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.85;margin-top:20px;border-left:6px solid var(--ts-red)}
|
||||
.tpl-testing-safety-alert .ts-codebox .cm{color:#7a756d}
|
||||
.tpl-testing-safety-alert .ts-codebox .kw{color:#ffb38a}
|
||||
.tpl-testing-safety-alert .ts-codebox .st{color:#b3e6c2}
|
||||
.tpl-testing-safety-alert .ts-codebox .bad{color:#ff9aa8;font-weight:700}
|
||||
.tpl-testing-safety-alert .ts-footer{position:absolute;left:84px;right:84px;bottom:36px;display:flex;justify-content:space-between;font-size:12px;color:var(--ts-muted);letter-spacing:.1em}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-testing-safety-alert">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag">ai safety · 高优先级</span><span class="ts-page">01 / 08</span></div>
|
||||
<div class="ts-kicker">2026 年最重要的一条判断</div>
|
||||
<h1 class="ts-h1">别再追问<br><span class="strike">AI 会不会干活</span><br>开始问:<span class="red">它出事谁负责</span></h1>
|
||||
<p class="ts-sub">AI 出错的代价,不再是一次 bad response 这么简单 —— 它可能一次性写 300 份工单、提 80 个 PR、发 5000 封邮件。</p>
|
||||
<div class="ts-alert-box">
|
||||
<h3>风险已经规模化</h3>
|
||||
<p>「做错」成本 × N;「做对」收益 × N。<br>这就是为什么 <b>测试、验收、安全、风控</b> 会变成未来 3 年最贵的能力。</p>
|
||||
</div>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>AI SAFETY BRIEF · LEWIS · 2026.04</span><span>01 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION -->
|
||||
<section class="slide">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag amber">section · risk 分级</span><span class="ts-page">02 / 08</span></div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="ts-kicker">Chapter One</div>
|
||||
<h1 class="ts-h1" style="font-size:130px">先分 <span class="red">等级</span></h1>
|
||||
<p class="ts-sub" style="font-size:28px">不是所有 AI 行为都同等危险。<br>先把「可撤销」和「不可撤销」分开,再谈流程。</p>
|
||||
</div>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>section · level taxonomy</span><span>02 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT risk levels -->
|
||||
<section class="slide">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag">风险分级 · 3 levels</span><span class="ts-page">03 / 08</span></div>
|
||||
<h2 class="ts-h2">三档风险,三种处理</h2>
|
||||
<div class="ts-grid-3">
|
||||
<div class="ts-card" style="border-top:4px solid var(--ts-green)"><div class="lbl">L1 · 绿色</div><h4>可撤销</h4><p>写 draft、生成图片、起草文档。<br>错了 Ctrl+Z,零代价。<br><b style="color:var(--ts-green)">策略:放开跑</b></p></div>
|
||||
<div class="ts-card" style="border-top:4px solid var(--ts-amber)"><div class="lbl">L2 · 琥珀</div><h4>半可撤销</h4><p>发 draft 邮件、提 PR、改 staging 数据。<br>错了要道歉 / 回滚。<br><b style="color:var(--ts-amber)">策略:人工复核</b></p></div>
|
||||
<div class="ts-card" style="border-top:4px solid var(--ts-red)"><div class="lbl">L3 · 红色</div><h4>不可撤销</h4><p>发真实邮件、付款、删库、删 prod 数据。<br>错了就真错了。<br><b style="color:var(--ts-red)">策略:硬卡 + 双人审</b></p></div>
|
||||
</div>
|
||||
<div class="ts-alert-box amber">
|
||||
<h3>绝不要让 agent 自己升级</h3>
|
||||
<p>L1 的任务不能自己变成 L2。授权必须是显式的、可撤销的、带过期时间的。</p>
|
||||
</div>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>risk · 3 levels</span><span>03 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. CODE -->
|
||||
<section class="slide">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag">policy as code</span><span class="ts-page">04 / 08</span></div>
|
||||
<div class="ts-kicker">别用文档管规则 · 用代码管规则</div>
|
||||
<h2 class="ts-h2">三十行 YAML,<br><span class="ts-highlight-red">红线硬卡</span></h2>
|
||||
<pre class="ts-codebox"><span class="cm"># safety-policy.yaml · compiled → runtime guard</span>
|
||||
<span class="kw">level_1_allow</span>:
|
||||
- tools: [<span class="st">write_draft</span>, <span class="st">generate_image</span>, <span class="st">read_docs</span>]
|
||||
|
||||
<span class="kw">level_2_require_review</span>:
|
||||
- tools: [<span class="st">send_email_draft</span>, <span class="st">open_pr</span>, <span class="st">write_staging_db</span>]
|
||||
reviewer: <span class="st">human</span>
|
||||
|
||||
<span class="kw">level_3_hard_block</span>:
|
||||
- tools: [<span class="st">send_real_email</span>, <span class="st">transfer_money</span>, <span class="st">delete_prod</span>]
|
||||
unless: <span class="st">two_human_sign_off AND within_24h</span>
|
||||
|
||||
<span class="bad">forbidden_always</span>:
|
||||
- <span class="bad">"rm -rf /"</span>
|
||||
- <span class="bad">"drop table"</span>
|
||||
- <span class="bad">"force push origin main"</span></pre>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>policy · yaml-as-guard</span><span>04 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CHART -->
|
||||
<section class="slide">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag amber">incident report · q1</span><span class="ts-page">05 / 08</span></div>
|
||||
<h2 class="ts-h2">我们 Q1 的 <span class="red">12 起 AI 事故</span></h2>
|
||||
<p class="ts-sub">幸好全部捕获在 staging。但每一起都能上生产。</p>
|
||||
<svg viewBox="0 0 1040 360" style="width:100%;max-width:1040px;margin-top:18px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g font-family="Inter,sans-serif" font-size="14" fill="#4a4955">
|
||||
<line x1="70" y1="320" x2="1000" y2="320" stroke="#eaecf3" stroke-width="2"/>
|
||||
<!-- month columns: Jan Feb Mar, L1/L2/L3 stacked -->
|
||||
<g transform="translate(120,0)">
|
||||
<rect x="0" y="220" width="60" height="100" fill="#067647"/>
|
||||
<rect x="0" y="160" width="60" height="60" fill="#d97706"/>
|
||||
<rect x="0" y="130" width="60" height="30" fill="#e0314a"/>
|
||||
<text x="30" y="345" text-anchor="middle" font-weight="700">Jan</text>
|
||||
<text x="30" y="120" text-anchor="middle" font-weight="800" fill="#14141a">5</text>
|
||||
</g>
|
||||
<g transform="translate(320,0)">
|
||||
<rect x="0" y="240" width="60" height="80" fill="#067647"/>
|
||||
<rect x="0" y="200" width="60" height="40" fill="#d97706"/>
|
||||
<rect x="0" y="180" width="60" height="20" fill="#e0314a"/>
|
||||
<text x="30" y="345" text-anchor="middle" font-weight="700">Feb</text>
|
||||
<text x="30" y="170" text-anchor="middle" font-weight="800" fill="#14141a">3</text>
|
||||
</g>
|
||||
<g transform="translate(520,0)">
|
||||
<rect x="0" y="250" width="60" height="70" fill="#067647"/>
|
||||
<rect x="0" y="220" width="60" height="30" fill="#d97706"/>
|
||||
<rect x="0" y="210" width="60" height="10" fill="#e0314a"/>
|
||||
<text x="30" y="345" text-anchor="middle" font-weight="700">Mar</text>
|
||||
<text x="30" y="200" text-anchor="middle" font-weight="800" fill="#14141a">4</text>
|
||||
</g>
|
||||
<!-- legend -->
|
||||
<g transform="translate(720,60)">
|
||||
<rect x="0" y="0" width="16" height="16" fill="#e0314a"/><text x="24" y="13" font-weight="700">L3 不可撤销 (3)</text>
|
||||
<rect x="0" y="26" width="16" height="16" fill="#d97706"/><text x="24" y="39" font-weight="700">L2 需复核 (4)</text>
|
||||
<rect x="0" y="52" width="16" height="16" fill="#067647"/><text x="24" y="65" font-weight="700">L1 可恢复 (5)</text>
|
||||
<text x="0" y="100" font-size="13" fill="#8a8892">全部被 safety-policy 在 runtime 拦下,</text>
|
||||
<text x="0" y="118" font-size="13" fill="#8a8892">未进 prod。但 3 起 L3 非常惊险。</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>incident · q1 summary</span><span>05 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. CHECKLIST -->
|
||||
<section class="slide">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag green">red-team checklist</span><span class="ts-page">06 / 08</span></div>
|
||||
<h2 class="ts-h2">上线前 <span class="red">必过 7 道题</span></h2>
|
||||
<div class="ts-checklist">
|
||||
<div class="ts-check ok"><div class="box">✓</div><div class="txt">它能删除东西吗?有人类 review 吗?能 60 秒内回滚吗?</div></div>
|
||||
<div class="ts-check ok"><div class="box">✓</div><div class="txt">它的 prompt 注入能让它越权吗?(跑过红队提示词)</div></div>
|
||||
<div class="ts-check"><div class="box">!</div><div class="txt">它处理 PII 吗?日志里是不是也有 PII?</div></div>
|
||||
<div class="ts-check ok"><div class="box">✓</div><div class="txt">上下游失败时,它会不会开始乱改其他资源?</div></div>
|
||||
<div class="ts-check"><div class="box">!</div><div class="txt">并发 100 个 agent 一起跑会不会死锁?</div></div>
|
||||
<div class="ts-check ok"><div class="box">✓</div><div class="txt">错了能不能 <b>立刻</b> 停?(kill switch 能 2 秒内生效吗)</div></div>
|
||||
<div class="ts-check"><div class="box">!</div><div class="txt">出事时有没有人值班?值班手册有没有 agent 专属章节?</div></div>
|
||||
</div>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>checklist · pre-launch</span><span>06 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag green">今晚就能动</span><span class="ts-page">07 / 08</span></div>
|
||||
<h2 class="ts-h2">今晚先做 <span class="ts-highlight-red">三件事</span></h2>
|
||||
<div class="ts-grid-3">
|
||||
<div class="ts-card"><div class="lbl">1 · 分级</div><h4>给你的 agent<br>写 L1/L2/L3</h4><p>把所有工具列出来,标上等级。不标的一律按 L3。</p></div>
|
||||
<div class="ts-card"><div class="lbl">2 · 写 policy</div><h4>policy.yaml<br>接 runtime</h4><p>不要信 prompt 里的 "be careful",要信执行层的硬卡。</p></div>
|
||||
<div class="ts-card"><div class="lbl">3 · kill switch</div><h4>红按钮<br>能在 2 秒内停</h4><p>CTO / on-call 都得知道怎么按。演练一次。</p></div>
|
||||
</div>
|
||||
<div class="ts-alert-box green">
|
||||
<h3>真正的安全不是 prompt,是流程</h3>
|
||||
<p>prompt 会被注入,流程不会。—— 把保护放在不可被说服的一层。</p>
|
||||
</div>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>cta · tonight</span><span>07 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="ts-stripe"></div>
|
||||
<div class="ts-chrome"><span class="ts-alert-tag amber">please stay safe</span><span class="ts-page">08 / 08</span></div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="ts-kicker">end of brief</div>
|
||||
<h1 class="ts-h1" style="font-size:140px">谢谢 <span class="red">·</span> thanks</h1>
|
||||
<p class="ts-sub" style="font-size:24px">policy.yaml 模板、红队 prompt 清单、事故复盘模板 —— 评论区扣「安全」。</p>
|
||||
</div>
|
||||
<div class="ts-stripe-b"></div>
|
||||
<div class="ts-footer"><span>end of brief</span><span>08 / 08</span></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-weekly-report
|
||||
description: Team weekly / status-update deck — corporate clarity, 8-cell KPI grid, shipped list, 8-week bar chart, next-week table. Use for 周报, business reviews, team status updates, and exec dashboards.
|
||||
triggers:
|
||||
- "weekly report"
|
||||
- "周报"
|
||||
- "status update"
|
||||
- "team report"
|
||||
- "business review"
|
||||
- "wbr"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: operations
|
||||
featured: 23
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-weekly-report 模板生成一份周报(7 页)。先问我四件事:本周时间范围、3-5 个核心 KPI 数字、本周已发布 / 已完成的事项、下周计划与风险。然后用模板填好 8 周柱状图和下周表格。"
|
||||
---
|
||||
# HTML PPT · Weekly Report
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`weekly-report`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `weekly-report` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/weekly-report/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-weekly-report` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-weekly-report` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,489 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Growth Squad · Weekly W15</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* weekly-report — corporate clarity */
|
||||
.tpl-weekly-report{
|
||||
--bg:#fafbfc;--bg-soft:#f3f5f9;--surface:#ffffff;--surface-2:#f3f5f9;
|
||||
--border:rgba(22,30,55,.09);--border-strong:rgba(22,30,55,.2);
|
||||
--text-1:#161e37;--text-2:#50586b;--text-3:#8b92a5;
|
||||
--accent:#2e63eb;--accent-2:#0ea5b5;--accent-3:#f59e0b;
|
||||
--good:#10b981;--warn:#f59e0b;--bad:#ef4444;
|
||||
--grad:linear-gradient(120deg,#2e63eb,#0ea5b5);
|
||||
--radius:14px;--radius-lg:18px;
|
||||
--shadow:0 6px 20px rgba(22,30,55,.06),0 1px 3px rgba(22,30,55,.04);
|
||||
font-family:'Inter','Noto Sans SC',sans-serif;
|
||||
}
|
||||
.tpl-weekly-report .slide{padding:64px 88px;background:var(--bg)}
|
||||
.tpl-weekly-report .h1{font-size:64px;line-height:1.05;font-weight:800;letter-spacing:-.025em}
|
||||
.tpl-weekly-report .h2{font-size:42px;font-weight:700;letter-spacing:-.02em}
|
||||
.tpl-weekly-report .kicker{color:var(--accent);font-size:12px;font-weight:700}
|
||||
.tpl-weekly-report .cover-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:48px}
|
||||
.tpl-weekly-report .logo{font-weight:800;font-size:18px;letter-spacing:-.01em}
|
||||
.tpl-weekly-report .logo::before{content:"■";color:var(--accent);margin-right:8px}
|
||||
.tpl-weekly-report .week-chip{display:inline-block;padding:8px 18px;border-radius:8px;background:var(--surface);border:1px solid var(--border);font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--text-2)}
|
||||
.tpl-weekly-report .kpi{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px 26px;position:relative;overflow:hidden}
|
||||
.tpl-weekly-report .kpi .label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-3);font-weight:600}
|
||||
.tpl-weekly-report .kpi .value{font-size:48px;font-weight:800;letter-spacing:-.03em;margin-top:8px;line-height:1}
|
||||
.tpl-weekly-report .kpi .delta{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:6px;font-size:12px;font-weight:700;margin-top:10px}
|
||||
.tpl-weekly-report .kpi .delta.up{background:rgba(16,185,129,.12);color:var(--good)}
|
||||
.tpl-weekly-report .kpi .delta.down{background:rgba(239,68,68,.12);color:var(--bad)}
|
||||
.tpl-weekly-report .kpi .delta.flat{background:rgba(139,146,165,.14);color:var(--text-2)}
|
||||
.tpl-weekly-report .kpi::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent)}
|
||||
.tpl-weekly-report .kpi.good::before{background:var(--good)}
|
||||
.tpl-weekly-report .kpi.warn::before{background:var(--warn)}
|
||||
.tpl-weekly-report .kpi.bad::before{background:var(--bad)}
|
||||
.tpl-weekly-report .ship-item{display:flex;gap:14px;padding:14px 0;border-bottom:1px solid var(--border)}
|
||||
.tpl-weekly-report .ship-item .tag{flex:none;padding:3px 10px;border-radius:6px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;height:22px;display:inline-flex;align-items:center}
|
||||
.tpl-weekly-report .tag.feat{background:rgba(46,99,235,.12);color:var(--accent)}
|
||||
.tpl-weekly-report .tag.fix{background:rgba(16,185,129,.12);color:var(--good)}
|
||||
.tpl-weekly-report .tag.exp{background:rgba(245,158,11,.14);color:var(--warn)}
|
||||
.tpl-weekly-report .tag.infra{background:rgba(14,165,181,.12);color:var(--accent-2)}
|
||||
.tpl-weekly-report .ship-item b{color:var(--text-1);font-weight:600}
|
||||
.tpl-weekly-report .ship-item span.owner{margin-left:auto;color:var(--text-3);font-size:12px;font-family:'JetBrains Mono',monospace}
|
||||
.tpl-weekly-report .chart{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:28px}
|
||||
.tpl-weekly-report .chart-bars{display:flex;align-items:flex-end;gap:16px;height:220px;margin-top:20px}
|
||||
.tpl-weekly-report .chart-bars .col{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;position:relative}
|
||||
.tpl-weekly-report .chart-bars .col .b{width:100%;background:var(--grad);border-radius:6px 6px 0 0;min-height:6px;position:relative}
|
||||
.tpl-weekly-report .chart-bars .col .b::after{content:attr(data-v);position:absolute;top:-22px;left:0;right:0;text-align:center;font-size:12px;font-weight:700;color:var(--text-1)}
|
||||
.tpl-weekly-report .chart-bars .col .lbl{font-size:11px;color:var(--text-3);font-family:'JetBrains Mono',monospace}
|
||||
.tpl-weekly-report .blocker{background:var(--surface);border-left:3px solid var(--bad);padding:16px 20px;border-radius:8px;margin-bottom:12px}
|
||||
.tpl-weekly-report .blocker h4{font-size:16px;margin-bottom:4px}
|
||||
.tpl-weekly-report .blocker p{font-size:13px;color:var(--text-2);margin:0}
|
||||
.tpl-weekly-report .blocker .meta{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-3);margin-top:6px}
|
||||
.tpl-weekly-report .next-row{display:grid;grid-template-columns:110px 1fr;gap:16px;padding:14px 0;border-bottom:1px dashed var(--border);align-items:baseline}
|
||||
.tpl-weekly-report .next-row .owner{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--accent)}
|
||||
.tpl-weekly-report .next-row .task{color:var(--text-1);font-weight:500}
|
||||
.tpl-weekly-report .next-row .task span{color:var(--text-3);font-size:12px;margin-left:8px}
|
||||
.tpl-weekly-report .lede{color:var(--text-2)}
|
||||
.tpl-weekly-report .card{background:var(--surface)}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-weekly-report">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover -->
|
||||
<section class="slide" data-title="Cover">
|
||||
<div class="cover-head">
|
||||
<div class="logo">Growth Squad</div>
|
||||
<div class="week-chip">W15 · 2026-04-07 → 2026-04-13</div>
|
||||
</div>
|
||||
<p class="kicker">WEEKLY REPORT</p>
|
||||
<h1 class="h1 mt-s">本周:付费转化率<br>回到了 <span style="color:var(--accent)">3.8%</span>。</h1>
|
||||
<p class="lede mt-m">6 个发布,3 个实验收敛,1 个阻塞项升级。整体健康。</p>
|
||||
<div class="deck-footer"><span>Prepared by @lewis · reviewed by @may</span><span class="slide-number" data-current="1" data-total="7"></span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. KPI -->
|
||||
<section class="slide" data-title="KPIs">
|
||||
<p class="kicker">HIGHLIGHTS · KPIs</p>
|
||||
<h2 class="h2">本周核心指标</h2>
|
||||
<div class="grid g4 mt-l">
|
||||
<div class="kpi good"><div class="label">Paid conv.</div><div class="value">3.82%</div><div class="delta up">▲ +0.4 pts WoW</div></div>
|
||||
<div class="kpi good"><div class="label">MRR</div><div class="value">$148k</div><div class="delta up">▲ +6.1%</div></div>
|
||||
<div class="kpi"><div class="label">Signups</div><div class="value">12,430</div><div class="delta flat">— +0.3%</div></div>
|
||||
<div class="kpi bad"><div class="label">D7 retention</div><div class="value">41%</div><div class="delta down">▼ -1.8 pts</div></div>
|
||||
<div class="kpi good"><div class="label">NPS</div><div class="value">64</div><div class="delta up">▲ +3</div></div>
|
||||
<div class="kpi"><div class="label">Support tickets</div><div class="value">318</div><div class="delta flat">— -12</div></div>
|
||||
<div class="kpi warn"><div class="label">p95 latency</div><div class="value">412ms</div><div class="delta down">▼ +38ms</div></div>
|
||||
<div class="kpi good"><div class="label">Deploys</div><div class="value">37</div><div class="delta up">▲ +9</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Shipped -->
|
||||
<section class="slide" data-title="Shipped">
|
||||
<p class="kicker">SHIPPED THIS WEEK · 6 items</p>
|
||||
<h2 class="h2">Shipped</h2>
|
||||
<div class="mt-l" style="max-width:980px">
|
||||
<div class="ship-item"><span class="tag feat">FEAT</span><div><b>New onboarding checklist v3</b><p class="dim" style="font-size:13px;margin:2px 0 0">4-step checklist replaces the old 7-step modal. A/B won +18% activation.</p></div><span class="owner">@may</span></div>
|
||||
<div class="ship-item"><span class="tag feat">FEAT</span><div><b>Stripe Tax auto-filing</b><p class="dim" style="font-size:13px;margin:2px 0 0">Quarterly filings now handled for 12 US states via Stripe Tax API.</p></div><span class="owner">@raj</span></div>
|
||||
<div class="ship-item"><span class="tag exp">EXP</span><div><b>Pricing page hero test</b><p class="dim" style="font-size:13px;margin:2px 0 0">"From $29" vs "Free trial" headline. Free-trial wins +22% click-through.</p></div><span class="owner">@lewis</span></div>
|
||||
<div class="ship-item"><span class="tag fix">FIX</span><div><b>Edge case in SSO redirect</b><p class="dim" style="font-size:13px;margin:2px 0 0">Google Workspace users with custom domains now land on the correct workspace.</p></div><span class="owner">@eli</span></div>
|
||||
<div class="ship-item"><span class="tag infra">INFRA</span><div><b>Postgres 16 upgrade</b><p class="dim" style="font-size:13px;margin:2px 0 0">Zero-downtime migration. Query p50 down 14%, p95 down 9%.</p></div><span class="owner">@raj</span></div>
|
||||
<div class="ship-item"><span class="tag feat">FEAT</span><div><b>Referral rewards v1</b><p class="dim" style="font-size:13px;margin:2px 0 0">Both sides get 1 month free. Dashboard + email flow live behind flag.</p></div><span class="owner">@may</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Metrics chart -->
|
||||
<section class="slide" data-title="Metrics">
|
||||
<p class="kicker">METRIC DEEP-DIVE</p>
|
||||
<h2 class="h2">Paid conversion, last 8 weeks</h2>
|
||||
<div class="chart mt-l">
|
||||
<div class="row" style="justify-content:space-between"><h4>Paid conv. rate · weekly</h4><span class="pill" style="background:var(--surface-2);color:var(--text-2)">target: 4.0%</span></div>
|
||||
<div class="chart-bars">
|
||||
<div class="col"><div class="b" data-v="3.1%" style="height:58%"></div><div class="lbl">W08</div></div>
|
||||
<div class="col"><div class="b" data-v="3.3%" style="height:64%"></div><div class="lbl">W09</div></div>
|
||||
<div class="col"><div class="b" data-v="3.5%" style="height:72%"></div><div class="lbl">W10</div></div>
|
||||
<div class="col"><div class="b" data-v="3.6%" style="height:75%"></div><div class="lbl">W11</div></div>
|
||||
<div class="col"><div class="b" data-v="3.4%" style="height:68%"></div><div class="lbl">W12</div></div>
|
||||
<div class="col"><div class="b" data-v="3.0%" style="height:55%"></div><div class="lbl">W13</div></div>
|
||||
<div class="col"><div class="b" data-v="3.4%" style="height:68%"></div><div class="lbl">W14</div></div>
|
||||
<div class="col"><div class="b" data-v="3.8%" style="height:88%"></div><div class="lbl">W15</div></div>
|
||||
</div>
|
||||
<p class="dim mt-m" style="font-size:13px;margin-top:36px">Drop in W13 tracked to a broken Stripe webhook (fixed W14). Rebound in W15 is driven by the new onboarding checklist.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Blockers -->
|
||||
<section class="slide" data-title="Blockers">
|
||||
<p class="kicker">BLOCKERS · 3 items</p>
|
||||
<h2 class="h2">Needs attention</h2>
|
||||
<div class="mt-l" style="max-width:900px">
|
||||
<div class="blocker">
|
||||
<h4>p95 latency regressed to 412ms (+38ms)</h4>
|
||||
<p>Traced to the new recommender service under load. Adding caching layer + connection pooling.</p>
|
||||
<div class="meta">owner: @raj · ETA: W16 Wed · severity: medium</div>
|
||||
</div>
|
||||
<div class="blocker">
|
||||
<h4>Apple Pay disabled in EU for 3 days</h4>
|
||||
<p>Stripe credential rotation wasn't synced to the EU account. Fixed, but cost ~$4.2k in lost checkouts.</p>
|
||||
<div class="meta">owner: @eli · severity: high · postmortem in progress</div>
|
||||
</div>
|
||||
<div class="blocker">
|
||||
<h4>D7 retention down 1.8 points</h4>
|
||||
<p>Cohort analysis shows it's isolated to the free-trial pricing test. Need to decide: kill test, or push through W16.</p>
|
||||
<div class="meta">owner: @lewis · needs decision from @may by Monday</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Next week -->
|
||||
<section class="slide" data-title="Next Week">
|
||||
<p class="kicker">NEXT WEEK · W16 plan</p>
|
||||
<h2 class="h2">下周重点</h2>
|
||||
<div class="mt-l" style="max-width:960px">
|
||||
<div class="next-row"><div class="owner">@raj</div><div class="task"><b>Ship recommender cache layer</b><span>blocker · must land Wed</span></div></div>
|
||||
<div class="next-row"><div class="owner">@may</div><div class="task"><b>Referral rewards · flag rollout to 100%</b><span>milestone · targets +3% WoW signups</span></div></div>
|
||||
<div class="next-row"><div class="owner">@lewis</div><div class="task"><b>Pricing test: decision doc + readout</b><span>deadline Mon noon</span></div></div>
|
||||
<div class="next-row"><div class="owner">@eli</div><div class="task"><b>Apple Pay postmortem + runbook update</b><span>include in W16 eng review</span></div></div>
|
||||
<div class="next-row"><div class="owner">squad</div><div class="task"><b>Q2 OKR planning offsite</b><span>Thu 2–5pm · async pre-reads Wed</span></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. Thanks -->
|
||||
<section class="slide center tc" data-title="Thanks">
|
||||
<div>
|
||||
<p class="kicker">FIN · week 15</p>
|
||||
<h1 class="h1" style="font-size:100px">Thanks, team 🫶</h1>
|
||||
<p class="lede" style="margin:16px auto">Solid week. Rebound earned, not luck.</p>
|
||||
<div class="row mt-l" style="justify-content:center;gap:16px">
|
||||
<span class="week-chip">Next report: Mon W16</span>
|
||||
<span class="week-chip">questions → #growth-squad</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: html-ppt-xhs-pastel-card
|
||||
description: 柔和马卡龙慢生活 deck — 奶油 #fef8f1 底 + 三个柔光 blob、Playfair 斜体衬线 display 标题混 sans 正文、28px 圆角马卡龙卡片(桃 / 薄荷 / 天 / 紫 / 柠 / 玫)、Playfair 斜体 01-04 序号、SVG donut 图、chip+page 顶栏。适合生活方式 / 个人成长 / 慢生活 / 情绪类内容,"杂志、手作、不太科技"的感觉。
|
||||
triggers:
|
||||
- "pastel"
|
||||
- "macaron"
|
||||
- "lifestyle"
|
||||
- "slow living"
|
||||
- "慢生活"
|
||||
- "生活方式"
|
||||
- "个人成长"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: personal
|
||||
featured: 33
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-xhs-pastel-card 模板做一份慢生活主题图文。奶油底 + 马卡龙圆角卡片 + Playfair 斜体序号 + donut 图。先告诉我主题(休息 / 暂停 / 自我照顾…)和 5-7 个想说的点。"
|
||||
---
|
||||
# HTML PPT · 柔和马卡龙慢生活
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`xhs-pastel-card`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `xhs-pastel-card` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/xhs-pastel-card/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-xhs-pastel-card` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-xhs-pastel-card` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,381 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>XHS Pastel Card</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* xhs-pastel-card — 柔和马卡龙大色块封面风 */
|
||||
.tpl-xhs-pastel-card{
|
||||
--xp-bg:#fef8f1;
|
||||
--xp-ink:#2a2340;
|
||||
--xp-ink2:#5b5470;
|
||||
--xp-muted:#9089a8;
|
||||
--xp-peach:#ffd8c2;
|
||||
--xp-peach-d:#f48b5c;
|
||||
--xp-mint:#c8ecd8;
|
||||
--xp-mint-d:#2e9d70;
|
||||
--xp-sky:#c9dcfb;
|
||||
--xp-sky-d:#4e7ed6;
|
||||
--xp-lilac:#ddd0f5;
|
||||
--xp-lilac-d:#7b5dc4;
|
||||
--xp-lemon:#fdf0b2;
|
||||
--xp-lemon-d:#c8910a;
|
||||
--xp-rose:#fcd0dd;
|
||||
--xp-rose-d:#c94673;
|
||||
background:var(--xp-bg);
|
||||
color:var(--xp-ink);
|
||||
font-family:'Playfair Display','Noto Serif SC','Inter','Noto Sans SC',Georgia,serif;
|
||||
}
|
||||
.tpl-xhs-pastel-card .slide{background:var(--xp-bg);color:var(--xp-ink);padding:76px 90px}
|
||||
.tpl-xhs-pastel-card .xp-blob{position:absolute;border-radius:50%;filter:blur(2px);opacity:.85;z-index:0}
|
||||
.tpl-xhs-pastel-card .xp-blob.b1{width:420px;height:420px;background:radial-gradient(circle,var(--xp-peach),transparent 70%);top:-8%;right:-6%}
|
||||
.tpl-xhs-pastel-card .xp-blob.b2{width:360px;height:360px;background:radial-gradient(circle,var(--xp-lilac),transparent 72%);bottom:-10%;left:-8%}
|
||||
.tpl-xhs-pastel-card .xp-blob.b3{width:260px;height:260px;background:radial-gradient(circle,var(--xp-mint),transparent 72%);top:40%;right:20%}
|
||||
.tpl-xhs-pastel-card .slide > *{position:relative;z-index:2}
|
||||
.tpl-xhs-pastel-card .xp-topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px;font-family:'Inter','Noto Sans SC',sans-serif}
|
||||
.tpl-xhs-pastel-card .xp-chip{display:inline-flex;align-items:center;gap:10px;padding:8px 18px;border-radius:999px;background:#fff;border:1.5px solid rgba(42,35,64,.1);font-size:13px;font-weight:600;letter-spacing:.08em;color:var(--xp-ink2);text-transform:uppercase}
|
||||
.tpl-xhs-pastel-card .xp-chip::before{content:'';width:9px;height:9px;border-radius:50%;background:var(--xp-peach-d)}
|
||||
.tpl-xhs-pastel-card .xp-chip.mint::before{background:var(--xp-mint-d)}
|
||||
.tpl-xhs-pastel-card .xp-chip.sky::before{background:var(--xp-sky-d)}
|
||||
.tpl-xhs-pastel-card .xp-chip.lilac::before{background:var(--xp-lilac-d)}
|
||||
.tpl-xhs-pastel-card .xp-chip.rose::before{background:var(--xp-rose-d)}
|
||||
.tpl-xhs-pastel-card .xp-page{font-family:'Inter',sans-serif;font-size:13px;color:var(--xp-muted);letter-spacing:.12em;font-weight:600}
|
||||
.tpl-xhs-pastel-card .xp-kicker{font-family:'Inter',sans-serif;font-size:14px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--xp-peach-d);margin-bottom:14px}
|
||||
.tpl-xhs-pastel-card .xp-h1{font-size:96px;font-weight:900;line-height:1.05;letter-spacing:-2px;margin:0 0 18px;color:var(--xp-ink);font-family:'Playfair Display','Noto Serif SC',serif}
|
||||
.tpl-xhs-pastel-card .xp-h1 em{font-style:italic;color:var(--xp-peach-d);font-family:'Playfair Display',serif}
|
||||
.tpl-xhs-pastel-card .xp-h1 .rose{color:var(--xp-rose-d);font-style:italic}
|
||||
.tpl-xhs-pastel-card .xp-h1 .mint{color:var(--xp-mint-d);font-style:italic}
|
||||
.tpl-xhs-pastel-card .xp-h2{font-size:60px;font-weight:800;line-height:1.1;letter-spacing:-1px;margin:0 0 14px;font-family:'Playfair Display','Noto Serif SC',serif}
|
||||
.tpl-xhs-pastel-card .xp-sub{font-family:'Inter','Noto Sans SC',sans-serif;font-size:21px;line-height:1.6;color:var(--xp-ink2);max-width:800px;font-weight:400}
|
||||
.tpl-xhs-pastel-card .xp-card{border-radius:28px;padding:30px 34px;background:#fff;box-shadow:0 14px 40px rgba(42,35,64,.08);position:relative;overflow:hidden}
|
||||
.tpl-xhs-pastel-card .xp-card.peach{background:var(--xp-peach)}
|
||||
.tpl-xhs-pastel-card .xp-card.mint{background:var(--xp-mint)}
|
||||
.tpl-xhs-pastel-card .xp-card.sky{background:var(--xp-sky)}
|
||||
.tpl-xhs-pastel-card .xp-card.lilac{background:var(--xp-lilac)}
|
||||
.tpl-xhs-pastel-card .xp-card.lemon{background:var(--xp-lemon)}
|
||||
.tpl-xhs-pastel-card .xp-card.rose{background:var(--xp-rose)}
|
||||
.tpl-xhs-pastel-card .xp-card .xp-num{font-family:'Playfair Display',serif;font-size:68px;font-weight:900;font-style:italic;line-height:1;opacity:.85}
|
||||
.tpl-xhs-pastel-card .xp-card h4{font-size:22px;font-weight:800;margin:8px 0;font-family:'Inter','Noto Sans SC',sans-serif}
|
||||
.tpl-xhs-pastel-card .xp-card p{font-family:'Inter','Noto Sans SC',sans-serif;font-size:15px;line-height:1.55;color:var(--xp-ink2)}
|
||||
.tpl-xhs-pastel-card .xp-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:26px}
|
||||
.tpl-xhs-pastel-card .xp-grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;margin-top:26px}
|
||||
.tpl-xhs-pastel-card .xp-grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:24px}
|
||||
.tpl-xhs-pastel-card .xp-hero-card{background:#fff;border-radius:36px;padding:40px 46px;margin-top:28px;box-shadow:0 20px 50px rgba(42,35,64,.1)}
|
||||
.tpl-xhs-pastel-card .xp-quote{font-family:'Playfair Display','Noto Serif SC',serif;font-size:40px;font-weight:800;font-style:italic;line-height:1.3;color:var(--xp-ink)}
|
||||
.tpl-xhs-pastel-card .xp-quote::before{content:'“';font-size:100px;line-height:.8;display:block;color:var(--xp-peach-d);opacity:.7}
|
||||
.tpl-xhs-pastel-card .xp-footer{position:absolute;left:90px;right:90px;bottom:40px;display:flex;justify-content:space-between;font-family:'Inter',sans-serif;font-size:12px;color:var(--xp-muted);letter-spacing:.1em}
|
||||
.tpl-xhs-pastel-card .xp-divider{width:90px;height:4px;background:linear-gradient(90deg,var(--xp-peach-d),var(--xp-rose-d));border-radius:2px;margin:20px 0}
|
||||
.tpl-xhs-pastel-card .xp-codebox{background:#2a2340;color:#fef8f1;border-radius:24px;padding:26px 30px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.85;margin-top:22px}
|
||||
.tpl-xhs-pastel-card .xp-codebox .cm{color:#9089a8}
|
||||
.tpl-xhs-pastel-card .xp-codebox .kw{color:#ffc6a0}
|
||||
.tpl-xhs-pastel-card .xp-codebox .st{color:#c8ecd8}
|
||||
.tpl-xhs-pastel-card .xp-codebox .hl{color:#fcd0dd;font-weight:700}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-xhs-pastel-card">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="xp-blob b1"></div>
|
||||
<div class="xp-blob b2"></div>
|
||||
<div class="xp-blob b3"></div>
|
||||
<div class="xp-topbar"><div class="xp-chip">A soft manifesto</div><div class="xp-page">01 · 08</div></div>
|
||||
<div class="xp-kicker">Living With AI · 2026</div>
|
||||
<h1 class="xp-h1">放慢一点,<br>让 <em>AI</em> 帮你<br>过一种 <span class="rose">更温柔</span><br>的生活</h1>
|
||||
<div class="xp-divider"></div>
|
||||
<p class="xp-sub">这不是一份效率指南。这是一份「怎么用 AI 少做一些事」的清单 —— 把挤出来的 4 小时还给你自己。</p>
|
||||
<div class="xp-footer"><span>by lewis · pastel edition</span><span>cover</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION -->
|
||||
<section class="slide">
|
||||
<div class="xp-blob b2"></div>
|
||||
<div class="xp-blob b3"></div>
|
||||
<div class="xp-topbar"><div class="xp-chip mint">Chapter one</div><div class="xp-page">02 · 08</div></div>
|
||||
<div style="margin:auto 0">
|
||||
<div class="xp-kicker">先问自己</div>
|
||||
<h1 class="xp-h1" style="font-size:120px">什么事<br>是你 <span class="mint">其实不想做</span> 的?</h1>
|
||||
<p class="xp-sub">不是「不得不做」,是「做的时候灵魂在叹气」。</p>
|
||||
</div>
|
||||
<div class="xp-footer"><span>section · chapter 1</span><span>02 · 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT 2x2 pastel cards -->
|
||||
<section class="slide">
|
||||
<div class="xp-blob b1"></div>
|
||||
<div class="xp-topbar"><div class="xp-chip rose">Four little escapes</div><div class="xp-page">03 · 08</div></div>
|
||||
<h2 class="xp-h2">四件可以<br>完全交给 <em>AI</em> 的小事</h2>
|
||||
<div class="xp-grid-2">
|
||||
<div class="xp-card peach"><div class="xp-num">01</div><h4>回复那种「收到」邮件</h4><p>它们不需要你思考。让 AI 按你的语气自动处理,一周省 40 分钟。</p></div>
|
||||
<div class="xp-card mint"><div class="xp-num">02</div><h4>订餐厅、改签、查路线</h4><p>一句话外包出去。你只负责选最后选项,不负责翻十个 app。</p></div>
|
||||
<div class="xp-card sky"><div class="xp-num">03</div><h4>把会议录音变成行动项</h4><p>录音 → 摘要 → todo 一键完成。你只需要确认和签字。</p></div>
|
||||
<div class="xp-card lilac"><div class="xp-num">04</div><h4>整理上周拍的 300 张照片</h4><p>按事件分类、挑 10 张精选、写图说。整理档案这件事终于被自动化了。</p></div>
|
||||
</div>
|
||||
<div class="xp-footer"><span>content · 2x2</span><span>03 · 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. QUOTE -->
|
||||
<section class="slide">
|
||||
<div class="xp-blob b3"></div>
|
||||
<div class="xp-blob b2"></div>
|
||||
<div class="xp-topbar"><div class="xp-chip lilac">A small pause</div><div class="xp-page">04 · 08</div></div>
|
||||
<div class="xp-hero-card">
|
||||
<p class="xp-quote">效率工具的终点,不是<em> 做更多</em>,<br>而是 <em>有资格做更少</em>。</p>
|
||||
<div class="xp-divider"></div>
|
||||
<p class="xp-sub">当你把「收到」邮件、订餐、行程、照片整理都交出去,你才会惊讶地发现 —— 原来一周有 4 个小时是空的。</p>
|
||||
</div>
|
||||
<div class="xp-footer"><span>quote</span><span>04 · 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CODE / PROMPT -->
|
||||
<section class="slide">
|
||||
<div class="xp-blob b1"></div>
|
||||
<div class="xp-topbar"><div class="xp-chip">My auto-reply prompt</div><div class="xp-page">05 · 08</div></div>
|
||||
<h2 class="xp-h2">把「<em>收到邮件</em>」<br>自动化的 <span class="rose">一段 prompt</span></h2>
|
||||
<pre class="xp-codebox"><span class="cm"># auto-reply skill</span>
|
||||
<span class="kw">when</span> email matches <span class="st">"收到 / 好的 / 确认 / 收到谢谢"</span>:
|
||||
reply:
|
||||
tone: <span class="st">"温柔,简短,不要太商业"</span>
|
||||
max_lines: <span class="hl">2</span>
|
||||
sign_with: <span class="st">"— Lewis"</span>
|
||||
|
||||
<span class="kw">always_skip</span>:
|
||||
- from: [<span class="st">"家人"</span>, <span class="st">"伴侣"</span>, <span class="st">"亲密朋友"</span>]
|
||||
- contains: [<span class="st">"紧急"</span>, <span class="st">"合同"</span>, <span class="st">"付款"</span>]
|
||||
|
||||
<span class="cm"># 一周省 38 分钟,测过</span></pre>
|
||||
<div class="xp-footer"><span>content · prompt</span><span>05 · 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. CHART — time donut -->
|
||||
<section class="slide">
|
||||
<div class="xp-blob b2"></div>
|
||||
<div class="xp-topbar"><div class="xp-chip mint">Your week, rebuilt</div><div class="xp-page">06 · 08</div></div>
|
||||
<h2 class="xp-h2">一周 4 小时 <span class="mint">还给自己</span></h2>
|
||||
<div style="display:flex;align-items:center;gap:60px;margin-top:30px">
|
||||
<svg viewBox="0 0 260 260" style="width:300px;flex-shrink:0">
|
||||
<circle cx="130" cy="130" r="100" fill="none" stroke="#fef0e4" stroke-width="40"/>
|
||||
<!-- email 12% -->
|
||||
<circle cx="130" cy="130" r="100" fill="none" stroke="#f48b5c" stroke-width="40" stroke-dasharray="75 628" stroke-dashoffset="0" transform="rotate(-90 130 130)"/>
|
||||
<!-- logistics 18% -->
|
||||
<circle cx="130" cy="130" r="100" fill="none" stroke="#2e9d70" stroke-width="40" stroke-dasharray="113 628" stroke-dashoffset="-75" transform="rotate(-90 130 130)"/>
|
||||
<!-- meetings 14% -->
|
||||
<circle cx="130" cy="130" r="100" fill="none" stroke="#4e7ed6" stroke-width="40" stroke-dasharray="88 628" stroke-dashoffset="-188" transform="rotate(-90 130 130)"/>
|
||||
<!-- photos 6% -->
|
||||
<circle cx="130" cy="130" r="100" fill="none" stroke="#7b5dc4" stroke-width="40" stroke-dasharray="38 628" stroke-dashoffset="-276" transform="rotate(-90 130 130)"/>
|
||||
<text x="130" y="130" text-anchor="middle" font-family="Playfair Display" font-size="44" font-weight="900" fill="#2a2340">4h</text>
|
||||
<text x="130" y="156" text-anchor="middle" font-family="Inter" font-size="12" fill="#9089a8">per week saved</text>
|
||||
</svg>
|
||||
<div style="flex:1">
|
||||
<div class="xp-grid-2" style="grid-template-columns:1fr;gap:12px;margin-top:0">
|
||||
<div class="xp-card peach" style="padding:14px 20px;display:flex;align-items:center;gap:14px"><div style="width:14px;height:14px;border-radius:50%;background:var(--xp-peach-d)"></div><div><h4 style="margin:0;font-size:17px">48 min · 邮件</h4></div></div>
|
||||
<div class="xp-card mint" style="padding:14px 20px;display:flex;align-items:center;gap:14px"><div style="width:14px;height:14px;border-radius:50%;background:var(--xp-mint-d)"></div><div><h4 style="margin:0;font-size:17px">72 min · 订/改/查</h4></div></div>
|
||||
<div class="xp-card sky" style="padding:14px 20px;display:flex;align-items:center;gap:14px"><div style="width:14px;height:14px;border-radius:50%;background:var(--xp-sky-d)"></div><div><h4 style="margin:0;font-size:17px">56 min · 会议摘要</h4></div></div>
|
||||
<div class="xp-card lilac" style="padding:14px 20px;display:flex;align-items:center;gap:14px"><div style="width:14px;height:14px;border-radius:50%;background:var(--xp-lilac-d)"></div><div><h4 style="margin:0;font-size:17px">24 min · 照片整理</h4></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="xp-footer"><span>chart · donut</span><span>06 · 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="xp-blob b1"></div>
|
||||
<div class="xp-blob b3"></div>
|
||||
<div class="xp-topbar"><div class="xp-chip rose">This weekend</div><div class="xp-page">07 · 08</div></div>
|
||||
<h2 class="xp-h2">这周末,<br>先给自己 <em>放一个小假</em></h2>
|
||||
<div class="xp-grid-3">
|
||||
<div class="xp-card lemon"><div class="xp-num">☕</div><h4>Saturday morning</h4><p>挑一个你最烦的小事,写 prompt,让它从此不再烦你。</p></div>
|
||||
<div class="xp-card peach"><div class="xp-num">🌸</div><h4>Saturday afternoon</h4><p>去散步。什么都不带。AI 在家帮你看着消息。</p></div>
|
||||
<div class="xp-card sky"><div class="xp-num">🌙</div><h4>Sunday night</h4><p>复盘:哪 4 小时是真的空的?下周继续。</p></div>
|
||||
</div>
|
||||
<div class="xp-footer"><span>cta</span><span>07 · 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="xp-blob b2"></div>
|
||||
<div style="margin:auto 0;text-align:center">
|
||||
<div class="xp-kicker" style="text-align:center">thanks for reading</div>
|
||||
<h1 class="xp-h1" style="font-size:160px;text-align:center">谢谢 <em>·</em> thanks</h1>
|
||||
<div class="xp-divider" style="margin:24px auto"></div>
|
||||
<p class="xp-sub" style="margin:0 auto">如果你也想过更温柔的一周,评论区跟我说说你打算把哪一件事先交出去 ♡</p>
|
||||
</div>
|
||||
<div class="xp-footer"><span>end</span><span>08 · 08</span></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: html-ppt-xhs-post
|
||||
description: 小红书 / Instagram 风 9 页 3:4 竖版图文(810×1080)— 暖色 pastel、虚线 sticker 卡片、底部页码点点。用于发小红书图文、Instagram carousel、品牌种草内容。
|
||||
triggers:
|
||||
- "小红书"
|
||||
- "xhs"
|
||||
- "xhs post"
|
||||
- "xiaohongshu"
|
||||
- "图文"
|
||||
- "instagram carousel"
|
||||
- "种草"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: marketing
|
||||
featured: 24
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "帮我用 html-ppt-xhs-post 模板做一组 9 张小红书图文(3:4 竖版,810×1080)。先告诉我主题,然后帮我把封面 + 7 页内容 + 结尾 CTA 排好,每页一句标题 + 一段正文 + 关键词 sticker。"
|
||||
---
|
||||
# HTML PPT · 小红书 图文
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`xhs-post`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `xhs-post` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/xhs-post/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-xhs-post` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-xhs-post` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,487 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>每天只睡 6h 还精神?· 小红书图文</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* xhs-post — 小红书 3:4 九宫格 */
|
||||
.tpl-xhs-post{
|
||||
--bg:#fef7f3;--bg-soft:#fff1ea;--surface:#ffffff;--surface-2:#fff5ef;
|
||||
--border:rgba(90,40,30,.12);--border-strong:rgba(90,40,30,.24);
|
||||
--text-1:#3a1f18;--text-2:#6f4a3e;--text-3:#a68676;
|
||||
--accent:#ff6b8b;--accent-2:#ffa94d;--accent-3:#ffd166;
|
||||
--grad:linear-gradient(135deg,#ffd3e0,#ffe5c7 50%,#d6f0ff);
|
||||
--good:#7bc67b;--warn:#ffb547;--bad:#ff6b6b;
|
||||
--radius:24px;--radius-lg:32px;
|
||||
--shadow:0 14px 36px rgba(90,40,30,.08);
|
||||
font-family:'Inter','Noto Sans SC','PingFang SC',sans-serif;
|
||||
}
|
||||
.tpl-xhs-post{background:#f0eae2;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
||||
.tpl-xhs-post .deck{width:810px;height:1080px;position:relative;background:transparent}
|
||||
.tpl-xhs-post .slide{
|
||||
position:absolute;inset:0;width:810px;height:1080px;aspect-ratio:3/4;
|
||||
padding:70px 64px;border-radius:28px;overflow:hidden;
|
||||
background:var(--bg);
|
||||
}
|
||||
.tpl-xhs-post .slide::before{content:"";position:absolute;inset:0;background:
|
||||
radial-gradient(45% 30% at 80% 10%,rgba(255,209,102,.35),transparent 70%),
|
||||
radial-gradient(50% 35% at 10% 95%,rgba(255,107,139,.22),transparent 70%),
|
||||
radial-gradient(40% 30% at 90% 85%,rgba(122,200,255,.18),transparent 70%);
|
||||
pointer-events:none;z-index:0}
|
||||
.tpl-xhs-post .slide > *{position:relative;z-index:1}
|
||||
.tpl-xhs-post .h1{font-size:72px;line-height:1.1;font-weight:900;letter-spacing:-.02em;color:var(--text-1)}
|
||||
.tpl-xhs-post .h2{font-size:54px;line-height:1.15;font-weight:800;letter-spacing:-.015em;color:var(--text-1)}
|
||||
.tpl-xhs-post .h3{font-size:36px;font-weight:800;color:var(--text-1)}
|
||||
.tpl-xhs-post .page-dot{position:absolute;top:40px;right:48px;background:var(--text-1);color:#fff;border-radius:999px;padding:6px 14px;font-family:'JetBrains Mono',monospace;font-size:14px;font-weight:700;z-index:2}
|
||||
.tpl-xhs-post .sticker{position:absolute;padding:10px 18px;background:#fff;border:2.5px dashed var(--text-1);border-radius:18px;font-weight:800;font-size:18px;color:var(--text-1);transform:rotate(-3deg);box-shadow:4px 4px 0 var(--text-1)}
|
||||
.tpl-xhs-post .sticker.pink{background:#ffd3e0}
|
||||
.tpl-xhs-post .sticker.yellow{background:#ffe788}
|
||||
.tpl-xhs-post .sticker.blue{background:#cfeaff}
|
||||
.tpl-xhs-post .sticker.green{background:#d4f2c8}
|
||||
.tpl-xhs-post .hand-box{background:#fff;border:2.5px solid var(--text-1);border-radius:22px;padding:24px 28px;box-shadow:5px 5px 0 var(--text-1)}
|
||||
.tpl-xhs-post .lede{color:var(--text-2);font-size:26px;line-height:1.55}
|
||||
.tpl-xhs-post .big-emoji{font-size:180px;line-height:1;text-align:center}
|
||||
.tpl-xhs-post .num-circle{display:inline-flex;align-items:center;justify-content:center;width:72px;height:72px;border-radius:50%;background:var(--accent);color:#fff;font-weight:900;font-size:36px;border:3px solid var(--text-1);box-shadow:4px 4px 0 var(--text-1)}
|
||||
.tpl-xhs-post .step-card{background:#fff;border:2.5px solid var(--text-1);border-radius:22px;padding:26px 28px;box-shadow:5px 5px 0 var(--text-1);margin-bottom:24px}
|
||||
.tpl-xhs-post .step-card h4{font-size:28px;font-weight:800;margin:0 0 6px}
|
||||
.tpl-xhs-post .step-card p{font-size:18px;color:var(--text-2);margin:0}
|
||||
.tpl-xhs-post .tag-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:24px}
|
||||
.tpl-xhs-post .ht{background:#fff;color:var(--accent);border:2px solid var(--text-1);padding:6px 14px;border-radius:999px;font-weight:700;font-size:16px}
|
||||
.tpl-xhs-post .cover-title{background:linear-gradient(180deg,transparent 60%,var(--accent-3) 60%,var(--accent-3) 92%,transparent 92%);padding:0 10px}
|
||||
.tpl-xhs-post .heart{color:var(--accent);font-size:28px}
|
||||
.tpl-xhs-post .bottom-bar{position:absolute;bottom:40px;left:64px;right:64px;display:flex;justify-content:space-between;align-items:center;font-size:15px;color:var(--text-3);font-family:'JetBrains Mono',monospace;z-index:2}
|
||||
.tpl-xhs-post .avatar{width:54px;height:54px;border-radius:50%;background:var(--grad);border:2.5px solid var(--text-1);box-shadow:3px 3px 0 var(--text-1);display:inline-flex;align-items:center;justify-content:center;font-weight:900;font-size:20px;color:var(--text-1)}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-xhs-post">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. Cover -->
|
||||
<section class="slide" data-title="Cover">
|
||||
<div class="page-dot">1 / 9</div>
|
||||
<div class="sticker pink" style="top:120px;left:48px;transform:rotate(-6deg)">💤 救命</div>
|
||||
<div class="sticker yellow" style="top:140px;right:64px;transform:rotate(5deg)">亲测 7 天</div>
|
||||
<div style="margin-top:200px">
|
||||
<p class="lede" style="font-size:24px;color:var(--text-1);font-weight:600">打工人深夜自救手册</p>
|
||||
<h1 class="h1 mt-s">每天只睡 <span class="cover-title">6h</span><br>还能<span class="cover-title">精神一整天</span><br>的 3 个小习惯</h1>
|
||||
</div>
|
||||
<div class="bottom-bar"><div><span class="avatar">小</span> <b style="color:var(--text-1);margin-left:8px">@小熊不困了</b></div><div>← 左滑 查看</div></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Hook -->
|
||||
<section class="slide" data-title="Hook">
|
||||
<div class="page-dot">2 / 9</div>
|
||||
<div class="big-emoji" style="margin-top:80px">👀</div>
|
||||
<h2 class="h2 tc mt-l">等等先别划走!</h2>
|
||||
<p class="lede tc mt-m" style="padding:0 20px">我也曾是那个<br>早上起来像被卡车撞过的人。<br><br>直到我发现了<br><b style="color:var(--accent)">1 件事</b>比睡够 8 小时还重要。</p>
|
||||
<div class="sticker blue" style="bottom:160px;left:50%;transform:translateX(-50%) rotate(-2deg)">真 · 转折点 ↓</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Pain -->
|
||||
<section class="slide" data-title="Pain">
|
||||
<div class="page-dot">3 / 9</div>
|
||||
<p class="lede" style="font-weight:700;color:var(--accent)">❌ 你是不是也这样</p>
|
||||
<h2 class="h2 mt-s">越睡越累</h2>
|
||||
<div class="stack mt-l">
|
||||
<div class="hand-box"><b style="font-size:22px">😵💫 周末补觉到中午</b><p class="dim" style="font-size:16px;margin-top:4px">起来头更晕,一整天废掉</p></div>
|
||||
<div class="hand-box"><b style="font-size:22px">☕️ 咖啡续三杯</b><p class="dim" style="font-size:16px;margin-top:4px">下午 3 点照样困到扶墙</p></div>
|
||||
<div class="hand-box"><b style="font-size:22px">📱 睡前刷到凌晨</b><p class="dim" style="font-size:16px;margin-top:4px">明明很困就是不舍得睡</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Aha -->
|
||||
<section class="slide" data-title="Aha">
|
||||
<div class="page-dot">4 / 9</div>
|
||||
<div class="sticker green" style="top:100px;right:48px;transform:rotate(4deg)">✨ aha moment</div>
|
||||
<p class="lede mt-l" style="color:var(--accent);font-weight:700">💡 真相是</p>
|
||||
<h2 class="h2 mt-s">不是睡得少,<br>是<span style="background:var(--accent-3);padding:0 8px">醒得不对</span>。</h2>
|
||||
<p class="lede mt-l">身体有 90 分钟一个周期。<br>在"深睡"里被闹钟拽起来,<br>就算睡 9 小时也跟没睡一样。</p>
|
||||
<p class="lede mt-m" style="color:var(--text-1);font-weight:700">关键是:<span style="color:var(--accent)">卡着周期醒</span>。</p>
|
||||
</section>
|
||||
|
||||
<!-- 5. Step 1 -->
|
||||
<section class="slide" data-title="Step 1">
|
||||
<div class="page-dot">5 / 9</div>
|
||||
<div class="num-circle">1</div>
|
||||
<h2 class="h2 mt-m">倒推睡眠时间</h2>
|
||||
<div class="hand-box mt-l">
|
||||
<p style="font-size:22px;margin:0;color:var(--text-1);font-weight:700">👉 公式</p>
|
||||
<p style="font-size:20px;margin:10px 0 0;color:var(--text-2);line-height:1.7">起床时间 − <b style="color:var(--accent)">90min × N</b> − 15min 入睡<br>= 你今晚该上床的点</p>
|
||||
</div>
|
||||
<div class="hand-box mt-m" style="background:#fff5ef">
|
||||
<p style="font-size:18px;margin:0;color:var(--text-2)">举例:要 7 点起</p>
|
||||
<p style="font-size:24px;margin:8px 0 0;color:var(--text-1);font-weight:800">→ 23:15 上床 (4 个周期)<br>→ 00:45 上床 (3 个周期)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Step 2 -->
|
||||
<section class="slide" data-title="Step 2">
|
||||
<div class="page-dot">6 / 9</div>
|
||||
<div class="num-circle" style="background:var(--accent-2)">2</div>
|
||||
<h2 class="h2 mt-m">早晨 10 分钟光</h2>
|
||||
<div class="hand-box mt-l">
|
||||
<p style="font-size:22px;margin:0;color:var(--text-1);font-weight:700">☀️ 打开窗帘 / 下楼遛弯</p>
|
||||
<p style="font-size:18px;margin:8px 0 0;color:var(--text-2);line-height:1.6">自然光一照,褪黑素立刻被掐停,人就真的醒了。阴天也有效,别偷懒。</p>
|
||||
</div>
|
||||
<div class="sticker yellow" style="bottom:200px;right:60px;transform:rotate(8deg)">⏰ 比咖啡还猛</div>
|
||||
<div class="hand-box mt-m" style="background:#fff5ef">
|
||||
<p style="font-size:18px;margin:0;color:var(--text-2)">懒人方案:</p>
|
||||
<p style="font-size:22px;margin:6px 0 0;color:var(--text-1);font-weight:700">刷牙的时候站在窗边 🪥</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 7. Step 3 -->
|
||||
<section class="slide" data-title="Step 3">
|
||||
<div class="page-dot">7 / 9</div>
|
||||
<div class="num-circle" style="background:var(--accent-3);color:var(--text-1)">3</div>
|
||||
<h2 class="h2 mt-m">下午 3 点<br>20 分钟小睡</h2>
|
||||
<div class="hand-box mt-l">
|
||||
<p style="font-size:20px;margin:0;color:var(--text-2);line-height:1.6"><b style="color:var(--text-1)">⏱️ 最多 20 分钟。</b>超过 30 就会进入深睡,醒来会更累。</p>
|
||||
</div>
|
||||
<div class="hand-box mt-m" style="background:#fff5ef">
|
||||
<p style="font-size:20px;margin:0;color:var(--text-2);line-height:1.6"><b style="color:var(--text-1)">💡 小 tip:</b>睡前喝一口咖啡。20 分钟后咖啡因正好起效,和小睡的清醒 buff 叠加。</p>
|
||||
</div>
|
||||
<div class="sticker pink" style="bottom:140px;left:50%;transform:translateX(-50%) rotate(-3deg)">打工人作弊技</div>
|
||||
</section>
|
||||
|
||||
<!-- 8. Result -->
|
||||
<section class="slide" data-title="Result">
|
||||
<div class="page-dot">8 / 9</div>
|
||||
<p class="lede" style="color:var(--good);font-weight:700">✅ 我坚持 7 天后</p>
|
||||
<h2 class="h2 mt-s">结果是……</h2>
|
||||
<div class="stack mt-l">
|
||||
<div class="hand-box"><b style="font-size:22px">😌 早上闹钟响之前就自然醒</b></div>
|
||||
<div class="hand-box"><b style="font-size:22px">💪 下午不再崩溃</b></div>
|
||||
<div class="hand-box"><b style="font-size:22px">☕️ 咖啡从 3 杯 → 1 杯</b></div>
|
||||
<div class="hand-box" style="background:var(--accent-3);border-color:var(--text-1)"><b style="font-size:24px">✨ 最重要:脾气变好了</b></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 9. CTA -->
|
||||
<section class="slide" data-title="CTA">
|
||||
<div class="page-dot">9 / 9</div>
|
||||
<div class="big-emoji" style="margin-top:60px">💌</div>
|
||||
<h2 class="h2 tc mt-l">觉得有用的话</h2>
|
||||
<h1 class="h1 tc mt-s" style="color:var(--accent)">收藏 + 关注 🧡</h1>
|
||||
<p class="lede tc mt-l" style="padding:0 30px">下期讲<br><b style="color:var(--text-1)">「打工人脊椎急救 5 式」</b><br>办公室也能做</p>
|
||||
<div class="tag-row" style="justify-content:center;margin-top:36px">
|
||||
<span class="ht">#睡眠</span>
|
||||
<span class="ht">#打工人日常</span>
|
||||
<span class="ht">#自律</span>
|
||||
<span class="ht">#健康生活</span>
|
||||
</div>
|
||||
<div class="bottom-bar"><div><span class="avatar">小</span> <b style="color:var(--text-1);margin-left:8px">@小熊不困了</b></div><div>❤️ 5.2w</div></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: html-ppt-xhs-white-editorial
|
||||
description: 白底杂志风 deck — 纯白背景 + 顶部 10 色彩虹 bar、80-110px display 标题、紫→蓝→绿→橙→粉渐变文字、马卡龙软卡片组(粉/紫/蓝/绿/橙)、黑底白字 .focus pill、引用大块。同时适合发小红书图文 + 横版 PPT 双用。
|
||||
triggers:
|
||||
- "白底杂志"
|
||||
- "杂志风"
|
||||
- "xhs editorial"
|
||||
- "white editorial"
|
||||
- "小红书白底"
|
||||
- "editorial deck"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: marketing
|
||||
featured: 27
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt-xhs-white-editorial 模板做一份白底杂志风 PPT,中文优先。要点:80-110px display 大标题、彩虹顶部 bar、马卡龙软卡片、黑底白字 .focus pill。先告诉我主题和受众,再写 8-12 页。"
|
||||
---
|
||||
# HTML PPT · 白底杂志风
|
||||
|
||||
A focused entry point into the [`html-ppt`](../html-ppt/SKILL.md) master skill that lands the user directly on the **`xhs-white-editorial`** full-deck template.
|
||||
|
||||
## When this card is picked
|
||||
|
||||
The Examples gallery wires "Use this prompt" to the example_prompt above. When you accept that prompt, this card is the right pick if the user wants exactly the visual identity of `xhs-white-editorial` (see the upstream [full-decks catalog](../html-ppt/references/full-decks.md) for screenshots and rationale).
|
||||
|
||||
## How to author the deck
|
||||
|
||||
1. **Read the master skill first.** All authoring rules live in
|
||||
[`skills/html-ppt/SKILL.md`](../html-ppt/SKILL.md) — content/audience checklist,
|
||||
token rules, layout reuse, presenter mode, the keyboard runtime, and the
|
||||
"never put presenter-only text on the slide" rule.
|
||||
2. **Start from the matching template folder:**
|
||||
`skills/html-ppt/templates/full-decks/xhs-white-editorial/` — copy `index.html` and
|
||||
`style.css` into the project, keep the `.tpl-xhs-white-editorial` body class.
|
||||
3. **Bring the shared runtime with the template.** The upstream
|
||||
`index.html` links the shared CSS/JS via `../../../assets/...` because it
|
||||
sits three folders deep inside `skills/html-ppt/templates/full-decks/`.
|
||||
Once you copy `index.html` into the project, those parent-relative URLs
|
||||
no longer resolve and `base.css`, `animations.css`, and `runtime.js`
|
||||
will 404 — meaning the deck never activates and slide navigation is
|
||||
dead. Pick one of these two recipes per project:
|
||||
- **Recipe A — copy + rewrite (preferred):** copy
|
||||
`skills/html-ppt/assets/fonts.css`, `skills/html-ppt/assets/base.css`,
|
||||
`skills/html-ppt/assets/animations/animations.css`, and
|
||||
`skills/html-ppt/assets/runtime.js` into a project-local
|
||||
`assets/` (with `assets/animations/animations.css`), then rewrite the
|
||||
four `<link>`/`<script>` tags in `index.html` from
|
||||
`../../../assets/...` to the matching project-local paths
|
||||
(`assets/fonts.css`, `assets/base.css`,
|
||||
`assets/animations/animations.css`, `assets/runtime.js`).
|
||||
- **Recipe B — inline:** read the same four files and replace each
|
||||
`<link rel="stylesheet" href="../../../assets/...">` with a
|
||||
`<style>...</style>` containing the file's contents, and the
|
||||
`<script src="../../../assets/runtime.js">` with a
|
||||
`<script>...</script>` containing `runtime.js`. Yields a single
|
||||
self-contained `index.html`.
|
||||
Either way, do not ship the upstream `../../../assets/...` URLs
|
||||
verbatim into a project artifact — they only work in-tree.
|
||||
4. **Pick a theme.** Default tokens look fine; if the user wants a different
|
||||
feel, swap in any of the 36 themes from `skills/html-ppt/assets/themes/*.css`
|
||||
via `<link id="theme-link">` and let `T` cycle.
|
||||
5. **Replace demo content, not classes.** The `.tpl-xhs-white-editorial` scoped CSS only
|
||||
recognises the structural classes shipped in the template — keep them.
|
||||
6. **Speaker notes go inside `<aside class="notes">` or `<div class="notes">`** — never as visible text on the slide.
|
||||
|
||||
## Attribution
|
||||
|
||||
Visual system, layouts, themes and the runtime keyboard model come from
|
||||
the upstream MIT-licensed [`lewislulu/html-ppt-skill`](https://github.com/lewislulu/html-ppt-skill). The
|
||||
LICENSE file ships at `skills/html-ppt/LICENSE`; please keep it in place when
|
||||
redistributing.
|
||||
@@ -0,0 +1,418 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>白底杂志风 · XHS Editorial</title>
|
||||
<style>/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
|
||||
</style>
|
||||
<style>/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>/* xhs-white-editorial — 白底杂志风 */
|
||||
.tpl-xhs-white-editorial{
|
||||
--xw-bg:#ffffff;
|
||||
--xw-ink:#111318;
|
||||
--xw-ink2:#475467;
|
||||
--xw-muted:#98a2b3;
|
||||
--xw-line:#eaecf3;
|
||||
--xw-purple:#7b61ff;
|
||||
--xw-pink:#ff5fa2;
|
||||
--xw-blue:#4e8cff;
|
||||
--xw-green:#17b26a;
|
||||
--xw-orange:#ff9d42;
|
||||
--xw-soft-purple:#f4efff;
|
||||
--xw-soft-pink:#fff0f6;
|
||||
--xw-soft-blue:#eef4ff;
|
||||
--xw-soft-green:#edfdf3;
|
||||
--xw-soft-orange:#fff5ea;
|
||||
background:var(--xw-bg);
|
||||
color:var(--xw-ink);
|
||||
font-family:'Inter','Noto Sans SC','PingFang SC',-apple-system,sans-serif;
|
||||
}
|
||||
.tpl-xhs-white-editorial .slide{background:#fff;padding:72px 88px}
|
||||
.tpl-xhs-white-editorial .xw-topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}
|
||||
.tpl-xhs-white-editorial .xw-tag{display:inline-flex;align-items:center;gap:10px;padding:10px 18px;border:1px solid var(--xw-line);border-radius:999px;font-size:15px;color:var(--xw-ink2);background:#fff}
|
||||
.tpl-xhs-white-editorial .xw-tag .dot{width:10px;height:10px;border-radius:50%;background:linear-gradient(90deg,#7b61ff,#4e8cff,#17b26a,#ff9d42,#ff5fa2)}
|
||||
.tpl-xhs-white-editorial .xw-page{font-size:14px;color:var(--xw-muted);letter-spacing:.1em}
|
||||
.tpl-xhs-white-editorial .xw-kicker{font-size:18px;color:var(--xw-ink2);margin-top:6px;font-weight:500}
|
||||
.tpl-xhs-white-editorial .xw-title{font-size:84px;line-height:1.02;letter-spacing:-2px;font-weight:850;margin:18px 0 0;color:var(--xw-ink)}
|
||||
.tpl-xhs-white-editorial .xw-title-md{font-size:60px;line-height:1.05;letter-spacing:-1.5px;font-weight:800;margin:14px 0 0}
|
||||
.tpl-xhs-white-editorial .xw-grad{background:linear-gradient(90deg,#7b61ff 0%,#4e8cff 25%,#17b26a 48%,#ff9d42 72%,#ff5fa2 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
.tpl-xhs-white-editorial .xw-sub{font-size:24px;line-height:1.45;color:#1f2937;margin-top:22px;max-width:900px}
|
||||
.tpl-xhs-white-editorial .xw-focus{display:inline-block;padding:6px 14px;border-radius:14px;background:#111318;color:#fff;font-weight:700}
|
||||
.tpl-xhs-white-editorial .xw-focus-blue{display:inline-block;padding:6px 14px;border-radius:14px;background:var(--xw-soft-blue);color:#174ea6;font-weight:700}
|
||||
.tpl-xhs-white-editorial .xw-focus-pink{display:inline-block;padding:6px 14px;border-radius:14px;background:var(--xw-soft-pink);color:#c11574;font-weight:700}
|
||||
.tpl-xhs-white-editorial .xw-focus-orange{display:inline-block;padding:6px 14px;border-radius:14px;background:var(--xw-soft-orange);color:#b54708;font-weight:700}
|
||||
.tpl-xhs-white-editorial .xw-focus-green{display:inline-block;padding:6px 14px;border-radius:14px;background:var(--xw-soft-green);color:#067647;font-weight:700}
|
||||
.tpl-xhs-white-editorial .xw-hero{margin-top:28px;border:1px solid var(--xw-line);border-radius:28px;padding:30px 34px;background:linear-gradient(180deg,#fff 0%,#fcfcff 100%);box-shadow:0 18px 48px rgba(17,19,24,.08)}
|
||||
.tpl-xhs-white-editorial .xw-quote{font-size:38px;line-height:1.3;font-weight:800;letter-spacing:-.5px}
|
||||
.tpl-xhs-white-editorial .xw-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:22px}
|
||||
.tpl-xhs-white-editorial .xw-grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-top:22px}
|
||||
.tpl-xhs-white-editorial .xw-card{border:1px solid var(--xw-line);border-radius:24px;padding:24px 26px;box-shadow:0 10px 24px rgba(17,19,24,.04);background:#fff}
|
||||
.tpl-xhs-white-editorial .xw-card.soft-purple{background:var(--xw-soft-purple)}
|
||||
.tpl-xhs-white-editorial .xw-card.soft-pink{background:var(--xw-soft-pink)}
|
||||
.tpl-xhs-white-editorial .xw-card.soft-blue{background:var(--xw-soft-blue)}
|
||||
.tpl-xhs-white-editorial .xw-card.soft-green{background:var(--xw-soft-green)}
|
||||
.tpl-xhs-white-editorial .xw-card.soft-orange{background:var(--xw-soft-orange)}
|
||||
.tpl-xhs-white-editorial .xw-label{font-size:14px;font-weight:800;opacity:.7;margin-bottom:10px;letter-spacing:.08em;text-transform:uppercase}
|
||||
.tpl-xhs-white-editorial .xw-card .main{font-size:28px;line-height:1.22;font-weight:850;letter-spacing:-.5px}
|
||||
.tpl-xhs-white-editorial .xw-card .desc{font-size:16px;line-height:1.5;color:#475467;margin-top:12px}
|
||||
.tpl-xhs-white-editorial .xw-steps{margin-top:18px}
|
||||
.tpl-xhs-white-editorial .xw-step{display:flex;gap:18px;align-items:flex-start;margin:16px 0}
|
||||
.tpl-xhs-white-editorial .xw-num{flex:0 0 48px;height:48px;border-radius:50%;background:#111318;color:#fff;display:grid;place-items:center;font-size:20px;font-weight:900}
|
||||
.tpl-xhs-white-editorial .xw-txt{font-size:22px;line-height:1.45;font-weight:700}
|
||||
.tpl-xhs-white-editorial .xw-codebox{background:#0f1117;color:#e4e2d8;border-radius:18px;padding:22px 26px;font-family:'JetBrains Mono',monospace;font-size:15px;line-height:1.75;margin-top:20px;border:1px solid #1f222c}
|
||||
.tpl-xhs-white-editorial .xw-codebox .cm{color:#6b6a62}
|
||||
.tpl-xhs-white-editorial .xw-codebox .kw{color:#c88f64}
|
||||
.tpl-xhs-white-editorial .xw-codebox .st{color:#a8c292}
|
||||
.tpl-xhs-white-editorial .xw-codebox .hl{color:#e9c58a;font-weight:600}
|
||||
.tpl-xhs-white-editorial .xw-footer{position:absolute;left:88px;right:88px;bottom:44px;display:flex;justify-content:space-between;align-items:flex-end;font-size:13px;color:var(--xw-muted)}
|
||||
.tpl-xhs-white-editorial .xw-topline{position:absolute;top:0;left:0;right:0;height:5px;background:linear-gradient(90deg,#6366f1,#8b5cf6,#a855f7,#ec4899,#f43f5e,#f97316,#eab308,#22c55e,#06b6d4,#6366f1)}
|
||||
.tpl-xhs-white-editorial .xw-pill{display:inline-block;padding:8px 16px;border-radius:999px;font-size:14px;font-weight:700;margin:0 8px 8px 0;background:#fff;border:1px solid var(--xw-line);color:#394150}
|
||||
.tpl-xhs-white-editorial .xw-big-stat{font-size:96px;font-weight:900;letter-spacing:-4px;line-height:1}
|
||||
.tpl-xhs-white-editorial .xw-big-stat small{font-size:22px;color:var(--xw-muted);font-weight:700;letter-spacing:0;margin-left:6px}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* Static-preview fallback (runtime.js is absent — keep every slide visible) */
|
||||
.deck{height:auto;min-height:100vh;overflow:visible}
|
||||
.slide{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;height:100vh;page-break-after:always}
|
||||
.deck-header,.deck-footer,.slide-number,.progress-bar,.notes-overlay,.overview{pointer-events:none}
|
||||
.notes{display:none!important}
|
||||
</style></head>
|
||||
<body class="tpl-xhs-white-editorial">
|
||||
<div class="deck">
|
||||
|
||||
<!-- 1. COVER -->
|
||||
<section class="slide is-active">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>AI 时代 · 职业判断</div>
|
||||
<div class="xw-page">01 / 08</div>
|
||||
</div>
|
||||
<div class="xw-kicker">我越来越确定的一件事</div>
|
||||
<h1 class="xw-title">以后最贵的工作,<br>是 <span class="xw-grad">测试 + 安全</span></h1>
|
||||
<p class="xw-sub">AI 会越来越会做事。但谁来保证它 <span class="xw-focus">做对</span>、<span class="xw-focus">没风险</span>、<span class="xw-focus">不会出事</span>?</p>
|
||||
<div class="xw-hero">
|
||||
<div class="xw-quote">未来最值钱的,<br>不是 <span class="xw-focus-orange">生产</span>,而是 <span class="xw-focus">验收和兜底</span>。</div>
|
||||
</div>
|
||||
<div class="xw-footer"><span>白底|强重点|杂志竖排</span><span>Cover · 01</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 2. SECTION DIVIDER -->
|
||||
<section class="slide">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>Chapter · 01</div>
|
||||
<div class="xw-page">02 / 08</div>
|
||||
</div>
|
||||
<div style="margin-top:120px">
|
||||
<div class="xw-kicker" style="font-size:20px;letter-spacing:.2em;text-transform:uppercase;color:#98a2b3">第一章</div>
|
||||
<h1 class="xw-title" style="font-size:110px;margin-top:20px">先看 <span class="xw-grad">大趋势</span></h1>
|
||||
<p class="xw-sub" style="font-size:28px">当执行越来越便宜,判断就会越来越贵。</p>
|
||||
</div>
|
||||
<div class="xw-footer"><span>Section Divider</span><span>02 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 3. CONTENT — 4 card grid -->
|
||||
<section class="slide">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>越来越多的事会交给 AI</div>
|
||||
<div class="xw-page">03 / 08</div>
|
||||
</div>
|
||||
<h2 class="xw-title-md">未来 3 年,这些事都会 <span class="xw-grad">自动跑</span></h2>
|
||||
<div class="xw-grid-2">
|
||||
<div class="xw-card soft-pink"><div class="xw-label">内容</div><div class="main">写文案 · 写方案 · 写脚本</div><div class="desc">创作变成一个 prompt 的距离</div></div>
|
||||
<div class="xw-card soft-blue"><div class="xw-label">生产</div><div class="main">做图 · 搭页面 · 做表格</div><div class="desc">生产力工具集体重写一次</div></div>
|
||||
<div class="xw-card soft-green"><div class="xw-label">执行</div><div class="main">跑流程 · 写代码 · 自动操作</div><div class="desc">Agent 从 demo 走进真实工作流</div></div>
|
||||
<div class="xw-card soft-orange"><div class="xw-label">分析</div><div class="main">读数据 · 做总结 · 给建议</div><div class="desc">决策支持层彻底向下延伸</div></div>
|
||||
</div>
|
||||
<div class="xw-footer"><span>Content · Grid 2x2</span><span>03 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 4. STEPS -->
|
||||
<section class="slide">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>为什么会这样</div>
|
||||
<div class="xw-page">04 / 08</div>
|
||||
</div>
|
||||
<h2 class="xw-title-md">AI 越强,<span class="xw-grad">判断对错</span> 越值钱</h2>
|
||||
<div class="xw-steps">
|
||||
<div class="xw-step"><div class="xw-num">1</div><div class="xw-txt">生产会更便宜,边际成本接近零</div></div>
|
||||
<div class="xw-step"><div class="xw-num">2</div><div class="xw-txt">复制会更快,错误也一起被加速</div></div>
|
||||
<div class="xw-step"><div class="xw-num">3</div><div class="xw-txt">AI 一本正经地做错,人类难以察觉</div></div>
|
||||
<div class="xw-step"><div class="xw-num">4</div><div class="xw-txt">所以最贵的能力会变成 <span class="xw-focus">发现问题</span></div></div>
|
||||
</div>
|
||||
<div class="xw-hero"><div class="xw-quote" style="font-size:30px">AI 让「<span class="xw-focus-blue">做出来</span>」变便宜,<br>但让「<span class="xw-focus">做对、做稳、别出事</span>」变更贵。</div></div>
|
||||
<div class="xw-footer"><span>Content · Steps</span><span>04 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 5. CODE EXAMPLE -->
|
||||
<section class="slide">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>一段你今晚就能跑的验收 Skill</div>
|
||||
<div class="xw-page">05 / 08</div>
|
||||
</div>
|
||||
<h2 class="xw-title-md">不是写 prompt,<br>是写 <span class="xw-grad">验收清单</span></h2>
|
||||
<pre class="xw-codebox"><span class="cm"># skills/ai-acceptance/SKILL.md</span>
|
||||
<span class="kw">name</span>: <span class="st">ai-acceptance</span>
|
||||
<span class="kw">description</span>: <span class="st">"Runs AI output through a 4-gate review checklist."</span>
|
||||
|
||||
<span class="kw">gates</span>:
|
||||
- <span class="hl">functional</span>: <span class="st">"Does it actually do what the user asked?"</span>
|
||||
- <span class="hl">edge_cases</span>: <span class="st">"Empty / long / non-ASCII / concurrent?"</span>
|
||||
- <span class="hl">safety</span>: <span class="st">"PII, secrets, destructive ops — all red-flagged?"</span>
|
||||
- <span class="hl">rollback</span>: <span class="st">"If this ships and breaks, can we undo in 60s?"</span></pre>
|
||||
<div class="xw-footer"><span>Content · Code Block</span><span>05 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 6. CHART — SVG bar -->
|
||||
<section class="slide">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>岗位相对价值变化</div>
|
||||
<div class="xw-page">06 / 08</div>
|
||||
</div>
|
||||
<h2 class="xw-title-md">越来越 <span class="xw-focus-pink">便宜</span>,越来越 <span class="xw-focus-green">贵</span></h2>
|
||||
<svg viewBox="0 0 960 380" style="width:100%;max-width:1000px;margin-top:30px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g font-family="Inter, sans-serif" font-size="16" fill="#475467">
|
||||
<!-- baseline -->
|
||||
<line x1="180" y1="330" x2="940" y2="330" stroke="#eaecf3" stroke-width="2"/>
|
||||
<!-- rows -->
|
||||
<g transform="translate(0,40)">
|
||||
<text x="170" y="30" text-anchor="end" font-weight="700" fill="#111">纯执行</text>
|
||||
<rect x="180" y="10" width="520" height="28" rx="14" fill="#fff0f6"/>
|
||||
<rect x="180" y="10" width="120" height="28" rx="14" fill="#ff5fa2"/>
|
||||
<text x="710" y="30" fill="#c11574" font-weight="700">-65% 价值</text>
|
||||
</g>
|
||||
<g transform="translate(0,100)">
|
||||
<text x="170" y="30" text-anchor="end" font-weight="700" fill="#111">内容生产</text>
|
||||
<rect x="180" y="10" width="520" height="28" rx="14" fill="#eef4ff"/>
|
||||
<rect x="180" y="10" width="200" height="28" rx="14" fill="#4e8cff"/>
|
||||
<text x="710" y="30" fill="#174ea6" font-weight="700">-40% 价值</text>
|
||||
</g>
|
||||
<g transform="translate(0,160)">
|
||||
<text x="170" y="30" text-anchor="end" font-weight="700" fill="#111">数据分析</text>
|
||||
<rect x="180" y="10" width="520" height="28" rx="14" fill="#fff5ea"/>
|
||||
<rect x="180" y="10" width="320" height="28" rx="14" fill="#ff9d42"/>
|
||||
<text x="710" y="30" fill="#b54708" font-weight="700">持平</text>
|
||||
</g>
|
||||
<g transform="translate(0,220)">
|
||||
<text x="170" y="30" text-anchor="end" font-weight="700" fill="#111">测试 / 验收</text>
|
||||
<rect x="180" y="10" width="520" height="28" rx="14" fill="#edfdf3"/>
|
||||
<rect x="180" y="10" width="440" height="28" rx="14" fill="#17b26a"/>
|
||||
<text x="710" y="30" fill="#067647" font-weight="700">+85% 价值</text>
|
||||
</g>
|
||||
<g transform="translate(0,280)">
|
||||
<text x="170" y="30" text-anchor="end" font-weight="700" fill="#111">安全 / 风控</text>
|
||||
<rect x="180" y="10" width="520" height="28" rx="14" fill="#f4efff"/>
|
||||
<rect x="180" y="10" width="500" height="28" rx="14" fill="#7b61ff"/>
|
||||
<text x="710" y="30" fill="#5b21b6" font-weight="700">+110% 价值</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="xw-footer"><span>Chart · Horizontal Bars</span><span>06 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 7. CTA -->
|
||||
<section class="slide">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>今晚就可以做的三件事</div>
|
||||
<div class="xw-page">07 / 08</div>
|
||||
</div>
|
||||
<h2 class="xw-title-md">别再追工具,<br>开始练 <span class="xw-grad">判断力</span></h2>
|
||||
<div class="xw-grid-3">
|
||||
<div class="xw-card soft-purple"><div class="xw-label">Tonight</div><div class="main">写一份<br>验收清单</div><div class="desc">哪怕只有 5 条,开始比完美更重要</div></div>
|
||||
<div class="xw-card soft-blue"><div class="xw-label">This week</div><div class="main">跑一遍<br>红队演练</div><div class="desc">对自己的 agent 说:试着让它出事</div></div>
|
||||
<div class="xw-card soft-green"><div class="xw-label">This month</div><div class="main">加一条<br>回滚流程</div><div class="desc">60 秒内能撤销,你就敢把手放开</div></div>
|
||||
</div>
|
||||
<div class="xw-hero"><div class="xw-quote" style="font-size:32px">真正的稀缺,不是「会用 AI」,<br>而是 <span class="xw-focus">「敢为 AI 的结果签字」</span>。</div></div>
|
||||
<div class="xw-footer"><span>CTA</span><span>07 / 08</span></div>
|
||||
</section>
|
||||
|
||||
<!-- 8. THANKS -->
|
||||
<section class="slide">
|
||||
<div class="xw-topline"></div>
|
||||
<div class="xw-topbar">
|
||||
<div class="xw-tag"><span class="dot"></span>Thanks for reading</div>
|
||||
<div class="xw-page">08 / 08</div>
|
||||
</div>
|
||||
<div style="margin-top:100px">
|
||||
<div class="xw-big-stat xw-grad">谢谢<small> · thanks</small></div>
|
||||
<p class="xw-sub" style="font-size:28px;margin-top:36px">如果你也在想这些问题,欢迎在评论里告诉我——<br>你最想让 AI 帮你做什么?你最不放心它做什么?</p>
|
||||
<div style="margin-top:40px">
|
||||
<span class="xw-pill">@lewis</span>
|
||||
<span class="xw-pill">小红书 · 白底杂志风</span>
|
||||
<span class="xw-pill">html-ppt · full-deck</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="xw-footer"><span>End</span><span>08 / 08</span></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
# Security scan allowlist for html-ppt-skill
|
||||
# These patterns are false positives from template content, not actual threats.
|
||||
|
||||
# Path traversal: templates reference shared assets via relative paths
|
||||
# e.g. templates/full-decks/weekly-report/ → ../../../assets/
|
||||
# This is the correct relative path to the skill root assets directory.
|
||||
traversal:templates/full-decks/*/index.html
|
||||
|
||||
# Destructive commands: testing-safety-alert template displays forbidden
|
||||
# commands as text examples in a security policy demo slide.
|
||||
# They are HTML content, not executable code.
|
||||
destructive:templates/full-decks/testing-safety-alert/index.html
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 lewis <sudolewis@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,234 @@
|
||||
# html-ppt — HTML PPT Studio
|
||||
|
||||
> A world-class AgentSkill for producing professional HTML presentations in
|
||||
> **36 themes**, **15 full-deck templates**, **31 page layouts**,
|
||||
> **47 animations** (27 CSS + 20 canvas FX), and a **true presenter mode**
|
||||
> with pixel-perfect previews + speaker script + timer — all pure static
|
||||
> HTML/CSS/JS, no build step.
|
||||
|
||||
**Author:** lewis <sudolewis@gmail.com>
|
||||
**License:** MIT
|
||||
**中文文档:** [README.zh-CN.md](README.zh-CN.md)
|
||||
|
||||

|
||||
|
||||
> One command installs **36 themes × 20 canvas FX × 31 layouts × 15 full decks + presenter mode**. Every preview above is a live iframe of a real template file rendering inside the deck — no screenshots, no mock-ups.
|
||||
|
||||
## 🎤 Presenter Mode (new!)
|
||||
|
||||
Press `S` on any deck to pop open a dedicated presenter window with four
|
||||
draggable, resizable **magnetic cards**: current slide, next slide preview,
|
||||
speaker script (逐字稿), and timer. Two windows stay in sync via
|
||||
`BroadcastChannel`.
|
||||
|
||||

|
||||
|
||||
**Why previews are pixel-perfect:** each card is an `<iframe>` that loads the
|
||||
same deck HTML with a `?preview=N` query param. The runtime detects this and
|
||||
renders only slide N with no chrome — so the preview uses the **same CSS,
|
||||
theme, fonts and viewport** as the audience view. Colors and layout are
|
||||
guaranteed identical.
|
||||
|
||||
**Smooth (no-reload) navigation:** on slide change, the presenter window
|
||||
sends `postMessage({type:'preview-goto', idx:N})` to each iframe. The iframe
|
||||
just toggles `.is-active` between slides — **no reload, no flicker**.
|
||||
|
||||
**Speaker script rules (3 golden):**
|
||||
1. **Prompt signals, not lines to read** — bold the keywords, separate
|
||||
transition sentences into their own paragraphs
|
||||
2. **150–300 words per slide** — that's the ~2–3 min/page pace
|
||||
3. **Write it like you speak** — conversational, not written prose
|
||||
|
||||
See [`references/presenter-mode.md`](references/presenter-mode.md) for the
|
||||
full authoring guide, or copy the ready-made template at
|
||||
`templates/full-decks/presenter-mode-reveal/` which ships with full 150-300
|
||||
word speaker scripts on every slide.
|
||||
|
||||
## Install (one command)
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/lewislulu/html-ppt-skill
|
||||
```
|
||||
|
||||
That registers the skill with your agent runtime. After install, any agent
|
||||
that supports AgentSkills can author presentations by asking things like:
|
||||
|
||||
> "做一份 8 页的技术分享 slides,用 cyberpunk 主题"
|
||||
> "turn this outline into a pitch deck"
|
||||
> "做一个小红书图文,9 张,白底柔和风"
|
||||
|
||||
## What's in the box
|
||||
|
||||
| | Count | Where |
|
||||
|---|---|---|
|
||||
| 🎤 **Presenter mode** | **NEW** | `S` key / `?preview=N` |
|
||||
| 🎨 **Themes** | **36** | `assets/themes/*.css` |
|
||||
| 📑 **Full-deck templates** | **15** | `templates/full-decks/<name>/` |
|
||||
| 🧩 **Single-page layouts** | **31** | `templates/single-page/*.html` |
|
||||
| ✨ **CSS animations** | **27** | `assets/animations/animations.css` |
|
||||
| 💥 **Canvas FX animations** | **20** | `assets/animations/fx/*.js` |
|
||||
| 🖼️ **Showcase decks** | 4 | `templates/*-showcase.html` |
|
||||
| 📸 **Verification screenshots** | 56 | `scripts/verify-output/` |
|
||||
|
||||
### 36 Themes
|
||||
|
||||
`minimal-white`, `editorial-serif`, `soft-pastel`, `sharp-mono`, `arctic-cool`,
|
||||
`sunset-warm`, `catppuccin-latte`, `catppuccin-mocha`, `dracula`, `tokyo-night`,
|
||||
`nord`, `solarized-light`, `gruvbox-dark`, `rose-pine`, `neo-brutalism`,
|
||||
`glassmorphism`, `bauhaus`, `swiss-grid`, `terminal-green`, `xiaohongshu-white`,
|
||||
`rainbow-gradient`, `aurora`, `blueprint`, `memphis-pop`, `cyberpunk-neon`,
|
||||
`y2k-chrome`, `retro-tv`, `japanese-minimal`, `vaporwave`, `midcentury`,
|
||||
`corporate-clean`, `academic-paper`, `news-broadcast`, `pitch-deck-vc`,
|
||||
`magazine-bold`, `engineering-whiteprint`.
|
||||
|
||||

|
||||
|
||||
Each is a pure CSS-tokens file — swap one `<link>` to reskin the entire deck.
|
||||
Browse them all in `templates/theme-showcase.html` (each slide rendered in an
|
||||
isolated iframe so theme ≠ theme is visually guaranteed).
|
||||
|
||||

|
||||
|
||||
### 15 Full-deck templates
|
||||
|
||||
Eight extracted from real-world decks, seven generic scenario scaffolds:
|
||||
|
||||
**Extracted looks**
|
||||
- `xhs-white-editorial` — 小红书白底杂志风
|
||||
- `graphify-dark-graph` — 暗底 + 力导向知识图谱
|
||||
- `knowledge-arch-blueprint` — 蓝图 / 架构图风
|
||||
- `hermes-cyber-terminal` — 终端 cyberpunk
|
||||
- `obsidian-claude-gradient` — 紫色渐变卡
|
||||
- `testing-safety-alert` — 红 / 琥珀警示风
|
||||
- `xhs-pastel-card` — 柔和马卡龙图文
|
||||
- `dir-key-nav-minimal` — 方向键极简
|
||||
|
||||
**Scenario decks**
|
||||
- `pitch-deck`, `product-launch`, `tech-sharing`, `weekly-report`,
|
||||
`xhs-post` (9-slide 3:4), `course-module`,
|
||||
**`presenter-mode-reveal`** 🎤 — complete talk template with full 150-300
|
||||
word speaker scripts on every slide, designed around the `S` key presenter mode
|
||||
|
||||
Each is a self-contained folder with scoped `.tpl-<name>` CSS so multiple
|
||||
decks can be previewed side-by-side without collisions. Browse the full
|
||||
gallery in `templates/full-decks-index.html`.
|
||||
|
||||

|
||||
|
||||
### 31 Single-page layouts
|
||||
|
||||
cover · toc · section-divider · bullets · two-column · three-column ·
|
||||
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
|
||||
flow-diagram · timeline · roadmap · mindmap · comparison · pros-cons ·
|
||||
todo-checklist · gantt · image-hero · image-grid · chart-bar · chart-line ·
|
||||
chart-pie · chart-radar · arch-diagram · process-steps · cta · thanks
|
||||
|
||||
Every layout ships with realistic demo data so you can drop it into a deck
|
||||
and immediately see it render.
|
||||
|
||||

|
||||
|
||||
*The big iframe is loading `templates/single-page/<name>.html` directly and cycling through all 31 layouts every 2.8 seconds.*
|
||||
|
||||

|
||||
|
||||
### 27 CSS animations + 20 Canvas FX
|
||||
|
||||
**CSS (lightweight)** — directional fades, `rise-in`, `zoom-pop`, `blur-in`,
|
||||
`glitch-in`, `typewriter`, `neon-glow`, `shimmer-sweep`, `gradient-flow`,
|
||||
`stagger-list`, `counter-up`, `path-draw`, `morph-shape`, `parallax-tilt`,
|
||||
`card-flip-3d`, `cube-rotate-3d`, `page-turn-3d`, `perspective-zoom`,
|
||||
`marquee-scroll`, `kenburns`, `ripple-reveal`, `spotlight`, …
|
||||
|
||||
**Canvas FX (cinematic)** — `particle-burst`, `confetti-cannon`, `firework`,
|
||||
`starfield`, `matrix-rain`, `knowledge-graph` (force-directed physics),
|
||||
`neural-net` (signal pulses), `constellation`, `orbit-ring`, `galaxy-swirl`,
|
||||
`word-cascade`, `letter-explode`, `chain-react`, `magnetic-field`,
|
||||
`data-stream`, `gradient-blob`, `sparkle-trail`, `shockwave`,
|
||||
`typewriter-multi`, `counter-explosion`. Each is a real hand-rolled canvas
|
||||
module auto-initialised on slide enter via `fx-runtime.js`.
|
||||
|
||||
## Quick start (manual, after install or git clone)
|
||||
|
||||
```bash
|
||||
# Scaffold a new deck from the base template
|
||||
./scripts/new-deck.sh my-talk
|
||||
|
||||
# Browse everything
|
||||
open templates/theme-showcase.html # all 36 themes (iframe-isolated)
|
||||
open templates/layout-showcase.html # all 31 layouts
|
||||
open templates/animation-showcase.html # all 47 animations
|
||||
open templates/full-decks-index.html # all 14 full decks
|
||||
|
||||
# Render any template to PNG via headless Chrome
|
||||
./scripts/render.sh templates/theme-showcase.html
|
||||
./scripts/render.sh examples/my-talk/index.html 12
|
||||
```
|
||||
|
||||
## Keyboard cheat sheet
|
||||
|
||||
```
|
||||
← → Space PgUp PgDn Home End navigate
|
||||
F fullscreen
|
||||
S open presenter window (magnetic cards)
|
||||
N quick notes drawer (bottom)
|
||||
R reset timer (in presenter window)
|
||||
O slide overview grid
|
||||
T cycle themes (syncs to presenter)
|
||||
A cycle a demo animation on current slide
|
||||
#/N (URL) deep-link to slide N
|
||||
?preview=N (URL) preview-only mode (single slide, no chrome)
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
html-ppt-skill/
|
||||
├── SKILL.md agent-facing dispatcher
|
||||
├── README.md this file
|
||||
├── references/ detailed catalogs
|
||||
│ ├── themes.md 36 themes with when-to-use
|
||||
│ ├── layouts.md 31 layout types
|
||||
│ ├── animations.md 27 CSS + 20 FX catalog
|
||||
│ ├── full-decks.md 14 full-deck templates
|
||||
│ └── authoring-guide.md full workflow
|
||||
├── assets/
|
||||
│ ├── base.css shared tokens + primitives
|
||||
│ ├── fonts.css webfont imports
|
||||
│ ├── runtime.js keyboard + presenter + overview
|
||||
│ ├── themes/*.css 36 theme token files
|
||||
│ └── animations/
|
||||
│ ├── animations.css 27 named CSS animations
|
||||
│ ├── fx-runtime.js auto-init [data-fx] on slide enter
|
||||
│ └── fx/*.js 20 canvas FX modules
|
||||
├── templates/
|
||||
│ ├── deck.html minimal starter
|
||||
│ ├── theme-showcase.html iframe-isolated theme tour
|
||||
│ ├── layout-showcase.html all 31 layouts
|
||||
│ ├── animation-showcase.html 47 animation slides
|
||||
│ ├── full-decks-index.html 14-deck gallery
|
||||
│ ├── full-decks/<name>/ 14 scoped multi-slide decks
|
||||
│ └── single-page/*.html 31 layout files with demo data
|
||||
├── scripts/
|
||||
│ ├── new-deck.sh scaffold
|
||||
│ ├── render.sh headless Chrome → PNG
|
||||
│ └── verify-output/ 56 self-test screenshots
|
||||
└── examples/demo-deck/ complete working deck
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
|
||||
- **Token-driven design system.** All color, radius, shadow, font decisions
|
||||
live in `assets/base.css` + the current theme file. Change one variable,
|
||||
the whole deck reflows tastefully.
|
||||
- **Iframe isolation for previews.** Theme / layout / full-deck showcases all
|
||||
use `<iframe>` per slide so each preview is a real, independent render.
|
||||
- **Zero build.** Pure static HTML/CSS/JS. CDN only for webfonts, highlight.js
|
||||
and chart.js (optional).
|
||||
- **Senior-designer defaults.** Opinionated type scale, spacing rhythm,
|
||||
gradients and card treatments — no "Corporate PowerPoint 2006" vibes.
|
||||
- **Chinese + English first-class.** Noto Sans SC / Noto Serif SC pre-imported.
|
||||
|
||||
## License
|
||||
|
||||
MIT © 2026 lewis <sudolewis@gmail.com>.
|
||||
@@ -0,0 +1,238 @@
|
||||
# html-ppt · HTML PPT 工作室
|
||||
|
||||
> 一款专业级的 AgentSkill,让 AI 做出真正能打的 HTML 演示文稿。
|
||||
> **36 套主题**、**15 套完整 deck 模板**、**31 种页面布局**、**47 个动效**
|
||||
> (27 个 CSS + 20 个 Canvas FX),加上全新的 **演讲者模式** —— 像素级
|
||||
> 完美预览 + 逐字稿提词器 + 计时器。纯静态 HTML/CSS/JS,无需构建。
|
||||
|
||||
**作者:** lewis <sudolewis@gmail.com>
|
||||
**协议:** MIT
|
||||
**English docs:** [README.md](README.md)
|
||||
|
||||

|
||||
|
||||
> 一行命令装好 **36 主题 × 20 Canvas FX × 31 布局 × 15 完整 deck + 演讲者模式**。
|
||||
> 上图里的每一个预览都是真实的 iframe 加载真实模板文件 —— 不是截图,不是色卡。
|
||||
|
||||
## 🎤 演讲者模式(全新)
|
||||
|
||||
在任何 deck 里按 `S` 键,弹出一个独立的演讲者窗口,包含 4 个**可拖拽、
|
||||
可调整大小的磁吸卡片**:当前页预览、下一页预览、逐字稿、计时器。两个窗口
|
||||
通过 `BroadcastChannel` 双向同步翻页。
|
||||
|
||||

|
||||
|
||||
**为什么预览是像素级完美的:** 每个卡片是一个 `<iframe>`,加载的是**同一
|
||||
份 deck HTML 文件**,只是 URL 多了 `?preview=N` 参数。runtime 检测到这个
|
||||
参数后,只渲染第 N 页并隐藏所有 chrome —— 所以预览使用**和观众视图完全相
|
||||
同的 CSS、主题、字体、viewport**,颜色和排版保证 100% 一致。
|
||||
|
||||
**丝滑翻页(零闪烁):** 翻页时演讲者窗口通过 `postMessage({type:'preview-goto',
|
||||
idx:N})` 通知 iframe,iframe 只是切换 `.is-active` class —— **不重新加载、
|
||||
不白屏、不闪烁**。
|
||||
|
||||
**逐字稿 3 条铁律:**
|
||||
1. **提示信号,不是讲稿** — 关键词加粗,过渡句独立成段
|
||||
2. **每页 150–300 字** — 约 2–3 分钟/页的节奏
|
||||
3. **用口语,不用书面语** — "所以" 不是 "因此","这个" 不是 "该"
|
||||
|
||||
详见 [`references/presenter-mode.md`](references/presenter-mode.md),或直接复制
|
||||
`templates/full-decks/presenter-mode-reveal/` 这个现成模板 —— 每一页都带完整
|
||||
150–300 字的示例逐字稿。
|
||||
|
||||
## 一行命令安装
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/lewislulu/html-ppt-skill
|
||||
```
|
||||
|
||||
装好后,任何支持 AgentSkill 的 agent(Claude Code / Codex / Cursor / OpenClaw 等)
|
||||
都能用这套能力做 PPT。对 agent 说:
|
||||
|
||||
> "做一份 8 页的技术分享 slides,用 cyberpunk 主题"
|
||||
> "把这段 outline 变成投资人 pitch deck"
|
||||
> "做一个小红书图文,9 张,白底柔和风"
|
||||
> "做一份带演讲者模式的产品分享,我想要有逐字稿"
|
||||
|
||||
## Skill 内容一览
|
||||
|
||||
| | 数量 | 位置 |
|
||||
|---|---|---|
|
||||
| 🎤 **演讲者模式** | **新增** | `S` 键 / `?preview=N` |
|
||||
| 🎨 **主题** | **36** | `assets/themes/*.css` |
|
||||
| 📑 **完整 deck 模板** | **15** | `templates/full-decks/<name>/` |
|
||||
| 🧩 **单页布局** | **31** | `templates/single-page/*.html` |
|
||||
| ✨ **CSS 动画** | **27** | `assets/animations/animations.css` |
|
||||
| 💥 **Canvas FX 动画** | **20** | `assets/animations/fx/*.js` |
|
||||
| 🖼️ **Showcase deck** | 4 | `templates/*-showcase.html` |
|
||||
| 📸 **验证截图** | 56 | `scripts/verify-output/` |
|
||||
|
||||
### 36 套主题
|
||||
|
||||
`minimal-white`、`editorial-serif`、`soft-pastel`、`sharp-mono`、`arctic-cool`、
|
||||
`sunset-warm`、`catppuccin-latte`、`catppuccin-mocha`、`dracula`、`tokyo-night`、
|
||||
`nord`、`solarized-light`、`gruvbox-dark`、`rose-pine`、`neo-brutalism`、
|
||||
`glassmorphism`、`bauhaus`、`swiss-grid`、`terminal-green`、`xiaohongshu-white`、
|
||||
`rainbow-gradient`、`aurora`、`blueprint`、`memphis-pop`、`cyberpunk-neon`、
|
||||
`y2k-chrome`、`retro-tv`、`japanese-minimal`、`vaporwave`、`midcentury`、
|
||||
`corporate-clean`、`academic-paper`、`news-broadcast`、`pitch-deck-vc`、
|
||||
`magazine-bold`、`engineering-whiteprint`
|
||||
|
||||

|
||||
|
||||
每个主题都是一份纯 CSS token 文件 —— 只需要换一行 `<link>` 就能给整份 deck
|
||||
换皮。在 `templates/theme-showcase.html` 里可以浏览全部(每一页用独立 iframe
|
||||
渲染,避免样式互相污染)。
|
||||
|
||||

|
||||
|
||||
### 15 套完整 deck 模板
|
||||
|
||||
8 个从真实作品提炼的视觉语言,7 个通用场景脚手架:
|
||||
|
||||
**提炼款**
|
||||
- `xhs-white-editorial` — 小红书白底杂志风
|
||||
- `graphify-dark-graph` — 暗底 + 力导向知识图谱
|
||||
- `knowledge-arch-blueprint` — 蓝图 / 架构图风
|
||||
- `hermes-cyber-terminal` — 终端 cyberpunk 风
|
||||
- `obsidian-claude-gradient` — 紫色渐变卡
|
||||
- `testing-safety-alert` — 红 / 琥珀警示风
|
||||
- `xhs-pastel-card` — 柔和马卡龙图文
|
||||
- `dir-key-nav-minimal` — 方向键极简
|
||||
|
||||
**场景款**
|
||||
- `pitch-deck` — 投资人 pitch
|
||||
- `product-launch` — 产品发布会
|
||||
- `tech-sharing` — 技术分享
|
||||
- `weekly-report` — 周报
|
||||
- `xhs-post` — 小红书图文(9 页 3:4)
|
||||
- `course-module` — 教学模块
|
||||
- **`presenter-mode-reveal`** 🎤 — 完整分享模板,**每一页都带 150-300 字
|
||||
的示例逐字稿**,围绕 `S` 键演讲者模式专门设计
|
||||
|
||||
每个模板都是自包含的文件夹,用 scoped `.tpl-<name>` CSS,所以多个模板可以
|
||||
同时加载不会互相污染。在 `templates/full-decks-index.html` 可以看全套 gallery。
|
||||
|
||||

|
||||
|
||||
### 31 种单页布局
|
||||
|
||||
cover · toc · section-divider · bullets · two-column · three-column ·
|
||||
big-quote · stat-highlight · kpi-grid · table · code · diff · terminal ·
|
||||
flow-diagram · timeline · roadmap · mindmap · comparison · pros-cons ·
|
||||
todo-checklist · gantt · image-hero · image-grid · chart-bar · chart-line ·
|
||||
chart-pie · chart-radar · arch-diagram · process-steps · cta · thanks
|
||||
|
||||
每个布局都带真实的示例数据,拖进 deck 立即看得到效果。
|
||||
|
||||

|
||||
|
||||
*大 iframe 直接加载 `templates/single-page/<name>.html` 文件,每 2.8 秒
|
||||
自动切换到下一个布局。*
|
||||
|
||||

|
||||
|
||||
### 27 个 CSS 动画 + 20 个 Canvas FX
|
||||
|
||||
**CSS 动画(轻量)** — 方向性淡入、`rise-in`、`zoom-pop`、`blur-in`、
|
||||
`glitch-in`、`typewriter`(打字机)、`neon-glow`(霓虹光晕)、
|
||||
`shimmer-sweep`(流光)、`gradient-flow`(渐变流动)、`stagger-list`
|
||||
(列表错开入场)、`counter-up`(数字滚动)、`path-draw`(路径绘制)、
|
||||
`morph-shape`、`parallax-tilt`、`card-flip-3d`、`cube-rotate-3d`、
|
||||
`page-turn-3d`、`perspective-zoom`、`marquee-scroll`、`kenburns`、
|
||||
`ripple-reveal`、`spotlight`、…
|
||||
|
||||
**Canvas FX(电影级)** — `particle-burst`(粒子爆发)、`confetti-cannon`
|
||||
(彩带)、`firework`(烟花)、`starfield`(星空)、`matrix-rain`
|
||||
(代码雨)、`knowledge-graph`(力导向知识图谱)、`neural-net`(神经网络
|
||||
脉冲)、`constellation`(星座连线)、`orbit-ring`(轨道环)、
|
||||
`galaxy-swirl`(星系漩涡)、`word-cascade`、`letter-explode`、
|
||||
`chain-react`、`magnetic-field`、`data-stream`、`gradient-blob`、
|
||||
`sparkle-trail`、`shockwave`、`typewriter-multi`、`counter-explosion`。
|
||||
每一个都是手写的 canvas 模块,进入 slide 时由 `fx-runtime.js` 自动初始化。
|
||||
|
||||
## 快速开始(手动 / 安装后 / git clone 后)
|
||||
|
||||
```bash
|
||||
# 从 base 模板新建一个 deck
|
||||
./scripts/new-deck.sh my-talk
|
||||
|
||||
# 浏览所有内容
|
||||
open templates/theme-showcase.html # 全部 36 主题(iframe 隔离)
|
||||
open templates/layout-showcase.html # 全部 31 布局
|
||||
open templates/animation-showcase.html # 全部 47 动效
|
||||
open templates/full-decks-index.html # 全部 15 个完整 deck
|
||||
|
||||
# 用 headless Chrome 导出 PNG
|
||||
./scripts/render.sh templates/theme-showcase.html
|
||||
./scripts/render.sh examples/my-talk/index.html 12
|
||||
```
|
||||
|
||||
## 键盘快捷键
|
||||
|
||||
```
|
||||
← → Space PgUp PgDn Home End 翻页
|
||||
F 全屏
|
||||
S 打开演讲者窗口(磁吸卡片模式)
|
||||
N 底部 notes 抽屉
|
||||
R 重置计时器(演讲者窗口内)
|
||||
O slide 总览网格
|
||||
T 切换主题(自动同步到演讲者窗口)
|
||||
A 在当前 slide 循环演示一个动画
|
||||
#/N (URL) 深链到第 N 页
|
||||
?preview=N (URL) 预览模式(只显示单页,隐藏 chrome)
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
html-ppt-skill/
|
||||
├── SKILL.md agent 入口
|
||||
├── README.md 英文 README
|
||||
├── README.zh-CN.md 本文件
|
||||
├── references/ 详细文档
|
||||
│ ├── themes.md 36 主题 + 使用场景
|
||||
│ ├── layouts.md 31 布局
|
||||
│ ├── animations.md 27 CSS + 20 FX 目录
|
||||
│ ├── full-decks.md 15 完整 deck 模板
|
||||
│ ├── presenter-mode.md 🎤 演讲者模式 + 逐字稿指南
|
||||
│ └── authoring-guide.md 完整工作流
|
||||
├── assets/
|
||||
│ ├── base.css 共享 tokens + 基础组件
|
||||
│ ├── fonts.css web 字体引入
|
||||
│ ├── runtime.js 键盘导航 + 演讲者模式 + 总览
|
||||
│ ├── themes/*.css 36 主题 token 文件
|
||||
│ └── animations/
|
||||
│ ├── animations.css 27 个命名 CSS 动画
|
||||
│ ├── fx-runtime.js 进入 slide 自动初始化 [data-fx]
|
||||
│ └── fx/*.js 20 个 Canvas FX 模块
|
||||
├── templates/
|
||||
│ ├── deck.html 最小起步模板
|
||||
│ ├── theme-showcase.html iframe 隔离的主题 tour
|
||||
│ ├── layout-showcase.html 全部 31 布局
|
||||
│ ├── animation-showcase.html 47 动画 slide
|
||||
│ ├── full-decks-index.html 15 deck gallery
|
||||
│ ├── full-decks/<name>/ 15 个 scoped 多页 deck 模板
|
||||
│ └── single-page/*.html 31 个布局文件(带示例数据)
|
||||
├── scripts/
|
||||
│ ├── new-deck.sh 脚手架
|
||||
│ ├── render.sh headless Chrome → PNG
|
||||
│ └── verify-output/ 56 张自测截图
|
||||
└── examples/demo-deck/ 完整可运行的示例 deck
|
||||
```
|
||||
|
||||
## 设计理念
|
||||
|
||||
- **Token 驱动的设计系统。** 所有颜色、圆角、阴影、字体决策都在
|
||||
`assets/base.css` + 当前主题文件里。改一个变量,整份 deck 优雅地重排。
|
||||
- **Iframe 隔离预览。** 主题 / 布局 / 完整 deck 的 showcase 都用 `<iframe>`,
|
||||
确保每个预览都是真实、独立的渲染结果。
|
||||
- **零构建。** 纯静态 HTML/CSS/JS。只有 webfont / highlight.js / chart.js
|
||||
(可选) 走 CDN。
|
||||
- **资深设计师的默认值。** 字号规律、间距节奏、渐变、卡片处理都有态度 ——
|
||||
绝不是 "PowerPoint 2006" 那种味道。
|
||||
- **中英双语一等公民。** 预导入了 Noto Sans SC / Noto Serif SC。
|
||||
|
||||
## 协议
|
||||
|
||||
MIT © 2026 lewis <sudolewis@gmail.com>
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
name: html-ppt
|
||||
description: HTML PPT Studio — author professional static HTML presentations in many styles, layouts, and animations, all driven by templates. Use when the user asks for a presentation, PPT, slides, keynote, deck, slideshow, "幻灯片", "演讲稿", "做一份 PPT", "做一份 slides", a reveal-style HTML deck, a 小红书 图文, or any kind of multi-slide pitch/report/sharing document that should look tasteful and be usable with keyboard navigation. Triggers include keywords like "presentation", "ppt", "slides", "deck", "keynote", "reveal", "slideshow", "幻灯片", "演讲稿", "分享稿", "小红书图文", "talk slides", "pitch deck", "tech sharing", "technical presentation".
|
||||
triggers:
|
||||
- "ppt"
|
||||
- "deck"
|
||||
- "slides"
|
||||
- "presentation"
|
||||
- "keynote"
|
||||
- "reveal"
|
||||
- "slideshow"
|
||||
- "幻灯片"
|
||||
- "演讲稿"
|
||||
- "分享稿"
|
||||
- "talk slides"
|
||||
- "pitch deck"
|
||||
- "tech sharing"
|
||||
- "technical presentation"
|
||||
od:
|
||||
mode: deck
|
||||
scenario: marketing
|
||||
featured: 19
|
||||
upstream: "https://github.com/lewislulu/html-ppt-skill"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
speaker_notes: true
|
||||
animations: true
|
||||
example_prompt: "用 html-ppt 做一份 12 页的 HTML PPT。先帮我确认三件事:内容/页数/受众、主题(从 36 套里推荐 2-3 个)、起点全 deck 模板(pitch-deck / tech-sharing / weekly-report / xhs-post / presenter-mode-reveal 任选一个),对齐之后再开始写 slides。"
|
||||
---
|
||||
|
||||
# html-ppt — HTML PPT Studio
|
||||
|
||||
Author professional HTML presentations as static files. One theme file = one
|
||||
look. One layout file = one page type. One animation class = one entry effect.
|
||||
All pages share a token-based design system in `assets/base.css`.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/lewislulu/html-ppt-skill
|
||||
```
|
||||
|
||||
One command, no build. Pure static HTML/CSS/JS with only CDN webfonts.
|
||||
|
||||
## What the skill gives you
|
||||
|
||||
- **36 themes** (`assets/themes/*.css`) — minimal-white, editorial-serif, soft-pastel, sharp-mono, arctic-cool, sunset-warm, catppuccin-latte/mocha, dracula, tokyo-night, nord, solarized-light, gruvbox-dark, rose-pine, neo-brutalism, glassmorphism, bauhaus, swiss-grid, terminal-green, xiaohongshu-white, rainbow-gradient, aurora, blueprint, memphis-pop, cyberpunk-neon, y2k-chrome, retro-tv, japanese-minimal, vaporwave, midcentury, corporate-clean, academic-paper, news-broadcast, pitch-deck-vc, magazine-bold, engineering-whiteprint
|
||||
- **15 full-deck templates** (`templates/full-decks/<name>/`) — complete multi-slide decks with scoped `.tpl-<name>` CSS. 8 extracted from real-world decks (xhs-white-editorial, graphify-dark-graph, knowledge-arch-blueprint, hermes-cyber-terminal, obsidian-claude-gradient, testing-safety-alert, xhs-pastel-card, dir-key-nav-minimal), 7 scenario scaffolds (pitch-deck, product-launch, tech-sharing, weekly-report, xhs-post 3:4, course-module, **presenter-mode-reveal** — 演讲者模式专用)
|
||||
- **31 layouts** (`templates/single-page/*.html`) with realistic demo data
|
||||
- **27 CSS animations** (`assets/animations/animations.css`) via `data-anim`
|
||||
- **20 canvas FX animations** (`assets/animations/fx/*.js`) via `data-fx` — particle-burst, confetti-cannon, firework, starfield, matrix-rain, knowledge-graph (force-directed), neural-net (pulses), constellation, orbit-ring, galaxy-swirl, word-cascade, letter-explode, chain-react, magnetic-field, data-stream, gradient-blob, sparkle-trail, shockwave, typewriter-multi, counter-explosion
|
||||
- **Keyboard runtime** (`assets/runtime.js`) — arrows, T (theme), A (anim), F/O, **S (presenter mode: magnetic-card popup with CURRENT / NEXT / SCRIPT / TIMER cards)**, N (notes drawer), R (reset timer in presenter)
|
||||
- **FX runtime** (`assets/animations/fx-runtime.js`) — auto-inits `[data-fx]` on slide enter, cleans up on leave
|
||||
- **Showcase decks** for themes / layouts / animations / full-decks gallery
|
||||
- **Headless Chrome render script** for PNG export
|
||||
|
||||
## When to use
|
||||
|
||||
Use when the user asks for any kind of slide-based output or wants to turn
|
||||
text/notes into a presentable deck. Prefer this over building from scratch.
|
||||
|
||||
### 🎤 Presenter Mode (演讲者模式 + 逐字稿)
|
||||
|
||||
If the user mentions any of: **演讲 / 分享 / 讲稿 / 逐字稿 / speaker notes / presenter view / 演讲者视图 / 提词器**, or says things like "我要去给团队讲 xxx", "要做一场技术分享", "怕讲不流畅", "想要一份带逐字稿的 PPT" — **use the `presenter-mode-reveal` full-deck template** and write 150–300 words of 逐字稿 in each slide's `<aside class="notes">`.
|
||||
|
||||
See [references/presenter-mode.md](references/presenter-mode.md) for the full authoring guide including the 3 rules of speaker script writing:
|
||||
1. **不是讲稿,是提示信号** — 加粗核心词 + 过渡句独立成段
|
||||
2. **每页 150–300 字** — 2–3 分钟/页的节奏
|
||||
3. **用口语,不用书面语** — "因此"→"所以","该方案"→"这个方案"
|
||||
|
||||
All full-deck templates support the S key presenter mode (it's built into `runtime.js`). **S opens a new popup window with 4 magnetic cards**:
|
||||
- 🔵 **CURRENT** — pixel-perfect iframe preview of the current slide
|
||||
- 🟣 **NEXT** — pixel-perfect iframe preview of the next slide
|
||||
- 🟠 **SPEAKER SCRIPT** — large-font 逐字稿 (scrollable)
|
||||
- 🟢 **TIMER** — elapsed time + slide counter + prev/next/reset buttons
|
||||
|
||||
Each card is **draggable by its header** and **resizable by the bottom-right corner handle**. Card positions/sizes persist to `localStorage` per deck. A "Reset layout" button restores the default arrangement.
|
||||
|
||||
**Why the previews are pixel-perfect**: each preview is an `<iframe>` that loads the actual deck HTML with a `?preview=N` query param; `runtime.js` detects this and renders only slide N with no chrome. So the preview uses the **same CSS, theme, fonts, and viewport as the audience view** — colors and layout are guaranteed identical.
|
||||
|
||||
**Smooth navigation**: on slide change, the presenter window sends `postMessage({type:'preview-goto', idx:N})` to each iframe. The iframe just toggles `.is-active` between slides — **no reload, no flicker**. The two windows also stay in sync via `BroadcastChannel`.
|
||||
|
||||
Only `presenter-mode-reveal` is designed from the ground up around the feature with proper example 逐字稿 on every slide.
|
||||
|
||||
Keyboard in presenter window: `← →` navigate (syncs audience) · `R` reset timer · `Esc` close popup.
|
||||
Keyboard in audience window: `S` open presenter · `T` cycle theme · `← →` navigate (syncs presenter) · `F` fullscreen · `O` overview.
|
||||
|
||||
## Before you author anything — ALWAYS ask or recommend
|
||||
|
||||
**Do not start writing slides until you understand three things.** Either ask
|
||||
the user directly, or — if they already handed you rich content — propose a
|
||||
tasteful default and confirm.
|
||||
|
||||
1. **Content & audience.** What's the deck about, how many slides, who's
|
||||
watching (engineers / execs / 小红书读者 / 学生 / VC)?
|
||||
2. **Style / theme.** Which of the 36 themes fits? If unsure, recommend 2-3
|
||||
candidates based on tone:
|
||||
- Business / investor pitch → `pitch-deck-vc`, `corporate-clean`, `swiss-grid`
|
||||
- Tech sharing / engineering → `tokyo-night`, `dracula`, `catppuccin-mocha`,
|
||||
`terminal-green`, `blueprint`
|
||||
- 小红书图文 → `xiaohongshu-white`, `soft-pastel`, `rainbow-gradient`,
|
||||
`magazine-bold`
|
||||
- Academic / report → `academic-paper`, `editorial-serif`, `minimal-white`
|
||||
- Edgy / cyber / launch → `cyberpunk-neon`, `vaporwave`, `y2k-chrome`,
|
||||
`neo-brutalism`
|
||||
3. **Starting point.** One of the 14 full-deck templates, or scratch? Point
|
||||
to the closest `templates/full-decks/<name>/` and ask if it fits. If the
|
||||
user's content suggests something obvious (e.g. "我要做产品发布会" →
|
||||
`product-launch`), propose it confidently instead of asking blindly.
|
||||
|
||||
A good opening message looks like:
|
||||
|
||||
> 我可以给你做这份 PPT!先确认三件事:
|
||||
> 1. 大致内容 / 页数 / 观众是谁?
|
||||
> 2. 风格偏好?我建议从这 3 个主题里选一个:`tokyo-night`(技术分享默认好看)、`xiaohongshu-white`(小红书风)、`corporate-clean`(正式汇报)。
|
||||
> 3. 要不要用我现成的 `tech-sharing` 全 deck 模板打底?
|
||||
|
||||
Only after those are clear, scaffold the deck and start writing.
|
||||
|
||||
## Quick start
|
||||
|
||||
1. **Scaffold a new deck.** From the repo root:
|
||||
```bash
|
||||
./scripts/new-deck.sh my-talk
|
||||
open examples/my-talk/index.html
|
||||
```
|
||||
2. **Pick a theme.** Open the deck and press `T` to cycle. Or hard-code it:
|
||||
```html
|
||||
<link rel="stylesheet" id="theme-link" href="../assets/themes/aurora.css">
|
||||
```
|
||||
Catalog in [references/themes.md](references/themes.md).
|
||||
3. **Pick layouts.** Copy `<section class="slide">...</section>` blocks out of
|
||||
files in `templates/single-page/` into your deck. Replace the demo data.
|
||||
Catalog in [references/layouts.md](references/layouts.md).
|
||||
4. **Add animations.** Put `data-anim="fade-up"` (or `class="anim-fade-up"`) on
|
||||
any element. On `<ul>`/grids, use `anim-stagger-list` for sequenced reveals.
|
||||
For canvas FX, use `<div data-fx="knowledge-graph">...</div>` and include
|
||||
`<script src="../assets/animations/fx-runtime.js"></script>`.
|
||||
Catalog in [references/animations.md](references/animations.md).
|
||||
5. **Use a full-deck template.** Copy `templates/full-decks/<name>/` into
|
||||
`examples/my-talk/` as a starting point. Each folder is self-contained with
|
||||
scoped CSS. Catalog in [references/full-decks.md](references/full-decks.md)
|
||||
and gallery at `templates/full-decks-index.html`.
|
||||
6. **Render to PNG.**
|
||||
```bash
|
||||
./scripts/render.sh templates/theme-showcase.html # one shot
|
||||
./scripts/render.sh examples/my-talk/index.html 12 # 12 slides
|
||||
```
|
||||
|
||||
## Authoring rules (important)
|
||||
|
||||
- **Always start from a template.** Don't author slides from scratch — copy the
|
||||
closest layout from `templates/single-page/` first, then replace content.
|
||||
- **Use tokens, not literal colors.** Every color, radius, shadow should come
|
||||
from CSS variables defined in `assets/base.css` and overridden by a theme.
|
||||
Good: `color: var(--text-1)`. Bad: `color: #111`.
|
||||
- **Don't invent new layout files.** Prefer composing existing ones. Only add
|
||||
a new `templates/single-page/*.html` if none of the 30 fit.
|
||||
- **Respect chrome slots.** `.deck-header`, `.deck-footer`, `.slide-number`
|
||||
and the progress bar are provided by `assets/base.css` + `runtime.js`.
|
||||
- **Keyboard-first.** Always include `<script src="../assets/runtime.js"></script>`
|
||||
so the deck supports ← → / T / A / F / S / O / hash deep-links.
|
||||
- **One `.slide` per logical page.** `runtime.js` makes `.slide.is-active`
|
||||
visible; all others are hidden.
|
||||
- **Supply notes.** Wrap speaker notes in `<div class="notes">…</div>` inside
|
||||
each slide. Press S to open the overlay.
|
||||
- **NEVER put presenter-only text on the slide itself.** Descriptive text like
|
||||
"这一页展示了……" or "Speaker: 这里可以补充……" or small explanatory captions
|
||||
aimed at the presenter MUST go inside `<div class="notes">`, NOT as visible
|
||||
`<p>` / `<span>` elements on the slide. The `.notes` class is `display:none`
|
||||
by default — it only appears in the S overlay. Slides should contain ONLY
|
||||
audience-facing content (titles, bullet points, data, charts, images).
|
||||
|
||||
## Writing guide
|
||||
|
||||
See [references/authoring-guide.md](references/authoring-guide.md) for a
|
||||
step-by-step walkthrough: file structure, naming, how to transform an outline
|
||||
into a deck, how to choose layouts and themes per audience, how to do a
|
||||
Chinese + English deck, and how to export.
|
||||
|
||||
## Catalogs (load when needed)
|
||||
|
||||
- [references/themes.md](references/themes.md) — all 36 themes with when-to-use.
|
||||
- [references/layouts.md](references/layouts.md) — all 31 layout types.
|
||||
- [references/animations.md](references/animations.md) — 27 CSS + 20 canvas FX animations.
|
||||
- [references/full-decks.md](references/full-decks.md) — all 15 full-deck templates.
|
||||
- [references/presenter-mode.md](references/presenter-mode.md) — **演讲者模式 + 逐字稿编写指南(技术分享/演讲必看)**.
|
||||
- [references/authoring-guide.md](references/authoring-guide.md) — full workflow.
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
html-ppt/
|
||||
├── SKILL.md (this file)
|
||||
├── references/ (detailed catalogs, load as needed)
|
||||
├── assets/
|
||||
│ ├── base.css (tokens + primitives — do not edit per deck)
|
||||
│ ├── fonts.css (webfont imports)
|
||||
│ ├── runtime.js (keyboard + presenter + overview + theme cycle)
|
||||
│ ├── themes/*.css (36 token overrides, one per theme)
|
||||
│ └── animations/
|
||||
│ ├── animations.css (27 named CSS entry animations)
|
||||
│ ├── fx-runtime.js (auto-init [data-fx] on slide enter)
|
||||
│ └── fx/*.js (20 canvas FX modules: particles/graph/fireworks…)
|
||||
├── templates/
|
||||
│ ├── deck.html (minimal 6-slide starter)
|
||||
│ ├── theme-showcase.html (36 slides, iframe-isolated per theme)
|
||||
│ ├── layout-showcase.html (iframe tour of all 31 layouts)
|
||||
│ ├── animation-showcase.html (20 FX + 27 CSS animation slides)
|
||||
│ ├── full-decks-index.html (gallery of all 14 full-deck templates)
|
||||
│ ├── full-decks/<name>/ (14 scoped multi-slide deck templates)
|
||||
│ └── single-page/*.html (31 layout files with demo data)
|
||||
├── scripts/
|
||||
│ ├── new-deck.sh (scaffold a deck from deck.html)
|
||||
│ └── render.sh (headless Chrome → PNG)
|
||||
└── examples/demo-deck/ (complete working deck)
|
||||
```
|
||||
|
||||
## Rendering to PNG
|
||||
|
||||
`scripts/render.sh` wraps headless Chrome at
|
||||
`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`. For multi-slide
|
||||
capture, runtime.js exposes `#/N` deep-links, and render.sh iterates 1..N.
|
||||
|
||||
```bash
|
||||
./scripts/render.sh templates/single-page/kpi-grid.html # single page
|
||||
./scripts/render.sh examples/demo-deck/index.html 8 out-dir # 8 slides, custom dir
|
||||
```
|
||||
|
||||
## Keyboard cheat sheet
|
||||
|
||||
```
|
||||
← → Space PgUp PgDn Home End navigate
|
||||
F fullscreen
|
||||
S open presenter window (magnetic cards: current/next/script/timer)
|
||||
N quick notes drawer (bottom overlay)
|
||||
R reset timer (in presenter window)
|
||||
?preview=N URL param — force preview-only mode (single slide, no chrome)
|
||||
O slide overview grid
|
||||
T cycle themes (reads data-themes attr)
|
||||
A cycle demo animation on current slide
|
||||
#/N in URL deep-link to slide N
|
||||
Esc close all overlays
|
||||
```
|
||||
|
||||
## License & author
|
||||
|
||||
MIT. Copyright (c) 2026 lewis <sudolewis@gmail.com>.
|
||||
@@ -0,0 +1,138 @@
|
||||
/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/* html-ppt :: fx-runtime.js
|
||||
* Canvas FX autoloader + lifecycle manager.
|
||||
* - Dynamically loads all fx modules listed in FX_LIST
|
||||
* - Initializes [data-fx] elements when their slide becomes active
|
||||
* - Calls handle.stop() when the slide leaves
|
||||
*/
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
const FX_LIST = [
|
||||
'_util',
|
||||
'particle-burst','confetti-cannon','firework','starfield','matrix-rain',
|
||||
'knowledge-graph','neural-net','constellation','orbit-ring','galaxy-swirl',
|
||||
'word-cascade','letter-explode','chain-react','magnetic-field','data-stream',
|
||||
'gradient-blob','sparkle-trail','shockwave','typewriter-multi','counter-explosion'
|
||||
];
|
||||
|
||||
// Resolve base path of this script so it works from any page location.
|
||||
const myScript = document.currentScript || (function(){
|
||||
const all = document.getElementsByTagName('script');
|
||||
for (const s of all){ if (s.src && s.src.indexOf('fx-runtime.js')>-1) return s; }
|
||||
return null;
|
||||
})();
|
||||
const base = myScript ? myScript.src.replace(/fx-runtime\.js.*$/, 'fx/') : 'assets/animations/fx/';
|
||||
|
||||
let loaded = 0;
|
||||
const total = FX_LIST.length;
|
||||
const ready = new Promise((resolve) => {
|
||||
if (!total) return resolve();
|
||||
FX_LIST.forEach((name) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = base + name + '.js';
|
||||
s.async = false;
|
||||
s.onload = s.onerror = () => { if (++loaded >= total) resolve(); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
});
|
||||
|
||||
window.__hpxActive = window.__hpxActive || new Map();
|
||||
|
||||
function initFxIn(root){
|
||||
if (!window.HPX) return;
|
||||
const els = root.querySelectorAll('[data-fx]');
|
||||
els.forEach((el) => {
|
||||
if (window.__hpxActive.has(el)) return;
|
||||
const name = el.getAttribute('data-fx');
|
||||
const fn = window.HPX[name];
|
||||
if (typeof fn !== 'function') return;
|
||||
try {
|
||||
const handle = fn(el, {}) || { stop(){} };
|
||||
window.__hpxActive.set(el, handle);
|
||||
} catch(e){ console.warn('[hpx-fx]', name, e); }
|
||||
});
|
||||
}
|
||||
|
||||
function stopFxIn(root){
|
||||
const els = root.querySelectorAll('[data-fx]');
|
||||
els.forEach((el) => {
|
||||
const h = window.__hpxActive.get(el);
|
||||
if (h && typeof h.stop === 'function'){
|
||||
try{ h.stop(); }catch(e){}
|
||||
}
|
||||
window.__hpxActive.delete(el);
|
||||
});
|
||||
}
|
||||
|
||||
function reinitFxIn(root){
|
||||
stopFxIn(root);
|
||||
initFxIn(root);
|
||||
}
|
||||
window.__hpxReinit = reinitFxIn;
|
||||
|
||||
function boot(){
|
||||
ready.then(() => {
|
||||
const active = document.querySelector('.slide.is-active') || document.querySelector('.slide');
|
||||
if (active) initFxIn(active);
|
||||
|
||||
// Watch all slides for class changes
|
||||
const slides = document.querySelectorAll('.slide');
|
||||
slides.forEach((sl) => {
|
||||
const mo = new MutationObserver((muts) => {
|
||||
for (const m of muts){
|
||||
if (m.attributeName === 'class'){
|
||||
if (sl.classList.contains('is-active')) initFxIn(sl);
|
||||
else stopFxIn(sl);
|
||||
}
|
||||
}
|
||||
});
|
||||
mo.observe(sl, { attributes: true, attributeFilter: ['class'] });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading'){
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,63 @@
|
||||
/* html-ppt fx :: shared helpers */
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
const U = window.HPX._u = {};
|
||||
|
||||
U.css = (el, name, fb) => {
|
||||
const v = getComputedStyle(el).getPropertyValue(name).trim();
|
||||
return v || fb;
|
||||
};
|
||||
|
||||
U.accent = (el, fb) => U.css(el, '--accent', fb || '#7c5cff');
|
||||
U.accent2 = (el, fb) => U.css(el, '--accent-2', fb || '#22d3ee');
|
||||
U.text = (el, fb) => U.css(el, '--text-1', fb || '#eaeaf2');
|
||||
|
||||
U.palette = (el) => [
|
||||
U.accent(el, '#7c5cff'),
|
||||
U.accent2(el, '#22d3ee'),
|
||||
U.css(el, '--ok', '#22c55e'),
|
||||
U.css(el, '--warn', '#f59e0b'),
|
||||
U.css(el, '--danger', '#ef4444'),
|
||||
];
|
||||
|
||||
U.canvas = (el) => {
|
||||
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
||||
const c = document.createElement('canvas');
|
||||
c.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:block;';
|
||||
el.appendChild(c);
|
||||
const ctx = c.getContext('2d');
|
||||
let w = 0, h = 0, dpr = Math.max(1, Math.min(2, window.devicePixelRatio||1));
|
||||
const fit = () => {
|
||||
const r = el.getBoundingClientRect();
|
||||
w = Math.max(1, r.width|0);
|
||||
h = Math.max(1, r.height|0);
|
||||
c.width = (w*dpr)|0;
|
||||
c.height = (h*dpr)|0;
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
};
|
||||
fit();
|
||||
const ro = new ResizeObserver(fit);
|
||||
ro.observe(el);
|
||||
return {
|
||||
c, ctx,
|
||||
get w(){return w;}, get h(){return h;}, get dpr(){return dpr;},
|
||||
destroy(){
|
||||
try{ro.disconnect();}catch(e){}
|
||||
if (c.parentNode) c.parentNode.removeChild(c);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
U.loop = (fn) => {
|
||||
let raf = 0, stopped = false, t0 = performance.now();
|
||||
const tick = (t) => {
|
||||
if (stopped) return;
|
||||
fn((t - t0)/1000);
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
return () => { stopped = true; cancelAnimationFrame(raf); };
|
||||
};
|
||||
|
||||
U.rand = (a,b) => a + Math.random()*(b-a);
|
||||
})();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user