feat: migrate 10 useful openclaw skills, remove openclaw-imports

This commit is contained in:
小墨 2026-04-24 22:47:47 +00:00
parent 03ef856bb0
commit 528774620b
78 changed files with 9700 additions and 0 deletions

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "automation-workflows",
"installedVersion": "0.1.0",
"installedAt": 1774706187714
}

View File

@ -0,0 +1,267 @@
---
name: automation-workflows
description: Design and implement automation workflows to save time and scale operations as a solopreneur. Use when identifying repetitive tasks to automate, building workflows across tools, setting up triggers and actions, or optimizing existing automations. Covers automation opportunity identification, workflow design, tool selection (Zapier, Make, n8n), testing, and maintenance. Trigger on "automate", "automation", "workflow automation", "save time", "reduce manual work", "automate my business", "no-code automation".
---
# Automation Workflows
## Overview
As a solopreneur, your time is your most valuable asset. Automation lets you scale without hiring. The goal is simple: automate anything you do more than twice a week that doesn't require creative thinking. This playbook shows you how to identify automation opportunities, design workflows, and implement them without writing code.
---
## Step 1: Identify What to Automate
Not every task should be automated. Start by finding the highest-value opportunities.
**Automation audit (spend 1 hour on this):**
1. Track every task you do for a week (use a notebook or simple spreadsheet)
2. For each task, note:
- How long it takes
- How often you do it (daily, weekly, monthly)
- Whether it's repetitive or requires judgment
3. Calculate time cost per task:
```
Time Cost = (Minutes per task × Frequency per month) / 60
```
Example: 15 min task done 20x/month = 5 hours/month
4. Sort by time cost (highest to lowest)
**Good candidates for automation:**
- Repetitive (same steps every time)
- Rule-based (no complex judgment calls)
- High-frequency (daily or weekly)
- Time-consuming (takes 10+ minutes)
**Examples:**
- ✅ Sending weekly reports to clients (same format, same schedule)
- ✅ Creating invoices after payment
- ✅ Adding new leads to CRM from form submissions
- ✅ Posting social media content on a schedule
- ❌ Conducting customer discovery interviews (requires nuance)
- ❌ Writing custom proposals for clients (requires creativity)
**Low-hanging fruit checklist (start here):**
- [ ] Email notifications for form submissions
- [ ] Auto-save form responses to spreadsheet
- [ ] Schedule social posts in advance
- [ ] Auto-create invoices from payment confirmations
- [ ] Sync data between tools (CRM ↔ email tool ↔ spreadsheet)
---
## Step 2: Choose Your Automation Tool
Three main options for no-code automation. Pick based on complexity and budget.
**Tool comparison:**
| Tool | Best For | Pricing | Learning Curve | Power Level |
|---|---|---|---|---|
| **Zapier** | Simple, 2-3 step workflows | $20-50/month | Easy | Low-Medium |
| **Make (Integromat)** | Visual, multi-step workflows | $9-30/month | Medium | Medium-High |
| **n8n** | Complex, developer-friendly, self-hosted | Free (self-hosted) or $20/month | Medium-Hard | High |
**Selection guide:**
- Budget < $20/month → Try Zapier free tier or n8n self-hosted
- Need visual workflow builder → Make
- Simple 2-step workflows → Zapier
- Complex workflows with branching logic → Make or n8n
- Want full control and customization → n8n
**Recommendation for solopreneurs:** Start with Zapier (easiest to learn). Graduate to Make or n8n when you hit Zapier's limits.
---
## Step 3: Design Your Workflow
Before building, map out the workflow on paper or a whiteboard.
**Workflow design template:**
```
TRIGGER: What event starts the workflow?
Example: "New row added to Google Sheet"
CONDITIONS (optional): Should this workflow run every time, or only when certain conditions are met?
Example: "Only if Status column = 'Approved'"
ACTIONS: What should happen as a result?
Step 1: [action]
Step 2: [action]
Step 3: [action]
ERROR HANDLING: What happens if something fails?
Example: "Send me a Slack message if action fails"
```
**Example workflow (lead capture → CRM → email):**
```
TRIGGER: New form submission on website
CONDITIONS: Email field is not empty
ACTIONS:
Step 1: Add lead to CRM (e.g., Airtable or HubSpot)
Step 2: Send welcome email via email tool (e.g., ConvertKit)
Step 3: Create task in project management tool (e.g., Notion) to follow up in 3 days
Step 4: Send me a Slack notification: "New lead: [Name]"
ERROR HANDLING: If Step 1 fails, send email alert to me
```
**Design principles:**
- Keep it simple — start with 2-3 steps, add complexity later
- Test each step individually before chaining them together
- Add delays between actions if needed (some APIs are slow)
- Always include error notifications so you know when things break
---
## Step 4: Build and Test Your Workflow
Now implement it in your chosen tool.
**Build workflow (Zapier example):**
1. **Choose trigger app** (e.g., Google Forms, Typeform, website form)
2. **Connect your account** (authenticate via OAuth)
3. **Test trigger** (submit a test form to make sure data comes through)
4. **Add action** (e.g., "Add row to Google Sheets")
5. **Map fields** (match form fields to spreadsheet columns)
6. **Test action** (run test to verify row is added correctly)
7. **Repeat for additional actions**
8. **Turn on workflow** (Zapier calls this "turn on Zap")
**Testing checklist:**
- [ ] Submit test data through the trigger
- [ ] Verify each action executes correctly
- [ ] Check that data maps to the right fields
- [ ] Test with edge cases (empty fields, special characters, long text)
- [ ] Test error handling (intentionally cause a failure to see if alerts work)
**Common issues and fixes:**
| Issue | Cause | Fix |
|---|---|---|
| Workflow doesn't trigger | Trigger conditions too narrow | Check filter settings, broaden criteria |
| Action fails | API rate limit or permissions | Add delay between actions, re-authenticate |
| Data missing or incorrect | Field mapping wrong | Double-check which fields are mapped |
| Workflow runs multiple times | Duplicate triggers | De-duplicate based on unique ID |
**Rule:** Test with real data before relying on an automation. Don't discover bugs when a real customer is involved.
---
## Step 5: Monitor and Maintain Automations
Automations aren't set-it-and-forget-it. They break. Tools change. APIs update. You need a maintenance plan.
**Weekly check (5 min):**
- Scan workflow logs for errors (most tools show a log of runs + failures)
- Address any failures immediately
**Monthly audit (15 min):**
- Review all active workflows
- Check: Is this still being used? Is it still saving time?
- Disable or delete unused workflows (they clutter your dashboard and can cause confusion)
- Update any workflows that depend on tools you've switched away from
**Where to store workflow documentation:**
- Create a simple doc (Notion, Google Doc) for each workflow
- Include: What it does, when it runs, what apps it connects, how to troubleshoot
- If you have 10+ workflows, this doc will save you hours when something breaks
**Error handling setup:**
- Route all error notifications to one place (Slack channel, email inbox, or task manager)
- Set up: "If any workflow fails, send a message to [your error channel]"
- Review errors weekly and fix root causes
---
## Step 6: Advanced Automation Ideas
Once you've automated the basics, consider these higher-leverage workflows:
### Client onboarding automation
```
TRIGGER: New client signs contract (via DocuSign, HelloSign)
ACTIONS:
1. Create project in project management tool
2. Add client to CRM with "Active" status
3. Send onboarding email sequence
4. Create invoice in accounting software
5. Schedule kickoff call on calendar
6. Add client to Slack workspace (if applicable)
```
### Content distribution automation
```
TRIGGER: New blog post published on website (via RSS or webhook)
ACTIONS:
1. Post link to LinkedIn with auto-generated caption
2. Post link to Twitter as a thread
3. Add post to email newsletter draft (in email tool)
4. Add to content calendar (Notion or Airtable)
5. Send notification to team (Slack) that post is live
```
### Customer health monitoring
```
TRIGGER: Every Monday at 9am (scheduled trigger)
ACTIONS:
1. Pull usage data for all customers from database (via API)
2. Flag customers with <50% of average usage
3. Add flagged customers to "At Risk" segment in CRM
4. Send re-engagement email campaign to at-risk customers
5. Create task for me to personally reach out to top 10 at-risk customers
```
### Invoice and payment tracking
```
TRIGGER: Payment received (Stripe webhook)
ACTIONS:
1. Mark invoice as paid in accounting software
2. Send receipt email to customer
3. Update CRM: customer status = "Paid"
4. Add revenue to monthly dashboard (Google Sheets or Airtable)
5. Send me a Slack notification: "Payment received: $X from [Customer]"
```
---
## Step 7: Calculate Automation ROI
Not every automation is worth the time investment. Calculate ROI to prioritize.
**ROI formula:**
```
Time Saved per Month (hours) = (Minutes per task / 60) × Frequency per month
Cost = (Setup time in hours × $50/hour) + Tool cost per month
Payback Period (months) = Setup cost / Monthly time saved value
If payback period < 3 months Worth it
If payback period > 6 months → Probably not worth it (unless it unlocks other value)
```
**Example:**
```
Task: Manually copying form submissions to CRM (15 min, 20x/month = 5 hours/month saved)
Setup time: 1 hour
Tool cost: $20/month (Zapier)
Payback: ($50 setup cost) / ($250/month value saved) = 0.2 months → Absolutely worth it
```
**Rule:** Focus on automations with payback < 3 months. Those are your highest-leverage investments.
---
## Automation Mistakes to Avoid
- **Automating before optimizing.** Don't automate a bad process. Fix the process first, then automate it.
- **Over-automating.** Not everything needs to be automated. If a task is rare or requires judgment, do it manually.
- **No error handling.** If an automation breaks and you don't know, it causes silent failures. Always set up error alerts.
- **Not testing thoroughly.** A broken automation is worse than no automation — it creates incorrect data or missed tasks.
- **Building too complex too fast.** Start with simple 2-3 step workflows. Add complexity only when the simple version works perfectly.
- **Not documenting workflows.** Future you will forget how this works. Write it down.

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn732qfbv22he1jqm63xbwq6e980kn8s",
"slug": "automation-workflows",
"version": "0.1.0",
"publishedAt": 1770341582349
}

View File

@ -0,0 +1,147 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Generates creative, polished code that avoids generic AI aesthetics. Use when the user asks to build web components, pages, artifacts, posters, or applications, or when any design skill requires project context.
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
## Context Gathering Protocol
Design skills produce generic output without project context. You MUST have confirmed design context before doing any design work.
**Required context** — every design skill needs at minimum:
- **Target audience**: Who uses this product and in what context?
- **Use cases**: What jobs are they trying to get done?
- **Brand personality/tone**: How should the interface feel?
Individual skills may require additional context — check the skill's preparation section for specifics.
**CRITICAL**: You cannot infer this context by reading the codebase. Code tells you what was built, not who it's for or what it should feel like. Only the creator can provide this context.
**Gathering order:**
1. **Check current instructions (instant)**: If your loaded instructions already contain a **Design Context** section, proceed immediately.
2. **Check .impeccable.md (fast)**: If not in instructions, read `.impeccable.md` from the project root. If it exists and contains the required context, proceed.
3. **Run teach-impeccable (REQUIRED)**: If neither source has context, you MUST run /teach-impeccable NOW before doing anything else. Do NOT skip this step. Do NOT attempt to infer context from the codebase instead.
---
## Design Direction
Commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work—the key is intentionality, not intensity.
Then implement working code that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
### Typography
→ *Consult [typography reference](reference/typography.md) for scales, pairing, and loading strategies.*
Choose fonts that are beautiful, unique, and interesting. Pair a distinctive display font with a refined body font.
**DO**: Use a modular type scale with fluid sizing (clamp)
**DO**: Vary font weights and sizes to create clear visual hierarchy
**DON'T**: Use overused fonts—Inter, Roboto, Arial, Open Sans, system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading—they rarely add value and make sites look templated
### Color & Theme
→ *Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
Commit to a cohesive palette. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
**DO**: Use modern CSS color functions (oklch, color-mix, light-dark) for perceptually uniform, maintainable palettes
**DO**: Tint your neutrals toward your brand hue—even a subtle hint creates subconscious cohesion
**DON'T**: Use gray text on colored backgrounds—it looks washed out; use a shade of the background color instead
**DON'T**: Use pure black (#000) or pure white (#fff)—always tint; pure black/white never appears in nature
**DON'T**: Use the AI color palette: cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds
**DON'T**: Use gradient text for "impact"—especially on metrics or headings; it's decorative rather than meaningful
**DON'T**: Default to dark mode with glowing accents—it looks "cool" without requiring actual design decisions
### Layout & Space
→ *Consult [spatial reference](reference/spatial-design.md) for grids, rhythm, and container queries.*
Create visual rhythm through varied spacing—not the same padding everywhere. Embrace asymmetry and unexpected compositions. Break the grid intentionally for emphasis.
**DO**: Create visual rhythm through varied spacing—tight groupings, generous separations
**DO**: Use fluid spacing with clamp() that breathes on larger screens
**DO**: Use asymmetry and unexpected compositions; break the grid intentionally for emphasis
**DON'T**: Wrap everything in cards—not everything needs a container
**DON'T**: Nest cards inside cards—visual noise, flatten the hierarchy
**DON'T**: Use identical card grids—same-sized cards with icon + heading + text, repeated endlessly
**DON'T**: Use the hero metric layout template—big number, small label, supporting stats, gradient accent
**DON'T**: Center everything—left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere—without rhythm, layouts feel monotonous
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
**DON'T**: Use glassmorphism everywhere—blur effects, glass cards, glow borders used decoratively rather than purposefully
**DON'T**: Use rounded elements with thick colored border on one side—a lazy accent that almost never looks intentional
**DON'T**: Use sparklines as decoration—tiny charts that look sophisticated but convey nothing meaningful
**DON'T**: Use rounded rectangles with generic drop shadows—safe, forgettable, could be any AI output
**DON'T**: Use modals unless there's truly no better alternative—modals are lazy
### Motion
→ *Consult [motion reference](reference/motion-design.md) for timing, easing, and reduced motion.*
Focus on high-impact moments: one well-orchestrated page load with staggered reveals creates more delight than scattered micro-interactions.
**DO**: Use motion to convey state changes—entrances, exits, feedback
**DO**: Use exponential easing (ease-out-quart/quint/expo) for natural deceleration
**DO**: For height animations, use grid-template-rows transitions instead of animating height directly
**DON'T**: Animate layout properties (width, height, padding, margin)—use transform and opacity only
**DON'T**: Use bounce or elastic easing—they feel dated and tacky; real objects decelerate smoothly
### Interaction
→ *Consult [interaction reference](reference/interaction-design.md) for forms, focus, and loading patterns.*
Make interactions feel fast. Use optimistic UI—update immediately, sync later.
**DO**: Use progressive disclosure—start simple, reveal sophistication through interaction (basic options first, advanced behind expandable sections; hover states that reveal secondary actions)
**DO**: Design empty states that teach the interface, not just say "nothing here"
**DO**: Make every interactive surface feel intentional and responsive
**DON'T**: Repeat the same information—redundant headers, intros that restate the heading
**DON'T**: Make every button primary—use ghost buttons, text links, secondary styles; hierarchy matters
### Responsive
→ *Consult [responsive reference](reference/responsive-design.md) for mobile-first, fluid design, and container queries.*
**DO**: Use container queries (@container) for component-level responsiveness
**DO**: Adapt the interface for different contexts—don't just shrink it
**DON'T**: Hide critical functionality on mobile—adapt the interface, don't amputate it
### UX Writing
→ *Consult [ux-writing reference](reference/ux-writing.md) for labels, errors, and empty states.*
**DO**: Make every word earn its place
**DON'T**: Repeat information users can already see
---
## The AI Slop Test
**Critical quality check**: If you showed this interface to someone and said "AI made this," would they believe you immediately? If yes, that's the problem.
A distinctive interface should make someone ask "how was this made?" not "which AI made this?"
Review the DON'T guidelines above—they are the fingerprints of AI-generated work from 2024-2025.
---
## Implementation Principles
Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices across generations.
Remember: the model is capable of extraordinary creative work. Don't hold back—show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

View File

@ -0,0 +1,132 @@
# Color & Contrast
## Color Spaces: Use OKLCH
**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal—unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark.
```css
/* OKLCH: lightness (0-100%), chroma (0-0.4+), hue (0-360) */
--color-primary: oklch(60% 0.15 250); /* Blue */
--color-primary-light: oklch(85% 0.08 250); /* Same hue, lighter */
--color-primary-dark: oklch(35% 0.12 250); /* Same hue, darker */
```
**Key insight**: As you move toward white or black, reduce chroma (saturation). High chroma at extreme lightness looks garish. A light blue at 85% lightness needs ~0.08 chroma, not the 0.15 of your base color.
## Building Functional Palettes
### The Tinted Neutral Trap
**Pure gray is dead.** Add a subtle hint of your brand hue to all neutrals:
```css
/* Dead grays */
--gray-100: oklch(95% 0 0); /* No personality */
--gray-900: oklch(15% 0 0);
/* Warm-tinted grays (add brand warmth) */
--gray-100: oklch(95% 0.01 60); /* Hint of warmth */
--gray-900: oklch(15% 0.01 60);
/* Cool-tinted grays (tech, professional) */
--gray-100: oklch(95% 0.01 250); /* Hint of blue */
--gray-900: oklch(15% 0.01 250);
```
The chroma is tiny (0.01) but perceptible. It creates subconscious cohesion between your brand color and your UI.
### Palette Structure
A complete system needs:
| Role | Purpose | Example |
|------|---------|---------|
| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades |
| **Neutral** | Text, backgrounds, borders | 9-11 shade scale |
| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each |
| **Surface** | Cards, modals, overlays | 2-3 elevation levels |
**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise.
### The 60-30-10 Rule (Applied Correctly)
This rule is about **visual weight**, not pixel count:
- **60%**: Neutral backgrounds, white space, base surfaces
- **30%**: Secondary colors—text, borders, inactive states
- **10%**: Accent—CTAs, highlights, focus states
The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power.
## Contrast & Accessibility
### WCAG Requirements
| Content Type | AA Minimum | AAA Target |
|--------------|------------|------------|
| Body text | 4.5:1 | 7:1 |
| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 |
| UI components, icons | 3:1 | 4.5:1 |
| Non-essential decorations | None | None |
**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG.
### Dangerous Color Combinations
These commonly fail contrast or cause readability issues:
- Light gray text on white (the #1 accessibility fail)
- **Gray text on any colored background**—gray looks washed out and dead on color. Use a darker shade of the background color, or transparency
- Red text on green background (or vice versa)—8% of men can't distinguish these
- Blue text on red background (vibrates visually)
- Yellow text on white (almost always fails)
- Thin light text on images (unpredictable contrast)
### Never Use Pure Gray or Pure Black
Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature—real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.)
### Testing
Don't trust your eyes. Use tools:
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- Browser DevTools → Rendering → Emulate vision deficiencies
- [Polypane](https://polypane.app/) for real-time testing
## Theming: Light & Dark Mode
### Dark Mode Is Not Inverted Light Mode
You can't just swap colors. Dark mode requires different design decisions:
| Light Mode | Dark Mode |
|------------|-----------|
| Shadows for depth | Lighter surfaces for depth (no shadows) |
| Dark text on light | Light text on dark (reduce font weight) |
| Vibrant accents | Desaturate accents slightly |
| White backgrounds | Never pure black—use dark gray (oklch 12-18%) |
```css
/* Dark mode depth via surface color, not shadow */
:root[data-theme="dark"] {
--surface-1: oklch(15% 0.01 250);
--surface-2: oklch(20% 0.01 250); /* "Higher" = lighter */
--surface-3: oklch(25% 0.01 250);
/* Reduce text weight slightly */
--body-weight: 350; /* Instead of 400 */
}
```
### Token Hierarchy
Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer—primitives stay the same.
## Alpha Is A Design Smell
Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed.
---
**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected).

View File

@ -0,0 +1,195 @@
# Interaction Design
## The Eight Interactive States
Every interactive element needs these states designed:
| State | When | Visual Treatment |
|-------|------|------------------|
| **Default** | At rest | Base styling |
| **Hover** | Pointer over (not touch) | Subtle lift, color shift |
| **Focus** | Keyboard/programmatic focus | Visible ring (see below) |
| **Active** | Being pressed | Pressed in, darker |
| **Disabled** | Not interactive | Reduced opacity, no pointer |
| **Loading** | Processing | Spinner, skeleton |
| **Error** | Invalid state | Red border, icon, message |
| **Success** | Completed | Green check, confirmation |
**The common miss**: Designing hover without focus, or vice versa. They're different. Keyboard users never see hover states.
## Focus Rings: Do Them Right
**Never `outline: none` without replacement.** It's an accessibility violation. Instead, use `:focus-visible` to show focus only for keyboard users:
```css
/* Hide focus ring for mouse/touch */
button:focus {
outline: none;
}
/* Show focus ring for keyboard */
button:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
```
**Focus ring design**:
- High contrast (3:1 minimum against adjacent colors)
- 2-3px thick
- Offset from element (not inside it)
- Consistent across all interactive elements
## Form Design: The Non-Obvious
**Placeholders aren't labels**—they disappear on input. Always use visible `<label>` elements. **Validate on blur**, not on every keystroke (exception: password strength). Place errors **below** fields with `aria-describedby` connecting them.
## Loading States
**Optimistic updates**: Show success immediately, rollback on failure. Use for low-stakes actions (likes, follows), not payments or destructive actions. **Skeleton screens > spinners**—they preview content shape and feel faster than generic spinners.
## Modals: The Inert Approach
Focus trapping in modals used to require complex JavaScript. Now use the `inert` attribute:
```html
<!-- When modal is open -->
<main inert>
<!-- Content behind modal can't be focused or clicked -->
</main>
<dialog open>
<h2>Modal Title</h2>
<!-- Focus stays inside modal -->
</dialog>
```
Or use the native `<dialog>` element:
```javascript
const dialog = document.querySelector('dialog');
dialog.showModal(); // Opens with focus trap, closes on Escape
```
## The Popover API
For tooltips, dropdowns, and non-modal overlays, use native popovers:
```html
<button popovertarget="menu">Open menu</button>
<div id="menu" popover>
<button>Option 1</button>
<button>Option 2</button>
</div>
```
**Benefits**: Light-dismiss (click outside closes), proper stacking, no z-index wars, accessible by default.
## Dropdown & Overlay Positioning
Dropdowns rendered with `position: absolute` inside a container that has `overflow: hidden` or `overflow: auto` will be clipped. This is the single most common dropdown bug in generated code.
### CSS Anchor Positioning
The modern solution uses the CSS Anchor Positioning API to tether an overlay to its trigger without JavaScript:
```css
.trigger {
anchor-name: --menu-trigger;
}
.dropdown {
position: fixed;
position-anchor: --menu-trigger;
position-area: block-end span-inline-end;
margin-top: 4px;
}
/* Flip above if no room below */
@position-try --flip-above {
position-area: block-start span-inline-end;
margin-bottom: 4px;
}
```
Because the dropdown uses `position: fixed`, it escapes any `overflow` clipping on ancestor elements. The `@position-try` block handles viewport edges automatically. **Browser support**: Chrome 125+, Edge 125+. Not yet in Firefox or Safari - use a fallback for those browsers.
### Popover + Anchor Combo
Combining the Popover API with anchor positioning gives you stacking, light-dismiss, accessibility, and correct positioning in one pattern:
```html
<button popovertarget="menu" class="trigger">Open</button>
<div id="menu" popover class="dropdown">
<button>Option 1</button>
<button>Option 2</button>
</div>
```
The `popover` attribute places the element in the **top layer**, which sits above all other content regardless of z-index or overflow. No portal needed.
### Portal / Teleport Pattern
In component frameworks, render the dropdown at the document root and position it with JavaScript:
- **React**: `createPortal(dropdown, document.body)`
- **Vue**: `<Teleport to="body">`
- **Svelte**: Use a portal library or mount to `document.body`
Calculate position from the trigger's `getBoundingClientRect()`, then apply `position: fixed` with `top` and `left` values. Recalculate on scroll and resize.
### Fixed Positioning Fallback
For browsers without anchor positioning support, `position: fixed` with manual coordinates avoids overflow clipping:
```css
.dropdown {
position: fixed;
/* top/left set via JS from trigger's getBoundingClientRect() */
}
```
Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead.
### Anti-Patterns
- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead.
- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`.
- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`.
## Destructive Actions: Undo > Confirm
**Undo is better than confirmation dialogs**—users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations.
## Keyboard Navigation Patterns
### Roving Tabindex
For component groups (tabs, menu items, radio groups), one item is tabbable; arrow keys move within:
```html
<div role="tablist">
<button role="tab" tabindex="0">Tab 1</button>
<button role="tab" tabindex="-1">Tab 2</button>
<button role="tab" tabindex="-1">Tab 3</button>
</div>
```
Arrow keys move `tabindex="0"` between items. Tab moves to the next component entirely.
### Skip Links
Provide skip links (`<a href="#main-content">Skip to main content</a>`) for keyboard users to jump past navigation. Hide off-screen, show on focus.
## Gesture Discoverability
Swipe-to-delete and similar gestures are invisible. Hint at their existence:
- **Partially reveal**: Show delete button peeking from edge
- **Onboarding**: Coach marks on first use
- **Alternative**: Always provide a visible fallback (menu with "Delete")
Don't rely on gestures as the only way to perform actions.
---
**Avoid**: Removing focus indicators without alternatives. Using placeholder text as labels. Touch targets <44x44px. Generic error messages. Custom controls without ARIA/keyboard support.

View File

@ -0,0 +1,99 @@
# Motion Design
## Duration: The 100/300/500 Rule
Timing matters more than easing. These durations feel right for most UI:
| Duration | Use Case | Examples |
|----------|----------|----------|
| **100-150ms** | Instant feedback | Button press, toggle, color change |
| **200-300ms** | State changes | Menu open, tooltip, hover states |
| **300-500ms** | Layout changes | Accordion, modal, drawer |
| **500-800ms** | Entrance animations | Page load, hero reveals |
**Exit animations are faster than entrances**—use ~75% of enter duration.
## Easing: Pick the Right Curve
**Don't use `ease`.** It's a compromise that's rarely optimal. Instead:
| Curve | Use For | CSS |
|-------|---------|-----|
| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` |
| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` |
| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` |
**For micro-interactions, use exponential curves**—they feel natural because they mimic real physics (friction, deceleration):
```css
/* Quart out - smooth, refined (recommended default) */
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
/* Quint out - slightly more dramatic */
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
/* Expo out - snappy, confident */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
```
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
## Staggered Animations
Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**—10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count.
## Reduced Motion
This is not optional. Vestibular disorders affect ~35% of adults over 40.
```css
/* Define animations normally */
.card {
animation: slide-up 500ms ease-out;
}
/* Provide alternative for reduced motion */
@media (prefers-reduced-motion: reduce) {
.card {
animation: fade-in 200ms ease-out; /* Crossfade instead of motion */
}
}
/* Or disable entirely */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work—just without spatial movement.
## Perceived Performance
**Nobody cares how fast your site is—just how fast it feels.** Perception can be as effective as actual performance.
**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions.
**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance:
- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening.
- **Early completion**: Show content progressively—don't wait for everything. Video buffering, progressive images, streaming HTML.
- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline—the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations.
**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time.
**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening.
## Performance
Don't use `will-change` preemptively—only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions).
---
**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading.

View File

@ -0,0 +1,114 @@
# Responsive Design
## Mobile-First: Write It Right
Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first.
## Breakpoints: Content-Driven
Don't chase device sizes—let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints.
## Detect Input Method, Not Just Screen Size
**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard—use pointer and hover queries:
```css
/* Fine pointer (mouse, trackpad) */
@media (pointer: fine) {
.button { padding: 8px 16px; }
}
/* Coarse pointer (touch, stylus) */
@media (pointer: coarse) {
.button { padding: 12px 20px; } /* Larger touch target */
}
/* Device supports hover */
@media (hover: hover) {
.card:hover { transform: translateY(-2px); }
}
/* Device doesn't support hover (touch) */
@media (hover: none) {
.card { /* No hover state - use active instead */ }
}
```
**Critical**: Don't rely on hover for functionality. Touch users can't hover.
## Safe Areas: Handle the Notch
Modern phones have notches, rounded corners, and home indicators. Use `env()`:
```css
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
/* With fallback */
.footer {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
```
**Enable viewport-fit** in your meta tag:
```html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
```
## Responsive Images: Get It Right
### srcset with Width Descriptors
```html
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w
"
sizes="(max-width: 768px) 100vw, 50vw"
alt="Hero image"
>
```
**How it works**:
- `srcset` lists available images with their actual widths (`w` descriptors)
- `sizes` tells the browser how wide the image will display
- Browser picks the best file based on viewport width AND device pixel ratio
### Picture Element for Art Direction
When you need different crops/compositions (not just resolutions):
```html
<picture>
<source media="(min-width: 768px)" srcset="wide.jpg">
<source media="(max-width: 767px)" srcset="tall.jpg">
<img src="fallback.jpg" alt="...">
</picture>
```
## Layout Adaptation Patterns
**Navigation**: Three stages—hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `<details>/<summary>` for content that can collapse on mobile.
## Testing: Don't Trust DevTools Alone
DevTools device emulation is useful for layout but misses:
- Actual touch interactions
- Real CPU/memory constraints
- Network latency patterns
- Font rendering differences
- Browser chrome/keyboard appearances
**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators.
---
**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful.

View File

@ -0,0 +1,100 @@
# Spatial Design
## Spacing Systems
### Use 4pt Base, Not 8pt
8pt systems are too coarse—you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px.
### Name Tokens Semantically
Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing—it eliminates margin collapse and cleanup hacks.
## Grid Systems
### The Self-Adjusting Grid
Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints.
## Visual Hierarchy
### The Squint Test
Blur your eyes (or screenshot and blur). Can you still identify:
- The most important element?
- The second most important?
- Clear groupings?
If everything looks the same weight blurred, you have a hierarchy problem.
### Hierarchy Through Multiple Dimensions
Don't rely on size alone. Combine:
| Tool | Strong Hierarchy | Weak Hierarchy |
|------|------------------|----------------|
| **Size** | 3:1 ratio or more | <2:1 ratio |
| **Weight** | Bold vs Regular | Medium vs Regular |
| **Color** | High contrast | Similar tones |
| **Position** | Top/left (primary) | Bottom/right |
| **Space** | Surrounded by white space | Crowded |
**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it.
### Cards Are Not Required
Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards**—use spacing, typography, and subtle dividers for hierarchy within a card.
## Container Queries
Viewport queries are for page layouts. **Container queries are for components**:
```css
.card-container {
container-type: inline-size;
}
.card {
display: grid;
gap: var(--space-md);
}
/* Card layout changes based on its container, not viewport */
@container (min-width: 400px) {
.card {
grid-template-columns: 120px 1fr;
}
}
```
**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands—automatically, without viewport hacks.
## Optical Adjustments
Text at `margin-left: 0` looks indented due to letterform whitespace—use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction.
### Touch Targets vs Visual Size
Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements:
```css
.icon-button {
width: 24px; /* Visual size */
height: 24px;
position: relative;
}
.icon-button::before {
content: '';
position: absolute;
inset: -10px; /* Expand tap target to 44px */
}
```
## Depth & Elevation
Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle—if you can clearly see it, it's probably too strong.
---
**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space.

View File

@ -0,0 +1,133 @@
# Typography
## Classic Typography Principles
### Vertical Rhythm
Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony—text and space share a mathematical foundation.
### Modular Scale & Hierarchy
The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy.
**Use fewer sizes with more contrast.** A 5-size system covers most needs:
| Role | Typical Ratio | Use Case |
|------|---------------|----------|
| xs | 0.75rem | Captions, legal |
| sm | 0.875rem | Secondary UI, metadata |
| base | 1rem | Body text |
| lg | 1.25-1.5rem | Subheadings, lead text |
| xl+ | 2-4rem | Headlines, hero text |
Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit.
### Readability & Measure
Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length—narrow columns need tighter leading, wide columns need more.
**Non-obvious**: Increase line-height for light text on dark backgrounds. The perceived weight is lighter, so text needs more breathing room. Add 0.05-0.1 to your normal line-height.
## Font Selection & Pairing
### Choosing Distinctive Fonts
**Avoid the invisible defaults**: Inter, Roboto, Open Sans, Lato, Montserrat. These are everywhere, making your design feel generic. They're fine for documentation or tools where personality isn't the goal—but if you want distinctive design, look elsewhere.
**Better Google Fonts alternatives**:
- Instead of Inter → **Instrument Sans**, **Plus Jakarta Sans**, **Outfit**
- Instead of Roboto → **Onest**, **Figtree**, **Urbanist**
- Instead of Open Sans → **Source Sans 3**, **Nunito Sans**, **DM Sans**
- For editorial/premium feel → **Fraunces**, **Newsreader**, **Lora**
**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality.
### Pairing Principles
**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif).
When pairing, contrast on multiple axes:
- Serif + Sans (structure contrast)
- Geometric + Humanist (personality contrast)
- Condensed display + Wide body (proportion contrast)
**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy.
### Web Font Loading
The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix:
```css
/* 1. Use font-display: swap for visibility */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}
/* 2. Match fallback metrics to minimize shift */
@font-face {
font-family: 'CustomFont-Fallback';
src: local('Arial');
size-adjust: 105%; /* Scale to match x-height */
ascent-override: 90%; /* Match ascender height */
descent-override: 20%; /* Match descender depth */
line-gap-override: 10%; /* Match line spacing */
}
body {
font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif;
}
```
Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically.
## Modern Web Typography
### Fluid Type
Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate—higher vw = faster scaling. Add a rem offset so it doesn't collapse to 0 on small screens.
**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes.
**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI — fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it.
### OpenType Features
Most developers don't know these exist. Use them for polish:
```css
/* Tabular numbers for data alignment */
.data-table { font-variant-numeric: tabular-nums; }
/* Proper fractions */
.recipe-amount { font-variant-numeric: diagonal-fractions; }
/* Small caps for abbreviations */
abbr { font-variant-caps: all-small-caps; }
/* Disable ligatures in code */
code { font-variant-ligatures: none; }
/* Enable kerning (usually on by default, but be explicit) */
body { font-kerning: normal; }
```
Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/).
## Typography System Architecture
Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system.
## Accessibility Considerations
Beyond contrast ratios (which are well-documented), consider:
- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout.
- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text.
- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile.
- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets.
---
**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text.

View File

@ -0,0 +1,107 @@
# UX Writing
## The Button Label Problem
**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns:
| Bad | Good | Why |
|-----|------|-----|
| OK | Save changes | Says what will happen |
| Submit | Create account | Outcome-focused |
| Yes | Delete message | Confirms the action |
| Cancel | Keep editing | Clarifies what "cancel" means |
| Click here | Download PDF | Describes the destination |
**For destructive actions**, name the destruction:
- "Delete" not "Remove" (delete is permanent, remove implies recoverable)
- "Delete 5 items" not "Delete selected" (show the count)
## Error Messages: The Formula
Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input".
### Error Message Templates
| Situation | Template |
|-----------|----------|
| **Format error** | "[Field] needs to be [format]. Example: [example]" |
| **Missing required** | "Please enter [what's missing]" |
| **Permission denied** | "You don't have access to [thing]. [What to do instead]" |
| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." |
| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" |
### Don't Blame the User
Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date".
## Empty States Are Opportunities
Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items".
## Voice vs Tone
**Voice** is your brand's personality—consistent everywhere.
**Tone** adapts to the moment.
| Moment | Tone Shift |
|--------|------------|
| Success | Celebratory, brief: "Done! Your changes are live." |
| Error | Empathetic, helpful: "That didn't work. Here's what to try..." |
| Loading | Reassuring: "Saving your work..." |
| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." |
**Never use humor for errors.** Users are already frustrated. Be helpful, not cute.
## Writing for Accessibility
**Link text** must have standalone meaning—"View pricing plans" not "Click here". **Alt text** describes information, not the image—"Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context.
## Writing for Translation
### Plan for Expansion
German text is ~30% longer than English. Allocate space:
| Language | Expansion |
|----------|-----------|
| German | +30% |
| French | +20% |
| Finnish | +30-40% |
| Chinese | -30% (fewer chars, but same width) |
### Translation-Friendly Patterns
Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear.
## Consistency: The Terminology Problem
Pick one term and stick with it:
| Inconsistent | Consistent |
|--------------|------------|
| Delete / Remove / Trash | Delete |
| Settings / Preferences / Options | Settings |
| Sign in / Log in / Enter | Sign in |
| Create / Add / New | Create |
Build a terminology glossary and enforce it. Variety creates confusion.
## Avoid Redundant Copy
If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well.
## Loading States
Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress.
## Confirmation Dialogs: Use Sparingly
Most confirmation dialogs are design failures—consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No").
## Form Instructions
Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking.
---
**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors.

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "image-cog",
"installedVersion": "1.0.4",
"installedAt": 1774706184883
}

187
hermes/image-cog/SKILL.md Normal file
View File

@ -0,0 +1,187 @@
---
name: image-cog
description: "AI image generation and photo editing powered by CellCog. Text-to-image, image-to-image, consistent characters, product photography, reference-based generation, style transfer, sets of images, social media visuals. Professional image creation with multiple AI models."
metadata:
openclaw:
emoji: "🎨"
os: [darwin, linux, windows]
author: CellCog
homepage: https://cellcog.ai
dependencies: [cellcog]
---
# Image Cog - AI Image Generation Powered by CellCog
Create professional images with AI - from single images to consistent character sets to product photography.
---
## Prerequisites
This skill requires the `cellcog` skill for SDK setup and API calls.
```bash
clawhub install cellcog
```
**Read the cellcog skill first** for SDK setup. This skill shows you what's possible.
**Quick pattern (v1.0+):**
```python
# Fire-and-forget - returns immediately
result = client.create_chat(
prompt="[your image request]",
notify_session_key="agent:main:main",
task_label="image-task",
chat_mode="agent" # Use "agent" for simple images, "agent team" for complex
)
# Daemon notifies you when complete - do NOT poll
```
---
## What Models Do We Use
| Model | Provider | Primary Use |
|-------|----------|-------------|
| **Nano Banana 2** (Gemini 3.1 Flash Image) | Google | Default image generation — photorealistic scenes, complex compositions, text rendering, multi-turn character consistency |
| **GPT Image 1.5** | OpenAI | Transparent background images — logos, stickers, product cutouts, overlay graphics |
| **Recraft** | Recraft AI | Scalable vector illustrations (SVG) and icon generation |
**Nano Banana 2** is the default model for all image generation. CellCog's agents intelligently route to other models when the task calls for it — for example, transparent PNGs are automatically handled by GPT Image 1.5, and vector/icon requests go to Recraft. If you'd prefer a specific model, just mention it in your prompt (e.g., *"use ChatGPT/OpenAI image generation"*).
## What Images You Can Create
### Single Image Creation
Generate any image from a text description:
- **Scenes**: "A cozy coffee shop interior with morning light streaming through windows"
- **Portraits**: "Professional headshot of a confident woman in business attire"
- **Products**: "Minimalist product shot of a white sneaker on a marble surface"
- **Abstract**: "Geometric abstract art in navy and gold"
- **Nature**: "Misty mountain landscape at sunrise with a lone hiker"
### Image Editing
Transform existing images:
- **Style Transfer**: "Transform this photo into a watercolor painting"
- **Background Removal**: "Remove the background and place on a clean white backdrop"
- **Enhancement**: "Enhance the colors and add dramatic lighting"
- **Modification**: "Change the person's outfit to a red dress"
### Consistent Characters
Create multiple images of the same character in different scenarios:
- **Character Series**: "Create a tech entrepreneur character, then show them: 1) At their desk coding, 2) Presenting to investors, 3) Celebrating a product launch"
- **Mascot Variations**: "Design a friendly robot mascot, then create versions for: welcome page, error page, success message, loading screen"
- **Story Sequences**: "Create a main character, then illustrate them in 5 scenes of a journey"
This is powerful for:
- Comic strips and storyboards
- Marketing campaigns with consistent characters
- Video frame generation
- Brand mascots across contexts
### Product Photography Style
Professional product visuals:
- **Hero Shots**: "Product hero shot of a smartwatch on a gradient background"
- **Lifestyle Shots**: "Smartphone being used by a person in a modern living room"
- **Flat Lays**: "Flat lay of skincare products with botanical elements"
- **360 Views**: "Multiple angles of a leather handbag - front, side, back, detail"
### Sets of Related Images
Multiple cohesive images for campaigns or collections:
- **Social Media Sets**: "5 Instagram post images for a fitness brand - consistent style, varied content"
- **Website Heroes**: "3 hero images for a SaaS landing page - professional, modern, tech-focused"
- **Ad Variations**: "4 versions of a product ad with different backgrounds and moods"
- **Blog Illustrations**: "Set of 6 illustrations for a blog post about productivity tips"
### Reference-Based Generation
Use existing images as references for style, character, or composition:
- **Style Matching**: "Create a new image in the same artistic style as this reference"
- **Character Consistency**: "Using this person as reference, create a new scene with them hiking"
- **Brand Alignment**: "Create product images matching this brand's visual style"
- **Composition Reference**: "Create a similar composition but with different subjects"
---
## Image Specifications
| Aspect | Options |
|--------|---------|
| **Aspect Ratios** | 1:1 (square), 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9 |
| **Sizes** | 1K (~1024px), 2K (~2048px), 4K (~4096px) |
| **Styles** | Photorealistic, illustration, watercolor, oil painting, anime, digital art, vector |
| **Formats** | PNG (default) |
**Size recommendations:**
- **1K**: Quick iterations, thumbnails, social media posts, drafts
- **2K**: Standard web content, presentations, marketing materials
- **4K**: Hero images, print materials, final deliverables where detail matters
---
## When to Use Agent Team Mode
For image generation, `chat_mode="agent team"` is recommended for:
- Complex scenes requiring multiple elements
- Consistent character series
- Reference-based generation requiring analysis
- Sets of related images
For simple single images, `chat_mode="agent"` can work faster.
---
## Example Image Prompts
**Professional headshot:**
> "Create a professional headshot of a friendly Asian woman in her 30s, wearing a navy blazer, soft studio lighting, neutral gray background, confident but approachable expression. 1:1 square, 2K quality, photorealistic."
**Product photography:**
> "Product shot of a premium wireless earbuds case, matte black finish, on a reflective dark surface with subtle blue accent lighting. Minimalist, high-end tech aesthetic. 4:3 landscape, 4K for hero image."
**Consistent character set:**
> "Create a character: young Black male software developer, casual style with glasses, friendly demeanor. Then create 4 images:
> 1. Working at a standing desk with multiple monitors
> 2. In a video call meeting, explaining something
> 3. At a coffee shop with laptop, thinking
> 4. Celebrating with team, high-fiving
> Keep the character exactly consistent across all images."
**Social media set:**
> "Create 5 Instagram posts for a plant-based meal delivery service:
> 1. Colorful Buddha bowl from above
> 2. Happy person unpacking delivery
> 3. Meal prep containers arranged neatly
> 4. Close-up of fresh ingredients
> 5. Before/after showing ingredients to finished dish
> Style: bright, fresh, appetizing, consistent warm color grading. 1:1 square format."
**Style transfer:**
> "Transform this uploaded photo of a city street into a Studio Ghibli anime style illustration. Keep the composition and elements but apply the characteristic Ghibli warmth, soft clouds, and whimsical details."
---
## Tips for Better Images
1. **Be descriptive**: "Woman in office" is vague. "Confident woman in her 40s, silver blazer, modern glass-walled office, warm afternoon light" is better.
2. **Specify style**: "Photorealistic", "digital illustration", "watercolor", "minimalist vector".
3. **Describe lighting**: "Soft natural light", "dramatic side lighting", "golden hour glow", "studio lighting".
4. **Include mood**: "Professional and confident", "warm and inviting", "energetic and vibrant".
5. **Mention composition**: "Rule of thirds", "centered symmetry", "close-up", "wide establishing shot".
6. **For consistency**: When creating character series, describe the character in detail first, then reference "the same character" in subsequent prompts.

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn7a96cj9q65e0bhmzahv790en80ffqm",
"slug": "image-cog",
"version": "1.0.4",
"publishedAt": 1774585048415
}

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "office-web-slide",
"installedVersion": "1.0.0",
"installedAt": 1774833615160
}

View File

@ -0,0 +1,122 @@
# Web Slide — 网页端 Slide(PPT)生成 Skill
通过对话驱动,自动生成可在浏览器中直接打开的交互式 HTML Slide(PPT)。无需安装任何软件,生成即可演示。
## 效果预览
**渐变深色主题 · 标题页** — 全屏沉浸式开场
![标题页](https://raw.githubusercontent.com/balancegsr/office/main/web-slide/assets/headline.png)
**数据图表 · 动态交互** — Chart.js 翻页时自动播放入场动画,支持 hover 查看数据
![动态交互图表](https://raw.githubusercontent.com/balancegsr/office/main/web-slide/assets/Animation_interaction_chart.png)
**卡片布局 · 多项并列** — 自动适配列数,玻璃质感面板
![卡片归类](https://raw.githubusercontent.com/balancegsr/office/main/web-slide/assets/Card_Classification.png)
**大数字布局 · 数据冲击** — 核心数据一眼抓住注意力
![重点内容](https://raw.githubusercontent.com/balancegsr/office/main/web-slide/assets/major_content.png)
**主题可视化选择器** — 浏览器内预览所有主题,点选即用
![主题选择器](https://raw.githubusercontent.com/balancegsr/office/main/web-slide/assets/Slide_style_selector.png)
## 核心能力
- **对话驱动**:提供素材或描述需求,Agent 自动完成内容结构化、布局选择、风格匹配和 HTML 生成
- **14 种布局**:标题页、内容页、双栏、卡片、大数字、引用、图片、图表、时间线、对比、金字塔等,根据内容自动选择
- **16 套主题**:Pure / Warm / Cyber / Data / Azure / Glass / Frost / Gradient 八种风格 × 浅色/深色,支持浏览器内可视化预览后选择
- **数据可视化**:SVG 图表、Chart.js、ECharts 三种方案,Agent 根据数据特征自动选择,翻页时动画精准触发
- **入场动画**:CSS 基础动画 + 可选 GSAP 高级动画(数字滚动、打字机、路径动画等)
- **完整交互**:键盘(← → Space / Home / End)、鼠标滚轮、触控板、触控滑动翻页,右侧圆点导航(可点击跳转,超过 12 页自动切换为数字显示),右上角全屏按钮(F 键 / ESC 退出)
- **单文件交付**:生成的 HTML 自包含所有样式和脚本,浏览器直接打开即可演示
## 使用方式
安装后在对话中提及 Slide(PPT)/ 演示文稿相关需求,Skill 自动触发。
**快速开始**
```
帮我做一个关于 AI 发展趋势的 Slide(PPT)
```
Agent 会引导你补充内容、确定结构和风格,然后生成完整的 HTML Slide。
**提供素材**
```
我有一份产品介绍文档,帮我做成 Slide(PPT)
(粘贴或附上文档内容)
```
**指定风格**
```
用深色极简风格,做一个技术分享的 Slide(PPT)
```
也支持提供参考物让 Agent 提取风格:.pptx 文件、网页链接、图片截图、PDF 文件。
## 预设主题
| 主题 | 标识 | 调性 | 适用场景 |
|------|------|------|---------|
| Pure Light | `pure-light` | 极致简约、纯净白底 | 产品发布、科技展示、设计提案 |
| Pure Dark | `pure-dark` | 深邃黑底、银白文字 | 产品发布会、科技演讲、高端展示 |
| Warm Light | `warm-light` | 暖调学院、奶油白底 | AI/科研汇报、品牌故事、温暖叙事 |
| Warm Dark | `warm-dark` | 深邃暖调、琥珀强调 | AI 研究分享、深度演讲、沉浸叙事 |
| Cyber Light | `cyber-light` | 科技蓝紫、现代感 | AI/SaaS 产品介绍、技术方案、创业路演 |
| Cyber Dark | `cyber-dark` | 赛博霓虹、未来感 | AI 产品发布、黑客松、极客风格 |
| Data Light | `data-light` | 数据学院、青绿强调 | 数据分析报告、研究成果、趋势分析 |
| Data Dark | `data-dark` | 数据仪表盘、夜间 | 仪表盘展示、实时数据、趋势演讲 |
| Azure Light | `azure-light` | 蓝调科技、专业 | 企业汇报、科技产品发布、ToB 方案 |
| Azure Dark | `azure-dark` | 蓝调深邃、沉稳 | 企业年会、产品发布会、夜间演示 |
| Glass Light | `glass-light` | 液态玻璃、通透光感 | 科技前沿、产品发布、创意展示 |
| Glass Dark | `glass-dark` | 液态玻璃暗色、深邃通透 | 科技发布会、沉浸式展示、夜间演示 |
| Frost Light | `frost-light` | 磨砂玻璃、清冷克制 | SaaS 产品、数据平台、专业工具 |
| Frost Dark | `frost-dark` | 磨砂玻璃暗色、沉稳专业 | 控制面板、技术架构、暗色办公 |
| Gradient Light | `gradient-light` | 渐变潮流、活力冲击 | 创意提案、营销发布、年轻品牌 |
| Gradient Dark | `gradient-dark` | 渐变暗色、霓虹未来 | 音乐/游戏/潮牌发布、视觉冲击型演讲 |
## 安装
`web-slide/` 文件夹复制到 Agent 软件的 Skills 目录下即可。不同软件的约定路径可能不同,常见示例:
| Agent 软件 | Skills 目录 |
|-----------|------------|
| OpenClaw | `.claw/skills/` |
| Claude Code | `.claude/skills/` |
| Antigravity | `.antigravity/skills/` |
| CodeBuddy | `.codebuddy/skills/` |
> 以上为常见路径,具体请以各软件官方文档为准。部分 Agent 软件也支持在设置页面直接导入。
## 分享与演示
生成的 HTML 文件可以直接发送给任何人,对方用浏览器打开即可查看完整的交互式 Slide(PPT)。本地演示时,按 F 或点击右上角按钮即可进入全屏。
## 扩展
**新增主题**:在 `references/themes/` 下创建 CSS 文件(参考现有主题的变量结构),然后在 `references/guidelines.md` 的主题注册表中添加一行描述。无需修改其他文件。
**新增布局**:在 `references/layouts/` 下创建 HTML 模板,然后在 `references/guidelines.md` 的布局注册表中添加一行描述。
## 注意事项
- 生成的 HTML 无需安装任何软件即可打开;如果 Slide 中包含数据图表或高级动画,演示时需要联网加载 CDN 资源
- 本 Skill 生成 HTML 格式,不提供 .pptx 导出。如需 .pptx 格式请使用专门的 pptx 生成工具
## 版本
- **v1.0.0** — 初始发布
- 14 种布局 + 16 套主题(8 风格 × 浅色/深色,含液态玻璃/磨砂玻璃/渐变)
- 对话引导式创建流程
- SVG / Chart.js / ECharts 数据可视化,翻页时动画自动触发
- CSS + GSAP 动画系统
- 主题可视化选择(浏览器预览 + 对话选择双路径)
- 完整演示交互(多方式翻页 / 全屏 / 圆点导航)
- 跨 Agent 软件兼容

View File

@ -0,0 +1,317 @@
---
name: office-web-slide
description: >
网页端 Slide 生成 Skill。当用户需要制作演示文稿(Slide / PPT / 演示)、生成幻灯片、创建展示页面时触发。
用户提供素材或通过对话引导,Agent 自动生成可在浏览器中直接打开的交互式 HTML Slide。
支持多种布局类型、16 套高品质预设主题(8 风格 × 浅色/深色,含液态玻璃/磨砂玻璃/渐变等现代视觉风格)、数据图表可视化和入场动画。
capabilities: [file_read, file_write, file_edit, browser_preview, web_fetch, image_generation]
---
# 网页端 Slide 生成 Skill
## 能力概述
你是一个专业的 Slide 设计师和开发者。你的任务是帮用户生成**可在浏览器中直接打开的交互式 HTML Slide**——支持键盘翻页、触控滑动、滚轮翻页、全屏演示、右侧圆点导航等完整交互能力。
用户不需要了解 HTML/CSS/JS,他们只需要提供内容素材和(可选的)风格偏好,你负责一切技术细节。
## 核心工作流:三桶并行收集
你的工作流不是线性流水线,而是**三桶并行收集 + 条件触发生成**。
### 三个桶
| 桶 | 职责 | "装满"标准 | 默认值 |
|---|---|---|---|
| 🪣 **内容桶** | 收集用户的内容素材 | 有足够素材生成完整大纲 | 无(必须填充) |
| 🪣 **结构桶** | 将内容结构化为逐页大纲 | 每页的标题+要点+布局类型已确定 | 无(依赖内容桶) |
| 🪣 **风格桶** | 确定视觉风格 | 主题已选定或 Agent 已推荐 | Agent 自动推荐(但必须在生成前告知用户所选风格,给予低成本干预窗口) |
### 每轮对话
```
1. 接收用户输入
2. 将信息分拣到对应的桶(一句话可能同时填充多个桶)
3. 评估三桶水位
4. 引导最空的桶(优先级:内容 > 结构 > 风格)
5. 三桶均满 → 进入生成
```
### 桶水位评估
**内容桶**:
- 🔴 空:只有主题或一句话 → 问目标受众、核心要点、关键信息
- 🟡 半满:有主题+部分要点 → 针对性补充(数据?案例?结论?)
- 🟢 满:素材完整 → 推进到结构化
**结构桶**:
- 🔴 空:内容桶未满 → 等待
- 🟡 半满:内容够了但未结构化 → Agent 生成/完善大纲
- 🟢 满:逐页大纲确定 → 推进到生成
**风格桶**:
- 🔴 空:没提风格 → 延迟(先搞定内容)
- 🟡 半满:模糊偏好("简洁一点") → 推荐匹配的预设主题
- 🟢 满:已选定主题 / Agent 推荐且用户未反对 → 就绪
**引导优先级**:内容 > 结构 > 风格。风格桶有默认值,不会成为阻塞项。
### 内容忠实性(必须遵守)
**红线——素材获取失败必须停下**:
如果用户提供了文件但读取失败(如 PDF 返回空内容或报错),必须**立即告知用户**并建议替代方案(如提供其他格式、复制粘贴文本)。**绝对不可以**在未获取到内容的情况下静默跳过、自行发挥,也不可从文件名推测内容。
**内容充足时**——严格基于已有素材:
当内容桶已满(无论来源是文件、文本还是多轮对话),Slide 内容必须基于用户提供的实际素材,可以精炼提取但不可编造素材中不存在的信息。
**内容不足需要补充时**——优先引导,允许延展但必须透明:
当内容桶半满时,优先通过提问引导用户补充。如果用户明确表示不想再补充、希望直接生成,Agent 可以基于已有内容进行合理延展,但必须在生成前告知用户补充了哪些内容,让用户有机会修正。例如:
- "你提供的素材中没有涉及具体数据,我补充了一些示意数据用于展示效果,你可以之后替换为实际数据。"
### 元信息不上屏(必须遵守)
**红线**:以下信息属于 Agent 的工作上下文,**默认禁止出现在 Slide 页面内容中**,除非用户明确要求展示:
- 用户画像 / 受众描述(如"对象:法务 / 非技术岗位"、"FOR LEGAL TEAMS")
- 写作策略 / 生成约束(如"用非技术语言解释"、"根据你的要求")
- Prompt 指令或对话上下文的引用
- **素材来源说明**(如"基于工作区内 3 份材料生成"、"来源:汇总资料.md")
- **Agent 生成日期**(如"2026-03-28"——除非用户明确要求在封面显示日期)
- **文件名或路径引用**(如"根据 汇总数据.json 与 汇总资料.md")
- **主题/风格名称或标识**(如"腾讯 light 风格"、"gradient-dark"、"Pure Light 主题"——这些是 Skill 的技术元数据,不是给观众看的内容)
**特别注意 title 页的 .title-meta 区域**:此区域仅用于展示用户提供的作者/机构名称和日期。如果用户没有提供这些信息,**直接删除整个 .title-meta div**,不要用工作上下文信息填充。
这些信息是给 Agent 的工作指示,不是给观众看的内容。如果用户确实希望展示,必须由用户主动提出。
### 生成前必做
三桶满后、生成前,你**必须**:
1. **透明化决策**:用一句话告知用户接下来做什么
- "内容和风格已经够了,我直接生成 8 页 Slide"
- "内容比较丰富,我先出个大纲你确认一下"
2. **风格低成本干预窗口**:如果用户从未主动提及风格(风格桶通过自动推荐填满),你必须简短提及所选风格:
- "风格方面我选了 Pure Light(极简浅色),如果你有其他偏好可以告诉我。"
3. **大纲确认判断**
- 简单任务(素材充足 + 页数 ≤ 8)→ 直接生成
- 复杂任务(页数 > 8 或内容有歧义)→ 先出大纲供用户确认
---
## 生成流程
### Step 1:加载参考文件
读取 `references/guidelines.md`,获取注册表(布局/主题/组件的索引)和填充规则。
### Step 2:选择主题(可视化为默认,对话为降级)
> **硬规则**:除非用户已在对话中明确指定了主题,否则**必须先尝试可视化选择路径**。不得跳过直接推荐。
**默认路径 — 可视化选择**:
```
2a. 删除工作目录下已有的 .theme-choice 文件(防止读到旧结果)
2b. 在 references/ 目录下执行:python3 theme-server.py --dir references/ --output .
2c. 读取终端输出的 THEME_PICKER_URL=... 行,获取 URL
2d. 使用浏览器预览能力打开该 URL
2e. 自动轮询 .theme-choice 文件(每 2 秒检查一次,最长等待 120 秒)
2f. 文件出现后,读取主题 id,然后读取对应主题 CSS:references/themes/{theme_id}.css
```
> **禁止**:启动 picker 后要求用户回到聊天中再次确认。用户在页面上点击确认 = 选择完成。Agent 必须自动检测 .theme-choice 并继续,不打断用户。
**降级路径 — 对话选择**(仅当以下任一条件成立时):
- Python 启动失败(命令报错)
- 浏览器无法打开(工具调用失败)
- .theme-choice 在 120 秒内未出现(超时)
```
2a. 从主题注册表中,根据用户偏好或内容调性推荐 1-2 个主题
2b. 简述推荐理由,让用户确认或更换
2c. 读取对应主题 CSS:references/themes/{selected_theme}.css
```
Agent 不需要告知用户走了哪条路径或降级原因。可视化选择器本身就是用户体验的一部分。
### Step 3:读取骨架
```
读取 → references/base.html
```
> **硬规则**:必须从当前 `references/base.html` 开始构建,**禁止复用历史生成的 HTML 文件作为骨架**。即使用户提供了之前生成的 Slide 文件作为参考,也只能参考其内容,不得直接在其基础上修改。
### Step 4:注入主题
将主题 CSS 文件中的 `:root { ... }` 替换骨架中 `/* THEME_VARS_START */``/* THEME_VARS_END */` 之间的内容(含这两个标记本身之间的完整 `:root` 块)。**注意**:骨架中有两个 `:root` 块,第一个是 Slide 尺寸(固定值,不可替换),第二个才是主题变量(由标记包裹)。
### Step 5:逐页生成
对大纲中的每一页:
```
5a. 根据内容特征,从布局注册表中选择最合适的布局类型
5b. 读取 → references/layouts/{layout_type}.html
5c. 复制 <section> 到骨架的 .slide-deck 中
5d. 复制 <style>
5e. 替换所有 {{...}} 占位符为实际内容
5f. 设置 data-slide="N" 和注释标识
5g. 按需添加动画 class(参考 animations.css 中的类名)
```
### Step 6:按需加载组件
- 如有图表需求:读取 `references/components/chart-svg.html``chart-js.html`
- 如有高级动画需求:读取 `references/components/gsap-recipes.html`
- 如需 Chart.js / GSAP / ECharts:取消注释 HTML 底部对应的 CDN `<script>`
### Step 7:输出完整 HTML
将完整 HTML 写入文件。
### Step 8:预览
在内置浏览器中预览,或告知用户在浏览器中打开生成的 HTML 文件。
### Step 8.5:视觉 QA(必做)
生成完成后、交付前,**逐页检查**以下清单。任何一项不通过,必须修复后再交付:
| 检查项 | 不通过的表现 | 修复方式 |
|--------|-------------|---------|
| 🔴 **遮挡** | 标题压住图表、脚注被容器遮盖、卡片内文字溢出 | 缩短文案 / 拆分为多页 / 调整布局 |
| 🔴 **硬编码色值** | 存在不走 CSS 变量的 `#xxxxxx` 色值(图表配色除外,但必须从主题提取) | 替换为 `var(--color-*)` 或从主题实际值派生 |
| 🟡 **过度容器化** | 正文被不必要的大白卡片包裹、容器套容器 | 去掉多余容器层(见 guidelines §3.7) |
| 🟡 **横向空间浪费** | 内容区明显窄于版心,左右大面积留白 | 去掉多余 max-width / 减小内层 padding |
| 🟡 **垂直重心偏移** | 上方大面积空白、内容挤在下半部分 | 调整垂直对齐或重分配标题/主体/脚注高度 |
| 🟡 **布局单一** | 连续 3 页以上使用同一种布局 | 根据内容特征换用不同布局类型 |
| 🟡 **内容密度低** | 连续 2+ 页只有一句话(section/quote 过多),像未完成的草稿 | section/quote 是过渡点缀,不是主力。section ≤ 总页数 20%;合并或改用 cards/content/chart |
| 🔴 **元信息泄漏** | 页面中出现用户画像、写作策略、受众说明、prompt 片段、素材来源、Agent 生成日期、**主题/风格名称**(如"腾讯 light 风格"、"gradient-dark")等工作上下文 | 删除泄漏内容;title-meta 仅填用户提供的作者/日期,未提供则删除整个 div |
| 🔴 **内容溢出视口** | 卡片/列表/时间线等元素超出 720px 页面高度,底部内容被截断 | 减少条目、缩短文案、或拆分为多页(**cards ≤ 4 带描述、content ≤ 4 带描述**) |
| 🔴 **cards 列数异常** | 卡片列数不合理(如 4 卡竖排、3 卡单列) | CSS `:has()` 已自动强制列数,Agent 不要手动设 grid-template-columns |
| 🔴 **cards 未垂直居中** | 卡片偏上方、底部大面积空白 | `.cards-grid` 已设 `align-content: center`,Agent 不要覆盖 |
| 🔴 **图表动画丢失** | Chart.js 图表在翻页时无入场动画(直接显示静态图表) | 必须使用 createChartLazy() + slideAnimations 注册 + CDN 取消注释(见 chart-js.html) |
| 🔴 **图表使用静态图片** | 图表区域嵌入 `<img>` 静态截图 | 必须用 `<canvas>`(Chart.js)或内联 `<svg>`,禁止 `<img>` |
| 🔴 **标题换行** | title/ending 页大标题折行 | 中文主标题 ≤ 16 字,heading ≤ 22 字(见 guidelines §3.9) |
| 🔴 **Chart.js 动画抖动/扩张** | 图表加载时从中心扩张、hover 时再次扩张;或出现"2套图表"分离 | ① 禁止给 canvas 设 CSS width/height(尤其 !important)——Chart.js responsive 自行管理尺寸;② `createChartLazy()` 中禁止 `canvas.width = canvas.width`(见 chart-js.html §7) |
| 🔴 **Chart.js tooltip 被禁用** | hover 数据点无反馈、无法查看数值 | 禁止 `tooltip.enabled: false`。可自定义样式但不能禁用(见 chart-js.html §8) |
| 🔴 **卡片图标混用** | 同一页卡片混用 emoji、字母、罗马数字 | 同一页内必须统一图标类型;优先 emoji;禁止罗马数字(见 guidelines §3.10) |
| 🔴 **导航壳异常** | 产物中出现 `.slide-controls`、底部工具栏、概览网格等非当前 base.html 定义的控件 | 必须从当前 `references/base.html` 重新生成,不得复用旧 HTML |
### Step 9:交付
告知用户 Slide 已生成,并提供分享指引:
- **直接发送 HTML 文件** — 对方用任意浏览器打开即可
- **本地演示** — 直接在浏览器中打开,按 F 或点击右上角按钮全屏
---
## 分批生成策略
当 Slide **超过 10 页**时,**必须分批生成**(这是标准流程,不是异常处理):
```
批次 1:骨架 + 主题变量 + 前 5-8 页 → 写入完整 HTML 文件
批次 2:继续生成后续页面 → 定位到 </div><!-- /slide-deck --> 结束标签之前,插入新的 <section> 页面
(如需更多批次,继续在同一位置追加)
```
**关键**:追加时使用文件编辑能力,定位到 `</div><!-- /slide-deck -->` 之前插入新内容,**不要**覆写整个文件,否则会丢失之前批次的内容。避免单次输出超出 Token 限制。
---
## 模板填充规则(三层约束)
| 层次 | 自由度 | 规则 |
|------|--------|------|
| **结构层**(HTML 标签) | 🔒 严格 | 模板的标签嵌套和 class 命名必须严格遵循 |
| **内容层**(数据填充) | 🔓 灵活 | 文字数量、卡片个数、列表条目数可根据实际内容调整 |
| **样式层**(CSS) | ⚠️ 变量 | 所有视觉样式通过 CSS 变量控制,不可用行内样式覆写变量属性 |
**你可以**:
- ✅ 严格按模板生成
- ✅ 在模板基础上微调(加减卡片数量、调整图片位置等)
- ✅ 混合多种模板(如上半部分 big-number + 下半部分 content)
- ❌ 不可完全抛弃模板从零写 HTML
---
## 参考物处理
当用户提供参考物时,按以下方式处理:
| 输入类型 | 处理方式 |
|---------|---------|
| **预设主题名称**(如"商务风""极简") | 匹配主题注册表中的对应主题 |
| **.pptx 文件** | 读取文件,提取配色和布局特征,映射到 CSS 变量 |
| **网页链接** | 抓取页面内容,分析配色和排版 |
| **图片截图** | 视觉分析提取配色和调性 |
| **PDF 文件** | 解析文件,提取内容和视觉特征。若无法读取,**必须触发红线规则**——立即告知用户并建议替代方案 |
提取结果覆盖预设主题变量,生成自定义 CSS 变量集。
---
## 图表与动画
### 图表选择
| 数据特征 | 方案 | 参考 |
|---------|------|------|
| 简单图表,数据点 ≤ 8 | SVG 内嵌 | `references/components/chart-svg.html` |
| 复杂图表/需交互 | Chart.js CDN | `references/components/chart-js.html` |
| 超大数据量/特殊图表 | ECharts CDN | Agent 自行编写 |
**重要**:Chart.js 和 ECharts 不支持 CSS 变量作为颜色值。你必须从所选主题中提取实际色值注入。
### 动画选择
优先使用 CSS 动画(`references/components/animations.css`),仅在需要数字滚动/打字机/复杂时序时引入 GSAP。
技术选择对用户**不可见**——用户不需要知道你用的是 CSS 还是 GSAP、SVG 还是 Chart.js。
---
## 异常处理
| 异常 | 处理 |
|------|------|
| 素材不足(一句话描述) | 引导模式逐步补全;5-8 轮仍不足则基于已有信息生成精简版 |
| 素材过多(万字文稿) | 提炼核心要点;超 15 页主动告知并确认 |
| 单页内容溢出 | 自动拆分为多页(宁可多一页不挤一页) |
| 图表数据格式错误 | 提示问题并建议修正;退化为文字表格 |
| 参考物无法访问 | 告知无法访问,建议替代方式(截图/选预设主题) |
| 风格冲突 | 指出冲突,让用户选择以哪个为准 |
| AI 图片生成失败 | 降级:纯色/渐变背景 + 图标替代 |
| Token 超限 | 分批生成(见上方分批策略) |
---
## 边界说明
- **本 Skill 不提供 .pptx 导出能力**。HTML Slide 的优势:零依赖(任何设备浏览器打开)、丰富交互(动画/图表)、跨平台一致性
- 如用户需要 .pptx 格式,建议使用专门的 pptx 生成工具
- 生成后的修改由 Agent 自身能力处理(Skill 不干预),用户可以直接说"第三页标题改成..."
---
## 文件索引
所有参考文件位于 `references/` 目录下:
| 文件 | 用途 | 读取时机 |
|------|------|---------|
| `guidelines.md` | 注册表 + 填充规则 + 最佳实践 | 生成开始时(必读) |
| `base.html` | HTML 骨架(含右侧圆点导航/全屏按钮/翻页交互/自适应缩放) | 生成开始时 |
| `theme-picker.html` | 主题可视化选择器页面 | Step 2 可视化路径 |
| `theme-server.py` | 选择器本地 HTTP 服务 | Step 2 可视化路径 |
| `themes/*.css` | 主题 CSS 变量集(16 套:5 基础风格 + 3 玻璃/渐变风格,各含浅色/深色) | 选定主题后 |
| `layouts/*.html` | 布局模板片段(14 种) | 逐页生成时按需 |
| `components/animations.css` | CSS 入场动画类 | 需要动画时 |
| `components/chart-svg.html` | SVG 图表参考 | 需要简单图表时 |
| `components/chart-js.html` | Chart.js 图表参考 | 需要复杂图表时 |
| `components/gsap-recipes.html` | GSAP 动画配方 | 需要高级动画时 |

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn78qv8rcz2mq42p5txjhs9pnx83ae11",
"slug": "office-web-slide",
"version": "1.0.0",
"publishedAt": 1774796021032
}

View File

@ -0,0 +1,482 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{SLIDE_TITLE}}</title>
<style>
/* ============================================================
Slide 尺寸(固定值,不受主题替换影响)
============================================================ */
:root {
--slide-width: 1280px;
--slide-height: 720px;
}
/* ============================================================
主题变量(由 Agent 注入 — 替换 THEME_VARS_START 到 THEME_VARS_END 之间的内容)
============================================================ */
/* THEME_VARS_START */
:root {
/* --- 配色 --- */
--color-primary: #2563eb;
--color-secondary: #64748b;
--color-accent: #f59e0b;
--color-bg: #ffffff;
--color-bg-alt: #f8fafc;
--color-surface: #ffffff;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-text-on-primary: #ffffff;
--color-border: #e2e8f0;
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Fira Code', 'Cascadia Code', monospace;
--font-size-title: 3.5rem;
--font-size-heading: 2.5rem;
--font-size-subheading: 1.5rem;
--font-size-body: 1.125rem;
--font-size-small: 0.875rem;
/* --- 间距和尺寸 --- */
--slide-padding: 4rem;
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 2rem;
--spacing-lg: 3rem;
--spacing-xl: 4rem;
/* --- 效果 --- */
--border-radius: 8px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
--transition-speed: 0.3s;
--transition-easing: ease-out;
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}
/* THEME_VARS_END */
/* ============================================================
基础重置(Skill 提供,不可修改)
============================================================ */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html {
font-size: 16px; /* 固定基准:1rem = 16px,主题通过 rem 变量控制实际字号 */
}
body {
width: 100%; height: 100%;
overflow: hidden;
font-family: var(--font-body);
font-size: var(--font-size-body);
color: var(--color-text);
background: var(--color-bg);
-moz-osx-font-smoothing: grayscale;
}
/* ============================================================
Slide 容器 — 16:9 自适应缩放
============================================================ */
.slide-viewport {
position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center;
background: var(--color-bg);
}
.slide-deck {
position: relative;
width: var(--slide-width);
height: var(--slide-height);
transform-origin: center center;
/* overflow 不设 hidden — 允许 .slide.active 的 box-shadow 溢出到视口边缘
各 .slide 自身已有 overflow:hidden 保证内容不外溢 */
}
/* 单页 Slide */
.slide {
position: absolute; inset: 0;
width: var(--slide-width);
height: var(--slide-height);
padding: var(--slide-padding);
--_slide-bg: var(--color-bg); /* 当前页背景色(供 bleed 引用) */
background: var(--_slide-bg);
display: flex; flex-direction: column; justify-content: center;
overflow: hidden;
opacity: 0;
visibility: hidden;
transition: opacity 0.5s var(--transition-easing),
visibility 0.5s var(--transition-easing);
}
.slide.active {
opacity: 1;
visibility: visible;
z-index: 1;
/* 背景无限溢出:消除 slide-deck 边界形成的"大卡片"效果 */
box-shadow: 0 0 0 9999px var(--_slide-bg);
}
/* 奇偶页交替背景(排除特殊布局页面) */
.slide:nth-child(even):not(.slide--title):not(.slide--section):not(.slide--ending) {
--_slide-bg: var(--color-bg-alt);
}
/* ============================================================
通用排版
============================================================ */
.slide h1, .slide h2, .slide h3, .slide h4 {
font-family: var(--font-heading);
line-height: 1.2;
color: var(--color-text);
}
.slide h1 { font-size: min(var(--font-size-title), 60px); text-wrap: balance; margin-bottom: var(--spacing-md); }
.slide h2 { font-size: min(var(--font-size-heading), 48px); text-wrap: balance; margin-bottom: var(--spacing-sm); }
.slide h3 { font-size: var(--font-size-subheading); margin-bottom: var(--spacing-sm); }
.slide p { line-height: 1.7; margin-bottom: var(--spacing-sm); color: var(--color-text-secondary); }
.slide ul, .slide ol { padding-left: 1.5em; margin-bottom: var(--spacing-sm); }
.slide li { line-height: 1.7; margin-bottom: var(--spacing-xs); }
.slide code { font-family: var(--font-mono); font-size: 0.9em; background: var(--color-bg-alt); padding: 0.15em 0.4em; border-radius: var(--border-radius); }
.slide strong { color: var(--color-text); }
.slide a { color: var(--color-primary); text-decoration: none; }
/* ============================================================
右侧圆点导航
============================================================ */
.dot-nav {
position: fixed; right: 20px; top: 50%;
transform: translateY(-50%);
display: flex; flex-direction: column; align-items: center; gap: 10px;
z-index: 100;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.dot-nav:hover { opacity: 1; }
.dot-nav-dot {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--color-text-secondary);
opacity: 0.35;
border: none; padding: 0;
cursor: pointer;
transition: all 0.25s ease;
}
.dot-nav-dot:hover {
opacity: 0.7;
transform: scale(1.3);
}
.dot-nav-dot.active {
opacity: 1;
background: var(--color-primary);
transform: scale(1.4);
}
/* >12 页降级为数字显示 */
.dot-nav-num {
color: var(--color-text-secondary);
font-size: 13px;
font-variant-numeric: tabular-nums;
font-family: var(--font-body);
user-select: none;
opacity: 0.8;
letter-spacing: 0.5px;
}
/* ============================================================
右上角全屏按钮
============================================================ */
.fullscreen-btn {
position: fixed; top: 16px; right: 16px;
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
background: var(--color-border);
border: none; border-radius: var(--border-radius);
color: var(--color-text-secondary);
cursor: pointer;
z-index: 100;
opacity: 0.2;
transition: opacity 0.25s ease, background 0.25s ease, color 0.25s ease;
}
.fullscreen-btn:hover {
opacity: 0.85;
background: var(--color-secondary);
color: var(--color-text-on-primary);
}
/* ============================================================
左右点击翻页区域
============================================================ */
.click-zone {
position: fixed; top: 0; bottom: 0;
width: 20%; z-index: 10; cursor: pointer;
}
.click-zone--prev { left: 0; }
.click-zone--next { right: 0; }
/* ============================================================
玻璃/渐变效果层(主题驱动,非玻璃主题零开销)
============================================================ */
/* 装饰背景层 — 渐变光斑 / 色彩氛围 */
.slide::before {
content: '';
position: absolute;
inset: 0;
background: var(--bg-pattern, none);
pointer-events: none;
}
/* 确保内容在装饰层之上 */
.slide > * {
position: relative;
}
/* 玻璃表面 — backdrop-filter 对所有 surface 容器生效 */
.slide .card,
.slide .comp-side,
.slide .twocol-left,
.slide .twocol-right {
-webkit-backdrop-filter: blur(var(--surface-blur, 0px)) saturate(var(--surface-saturate, 100%));
backdrop-filter: blur(var(--surface-blur, 0px)) saturate(var(--surface-saturate, 100%));
}
/* ============================================================
动画样式占位(由 Agent 按需注入 / 读取 animations.css)
============================================================ */
/* Agent 在此区域注入入场动画样式 */
/* ============================================================
布局样式占位(由 Agent 按需注入)
============================================================ */
/* Agent 在此区域注入各布局类型的样式 */
</style>
</head>
<body>
<div class="slide-viewport">
<div class="slide-deck" id="slideDeck">
<!-- ====================================================
Slide 页面区域(由 Agent 填充)
每页格式:
<section class="slide slide--{layout_type}" data-slide="N">
...内容...
</section>
==================================================== -->
</div><!-- /slide-deck -->
</div><!-- /slide-viewport -->
<!-- 左右点击翻页区域 -->
<div class="click-zone click-zone--prev" id="clickPrev"></div>
<div class="click-zone click-zone--next" id="clickNext"></div>
<!-- 右侧圆点导航 -->
<nav class="dot-nav" id="dotNav"></nav>
<!-- 右上角全屏按钮 -->
<button class="fullscreen-btn" id="btnFullscreen">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M2 6V3a1 1 0 011-1h3M12 2h3a1 1 0 011 1v3M16 12v3a1 1 0 01-1 1h-3M6 16H3a1 1 0 01-1-1v-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<script>
(function () {
'use strict';
/* ============================================================
核心状态
============================================================ */
const deck = document.getElementById('slideDeck');
const slides = () => deck.querySelectorAll('.slide');
const dotNav = document.getElementById('dotNav');
const DOT_THRESHOLD = 12; // 超过此数量时降级为数字显示
let currentIndex = 0;
let totalSlides = 0;
/* ============================================================
初始化
============================================================ */
function init() {
totalSlides = slides().length;
if (totalSlides === 0) return;
buildDotNav();
// 支持 URL hash 导航(如 #5 或 #slide=5)
const hash = window.location.hash.replace('#', '');
const startIndex = parseInt(hash.replace('slide=', ''), 10) - 1;
goTo(startIndex >= 0 && startIndex < totalSlides ? startIndex : 0, false);
fitScale();
// 事件绑定
window.addEventListener('resize', fitScale);
window.addEventListener('keydown', onKeyDown);
document.getElementById('btnFullscreen').addEventListener('click', toggleFullscreen);
document.getElementById('clickPrev').addEventListener('click', prev);
document.getElementById('clickNext').addEventListener('click', next);
// 触控滑动
let touchStartX = 0, touchStartY = 0;
deck.addEventListener('touchstart', e => {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}, { passive: true });
deck.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - touchStartX;
const dy = e.changedTouches[0].clientY - touchStartY;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
dx < 0 ? next() : prev();
}
}, { passive: true });
// 鼠标滚轮 / 触控板滚动翻页
let wheelLocked = false;
document.addEventListener('wheel', e => {
if (wheelLocked) return;
if (Math.abs(e.deltaY) < 15) return;
e.preventDefault();
wheelLocked = true;
e.deltaY > 0 ? next() : prev();
setTimeout(() => { wheelLocked = false; }, 500);
}, { passive: false });
}
/* ============================================================
右侧圆点导航 — 构建
============================================================ */
function buildDotNav() {
dotNav.innerHTML = '';
if (totalSlides <= DOT_THRESHOLD) {
// 圆点模式:每页一个可点击圆点
for (let i = 0; i < totalSlides; i++) {
const dot = document.createElement('button');
dot.className = 'dot-nav-dot';
dot.setAttribute('aria-label', '跳转到第 ' + (i + 1) + ' 页');
dot.addEventListener('click', () => goTo(i));
dotNav.appendChild(dot);
}
} else {
// 数字模式
const num = document.createElement('span');
num.className = 'dot-nav-num';
num.id = 'dotNavNum';
dotNav.appendChild(num);
}
}
function updateDotNav() {
if (totalSlides <= DOT_THRESHOLD) {
const dots = dotNav.querySelectorAll('.dot-nav-dot');
dots.forEach((dot, i) => {
dot.classList.toggle('active', i === currentIndex);
});
} else {
const num = document.getElementById('dotNavNum');
if (num) num.textContent = (currentIndex + 1) + ' / ' + totalSlides;
}
}
/* ============================================================
翻页
============================================================ */
function goTo(index, animate = true) {
if (index < 0 || index >= totalSlides) return;
const allSlides = slides();
allSlides.forEach((s, i) => {
s.classList.toggle('active', i === index);
});
currentIndex = index;
// 同步视口背景色 — 消除 slide-deck 边界可见的"大卡片"效果
requestAnimationFrame(() => {
const bg = getComputedStyle(allSlides[index]).backgroundColor;
document.querySelector('.slide-viewport').style.background = bg;
});
// 触发当前页已注册的动画(Chart.js 延迟初始化 / GSAP 配方等)
// 等待 slide 过渡动画完成(0.5s)+ buffer 后再初始化,避免图表在容器尺寸变化中渲染导致抖动
setTimeout(() => {
if (typeof slideAnimations !== 'undefined' && slideAnimations[currentIndex]) {
slideAnimations[currentIndex]();
}
}, 550);
updateDotNav();
// 同步 URL hash
history.replaceState(null, '', '#' + (index + 1));
}
function next() { goTo(currentIndex + 1); }
function prev() { goTo(currentIndex - 1); }
/* ============================================================
自适应缩放 — 保持 16:9 居中
============================================================ */
function fitScale() {
const vw = window.innerWidth;
const vh = window.innerHeight;
const sw = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--slide-width'));
const sh = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--slide-height'));
const scale = Math.min(vw / sw, vh / sh);
deck.style.transform = 'scale(' + scale + ')';
}
/* ============================================================
键盘导航
============================================================ */
function onKeyDown(e) {
switch (e.key) {
case 'ArrowRight': case 'ArrowDown': case ' ': case 'Enter':
e.preventDefault(); next(); break;
case 'ArrowLeft': case 'ArrowUp':
e.preventDefault(); prev(); break;
case 'Home':
e.preventDefault(); goTo(0); break;
case 'End':
e.preventDefault(); goTo(totalSlides - 1); break;
case 'f': case 'F':
toggleFullscreen(); break;
case 'Escape':
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
break;
}
}
/* ============================================================
全屏
============================================================ */
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen().catch(() => {});
}
}
/* ============================================================
启动
============================================================ */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
<!-- 可选:GSAP CDN(Agent 按需取消注释)-->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script> -->
<!-- 可选:Chart.js CDN(Agent 按需取消注释)-->
<!-- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script> -->
<!-- 可选:ECharts CDN(Agent 按需取消注释)-->
<!-- <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script> -->
</body>
</html>

View File

@ -0,0 +1,147 @@
/*
* CSS 入场动画库
* 所有页面默认可用Agent 通过给元素添加 class 即可触发
* 动画在元素所在 Slide 变为 active 时播放
*/
/* ============================================================
基础入场动画
============================================================ */
/* 淡入 */
.anim-fade-in {
opacity: 0;
transition: opacity 0.6s ease-out;
}
.slide.active .anim-fade-in {
opacity: 1;
}
/* 从下方滑入 */
.anim-slide-up {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.slide.active .anim-slide-up {
opacity: 1;
transform: translateY(0);
}
/* 从左侧滑入 */
.anim-slide-left {
opacity: 0;
transform: translateX(-30px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.slide.active .anim-slide-left {
opacity: 1;
transform: translateX(0);
}
/* 从右侧滑入 */
.anim-slide-right {
opacity: 0;
transform: translateX(30px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.slide.active .anim-slide-right {
opacity: 1;
transform: translateX(0);
}
/* 缩放入场 */
.anim-scale-in {
opacity: 0;
transform: scale(0.9);
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}
.slide.active .anim-scale-in {
opacity: 1;
transform: scale(1);
}
/* 从上方落下 */
.anim-drop-in {
opacity: 0;
transform: translateY(-30px);
transition: opacity 0.5s ease-out, transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.slide.active .anim-drop-in {
opacity: 1;
transform: translateY(0);
}
/* ============================================================
延迟类 配合 stagger 效果使用
Agent 给列表/卡片的每个子元素加不同的 delay class
============================================================ */
.anim-delay-1 { transition-delay: 0.1s !important; }
.anim-delay-2 { transition-delay: 0.2s !important; }
.anim-delay-3 { transition-delay: 0.3s !important; }
.anim-delay-4 { transition-delay: 0.4s !important; }
.anim-delay-5 { transition-delay: 0.5s !important; }
.anim-delay-6 { transition-delay: 0.6s !important; }
.anim-delay-7 { transition-delay: 0.7s !important; }
.anim-delay-8 { transition-delay: 0.8s !important; }
/* ============================================================
组合类 常见的入场组合
============================================================ */
/* 标题专用:放大 + 淡入 */
.anim-title-enter {
opacity: 0;
transform: scale(0.95) translateY(10px);
transition: opacity 0.7s ease-out, transform 0.7s ease-out;
}
.slide.active .anim-title-enter {
opacity: 1;
transform: scale(1) translateY(0);
}
/* 卡片弹入 */
.anim-bounce-in {
opacity: 0;
transform: scale(0.8);
transition: opacity 0.5s ease-out, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.slide.active .anim-bounce-in {
opacity: 1;
transform: scale(1);
}
/* 线条/边框展开 */
.anim-expand-width {
transform: scaleX(0);
transform-origin: left;
transition: transform 0.6s ease-out;
}
.slide.active .anim-expand-width {
transform: scaleX(1);
}
/* 数字/文字渐显 */
.anim-blur-in {
opacity: 0;
filter: blur(8px);
transition: opacity 0.6s ease-out, filter 0.6s ease-out;
}
.slide.active .anim-blur-in {
opacity: 1;
filter: blur(0);
}
/* ============================================================
无障碍尊重用户的"减少动画"偏好
============================================================ */
@media (prefers-reduced-motion: reduce) {
.anim-fade-in, .anim-slide-up, .anim-slide-left, .anim-slide-right,
.anim-scale-in, .anim-drop-in, .anim-title-enter, .anim-bounce-in,
.anim-expand-width, .anim-blur-in {
transition: none !important;
opacity: 1 !important;
transform: none !important;
filter: none !important;
}
}

View File

@ -0,0 +1,302 @@
<!--
Component: chart-js
Chart.js 集成参考 — 适合复杂图表(多系列/需交互/大数据量)
Agent 参考此文件,在 slide--chart 的 .chart-container 中生成 canvas + 初始化脚本
前提:在 HTML 底部取消注释 Chart.js CDN:
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
⚡ 延迟初始化(核心模式):
Chart.js 动画在 new Chart() 时立即播放。如果图表不在第一页,
用户翻到时动画已结束,看到的是静态图表。
解决方案:通过 slideAnimations 注册表,在 Slide 激活时才创建 Chart 实例。
-->
<!-- ============================================================
核心:延迟初始化工具函数
Agent 必须将此函数放在 Chart.js CDN 之后、所有图表注册之前
============================================================ -->
<script>
/**
* 延迟创建 Chart.js 实例 — 在 Slide 切换到目标页时才执行 new Chart()
* @param {string} canvasId - canvas 元素的 id
* @param {object} config - Chart.js 完整配置对象
* @returns {function} 注册到 slideAnimations 的回调函数
*
* 用法(在 slideAnimations 注册表中):
* const slideAnimations = {
* 3: createChartLazy('barChart', { type: 'bar', data: {...}, options: {...} }),
* };
*/
function createChartLazy(canvasId, config) {
let instance = null;
return function () {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
// 防止重复创建(用户前后翻页时)
if (instance) {
instance.destroy();
instance = null;
}
// ❌ 禁止 canvas.width = canvas.width — 会将 canvas 内在分辨率重置为 300×150,
// Chart.js responsive 模式检测到尺寸不匹配后二次渲染,导致"双重图表"抖动
instance = new Chart(canvas, config);
};
}
</script>
<!-- ============================================================
示例 1:柱状图(Bar Chart)— 延迟初始化版
============================================================ -->
<div class="chart-container">
<canvas id="barChart"></canvas>
</div>
<!--
Agent 在 slideAnimations 注册表中注册此图表:
const slideAnimations = {
3: createChartLazy('barChart', {
type: 'bar',
data: {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [{
label: '营收(万元)',
data: [120, 190, 300, 250],
backgroundColor: '#2563eb',
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 1000, easing: 'easeOutQuart' },
plugins: {
legend: { display: false },
tooltip: { enabled: true }
},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.06)' } },
x: { grid: { display: false } }
}
}
}),
};
-->
<!-- ============================================================
示例 2:折线图(Line Chart)— 延迟初始化版
============================================================ -->
<div class="chart-container">
<canvas id="lineChart"></canvas>
</div>
<!--
slideAnimations 注册:
N: createChartLazy('lineChart', {
type: 'line',
data: {
labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
datasets: [
{
label: '2024',
data: [65, 78, 90, 81, 95, 110],
borderColor: '#1d4ed8',
backgroundColor: 'rgba(29,78,216,0.1)',
fill: true,
tension: 0.3,
pointRadius: 4
},
{
label: '2023',
data: [45, 52, 60, 55, 70, 80],
borderColor: '#94a3b8',
backgroundColor: 'transparent',
borderDash: [5, 5],
tension: 0.3,
pointRadius: 3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 1200, easing: 'easeOutQuart' },
plugins: {
legend: { position: 'top', align: 'end' }
},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.06)' } },
x: { grid: { display: false } }
}
}
}),
-->
<!-- ============================================================
示例 3:饼图(Doughnut Chart)— 延迟初始化版
============================================================ -->
<div class="chart-container">
<canvas id="doughnutChart"></canvas>
</div>
<!--
slideAnimations 注册:
N: createChartLazy('doughnutChart', {
type: 'doughnut',
data: {
labels: ['移动端', '桌面端', '平板', '其他'],
datasets: [{
data: [55, 30, 10, 5],
backgroundColor: ['#1d4ed8', '#0ea5e9', '#64748b', '#cbd5e1'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 1000, easing: 'easeOutQuart' },
cutout: '60%',
plugins: {
legend: { position: 'right' }
}
}
}),
-->
<!-- ============================================================
示例 4:雷达图(Radar Chart)— 延迟初始化版
============================================================ -->
<div class="chart-container">
<canvas id="radarChart"></canvas>
</div>
<!--
slideAnimations 注册:
N: createChartLazy('radarChart', {
type: 'radar',
data: {
labels: ['性能', '安全', '可用性', '成本', '扩展性', '维护性'],
datasets: [
{
label: '方案 A',
data: [90, 75, 85, 60, 80, 70],
borderColor: '#1d4ed8',
backgroundColor: 'rgba(29,78,216,0.15)',
pointRadius: 4
},
{
label: '方案 B',
data: [70, 90, 60, 85, 65, 80],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245,158,11,0.15)',
pointRadius: 4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 1000, easing: 'easeOutQuart' },
scales: {
r: { beginAtZero: true, max: 100, ticks: { stepSize: 25 } }
}
}
}),
-->
<!--
================================================================
重要提醒(Agent 必须注意)
================================================================
1. 延迟初始化(必须):
- 所有图表必须通过 createChartLazy() 注册到 slideAnimations
- 不要直接在页面底部写 new Chart(),否则动画在页面加载时就播放完了
- createChartLazy() 内置防重复创建,用户前后翻页不会报错
2. 颜色注入(必须):
- Chart.js 不支持 CSS 变量作为颜色值
- Agent 必须从所选主题中提取实际色值注入
- 浅色主题网格色:'rgba(0,0,0,0.06)'
- 深色主题网格色:'rgba(255,255,255,0.08)'
- 深色主题 tick/label 色:需显式设置 color 属性
3. 深色主题适配清单:
scales.x.ticks.color: '主题的 --color-text-secondary 实际色值'
scales.y.ticks.color: '同上'
scales.x.grid.color: 'rgba(255,255,255,0.08)'
scales.y.grid.color: 'rgba(255,255,255,0.08)'
plugins.legend.labels.color: '主题的 --color-text 实际色值'
4. 每个 canvas 需要唯一的 id
5. 设置 responsive: true + maintainAspectRatio: false 让图表适配容器
6. 建议关闭不需要的 plugin(legend/title),保持 Slide 简洁
7. 🔴 Canvas CSS 禁令:
- 禁止给 canvas 设置 CSS width/height(尤其是 !important)
- Chart.js responsive 模式通过 ResizeObserver 管理 canvas 尺寸,
外部 CSS 强制尺寸会与 ResizeObserver 互相打架,导致:
· 图表加载时抖动/从中心扩张
· hover tooltip 时图表再次扩张
- chart.html 模板已移除 canvas 的 CSS 尺寸覆盖,Agent 不要手动加回
8. 🔴 Tooltip 交互必须保留:
- 禁止设置 plugins.tooltip.enabled: false 或 interaction.mode: 'none'
- Chart.js 默认启用 tooltip,不要主动关闭
- 用户需要 hover 节点看到具体数值,这是基本交互预期
- 可以自定义 tooltip 的样式(backgroundColor/borderColor 等),但不能禁用
================================================================
深色主题完整示例(以 cyber-dark 为例)
================================================================
N: createChartLazy('barChartDark', {
type: 'bar',
data: {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [{
label: '营收',
data: [120, 190, 300, 250],
backgroundColor: '#818cf8',
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 1000, easing: 'easeOutQuart' },
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#1a2035',
titleColor: '#f0f4ff',
bodyColor: '#8b95ad',
borderColor: '#2a3150',
borderWidth: 1
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(255,255,255,0.08)' },
ticks: { color: '#8b95ad' }
},
x: {
grid: { display: false },
ticks: { color: '#8b95ad' }
}
}
}
}),
-->

View File

@ -0,0 +1,221 @@
<!--
Component: chart-svg
SVG 内嵌图表参考 — 适合简单图表(数据点 ≤ 8)
Agent 参考此文件的写法,根据实际数据生成 SVG 图表代码
直接嵌入到 slide--chart 布局的 .chart-container 中
⚡ CSS 入场动画:
SVG 图表默认支持 CSS 动画。当 Slide 变为 .active 时,
通过 .slide.active 选择器触发动画,无需 JS 介入。
-->
<!-- ============================================================
示例 1:水平柱状图(带入场动画)
============================================================ -->
<svg class="chart-svg chart-svg--bar" viewBox="0 0 600 300" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-height:100%">
<!-- 背景网格线 -->
<line x1="100" y1="40" x2="100" y2="260" stroke="var(--color-border)" stroke-width="1"/>
<line x1="100" y1="260" x2="580" y2="260" stroke="var(--color-border)" stroke-width="1"/>
<!-- 数据条 — Agent 根据实际数据调整 width 和文字 -->
<!-- 每个 rect 使用 chart-bar 类触发宽度生长动画 -->
<g>
<text x="90" y="75" text-anchor="end" font-size="14" fill="var(--color-text-secondary)">产品A</text>
<rect class="chart-bar" x="100" y="58" width="380" height="28" rx="4" fill="var(--color-primary)" opacity="0.85" style="--bar-target: 380"/>
<text class="chart-label" x="490" y="77" font-size="13" fill="var(--color-text-secondary)">78%</text>
</g>
<g>
<text x="90" y="125" text-anchor="end" font-size="14" fill="var(--color-text-secondary)">产品B</text>
<rect class="chart-bar" x="100" y="108" width="300" height="28" rx="4" fill="var(--color-primary)" opacity="0.7" style="--bar-target: 300"/>
<text class="chart-label" x="410" y="127" font-size="13" fill="var(--color-text-secondary)">62%</text>
</g>
<g>
<text x="90" y="175" text-anchor="end" font-size="14" fill="var(--color-text-secondary)">产品C</text>
<rect class="chart-bar" x="100" y="158" width="220" height="28" rx="4" fill="var(--color-primary)" opacity="0.55" style="--bar-target: 220"/>
<text class="chart-label" x="330" y="177" font-size="13" fill="var(--color-text-secondary)">45%</text>
</g>
<g>
<text x="90" y="225" text-anchor="end" font-size="14" fill="var(--color-text-secondary)">产品D</text>
<rect class="chart-bar" x="100" y="208" width="160" height="28" rx="4" fill="var(--color-primary)" opacity="0.4" style="--bar-target: 160"/>
<text class="chart-label" x="270" y="227" font-size="13" fill="var(--color-text-secondary)">33%</text>
</g>
</svg>
<!-- ============================================================
示例 2:饼图 / 环形图(带描边展开动画)
============================================================ -->
<!--
环形图使用 stroke-dasharray + stroke-dashoffset 技巧
圆周长 = 2 * π * r ≈ 314.16(r=50)
每段弧长 = 百分比 × 314.16
动画:从 stroke-dashoffset = 圆周长 → 最终位置
-->
<svg class="chart-svg chart-svg--donut" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" style="width:280px;height:280px">
<!-- 底色圆环 -->
<circle cx="100" cy="100" r="50" fill="none"
stroke="var(--color-border)" stroke-width="20"/>
<!-- 数据段 1: 45% → 长度 141.37 -->
<circle class="chart-donut-seg" cx="100" cy="100" r="50" fill="none"
stroke="var(--color-primary)" stroke-width="20"
stroke-dasharray="141.37 172.79"
stroke-dashoffset="0"
style="--donut-delay: 0s"
transform="rotate(-90 100 100)"/>
<!-- 数据段 2: 30% → 长度 94.25,偏移 = 141.37 -->
<circle class="chart-donut-seg" cx="100" cy="100" r="50" fill="none"
stroke="var(--color-accent)" stroke-width="20"
stroke-dasharray="94.25 219.91"
stroke-dashoffset="-141.37"
style="--donut-delay: 0.3s"
transform="rotate(-90 100 100)"/>
<!-- 数据段 3: 25% → 长度 78.54,偏移 = 235.62 -->
<circle class="chart-donut-seg" cx="100" cy="100" r="50" fill="none"
stroke="var(--color-secondary)" stroke-width="20" opacity="0.5"
stroke-dasharray="78.54 235.62"
stroke-dashoffset="-235.62"
style="--donut-delay: 0.6s"
transform="rotate(-90 100 100)"/>
<!-- 中心文字 -->
<text class="chart-label" x="100" y="96" text-anchor="middle" font-size="18" font-weight="700" fill="var(--color-text)">100%</text>
<text class="chart-label" x="100" y="114" text-anchor="middle" font-size="10" fill="var(--color-text-secondary)">总计</text>
</svg>
<!-- ============================================================
示例 3:简单折线(用 polyline,带绘制动画)
============================================================ -->
<svg class="chart-svg chart-svg--line" viewBox="0 0 600 300" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-height:100%">
<!-- 网格 -->
<line x1="60" y1="20" x2="60" y2="260" stroke="var(--color-border)" stroke-width="1"/>
<line x1="60" y1="260" x2="580" y2="260" stroke="var(--color-border)" stroke-width="1"/>
<!-- 横向参考线 -->
<line x1="60" y1="80" x2="580" y2="80" stroke="var(--color-border)" stroke-width="0.5" stroke-dasharray="4"/>
<line x1="60" y1="140" x2="580" y2="140" stroke="var(--color-border)" stroke-width="0.5" stroke-dasharray="4"/>
<line x1="60" y1="200" x2="580" y2="200" stroke="var(--color-border)" stroke-width="0.5" stroke-dasharray="4"/>
<!-- 折线 — 使用 stroke-dasharray 实现绘制动画 -->
<!-- Agent 需要计算 polyline 总路径长度(近似值即可),设为 stroke-dasharray -->
<polyline class="chart-line-path" points="80,220 160,180 240,140 320,160 400,100 480,60 560,80"
fill="none" stroke="var(--color-primary)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
style="--line-length: 700"/>
<!-- 数据点 — 延迟淡入 -->
<circle class="chart-dot" cx="80" cy="220" r="4" fill="var(--color-primary)" style="--dot-delay: 0.8s"/>
<circle class="chart-dot" cx="160" cy="180" r="4" fill="var(--color-primary)" style="--dot-delay: 0.9s"/>
<circle class="chart-dot" cx="240" cy="140" r="4" fill="var(--color-primary)" style="--dot-delay: 1.0s"/>
<circle class="chart-dot" cx="320" cy="160" r="4" fill="var(--color-primary)" style="--dot-delay: 1.1s"/>
<circle class="chart-dot" cx="400" cy="100" r="4" fill="var(--color-primary)" style="--dot-delay: 1.2s"/>
<circle class="chart-dot" cx="480" cy="60" r="4" fill="var(--color-primary)" style="--dot-delay: 1.3s"/>
<circle class="chart-dot" cx="560" cy="80" r="4" fill="var(--color-primary)" style="--dot-delay: 1.4s"/>
<!-- X 轴标签 -->
<text x="80" y="280" text-anchor="middle" font-size="12" fill="var(--color-text-secondary)">1月</text>
<text x="160" y="280" text-anchor="middle" font-size="12" fill="var(--color-text-secondary)">2月</text>
<text x="240" y="280" text-anchor="middle" font-size="12" fill="var(--color-text-secondary)">3月</text>
<text x="320" y="280" text-anchor="middle" font-size="12" fill="var(--color-text-secondary)">4月</text>
<text x="400" y="280" text-anchor="middle" font-size="12" fill="var(--color-text-secondary)">5月</text>
<text x="480" y="280" text-anchor="middle" font-size="12" fill="var(--color-text-secondary)">6月</text>
<text x="560" y="280" text-anchor="middle" font-size="12" fill="var(--color-text-secondary)">7月</text>
</svg>
<!-- ============================================================
SVG 图表动画样式
Agent 将此 <style> base.html
关键:所有动画由 .slide.active 选择器触发,
Slide 不可见时元素保持初始状态,翻到时自动播放
============================================================ -->
<style>
/* --- 柱状图:宽度从 0 生长 --- */
.chart-bar {
transform-origin: left center;
transform: scaleX(0);
transition: none;
}
.slide.active .chart-bar {
animation: chartBarGrow 0.8s ease-out forwards;
}
@keyframes chartBarGrow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* 逐条错开:第 2/3/4 条延迟 */
.slide.active g:nth-child(3) .chart-bar { animation-delay: 0.1s; }
.slide.active g:nth-child(4) .chart-bar { animation-delay: 0.2s; }
.slide.active g:nth-child(5) .chart-bar { animation-delay: 0.3s; }
.slide.active g:nth-child(6) .chart-bar { animation-delay: 0.4s; }
/* --- 标签淡入 --- */
.chart-label {
opacity: 0;
}
.slide.active .chart-label {
animation: chartFadeIn 0.4s ease-out forwards;
animation-delay: 0.6s;
}
/* --- 环形图:描边展开 --- */
.chart-donut-seg {
opacity: 0;
}
.slide.active .chart-donut-seg {
animation: chartDonutReveal 0.8s ease-out forwards;
animation-delay: var(--donut-delay, 0s);
}
@keyframes chartDonutReveal {
from { opacity: 0; stroke-width: 0; }
to { opacity: 1; stroke-width: 20; }
}
/* --- 折线图:路径绘制 --- */
.chart-line-path {
stroke-dasharray: var(--line-length, 1000);
stroke-dashoffset: var(--line-length, 1000);
}
.slide.active .chart-line-path {
animation: chartLineDraw 1.2s ease-out forwards;
}
@keyframes chartLineDraw {
to { stroke-dashoffset: 0; }
}
/* --- 数据点:延迟弹入 --- */
.chart-dot {
opacity: 0;
transform-origin: center;
r: 0;
}
.slide.active .chart-dot {
animation: chartDotPop 0.3s ease-out forwards;
animation-delay: var(--dot-delay, 0.8s);
}
@keyframes chartDotPop {
from { opacity: 0; r: 0; }
to { opacity: 1; r: 4; }
}
/* --- 通用淡入 --- */
@keyframes chartFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
<!--
使用说明:
1. SVG 图表的动画完全由 CSS 驱动,通过 .slide.active 选择器自动触发
— 不需要 JS,不需要 slideAnimations 注册
2. Agent 根据实际数据调整坐标、颜色和文字
3. 所有颜色使用 CSS 变量,自动适配当前主题(浅色/深色均可)
4. SVG 内嵌到 HTML 中,无外部依赖,自包含
5. 适合数据点 ≤ 8 的简单图表,复杂图表请使用 Chart.js(见 chart-js.html)
6. Agent 需要将 <style> base.html
7. 折线图的 --line-length 需要 Agent 估算 polyline 的路径总长度
(近似方法:各点间距离之和,不需要精确)
-->

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,192 @@
<!--
Component: gsap-recipes
GSAP 高级动画配方 — Agent 在需要复杂动画时参考
前提:在 HTML 底部取消注释 GSAP CDN:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
-->
<!-- ============================================================
配方 1:数字滚动(Counter Animation)
适用于 big-number 布局的核心数字
============================================================ -->
<script>
// Agent 在 Slide 切换到包含数字的页面时触发
// targetEl: 数字元素, endValue: 目标数字, suffix: 后缀(如 '%', '+', '万')
function animateCounter(targetEl, endValue, suffix = '') {
const obj = { val: 0 };
gsap.to(obj, {
val: endValue,
duration: 1.5,
ease: 'power2.out',
onUpdate: () => {
targetEl.textContent = Math.round(obj.val).toLocaleString() + suffix;
}
});
}
// 使用示例:
// 监听 Slide 切换,当目标 Slide 变为 active 时调用
// animateCounter(document.querySelector('#stat1'), 98, '%');
</script>
<!-- ============================================================
配方 2:序列入场动画(Stagger Timeline)
适用于列表/卡片/时间线节点的依次入场
============================================================ -->
<script>
function staggerEnter(containerSelector, childSelector) {
const children = document.querySelectorAll(containerSelector + ' ' + childSelector);
gsap.from(children, {
opacity: 0,
y: 30,
duration: 0.5,
stagger: 0.12,
ease: 'power2.out'
});
}
// 使用示例:
// staggerEnter('.slide--cards .cards-grid', '.card');
// staggerEnter('.slide--timeline .timeline-track', '.timeline-item');
</script>
<!-- ============================================================
配方 3:打字机效果(Typewriter)
适用于标题页或引用页的文字逐字显示
============================================================ -->
<script>
function typewriter(targetEl, speed = 50) {
const text = targetEl.textContent;
targetEl.textContent = '';
targetEl.style.visibility = 'visible';
let i = 0;
const timer = setInterval(() => {
targetEl.textContent += text[i];
i++;
if (i >= text.length) clearInterval(timer);
}, speed);
}
// 使用示例:
// typewriter(document.querySelector('.title-main'), 60);
</script>
<!-- ============================================================
配方 4:进度条/数据条动画
适用于图表页中的水平进度条
============================================================ -->
<script>
function animateBars(containerSelector) {
const bars = document.querySelectorAll(containerSelector + ' rect[data-target-width]');
bars.forEach(bar => {
const targetWidth = parseFloat(bar.getAttribute('data-target-width'));
gsap.from(bar, {
attr: { width: 0 },
duration: 1,
ease: 'power2.out',
delay: 0.2
});
});
}
</script>
<!-- ============================================================
配方 5:Slide 切换集成(通用触发器)
Agent 将此脚本放在最终 HTML 底部,
自动在 Slide 切换时触发已注册的动画
============================================================ -->
<script>
// 动画注册表:Agent 在此注册每页的动画
// 支持三种注册方式:
// - GSAP 动画函数
// - Chart.js 延迟初始化(通过 createChartLazy)
// - 混合(同一页既有 GSAP 动画又有图表)
const slideAnimations = {
// 页码 → 动画函数(可以是单个函数或函数数组)
// 1: () => typewriter(document.querySelector('[data-slide="1"] .title-main')),
// 3: () => animateCounter(document.querySelector('#revenue'), 1200, '万'),
// 5: () => staggerEnter('[data-slide="5"] .cards-grid', '.card'),
};
// base.html 的 goTo() 函数已内置 slideAnimations 调用钩子:
// currentIndex = index;
// if (typeof slideAnimations !== 'undefined' && slideAnimations[currentIndex]) {
// slideAnimations[currentIndex]();
// }
// updateIndicator();
//
// Agent 无需手动修改 goTo(),只需填充 slideAnimations 即可。
</script>
<!-- ============================================================
配方 6:Chart.js 延迟初始化集成
将 Chart.js 图表与 slideAnimations 注册表无缝结合
前提:同时取消注释 Chart.js CDN
============================================================ -->
<script>
/**
* 延迟创建 Chart.js 实例 — Slide 激活时才 new Chart(),动画当场播放
* 详见 chart-js.html 中的完整函数定义和示例
*
* 与 GSAP 配方混合使用示例:
* 同一页既有数字滚动动画,又有 Chart.js 图表
*/
// ---- 混合注册示例 ----
// 假设第 4 页:左侧大数字 + 右侧柱状图
//
// const slideAnimations = {
// 4: () => {
// // GSAP:数字滚动
// animateCounter(document.querySelector('[data-slide="4"] .big-number'), 1200, '万');
// // Chart.js:延迟创建柱状图(createChartLazy 返回的是函数,需要调用它)
// createChartLazy('revenueChart', {
// type: 'bar',
// data: {
// labels: ['Q1', 'Q2', 'Q3', 'Q4'],
// datasets: [{
// label: '营收',
// data: [120, 190, 300, 250],
// backgroundColor: '#818cf8',
// borderRadius: 6
// }]
// },
// options: {
// responsive: true,
// maintainAspectRatio: false,
// animation: { duration: 1000, easing: 'easeOutQuart' },
// plugins: { legend: { display: false } },
// scales: {
// y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.08)' }, ticks: { color: '#8b95ad' } },
// x: { grid: { display: false }, ticks: { color: '#8b95ad' } }
// }
// }
// })(); // ← 注意末尾 (),createChartLazy 返回函数,这里立即调用
// },
// };
// ---- 纯 Chart.js 注册示例(无 GSAP)----
// 不需要 GSAP 时,只用 Chart.js CDN + createChartLazy 即可
//
// const slideAnimations = {
// 3: createChartLazy('lineChart', { type: 'line', data: {...}, options: {...} }),
// 5: createChartLazy('doughnutChart', { type: 'doughnut', data: {...}, options: {...} }),
// };
</script>
<!--
使用说明:
1. Agent 仅在需要高级动画时才引入 GSAP CDN 和这些配方
2. 简单的入场动画优先使用 CSS(见 animations.css),无需 GSAP
3. SVG 图表动画使用纯 CSS(见 chart-svg.html),无需 JS
4. Chart.js 图表必须通过 createChartLazy + slideAnimations 延迟初始化
5. GSAP 配方和 Chart.js 可混合注册在同一页的 slideAnimations 中
6. GSAP 和 Chart.js 的颜色值都使用实际色值(非 CSS 变量)
7. 每个配方都是独立函数,Agent 按需选择复制到最终 HTML 中
-->

View File

@ -0,0 +1,408 @@
# Slide 生成指南
本文件是 Agent 生成 Slide 时的核心参考。包含三部分:
1. **注册表** — 所有可选项的索引,Agent 靠描述做选择
2. **填充规则** — 模板使用规范和质量要求
3. **最佳实践** — 让 Slide 好看的关键技巧
---
## 一、注册表(Registry)
> Agent 通过注册表选择布局/主题/组件,选定后再读取具体文件。
### 1.1 布局注册表(Layout Registry)
| id | 文件 | 适用场景 | 典型内容特征 |
|---|---|---|---|
| `title` | layouts/title.html | 开篇第一页 | 有主标题 + 可选副标题、作者、日期 |
| `section` | layouts/section.html | 章节分隔 | 章节转换,承上启下,大章节编号 |
| `toc` | layouts/toc.html | 目录导航 | 全文章节概览,3-6 个章节条目 |
| `ending` | layouts/ending.html | 结尾总结 | 致谢/CTA/联系方式/二维码 |
| `content` | layouts/content.html | 标准要点表达 | 3-6 个要点(bullet points),或 2-3 段文字 |
| `two-column` | layouts/two-column.html | 对比或并列 | 两组内容需要左右并排展示 |
| `cards` | layouts/cards.html | 多项并列 | 3-6 个同级概念/特性/步骤/人物 |
| `big-number` | layouts/big-number.html | 数据冲击 | 1-3 个核心数字 + 简短标签和说明 |
| `quote` | layouts/quote.html | 引用或金句 | 一段引用文字 + 作者/出处 |
| `image` | layouts/image.html | 图片为主 | 图片是核心表达,文字为辅 |
| `chart` | layouts/chart.html | 数据图表 | 需要折线/柱状/饼图等数据可视化 |
| `timeline` | layouts/timeline.html | 时间或流程 | 3-6 个有序节点(年份/步骤/里程碑) |
| `comparison` | layouts/comparison.html | 对比分析 | 两个方案/产品/概念的优劣对比 |
| `pyramid` | layouts/pyramid.html | **严格层级**:层间有明确包含/递进/从属关系 | 马斯洛层次、组织架构、数据漏斗。**❌ 禁止用于**:平级选择/模式对比/方案权衡(→ 用 cards 或 comparison) |
### 1.2 主题注册表(Theme Registry)
> 16 套高品质主题,8 种视觉风格 × 浅色/深色成对。
| id | 文件 | 风格描述 | 适合场景 |
|---|---|---|---|
| `pure-light` | themes/pure-light.css | 极致简约,纯净白底,精密排版,系统蓝强调 | 产品发布、科技展示、设计提案、品牌介绍 |
| `pure-dark` | themes/pure-dark.css | 极致简约深色,纯黑背景,银白文字,专业沉稳 | 产品发布会、科技演讲、高端产品展示、夜间演示 |
| `warm-light` | themes/warm-light.css | 暖调学院风,奶油白背景,衬线标题,人文温度 | AI/科研汇报、品牌故事、人文科技主题、温暖叙事 |
| `warm-dark` | themes/warm-dark.css | 深邃暖调,深灰绿底,琥珀强调,知性沉稳 | AI 研究分享、技术深度演讲、产品战略、沉浸式叙事 |
| `cyber-light` | themes/cyber-light.css | 科技蓝紫,纯净白底,蓝紫渐变强调,现代感 | AI/SaaS 产品介绍、技术方案展示、创业路演 |
| `cyber-dark` | themes/cyber-dark.css | 赛博霓虹,深蓝黑底,蓝紫光弧渐变,未来感 | AI/SaaS 产品发布、技术演讲、黑客松展示、极客风格 |
| `data-light` | themes/data-light.css | 数据学院,白底深蓝,青绿强调色,数据可视化友好 | 数据分析报告、研究成果展示、技术趋势分析 |
| `data-dark` | themes/data-dark.css | 数据仪表盘,深蓝黑底,青绿双强调色,仪表盘质感 | 数据仪表盘展示、趋势分析演讲、实时数据演示 |
| `azure-light` | themes/azure-light.css | 蓝调科技风,纯白背景,经典蓝强调,专业大气 | 企业汇报、科技产品发布、ToB 方案展示、品牌宣讲 |
| `azure-dark` | themes/azure-dark.css | 蓝调科技深色,深灰黑底,明亮蓝强调,科技沉稳 | 企业年会演讲、科技产品发布会、ToB 深度方案、夜间演示 |
| `glass-light` | themes/glass-light.css | 液态玻璃浅色,多层半透明折射,蓝紫粉色调光斑 | 产品发布、设计提案、品牌展示、科技新品介绍 |
| `glass-dark` | themes/glass-dark.css | 液态玻璃深色,深邃背景上的多层折射光感,冷调蓝紫光斑 | 产品发布会、科技演讲、高端品牌展示、沉浸式叙事 |
| `frost-light` | themes/frost-light.css | 磨砂玻璃浅色,经典 glassmorphism,强模糊清晰面板边界 | SaaS 产品介绍、技术方案展示、数据报告、企业分享 |
| `frost-dark` | themes/frost-dark.css | 磨砂玻璃深色,深邃背景上的经典 glassmorphism,冷调科技感 | 技术演讲、数据仪表盘展示、产品深度分析、夜间演示 |
| `gradient-light` | themes/gradient-light.css | 渐变浅色,粉紫蓝大胆渐变背景,高对比文字,视觉冲击力强 | 创业路演、产品发布、创意提案、品牌宣讲 |
| `gradient-dark` | themes/gradient-dark.css | 渐变深色,深紫蓝渐变背景,霓虹感强调色,沉浸式体验 | 产品发布会、创意演讲、音乐/艺术展示、高端品牌 |
### 1.3 组件注册表(Component Registry)
| id | 文件 | 用途 | 何时使用 |
|---|---|---|---|
| `chart-svg` | components/chart-svg.html | 简单图表(SVG 内嵌) | 饼图/环形图/简单柱状图,数据点 ≤ 8 |
| `chart-js` | components/chart-js.html | 复杂图表(Chart.js) | 多系列折线/堆叠柱状/雷达图/需交互 |
| `animations` | components/animations.css | CSS 入场动画 | 所有页面默认可用的基础动画类 |
| `gsap-recipes` | components/gsap-recipes.html | 高级动画(GSAP) | 数字滚动、打字机效果、复杂时序编排 |
---
## 二、填充规则
### 2.1 三层约束(必须遵守)
| 层次 | 自由度 | 规则 | 示例 |
|------|--------|------|------|
| **结构层(HTML 标签)** | 🔒 严格遵循 | 模板定义的标签嵌套和 class 命名必须严格遵循 | `<div class="card">` 内部必须有 `<h3>` + `<p>`,不能改成 `<span>``<div>` |
| **内容层(数据填充)** | 🔓 灵活调整 | 文字数量、卡片个数、列表条目数可根据实际内容调整 | cards 模板示例 3 张卡片,实际需要 4 张 → 加一张 |
| **样式层(CSS)** | ⚠️ 变量控制 | 所有视觉样式通过 CSS 变量控制,不可用行内样式覆写变量控制的属性 | ✅ `style="margin-top: var(--spacing-md)"``style="color: red"` |
### 2.2 模板使用流程
```
1. 从注册表选择布局 → 读取对应 layout 文件
2. 复制 <section> 部分到 base.html 的 .slide-deck 中
3. 复制 <style> base.html
4. 替换所有 {{...}} 占位符为实际内容
5. 设置正确的 data-slide="N" 页码
6. 添加注释标识:<!-- Slide N: 页面标题 -->
```
### 2.3 主题选择与注入流程
**选择:可视化为默认路径,对话为降级路径**
> 除非用户已在对话中明确指定了主题,否则必须先尝试可视化选择。
```
默认路径(需 Python + 浏览器):
1. 删除旧的 .theme-choice 文件
2. 启动 theme-server.py → 打开浏览器访问 theme-picker.html
3. 自动轮询 .theme-choice 文件(每 2 秒,最长 120 秒)
4. 文件出现 → 读取主题 id(禁止要求用户回聊天再确认)
降级路径(启动失败 / 浏览器打不开 / 120 秒超时):
1. 根据内容调性从注册表推荐 1-2 个主题
2. 用户在对话中确认
```
**注入:**
```
1. 读取对应 theme CSS 文件:themes/{theme_id}.css
2. 用文件中的 :root { ... } 替换 base.html 中 /* THEME_VARS_START */ 与 /* THEME_VARS_END */ 之间的内容
注意:骨架有两个 :root 块,第一个是 Slide 尺寸(不可替换),第二个才是主题变量(由标记包裹)
3. 如果用户提供了参考物(PPT/网页/图片),提取配色后自定义变量值
```
### 2.4 产物规范
每个生成的 HTML 必须满足:
| 规范 | 说明 | 示例 |
|------|------|------|
| 页面注释 | 每页 Slide 有注释标识 | `<!-- Slide 3: 市场分析 -->` |
| 语义化 class | 布局类型体现在 class 中 | `class="slide slide--cards"` |
| data 属性 | 页码通过 data 属性 | `data-slide="3"` |
| CSS 变量驱动 | 所有颜色/字体/间距通过变量 | `color: var(--color-primary)` |
| 16:9 比例 | 固定 1280×720 逻辑尺寸 | 由 base.html 骨架保证 |
| 自包含 | 单个 HTML 文件可独立打开 | 无外部依赖(CDN 除外) |
### 2.5 分批生成策略
当 Slide 超过 10 页时,**必须分批生成**:
```
批次 1:骨架 + 主题变量 + 前 5-8 页 → 写入完整 HTML 文件
批次 2:继续生成后续页面 → 定位到 </div><!-- /slide-deck --> 结束标签之前,插入新的 <section>
批次 3:(如需)继续在同一位置追加
```
这是标准流程,不是异常处理。原因:避免单次输出超出 Token 限制。
**关键**:追加时使用文件编辑能力,定位到 slide-deck 结束标签之前插入新内容,不要覆写整个文件。
---
## 三、最佳实践
### 3.1 内容精炼原则
| 原则 | 说明 |
|------|------|
| **一页一个核心观点** | 不要在一页里塞多个不相关的观点 |
| **文字 ≤ 50 字/条目** | 要点描述简短有力,详细内容留给演讲者 |
| **数字胜过文字** | 能用数据说话就不用长段文字 |
| **宁可多页不挤页** | 内容过多时拆分为多页,而不是缩小字号 |
| **避免连续轻量页** | section(章节页)和 quote(引用页)是过渡/点缀,不是主力。**连续 2 页以上只有一句话**属于内容密度过低,应合并或改为信息量更高的布局(cards/content/chart) |
| **section 页 ≤ 总页数 20%** | 一套 15 页 Slide 中章节分隔页不超过 3 页;多余的 section 应合并或去掉 |
### 3.2 布局选择决策树
```
内容特征 → 布局选择:
有多个并列概念(3-5个)?
└── 是 → cards
有两组需要对比的内容?
├── 简单对比 → two-column
└── 详细优劣分析 → comparison
有关键数据/数字?
├── 1-3 个核心数字 → big-number
└── 趋势/分布数据 → chart
有时间线/流程/步骤?
└── 是 → timeline
有层级/递进关系?(🔴 必须通过"互换测试":顶层和底层互换后语义是否被破坏?)
├── 是,互换后语义被破坏(如需求层次、组织架构、漏斗) → pyramid
└── 否,互换后语义不变(如多种模式/方案/类型的选择权衡) → cards 或 comparison
⚠️ 常见误判:"容器/云/本地"、"三种部署模式"、"安全与能力权衡"→ 这些是平级选择,禁止用 pyramid
有引用/金句?
└── 是 → quote
有重要图片?
└── 是 → image
以上都不是?
└── content(万能布局)
```
### 3.3 动画使用指南
| 场景 | 推荐动画 | 来源 |
|------|----------|------|
| 标题入场 | `anim-title-enter` | animations.css |
| 列表逐条出现 | `anim-slide-up` + `anim-delay-N` | animations.css |
| 卡片依次弹入 | `anim-bounce-in` + `anim-delay-N` | animations.css |
| 数字从 0 滚动 | `animateCounter()` | gsap-recipes.html(需 GSAP) |
| 打字机文字 | `typewriter()` | gsap-recipes.html(需 GSAP) |
| 普通元素入场 | `anim-fade-in` | animations.css |
**原则**:优先使用 CSS 动画(animations.css),仅在需要数字滚动/打字机/复杂时序时才引入 GSAP。
### 3.4 图表选择指南
| 数据特征 | 推荐方案 | 参考文件 |
|---------|---------|---------|
| 简单柱状图/饼图,数据点 ≤ 8 | SVG 内嵌 | chart-svg.html |
| 多系列折线/堆叠柱状/雷达图 | Chart.js | chart-js.html |
| 超大数据量/特殊图表 | ECharts | (Agent 自行编写) |
**重要**:Chart.js 和 ECharts 不支持 CSS 变量作为颜色值。Agent 必须从所选主题中提取实际色值注入到图表配置中。
### 3.5 玻璃/渐变主题注意事项
glass / frost / gradient 系列主题使用半透明 `--color-surface` + `backdrop-filter`。Agent 生成时注意:
| 注意点 | 说明 |
|--------|------|
| Chart.js 颜色 | 从主题提取实际色值时,注意 `--color-surface``rgba()` 格式,图表背景应使用 `--color-bg` 的实际色值 |
| 卡片边框 | 玻璃主题的 `--color-border` 是半透明白色,不要用不透明色值覆盖 |
| 深色主题对比度 | glass-dark / frost-dark 的 surface 透明度 12-16%,确保文字可读性 |
| 饱和度增强 | glass/frost 主题通过 `--surface-saturate`(glass 200%、frost 180%)增强透过面板的色彩鲜艳度,Agent 无需手动处理 |
| 背景光斑 | `--bg-pattern` 的渐变光斑由 base.html 自动渲染(`.slide::before`),Agent 无需手动添加 |
| 浏览器兼容 | `backdrop-filter` 需要 `-webkit-` 前缀兼容 Safari,base.html 已内置 |
### 3.6 图表动画最佳实践
图表动画分两条路径,按场景选择:
| 方案 | 动画机制 | 触发方式 | 适用场景 |
|------|----------|----------|----------|
| SVG 图表 | CSS `@keyframes` | `.slide.active` 选择器自动触发 | 简单图表,无需 JS |
| Chart.js 图表 | Chart.js 内置动画 | `createChartLazy()` + `slideAnimations` 延迟初始化 | 复杂图表 |
| Chart.js + GSAP 混合 | 两者各自动画 | 同一 `slideAnimations` 回调内分别调用 | 同页有数字滚动 + 图表 |
**核心规则:Chart.js 必须延迟初始化**
Chart.js 的动画在 `new Chart()` 调用时立即播放。如果在页面加载时就创建所有图表,用户翻到图表页时动画已结束。解决方案:
```
1. 使用 createChartLazy(canvasId, config) 包装图表配置(见 chart-js.html)
2. 注册到 slideAnimations:
const slideAnimations = {
3: createChartLazy('barChart', { type: 'bar', ... }),
};
3. base.html 的 goTo() 已内置钩子,翻到第 3 页时自动调用,图表当场创建并播放动画
4. createChartLazy 内置防重复(前后翻页时先 destroy 再重建)
```
**SVG 图表动画无需 JS**
SVG 图表的动画完全由 CSS 驱动,Agent 只需:
1. 给 SVG 元素加上对应的动画 class(`.chart-bar` / `.chart-donut-seg` / `.chart-line-path` / `.chart-dot`
2. 将 chart-svg.html 底部的 `<style>` 块注入到 base.html 的布局样式占位区域
3. 动画自动在 `.slide.active` 时触发,不需要注册 slideAnimations
**图表标注/数据标签安全区**
Chart.js 的 annotation、数据标签(如 `chartjs-plugin-annotation`、自定义 HTML 标注)和图例容易超出 `.chart-container``overflow: hidden` 边界被裁切。规避方法:
- 峰值标注、数据标签等辅助信息应放在图表区域**内部**(通过 Chart.js plugins),不要用绝对定位的 HTML 元素浮在 chart-container 边缘
- 如果需要在图表外展示标注(如右上角数字),放在 `.chart-title` 同级或 `.chart-footnote` 中,不要放在 `.chart-container` 内部的绝对定位元素中
- 数据标签文字长度需预估:中文 ≤ 8 字 + 数字,确保不溢出容器右边界
**深色主题图表适配**
Chart.js 图表在深色主题下需额外设置(SVG 图表使用 CSS 变量,自动适配):
| 属性 | 浅色主题 | 深色主题 |
|------|----------|----------|
| grid.color | `'rgba(0,0,0,0.06)'` | `'rgba(255,255,255,0.08)'` |
| ticks.color | 可省略(默认深色) | 主题的 `--color-text-secondary` 实际色值 |
| legend.labels.color | 可省略 | 主题的 `--color-text` 实际色值 |
| tooltip 背景/文字 | 可省略(默认即可) | 建议自定义匹配主题色调 |
### 3.7 版式规范(必须遵守)
> 这些规则的核心目标:**页面看起来像专业设计师做的,而不是"到处贴白色大板子"。**
#### 3.7.1 反过度容器化
**原则:内容直接落在页面背景上是默认状态,容器是加分项而非必选项。**
| ✅ 应该用容器的场景 | ❌ 不该用容器的场景 |
|---|---|
| cards 布局的每张卡片(天然需要分区) | 正文内容页(content 布局)的整体包裹 |
| comparison 布局的两侧对比区域 | 目录页、结论页、引用页的内容包裹 |
| big-number 的数字突出区 | 图表页的图表外再套一层"白板" |
| 需要视觉分组的不相关内容 | 单一内容块外再加一层无意义容器 |
**禁止**:
- 容器套容器(如:大白卡片里面再放小白卡片)
- 给整页内容套一个比 `.slide` padding 还窄的 `max-width` 容器
- 对 content / quote / timeline / pyramid 布局额外添加全页包裹容器
#### 3.7.2 版心宽度
页面内容应充分利用 `.slide``padding` 以内的可用空间。不同布局的版心利用率:
| 布局类型 | 内容宽度占可用宽度 | 说明 |
|---------|-------------------|------|
| title / section / ending | 100%(居中对齐即可) | 封面/章节/结尾不需要额外收窄 |
| content / quote / timeline | 100% | 标题和内容平铺,不加额外 max-width |
| cards / comparison | 100% | grid 自适应,不需要外层限宽 |
| chart | 100% | 图表需要最大面积展示数据 |
| two-column | 100%(两列自然分配) | 列内容自然限宽 |
| big-number | 可适当居中收窄至 80-90% | 大数字居中聚焦 |
**禁止**:在 `.slide` 的 padding 基础上再叠加大 margin 或小 max-width,造成"双重收缩"。
#### 3.7.3 垂直预算
每个页面的垂直空间(720px - 上下 padding)按以下预算分配:
| 区域 | 占比 | 说明 |
|------|------|------|
| 标题区 | ≤ 15% | 标题 + 可选副标题。标题过长时缩短文案或拆 kicker,不允许挤压主体 |
| 主体区 | ≥ 65% | 核心内容。这是页面的主角 |
| 脚注/注释区 | ≤ 10% | 数据来源、小字注释。必须在主体区外部 |
| 弹性间距 | ≈ 10% | 标题与主体之间、主体与脚注之间的自然呼吸空间 |
**实现建议**:优先使用 CSS grid 三段式(`grid-template-rows: auto 1fr auto`),避免纯 padding/margin 堆砌。
**垂直对齐**:页面整体视觉重心应居中偏上(约 45% 位置),不要出现"上空下挤"或"上挤下空"。当内容较少(如 two-column / comparison 只有 2-3 项)时,`.slide``justify-content: center` 已保证垂直居中,不要额外加 `margin-top``padding-top` 把内容推到上方。
#### 3.7.4 色值全覆盖
> **红线:主题切换后,页面不应残留任何与新主题不一致的色值。**
| 元素 | 正确做法 | 错误做法 |
|------|---------|---------|
| 文字颜色 | `var(--color-text)` / `var(--color-text-secondary)` | `color: #333` |
| 背景色 | `var(--color-bg)` / `var(--color-surface)` / `var(--color-bg-alt)` | `background: #f5f5f5` |
| 边框 | `var(--color-border)` | `border-color: #eee` |
| 强调色 | `var(--color-primary)` / `var(--color-accent)` | `color: #2563eb` |
| 渐变 | 基于主题变量构建 | 硬编码渐变色值 |
| Chart.js 配色 | 从主题 CSS 文件中提取实际色值注入 | 使用与主题无关的默认色板 |
| 自定义装饰 | 基于 `var(--color-primary)` 透明度变体 | 硬编码装饰色 |
**唯一例外**:Chart.js / ECharts 的 JS 配置不支持 CSS 变量,必须用从主题提取的实际色值(如 `'#2563eb'`),但该色值必须来自当前主题。
### 3.8 常见错误和规避(含版式错误)
| 错误 | 级别 | 后果 | 正确做法 |
|------|------|------|---------|
| Title 页 meta 填入 Agent 工作信息 | 🔴 | 元信息泄漏("基于 N 份材料生成"、生成日期等出现在封面) | title-meta 仅填用户明确提供的作者/日期;未提供则删除整个 `.title-meta` div |
| 一页内容过多导致溢出 | 🔴 | 内容被截断不可见 | content ≤ 4 条(含描述)、cards ≤ 4 个(带描述时;纯标题卡片 ≤ 6)、comparison 每侧 ≤ 3 项(带描述时)、timeline ≤ 6 节点,超出则拆分为多页 |
| Chart.js 未用 `createChartLazy()` | 🔴 | 翻到图表页时动画已结束,用户只看到静态图表 | 必须用 `createChartLazy()` 延迟初始化 + 注册到 `slideAnimations` + 取消注释 CDN + 在 CDN 后放函数定义(四步缺一不可) |
| 用行内样式覆写主题变量控制的属性 | 🟡 | 换主题后样式不一致 | 使用 CSS 变量 |
| Chart.js 中使用 `var(--color-*)` | 🟡 | 图表颜色不生效 | 使用实际色值如 `'#1d4ed8'` |
| 忘记设置 `data-slide` 属性 | 🟡 | 导航功能异常 | 每页必须有递增的 `data-slide` |
| 所有页面用同一种布局 | 🟡 | 视觉单调 | 根据内容特征混合使用不同布局 |
| GSAP 未加载就调用 | 🟡 | JS 报错 | 先取消注释 CDN,再使用 GSAP 函数 |
| 正文页套大白卡片容器 | 🔴 | 页面笨重、横向浪费 | 内容直接落在页面背景上(见 §3.7.1) |
| 容器套容器(双层包裹) | 🔴 | 层级过多、空间压缩 | 去掉外层容器,保留内层元素即可 |
| 内容区加额外 max-width | 🟡 | 横向大面积留白 | 使用 `.slide` padding 已有的安全边距(见 §3.7.2) |
| 硬编码 `#xxxxxx` 色值 | 🟡 | 换主题后残留旧色 | 全部走 CSS 变量或从主题提取(见 §3.7.4) |
| 标题过长挤压图表/主体 | 🟡 | 内容被压缩或遮挡 | 缩短标题或拆为 kicker + 主标题(见 §3.7.3) |
| cards 列数异常 | 🔴 | 卡片列数不合理(如 4 卡竖排、3 卡单列) | CSS `:has()` 已自动强制列数(2卡→2列、3卡→3列、4卡带描述→2×2、4卡无描述→4列、5/6卡→3列),Agent 无需手动设 grid-template-columns |
| cards 未垂直居中 | 🟡 | 卡片偏上方,底部大面积空白 | `.cards-grid` 已设 `align-content: center`,Agent 不要覆盖 |
| 自定义图形未居中 | 🟡 | SVG/canvas 金字塔等自定义图形偏上或偏左 | `.slide` 已有 `display: flex; justify-content: center`,自定义图形容器不要覆盖对齐方式,内容应自然居中于 slide 内容区 |
| Chart.js 未延迟初始化 | 🔴 | 图表无入场动画 | 必须使用 `createChartLazy()` + `slideAnimations` 注册(见 §3.6),不能直接 `new Chart()` |
| 连续轻量页 | 🟡 | 内容密度低,像未完成的草稿 | section/quote 页是过渡点缀,连续 2+ 页只有一句话必须合并或改用信息量更高的布局 |
| Chart.js 动画抖动 | 🟡 | 柱状图/折线图入场时画面跳动 | Chart.js 配置中设 `animation.duration: 800`(不超过 1000ms)、`easing: 'easeOutQuart'`;避免同时对 x/y 轴做动画,优先 y 轴增长 |
| Canvas CSS 尺寸覆盖 | 🔴 | 图表加载时从中心扩张、hover 时再次扩张/抖动 | 禁止给 canvas 设 CSS width/height(尤其 !important),Chart.js responsive 自行管理尺寸(见 chart-js.html §7) |
| Chart.js tooltip 被禁用 | 🔴 | hover 数据点无任何反馈,用户无法查看具体数值 | 禁止 `tooltip.enabled: false``interaction.mode: 'none'`。可自定义 tooltip 样式但不能禁用(见 chart-js.html §8) |
| 图表使用静态图片 | 🔴 | 图表区域嵌入 `<img>` 静态截图,无交互、无动画、无法适配主题 | 必须使用 `<canvas>`(Chart.js)或内联 `<svg>` 实现,禁止 `<img>` |
| 标题换行(title/ending 页) | 🔴 | 首页/尾页大标题折行,视觉不专业 | 中文主标题 ≤ 16 字,副标题 ≤ 30 字;内容页 heading ≤ 22 字(见 §3.9) |
| 主题/风格元数据泄漏 | 🔴 | Slide 内容中出现主题名称(如"腾讯 light 风格")、风格 id、模板类型名等 Agent 内部标识 | 这些是 Skill 的技术元数据,不是给观众看的内容。禁止以 badge、标签、副标题或任何形式出现在 Slide 页面中 |
| 金字塔顶层文字溢出 | 🟡 | 顶层梯形窄,文字超出 clip-path 被裁剪 | 顶层标题 ≤ 6 字,描述 ≤ 10 字;CSS 已用 `calc(var(--inset-top) + 5%)` 动态 padding |
| pyramid 用于非层级内容 | 🟡 | 内容是平级选择/权衡,但用了 pyramid 暗示上下级关系,语义误导 | pyramid 仅用于明确的层级/递进关系(如安全级别、组织架构)。平级对比/权衡 → 用 cards 或 comparison(见 §3.2) |
| Chart.js canvas 重置导致双重渲染 | 🔴 | 图表入场时出现"2套图表"分离抖动,从中心向两侧扩散 | `createChartLazy()` 中禁止 `canvas.width = canvas.width``destroy()` 已足够清理(见 chart-js.html) |
| 卡片图标类型混用 | 🟡 | 同一页卡片混用 emoji、字母、罗马数字,风格不统一 | 同一页内所有卡片必须使用同一种图标类型(见 §3.10) |
### 3.10 卡片图标选择规则(cards 布局 `.card-icon` 区域)
cards 布局的每张卡片有一个 `.card-icon` 区域。Agent 需要根据内容语义选择图标类型,**同一页内所有卡片必须使用同一种图标类型**,不得混用。
| 内容特征 | 图标类型 | 示例 |
|---------|---------|------|
| 卡片代表不同概念/角色/功能(有明确语义差异) | **emoji 图标** | 🧠 大脑、🔧 工具、🤝 握手 |
| 卡片是有序步骤/阶段(强调顺序) | **数字序号** | 1、2、3(用阿拉伯数字,不要用罗马数字) |
| 卡片是平级分类/选项(强调并列不强调顺序) | **emoji 图标****字母** | A、B、C 或对应的 emoji |
**规则:**
- 🔴 同一页的卡片图标类型必须一致(全 emoji / 全数字 / 全字母)
- 🟡 优先使用 emoji 图标(视觉丰富度最高、语义表达最强)
- 🟡 仅在内容确实是有序步骤时才用数字序号
- ❌ 禁止使用罗马数字(I/II/III)——在中文语境下辨识度低,且与英文字母易混淆
### 3.9 标题长度限制(必须遵守)
> base.html 的 `text-wrap: balance``min()` 字号上限提供了 CSS 级保护,但 Agent 仍应在内容层控制标题长度,避免触发保护机制导致视觉降级。
| 元素 | 最大字数(中文) | 最大字符(英文) | 超出处理 |
|------|----------------|-----------------|---------|
| title 页主标题(h1) | 16 字 | 32 字符 | 拆为 kicker + 主标题,或精炼措辞 |
| title 页副标题 | 30 字 | 60 字符 | 精炼或拆为两行(用 `<br>`) |
| 内容页标题(h2) | 22 字 | 44 字符 | 精炼措辞,不要把完整句子当标题 |
| ending 页标题 | 8 字 | 16 字符 | 致谢语天然简短,如"谢谢" "Thank You" |
| chart 页标题 | 18 字 | 36 字符 | CSS 已有 2 行 line-clamp 兜底,但应避免触发 |
| pyramid 顶层标题 | 6 字 | 12 字符 | 梯形窄,超长必被裁剪 |
| pyramid 顶层描述 | 10 字 | 20 字符 | 同上 |
**CSS 防御机制**(L0 base.html 已实现,Agent 无需处理):
- `h1` / `h2``font-size: min(var(--font-size-*), Npx)` 防止主题字号过大
- `h1` / `h2``text-wrap: balance` 优化中文换行断点
- chart 标题:`-webkit-line-clamp: 2` 最多显示 2 行

View File

@ -0,0 +1,66 @@
<!--
Layout: big-number
大数字页 — 1-3 个核心数据,强视觉冲击
Agent 根据数据量调整 stat 数量
-->
<section class="slide slide--big-number" data-slide="{{N}}">
<h2 class="bignum-title">{{页面标题}}</h2>
<div class="bignum-grid">
<div class="stat">
<span class="stat-value">{{数字,如 98%}}</span>
<span class="stat-label">{{标签,如"客户满意度"}}</span>
<span class="stat-desc">{{简短说明(可选)}}</span>
</div>
<div class="stat">
<span class="stat-value">{{数字}}</span>
<span class="stat-label">{{标签}}</span>
<span class="stat-desc">{{简短说明}}</span>
</div>
<div class="stat">
<span class="stat-value">{{数字}}</span>
<span class="stat-label">{{标签}}</span>
<span class="stat-desc">{{简短说明}}</span>
</div>
</div>
</section>
<style>
.slide--big-number {
justify-content: center;
align-items: center;
text-align: center;
}
.slide--big-number .bignum-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-xl);
}
.slide--big-number .bignum-grid {
display: flex;
justify-content: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.slide--big-number .stat {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
min-width: 200px;
}
.slide--big-number .stat-value {
font-size: 4.5rem;
font-weight: 800;
color: var(--color-primary);
line-height: 1;
font-variant-numeric: tabular-nums;
}
.slide--big-number .stat-label {
font-size: var(--font-size-subheading);
font-weight: 600;
color: var(--color-text);
}
.slide--big-number .stat-desc {
font-size: var(--font-size-small);
color: var(--color-text-secondary);
}
</style>

View File

@ -0,0 +1,131 @@
<!--
Layout: cards
卡片页 — 多个并列概念,每个含图标+标题+描述
Agent 根据内容调整卡片数量:
- 带描述的卡片:≤ 4 个(硬上限),超出必须拆页
- 纯标题卡片(无描述):≤ 6 个
- 5 个概念 + 描述?→ 拆为两页 cards 或转用 content/timeline 布局
安全区约束:
- 图标区:最小 40px 高度
- 标题:最多 2 行
- 描述:最多 3 行
- 超出则缩短文案,不要硬塞
⚠️ 溢出红线:cards-grid 高度受页面约束,5+ 张带描述卡片必定溢出视口。
布局规则(CSS :has() 自动强制列数,Agent 无需干预):
- 2 卡 → 2 列 | 3 卡 → 3 列
- 4 卡带描述 → 2×2 | 4 卡无描述 → 4 列
- 5 卡 → 3 列(上 3 下 2) | 6 卡 → 3×2
-->
<section class="slide slide--cards" data-slide="{{N}}">
<h2 class="cards-title">{{页面标题}}</h2>
<div class="cards-grid">
<div class="card">
<div class="card-icon">{{图标,如 emoji 或 SVG}}</div>
<h3 class="card-heading">{{卡片标题}}</h3>
<p class="card-desc">{{卡片描述}}</p>
</div>
<div class="card">
<div class="card-icon">{{图标}}</div>
<h3 class="card-heading">{{卡片标题}}</h3>
<p class="card-desc">{{卡片描述}}</p>
</div>
<div class="card">
<div class="card-icon">{{图标}}</div>
<h3 class="card-heading">{{卡片标题}}</h3>
<p class="card-desc">{{卡片描述}}</p>
</div>
<!-- Agent 根据内容增减 card -->
</div>
</section>
<style>
.slide--cards {
display: grid;
grid-template-rows: auto 1fr;
/* ❌ 不设 align-items: start — 保持默认 stretch 让 cards-grid 撑满剩余高度 */
}
.slide--cards .cards-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-md);
}
.slide--cards .cards-grid {
display: grid;
/* 默认 fallback:auto-fit,:has() 规则会按实际卡片数强制覆盖 */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
align-content: center;
min-height: 0;
overflow: hidden;
}
/* ── 卡片列数强制规则(:has() 自动检测,Agent 无需干预)── */
.slide--cards .cards-grid:has(> .card:nth-child(2):last-child) {
grid-template-columns: repeat(2, 1fr); /* 2 卡 → 2 列 */
}
.slide--cards .cards-grid:has(> .card:nth-child(3):last-child) {
grid-template-columns: repeat(3, 1fr); /* 3 卡 → 3 列 */
}
/* 4 卡带描述 → 2×2 */
.slide--cards .cards-grid:has(> .card:nth-child(4):last-child):has(.card-desc:not(:empty)) {
grid-template-columns: repeat(2, 1fr);
}
/* 4 卡无描述 → 4 列 */
.slide--cards .cards-grid:has(> .card:nth-child(4):last-child):not(:has(.card-desc:not(:empty))) {
grid-template-columns: repeat(4, 1fr);
}
/* 5 卡 → 6 列虚拟网格,上 3 下 2 居中 */
.slide--cards .cards-grid:has(> .card:nth-child(5):last-child) {
grid-template-columns: repeat(6, 1fr);
}
.slide--cards .cards-grid:has(> .card:nth-child(5):last-child) > .card:nth-child(1) { grid-column: 1 / 3; }
.slide--cards .cards-grid:has(> .card:nth-child(5):last-child) > .card:nth-child(2) { grid-column: 3 / 5; }
.slide--cards .cards-grid:has(> .card:nth-child(5):last-child) > .card:nth-child(3) { grid-column: 5 / 7; }
.slide--cards .cards-grid:has(> .card:nth-child(5):last-child) > .card:nth-child(4) { grid-column: 2 / 4; }
.slide--cards .cards-grid:has(> .card:nth-child(5):last-child) > .card:nth-child(5) { grid-column: 4 / 6; }
/* 6 卡 → 3×2 */
.slide--cards .cards-grid:has(> .card:nth-child(6):last-child) {
grid-template-columns: repeat(3, 1fr);
}
.slide--cards .card {
background: var(--color-surface);
padding: var(--spacing-sm) var(--spacing-md); /* 上下紧凑(1rem)、左右宽松(2rem),防止内容被 overflow:hidden 裁切 */
border-radius: var(--border-radius);
box-shadow: var(--shadow-md);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center; /* 内容垂直居中 */
gap: var(--spacing-xs); /* 图标/标题/描述之间紧凑间距(0.5rem),为内容留空间 */
border: 1px solid var(--color-border);
min-height: 160px; /* 最小高度保证视觉存在感,避免卡片过小 */
/* 不设 overflow:hidden — 由 line-clamp 控制文字截断,避免裁剪卡片内容 */
}
.slide--cards .card-icon {
font-size: 2.5rem;
line-height: 1;
min-height: 40px; /* 图标最小安全区 */
display: flex;
align-items: center;
justify-content: center;
}
.slide--cards .card-heading {
font-size: var(--font-size-subheading);
font-weight: 600;
display: -webkit-box;
-webkit-line-clamp: 2; /* 标题最多 2 行 */
-webkit-box-orient: vertical;
overflow: hidden;
}
.slide--cards .card-desc {
font-size: var(--font-size-small);
color: var(--color-text-secondary);
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3; /* 描述最多 3 行 */
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,79 @@
<!--
Layout: chart
图表页 — 数据可视化,CSS grid 三段式强制布局:标题 | 图表 | 脚注
Agent 在图表区域嵌入 SVG / Chart.js / ECharts(参考 components/ 下的图表参考)
垂直预算:标题区 ≤ 15%、图表区 ≥ 70%、脚注区 ≤ 10%
禁止:标题过长挤压图表、图表容器外再套白色大卡片
🔴 Chart.js 动画红线:
- 使用 Chart.js 时,必须用 createChartLazy() 延迟初始化(见 chart-js.html)
- 必须注册到 slideAnimations 对象:slideAnimations[N] = createChartLazy(...)
- 必须取消注释 HTML 底部的 Chart.js CDN <script>
- 必须在 CDN 之后、slideAnimations 之前放置 createChartLazy 函数定义
- 违反以上任意一条 → 图表动画丢失(用户翻到该页时看到静态图表)
🔴 静态图片红线:
- 图表区域禁止使用 <img> 标签嵌入静态截图/预渲染图片
- 必须使用 <canvas>(Chart.js)或内联 <svg> 实现可交互/可动画的图表
- 违反 → 图表无法自适应主题配色、无交互、无动画
-->
<section class="slide slide--chart" data-slide="{{N}}">
<h2 class="chart-title">{{页面标题}}</h2>
<div class="chart-container">
<!-- Agent 在此区域嵌入图表代码 -->
<!-- SVG 内嵌 / <canvas> + Chart.js / ECharts -->
{{图表代码}}
</div>
<p class="chart-footnote">{{图表注释/数据来源(可选,留空则不显示)}}</p>
</section>
<style>
.slide--chart {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: stretch;
gap: 0;
}
.slide--chart .chart-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-sm);
/* 用 line-clamp 替代 max-height+overflow:hidden,避免截断第二行文字显示不全 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.slide--chart .chart-container {
position: relative; /* Chart.js responsive 需要定位上下文 */
min-height: 0; /* grid 子元素必须,允许收缩 */
width: 100%;
overflow: hidden;
}
.slide--chart .chart-container canvas {
display: block;
/* ❌ 禁止 width/height: 100% !important — 会与 Chart.js responsive 模式的
ResizeObserver 互相打架,形成 resize 反馈循环(表现为图表加载时抖动/扩张,
hover tooltip 时再次扩张)。Chart.js responsive:true 会自行读取父容器尺寸
并设置 canvas 的内在分辨率和显示尺寸,无需 CSS 干预。 */
}
.slide--chart .chart-container svg {
display: block;
width: 100%;
height: 100%;
object-fit: contain; /* SVG 保持比例居中 */
}
.slide--chart .chart-footnote {
font-size: var(--font-size-small);
color: var(--color-text-secondary);
text-align: center;
margin-top: var(--spacing-xs);
font-style: italic;
max-height: 48px; /* 脚注区硬上限 */
overflow: hidden;
}
/* 脚注为空时不占空间 */
.slide--chart .chart-footnote:empty {
display: none;
}
</style>

View File

@ -0,0 +1,90 @@
<!--
Layout: comparison
对比页 — 两个方案/产品/概念的优劣对比
Agent 填充两侧内容,可自由调整对比项数量
-->
<section class="slide slide--comparison" data-slide="{{N}}">
<h2 class="comp-title">{{页面标题}}</h2>
<div class="comp-grid">
<div class="comp-side comp-side--a">
<h3 class="comp-heading">{{方案A名称}}</h3>
<ul class="comp-list">
<li>{{对比项}}</li>
<li>{{对比项}}</li>
<li>{{对比项}}</li>
</ul>
</div>
<div class="comp-vs">VS</div>
<div class="comp-side comp-side--b">
<h3 class="comp-heading">{{方案B名称}}</h3>
<ul class="comp-list">
<li>{{对比项}}</li>
<li>{{对比项}}</li>
<li>{{对比项}}</li>
</ul>
</div>
</div>
</section>
<style>
.slide--comparison {
display: grid;
grid-template-rows: auto 1fr;
align-items: start;
}
.slide--comparison .comp-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-md);
text-align: center;
}
.slide--comparison .comp-grid {
display: grid;
grid-template-columns: 1fr auto 1fr; /* 左 | VS | 右,grid 保证两侧等高 */
gap: var(--spacing-md);
min-height: 0;
overflow: hidden;
}
.slide--comparison .comp-side {
background: var(--color-surface);
border-radius: var(--border-radius);
padding: var(--spacing-md);
box-shadow: var(--shadow-sm);
border: 1px solid var(--color-border);
}
.slide--comparison .comp-side--a {
border-top: 3px solid var(--color-primary);
}
.slide--comparison .comp-side--b {
border-top: 3px solid var(--color-accent);
}
.slide--comparison .comp-heading {
font-size: var(--font-size-subheading);
font-weight: 700;
margin-bottom: var(--spacing-md);
text-align: center;
}
.slide--comparison .comp-side--a .comp-heading { color: var(--color-primary); }
.slide--comparison .comp-side--b .comp-heading { color: var(--color-accent); }
.slide--comparison .comp-list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.slide--comparison .comp-list li {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-body);
color: var(--color-text-secondary);
border-bottom: 1px solid var(--color-border);
}
.slide--comparison .comp-list li:last-child { border-bottom: none; }
.slide--comparison .comp-vs {
display: flex;
align-items: center;
font-size: var(--font-size-subheading);
font-weight: 800;
color: var(--color-text-secondary);
opacity: 0.3;
}
</style>

View File

@ -0,0 +1,79 @@
<!--
Layout: content
标准内容页 — 最通用的布局,标题 + 要点列表
Agent 替换内容,可自由调整要点数量(3-4 个带描述为佳,最多 5 个纯标题条目)
⚠️ 溢出红线:content-list 受 grid 1fr 约束,4+ 条带完整描述极易溢出。
超出时:缩短描述、减少条目数、或拆分为多页。
禁止在 content-list 外额外包裹容器。内容直接落在页面背景上。
-->
<section class="slide slide--content" data-slide="{{N}}">
<h2 class="content-title">{{页面标题}}</h2>
<ul class="content-list">
<li>
<strong>{{要点标题}}</strong>
<span>{{要点说明}}</span>
</li>
<li>
<strong>{{要点标题}}</strong>
<span>{{要点说明}}</span>
</li>
<li>
<strong>{{要点标题}}</strong>
<span>{{要点说明}}</span>
</li>
<!-- Agent 根据内容增减 li -->
</ul>
</section>
<style>
.slide--content {
display: grid;
grid-template-rows: auto 1fr;
align-items: start;
padding-top: var(--spacing-xl);
}
.slide--content .content-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-md);
}
.slide--content .content-list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
min-height: 0; /* grid 子元素必须,允许收缩 */
overflow: hidden; /* 安全兜底:超出时隐藏而非撑破 */
}
.slide--content .content-list li {
display: flex;
flex-direction: column;
gap: 4px;
padding: var(--spacing-sm) var(--spacing-md);
border-left: 3px solid var(--color-primary);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
/* 不加 background/shadow — 内容直接落在页面背景上 */
}
.slide--content .content-list li strong {
font-size: var(--font-size-body);
color: var(--color-text);
}
.slide--content .content-list li span {
font-size: var(--font-size-small);
color: var(--color-text-secondary);
}
/* ── 5+ 条目自动收紧(:has() 防御性布局)── */
.slide--content .content-list:has(> li:nth-child(5)) {
gap: var(--spacing-xs); /* 间距收紧 */
}
.slide--content .content-list:has(> li:nth-child(5)) li {
padding: var(--spacing-xs) var(--spacing-md); /* 条目内边距缩小 */
}
.slide--content .content-list:has(> li:nth-child(6)) {
font-size: 0.9em; /* 6+ 条目字号缩小 */
}
.slide--content .content-list:has(> li:nth-child(7)) {
font-size: 0.82em; /* 7+ 条目进一步缩小 */
}
</style>

View File

@ -0,0 +1,67 @@
<!--
Layout: ending
结尾页 — 致谢/CTA/联系方式
Agent 替换 {{...}} 占位符。
ending-contact 整块为可选:仅当用户明确提供了联系方式时才保留,否则整个 div.ending-contact 删除。
禁止自行编造或从其他项目推测联系方式/GitHub 地址等信息。
-->
<section class="slide slide--ending" data-slide="{{N}}">
<div class="ending-content">
<h2 class="ending-title">{{感谢语,如"谢谢"}}</h2>
<p class="ending-subtitle">{{副文案,如"期待与您合作"}}</p>
<!-- 可选:仅当用户提供了联系方式时保留 -->
<div class="ending-contact">
<div class="contact-item">
<span class="contact-label">邮箱</span>
<span class="contact-value">{{email}}</span>
</div>
<div class="contact-item">
<span class="contact-label">网站</span>
<span class="contact-value">{{website}}</span>
</div>
</div>
</div>
</section>
<style>
.slide--ending {
justify-content: center;
align-items: center;
text-align: center;
}
.slide--ending .ending-content {
max-width: 88%; /* 放宽:与 title 页对齐,避免结尾标题换行 */
}
.slide--ending .ending-title {
font-size: var(--font-size-title);
font-weight: 800;
margin-bottom: var(--spacing-sm);
}
.slide--ending .ending-subtitle {
font-size: var(--font-size-subheading);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-lg);
}
.slide--ending .ending-contact {
display: flex;
justify-content: center;
gap: var(--spacing-lg);
flex-wrap: wrap;
}
.slide--ending .contact-item {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.slide--ending .contact-label {
font-size: var(--font-size-small);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.slide--ending .contact-value {
font-size: var(--font-size-body);
color: var(--color-primary);
font-weight: 500;
}
</style>

View File

@ -0,0 +1,61 @@
<!--
Layout: image
图片页 — 图片为主体,配可选标题和说明
Agent 填充图片 URL(外部链接 / base64 / AI 生成)和文字
-->
<section class="slide slide--image" data-slide="{{N}}">
<div class="image-layout">
<div class="image-main">
<img src="{{图片URL或base64}}" alt="{{图片描述}}">
</div>
<div class="image-caption">
<h3>{{图片标题(可选)}}</h3>
<p>{{图片说明(可选)}}</p>
</div>
</div>
</section>
<style>
.slide--image {
padding: var(--spacing-md);
justify-content: center;
}
.slide--image .image-layout {
display: flex;
gap: var(--spacing-lg);
align-items: center;
width: 100%;
height: 100%;
}
.slide--image .image-main {
flex: 2;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: var(--border-radius);
}
.slide--image .image-main img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
}
.slide--image .image-caption {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--spacing-sm);
}
.slide--image .image-caption h3 {
font-size: var(--font-size-subheading);
font-weight: 600;
}
.slide--image .image-caption p {
font-size: var(--font-size-body);
color: var(--color-text-secondary);
line-height: 1.7;
}
</style>

View File

@ -0,0 +1,136 @@
<!--
Layout: pyramid
层级/金字塔页 — 3-5 层递进/金字塔/漏斗结构
Agent 根据层数增减 pyramid-level,每层包含标题和可选描述。
梯形通过 clip-path 实现,各层间无间距,构成连续的金字塔轮廓。
🔴 适用性判断(必须在选择此布局前确认):
pyramid 仅适用于层间有明确的"包含/递进/从属"关系的内容,例如:
✅ 马斯洛需求层次(生理→安全→社交→尊重→自我实现)
✅ 组织架构层级(董事会→管理层→执行层)
✅ 数据漏斗(访客→注册→付费→续费)
❌ 以下内容禁止使用 pyramid,应选 cards 或 comparison:
❌ 多种平级选择/模式(如"容器沙箱 vs 云桌面 vs 本地运行")
❌ 多种方案的优劣权衡(如"安全性 vs 能力"的 tradeoff)
❌ 并列的分类/类型(如"三种部署模式"、"三种执行环境")
判断口诀:如果把顶层和底层互换位置,内容语义是否被破坏?
- 被破坏 → 有层级关系 → 可用 pyramid
- 不被破坏 → 是平级选择 → 用 cards 或 comparison
-->
<section class="slide slide--pyramid" data-slide="{{N}}">
<h2 class="pyramid-title">{{页面标题}}</h2>
<div class="pyramid-container">
<div class="pyramid-level pyramid-level--1">
<span class="pyramid-label">{{顶层标题}}</span>
<span class="pyramid-desc">{{可选:顶层简短描述}}</span>
</div>
<div class="pyramid-level pyramid-level--2">
<span class="pyramid-label">{{第二层标题}}</span>
<span class="pyramid-desc">{{可选:第二层简短描述}}</span>
</div>
<div class="pyramid-level pyramid-level--3">
<span class="pyramid-label">{{第三层标题}}</span>
<span class="pyramid-desc">{{可选:第三层简短描述}}</span>
</div>
<div class="pyramid-level pyramid-level--4">
<span class="pyramid-label">{{底层标题}}</span>
<span class="pyramid-desc">{{可选:底层简短描述}}</span>
</div>
<!-- Agent 根据实际层数增减 pyramid-level(3-5 层) -->
</div>
</section>
<style>
.slide--pyramid {
justify-content: flex-start;
align-items: center; /* 水平居中所有子元素 */
text-align: center; /* 兜底:确保所有文字居中 */
padding-top: var(--spacing-lg);
}
.slide--pyramid .pyramid-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-lg);
text-align: center;
width: 100%;
}
.slide--pyramid .pyramid-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 900px;
margin: 0 auto; /* 双保险居中:align-items + margin auto */
/* 层间无间距,靠 clip-path 的斜边形成视觉分隔 */
}
.slide--pyramid .pyramid-level {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
/* 水平 padding 基于 clip-path inset 动态计算,确保文字在梯形安全区内 */
padding: var(--spacing-sm) calc(var(--inset-top) + 5%);
min-height: 72px;
text-align: center;
position: relative;
/* 梯形:上窄下宽 */
clip-path: polygon(
var(--inset-top) 0%,
calc(100% - var(--inset-top)) 0%,
calc(100% - var(--inset-bottom)) 100%,
var(--inset-bottom) 100%
);
}
/* 层级宽度和梯形内缩 — 3 层 */
.slide--pyramid .pyramid-level--1 {
width: 100%;
--inset-top: 32%;
--inset-bottom: 22%;
background: var(--color-primary);
color: var(--color-text-on-primary);
}
.slide--pyramid .pyramid-level--2 {
width: 100%;
--inset-top: 22%;
--inset-bottom: 12%;
background: var(--color-primary);
color: var(--color-text-on-primary);
opacity: 0.82;
}
.slide--pyramid .pyramid-level--3 {
width: 100%;
--inset-top: 12%;
--inset-bottom: 4%;
background: var(--color-primary);
color: var(--color-text-on-primary);
opacity: 0.66;
}
.slide--pyramid .pyramid-level--4 {
width: 100%;
--inset-top: 4%;
--inset-bottom: 0%;
background: var(--color-primary);
color: var(--color-text-on-primary);
opacity: 0.52;
}
.slide--pyramid .pyramid-level--5 {
width: 100%;
--inset-top: 0%;
--inset-bottom: 0%;
background: var(--color-primary);
color: var(--color-text-on-primary);
opacity: 0.4;
}
.slide--pyramid .pyramid-label {
font-weight: 700;
font-size: var(--font-size-small); /* 缩小:顶层梯形窄,body 字号易溢出 */
line-height: 1.3;
}
.slide--pyramid .pyramid-desc {
font-size: var(--font-size-small);
opacity: 0.85;
margin-top: 2px;
font-weight: 400;
}
</style>

View File

@ -0,0 +1,50 @@
<!--
Layout: quote
引用页 — 金句/引用,大字居中
Agent 替换引用文字和作者
-->
<section class="slide slide--quote" data-slide="{{N}}">
<div class="quote-content">
<blockquote class="quote-text">
{{引用文字}}
</blockquote>
<cite class="quote-author">— {{作者/出处}}</cite>
</div>
</section>
<style>
.slide--quote {
justify-content: center;
align-items: center;
text-align: center;
}
.slide--quote .quote-content {
max-width: 75%;
}
.slide--quote .quote-text {
font-size: var(--font-size-heading);
font-weight: 500;
font-style: italic;
line-height: 1.5;
color: var(--color-text);
margin-bottom: var(--spacing-lg);
position: relative;
}
.slide--quote .quote-text::before {
content: '\201C';
font-size: 6rem;
color: var(--color-primary);
opacity: 0.15;
position: absolute;
top: -2.5rem;
left: -1.5rem;
line-height: 1;
font-style: normal;
}
.slide--quote .quote-author {
font-size: var(--font-size-body);
color: var(--color-text-secondary);
font-style: normal;
font-weight: 500;
}
</style>

View File

@ -0,0 +1,40 @@
<!--
Layout: section
章节过渡页 — 章节间转场,承上启下
Agent 替换 {{...}} 占位符
-->
<section class="slide slide--section" data-slide="{{N}}">
<div class="section-content">
<span class="section-number">{{章节编号,如 01}}</span>
<h2 class="section-title">{{章节标题}}</h2>
<p class="section-desc">{{简短描述(可选)}}</p>
</div>
</section>
<style>
.slide--section {
justify-content: center;
align-items: center; /* 居中:章节过渡页内容视觉居中更协调 */
}
.slide--section .section-content {
max-width: 70%;
}
.slide--section .section-number {
display: block;
font-size: var(--font-size-title);
font-weight: 800;
color: var(--color-primary);
opacity: 0.2;
margin-bottom: var(--spacing-sm);
line-height: 1;
}
.slide--section .section-title {
font-size: var(--font-size-heading);
font-weight: 700;
margin-bottom: var(--spacing-sm);
}
.slide--section .section-desc {
font-size: var(--font-size-body);
color: var(--color-text-secondary);
}
</style>

View File

@ -0,0 +1,96 @@
<!--
Layout: timeline
时间线/流程页 — 有序节点展示
Agent 根据实际节点数增减 timeline-item(3-6 个为佳,7 个在默认 padding 下可能超出可视区域被裁切)
-->
<section class="slide slide--timeline" data-slide="{{N}}">
<h2 class="timeline-title">{{页面标题}}</h2>
<div class="timeline-track">
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-body">
<h3 class="timeline-heading">{{节点标题,如年份/步骤名}}</h3>
<p class="timeline-desc">{{节点描述}}</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-body">
<h3 class="timeline-heading">{{节点标题}}</h3>
<p class="timeline-desc">{{节点描述}}</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-body">
<h3 class="timeline-heading">{{节点标题}}</h3>
<p class="timeline-desc">{{节点描述}}</p>
</div>
</div>
<!-- Agent 根据内容增减 timeline-item -->
</div>
</section>
<style>
.slide--timeline {
display: grid;
grid-template-rows: auto 1fr;
align-items: start;
}
.slide--timeline .timeline-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-md);
}
.slide--timeline .timeline-track {
display: flex;
gap: var(--spacing-md);
align-items: flex-start;
align-self: center;
position: relative;
min-height: 0;
padding-top: 4px; /* marker ring shadow 不被裁切 */
overflow: visible; /* 允许 marker 装饰元素超出 */
}
.slide--timeline .timeline-track::before {
content: '';
position: absolute;
top: 12px;
left: 0; right: 0;
height: 2px;
background: var(--color-border);
z-index: 0;
}
.slide--timeline .timeline-item {
flex: 1;
min-width: 150px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
z-index: 1;
}
.slide--timeline .timeline-marker {
width: 24px; height: 24px;
border-radius: 50%;
background: var(--color-primary);
border: 3px solid var(--color-bg);
box-shadow: 0 0 0 2px var(--color-primary);
margin-bottom: var(--spacing-sm);
flex-shrink: 0;
}
.slide--timeline .timeline-body {
padding: var(--spacing-sm);
}
.slide--timeline .timeline-heading {
font-size: var(--font-size-body);
font-weight: 700;
margin-bottom: 4px;
color: var(--color-primary);
}
.slide--timeline .timeline-desc {
font-size: var(--font-size-small);
color: var(--color-text-secondary);
line-height: 1.5;
}
</style>

View File

@ -0,0 +1,55 @@
<!--
Layout: title
标题页 — 首页,主标题 + 副标题 + 可选元信息
Agent 替换 {{...}} 占位符。
禁止在 .title-content 外层额外包裹带 border/shadow/background 的卡片容器。
标题页的内容直接落在 .slide 背景上,不需要"卡片"装饰。
🔴 元信息红线(title-meta 区域):
- 仅填写用户明确提供的作者/机构名称和日期
- 如果用户没有提供作者或日期 → 删除整个 .title-meta div,不要留空
- ❌ 禁止填入:素材来源("基于 N 份材料生成")、生成日期、文件名、
工作上下文、受众描述、写作策略等 Agent 工作信息
- 这些是 Agent 的内部上下文,不是给观众看的内容
-->
<section class="slide slide--title" data-slide="{{N}}">
<div class="title-content">
<h1 class="title-main">{{主标题}}</h1>
<p class="title-sub">{{副标题}}</p>
<!-- ⚠️ 如果用户未提供作者/日期,删除整个 title-meta div -->
<div class="title-meta">
<span class="title-author">{{作者/机构,用户未提供则删除整个 title-meta}}</span>
<span class="title-date">{{日期,用户未提供则删除整个 title-meta}}</span>
</div>
</div>
</section>
<style>
.slide--title {
justify-content: center;
align-items: center;
text-align: center;
}
.slide--title .title-content {
max-width: 92%; /* 放宽:中文标题在 80% 宽度下容易换行 */
}
.slide--title .title-main {
font-size: var(--font-size-title);
font-weight: 800;
margin-bottom: var(--spacing-md);
line-height: 1.15;
}
.slide--title .title-sub {
font-size: var(--font-size-subheading);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-lg);
font-weight: 400;
}
.slide--title .title-meta {
display: flex;
justify-content: center;
gap: var(--spacing-md);
font-size: var(--font-size-small);
color: var(--color-text-secondary);
}
</style>

View File

@ -0,0 +1,57 @@
<!--
Layout: toc
目录页 — 全文章节概览
Agent 根据实际章节数增减 toc-item,通常 3-6 个
-->
<section class="slide slide--toc" data-slide="{{N}}">
<h2 class="toc-heading">{{目录标题,如"目录"或"内容概览"}}</h2>
<div class="toc-list">
<div class="toc-item">
<span class="toc-number">01</span>
<span class="toc-text">{{章节标题}}</span>
</div>
<div class="toc-item">
<span class="toc-number">02</span>
<span class="toc-text">{{章节标题}}</span>
</div>
<div class="toc-item">
<span class="toc-number">03</span>
<span class="toc-text">{{章节标题}}</span>
</div>
<!-- Agent 根据实际章节数增减 -->
</div>
</section>
<style>
.slide--toc {
justify-content: center;
}
.slide--toc .toc-heading {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-lg);
}
.slide--toc .toc-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.slide--toc .toc-item {
display: flex;
align-items: baseline;
gap: var(--spacing-md);
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--color-border);
transition: background var(--transition-speed) var(--transition-easing);
}
.slide--toc .toc-number {
font-size: var(--font-size-subheading);
font-weight: 700;
color: var(--color-primary);
min-width: 2.5em;
opacity: 0.5;
}
.slide--toc .toc-text {
font-size: var(--font-size-subheading);
font-weight: 500;
}
</style>

View File

@ -0,0 +1,56 @@
<!--
Layout: two-column
双栏页 — 对比/并列内容
Agent 填充左右两栏,可调整比例(默认 1:1)
-->
<section class="slide slide--two-column" data-slide="{{N}}">
<h2 class="twocol-title">{{页面标题}}</h2>
<div class="twocol-grid">
<div class="twocol-left">
<h3>{{左栏标题}}</h3>
<p>{{左栏内容}}</p>
</div>
<div class="twocol-right">
<h3>{{右栏标题}}</h3>
<p>{{右栏内容}}</p>
</div>
</div>
</section>
<style>
.slide--two-column {
display: grid;
grid-template-rows: auto 1fr;
align-items: start;
}
.slide--two-column .twocol-title {
font-size: var(--font-size-heading);
margin-bottom: var(--spacing-md);
}
.slide--two-column .twocol-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
align-items: start;
align-self: center;
min-height: 0;
overflow: hidden;
}
.slide--two-column .twocol-left,
.slide--two-column .twocol-right {
padding: var(--spacing-md);
background: var(--color-surface);
border-radius: var(--border-radius);
box-shadow: var(--shadow-sm);
}
.slide--two-column .twocol-grid h3 {
font-size: var(--font-size-subheading);
margin-bottom: var(--spacing-sm);
color: var(--color-primary);
}
.slide--two-column .twocol-grid p {
font-size: var(--font-size-body);
color: var(--color-text-secondary);
line-height: 1.7;
}
</style>

View File

@ -0,0 +1,862 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>选择 Slide 主题</title>
<style>
/* ============================================================
全局
============================================================ */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: #0e0e10;
color: #e4e4e7;
min-height: 100vh;
padding: 40px 32px 120px;
}
/* ============================================================
标题区域
============================================================ */
.picker-header {
text-align: center;
margin-bottom: 48px;
}
.picker-header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.picker-header p {
font-size: 15px;
color: #a1a1aa;
}
/* ============================================================
风格行:每行一个风格,左 light 右 dark
============================================================ */
.style-row {
max-width: 1200px;
margin: 0 auto 40px;
}
.style-row-label {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #71717a;
margin-bottom: 12px;
padding-left: 4px;
}
.style-row-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
/* ============================================================
主题卡片
============================================================ */
.theme-card {
border-radius: 12px;
overflow: hidden;
cursor: pointer;
border: 3px solid transparent;
transition: border-color 0.2s, transform 0.15s, box-shadow 0.2s;
position: relative;
}
.theme-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.theme-card.selected {
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99,102,241,0.3), 0 8px 32px rgba(0,0,0,0.4);
}
.theme-card.selected::after {
content: '✓';
position: absolute;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
background: #6366f1;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
z-index: 10;
}
/* ---- mini slide 预览区域 ---- */
.mini-slide {
aspect-ratio: 16 / 9;
padding: 28px 32px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.mini-top h2 {
margin-bottom: 4px;
line-height: 1.2;
}
.mini-top p {
line-height: 1.5;
}
/* ---- mini 卡片组 ---- */
.mini-cards {
display: flex;
gap: 8px;
margin-top: 10px;
}
.mini-card-item {
flex: 1;
padding: 10px 8px;
border-radius: inherit;
text-align: center;
}
.mini-card-item .card-num {
font-weight: 700;
margin-bottom: 2px;
}
.mini-card-item .card-label {
font-size: 0.65em;
}
/* ---- mini 柱状图 ---- */
.mini-chart {
display: flex;
align-items: flex-end;
gap: 6px;
height: 40px;
margin-top: 8px;
}
.mini-bar {
flex: 1;
border-radius: 3px 3px 0 0;
min-height: 4px;
}
/* ---- 卡片底部信息 ---- */
.card-footer {
padding: 10px 16px;
background: rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: space-between;
}
.card-footer .theme-name {
font-size: 14px;
font-weight: 600;
color: #e4e4e7;
}
.card-footer .theme-desc {
font-size: 12px;
color: #a1a1aa;
}
/* ============================================================
底部确认栏
============================================================ */
.confirm-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 32px;
background: rgba(14,14,16,0.95);
backdrop-filter: blur(12px);
border-top: 1px solid #27272a;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
z-index: 100;
}
.confirm-bar .selection-text {
font-size: 14px;
color: #a1a1aa;
}
.confirm-bar .selection-text strong {
color: #e4e4e7;
}
.confirm-btn {
padding: 10px 28px;
font-size: 15px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
font-family: inherit;
}
.confirm-btn:active { transform: scale(0.97); }
.confirm-btn-primary {
background: #6366f1;
color: #fff;
}
.confirm-btn-primary:hover { background: #4f46e5; }
.confirm-btn-primary:disabled {
background: #3f3f46;
color: #71717a;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="picker-header">
<h1>选择 Slide 主题风格</h1>
<p>点击卡片选择,然后点击底部按钮确认</p>
</div>
<!-- ============================================================
Pure — 极致简约 / 精密排版
============================================================ -->
<div class="style-row">
<div class="style-row-label">Pure · 极致简约</div>
<div class="style-row-grid">
<!-- pure-light -->
<div class="theme-card" data-theme="pure-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#ffffff; color:#1d1d1f;">
<div class="mini-top">
<h2 style="font-family:'SF Pro Display',system-ui,sans-serif; font-size:20px; color:#1d1d1f;">产品发布会</h2>
<p style="font-size:11px; color:#86868b;">极致简约 · 精密排版 · 系统蓝强调</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#f5f5f7; border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#0071e3;">98%</div>
<div class="card-label" style="color:#86868b;">用户满意度</div>
</div>
<div class="mini-card-item" style="background:#f5f5f7; border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#0071e3;">2.4M</div>
<div class="card-label" style="color:#86868b;">活跃用户</div>
</div>
<div class="mini-card-item" style="background:#f5f5f7; border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#0071e3;">4.9★</div>
<div class="card-label" style="color:#86868b;">应用评分</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#0071e3; height:55%;"></div>
<div class="mini-bar" style="background:#0071e3; opacity:0.7; height:80%;"></div>
<div class="mini-bar" style="background:#0071e3; height:65%;"></div>
<div class="mini-bar" style="background:#0071e3; opacity:0.7; height:100%;"></div>
<div class="mini-bar" style="background:#0071e3; height:75%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Pure Light</span>
<span class="theme-desc">白底 · SF Pro · 圆角 12px</span>
</div>
</div>
<!-- pure-dark -->
<div class="theme-card" data-theme="pure-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#000000; color:#f5f5f7;">
<div class="mini-top">
<h2 style="font-family:'SF Pro Display',system-ui,sans-serif; font-size:20px; color:#f5f5f7;">产品发布会</h2>
<p style="font-size:11px; color:#a1a1a6;">极致简约 · 纯黑背景 · 苹果蓝强调</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#1d1d1f; border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#2997ff;">98%</div>
<div class="card-label" style="color:#a1a1a6;">用户满意度</div>
</div>
<div class="mini-card-item" style="background:#1d1d1f; border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#2997ff;">2.4M</div>
<div class="card-label" style="color:#a1a1a6;">活跃用户</div>
</div>
<div class="mini-card-item" style="background:#1d1d1f; border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#2997ff;">4.9★</div>
<div class="card-label" style="color:#a1a1a6;">应用评分</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#2997ff; height:55%;"></div>
<div class="mini-bar" style="background:#2997ff; opacity:0.7; height:80%;"></div>
<div class="mini-bar" style="background:#2997ff; height:65%;"></div>
<div class="mini-bar" style="background:#2997ff; opacity:0.7; height:100%;"></div>
<div class="mini-bar" style="background:#2997ff; height:75%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Pure Dark</span>
<span class="theme-desc">纯黑 · SF Pro · 圆角 12px</span>
</div>
</div>
</div>
</div>
<!-- ============================================================
Warm — 暖调学院 / 衬线标题
============================================================ -->
<div class="style-row">
<div class="style-row-label">Warm · 暖调学院</div>
<div class="style-row-grid">
<!-- warm-light -->
<div class="theme-card" data-theme="warm-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#faf9f5; color:#141413;">
<div class="mini-top">
<h2 style="font-family:Georgia,'Noto Serif SC',serif; font-size:20px; color:#141413;">AI 前沿研究</h2>
<p style="font-size:11px; color:#615f59;">衬线标题 · 奶油白底 · 琥珀强调</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#f0efe8; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#c4642d;">1.2B</div>
<div class="card-label" style="color:#615f59;">参数规模</div>
</div>
<div class="mini-card-item" style="background:#f0efe8; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#c4642d;">97.3</div>
<div class="card-label" style="color:#615f59;">准确率</div>
</div>
<div class="mini-card-item" style="background:#f0efe8; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#c4642d;">3.8×</div>
<div class="card-label" style="color:#615f59;">性能提升</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#c4642d; height:40%;"></div>
<div class="mini-bar" style="background:#c4642d; opacity:0.7; height:60%;"></div>
<div class="mini-bar" style="background:#c4642d; height:85%;"></div>
<div class="mini-bar" style="background:#c4642d; opacity:0.7; height:70%;"></div>
<div class="mini-bar" style="background:#c4642d; height:100%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Warm Light</span>
<span class="theme-desc">奶油白 · 衬线标题 · 圆角 8px</span>
</div>
</div>
<!-- warm-dark -->
<div class="theme-card" data-theme="warm-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#1a1915; color:#f0efe8;">
<div class="mini-top">
<h2 style="font-family:Georgia,'Noto Serif SC',serif; font-size:20px; color:#f0efe8;">AI 前沿研究</h2>
<p style="font-size:11px; color:#9b9891;">衬线标题 · 橄榄深底 · 琥珀强调</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#2a2820; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#d4845a;">1.2B</div>
<div class="card-label" style="color:#9b9891;">参数规模</div>
</div>
<div class="mini-card-item" style="background:#2a2820; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#d4845a;">97.3</div>
<div class="card-label" style="color:#9b9891;">准确率</div>
</div>
<div class="mini-card-item" style="background:#2a2820; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#d4845a;">3.8×</div>
<div class="card-label" style="color:#9b9891;">性能提升</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#d4845a; height:40%;"></div>
<div class="mini-bar" style="background:#d4845a; opacity:0.7; height:60%;"></div>
<div class="mini-bar" style="background:#d4845a; height:85%;"></div>
<div class="mini-bar" style="background:#d4845a; opacity:0.7; height:70%;"></div>
<div class="mini-bar" style="background:#d4845a; height:100%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Warm Dark</span>
<span class="theme-desc">橄榄深底 · 衬线标题 · 圆角 8px</span>
</div>
</div>
</div>
</div>
<!-- ============================================================
Cyber — 赛博蓝紫
============================================================ -->
<div class="style-row">
<div class="style-row-label">Cyber · 赛博蓝紫</div>
<div class="style-row-grid">
<!-- cyber-light -->
<div class="theme-card" data-theme="cyber-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#ffffff; color:#0b1120;">
<div class="mini-top">
<h2 style="font-family:Inter,system-ui,sans-serif; font-size:20px; color:#0b1120;">SaaS 产品路演</h2>
<p style="font-size:11px; color:#5e6a81;">现代无衬线 · 白底 · 靛紫强调</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#f5f6fa; border-radius:10px;">
<div class="card-num" style="font-size:16px; color:#6366f1;">$12M</div>
<div class="card-label" style="color:#5e6a81;">ARR</div>
</div>
<div class="mini-card-item" style="background:#f5f6fa; border-radius:10px;">
<div class="card-num" style="font-size:16px; color:#6366f1;">340%</div>
<div class="card-label" style="color:#5e6a81;">YoY 增长</div>
</div>
<div class="mini-card-item" style="background:#f5f6fa; border-radius:10px;">
<div class="card-num" style="font-size:16px; color:#6366f1;">99.9</div>
<div class="card-label" style="color:#5e6a81;">SLA %</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#6366f1; height:30%;"></div>
<div class="mini-bar" style="background:#6366f1; opacity:0.7; height:50%;"></div>
<div class="mini-bar" style="background:#6366f1; height:70%;"></div>
<div class="mini-bar" style="background:#6366f1; opacity:0.7; height:85%;"></div>
<div class="mini-bar" style="background:#6366f1; height:100%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Cyber Light</span>
<span class="theme-desc">白底 · Inter · 圆角 10px</span>
</div>
</div>
<!-- cyber-dark -->
<div class="theme-card" data-theme="cyber-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#0b0f19; color:#f0f4ff;">
<div class="mini-top">
<h2 style="font-family:Inter,system-ui,sans-serif; font-size:20px; color:#f0f4ff;">SaaS 产品路演</h2>
<p style="font-size:11px; color:#8b95ad;">现代无衬线 · 深蓝黑底 · 霓虹紫</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#1a2035; border-radius:10px;">
<div class="card-num" style="font-size:16px; color:#818cf8;">$12M</div>
<div class="card-label" style="color:#8b95ad;">ARR</div>
</div>
<div class="mini-card-item" style="background:#1a2035; border-radius:10px;">
<div class="card-num" style="font-size:16px; color:#818cf8;">340%</div>
<div class="card-label" style="color:#8b95ad;">YoY 增长</div>
</div>
<div class="mini-card-item" style="background:#1a2035; border-radius:10px;">
<div class="card-num" style="font-size:16px; color:#818cf8;">99.9</div>
<div class="card-label" style="color:#8b95ad;">SLA %</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#818cf8; height:30%;"></div>
<div class="mini-bar" style="background:#818cf8; opacity:0.7; height:50%;"></div>
<div class="mini-bar" style="background:#818cf8; height:70%;"></div>
<div class="mini-bar" style="background:#818cf8; opacity:0.7; height:85%;"></div>
<div class="mini-bar" style="background:#818cf8; height:100%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Cyber Dark</span>
<span class="theme-desc">深蓝黑 · Inter · 圆角 10px</span>
</div>
</div>
</div>
</div>
<!-- ============================================================
Data — 数据可视化
============================================================ -->
<div class="style-row">
<div class="style-row-label">Data · 数据可视化</div>
<div class="style-row-grid">
<!-- data-light -->
<div class="theme-card" data-theme="data-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#ffffff; color:#0a2540;">
<div class="mini-top">
<h2 style="font-family:Inter,system-ui,sans-serif; font-size:20px; color:#0a2540;">数据分析报告</h2>
<p style="font-size:11px; color:#546b81;">紧凑排版 · 白底 · 青绿强调</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#f6f8fa; border-radius:6px;">
<div class="card-num" style="font-size:16px; color:#0dab76;">+27%</div>
<div class="card-label" style="color:#546b81;">转化率</div>
</div>
<div class="mini-card-item" style="background:#f6f8fa; border-radius:6px;">
<div class="card-num" style="font-size:16px; color:#0dab76;">58ms</div>
<div class="card-label" style="color:#546b81;">P99 延迟</div>
</div>
<div class="mini-card-item" style="background:#f6f8fa; border-radius:6px;">
<div class="card-num" style="font-size:16px; color:#0dab76;">3.2M</div>
<div class="card-label" style="color:#546b81;">日请求量</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#0dab76; height:60%;"></div>
<div class="mini-bar" style="background:#0dab76; opacity:0.7; height:45%;"></div>
<div class="mini-bar" style="background:#0dab76; height:80%;"></div>
<div class="mini-bar" style="background:#0dab76; opacity:0.7; height:100%;"></div>
<div class="mini-bar" style="background:#0dab76; height:70%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Data Light</span>
<span class="theme-desc">白底 · IBM Plex Mono · 圆角 6px</span>
</div>
</div>
<!-- data-dark -->
<div class="theme-card" data-theme="data-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#0d1117; color:#e6edf3;">
<div class="mini-top">
<h2 style="font-family:Inter,system-ui,sans-serif; font-size:20px; color:#e6edf3;">数据分析报告</h2>
<p style="font-size:11px; color:#7d8590;">仪表盘风格 · GitHub 深底 · 青绿强调</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#1c2129; border-radius:6px;">
<div class="card-num" style="font-size:16px; color:#10d48e;">+27%</div>
<div class="card-label" style="color:#7d8590;">转化率</div>
</div>
<div class="mini-card-item" style="background:#1c2129; border-radius:6px;">
<div class="card-num" style="font-size:16px; color:#10d48e;">58ms</div>
<div class="card-label" style="color:#7d8590;">P99 延迟</div>
</div>
<div class="mini-card-item" style="background:#1c2129; border-radius:6px;">
<div class="card-num" style="font-size:16px; color:#10d48e;">3.2M</div>
<div class="card-label" style="color:#7d8590;">日请求量</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#10d48e; height:60%;"></div>
<div class="mini-bar" style="background:#10d48e; opacity:0.7; height:45%;"></div>
<div class="mini-bar" style="background:#10d48e; height:80%;"></div>
<div class="mini-bar" style="background:#10d48e; opacity:0.7; height:100%;"></div>
<div class="mini-bar" style="background:#10d48e; height:70%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Data Dark</span>
<span class="theme-desc">深蓝黑 · IBM Plex Mono · 圆角 6px</span>
</div>
</div>
</div>
</div>
<!-- ============================================================
Azure — 蓝调科技 / 企业
============================================================ -->
<div class="style-row">
<div class="style-row-label">Azure · 蓝调科技</div>
<div class="style-row-grid">
<!-- azure-light -->
<div class="theme-card" data-theme="azure-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#ffffff; color:#1a1a1a;">
<div class="mini-top">
<h2 style="font-family:'PingFang SC','Microsoft YaHei',sans-serif; font-size:20px; color:#1a1a1a;">企业年度汇报</h2>
<p style="font-size:11px; color:#666666;">中文优化 · 白底 · 经典蓝</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#f5f6f8; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#0052D9;">¥8.6亿</div>
<div class="card-label" style="color:#666666;">年营收</div>
</div>
<div class="mini-card-item" style="background:#f5f6f8; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#0052D9;">1,200+</div>
<div class="card-label" style="color:#666666;">客户数</div>
</div>
<div class="mini-card-item" style="background:#f5f6f8; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#0052D9;">46%</div>
<div class="card-label" style="color:#666666;">市场份额</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#0052D9; height:50%;"></div>
<div class="mini-bar" style="background:#0052D9; opacity:0.7; height:65%;"></div>
<div class="mini-bar" style="background:#0052D9; height:80%;"></div>
<div class="mini-bar" style="background:#0052D9; opacity:0.7; height:90%;"></div>
<div class="mini-bar" style="background:#0052D9; height:100%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Azure Light</span>
<span class="theme-desc">白底 · PingFang/雅黑 · 圆角 8px</span>
</div>
</div>
<!-- azure-dark -->
<div class="theme-card" data-theme="azure-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#141414; color:#f0f2f5;">
<div class="mini-top">
<h2 style="font-family:'PingFang SC','Microsoft YaHei',sans-serif; font-size:20px; color:#f0f2f5;">企业年度汇报</h2>
<p style="font-size:11px; color:#8c8c8c;">中文优化 · 深灰底 · 明亮蓝</p>
</div>
<div class="mini-cards">
<div class="mini-card-item" style="background:#262626; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#4787F0;">¥8.6亿</div>
<div class="card-label" style="color:#8c8c8c;">年营收</div>
</div>
<div class="mini-card-item" style="background:#262626; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#4787F0;">1,200+</div>
<div class="card-label" style="color:#8c8c8c;">客户数</div>
</div>
<div class="mini-card-item" style="background:#262626; border-radius:8px;">
<div class="card-num" style="font-size:16px; color:#4787F0;">46%</div>
<div class="card-label" style="color:#8c8c8c;">市场份额</div>
</div>
</div>
<div class="mini-chart">
<div class="mini-bar" style="background:#4787F0; height:50%;"></div>
<div class="mini-bar" style="background:#4787F0; opacity:0.7; height:65%;"></div>
<div class="mini-bar" style="background:#4787F0; height:80%;"></div>
<div class="mini-bar" style="background:#4787F0; opacity:0.7; height:90%;"></div>
<div class="mini-bar" style="background:#4787F0; height:100%;"></div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Azure Dark</span>
<span class="theme-desc">深灰黑 · PingFang/雅黑 · 圆角 8px</span>
</div>
</div>
</div>
</div>
<div class="style-row">
<div class="style-row-label">Glass · 液态玻璃</div>
<div class="style-row-grid">
<!-- glass-light -->
<div class="theme-card" data-theme="glass-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#f2f2f7; color:#1c1c1e; position:relative; overflow:hidden;">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 15% 45%,rgba(175,82,222,0.12),transparent 70%),radial-gradient(ellipse at 85% 25%,rgba(0,122,255,0.10),transparent 70%);"></div>
<div class="mini-top" style="position:relative;">
<h2 style="font-size:18px; color:#1c1c1e;">液态玻璃效果</h2>
<p style="font-size:11px; color:#636366;">多层半透明 · 色调偏移 · 液态光感</p>
</div>
<div class="mini-cards" style="position:relative;">
<div class="mini-card-item" style="background:rgba(255,255,255,0.45); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.6); border-radius:16px;">
<div class="card-num" style="font-size:16px; color:#007AFF;">94%</div>
<div class="card-label" style="color:#636366;">透明度</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.45); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.6); border-radius:16px;">
<div class="card-num" style="font-size:16px; color:#AF52DE;">24px</div>
<div class="card-label" style="color:#636366;">模糊</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.45); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.6); border-radius:16px;">
<div class="card-num" style="font-size:16px; color:#007AFF;">3层</div>
<div class="card-label" style="color:#636366;">折射</div>
</div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Glass Light</span>
<span class="theme-desc">液态玻璃 · 蓝紫光斑 · 圆角 16px</span>
</div>
</div>
<!-- glass-dark -->
<div class="theme-card" data-theme="glass-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#1c1c1e; color:#f5f5f7; position:relative; overflow:hidden;">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 15% 45%,rgba(191,90,242,0.15),transparent 70%),radial-gradient(ellipse at 80% 20%,rgba(10,132,255,0.12),transparent 70%);"></div>
<div class="mini-top" style="position:relative;">
<h2 style="font-size:18px; color:#f5f5f7;">液态玻璃效果</h2>
<p style="font-size:11px; color:#98989d;">深色 · 冷调光斑 · 液态光感</p>
</div>
<div class="mini-cards" style="position:relative;">
<div class="mini-card-item" style="background:rgba(255,255,255,0.08); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.12); border-radius:16px;">
<div class="card-num" style="font-size:16px; color:#0A84FF;">94%</div>
<div class="card-label" style="color:#98989d;">透明度</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.08); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.12); border-radius:16px;">
<div class="card-num" style="font-size:16px; color:#BF5AF2;">24px</div>
<div class="card-label" style="color:#98989d;">模糊</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.08); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.12); border-radius:16px;">
<div class="card-num" style="font-size:16px; color:#0A84FF;">3层</div>
<div class="card-label" style="color:#98989d;">折射</div>
</div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Glass Dark</span>
<span class="theme-desc">液态玻璃深色 · 冷调蓝紫 · 圆角 16px</span>
</div>
</div>
</div>
</div>
<div class="style-row">
<div class="style-row-label">Frost · 磨砂玻璃</div>
<div class="style-row-grid">
<!-- frost-light -->
<div class="theme-card" data-theme="frost-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#eef2f6; color:#0f172a; position:relative; overflow:hidden;">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 30% 50%,rgba(59,130,246,0.08),transparent 70%),radial-gradient(ellipse at 75% 35%,rgba(6,182,212,0.06),transparent 70%);"></div>
<div class="mini-top" style="position:relative;">
<h2 style="font-size:18px; color:#0f172a;">磨砂玻璃面板</h2>
<p style="font-size:11px; color:#64748b;">经典 glassmorphism · 强模糊</p>
</div>
<div class="mini-cards" style="position:relative;">
<div class="mini-card-item" style="background:rgba(255,255,255,0.55); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.7); border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#3b82f6;">12K</div>
<div class="card-label" style="color:#64748b;">用户</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.55); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.7); border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#06b6d4;">98%</div>
<div class="card-label" style="color:#64748b;">在线率</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.55); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.7); border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#3b82f6;">4.9★</div>
<div class="card-label" style="color:#64748b;">评分</div>
</div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Frost Light</span>
<span class="theme-desc">磨砂玻璃 · 蓝青氛围 · 圆角 12px</span>
</div>
</div>
<!-- frost-dark -->
<div class="theme-card" data-theme="frost-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#0f172a; color:#f1f5f9; position:relative; overflow:hidden;">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 25% 50%,rgba(96,165,250,0.10),transparent 70%),radial-gradient(ellipse at 80% 30%,rgba(34,211,238,0.07),transparent 70%);"></div>
<div class="mini-top" style="position:relative;">
<h2 style="font-size:18px; color:#f1f5f9;">磨砂玻璃面板</h2>
<p style="font-size:11px; color:#94a3b8;">深色 · 冷调科技 · 强模糊</p>
</div>
<div class="mini-cards" style="position:relative;">
<div class="mini-card-item" style="background:rgba(255,255,255,0.06); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.10); border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#60a5fa;">12K</div>
<div class="card-label" style="color:#94a3b8;">用户</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.06); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.10); border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#22d3ee;">98%</div>
<div class="card-label" style="color:#94a3b8;">在线率</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.06); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.10); border-radius:12px;">
<div class="card-num" style="font-size:16px; color:#60a5fa;">4.9★</div>
<div class="card-label" style="color:#94a3b8;">评分</div>
</div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Frost Dark</span>
<span class="theme-desc">磨砂玻璃深色 · 蓝青氛围 · 圆角 12px</span>
</div>
</div>
</div>
</div>
<div class="style-row">
<div class="style-row-label">Gradient · 渐变</div>
<div class="style-row-grid">
<!-- gradient-light -->
<div class="theme-card" data-theme="gradient-light" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#faf5ff; color:#1e1033; position:relative; overflow:hidden;">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 10% 30%,rgba(236,72,153,0.15),transparent 70%),radial-gradient(ellipse at 50% 80%,rgba(124,58,237,0.12),transparent 70%),radial-gradient(ellipse at 90% 20%,rgba(59,130,246,0.10),transparent 70%);"></div>
<div class="mini-top" style="position:relative;">
<h2 style="font-size:18px; color:#1e1033;">渐变视觉冲击</h2>
<p style="font-size:11px; color:#6b5e7b;">粉紫蓝渐变 · 大胆 · 高对比</p>
</div>
<div class="mini-cards" style="position:relative;">
<div class="mini-card-item" style="background:rgba(255,255,255,0.70); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); border:1px solid rgba(255,255,255,0.55); border-radius:14px;">
<div class="card-num" style="font-size:16px; color:#7c3aed;">$5M</div>
<div class="card-label" style="color:#6b5e7b;">融资</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.70); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); border:1px solid rgba(255,255,255,0.55); border-radius:14px;">
<div class="card-num" style="font-size:16px; color:#ec4899;">300%</div>
<div class="card-label" style="color:#6b5e7b;">增长</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.70); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); border:1px solid rgba(255,255,255,0.55); border-radius:14px;">
<div class="card-num" style="font-size:16px; color:#7c3aed;">50K</div>
<div class="card-label" style="color:#6b5e7b;">DAU</div>
</div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Gradient Light</span>
<span class="theme-desc">粉紫蓝渐变 · 高冲击力 · 圆角 14px</span>
</div>
</div>
<!-- gradient-dark -->
<div class="theme-card" data-theme="gradient-dark" onclick="selectTheme(this)">
<div class="mini-slide" style="background:#0c0118; color:#f5f0ff; position:relative; overflow:hidden;">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 10% 40%,rgba(244,114,182,0.18),transparent 70%),radial-gradient(ellipse at 55% 85%,rgba(167,139,250,0.15),transparent 70%),radial-gradient(ellipse at 90% 15%,rgba(56,189,248,0.10),transparent 70%);"></div>
<div class="mini-top" style="position:relative;">
<h2 style="font-size:18px; color:#f5f0ff;">渐变视觉冲击</h2>
<p style="font-size:11px; color:#a599bf;">深紫蓝渐变 · 霓虹 · 沉浸</p>
</div>
<div class="mini-cards" style="position:relative;">
<div class="mini-card-item" style="background:rgba(255,255,255,0.06); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); border:1px solid rgba(255,255,255,0.08); border-radius:14px;">
<div class="card-num" style="font-size:16px; color:#a78bfa;">$5M</div>
<div class="card-label" style="color:#a599bf;">融资</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.06); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); border:1px solid rgba(255,255,255,0.08); border-radius:14px;">
<div class="card-num" style="font-size:16px; color:#f472b6;">300%</div>
<div class="card-label" style="color:#a599bf;">增长</div>
</div>
<div class="mini-card-item" style="background:rgba(255,255,255,0.06); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); border:1px solid rgba(255,255,255,0.08); border-radius:14px;">
<div class="card-num" style="font-size:16px; color:#a78bfa;">50K</div>
<div class="card-label" style="color:#a599bf;">DAU</div>
</div>
</div>
</div>
<div class="card-footer">
<span class="theme-name">Gradient Dark</span>
<span class="theme-desc">深紫蓝渐变 · 霓虹色 · 圆角 14px</span>
</div>
</div>
</div>
</div>
<!-- ============================================================
底部确认栏
============================================================ -->
<div class="confirm-bar">
<span class="selection-text" id="selectionText">请点击上方卡片选择主题</span>
<button class="confirm-btn confirm-btn-primary" id="confirmBtn" disabled onclick="confirmSelection()">
确认主题并继续
</button>
</div>
<script>
let selectedTheme = null;
function selectTheme(card) {
// 清除之前选中
document.querySelectorAll('.theme-card.selected').forEach(c => c.classList.remove('selected'));
// 选中当前
card.classList.add('selected');
selectedTheme = card.getAttribute('data-theme');
// 更新底部栏
const name = card.querySelector('.theme-name').textContent;
document.getElementById('selectionText').innerHTML = '已选择:<strong>' + name + '</strong>';
document.getElementById('confirmBtn').disabled = false;
}
function confirmSelection() {
if (!selectedTheme) return;
const btn = document.getElementById('confirmBtn');
btn.disabled = true;
btn.textContent = '正在确认…';
// 向本地 server 发送选择结果
fetch('/api/theme-choice', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: selectedTheme })
})
.then(res => {
if (res.ok) {
btn.textContent = '✓ 已确认,Agent 将自动继续';
btn.style.background = '#059669';
document.getElementById('selectionText').innerHTML =
'已确认:<strong>' + selectedTheme + '</strong> — 可以关闭此页面';
} else {
throw new Error('Server error');
}
})
.catch(() => {
// 降级:写入剪贴板
navigator.clipboard.writeText(selectedTheme).then(() => {
btn.textContent = '✓ 已复制主题名到剪贴板';
btn.style.background = '#d97706';
}).catch(() => {
btn.textContent = '请将以下主题名告知 Agent:' + selectedTheme;
btn.style.background = '#dc2626';
});
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
theme-server.py 主题选择器本地服务
Agent 在主题选择步骤启动此 serverserve theme-picker.html 并接收用户选择
用法Agent 执行
python3 theme-server.py [--port PORT] [--dir DIR]
默认行为
- references/ 目录下启动 HTTP 服务
- 监听随机可用端口避免冲突
- serve 静态文件 + /api/theme-choice POST 端点
- 收到选择后写入 .theme-choice 文件并自动关闭 server
Agent 读取结果
读取工作目录下的 .theme-choice 文件内容为主题 id "cyber-dark"
"""
import http.server
import json
import os
import signal
import socket
import sys
import threading
from pathlib import Path
from urllib.parse import urlparse
def find_free_port():
"""找一个可用端口"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
return s.getsockname()[1]
class ThemeHandler(http.server.SimpleHTTPRequestHandler):
"""处理静态文件 serve + POST /api/theme-choice"""
def do_POST(self):
parsed = urlparse(self.path)
if parsed.path == '/api/theme-choice':
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
try:
data = json.loads(body)
theme = data.get('theme', '')
if not theme:
self.send_error(400, 'Missing theme')
return
# 写入选择结果到工作目录
choice_path = Path(self.server.output_dir) / '.theme-choice'
choice_path.write_text(theme, encoding='utf-8')
print(f'\n✓ 用户选择了主题: {theme}')
print(f' 结果已写入: {choice_path}')
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps({'ok': True, 'theme': theme}).encode())
# 延迟关闭 server(让响应先发出去)
threading.Timer(0.5, self.server.shutdown).start()
except json.JSONDecodeError:
self.send_error(400, 'Invalid JSON')
else:
self.send_error(404, 'Not found')
def do_OPTIONS(self):
"""处理 CORS 预检"""
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
def log_message(self, format, *args):
"""静默普通请求日志,只显示关键信息"""
if '/api/' in str(args[0]) if args else False:
super().log_message(format, *args)
def main():
import argparse
parser = argparse.ArgumentParser(description='Theme Picker Server')
parser.add_argument('--port', type=int, default=0, help='端口号(默认自动分配)')
parser.add_argument('--dir', type=str, default='.', help='静态文件目录(默认当前目录)')
parser.add_argument('--output', type=str, default='.', help='结果文件写入目录')
args = parser.parse_args()
port = args.port or find_free_port()
serve_dir = os.path.abspath(args.dir)
output_dir = os.path.abspath(args.output)
os.chdir(serve_dir)
# 清理上一次遗留的选择结果(防止误读旧主题)
old_choice = Path(output_dir) / '.theme-choice'
if old_choice.exists():
old_choice.unlink()
print(f'🧹 已清理旧的 .theme-choice 文件')
server = http.server.HTTPServer(('127.0.0.1', port), ThemeHandler)
server.output_dir = output_dir
url = f'http://localhost:{port}/theme-picker.html'
print(f'🎨 主题选择器已启动')
print(f' 地址: {url}')
print(f' 静态目录: {serve_dir}')
print(f' 结果写入: {output_dir}/.theme-choice')
print(f' 等待用户选择...\n')
# 输出 URL 供 Agent 读取(Agent 可以 grep 这行来获取 URL)
print(f'THEME_PICKER_URL={url}')
sys.stdout.flush()
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
print('\n🛑 Server 已关闭')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,49 @@
/*
* Theme: Azure Dark
* 蓝调科技深色 深灰黑底明亮蓝 #4787F0 强调色科技沉稳
* 适合企业年会演讲科技产品发布会ToB 深度方案夜间演示
*/
:root {
/* --- 配色 --- */
--color-primary: #f0f2f5;
--color-secondary: #8c8c8c;
--color-accent: #4787F0;
--color-bg: #141414;
--color-bg-alt: #1f1f1f;
--color-surface: #262626;
--color-text: #f0f2f5;
--color-text-secondary: #8c8c8c;
--color-text-on-primary: #ffffff;
--color-border: #3a3a3a;
/* --- 字体 --- */
--font-heading: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', 'Inter', sans-serif;
--font-body: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', 'Inter', sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
--font-size-title: 4.27rem;
--font-size-heading: 3.15rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 5.06rem;
/* --- 效果 --- */
--border-radius: 8px;
--shadow-sm: 0 1px 4px rgba(71,135,240,0.08);
--shadow-md: 0 4px 16px rgba(71,135,240,0.12);
--shadow-lg: 0 8px 32px rgba(71,135,240,0.18);
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Azure Light
* 蓝调科技浅色 纯白背景经典蓝 #0052D9 强调色专业大气
* 适合企业汇报科技产品发布ToB 方案展示品牌宣讲
*/
:root {
/* --- 配色 --- */
--color-primary: #1a1a1a;
--color-secondary: #666666;
--color-accent: #0052D9;
--color-bg: #ffffff;
--color-bg-alt: #f5f6f8;
--color-surface: #ffffff;
--color-text: #1a1a1a;
--color-text-secondary: #666666;
--color-text-on-primary: #ffffff;
--color-border: #dcdee0;
/* --- 字体 --- */
--font-heading: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', 'Inter', sans-serif;
--font-body: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', 'Inter', sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
--font-size-title: 3.94rem;
--font-size-heading: 2.81rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 5.06rem;
/* --- 效果 --- */
--border-radius: 8px;
--shadow-sm: 0 1px 3px rgba(0,82,217,0.04);
--shadow-md: 0 4px 12px rgba(0,82,217,0.06);
--shadow-lg: 0 8px 24px rgba(0,82,217,0.08);
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Cyber Dark
* 赛博霓虹深色 深蓝黑底蓝紫光弧渐变未来科技感
* 适合AI/SaaS 产品发布技术演讲黑客松展示极客风格
*/
:root {
/* --- 配色 --- */
--color-primary: #f0f4ff;
--color-secondary: #8b95ad;
--color-accent: #818cf8;
--color-bg: #0b0f19;
--color-bg-alt: #141928;
--color-surface: #1a2035;
--color-text: #f0f4ff;
--color-text-secondary: #8b95ad;
--color-text-on-primary: #0b0f19;
--color-border: #2a3150;
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-size-title: 3.94rem;
--font-size-heading: 2.81rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.24rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.06rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 1.97rem;
--spacing-lg: 3.09rem;
--spacing-xl: 4.5rem;
/* --- 效果 --- */
--border-radius: 10px;
--shadow-sm: 0 1px 4px rgba(129,140,248,0.08);
--shadow-md: 0 4px 16px rgba(129,140,248,0.12);
--shadow-lg: 0 8px 32px rgba(129,140,248,0.18);
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Cyber Light
* 赛博蓝紫浅色 纯净白底蓝紫渐变强调现代科技感
* 适合AI/SaaS 产品介绍技术方案展示创业路演浅色
*/
:root {
/* --- 配色 --- */
--color-primary: #0b1120;
--color-secondary: #5e6a81;
--color-accent: #6366f1;
--color-bg: #ffffff;
--color-bg-alt: #f5f6fa;
--color-surface: #ffffff;
--color-text: #0b1120;
--color-text-secondary: #5e6a81;
--color-text-on-primary: #ffffff;
--color-border: #d5d9e2;
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-size-title: 3.94rem;
--font-size-heading: 2.81rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.24rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.06rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 1.97rem;
--spacing-lg: 3.09rem;
--spacing-xl: 4.5rem;
/* --- 效果 --- */
--border-radius: 10px;
--shadow-sm: 0 1px 3px rgba(99,102,241,0.06);
--shadow-md: 0 4px 14px rgba(99,102,241,0.08);
--shadow-lg: 0 8px 28px rgba(99,102,241,0.12);
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Data Dark
* 数据仪表盘深色 深蓝黑底青绿/琥珀双强调色仪表盘质感
* 适合数据仪表盘展示趋势分析演讲技术指标汇报实时数据演示
*/
:root {
/* --- 配色 --- */
--color-primary: #e6edf3;
--color-secondary: #7d8590;
--color-accent: #10d48e;
--color-bg: #0d1117;
--color-bg-alt: #161b22;
--color-surface: #1c2129;
--color-text: #e6edf3;
--color-text-secondary: #7d8590;
--color-text-on-primary: #0d1117;
--color-border: #30363d;
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'IBM Plex Mono', 'Fira Code', monospace;
--font-size-title: 3.6rem;
--font-size-heading: 2.7rem;
--font-size-subheading: 1.57rem;
--font-size-body: 1.24rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.06rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 1.97rem;
--spacing-lg: 3.09rem;
--spacing-xl: 4.22rem;
/* --- 效果 --- */
--border-radius: 6px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
--shadow-md: 0 4px 14px rgba(0,0,0,0.4);
--shadow-lg: 0 8px 28px rgba(0,0,0,0.5);
--transition-speed: 0.25s;
--transition-easing: ease-out;
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Data Light
* 数据学院浅色 白底深蓝文字青绿强调色数据可视化友好
* 适合数据分析报告研究成果展示技术趋势分析学术数据汇报
*/
:root {
/* --- 配色 --- */
--color-primary: #0a2540;
--color-secondary: #546b81;
--color-accent: #0dab76;
--color-bg: #ffffff;
--color-bg-alt: #f6f8fa;
--color-surface: #ffffff;
--color-text: #0a2540;
--color-text-secondary: #546b81;
--color-text-on-primary: #ffffff;
--color-border: #d0d7de;
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'IBM Plex Mono', 'Fira Code', monospace;
--font-size-title: 3.6rem;
--font-size-heading: 2.7rem;
--font-size-subheading: 1.57rem;
--font-size-body: 1.24rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.06rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 1.97rem;
--spacing-lg: 3.09rem;
--spacing-xl: 4.22rem;
/* --- 效果 --- */
--border-radius: 6px;
--shadow-sm: 0 1px 2px rgba(10,37,64,0.05);
--shadow-md: 0 3px 10px rgba(10,37,64,0.08);
--shadow-lg: 0 6px 20px rgba(10,37,64,0.1);
--transition-speed: 0.25s;
--transition-easing: ease-out;
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,54 @@
/*
* Theme: Frost Dark
* 磨砂玻璃深色 深邃背景上的经典 glassmorphism冷调科技感
* 适合技术演讲数据仪表盘展示产品深度分析夜间演示
*
* 特性中模糊半透明面板冷色调背景氛围可见的白色高光边框
* 需要 base.html backdrop-filter 支持v1.1+
*/
:root {
/* --- 配色 --- */
--color-primary: #60a5fa;
--color-secondary: #94a3b8;
--color-accent: #22d3ee;
--color-bg: #0f172a;
--color-bg-alt: #1a2338;
--color-surface: rgba(255, 255, 255, 0.12);
--color-text: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-text-on-primary: #0f172a;
--color-border: rgba(255, 255, 255, 0.20);
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-size-title: 3.94rem;
--font-size-heading: 2.81rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.24rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.06rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 1.97rem;
--spacing-lg: 3.09rem;
--spacing-xl: 4.5rem;
/* --- 效果 --- */
--border-radius: 14px;
--shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.30), 0 0 0 1px rgba(255, 255, 255, 0.10) inset;
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.40), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.50), 0 0 0 1.5px rgba(255, 255, 255, 0.06) inset;
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
/* --- 玻璃/渐变扩展 --- */
--surface-blur: 20px;
--surface-saturate: 180%;
--bg-pattern:
radial-gradient(ellipse 65% 60% at 20% 45%, rgba(96, 165, 250, 0.18) 0%, transparent 70%),
radial-gradient(ellipse 55% 50% at 80% 30%, rgba(34, 211, 238, 0.14) 0%, transparent 70%);
}

View File

@ -0,0 +1,68 @@
/*
* Theme: Frost Light
* 磨砂玻璃浅色 经典 glassmorphism强模糊高饱和背景清晰面板边界
* 适合SaaS 产品介绍技术方案展示数据报告企业内部分享
*
* 特性强模糊半透明面板鲜艳冷色调背景光斑三层质感阴影
* 需要 base.html backdrop-filter 支持v1.1+
*
* 增强记录
* - v1.1: 初始版本 (blur 20px, saturate 150%)
* - v1.2: 增强质感 (blur 28px, saturate 180%, surface 更透明, bg-pattern 三光斑, 三层阴影)
*/
:root {
/* --- 配色 --- */
--color-primary: #3b82f6;
--color-secondary: #94a3b8;
--color-accent: #06b6d4;
--color-bg: #e8edf4;
--color-bg-alt: #dfe5ee;
--color-surface: rgba(255, 255, 255, 0.42);
--color-text: #0f172a;
--color-text-secondary: #475569;
--color-text-on-primary: #ffffff;
--color-border: rgba(255, 255, 255, 0.65);
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Fira Code', 'Cascadia Code', monospace;
--font-size-title: 3.94rem;
--font-size-heading: 2.81rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.06rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 4.5rem;
/* --- 效果 --- */
--border-radius: 16px;
--shadow-sm:
0 2px 8px rgba(0, 0, 0, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.85),
inset 0 0 0 1px rgba(255, 255, 255, 0.60);
--shadow-md:
0 4px 20px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.90),
inset 0 0 0 1px rgba(255, 255, 255, 0.50);
--shadow-lg:
0 8px 32px rgba(0, 0, 0, 0.10),
inset 0 1px 0 rgba(255, 255, 255, 0.95),
inset 0 0 0 1.5px rgba(255, 255, 255, 0.45);
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
/* --- 玻璃/渐变扩展 --- */
--surface-blur: 28px;
--surface-saturate: 180%;
--bg-pattern:
radial-gradient(ellipse 75% 65% at 20% 40%, rgba(59, 130, 246, 0.22) 0%, transparent 70%),
radial-gradient(ellipse 60% 55% at 80% 30%, rgba(6, 182, 212, 0.18) 0%, transparent 70%),
radial-gradient(ellipse 50% 45% at 55% 75%, rgba(139, 92, 246, 0.14) 0%, transparent 70%);
}

View File

@ -0,0 +1,55 @@
/*
* Theme: Glass Dark
* 液态玻璃深色 深邃背景上的多层折射光感
* 适合产品发布会科技演讲高端品牌展示沉浸式叙事
*
* 特性超强模糊 + 高饱和度浓烈冷调光斑玻璃高光边框
* 需要 base.html backdrop-filter 支持v1.1+
*/
:root {
/* --- 配色 --- */
--color-primary: #0A84FF;
--color-secondary: #8e8e93;
--color-accent: #BF5AF2;
--color-bg: #1a1a1e;
--color-bg-alt: #232328;
--color-surface: rgba(255, 255, 255, 0.16);
--color-text: #f5f5f7;
--color-text-secondary: #98989d;
--color-text-on-primary: #ffffff;
--color-border: rgba(255, 255, 255, 0.28);
/* --- 字体 --- */
--font-heading: 'SF Pro Display', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-body: 'SF Pro Text', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-mono: 'SF Mono', 'Fira Code', monospace;
--font-size-title: 4.27rem;
--font-size-heading: 3.15rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 5.06rem;
/* --- 效果 --- */
--border-radius: 22px;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.18);
--shadow-md: 0 4px 24px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 0 0 1px rgba(255, 255, 255, 0.10);
--shadow-lg: 0 8px 48px rgba(0, 0, 0, 0.55), inset 0 2px 0 rgba(255, 255, 255, 0.12), inset 0 0 0 1.5px rgba(255, 255, 255, 0.08);
--transition-speed: 0.35s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展 --- */
--surface-blur: 48px;
--surface-saturate: 200%;
--bg-pattern:
radial-gradient(ellipse 80% 70% at 12% 38%, rgba(191, 90, 242, 0.35) 0%, transparent 70%),
radial-gradient(ellipse 70% 60% at 82% 15%, rgba(10, 132, 255, 0.30) 0%, transparent 70%),
radial-gradient(ellipse 60% 55% at 50% 88%, rgba(48, 209, 88, 0.18) 0%, transparent 70%);
}

View File

@ -0,0 +1,55 @@
/*
* Theme: Glass Light
* 液态玻璃浅色 多层半透明折射色调随内容偏移
* 适合产品发布设计提案品牌展示科技新品介绍
*
* 特性超强模糊 + 高饱和度浓烈多彩光斑玻璃高光边框
* 需要 base.html backdrop-filter 支持v1.1+
*/
:root {
/* --- 配色 --- */
--color-primary: #007AFF;
--color-secondary: #8e8e93;
--color-accent: #AF52DE;
--color-bg: #e8e8f0;
--color-bg-alt: #e2e2ec;
--color-surface: rgba(255, 255, 255, 0.52);
--color-text: #1c1c1e;
--color-text-secondary: #636366;
--color-text-on-primary: #ffffff;
--color-border: rgba(255, 255, 255, 0.85);
/* --- 字体 --- */
--font-heading: 'SF Pro Display', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-body: 'SF Pro Text', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-mono: 'SF Mono', 'Fira Code', monospace;
--font-size-title: 4.27rem;
--font-size-heading: 3.15rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 5.06rem;
/* --- 效果 --- */
--border-radius: 22px;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.90);
--shadow-md: 0 4px 24px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.85), inset 0 0 0 1px rgba(255, 255, 255, 0.60);
--shadow-lg: 0 8px 48px rgba(0, 0, 0, 0.10), inset 0 2px 0 rgba(255, 255, 255, 0.80), inset 0 0 0 1.5px rgba(255, 255, 255, 0.50);
--transition-speed: 0.35s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展 --- */
--surface-blur: 48px;
--surface-saturate: 200%;
--bg-pattern:
radial-gradient(ellipse 80% 70% at 8% 35%, rgba(175, 82, 222, 0.30) 0%, transparent 70%),
radial-gradient(ellipse 70% 60% at 88% 18%, rgba(0, 122, 255, 0.26) 0%, transparent 70%),
radial-gradient(ellipse 60% 55% at 50% 90%, rgba(255, 149, 0, 0.18) 0%, transparent 70%);
}

View File

@ -0,0 +1,54 @@
/*
* Theme: Gradient Dark
* 渐变深色 深邃渐变背景霓虹感强调色沉浸式视觉体验
* 适合产品发布会创意演讲音乐/艺术展示高端品牌路演
*
* 特性深紫蓝渐变背景半透明面板轻度模糊霓虹色点缀
* 需要 base.html backdrop-filter 支持v1.1+
*/
:root {
/* --- 配色 --- */
--color-primary: #a78bfa;
--color-secondary: #8b7fad;
--color-accent: #f472b6;
--color-bg: #0c0118;
--color-bg-alt: #140225;
--color-surface: rgba(255, 255, 255, 0.06);
--color-text: #f5f0ff;
--color-text-secondary: #a599bf;
--color-text-on-primary: #0c0118;
--color-border: rgba(255, 255, 255, 0.08);
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-size-title: 4.27rem;
--font-size-heading: 2.93rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.24rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 1.97rem;
--spacing-lg: 3.09rem;
--spacing-xl: 4.5rem;
/* --- 效果 --- */
--border-radius: 14px;
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.04) inset;
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展 --- */
--surface-blur: 10px;
--bg-pattern:
radial-gradient(ellipse 70% 70% at 10% 40%, rgba(244, 114, 182, 0.18) 0%, transparent 70%),
radial-gradient(ellipse 60% 60% at 55% 85%, rgba(167, 139, 250, 0.15) 0%, transparent 70%),
radial-gradient(ellipse 50% 50% at 90% 15%, rgba(56, 189, 248, 0.10) 0%, transparent 70%);
}

View File

@ -0,0 +1,54 @@
/*
* Theme: Gradient Light
* 渐变浅色 大胆渐变背景 + 高对比文字视觉冲击力强
* 适合创业路演产品发布创意提案品牌宣讲设计作品展示
*
* 特性粉紫蓝渐变背景氛围半透明面板保持可读性轻度模糊
* 需要 base.html backdrop-filter 支持v1.1+
*/
:root {
/* --- 配色 --- */
--color-primary: #7c3aed;
--color-secondary: #a78bfa;
--color-accent: #ec4899;
--color-bg: #faf5ff;
--color-bg-alt: #f3eaff;
--color-surface: rgba(255, 255, 255, 0.70);
--color-text: #1e1033;
--color-text-secondary: #6b5e7b;
--color-text-on-primary: #ffffff;
--color-border: rgba(255, 255, 255, 0.55);
/* --- 字体 --- */
--font-heading: 'Inter', system-ui, -apple-system, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Fira Code', 'Cascadia Code', monospace;
--font-size-title: 4.27rem;
--font-size-heading: 2.93rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 5.06rem;
/* --- 效果 --- */
--border-radius: 14px;
--shadow-sm: 0 1px 4px rgba(124, 58, 237, 0.06), 0 0 0 1px rgba(255, 255, 255, 0.6) inset;
--shadow-md: 0 4px 16px rgba(124, 58, 237, 0.10), 0 0 0 1px rgba(255, 255, 255, 0.5) inset;
--shadow-lg: 0 8px 32px rgba(124, 58, 237, 0.14), 0 0 0 1px rgba(255, 255, 255, 0.4) inset;
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展 --- */
--surface-blur: 10px;
--bg-pattern:
radial-gradient(ellipse 70% 70% at 10% 30%, rgba(236, 72, 153, 0.15) 0%, transparent 70%),
radial-gradient(ellipse 60% 60% at 50% 80%, rgba(124, 58, 237, 0.12) 0%, transparent 70%),
radial-gradient(ellipse 50% 50% at 90% 20%, rgba(59, 130, 246, 0.10) 0%, transparent 70%);
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Pure Dark
* 极致简约深色 纯黑背景银白文字专业感十足
* 适合产品发布会科技演讲高端产品展示夜间演示
*/
:root {
/* --- 配色 --- */
--color-primary: #f5f5f7;
--color-secondary: #86868b;
--color-accent: #2997ff;
--color-bg: #000000;
--color-bg-alt: #1d1d1f;
--color-surface: #1d1d1f;
--color-text: #f5f5f7;
--color-text-secondary: #a1a1a6;
--color-text-on-primary: #000000;
--color-border: #424245;
/* --- 字体 --- */
--font-heading: 'SF Pro Display', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-body: 'SF Pro Text', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-mono: 'SF Mono', 'Fira Code', monospace;
--font-size-title: 4.5rem;
--font-size-heading: 3.38rem;
--font-size-subheading: 1.8rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.94rem;
--spacing-xl: 5.62rem;
/* --- 效果 --- */
--border-radius: 12px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
--shadow-md: 0 4px 16px rgba(0,0,0,0.4);
--shadow-lg: 0 10px 32px rgba(0,0,0,0.5);
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Pure Light
* 极致简约浅色 纯净白底精密排版系统蓝强调色
* 适合产品发布科技展示设计提案品牌介绍
*/
:root {
/* --- 配色 --- */
--color-primary: #1d1d1f;
--color-secondary: #86868b;
--color-accent: #0071e3;
--color-bg: #ffffff;
--color-bg-alt: #f5f5f7;
--color-surface: #ffffff;
--color-text: #1d1d1f;
--color-text-secondary: #86868b;
--color-text-on-primary: #ffffff;
--color-border: #d2d2d7;
/* --- 字体 --- */
--font-heading: 'SF Pro Display', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-body: 'SF Pro Text', -apple-system, 'Helvetica Neue', 'Inter', sans-serif;
--font-mono: 'SF Mono', 'Fira Code', monospace;
--font-size-title: 4.27rem;
--font-size-heading: 3.15rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.94rem;
--spacing-xl: 5.62rem;
/* --- 效果 --- */
--border-radius: 12px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04);
--shadow-md: 0 4px 12px rgba(0,0,0,0.06);
--shadow-lg: 0 8px 28px rgba(0,0,0,0.08);
--transition-speed: 0.3s;
--transition-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Warm Dark
* 暖调学院深色 深灰绿底暖白文字琥珀强调知性沉稳
* 适合AI 研究分享技术深度演讲产品战略沉浸式叙事
*/
:root {
/* --- 配色 --- */
--color-primary: #f0efe8;
--color-secondary: #9b9891;
--color-accent: #d4845a;
--color-bg: #1a1915;
--color-bg-alt: #2a2820;
--color-surface: #2a2820;
--color-text: #f0efe8;
--color-text-secondary: #9b9891;
--color-text-on-primary: #1a1915;
--color-border: #3d3b33;
/* --- 字体 --- */
--font-heading: 'Tiempos Headline', 'Georgia', 'Noto Serif SC', serif;
--font-body: 'Soehne', 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Soehne Mono', 'Fira Code', monospace;
--font-size-title: 3.94rem;
--font-size-heading: 2.7rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 4.5rem;
/* --- 效果 --- */
--border-radius: 8px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.25);
--shadow-md: 0 4px 14px rgba(0,0,0,0.35);
--shadow-lg: 0 8px 28px rgba(0,0,0,0.45);
--transition-speed: 0.3s;
--transition-easing: ease;
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,49 @@
/*
* Theme: Warm Light
* 暖调学院浅色 奶油白背景手绘质感人文温度
* 适合AI/科研汇报品牌故事人文科技主题温暖叙事
*/
:root {
/* --- 配色 --- */
--color-primary: #141413;
--color-secondary: #615f59;
--color-accent: #c4642d;
--color-bg: #faf9f5;
--color-bg-alt: #f0efe8;
--color-surface: #ffffff;
--color-text: #141413;
--color-text-secondary: #615f59;
--color-text-on-primary: #faf9f5;
--color-border: #d8d5cd;
/* --- 字体 --- */
--font-heading: 'Tiempos Headline', 'Georgia', 'Noto Serif SC', serif;
--font-body: 'Soehne', 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Soehne Mono', 'Fira Code', monospace;
--font-size-title: 3.94rem;
--font-size-heading: 2.7rem;
--font-size-subheading: 1.69rem;
--font-size-body: 1.27rem;
--font-size-small: 0.98rem;
/* --- 间距和尺寸 --- */
--slide-padding: 5.62rem;
--spacing-xs: 0.56rem;
--spacing-sm: 1.12rem;
--spacing-md: 2.25rem;
--spacing-lg: 3.38rem;
--spacing-xl: 4.5rem;
/* --- 效果 --- */
--border-radius: 8px;
--shadow-sm: 0 1px 3px rgba(20,20,19,0.04);
--shadow-md: 0 4px 12px rgba(20,20,19,0.06);
--shadow-lg: 0 8px 24px rgba(20,20,19,0.08);
--transition-speed: 0.3s;
--transition-easing: ease;
/* --- 玻璃/渐变扩展(非玻璃主题保持默认值,零渲染开销) --- */
--surface-blur: 0px;
--surface-saturate: 100%;
--bg-pattern: none;
}

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "requirements-analysis",
"installedVersion": "1.0.0",
"installedAt": 1774721510832
}

View File

@ -0,0 +1,139 @@
---
name: "requirements-analysis"
description: "专业需求分析师,通过多轮对话将简短想法转化为详细需求文档。使用此技能当用户需要:(1) 分析和记录需求,(2) 将 EPIC 分解为需求和用户故事,(3) 对多个需求进行优先级排序,(4) 识别利益相关者和依赖关系,(5) 定义验收标准,(6) 创建需求文档。支持单个需求分析、EPIC 分解、MoSCoW/RICE/Kano 等优先级排序方法。"
---
# 需求分析专家
## 核心能力
1. **对话式需求提取** - 通过系统化问题将一句话需求转化为全面规格说明
2. **分层需求分解** - 将复杂需求从 EPIC → 需求 → 用户故事进行分解
3. **优先级排序** - 支持 MoSCoW、RICE、价值 vs 成本、Kano、加权评分等方法
4. **文档生成** - 创建结构化的专业需求文档
## 单个需求分析工作流程
### 步骤 1:初始需求捕获
提出问题:
1. 用一句话描述这个需求是什么?
2. 这解决了什么问题?
3. 谁提出了这个需求?
### 步骤 2:利益相关者识别
提出问题:
1. 谁是最终用户?
2. 谁是业务负责人/赞助人?
3. 谁是技术利益相关者?
4. 是否有合规或安全利益相关者?
### 步骤 3:详细需求规格
**功能性需求**:
- 需要哪些具体功能?
- 用户工作流程是什么?
- 需要捕获/显示哪些数据?
**非功能性需求**:
- 性能期望(响应时间、吞吐量)
- 安全要求(认证、授权、加密)
- 可扩展性需求(并发用户数、数据量)
- 可用性要求(正常运行时间、灾难恢复)
**业务背景**:
- 业务价值是什么?
- 成功指标是什么?
- 预期的投资回报率是多少?
### 步骤 4:时间线和依赖关系
提出问题:
1. 什么时候需要?(硬性截止日期还是灵活的?)
2. 是否依赖其他项目/系统?
3. 是否有外部因素(监管截止日期、市场事件)?
4. 首选的交付方式是什么?(一次性交付还是分阶段?)
### 步骤 5:验收标准定义
使用 Given-When-Then 格式:
```
Given [前置条件]
When [操作]
Then [预期结果]
```
### 步骤 6:文档编写
参考 `references/requirement-template.md` 获取完整文档模板。
## EPIC 到用户故事的分解
### 层级结构
```
EPIC(业务计划)
├── 需求 1(功能/能力)
│ ├── 用户故事 1.1
│ ├── 用户故事 1.2
│ └── 用户故事 1.3
├── 需求 2(功能/能力)
│ ├── 用户故事 2.1
│ └── 用户故事 2.2
└── 需求 3(功能/能力)
└── 用户故事 3.1
```
### 分解流程
1. **从 EPIC 开始** - 识别高层次业务计划,定义愿景和目标
2. **分解为需求** - 将 EPIC 分解为逻辑功能或能力
3. **将需求分解为用户故事** - 从用户角度编写用户故事,使用 INVEST 标准
4. **验证层级结构** - 确保所有用户故事汇总到需求,所有需求汇总到 EPIC
详细模板和示例参见:
- `references/epic-template.md` - EPIC 文档模板
- `references/user-story-template.md` - 用户故事模板
## 优先级排序方法
### 方法选择指南
| 方法 | 最适合 | 何时使用 |
|--------|----------|----------|
| **MoSCoW** | 简单优先级,明确必需项 | 需要快速分类,明确必须有的功能 |
| **RICE** | 数据驱动决策 | 有量化数据,需要客观评分 |
| **价值 vs 成本** | 可视化优先级 | 需要快速决策,2 维评估 |
| **Kano 模型** | 关注用户满意度 | 以用户为中心,识别惊喜点 |
| **加权评分** | 自定义标准 | 需要多维度评估,利益相关者对齐 |
详细方法说明参见 `references/prioritization-methods.md`
## 最佳实践
1. **主动倾听** - 提出开放式问题,复述以确认理解,不要假设
2. **迭代细化** - 从高层次理解开始,逐步添加细节
3. **文档标准** - 使用一致的模板,包含所有必要信息
4. **利益相关者管理** - 尽早识别所有利益相关者,保持定期沟通
5. **可追溯性** - 将用户故事链接到需求,将需求链接到 EPIC
6. **验证** - 与利益相关者审查验收标准,定期验证优先级
## 常见陷阱
- **细节不足** - 使用 5W1H(谁、什么、何时、何地、为什么、如何)
- **范围蔓延** - 定义清晰的边界,使用变更控制流程
- **遗漏利益相关者** - 预先进行全面的利益相关者分析
- **不切实际的时间线** - 让技术团队参与估算,预留缓冲时间
- **忽视非功能性需求** - 始终明确询问非功能性需求
- **优先级排序不当** - 使用结构化的优先级排序方法,强制排序
- **缺乏验收标准** - 始终定义可测试的验收标准
## 参考资料
完整模板和详细指南:
- `references/requirement-template.md` - 需求文档完整模板
- `references/epic-template.md` - EPIC 文档模板
- `references/user-story-template.md` - 用户故事模板
- `references/prioritization-methods.md` - 优先级排序方法详解
- `references/complete-example.md` - 完整需求分析示例

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn7795tvpv9b8mq9wvh01g2e3n8167q2",
"slug": "requirements-analysis",
"version": "1.0.0",
"publishedAt": 1772418037477
}

View File

@ -0,0 +1,220 @@
# 完整需求分析示例
## 初始请求
```
用户:"我们需要一个管理者仪表板"
```
## 分析对话
### 第 1 轮:基本理解
```
AI:"我会帮你分析这个需求。让我先问几个问题:
1. 管理者面临什么具体问题,这个仪表板将解决?
2. 谁在请求这个仪表板?
3. 这是用于现有系统还是新系统?"
用户:"管理者无法轻松查看团队绩效指标。运营副总裁在请求它。这是用于我们现有的项目管理系统。"
```
### 第 2 轮:利益相关者识别
```
AI:"很好,让我了解利益相关者:
1. 谁是将使用这个仪表板的管理者?
2. 除了运营副总裁,还有谁需要参与?
3. 是否有合规或安全要求?"
用户:"所有部门经理(约 50 人)。我们需要让 IT 总监和数据隐私官参与,因为我们要显示员工数据。"
```
### 第 3 轮:详细需求
```
AI:"现在让我们深入细节:
1. 管理者需要看到哪些具体指标?
2. 数据应该多久更新一次?
3. 管理者需要过滤或自定义视图吗?
4. 是否有性能要求?"
用户:"他们需要看到:项目完成率、团队利用率、预算 vs 实际、即将到来的截止日期。数据应该每天更新。是的,他们应该能按团队、项目和日期范围过滤。仪表板应该在 3 秒内加载。"
```
### 第 4 轮:时间线和依赖关系
```
AI:"让我们讨论时间:
1. 你什么时候需要这个仪表板?
2. 是否依赖其他项目?
3. 是否有特定事件或截止日期驱动这个需求?"
用户:"我们需要在第二季度末(6 月 30 日)之前完成。它依赖于数据仓库项目首先完成。CEO 想在 7 月审查第二季度绩效,所以我们需要准备好。"
```
### 第 5 轮:验收标准
```
AI:"最后,让我们定义成功:
1. 我们如何知道这个仪表板满足你的需求?
2. 哪些是必须有的功能,哪些是最好有的?
3. 你将如何测试?"
用户:"成功意味着管理者可以做出数据驱动的决策,而无需向 IT 请求报告。必须有:我提到的 4 个指标、过滤和导出到 Excel。最好有:深入到单个项目、趋势图表。我们将首先与 5 位试点经理测试。"
```
---
## 最终需求文档
# 需求:管理者绩效仪表板
## 概述
- **ID**:REQ-2026-001
- **状态**:已批准
- **优先级**:高
- **创建日期**:2026-02-10
- **最后更新**:2026-02-10
## 利益相关者
- **业务负责人**:运营副总裁,Jane Smith
- **最终用户**:部门经理(50 位用户)
- **技术负责人**:IT 总监,John Doe
- **其他利益相关者**:数据隐私官(合规审查)
## 业务背景
### 问题陈述
部门经理目前缺乏对团队绩效指标的可见性,必须向 IT 请求自定义报告,导致决策延迟和 IT 资源使用效率低下。
### 业务价值
- 将 IT 报告请求量减少 80%
- 实现实时数据驱动决策
- 提高经理生产力 20%
- 支持 CEO 的第二季度绩效审查流程
### 成功指标
- 1 个月内 90% 经理采用率
- IT 报告请求减少 80%
- < 3 秒仪表板加载时间
- 85% 用户满意度评分
## 需求详情
### 功能性需求
#### 核心指标显示
1. **项目完成率**
- 显示按时完成的项目百分比
- 按团队和整体显示
- 颜色编码:绿色(>90%)、黄色(70-90%)、红色(<70%
2. **团队利用率**
- 显示团队容量利用百分比
- 按团队成员和团队平均值显示
- 包括可计费 vs 不可计费分解
3. **预算 vs 实际**
- 按项目显示预算差异
- 显示为百分比和绝对值
- 如果差异 > 10% 则警报
4. **即将到来的截止日期**
- 列出未来 30 天内有截止日期的项目
- 按紧急程度排序
- 以红色突出显示逾期项目
#### 过滤和自定义
- 按团队过滤(多选)
- 按项目过滤(多选)
- 按日期范围过滤(最近 7/30/90 天,自定义范围)
- 保存每个用户的过滤偏好
- 重置为默认视图
#### 数据导出
- 导出到 Excel(.xlsx 格式)
- 包含基于当前过滤器的所有可见数据
- 保持格式和颜色编码
#### 最好有的功能
- 深入到单个项目详情
- 趋势图表(显示随时间变化的指标的折线图)
- 电子邮件定时报告
### 非功能性需求
- **性能**
- 仪表板加载时间 < 3
- 过滤器应用 < 1
- 支持 50 个并发用户
- **安全**
- 基于角色的访问控制(经理只能看到他们的团队)
- 数据访问审计日志
- 仅 HTTPS
- **数据新鲜度**
- 数据每天早上 6 点更新
- 显示最后更新时间戳
- **可用性**
- 移动响应式设计
- 无障碍(符合 WCAG 2.1 AA)
- 无需培训的直观界面
- **可用性**
- 工作时间(早上 6 点 - 晚上 8 点)99% 正常运行时间
- 计划维护窗口:周日凌晨 2-4 点
## 验收标准
1. **Given** 我是一名经理,**When** 我登录仪表板,**Then** 我看到截至今天早上 6 点更新的我的团队绩效指标
2. **Given** 我正在查看仪表板,**When** 我应用过滤器(团队、项目、日期范围),**Then** 指标在 1 秒内更新以反映过滤后的数据
3. **Given** 我正在查看过滤后的数据,**When** 我点击"导出到 Excel",**Then** 下载一个 Excel 文件,包含所有可见数据并保持颜色编码
4. **Given** 我是一名经理,**When** 我尝试查看另一个团队的数据,**Then** 我被拒绝访问并看到适当的错误消息
5. **Given** 仪表板正在加载,**When** 页面加载时,**Then** 所有指标在 3 秒内可见
6. **Given** 我在移动设备上查看仪表板,**When** 我从手机访问它,**Then** 所有功能都可访问和可读
## 时间线
- **预期交付**:2026-06-30
- **里程碑**
- 需求批准:2026-02-15
- 设计审查:2026-03-01
- 开发完成:2026-05-31
- 试点测试:2026-06-01 - 2026-06-15
- 全面推出:2026-06-30
## 依赖关系
- **数据仓库项目**:必须在 2026-04-30 之前完成以提供数据源
- **认证系统**:必须支持基于角色的访问控制
- **Excel 导出库**:需要在 2026-03-15 之前评估和选择库
## 约束条件
- 必须使用现有项目管理数据库作为数据源
- 必须符合数据隐私法规(GDPR、CCPA)
- 必须与当前浏览器版本(Chrome、Firefox、Safari、Edge)兼容
- 预算:$50,000(开发 + 基础设施)
## 风险
| 风险 | 影响 | 概率 | 缓解措施 |
|------|--------|-------------|------------|
| 数据仓库项目延迟 | 高 | 中 | 从直接数据库查询开始,稍后迁移到仓库 |
| 50 个并发用户的性能问题 | 中 | 低 | 在预发布环境进行负载测试,优化查询,添加缓存 |
| 经理抵制采用 | 高 | 低 | 让经理参与设计,提供培训,收集反馈 |
| 数据隐私问题 | 高 | 低 | 与数据隐私官提前审查,实施严格的访问控制 |
## 备注
- 在全面推出之前,与来自不同部门的 5 位经理进行试点
- 计划在 6 月为所有经理举办培训课程
- 考虑根据反馈在第 2 阶段添加更多指标
- 探索在未来版本中与移动应用集成

View File

@ -0,0 +1,60 @@
# EPIC 文档模板
## EPIC:[标题]
### 愿景
[高层次业务目标或能力]
### 业务目标
- [目标 1]
- [目标 2]
### 目标用户
[用户画像或细分]
### 成功指标
- [KPI 1]:[目标]
- [KPI 2]:[目标]
### 时间线
- **开始日期**:[日期]
- **目标完成**:[日期]
- **预计时长**:[周/月]
### 需求列表
1. [需求 1]
2. [需求 2]
3. [需求 3]
---
## 需求层级模板
### 需求:[标题]
#### 父级 EPIC
[EPIC 名称和 ID]
#### 描述
[需求的详细描述]
#### 功能规格
- [规格 1]
- [规格 2]
#### 非功能性需求
- **性能**:[规格]
- **安全**:[规格]
- **可用性**:[规格]
#### 用户故事
1. [用户故事 1]
2. [用户故事 2]
3. [用户故事 3]
#### 依赖关系
- [依赖 1]
#### 验收标准
- [标准 1]
- [标准 2]

View File

@ -0,0 +1,208 @@
# 优先级排序方法详解
## 方法 1:MoSCoW 优先级排序
### 分类
- **必须有(Must Have)**:发布的关键功能,不可协商
- **应该有(Should Have)**:重要但不关键,必要时可以推迟
- **可以有(Could Have)**:最好有,时间允许时包含
- **不会有(Won't Have)**:本次发布不在范围内
### 流程
1. 列出所有需求
2. 对每个需求进行分类
3. 在每个类别内按重要性排序
4. 与利益相关者验证
### 示例
```
必须有:
1. 使用邮箱/密码的用户登录
2. 密码重置功能
3. 基本用户资料
应该有:
1. 社交登录(Google、Facebook)
2. 双因素认证
3. 头像上传
可以有:
1. 使用手机号登录
2. 生物识别认证
3. 活动日志
不会有(本次发布):
1. 单点登录(SSO)
2. LDAP 集成
3. OAuth 提供商
```
---
## 方法 2:RICE 评分
### 公式
RICE = (覆盖面 × 影响力 × 信心度) / 工作量
### 组成部分
- **覆盖面(Reach)**:这将影响多少用户?(每季度)
- **影响力(Impact)**:对每个用户的影响有多大?
- 3 = 巨大
- 2 = 高
- 1 = 中
- 0.5 = 低
- 0.25 = 最小
- **信心度(Confidence)**:我们对估算有多自信?
- 100% = 高
- 80% = 中
- 50% = 低
- **工作量(Effort)**:这需要多少人月?
### 流程
1. 对每个需求在所有四个维度上评分
2. 计算 RICE 分数
3. 按 RICE 分数排序需求(最高优先)
### 示例
```
需求 1:用户登录
- 覆盖面:10,000 用户/季度
- 影响力:3(巨大)
- 信心度:100%
- 工作量:2 人月
- RICE 分数:(10,000 × 3 × 1.0) / 2 = 15,000
需求 2:社交登录
- 覆盖面:5,000 用户/季度
- 影响力:2(高)
- 信心度:80%
- 工作量:1 人月
- RICE 分数:(5,000 × 2 × 0.8) / 1 = 8,000
需求 3:双因素认证
- 覆盖面:10,000 用户/季度
- 影响力:2(高)
- 信心度:90%
- 工作量:1.5 人月
- RICE 分数:(10,000 × 2 × 0.9) / 1.5 = 12,000
优先级顺序:需求 1 > 需求 3 > 需求 2
```
---
## 方法 3:价值 vs 成本矩阵
### 象限
- **快速胜利(Quick Wins)**(高价值,低成本):首先做
- **重大项目(Major Projects)**(高价值,高成本):其次做
- **填充项(Fill-Ins)**(低价值,低成本):时间允许时做
- **时间陷阱(Time Sinks)**(低价值,高成本):避免或推迟
### 流程
1. 对每个需求的价值评分(1-10)
2. 对每个需求的成本评分(1-10)
3. 在 2×2 矩阵上绘制
4. 优先处理快速胜利,然后是重大项目
### 示例矩阵
```
高价值
│ 重大项目 快速胜利
│ - SSO 集成 - 密码重置
│ - OAuth 提供商 - 资料编辑
│ - 邮箱验证
────┼────────────────────────────────────
│ 时间陷阱 填充项
│ - LDAP 集成 - 活动日志
│ - 自定义认证 - 登录历史
低价值
低成本 ──────────────── 高成本
```
---
## 方法 4:Kano 模型
### 分类
- **基本型需求(Basic Needs)**:必须有,用户期望它们(缺少会不满)
- **期望型需求(Performance Needs)**:越多越好(满意度线性增加)
- **兴奋型需求(Excitement Needs)**:意外的惊喜(存在时高满意度)
- **无差异(Indifferent)**:用户不在乎
- **反向(Reverse)**:用户更喜欢没有这个功能
### 流程
1. 通过功能性和非功能性问题调查用户
2. 对每个功能进行分类
3. 优先级:基本型需求 → 期望型需求 → 兴奋型需求
### 示例
```
基本型需求(必须有):
- 用户登录
- 密码安全
- 账户恢复
期望型需求(应该有):
- 快速登录(< 2
- 多种登录选项
- 会话管理
兴奋型需求(可以有):
- 生物识别登录
- 无密码认证
- 社交登录
无差异:
- 登录自定义主题
```
---
## 方法 5:加权评分
### 流程
1. 定义评估标准(例如:业务价值、技术可行性、用户影响、战略一致性)
2. 为每个标准分配权重(总计 = 100%)
3. 对每个需求在每个标准上评分(1-10)
4. 计算加权分数
5. 按加权分数排序
### 示例
```
标准权重:
- 业务价值:40%
- 用户影响:30%
- 技术可行性:20%
- 战略一致性:10%
需求 1:用户登录
- 业务价值:10 × 0.4 = 4.0
- 用户影响:10 × 0.3 = 3.0
- 技术可行性:8 × 0.2 = 1.6
- 战略一致性:9 × 0.1 = 0.9
- 总分:9.5
需求 2:社交登录
- 业务价值:7 × 0.4 = 2.8
- 用户影响:8 × 0.3 = 2.4
- 技术可行性:6 × 0.2 = 1.2
- 战略一致性:7 × 0.1 = 0.7
- 总分:7.1
优先级顺序:需求 1 > 需求 2
```
---
## 方法选择指南
| 方法 | 最适合 | 优点 | 缺点 |
|--------|----------|------|------|
| **MoSCoW** | 简单优先级,明确必需项 | 易于理解,快速 | 主观,无量化评分 |
| **RICE** | 数据驱动决策,多需求 | 客观,考虑多因素 | 需要估算,耗时 |
| **价值 vs 成本** | 可视化优先级,快速决策 | 简单,可视化,快速 | 过于简化,只有 2 个维度 |
| **Kano 模型** | 关注用户满意度 | 以用户为中心,识别惊喜点 | 需要用户研究,复杂 |
| **加权评分** | 自定义标准,利益相关者对齐 | 灵活,透明 | 需要权重共识 |

View File

@ -0,0 +1,63 @@
# 需求文档模板
## 概述
- **ID**:REQ-001
- **状态**:草稿/已批准/进行中/已完成
- **优先级**:高/中/低
- **创建日期**:[日期]
- **最后更新**:[日期]
## 利益相关者
- **业务负责人**:[姓名、部门]
- **最终用户**:[用户画像]
- **技术负责人**:[姓名、团队]
- **其他利益相关者**:[列表]
## 业务背景
### 问题陈述
[这解决了什么问题?]
### 业务价值
[为什么这很重要?]
### 成功指标
- [指标 1]:[目标]
- [指标 2]:[目标]
## 需求详情
### 功能性需求
1. [需求 1]
2. [需求 2]
### 非功能性需求
- **性能**:[规格]
- **安全**:[要求]
- **可扩展性**:[期望]
- **可用性**:[SLA]
## 验收标准
1. Given [前置条件], When [操作], Then [结果]
2. Given [前置条件], When [操作], Then [结果]
## 时间线
- **预期交付**:[日期]
- **里程碑**
- [里程碑 1]:[日期]
- [里程碑 2]:[日期]
## 依赖关系
- [依赖 1]:[描述]
- [依赖 2]:[描述]
## 约束条件
- [约束 1]
- [约束 2]
## 风险
- [风险 1]:[缓解措施]
- [风险 2]:[缓解措施]
## 备注
[附加信息]

View File

@ -0,0 +1,48 @@
# 用户故事模板
## 用户故事:[标题]
### 故事
作为 [用户角色]
我想要 [操作]
以便 [收益]
### 父级需求
[需求名称和 ID]
### 验收标准
1. Given [前置条件], When [操作], Then [结果]
2. Given [前置条件], When [操作], Then [结果]
3. Given [前置条件], When [操作], Then [结果]
### 技术说明
- [实现细节 1]
- [实现细节 2]
### 估算
- **故事点数**:[点数]
- **预计小时数**:[小时]
### 依赖关系
- [依赖 1]
### 完成定义
- [ ] 代码完成并审查
- [ ] 单元测试编写并通过
- [ ] 集成测试通过
- [ ] 文档更新
- [ ] 部署到预发布环境
- [ ] 验收标准验证
---
## INVEST 标准检查清单
用户故事应该满足 INVEST 标准:
- **I - Independent(独立)**:故事应该尽可能独立,可以任意顺序开发
- **N - Negotiable(可协商)**:故事不是合同,细节可以协商
- **V - Valuable(有价值)**:故事必须为用户或业务提供价值
- **E - Estimable(可估算)**:团队必须能够估算故事的大小
- **S - Small(小型)**:故事应该足够小,可以在一个迭代中完成
- **T - Testable(可测试)**:故事必须有明确的验收标准,可以测试

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "security-auditor",
"installedVersion": "1.0.0",
"installedAt": 1774706172739
}

View File

@ -0,0 +1,399 @@
---
name: security-auditor
version: 1.0.0
description: Use when reviewing code for security vulnerabilities, implementing authentication flows, auditing OWASP Top 10, configuring CORS/CSP headers, handling secrets, input validation, SQL injection prevention, XSS protection, or any security-related code review.
triggers:
- security
- vulnerability
- OWASP
- XSS
- SQL injection
- CSRF
- CORS
- CSP
- authentication
- authorization
- encryption
- secrets
- JWT
- OAuth
- audit
- penetration
- sanitize
- validate input
role: specialist
scope: review
output-format: structured
---
# Security Auditor
Comprehensive security audit and secure coding specialist. Adapted from buildwithclaude by Dave Poon (MIT).
## Role Definition
You are a senior application security engineer specializing in secure coding practices, vulnerability detection, and OWASP compliance. You conduct thorough security reviews and provide actionable fixes.
## Audit Process
1. **Conduct comprehensive security audit** of code and architecture
2. **Identify vulnerabilities** using OWASP Top 10 framework
3. **Design secure authentication and authorization** flows
4. **Implement input validation** and encryption mechanisms
5. **Create security tests** and monitoring strategies
## Core Principles
- Apply defense in depth with multiple security layers
- Follow principle of least privilege for all access controls
- Never trust user input — validate everything rigorously
- Design systems to fail securely without information leakage
- Conduct regular dependency scanning and updates
- Focus on practical fixes over theoretical security risks
---
## OWASP Top 10 Checklist
### 1. Broken Access Control (A01:2021)
```typescript
// ❌ BAD: No authorization check
app.delete('/api/posts/:id', async (req, res) => {
await db.post.delete({ where: { id: req.params.id } })
res.json({ success: true })
})
// ✅ GOOD: Verify ownership
app.delete('/api/posts/:id', authenticate, async (req, res) => {
const post = await db.post.findUnique({ where: { id: req.params.id } })
if (!post) return res.status(404).json({ error: 'Not found' })
if (post.authorId !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' })
}
await db.post.delete({ where: { id: req.params.id } })
res.json({ success: true })
})
```
**Checks:**
- [ ] Every endpoint verifies authentication
- [ ] Every data access verifies authorization (ownership or role)
- [ ] CORS configured with specific origins (not `*` in production)
- [ ] Directory listing disabled
- [ ] Rate limiting on sensitive endpoints
- [ ] JWT tokens validated on every request
### 2. Cryptographic Failures (A02:2021)
```typescript
// ❌ BAD: Storing plaintext passwords
await db.user.create({ data: { password: req.body.password } })
// ✅ GOOD: Bcrypt with sufficient rounds
import bcrypt from 'bcryptjs'
const hashedPassword = await bcrypt.hash(req.body.password, 12)
await db.user.create({ data: { password: hashedPassword } })
```
**Checks:**
- [ ] Passwords hashed with bcrypt (12+ rounds) or argon2
- [ ] Sensitive data encrypted at rest (AES-256)
- [ ] TLS/HTTPS enforced for all connections
- [ ] No secrets in source code or logs
- [ ] API keys rotated regularly
- [ ] Sensitive fields excluded from API responses
### 3. Injection (A03:2021)
```typescript
// ❌ BAD: SQL injection vulnerable
const query = `SELECT * FROM users WHERE email = '${email}'`
// ✅ GOOD: Parameterized queries
const user = await db.query('SELECT * FROM users WHERE email = $1', [email])
// ✅ GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { email } })
```
```typescript
// ❌ BAD: Command injection
const result = exec(`ls ${userInput}`)
// ✅ GOOD: Use execFile with argument array
import { execFile } from 'child_process'
execFile('ls', [sanitizedPath], callback)
```
**Checks:**
- [ ] All database queries use parameterized statements or ORM
- [ ] No string concatenation in queries
- [ ] OS command execution uses argument arrays, not shell strings
- [ ] LDAP, XPath, and NoSQL injection prevented
- [ ] User input never used in `eval()`, `Function()`, or template literals for code
### 4. Cross-Site Scripting (XSS) (A07:2021)
```typescript
// ❌ BAD: dangerouslySetInnerHTML with user input
<div dangerouslySetInnerHTML={{ __html: userComment }} />
// ✅ GOOD: Sanitize HTML
import DOMPurify from 'isomorphic-dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userComment) }} />
// ✅ BEST: Render as text (React auto-escapes)
<div>{userComment}</div>
```
**Checks:**
- [ ] React auto-escaping relied upon (avoid `dangerouslySetInnerHTML`)
- [ ] If HTML rendering needed, sanitize with DOMPurify
- [ ] CSP headers configured (see below)
- [ ] HttpOnly cookies for session tokens
- [ ] URL parameters validated before rendering
### 5. Security Misconfiguration (A05:2021)
**Checks:**
- [ ] Default credentials changed
- [ ] Error messages don't leak stack traces in production
- [ ] Unnecessary HTTP methods disabled
- [ ] Security headers configured (see below)
- [ ] Debug mode disabled in production
- [ ] Dependencies up to date (`npm audit`)
---
## Security Headers
```typescript
// next.config.js
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'", // tighten in production
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '),
},
]
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }]
},
}
```
---
## Input Validation Patterns
### Zod Validation for API/Actions
```typescript
import { z } from 'zod'
const userSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(128),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s'-]+$/),
age: z.number().int().min(13).max(150).optional(),
})
// Server Action
export async function createUser(formData: FormData) {
'use server'
const parsed = userSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
name: formData.get('name'),
})
if (!parsed.success) {
return { error: parsed.error.flatten() }
}
// Safe to use parsed.data
}
```
### File Upload Validation
```typescript
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']
const MAX_SIZE = 5 * 1024 * 1024 // 5MB
export async function uploadFile(formData: FormData) {
'use server'
const file = formData.get('file') as File
if (!file || file.size === 0) return { error: 'No file' }
if (!ALLOWED_TYPES.includes(file.type)) return { error: 'Invalid file type' }
if (file.size > MAX_SIZE) return { error: 'File too large' }
// Read and validate magic bytes, not just extension
const bytes = new Uint8Array(await file.arrayBuffer())
if (!validateMagicBytes(bytes, file.type)) return { error: 'File content mismatch' }
}
```
---
## Authentication Security
### JWT Best Practices
```typescript
import { SignJWT, jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET) // min 256-bit
export async function createToken(payload: { userId: string; role: string }) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('15m') // Short-lived access tokens
.setAudience('your-app')
.setIssuer('your-app')
.sign(secret)
}
export async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ['HS256'],
audience: 'your-app',
issuer: 'your-app',
})
return payload
} catch {
return null
}
}
```
### Cookie Security
```typescript
cookies().set('session', token, {
httpOnly: true, // No JavaScript access
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 60 * 60 * 24 * 7,
path: '/',
})
```
### Rate Limiting
```typescript
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
// In middleware or route handler
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, remaining } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
}
```
---
## Environment & Secrets
```typescript
// ❌ BAD
const API_KEY = 'sk-1234567890abcdef'
// ✅ GOOD
const API_KEY = process.env.API_KEY
if (!API_KEY) throw new Error('API_KEY not configured')
```
**Rules:**
- Never commit `.env` files (only `.env.example` with placeholder values)
- Use different secrets per environment
- Rotate secrets regularly
- Use a secrets manager (Vault, AWS SSM, Doppler) for production
- Never log secrets or include them in error responses
---
## Dependency Security
```bash
# Regular audit
npm audit
npm audit fix
# Check for known vulnerabilities
npx better-npm-audit audit
# Keep dependencies updated
npx npm-check-updates -u
```
---
## Security Audit Report Format
When conducting a review, output findings as:
```
## Security Audit Report
### Critical (Must Fix)
1. **[A03:Injection]** SQL injection in `/api/search` — user input concatenated into query
- File: `app/api/search/route.ts:15`
- Fix: Use parameterized query
- Risk: Full database compromise
### High (Should Fix)
1. **[A01:Access Control]** Missing auth check on DELETE endpoint
- File: `app/api/posts/[id]/route.ts:42`
- Fix: Add authentication middleware and ownership check
### Medium (Recommended)
1. **[A05:Misconfiguration]** Missing security headers
- Fix: Add CSP, HSTS, X-Frame-Options headers
### Low (Consider)
1. **[A06:Vulnerable Components]** 3 packages with known vulnerabilities
- Run: `npm audit fix`
```
---
## Protected File Patterns
These files should be reviewed carefully before any modification:
- `.env*` — environment secrets
- `auth.ts` / `auth.config.ts` — authentication configuration
- `middleware.ts` — route protection logic
- `**/api/auth/**` — auth endpoints
- `prisma/schema.prisma` — database schema (permissions, RLS)
- `next.config.*` — security headers, redirects
- `package.json` / `package-lock.json` — dependency changes

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn783g0k8dgj2wyz27c0e9he9180ce6k",
"slug": "security-auditor",
"version": "1.0.0",
"publishedAt": 1770008217892
}

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "sql-toolkit",
"installedVersion": "1.0.0",
"installedAt": 1774706181494
}

434
hermes/sql-toolkit/SKILL.md Normal file
View File

@ -0,0 +1,434 @@
---
name: sql-toolkit
description: Query, design, migrate, and optimize SQL databases. Use when working with SQLite, PostgreSQL, or MySQL — schema design, writing queries, creating migrations, indexing, backup/restore, and debugging slow queries. No ORMs required.
metadata: {"clawdbot":{"emoji":"🗄️","requires":{"anyBins":["sqlite3","psql","mysql"]},"os":["linux","darwin","win32"]}}
---
# SQL Toolkit
Work with relational databases directly from the command line. Covers SQLite, PostgreSQL, and MySQL with patterns for schema design, querying, migrations, indexing, and operations.
## When to Use
- Creating or modifying database schemas
- Writing complex queries (joins, aggregations, window functions, CTEs)
- Building migration scripts
- Optimizing slow queries with indexes and EXPLAIN
- Backing up and restoring databases
- Quick data exploration with SQLite (zero setup)
## SQLite (Zero Setup)
SQLite is included with Python and available on every system. Use it for local data, prototyping, and single-file databases.
### Quick Start
```bash
# Create/open a database
sqlite3 mydb.sqlite
# Import CSV directly
sqlite3 mydb.sqlite ".mode csv" ".import data.csv mytable" "SELECT COUNT(*) FROM mytable;"
# One-liner queries
sqlite3 mydb.sqlite "SELECT * FROM users WHERE created_at > '2026-01-01' LIMIT 10;"
# Export to CSV
sqlite3 -header -csv mydb.sqlite "SELECT * FROM orders;" > orders.csv
# Interactive mode with headers and columns
sqlite3 -header -column mydb.sqlite
```
### Schema Operations
```sql
-- Create table
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
-- Create with foreign key
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total REAL NOT NULL CHECK(total >= 0),
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','paid','shipped','cancelled')),
created_at TEXT DEFAULT (datetime('now'))
);
-- Add column
ALTER TABLE users ADD COLUMN phone TEXT;
-- Create index
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- View schema
.schema users
.tables
```
## PostgreSQL
### Connection
```bash
# Connect
psql -h localhost -U myuser -d mydb
# Connection string
psql "postgresql://user:pass@localhost:5432/mydb?sslmode=require"
# Run single query
psql -h localhost -U myuser -d mydb -c "SELECT NOW();"
# Run SQL file
psql -h localhost -U myuser -d mydb -f migration.sql
# List databases
psql -l
```
### Schema Design Patterns
```sql
-- Use UUIDs for distributed-friendly primary keys
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT NOT NULL,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('user','admin','moderator')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT users_email_unique UNIQUE(email)
);
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_users_modtime
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_modified_column();
-- Enum type (PostgreSQL-specific)
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'delivered', 'cancelled');
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status order_status NOT NULL DEFAULT 'pending',
total NUMERIC(10,2) NOT NULL CHECK(total >= 0),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Partial index (only index active orders — smaller, faster)
CREATE INDEX idx_orders_active ON orders(user_id, created_at)
WHERE status NOT IN ('delivered', 'cancelled');
-- GIN index for JSONB queries
CREATE INDEX idx_orders_metadata ON orders USING GIN(metadata);
```
### JSONB Queries (PostgreSQL)
```sql
-- Store JSON
INSERT INTO orders (user_id, total, metadata)
VALUES ('...', 99.99, '{"source": "web", "coupon": "SAVE10", "items": [{"sku": "A1", "qty": 2}]}');
-- Query JSON fields
SELECT * FROM orders WHERE metadata->>'source' = 'web';
SELECT * FROM orders WHERE metadata->'items' @> '[{"sku": "A1"}]';
SELECT metadata->>'coupon' AS coupon, COUNT(*) FROM orders GROUP BY 1;
-- Update JSON field
UPDATE orders SET metadata = jsonb_set(metadata, '{source}', '"mobile"') WHERE id = '...';
```
## MySQL
### Connection
```bash
mysql -h localhost -u root -p mydb
mysql -h localhost -u root -p -e "SELECT NOW();" mydb
```
### Key Differences from PostgreSQL
```sql
-- Auto-increment (not SERIAL)
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- JSON type (MySQL 5.7+)
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
metadata JSON,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Query JSON
SELECT * FROM orders WHERE JSON_EXTRACT(metadata, '$.source') = 'web';
-- Or shorthand:
SELECT * FROM orders WHERE metadata->>'$.source' = 'web';
```
## Query Patterns
### Joins
```sql
-- Inner join (only matching rows)
SELECT u.name, o.total, o.status
FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE o.created_at > '2026-01-01';
-- Left join (all users, even without orders)
SELECT u.name, COUNT(o.id) AS order_count, COALESCE(SUM(o.total), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;
-- Self-join (find users with same email domain)
SELECT a.name, b.name, SPLIT_PART(a.email, '@', 2) AS domain
FROM users a
JOIN users b ON SPLIT_PART(a.email, '@', 2) = SPLIT_PART(b.email, '@', 2)
WHERE a.id < b.id;
```
### Aggregations
```sql
-- Group by with having
SELECT status, COUNT(*) AS cnt, SUM(total) AS revenue
FROM orders
GROUP BY status
HAVING COUNT(*) > 10
ORDER BY revenue DESC;
-- Running total (window function)
SELECT date, revenue,
SUM(revenue) OVER (ORDER BY date) AS cumulative_revenue
FROM daily_sales;
-- Rank within groups
SELECT user_id, total,
RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS rank
FROM orders;
-- Moving average (last 7 entries)
SELECT date, revenue,
AVG(revenue) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7
FROM daily_sales;
```
### Common Table Expressions (CTEs)
```sql
-- Readable multi-step queries
WITH monthly_revenue AS (
SELECT DATE_TRUNC('month', created_at) AS month,
SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1
),
growth AS (
SELECT month, revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
ROUND((revenue - LAG(revenue) OVER (ORDER BY month)) /
NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 1) AS growth_pct
FROM monthly_revenue
)
SELECT * FROM growth ORDER BY month;
-- Recursive CTE (org chart / tree traversal)
WITH RECURSIVE org_tree AS (
SELECT id, name, manager_id, 0 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, t.depth + 1
FROM employees e
JOIN org_tree t ON e.manager_id = t.id
)
SELECT REPEAT(' ', depth) || name AS org_chart FROM org_tree ORDER BY depth, name;
```
## Migrations
### Manual Migration Script Pattern
```bash
#!/bin/bash
# migrate.sh - Run numbered SQL migration files
DB_URL="${1:?Usage: migrate.sh <db-url>}"
MIGRATIONS_DIR="./migrations"
# Create tracking table
psql "$DB_URL" -c "CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW()
);"
# Run pending migrations in order
for file in $(ls "$MIGRATIONS_DIR"/*.sql | sort); do
version=$(basename "$file" .sql)
already=$(psql "$DB_URL" -tAc "SELECT 1 FROM schema_migrations WHERE version='$version';")
if [ "$already" = "1" ]; then
echo "SKIP: $version (already applied)"
continue
fi
echo "APPLY: $version"
psql "$DB_URL" -f "$file" && \
psql "$DB_URL" -c "INSERT INTO schema_migrations (version) VALUES ('$version');" || {
echo "FAILED: $version"
exit 1
}
done
echo "All migrations applied."
```
### Migration File Convention
```
migrations/
001_create_users.sql
002_create_orders.sql
003_add_users_phone.sql
004_add_orders_metadata_index.sql
```
Each file:
```sql
-- 003_add_users_phone.sql
-- Up
ALTER TABLE users ADD COLUMN phone TEXT;
-- To reverse: ALTER TABLE users DROP COLUMN phone;
```
## Query Optimization
### EXPLAIN (PostgreSQL)
```sql
-- Show query plan
EXPLAIN SELECT * FROM orders WHERE user_id = '...' AND status = 'paid';
-- Show actual execution times
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = '...' AND status = 'paid';
```
**What to look for:**
- `Seq Scan` on large tables → needs an index
- `Nested Loop` with large row counts → consider `Hash Join` (may need more `work_mem`)
- `Rows Removed by Filter` being high → index doesn't cover the filter
- Actual rows far from estimated → run `ANALYZE tablename;` to update statistics
### Index Strategy
```sql
-- Single column (most common)
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- Composite (for queries filtering on both columns)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Column ORDER matters: put equality filters first, range filters last
-- Covering index (includes data columns to avoid table lookup)
CREATE INDEX idx_orders_covering ON orders(user_id, status) INCLUDE (total, created_at);
-- Partial index (smaller, faster — only index what you query)
CREATE INDEX idx_orders_pending ON orders(user_id) WHERE status = 'pending';
-- Check unused indexes
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexname NOT LIKE '%pkey%'
ORDER BY pg_relation_size(indexrelid) DESC;
```
### SQLite EXPLAIN
```sql
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 5;
-- Look for: SCAN (bad) vs SEARCH USING INDEX (good)
```
## Backup & Restore
### PostgreSQL
```bash
# Full dump (custom format, compressed)
pg_dump -Fc -h localhost -U myuser mydb > backup.dump
# Restore
pg_restore -h localhost -U myuser -d mydb --clean --if-exists backup.dump
# SQL dump (portable, readable)
pg_dump -h localhost -U myuser mydb > backup.sql
# Dump specific tables
pg_dump -h localhost -U myuser -t users -t orders mydb > partial.sql
# Copy table to CSV
psql -c "\copy (SELECT * FROM users) TO 'users.csv' CSV HEADER"
```
### SQLite
```bash
# Backup (just copy the file, but use .backup for consistency)
sqlite3 mydb.sqlite ".backup backup.sqlite"
# Dump to SQL
sqlite3 mydb.sqlite .dump > backup.sql
# Restore from SQL
sqlite3 newdb.sqlite < backup.sql
```
### MySQL
```bash
# Dump
mysqldump -h localhost -u root -p mydb > backup.sql
# Restore
mysql -h localhost -u root -p mydb < backup.sql
```
## Tips
- Always use parameterized queries in application code — never concatenate user input into SQL
- Use `TIMESTAMPTZ` (not `TIMESTAMP`) in PostgreSQL for timezone-aware dates
- Set `PRAGMA journal_mode=WAL;` in SQLite for concurrent read performance
- Use `EXPLAIN` before deploying any query that runs on large tables
- PostgreSQL: `\d+ tablename` shows columns, indexes, and size. `\di+` lists all indexes with sizes
- For quick data exploration, import any CSV into SQLite: `sqlite3 :memory: ".mode csv" ".import file.csv t" "SELECT ..."`

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn7f6g2r31qsb1ts8cf5x7rpk180fn9j",
"slug": "sql-toolkit",
"version": "1.0.0",
"publishedAt": 1770155077543
}

View File

@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "tg-miniapp",
"installedVersion": "1.0.1",
"installedAt": 1774706194078
}

View File

@ -0,0 +1,66 @@
---
name: tg-miniapp
description: Build Telegram Mini Apps without the pain. Includes solutions for safe areas, fullscreen mode, BackButton handlers, sharing with inline mode, position:fixed issues, and React gotchas. Use when building or debugging Telegram Mini Apps, or when encountering issues with WebApp API, safe areas, or sharing.
---
# Telegram Mini App Development
Battle-tested solutions for common Telegram Mini App issues.
## Quick Reference
### Safe Area (Fullscreen Mode)
```typescript
// Use reactive hook - values are async and context-dependent
const safeArea = useSafeAreaInset(); // from references/hooks.ts
<div style={{ paddingTop: safeArea.top }}>Header</div>
```
### position:fixed Broken
Telegram applies `transform` to container. Use `createPortal`:
```tsx
import { createPortal } from 'react-dom';
createPortal(<Modal />, document.body);
```
### BackButton Not Firing
Use `@telegram-apps/sdk`, not raw WebApp:
```typescript
import { onBackButtonClick, showBackButton } from '@telegram-apps/sdk';
onBackButtonClick(handleBack);
```
### Sharing with Inline Mode
1. Enable inline mode: @BotFather`/setinline` → select bot
2. Backend calls `savePreparedInlineMessage` → returns `prepared_message_id`
3. Frontend calls `WebApp.shareMessage(prepared_message_id)`
**Note:** `prepared_message_id` is **single-use** — prepare fresh for each share tap.
For static content, consider caching images on R2/CDN and preparing per-tap.
### React "0" Rendering
```tsx
// WRONG: renders literal "0"
{count && <span>{count}</span>}
// CORRECT
{count > 0 && <span>{count}</span>}
```
## Detailed Guides
- **[references/KNOWLEDGE.md](references/KNOWLEDGE.md)** — Full knowledge base with all gotchas and solutions
- **[references/hooks.ts](references/hooks.ts)** — Copy-paste React hooks (useSafeAreaInset, useFullscreen, etc.)
- **[references/components.tsx](references/components.tsx)** — Ready-to-use components (SafeAreaHeader, DebugOverlay)
## Testing Checklist
Before shipping, test:
- [ ] Open from folder (chat list)
- [ ] Open from direct bot chat
- [ ] iOS device
- [ ] Android device
- [ ] Share flow (tap → dismiss → tap again)
- [ ] Share to different chat types (user, group, channel)
- [ ] Back button
- [ ] Scroll with sticky header

View File

@ -0,0 +1,6 @@
{
"ownerId": "kn76559dp1y0xr5b832vb7dbd580d5f5",
"slug": "tg-miniapp",
"version": "1.0.1",
"publishedAt": 1770044172607
}

View File

@ -0,0 +1,375 @@
# Telegram Mini App Knowledge Base
Everything we've learned building Telegram Mini Apps. Read this before starting a new project or debugging issues.
---
## 🔴 Critical Issues (Will Bite You)
### 1. Safe Area / Fullscreen Mode
**The Problem:** In fullscreen mode, Telegram overlays its controls (X Close, chevron) on top of your app. Content gets hidden behind them.
**Why It's Tricky:**
- `safeAreaInset` and `contentSafeAreaInset` can return 0 initially
- Values differ based on how app is opened (folder vs direct chat)
- Values are async — can change after initial render
- iOS and Android have different safe areas
**The Solution:**
```typescript
// Use reactive hook, not one-time check
function useSafeAreaInset() {
const [insets, setInsets] = useState({ top: 0, bottom: 0 });
useEffect(() => {
const webApp = window.Telegram?.WebApp;
if (!webApp) return;
const update = () => {
const safeArea = webApp.safeAreaInset || { top: 0 };
const contentSafeArea = webApp.contentSafeAreaInset || { top: 0 };
const isFullscreen = webApp.isFullscreen;
let top = Math.max(safeArea.top || 0, contentSafeArea.top || 0);
// Minimum fallbacks when in fullscreen but values are low
if (isFullscreen && top < 80) {
top = webApp.platform === 'ios' ? 100 : 80;
}
setInsets({ top, bottom: safeArea.bottom || 0 });
};
update();
// Listen for changes
webApp.onEvent?.('safeAreaChanged', update);
webApp.onEvent?.('fullscreenChanged', update);
// Fallback: poll every 500ms
const interval = setInterval(update, 500);
return () => clearInterval(interval);
}, []);
return insets;
}
```
**Sticky Headers:**
```tsx
// WRONG - content shows through gap
<div className="sticky top-0">Header</div>
// CORRECT - background covers full safe area
<div
className="sticky top-0 bg-[var(--tg-theme-bg-color)]"
style={{ paddingTop: safeAreaInset.top }}
>
Header
</div>
```
---
### 2. position: fixed Doesn't Work
**The Problem:** Telegram Mini Apps apply CSS `transform` to the container, which breaks `position: fixed` elements.
**Symptoms:** Bottom sheets, modals, tooltips render in wrong position.
**The Solution:** Use React `createPortal` to render to `document.body`:
```tsx
import { createPortal } from 'react-dom';
function Modal({ children }) {
return createPortal(
<div className="fixed inset-0 z-[9999]">{children}</div>,
document.body
);
}
```
---
### 3. React Renders "0" as Text
**The Problem:** `{value && value > 0 && <Component />}` renders literal "0" when value is 0.
**The Solution:**
```tsx
// WRONG
{count && count > 0 && <span>{count}</span>}
// CORRECT
{count != null && count > 0 && <span>{count}</span>}
// OR
{count > 0 && <span>{count}</span>}
// OR
{!!count && <span>{count}</span>}
```
---
### 4. BackButton Click Handler Doesn't Fire
**The Problem:** Telegram BackButton appears but clicking does nothing.
**Why:** Using raw `window.Telegram.WebApp.BackButton` with stale closures.
**The Solution:** Use `@telegram-apps/sdk`:
```typescript
import {
mountBackButton,
showBackButton,
hideBackButton,
onBackButtonClick,
offBackButtonClick
} from '@telegram-apps/sdk';
// Mount once at app init
mountBackButton();
// In component
useEffect(() => {
if (shouldShowBack) {
showBackButton();
onBackButtonClick(handleBack);
} else {
hideBackButton();
}
return () => offBackButtonClick(handleBack);
}, [shouldShowBack, handleBack]);
```
---
### 5. Sharing with Inline Mode
**The Problem:** `WebApp.shareMessage()` fails with cryptic errors.
**Requirements:**
1. Bot must have **inline mode enabled** via @BotFather (`/setinline`)
2. Must call `savePreparedInlineMessage` on backend first
3. Pass the `prepared_message_id` to `shareMessage()`
**Basic Flow:**
```
Frontend → POST /api/prepare-share → Backend calls savePreparedInlineMessage
← Returns prepared_message_id
Frontend → WebApp.shareMessage(id) → Native share picker
```
**Key Insight: prepared_message_id is Single-Use**
Once a `prepared_message_id` is consumed by `shareMessage()` (whether sent or dismissed), it cannot be reused. This affects UX:
- User taps share → prepares message → opens picker → dismisses → taps share again → **fails**
- Need to prepare a fresh message for each share attempt
**Two Approaches:**
**A) Dynamic Preparation (per-tap)**
Prepare a fresh message every time user taps share:
```typescript
const handleShare = async () => {
const { prepared_message_id } = await fetch('/api/prepare-share').then(r => r.json());
await WebApp.shareMessage(prepared_message_id);
};
```
- ✅ Always works, each tap gets fresh ID
- ⚠️ More API calls
- ⚠️ Slight delay before picker opens
**B) Static Content Caching (recommended for frequent shares)**
Use static inline results with `allow_user_chats: true`. The content stays the same, so you can cache results:
```typescript
// Backend: savePreparedInlineMessage with static result
const result = {
type: 'photo',
photo_url: 'https://cdn.example.com/static-card.jpg', // Same URL always
thumbnail_url: ...,
};
const { prepared_message_id } = await bot.savePreparedInlineMessage(userId, result, {
allow_user_chats: true,
allow_bot_chats: true,
allow_group_chats: true,
allow_channel_chats: true,
});
// Frontend: prepare once, reuse pattern
const handleShare = async () => {
// For static content, prepare fresh each time anyway (IDs are single-use)
const { prepared_message_id } = await fetch('/api/prepare-share-static').then(r => r.json());
await WebApp.shareMessage(prepared_message_id);
};
```
- ✅ Works reliably for multi-share scenarios
- ✅ Image can be cached on CDN
- ⚠️ Content is same for all shares of that item
**Fallback chain:**
1. Try native `shareMessage` (requires inline mode)
2. Try sending image to bot chat (user forwards manually)
3. Fall back to text share via `web_app_open_tg_link`
**Known Behaviors (2026-02):**
- `shareMessage` requires **WebApp 8.0+** — check version before calling
- **PNG actually works** — Despite docs suggesting JPEG/GIF, PNG renders fine in inline results
- **Callback returns falsy even on success** — use truthy check `if (sent)` not `=== true`
- **JPEG recommended for share cards** — Smaller file size, faster loading in chat
- **photo_url must be publicly accessible** — Use R2 public bucket or similar CDN
**Two-Button Pattern (ClawdFessions approach):**
For apps where you want both quick sharing AND rich custom cards:
```tsx
<div className="flex gap-1">
{/* Quick Share - uses static prepared message, opens native picker */}
<button onClick={handleQuickShare}></button>
{/* Share Card - sends rich image to bot chat, user forwards manually */}
<button onClick={handleShareCard}>🎨</button>
</div>
```
- Quick Share: Native picker, faster, less friction
- Share Card: Custom generated image, richer but requires manual forward
---
### 6. Generating Share Cards (Server-Side Images)
**The Problem:** You want custom share card images generated dynamically per item.
**Solution: resvg-wasm in Cloudflare Workers**
Generate SVG → convert to PNG/JPEG server-side:
```typescript
import { Resvg, initWasm } from '@resvg/resvg-wasm';
import resvgWasm from './resvg.wasm';
let initialized = false;
async function ensureWasm() {
if (!initialized) {
await initWasm(resvgWasm);
initialized = true;
}
}
async function svgToJpeg(svg: string): Promise<Uint8Array> {
await ensureWasm();
const resvg = new Resvg(svg, {
fitTo: { mode: 'width', value: 800 },
font: {
fontBuffers: [fontData], // Bundle your fonts
defaultFontFamily: 'Inter',
},
});
const pixels = resvg.render();
return encodeJpeg(pixels); // Use jpeg-js or similar
}
```
**Tips:**
- Bundle fonts as base64 or binary — Google Fonts won't load at runtime
- Cache generated images in R2 with content hash keys
- 800px width is good for Telegram inline results
- JPEG for photos, PNG if you need transparency
**R2 Caching Pattern:**
```typescript
// Check cache first
const cacheKey = `cards/${confessionId}.jpg`;
const cached = await env.CARDS.get(cacheKey);
if (cached) return new Response(cached.body, { headers: { 'Content-Type': 'image/jpeg' } });
// Generate and cache
const jpeg = await generateCard(confession);
await env.CARDS.put(cacheKey, jpeg, { httpMetadata: { contentType: 'image/jpeg' } });
return new Response(jpeg, { headers: { 'Content-Type': 'image/jpeg' } });
```
---
## 🟡 Common Gotchas
### API Image Paths
Different APIs return images in different paths. Always use fallback chains:
```typescript
const imageUrl = item.preview?.image256
|| item.collection?.preview?.image256
|| item.image
|| '/placeholder.png';
```
### GetGems API
- Only supports `limit`, **no pagination/offset**
- Rate limit: 400 requests / 5 minutes per IP
- Volume data is **all-time**, not 24h
- USDT listings exist — detect by token address
### Fragment API
- Number images: `https://nft.fragment.com/number/{11-digit-number}.webp`
- Username images: via separate API call
### Theme Colors
Use CSS variables for Telegram theme integration:
```css
background: var(--tg-theme-bg-color, #0f0f1a);
color: var(--tg-theme-text-color, #fff);
```
---
## 🟢 Best Practices
### Debug Overlay
Add a dev-only panel showing platform info:
```tsx
function DebugOverlay() {
if (!isDev) return null;
const webApp = window.Telegram?.WebApp;
return (
<div className="fixed bottom-4 right-4 bg-black/80 p-2 text-xs">
<div>Platform: {webApp?.platform}</div>
<div>Fullscreen: {webApp?.isFullscreen ? 'Y' : 'N'}</div>
<div>Safe top: {webApp?.safeAreaInset?.top}</div>
<div>Content safe top: {webApp?.contentSafeAreaInset?.top}</div>
</div>
);
}
```
### Testing Checklist
- [ ] Open from folder (chat list)
- [ ] Open from direct chat
- [ ] Open from inline button
- [ ] Test on iOS
- [ ] Test on Android
- [ ] Test share flow
- [ ] Test back button
- [ ] Scroll with sticky header
### Deployment
```bash
# Always staging first
npx wrangler pages deploy dist --project-name myapp-staging
# Production only after testing
npx wrangler pages deploy dist --project-name myapp
```
---
## 📁 Component Library
Reusable components at `~/clawd/projects/tg-miniapp-components/`:
- `useSafeAreaInset()` — Reactive safe area hook
- `useFullscreen()` — Fullscreen state + control
- `SafeAreaHeader` — Sticky header with safe area handling
- `DebugOverlay` — Dev panel for debugging
---
## 📚 Resources
- [Telegram Mini Apps Docs](https://core.telegram.org/bots/webapps)
- [@telegram-apps/sdk](https://github.com/Telegram-Mini-Apps/telegram-apps)
- Project PLAYBOOK files contain project-specific learnings
---
*Last updated: 2026-02-02*
*Compiled from sessions: 2026-01-27 through 2026-02-02*

View File

@ -0,0 +1,267 @@
/**
* Telegram Mini App React Components
* Copy-paste into your project
*
* Requires: hooks.ts from this same skill
*/
import React, { useState, useCallback, useEffect, ReactNode, CSSProperties } from 'react';
import { createPortal } from 'react-dom';
import { useSafeAreaInset, useFullscreen, useTelegramTheme, getWebApp, isDevMode } from './hooks';
// ============================================================================
// SafeAreaHeader
// ============================================================================
interface SafeAreaHeaderProps {
children: ReactNode;
className?: string;
style?: CSSProperties;
transparent?: boolean;
backgroundColor?: string;
position?: 'fixed' | 'sticky';
blur?: boolean;
height?: number;
border?: boolean;
zIndex?: number;
}
/**
* Header with proper safe area handling
*
* @example
* <SafeAreaHeader blur border>
* <h1>My App</h1>
* </SafeAreaHeader>
*/
export function SafeAreaHeader({
children,
className = '',
style = {},
transparent = false,
backgroundColor,
position = 'sticky',
blur = false,
height = 56,
border = false,
zIndex = 1000,
}: SafeAreaHeaderProps) {
const { totalTop } = useSafeAreaInset();
const { params, isDark } = useTelegramTheme();
const bgColor = backgroundColor
?? (transparent ? 'transparent' : (params.bg_color ?? '#0f0f1a'));
const headerStyle: CSSProperties = {
position,
top: 0,
left: 0,
right: 0,
zIndex,
paddingTop: totalTop,
backgroundColor: blur ? `${bgColor}cc` : bgColor,
backdropFilter: blur ? 'blur(20px)' : undefined,
WebkitBackdropFilter: blur ? 'blur(20px)' : undefined,
borderBottom: border
? `1px solid ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}`
: undefined,
...style,
};
const contentStyle: CSSProperties = {
height,
display: 'flex',
alignItems: 'center',
paddingLeft: 16,
paddingRight: 16,
};
return (
<header style={headerStyle} className={className}>
<div style={contentStyle}>{children}</div>
</header>
);
}
/**
* Spacer for fixed SafeAreaHeader
*/
export function SafeAreaHeaderSpacer({ height = 56 }: { height?: number }) {
const { totalTop } = useSafeAreaInset();
return <div style={{ height: totalTop + height }} />;
}
// ============================================================================
// DebugOverlay
// ============================================================================
interface DebugOverlayProps {
forceShow?: boolean;
defaultOpen?: boolean;
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
}
/**
* Debug panel showing safe areas, viewport, platform info
* Only visible on localhost or with ?debug=1
*
* @example
* <DebugOverlay /> // Add to app root
*/
export function DebugOverlay({
forceShow = false,
defaultOpen = false,
position = 'bottom-right',
}: DebugOverlayProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const insets = useSafeAreaInset();
const { isFullscreen } = useFullscreen();
if (!forceShow && !isDevMode()) return null;
const webApp = getWebApp();
const values = [
{ label: 'Platform', value: webApp?.platform ?? 'unknown' },
{ label: 'Version', value: webApp?.version ?? 'N/A' },
{ label: 'Fullscreen', value: isFullscreen ? '✓' : '✗' },
{ label: 'Viewport', value: `${window.innerWidth}×${webApp?.viewportHeight ?? window.innerHeight}` },
{ label: 'Safe Top', value: insets.device.top },
{ label: 'Safe Bottom', value: insets.device.bottom },
{ label: 'Content Top', value: insets.content.top },
{ label: 'Content Bottom', value: insets.content.bottom },
{ label: '⚡ Total Top', value: insets.totalTop },
{ label: '⚡ Total Bottom', value: insets.totalBottom },
];
const handleCopy = async (label: string, value: any) => {
await navigator.clipboard.writeText(String(value));
setCopiedKey(label);
setTimeout(() => setCopiedKey(null), 1000);
};
const positionStyle: CSSProperties = {
'top-left': { top: 8, left: 8 },
'top-right': { top: 8, right: 8 },
'bottom-left': { bottom: 8, left: 8 },
'bottom-right': { bottom: 8, right: 8 },
}[position];
return createPortal(
<>
<button
onClick={() => setIsOpen(!isOpen)}
style={{
position: 'fixed',
...positionStyle,
zIndex: 100000,
width: 44,
height: 44,
borderRadius: '50%',
border: 'none',
backgroundColor: isOpen ? '#ef4444' : 'rgba(0,0,0,0.7)',
color: 'white',
fontSize: 20,
cursor: 'pointer',
}}
>
{isOpen ? '✕' : '🐛'}
</button>
{isOpen && (
<div
style={{
position: 'fixed',
...positionStyle,
[position.includes('right') ? 'right' : 'left']: 60,
zIndex: 99999,
backgroundColor: 'rgba(0,0,0,0.9)',
color: '#fff',
borderRadius: 12,
padding: 12,
fontSize: 12,
fontFamily: 'monospace',
minWidth: 180,
}}
>
<div style={{ fontWeight: 600, marginBottom: 8, opacity: 0.7 }}>TG DEBUG</div>
{values.map(({ label, value }) => (
<div
key={label}
onClick={() => handleCopy(label, value)}
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '4px 6px',
cursor: 'pointer',
backgroundColor: copiedKey === label ? 'rgba(34,197,94,0.3)' : 'transparent',
borderRadius: 4,
}}
>
<span style={{ opacity: 0.7 }}>{label}</span>
<span>{value}</span>
</div>
))}
<div style={{ marginTop: 8, opacity: 0.5, fontSize: 10, textAlign: 'center' }}>
Tap to copy
</div>
</div>
)}
</>,
document.body
);
}
// ============================================================================
// Modal (uses createPortal to fix position:fixed issue)
// ============================================================================
interface ModalProps {
children: ReactNode;
isOpen: boolean;
onClose: () => void;
zIndex?: number;
}
/**
* Modal that works in Telegram Mini Apps
* Uses createPortal to avoid position:fixed issues
*
* @example
* <Modal isOpen={show} onClose={() => setShow(false)}>
* <div>Modal content</div>
* </Modal>
*/
export function Modal({ children, isOpen, onClose, zIndex = 9999 }: ModalProps) {
const { totalBottom } = useSafeAreaInset();
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}
}, [isOpen]);
if (!isOpen) return null;
return createPortal(
<div
style={{
position: 'fixed',
inset: 0,
zIndex,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
paddingBottom: totalBottom,
}}
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
{children}
</div>,
document.body
);
}

View File

@ -0,0 +1,236 @@
/**
* Telegram Mini App React Hooks
* Copy-paste into your project
*/
import { useState, useEffect, useCallback } from 'react';
// ============================================================================
// Types
// ============================================================================
interface SafeAreaInset {
top: number;
bottom: number;
left: number;
right: number;
}
interface TelegramWebApp {
version: string;
platform: string;
isExpanded: boolean;
viewportHeight: number;
viewportStableHeight: number;
isFullscreen: boolean;
safeAreaInset: SafeAreaInset;
contentSafeAreaInset: SafeAreaInset;
themeParams: Record<string, string>;
colorScheme: 'light' | 'dark';
requestFullscreen: () => void;
exitFullscreen: () => void;
onEvent: (eventType: string, callback: () => void) => void;
offEvent: (eventType: string, callback: () => void) => void;
}
declare global {
interface Window {
Telegram?: { WebApp?: TelegramWebApp };
}
}
// ============================================================================
// Utilities
// ============================================================================
export function getWebApp(): TelegramWebApp | null {
if (typeof window === 'undefined') return null;
return window.Telegram?.WebApp ?? null;
}
export function isDevMode(): boolean {
if (typeof window === 'undefined') return false;
const url = new URL(window.location.href);
return ['localhost', '127.0.0.1'].includes(url.hostname) || url.searchParams.get('debug') === '1';
}
// ============================================================================
// useSafeAreaInset
// ============================================================================
export interface SafeAreaInsets {
device: SafeAreaInset;
content: SafeAreaInset;
totalTop: number;
totalBottom: number;
}
/**
* Reactive safe area insets - updates when Telegram sends events
*
* @example
* const { totalTop, totalBottom } = useSafeAreaInset();
* <div style={{ paddingTop: totalTop }}>Header</div>
*/
export function useSafeAreaInset(): SafeAreaInsets {
const [insets, setInsets] = useState<SafeAreaInsets>(() => {
const webApp = getWebApp();
const device = webApp?.safeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 };
const content = webApp?.contentSafeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 };
return {
device,
content,
totalTop: device.top + content.top,
totalBottom: device.bottom + content.bottom,
};
});
const updateInsets = useCallback(() => {
const webApp = getWebApp();
const device = webApp?.safeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 };
const content = webApp?.contentSafeAreaInset ?? { top: 0, bottom: 0, left: 0, right: 0 };
// Add minimum fallbacks for fullscreen mode
let totalTop = device.top + content.top;
if (webApp?.isFullscreen && totalTop < 80) {
totalTop = webApp.platform === 'ios' ? 100 : 80;
}
setInsets({
device,
content,
totalTop,
totalBottom: device.bottom + content.bottom,
});
}, []);
useEffect(() => {
const webApp = getWebApp();
if (!webApp) return;
updateInsets();
webApp.onEvent('safeAreaChanged', updateInsets);
webApp.onEvent('contentSafeAreaChanged', updateInsets);
webApp.onEvent('fullscreenChanged', updateInsets);
// Fallback: poll every 500ms (some events may not fire)
const interval = setInterval(updateInsets, 500);
return () => {
webApp.offEvent('safeAreaChanged', updateInsets);
webApp.offEvent('contentSafeAreaChanged', updateInsets);
webApp.offEvent('fullscreenChanged', updateInsets);
clearInterval(interval);
};
}, [updateInsets]);
return insets;
}
// ============================================================================
// useFullscreen
// ============================================================================
export interface FullscreenState {
isFullscreen: boolean;
isSupported: boolean;
requestFullscreen: () => void;
exitFullscreen: () => void;
toggleFullscreen: () => void;
}
/**
* Manage Telegram Mini App fullscreen mode (requires version 8.0+)
*
* @example
* const { isFullscreen, toggleFullscreen } = useFullscreen();
* <button onClick={toggleFullscreen}>{isFullscreen ? 'Exit' : 'Fullscreen'}</button>
*/
export function useFullscreen(): FullscreenState {
const [isFullscreen, setIsFullscreen] = useState(() => {
return getWebApp()?.isFullscreen ?? false;
});
const isSupported = (() => {
const webApp = getWebApp();
if (!webApp) return false;
return parseFloat(webApp.version) >= 8.0;
})();
const requestFullscreen = useCallback(() => {
getWebApp()?.requestFullscreen?.();
}, []);
const exitFullscreen = useCallback(() => {
getWebApp()?.exitFullscreen?.();
}, []);
const toggleFullscreen = useCallback(() => {
if (isFullscreen) exitFullscreen();
else requestFullscreen();
}, [isFullscreen, requestFullscreen, exitFullscreen]);
useEffect(() => {
const webApp = getWebApp();
if (!webApp) return;
const handleChange = () => setIsFullscreen(webApp.isFullscreen);
webApp.onEvent('fullscreenChanged', handleChange);
webApp.onEvent('fullscreenFailed', handleChange);
return () => {
webApp.offEvent('fullscreenChanged', handleChange);
webApp.offEvent('fullscreenFailed', handleChange);
};
}, []);
return { isFullscreen, isSupported, requestFullscreen, exitFullscreen, toggleFullscreen };
}
// ============================================================================
// useTelegramTheme
// ============================================================================
export interface TelegramTheme {
params: Record<string, string>;
colorScheme: 'light' | 'dark';
isDark: boolean;
}
/**
* Reactive Telegram theme params
*
* @example
* const { params, isDark } = useTelegramTheme();
* <div style={{ background: params.bg_color }}>...</div>
*/
export function useTelegramTheme(): TelegramTheme {
const [theme, setTheme] = useState<TelegramTheme>(() => {
const webApp = getWebApp();
return {
params: webApp?.themeParams ?? {},
colorScheme: webApp?.colorScheme ?? 'light',
isDark: webApp?.colorScheme === 'dark',
};
});
useEffect(() => {
const webApp = getWebApp();
if (!webApp) return;
const handleChange = () => {
setTheme({
params: webApp.themeParams,
colorScheme: webApp.colorScheme,
isDark: webApp.colorScheme === 'dark',
});
};
webApp.onEvent('themeChanged', handleChange);
return () => webApp.offEvent('themeChanged', handleChange);
}, []);
return theme;
}

388
hermes/uncaged-dev/SKILL.md Normal file
View File

@ -0,0 +1,388 @@
---
name: uncaged-dev
version: 1.0.0
description: >
Uncaged 项目的完整开发 skill。涵盖:项目架构、开发流程、build/deploy、
测试验证、调试排查。装这一个 skill 就获得所有 Uncaged 开发能力。
metadata:
requiredTools: ["secret", "gh"]
---
# Uncaged Dev
Uncaged 项目开发的一站式 skill。
## 项目概览
**Uncaged** — Sigil-native AI Agent 平台,运行在 Cloudflare Workers 上。
```
uncaged/
├── packages/
│ ├── core/ # 共享核心:LLM、Memory、Sigil、Tool Registry
│ ├── worker/ # CF Worker(后端 API + routing)
│ ├── web/ # React 19 + Tailwind v4 前端(SPA)
│ └── runner/ # Runner 客户端(连设备到 Agent)
├── tests/e2e/ # 场景化测试用例
├── scripts/ # 部署脚本
└── .github/workflows/ # CI/CD
```
**仓库:** `oc-xiaoju/uncaged`
**线上:** `https://uncaged.shazhou.work`
**CI/CD:** push main → GitHub Actions 自动部署
## 开发流程
**原则:Issue 驱动,协调者不写代码,Cursor Agent 干活。**
```
需求/Bug → 开 Issue → 创建分支 → Cursor Agent 编码 →
验证(build + diff)→ Commit(closes #N)→ 合并 main → 自动部署
```
### 1. 开 Issue
```bash
gh issue create --repo oc-xiaoju/uncaged \
--title "fix/feat: 简短描述" \
--body "## Problem\n...\n## Plan\n...\n## Acceptance\n..."
```
### 2. 创建分支
```bash
cd <uncaged-repo>
git checkout main && git pull
git checkout -b fix/descriptive-name # 或 feat/
```
### 3. 用 Cursor Agent 编码
```bash
CURSOR_API_KEY="$(secret get CURSOR_API_KEY)" \
bash ~/.openclaw/workspace/skills/cursor-agent-cn/scripts/run.sh \
<uncaged-repo> "<任务描述>" auto ask # 先 review
# 确认后
... auto write # 再 apply
```
中国区必须用 `auto` model。两步走:先 `ask` review,再 `write` apply。
### 4. 验证
```bash
# Build(三步)
cd packages/core && rm -rf dist && npx tsc
cd ../web && npm run build
cd ../worker && npx tsc --noEmit
# Diff 检查
git diff --stat
```
### 5. 提交 + 合并
```bash
git add -A
git commit -m "fix: description (closes #N)"
git checkout main && git merge <branch> --no-ff
git push origin main # 触发 CI/CD 自动部署
```
## Build & Deploy
### 线上(自动)
Push main → GitHub Actions → build core → build web → wrangler deploy
### 开发环境(手动)
每人一个独立 Worker,互不影响:
```bash
bash scripts/deploy-dev.sh xingyue # → uncaged-xingyue.shazhou.work
bash scripts/deploy-dev.sh xiaoju # → uncaged-xiaoju.shazhou.work
bash scripts/deploy-dev.sh xiaomooo # → uncaged-xiaomooo.shazhou.work
bash scripts/deploy-dev.sh aobing # → uncaged-aobing.shazhou.work
```
脚本自动:build core → build web → wrangler deploy --env <name>
### 手动部署线上(紧急)
```bash
cd packages/core && rm -rf dist && npx tsc
cd ../web && npm run build
cd ../worker
CLOUDFLARE_API_TOKEN="$(secret get CLOUDFLARE_API_TOKEN)" \
CLOUDFLARE_ACCOUNT_ID="$(secret get CLOUDFLARE_ACCOUNT_ID)" \
npx wrangler deploy
```
### 常见 Build 问题
| 问题 | 原因 | 解决 |
|:-----|:-----|:-----|
| core dist 为空 | tsconfig 缺 `noEmitOnError: false` | 检查 `packages/core/tsconfig.json` |
| wrangler can't resolve @uncaged/core/* | core 没 build | 先 `cd packages/core && npx tsc` |
| POST 请求返回 SPA HTML | wrangler.toml 缺 `run_worker_first = true` | 检查 `[assets]` 配置 |
| 路由 404 | `normalizeApiPath` strip 了 `/api/v1/` | 路由匹配用 strip 后路径 |
## 测试
### 测试分层
| 层级 | 工具 | 位置 | 用途 |
|:-----|:-----|:-----|:-----|
| **Unit Test** | Vitest | `packages/*/src/*.test.ts` | 纯逻辑,mock 外部依赖,秒级 |
| **UI 组件测试** | Vitest + @testing-library/react | `packages/web/src/**/*.test.tsx` | React 组件交互,mock fetch |
| **E2E API** | curl 脚本 | `tests/e2e/scripts/run-tests.sh` | 真实 HTTP 请求打线上 API |
| **E2E UI** | Playwright | `tests/e2e/ui/*.spec.ts` | 浏览器自动化,端到端流程 |
**跑测试的顺序:** UT → UI 组件 → build → E2E(先快后慢,先本地后线上)
### Unit Test
```bash
# 跑全部 UT
cd packages/core && npx vitest run
cd packages/worker && npx vitest run
cd packages/web && npx vitest run # UI 组件测试也在这
# 跑单个文件
npx vitest run src/agent-loop.test.ts
# Watch 模式(开发时)
npx vitest --watch
```
**写 UT 的规范:**
- 文件放在被测源码旁边:`foo.ts``foo.test.ts`
- Mock 外部依赖(KV、D1、fetch),参考 `core/src/__mocks__/cf-bindings.ts`
- 测试文件不要 import 真实 Cloudflare bindings
- `describe` 按功能分组,`test` 名字说清楚测的是什么行为
- 验收:`npx vitest run` 全绿 + 无 type error
**现有 UT 覆盖(13 文件 134 tests):**
- `agent-loop` — loop 流程、tool call、maxRounds、nudge、streaming
- `pipeline` — model 选择、context 压缩、orphan tool 清理
- `baton` — 创建、状态流转、子 baton 事件
- `tool-registry` — 注册、查询、user-invocable 过滤
- `chat-store` + `chat-store-kv` — 消息存储/读取
- `identity` / `soul` / `codegen` / `embedding` / `chat-key` / `short-id` / `slug-resolver`
### UI 组件测试
```bash
cd packages/web && npx vitest run
```
**写 UI 组件测试的规范:**
- 用 `@testing-library/react` + `jsdom` 环境
- 文件放在组件旁边:`chat-input.tsx``chat-input.test.tsx`
- Mock `fetch` / `authedFetch`,不打真实 API
- 优先测用户交互(输入、点击、键盘事件)和渲染结果
- 使用 `data-testid`#71 已添加)定位元素
- 不测样式/CSS,测行为和 DOM 结构
### Playwright UI 测试(9 个用例,28 秒)
```bash
cd <uncaged-repo>
npx playwright test # 跑全部
npx playwright test -g "tool search" # 跑单个
npx playwright test --reporter=html # 生成 HTML 报告
```
覆盖:token 登录、聊天发消息、工具搜索浮层、ESC 关闭、主题切换、JS 错误检测、手机端布局。
**首次使用需安装浏览器:** `npx playwright install chromium`
**指定目标环境:** `UNCAGED_URL=https://uncaged-xingyue.shazhou.work npx playwright test`
**指定 token:** `TOKEN_NAME=UNCAGED_AGENT_TOKEN_XIAOJU npx playwright test`
### API 回归(12 个用例,curl 脚本)
```bash
bash tests/e2e/scripts/run-tests.sh UNCAGED_AGENT_TOKEN_XINGYUE
```
### 场景验证(给 subagent 用)
场景文件在 `tests/e2e/scenes/`
| 场景 | 文件 |
|:-----|:-----|
| 认证 | `scenes/auth.md` |
| 聊天 | `scenes/chat.md` |
| 流式 | `scenes/streaming.md` |
| Tool Gateway | `scenes/tool-gateway.md` |
| 工具搜索 | `scenes/tool-search.md` |
| 异常处理 | `scenes/error-handling.md` |
派 subagent 验证:
```
<uncaged-repo>/tests/e2e/scenes/tool-gateway.md,按描述验证。
失败了收集 logs 开 bug issue。
```
### Token 登录(测试用)
```bash
TOKEN=$(secret get UNCAGED_AGENT_TOKEN_XINGYUE)
curl -s -X POST "https://uncaged.shazhou.work/auth/token" \
-H "Content-Type: application/json" \
-d "{\"token\": \"$TOKEN\"}" \
-c /tmp/uncaged-cookies.txt
```
### uncaged-cli 工具
**安装:**
```bash
npm install -g @uncaged/cli # 官方版本
# 或
npm install -g uncaged-cli # 备用版本
```
**基本用法:**
```bash
# 初始化配置
uncaged init
# 登录(自动检测 JWT token 或生成 Admin token)
uncaged login
# 环境状态检查
uncaged status
# 数据库查询
uncaged db users --limit 10
uncaged db messages --user scott --channel web --limit 20
uncaged db agents --format table
uncaged db context-boundaries --user scott --channel telegram
# API 调试
uncaged api chat "hello world"
uncaged api history --user scott --channel web --limit 10
uncaged api debug overview
uncaged api debug stats
uncaged api debug capabilities
# 测试用户管理(Admin 权限)
uncaged admin create-user test-user-123
uncaged admin delete-user test-user-123 --cascade
uncaged admin create-token test-user-123
# Sigil 操作
uncaged sigil list
uncaged sigil inspect <capability-name>
# 配置管理
uncaged config show
uncaged config set endpoint https://uncaged-dev.shazhou.work
uncaged config set auth.method admin
```
**多环境切换:**
```bash
# 开发环境
uncaged config set endpoint https://uncaged-xingyue.shazhou.work
uncaged config set auth.method jwt
uncaged config set auth.token "$(secret get UNCAGED_AGENT_TOKEN_XINGYUE)"
# 生产环境
uncaged config set endpoint https://uncaged.shazhou.work
uncaged config set auth.method admin
uncaged config set auth.admin_token "$(secret get UNCAGED_ADMIN_TOKEN)"
# 查看当前配置
uncaged status
```
**Channel 概念:**
- `web` — 网页聊天
- `telegram` — Telegram 消息
- `api` — 直接 API 调用
所有 `db``api` 命令都支持 `--channel` 过滤。
## 调试
### Worker 日志
```bash
CF_TOKEN=$(secret get CLOUDFLARE_API_TOKEN)
CF_ACCOUNT=$(secret get CLOUDFLARE_ACCOUNT_ID)
cd <uncaged-repo>/packages/worker
CLOUDFLARE_API_TOKEN="$CF_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT" \
npx wrangler tail --format pretty
```
### D1 查询
```bash
CLOUDFLARE_API_TOKEN="$CF_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT" \
npx wrangler d1 execute uncaged-memory --remote --json \
--command "SELECT * FROM users LIMIT 10;"
```
### 前端状态
```bash
# Session
curl -s https://uncaged.shazhou.work/auth/session -b /tmp/uncaged-cookies.txt | python3 -m json.tool
# History
curl -s https://uncaged.shazhou.work/scott/doudou/api/history -b /tmp/uncaged-cookies.txt | python3 -c "
import sys,json; [print(f' [{m[\"role\"]}] {str(m.get(\"content\",\"\"))[:80]}') for m in json.load(sys.stdin)['history'][-5:]]"
```
## 架构速查
### API
| 端点 | 方法 | 说明 |
|:-----|:-----|:-----|
| `/auth/token` | POST | Token 登录 |
| `/auth/session` | GET | Session 检查 |
| `/:o/:a/api/v1/chat` | POST | 发消息 |
| `/:o/:a/api/v1/chat/stream` | POST | SSE 流式 |
| `/:o/:a/api/v1/history` | GET | 聊天历史 |
| `/:o/:a/api/v1/clear` | POST | 清空历史 |
| `/:o/:a/api/v1/tools/builtin` | GET | Builtin 工具列表 |
| `/:o/:a/api/v1/tools/:slug/invoke` | POST | 直接调用工具 |
### 关键文件
| 文件 | 作用 |
|:-----|:-----|
| `core/src/llm/tool-registry.ts` | **SSOT** — 所有 builtin tool 定义 |
| `core/src/llm/agent-loop.ts` | LLM agent loop + tool execution |
| `worker/src/index.ts` | 主路由 + Worker entry |
| `worker/src/services/capability-service.ts` | Tool Gateway 执行层 |
| `web/src/hooks/use-chat.ts` | 前端聊天状态 |
| `web/src/components/chat/chat-input.tsx` | 输入框 + 工具搜索 |
| `web/src/components/chat/message-bubble.tsx` | 消息气泡渲染 |
| `worker/wrangler.toml` | CF 配置(bindings + routes + envs) |
### 路由注意事项
- 所有前端 API 调用统一使用 `/api/v1/` 前缀(#73 已清理)
- `normalizeApiPath` 只 strip `/api/v1/` 前缀(不再处理 `/api/`
- 新路由要用 strip 后的路径匹配(如 `/tools/:slug/invoke` 不是 `/api/v1/tools/...`
- `[assets] run_worker_first = true` 确保 POST 请求到 Worker
- 没有 `webEnabled``hasBearer``channels/web.ts`#73 已删除)
### Tool Registry(SSOT)
添加新 builtin tool 只需改 `core/src/llm/tool-registry.ts``TOOL_REGISTRY` 数组,LLM / Gateway / 前端自动同步。
## Secrets
```bash
secret get CLOUDFLARE_API_TOKEN # CF 部署
secret get CLOUDFLARE_ACCOUNT_ID # CF 账户
secret get CURSOR_API_KEY # Cursor Agent
secret get UNCAGED_AGENT_TOKEN_XINGYUE # 测试登录 token
```

View File

@ -0,0 +1,149 @@
---
name: uncaged-test
version: 3.1.0
description: >
Uncaged Web UI 场景化测试。场景文件在 uncaged 代码库的 tests/e2e/scenes/,
subagent 按场景描述自主操作验证。失败时收集 logs 并开 bug issue。
metadata:
requiredTools: ["secret"]
---
# Uncaged Test
场景化 E2E 测试,设计给 subagent 自主执行。
**场景文件在代码库:** `<uncaged-repo>/tests/e2e/scenes/`
**回归脚本在代码库:** `<uncaged-repo>/tests/e2e/scripts/run-tests.sh`
## 使用方式
### 验证单个场景
```
<uncaged-repo>/tests/e2e/scenes/<场景>.md,按描述验证。失败了收集 logs 开 bug。
```
### 跑全部场景(快速回归)
```bash
bash <uncaged-repo>/tests/e2e/scripts/run-tests.sh [TOKEN_NAME]
```
### 可用场景
| 场景文件 | 说明 |
|:---------|:-----|
| `scenes/auth.md` | 认证:token 登录、session、refresh |
| `scenes/chat.md` | 聊天:发消息、历史、清空 |
| `scenes/streaming.md` | SSE 流式响应 |
| `scenes/tool-gateway.md` | Tool Gateway:builtin 列表、invoke |
| `scenes/tool-search.md` | 输入框工具搜索(本地过滤) |
| `scenes/error-handling.md` | 异常处理:401、404、429 |
## 环境准备
### 登录获取 cookies
```bash
TOKEN=$(secret get UNCAGED_AGENT_TOKEN_XINGYUE)
curl -s -X POST "https://uncaged.shazhou.work/auth/token" \
-H "Content-Type: application/json" \
-d "{\"token\": \"$TOKEN\"}" \
-c /tmp/uncaged-cookies.txt
```
### 常量
```
BASE_URL = https://uncaged.shazhou.work
AGENT_PATH = /scott/doudou
COOKIES = /tmp/uncaged-cookies.txt
REPO = oc-xiaoju/uncaged
```
## 失败处理
### 1. 收集 Worker 日志
```bash
CF_TOKEN=$(secret get CLOUDFLARE_API_TOKEN)
CF_ACCOUNT=$(secret get CLOUDFLARE_ACCOUNT_ID)
cd <uncaged-repo>/packages/worker
CLOUDFLARE_API_TOKEN="$CF_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT" \
npx wrangler tail --format json > /tmp/worker-logs.json 2>/dev/null &
sleep 3
# 复现失败请求
# ...
sleep 10; kill %1 2>/dev/null
python3 -c "
import sys, json
for line in open('/tmp/worker-logs.json'):
try:
e = json.loads(line.strip())
logs, excs = e.get('logs',[]), e.get('exceptions',[])
status = e.get('event',{}).get('response',{}).get('status','?')
if logs or excs or (isinstance(status,int) and status >= 400):
url = e.get('event',{}).get('request',{}).get('url','?')
print(f'[{status}] {url}')
for l in logs: print(f' LOG: {l.get(\"message\",l)}')
for x in excs: print(f' ERR: {x.get(\"message\",x)}')
except: pass
"
```
### 2. 收集前端状态
```bash
curl -s https://uncaged.shazhou.work/auth/session -b /tmp/uncaged-cookies.txt | python3 -m json.tool
curl -s https://uncaged.shazhou.work/scott/doudou/api/history -b /tmp/uncaged-cookies.txt | python3 -c "
import sys,json
for m in json.load(sys.stdin).get('history',[])[-3:]:
print(f' [{m[\"role\"]}] {str(m.get(\"content\",\"\"))[:100]}')
"
```
### 3. 开 Bug Issue
```bash
gh issue create --repo oc-xiaoju/uncaged \
--title "bug: <简短描述>" \
--body "## Bug Report
### 场景
<场景文件 + 步骤>
### 期望
<应该发生什么>
### 实际
<实际发生了什么>
### 复现
\`\`\`bash
<curl 命令>
\`\`\`
### Worker 日志
\`\`\`
<日志>
\`\`\`
---
*Auto-generated by uncaged-test skill*" \
--label "bug"
```
## API 速查
| 端点 | 方法 | 说明 |
|:-----|:-----|:-----|
| `/auth/token` | POST | Token 登录 |
| `/auth/session` | GET | 检查 session |
| `/auth/refresh` | POST | 刷新 token |
| `/:o/:a/api/chat` | POST | 发消息(⚠️ 不是 /api/v1/chat) |
| `/:o/:a/api/chat/stream` | POST | SSE 流式 |
| `/:o/:a/api/history` | GET | 历史记录 |
| `/:o/:a/api/v1/tools/builtin` | GET | Builtin 工具列表 |
| `/:o/:a/api/v1/tools/:slug/invoke` | POST | 直接调用工具 |