418 lines
13 KiB
Markdown
418 lines
13 KiB
Markdown
|
|
# HTML Presentation Template
|
||
|
|
|
||
|
|
Reference architecture for generating slide presentations. Every presentation follows this structure.
|
||
|
|
|
||
|
|
## Base HTML Structure
|
||
|
|
|
||
|
|
```html
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="zh-CN">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>Presentation Title</title>
|
||
|
|
|
||
|
|
<!-- Fonts: Google Fonts with Chinese font families -->
|
||
|
|
<!-- Always include at least one Chinese font (Noto Sans SC / Noto Serif SC / LXGW WenKai) -->
|
||
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
|
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||
|
|
|
||
|
|
<style>
|
||
|
|
/* ===========================================
|
||
|
|
CSS CUSTOM PROPERTIES (THEME)
|
||
|
|
Change these to change the whole look
|
||
|
|
=========================================== */
|
||
|
|
:root {
|
||
|
|
/* Colors — from chosen style preset */
|
||
|
|
--bg-primary: #0a0f1c;
|
||
|
|
--bg-secondary: #111827;
|
||
|
|
--text-primary: #ffffff;
|
||
|
|
--text-secondary: #9ca3af;
|
||
|
|
--accent: #00ffcc;
|
||
|
|
--accent-glow: rgba(0, 255, 204, 0.3);
|
||
|
|
|
||
|
|
/* Typography — MUST use clamp() */
|
||
|
|
--font-display: 'Clash Display', sans-serif;
|
||
|
|
--font-body: 'Satoshi', sans-serif;
|
||
|
|
--title-size: clamp(2rem, 6vw, 5rem);
|
||
|
|
--subtitle-size: clamp(0.875rem, 2vw, 1.25rem);
|
||
|
|
--body-size: clamp(0.75rem, 1.2vw, 1rem);
|
||
|
|
|
||
|
|
/* Spacing — MUST use clamp() */
|
||
|
|
--slide-padding: clamp(1.5rem, 4vw, 4rem);
|
||
|
|
--content-gap: clamp(1rem, 2vw, 2rem);
|
||
|
|
|
||
|
|
/* Animation */
|
||
|
|
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||
|
|
--duration-normal: 0.6s;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ===========================================
|
||
|
|
BASE STYLES
|
||
|
|
=========================================== */
|
||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
|
|
||
|
|
/* --- PASTE viewport-base.css CONTENTS HERE --- */
|
||
|
|
|
||
|
|
/* ===========================================
|
||
|
|
ANIMATIONS
|
||
|
|
Trigger via .visible class (added by JS on scroll)
|
||
|
|
=========================================== */
|
||
|
|
.reveal {
|
||
|
|
opacity: 0;
|
||
|
|
transform: translateY(30px);
|
||
|
|
transition: opacity var(--duration-normal) var(--ease-out-expo),
|
||
|
|
transform var(--duration-normal) var(--ease-out-expo);
|
||
|
|
}
|
||
|
|
|
||
|
|
.slide.visible .reveal {
|
||
|
|
opacity: 1;
|
||
|
|
transform: translateY(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Stagger children for sequential reveal */
|
||
|
|
.reveal:nth-child(1) { transition-delay: 0.1s; }
|
||
|
|
.reveal:nth-child(2) { transition-delay: 0.2s; }
|
||
|
|
.reveal:nth-child(3) { transition-delay: 0.3s; }
|
||
|
|
.reveal:nth-child(4) { transition-delay: 0.4s; }
|
||
|
|
|
||
|
|
/* ... preset-specific styles ... */
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<!-- Optional: Progress bar -->
|
||
|
|
<div class="progress-bar"></div>
|
||
|
|
|
||
|
|
<!-- Optional: Navigation dots -->
|
||
|
|
<nav class="nav-dots"><!-- Generated by JS --></nav>
|
||
|
|
|
||
|
|
<!-- Slides -->
|
||
|
|
<section class="slide title-slide">
|
||
|
|
<h1 class="reveal">Presentation Title</h1>
|
||
|
|
<p class="reveal">Subtitle or author</p>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="slide">
|
||
|
|
<div class="slide-content">
|
||
|
|
<h2 class="reveal">Slide Title</h2>
|
||
|
|
<p class="reveal">Content...</p>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<!-- More slides... -->
|
||
|
|
|
||
|
|
<script>
|
||
|
|
/* ===========================================
|
||
|
|
SLIDE PRESENTATION CONTROLLER
|
||
|
|
=========================================== */
|
||
|
|
class SlidePresentation {
|
||
|
|
constructor() {
|
||
|
|
this.slides = document.querySelectorAll('.slide');
|
||
|
|
this.currentSlide = 0;
|
||
|
|
this.setupIntersectionObserver();
|
||
|
|
this.setupKeyboardNav();
|
||
|
|
this.setupTouchNav();
|
||
|
|
this.setupProgressBar();
|
||
|
|
this.setupNavDots();
|
||
|
|
}
|
||
|
|
|
||
|
|
setupIntersectionObserver() {
|
||
|
|
// Add .visible class when slides enter viewport
|
||
|
|
// Triggers CSS animations efficiently
|
||
|
|
}
|
||
|
|
|
||
|
|
setupKeyboardNav() {
|
||
|
|
// Arrow keys, Space, Page Up/Down
|
||
|
|
}
|
||
|
|
|
||
|
|
setupTouchNav() {
|
||
|
|
// Touch/swipe support for mobile
|
||
|
|
}
|
||
|
|
|
||
|
|
setupProgressBar() {
|
||
|
|
// Update progress bar on scroll
|
||
|
|
}
|
||
|
|
|
||
|
|
setupNavDots() {
|
||
|
|
// Generate and manage navigation dots
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
new SlidePresentation();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Required JavaScript Features
|
||
|
|
|
||
|
|
Every presentation must include:
|
||
|
|
|
||
|
|
1. **SlidePresentation Class** — Main controller with:
|
||
|
|
- Keyboard navigation (arrows, space, page up/down)
|
||
|
|
- Touch/swipe support
|
||
|
|
- Mouse wheel navigation
|
||
|
|
- Progress bar updates
|
||
|
|
- Navigation dots
|
||
|
|
|
||
|
|
2. **Intersection Observer** — For scroll-triggered animations:
|
||
|
|
- Add `.visible` class when slides enter viewport
|
||
|
|
- Trigger CSS transitions efficiently
|
||
|
|
|
||
|
|
3. **Optional Enhancements** (match to chosen style):
|
||
|
|
- Custom cursor with trail
|
||
|
|
- Particle system background (canvas)
|
||
|
|
- Parallax effects
|
||
|
|
- 3D tilt on hover
|
||
|
|
- Magnetic buttons
|
||
|
|
- Counter animations
|
||
|
|
|
||
|
|
4. **Inline Editing** (only if user opted in during Phase 1 — skip entirely if they said No):
|
||
|
|
- Edit toggle button (hidden by default, revealed via hover hotzone or `E` key)
|
||
|
|
- Auto-save to localStorage
|
||
|
|
- Export/save file functionality
|
||
|
|
- See "Inline Editing Implementation" section below
|
||
|
|
|
||
|
|
## Inline Editing Implementation (Opt-In Only)
|
||
|
|
|
||
|
|
**If the user chose "No" for inline editing in Phase 1, do NOT generate any edit-related HTML, CSS, or JS.**
|
||
|
|
|
||
|
|
**Do NOT use CSS `~` sibling selector for hover-based show/hide.** The CSS-only approach (`edit-hotzone:hover ~ .edit-toggle`) fails because `pointer-events: none` on the toggle button breaks the hover chain: user hovers hotzone -> button becomes visible -> mouse moves toward button -> leaves hotzone -> button disappears before click.
|
||
|
|
|
||
|
|
**Required approach: JS-based hover with 400ms delay timeout.**
|
||
|
|
|
||
|
|
HTML:
|
||
|
|
```html
|
||
|
|
<div class="edit-hotzone"></div>
|
||
|
|
<button class="edit-toggle" id="editToggle" title="Edit mode (E)">
|
||
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M12.1 1.3a1.5 1.5 0 0 1 2.1 2.1L5.6 12 2 14l2-3.6 8.1-9.1z"/></svg>
|
||
|
|
</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
CSS (visibility controlled by JS classes only):
|
||
|
|
```css
|
||
|
|
/* Do NOT use CSS ~ sibling selector for this!
|
||
|
|
pointer-events: none breaks the hover chain.
|
||
|
|
Must use JS with delay timeout. */
|
||
|
|
.edit-hotzone {
|
||
|
|
position: fixed; top: 0; left: 0;
|
||
|
|
width: 80px; height: 80px;
|
||
|
|
z-index: 10000;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.edit-toggle {
|
||
|
|
opacity: 0;
|
||
|
|
pointer-events: none;
|
||
|
|
transition: opacity 0.3s ease;
|
||
|
|
z-index: 10001;
|
||
|
|
}
|
||
|
|
.edit-toggle.show,
|
||
|
|
.edit-toggle.active {
|
||
|
|
opacity: 1;
|
||
|
|
pointer-events: auto;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
JS (three interaction methods):
|
||
|
|
```javascript
|
||
|
|
// 1. Click handler on the toggle button
|
||
|
|
document.getElementById('editToggle').addEventListener('click', () => {
|
||
|
|
editor.toggleEditMode();
|
||
|
|
});
|
||
|
|
|
||
|
|
// 2. Hotzone hover with 400ms grace period
|
||
|
|
const hotzone = document.querySelector('.edit-hotzone');
|
||
|
|
const editToggle = document.getElementById('editToggle');
|
||
|
|
let hideTimeout = null;
|
||
|
|
|
||
|
|
hotzone.addEventListener('mouseenter', () => {
|
||
|
|
clearTimeout(hideTimeout);
|
||
|
|
editToggle.classList.add('show');
|
||
|
|
});
|
||
|
|
hotzone.addEventListener('mouseleave', () => {
|
||
|
|
hideTimeout = setTimeout(() => {
|
||
|
|
if (!editor.isActive) editToggle.classList.remove('show');
|
||
|
|
}, 400);
|
||
|
|
});
|
||
|
|
editToggle.addEventListener('mouseenter', () => {
|
||
|
|
clearTimeout(hideTimeout);
|
||
|
|
});
|
||
|
|
editToggle.addEventListener('mouseleave', () => {
|
||
|
|
hideTimeout = setTimeout(() => {
|
||
|
|
if (!editor.isActive) editToggle.classList.remove('show');
|
||
|
|
}, 400);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 3. Hotzone direct click
|
||
|
|
hotzone.addEventListener('click', () => {
|
||
|
|
editor.toggleEditMode();
|
||
|
|
});
|
||
|
|
|
||
|
|
// 4. Keyboard shortcut (E key, skip when editing text)
|
||
|
|
document.addEventListener('keydown', (e) => {
|
||
|
|
if ((e.key === 'e' || e.key === 'E') && !e.target.getAttribute('contenteditable')) {
|
||
|
|
editor.toggleEditMode();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Image Pipeline (Skip If No Images)
|
||
|
|
|
||
|
|
If user chose "No images" in Phase 1, skip this entirely. If images were provided, process them before generating HTML.
|
||
|
|
|
||
|
|
**Dependency:** `pip install Pillow`
|
||
|
|
|
||
|
|
### Image Processing
|
||
|
|
|
||
|
|
```python
|
||
|
|
from PIL import Image, ImageDraw
|
||
|
|
|
||
|
|
# Circular crop (for logos on modern/clean styles)
|
||
|
|
def crop_circle(input_path, output_path):
|
||
|
|
img = Image.open(input_path).convert('RGBA')
|
||
|
|
w, h = img.size
|
||
|
|
size = min(w, h)
|
||
|
|
left, top = (w - size) // 2, (h - size) // 2
|
||
|
|
img = img.crop((left, top, left + size, top + size))
|
||
|
|
mask = Image.new('L', (size, size), 0)
|
||
|
|
ImageDraw.Draw(mask).ellipse([0, 0, size, size], fill=255)
|
||
|
|
img.putalpha(mask)
|
||
|
|
img.save(output_path, 'PNG')
|
||
|
|
|
||
|
|
# Resize (for oversized images that inflate HTML)
|
||
|
|
def resize_max(input_path, output_path, max_dim=1200):
|
||
|
|
img = Image.open(input_path)
|
||
|
|
img.thumbnail((max_dim, max_dim), Image.LANCZOS)
|
||
|
|
img.save(output_path, quality=85)
|
||
|
|
```
|
||
|
|
|
||
|
|
| Situation | Operation |
|
||
|
|
|-----------|-----------|
|
||
|
|
| Square logo on rounded aesthetic | `crop_circle()` |
|
||
|
|
| Image > 1MB | `resize_max(max_dim=1200)` |
|
||
|
|
| Wrong aspect ratio | Manual crop with `img.crop()` |
|
||
|
|
|
||
|
|
Save processed images with `_processed` suffix. Never overwrite originals.
|
||
|
|
|
||
|
|
### Image Placement
|
||
|
|
|
||
|
|
**Use direct file paths** (not base64) — presentations are viewed locally:
|
||
|
|
|
||
|
|
```html
|
||
|
|
<img src="assets/logo_round.png" alt="Logo" class="slide-image logo">
|
||
|
|
<img src="assets/screenshot.png" alt="Screenshot" class="slide-image screenshot">
|
||
|
|
```
|
||
|
|
|
||
|
|
```css
|
||
|
|
.slide-image {
|
||
|
|
max-width: 100%;
|
||
|
|
max-height: min(50vh, 400px);
|
||
|
|
object-fit: contain;
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
.slide-image.screenshot {
|
||
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
|
|
border-radius: 12px;
|
||
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
|
|
}
|
||
|
|
.slide-image.logo {
|
||
|
|
max-height: min(30vh, 200px);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Adapt border/shadow colors to match the chosen style's accent.** Never repeat the same image on multiple slides (except logos on title + closing).
|
||
|
|
|
||
|
|
**Placement patterns:** Logo centered on title slide. Screenshots in two-column layouts with text. Full-bleed images as slide backgrounds with text overlay (use sparingly).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Financial Data Integration
|
||
|
|
|
||
|
|
When the presentation includes financial data (charts, KPIs, tables), add these utilities:
|
||
|
|
|
||
|
|
### Chinese Number Formatting (Required for Financial Slides)
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
/* Include in <script> when any slide contains financial data */
|
||
|
|
function formatCN(num, decimals = 1) {
|
||
|
|
if (num === null || num === undefined) return '--';
|
||
|
|
const abs = Math.abs(num);
|
||
|
|
const sign = num < 0 ? '-' : '';
|
||
|
|
if (abs >= 1e8) return sign + (abs / 1e8).toFixed(decimals).replace(/\.0$/, '') + '\u4ebf';
|
||
|
|
if (abs >= 1e4) return sign + (abs / 1e4).toFixed(decimals).replace(/\.0$/, '') + '\u4e07';
|
||
|
|
return sign + abs.toLocaleString('zh-CN');
|
||
|
|
}
|
||
|
|
function formatPct(num, decimals = 2) {
|
||
|
|
if (num === null || num === undefined) return '--';
|
||
|
|
return (num * 100).toFixed(decimals) + '%';
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### ECharts Integration (Optional)
|
||
|
|
|
||
|
|
```html
|
||
|
|
<!-- Add before </body> when charts are needed -->
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||
|
|
```
|
||
|
|
|
||
|
|
Chart containers must init on slide visibility. See [financial-charts.md](financial-charts.md) for complete configuration reference.
|
||
|
|
|
||
|
|
### Trend Indicators (CSS Only, No Emoji)
|
||
|
|
|
||
|
|
```css
|
||
|
|
/* Up/down arrows as CSS pseudo-elements — never use emoji */
|
||
|
|
.trend-up::before {
|
||
|
|
content: '';
|
||
|
|
display: inline-block;
|
||
|
|
width: 0; height: 0;
|
||
|
|
border-left: 4px solid transparent;
|
||
|
|
border-right: 4px solid transparent;
|
||
|
|
border-bottom: 6px solid var(--color-positive, #dc2626);
|
||
|
|
margin-right: 0.3em;
|
||
|
|
vertical-align: middle;
|
||
|
|
}
|
||
|
|
.trend-down::before {
|
||
|
|
content: '';
|
||
|
|
display: inline-block;
|
||
|
|
width: 0; height: 0;
|
||
|
|
border-left: 4px solid transparent;
|
||
|
|
border-right: 4px solid transparent;
|
||
|
|
border-top: 6px solid var(--color-negative, #16a34a);
|
||
|
|
margin-right: 0.3em;
|
||
|
|
vertical-align: middle;
|
||
|
|
}
|
||
|
|
.trend-flat::before {
|
||
|
|
content: '--';
|
||
|
|
color: var(--text-secondary);
|
||
|
|
margin-right: 0.3em;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Code Quality
|
||
|
|
|
||
|
|
**Comments:** Every section needs clear comments explaining what it does and how to modify it.
|
||
|
|
|
||
|
|
**Accessibility:**
|
||
|
|
- Semantic HTML (`<section>`, `<nav>`, `<main>`)
|
||
|
|
- Keyboard navigation works fully
|
||
|
|
- ARIA labels where needed
|
||
|
|
- `prefers-reduced-motion` support (included in viewport-base.css)
|
||
|
|
- `lang="zh-CN"` on `<html>` for Chinese presentations
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
Single presentations:
|
||
|
|
```
|
||
|
|
presentation.html # Self-contained, all CSS/JS inline
|
||
|
|
assets/ # Images only, if any
|
||
|
|
```
|
||
|
|
|
||
|
|
Multiple presentations in one project:
|
||
|
|
```
|
||
|
|
[name].html
|
||
|
|
[name]-assets/
|
||
|
|
```
|