@open-aippt/core 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin.js +2 -0
- package/dist/build-DxTqmvsO.js +17 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +86 -0
- package/dist/config-CjzqjrEA.js +4280 -0
- package/dist/config-DIC-yVPp.d.ts +23 -0
- package/dist/design-cpzS8aud.js +35 -0
- package/dist/dev-BYuTeJbA.js +20 -0
- package/dist/format-BCeKbTOM.js +1605 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +467 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +3 -0
- package/dist/preview-DlQvnJPq.js +18 -0
- package/dist/sync-BPZ0m27m.js +139 -0
- package/dist/sync-EsYusbbL.js +3 -0
- package/dist/types-CHmFPIG_.d.ts +430 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +4 -0
- package/env.d.ts +59 -0
- package/package.json +103 -0
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +91 -0
- package/skills/create-theme/SKILL.md +250 -0
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +625 -0
- package/src/app/app.tsx +47 -0
- package/src/app/components/asset-view.tsx +966 -0
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +243 -0
- package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
- package/src/app/components/inspector/comment-widget.tsx +93 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +387 -0
- package/src/app/components/inspector/inspector-panel.tsx +1115 -0
- package/src/app/components/inspector/inspector-provider.tsx +1218 -0
- package/src/app/components/inspector/save-bar.tsx +48 -0
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/notes-drawer.tsx +120 -0
- package/src/app/components/overview-grid.tsx +363 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +80 -0
- package/src/app/components/panel/save-card.tsx +142 -0
- package/src/app/components/pdf-progress-toast.tsx +32 -0
- package/src/app/components/player.tsx +466 -0
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +315 -0
- package/src/app/components/present/help-overlay.tsx +57 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +39 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +46 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +66 -0
- package/src/app/components/present/use-touch-swipe.ts +66 -0
- package/src/app/components/shared-element.tsx +48 -0
- package/src/app/components/sidebar/folder-item.tsx +258 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
- package/src/app/components/sidebar/sidebar.tsx +284 -0
- package/src/app/components/slide-canvas.tsx +102 -0
- package/src/app/components/slide-transition-layer.tsx +844 -0
- package/src/app/components/style-panel/design-provider.tsx +148 -0
- package/src/app/components/style-panel/style-panel.tsx +349 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +59 -0
- package/src/app/components/themes/theme-detail.tsx +305 -0
- package/src/app/components/themes/themes-gallery.tsx +149 -0
- package/src/app/components/thumbnail-rail.tsx +805 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +99 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/components/ui/dialog.tsx +157 -0
- package/src/app/components/ui/dropdown-menu.tsx +245 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/popover.tsx +75 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +196 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +58 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/assets.ts +242 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/export-html.ts +326 -0
- package/src/app/lib/export-pdf.ts +298 -0
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/inspector/fiber.test.ts +154 -0
- package/src/app/lib/inspector/fiber.ts +85 -0
- package/src/app/lib/inspector/use-comments.ts +74 -0
- package/src/app/lib/inspector/use-editor.ts +73 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/print-ready.test.ts +32 -0
- package/src/app/lib/print-ready.ts +51 -0
- package/src/app/lib/sdk.test.ts +13 -0
- package/src/app/lib/sdk.ts +37 -0
- package/src/app/lib/slides.ts +26 -0
- package/src/app/lib/step-context.tsx +261 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/transition.ts +30 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-click-page-navigation.ts +60 -0
- package/src/app/lib/use-is-mobile.ts +21 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/lib/use-wheel-page-navigation.ts +99 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +213 -0
- package/src/app/routes/home.tsx +807 -0
- package/src/app/routes/presenter.tsx +418 -0
- package/src/app/routes/slide.tsx +1108 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +429 -0
- package/src/app/virtual.d.ts +51 -0
- package/src/locale/en.ts +416 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +422 -0
- package/src/locale/types.ts +443 -0
- package/src/locale/zh-cn.ts +414 -0
- package/src/locale/zh-tw.ts +414 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slide-authoring
|
|
3
|
+
description: Technical reference for writing or editing open-aippt pages — file contract, 1920×1080 canvas, type scale, layout, palette/visual direction, and assets. Consult this whenever you are about to write or modify any file under `slides/<id>/`, including from inside the `create-slide` or `apply-comments` workflows, or for any ad-hoc slide edit. Triggers on phrases like "edit slide", "tweak this page", "fix the layout", "change the palette", "investigate the slide framework", "how do slides work here".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring open-aippt pages
|
|
7
|
+
|
|
8
|
+
This skill is the **technical reference** for everything that happens inside `slides/<id>/index.tsx`. It does not own a workflow:
|
|
9
|
+
|
|
10
|
+
- `create-slide` owns "draft a new deck" — it asks the user scoping questions, then delegates the *how* to this skill.
|
|
11
|
+
- `apply-comments` owns "process inspector markers" — it finds markers and applies edits, but the edits themselves follow the rules here.
|
|
12
|
+
- `current-slide` resolves deictic references ("this page", "the slide I'm on") to a concrete `slideId` + `pageIndex`. Consult it **first** when the user references the current slide without naming it, then come back here for how to edit it.
|
|
13
|
+
- Any ad-hoc slide edit (manual tweak, one-off fix) should also consult this skill before touching the file.
|
|
14
|
+
|
|
15
|
+
When any of those paths reach the point of *writing React code for a page*, this is the source of truth. Do not duplicate the knowledge below into other skills — link here instead.
|
|
16
|
+
|
|
17
|
+
## Hard rules
|
|
18
|
+
|
|
19
|
+
- Put the slide under `slides/<kebab-case-id>/`.
|
|
20
|
+
- Entry is `slides/<id>/index.tsx`. Images/videos/fonts go under `slides/<id>/assets/`.
|
|
21
|
+
- Do **not** touch `package.json`, `open-aippt.config.ts`, or other slides.
|
|
22
|
+
- Do not add dependencies. Only `react` and standard web APIs are available.
|
|
23
|
+
- A slide is **one `index.tsx` plus `assets/`** — nothing else. Do not create sibling `.tsx`/`.ts` files (`Card.tsx`, `components/`, `helpers.ts`, etc.); helper components and constants go inside `index.tsx`. Do not create `README.md` or other prose files either.
|
|
24
|
+
|
|
25
|
+
## File contract
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// slides/<id>/index.tsx
|
|
29
|
+
import type { Page, SlideMeta } from '@open-aippt/core';
|
|
30
|
+
|
|
31
|
+
const Cover: Page = () => <div>…</div>;
|
|
32
|
+
const Body: Page = () => <div>…</div>;
|
|
33
|
+
|
|
34
|
+
export const meta: SlideMeta = {
|
|
35
|
+
title: 'My slide',
|
|
36
|
+
createdAt: '2026-05-16T12:00:00Z',
|
|
37
|
+
};
|
|
38
|
+
export default [Cover, Body] satisfies Page[];
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `export default` is a **non-empty array of zero-prop React components**, one per page, in order.
|
|
42
|
+
- `meta.title` (optional) shows in the slide header. Default is the folder name.
|
|
43
|
+
- The slide id is the kebab-case folder name. Pick something short and descriptive (`q2-roadmap`, `team-offsite-2026`).
|
|
44
|
+
- `meta.theme` (optional) marks the slide as built from a theme under `themes/`. The id must match a `<id>.md` basename. Surfaces a back-link chip on the slide card and lists the slide on `/themes/<id>`. Omit if the slide isn't derived from a registered theme.
|
|
45
|
+
- `meta.createdAt` is an **ISO 8601 string literal** (e.g. `'2026-05-16T12:00:00Z'`) set once when the slide is scaffolded. The home page uses it for the default "newest first" sort. Always include it on new slides — **immediately before writing the file, run `node -e "console.log(new Date().toISOString())"` via Bash and paste the exact output** as the value. Don't type a timestamp from memory — you will get the date or time wrong. Must be a plain string literal (no `new Date(...)` or imports in the slide itself) — the framework reads it via a regex at build time, not by evaluating the module.
|
|
46
|
+
|
|
47
|
+
## Editing an existing slide
|
|
48
|
+
|
|
49
|
+
A finished slide commonly runs 1000–1800 lines. When you only need to touch one page, **don't read the whole file** — locate the page first, then read just that range:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
grep -n ": Page = " slides/<id>/index.tsx
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This lists every `const Foo: Page = …` declaration with its line number. Read the target page with `Read` using `offset` + `limit` (~150 lines is usually enough to capture one page plus its helper components). Read the whole file only when you need cross-page context (palette audit, reordering, design const tweaks).
|
|
56
|
+
|
|
57
|
+
## Canvas
|
|
58
|
+
|
|
59
|
+
Every page renders into a fixed **1920 × 1080** canvas. The framework scales it; you design as if the viewport is literally 1920×1080.
|
|
60
|
+
|
|
61
|
+
- Use **absolute pixel values** for `font-size`, padding, positioning. No `rem`, no `vw`/`vh`, no `%` for type.
|
|
62
|
+
- The root element of each page should fill the canvas: `width: '100%'; height: '100%'`.
|
|
63
|
+
- Prefer inline `style={{ … }}`. Any CSS you load is global — scope classnames carefully.
|
|
64
|
+
|
|
65
|
+
### Type scale (start here, adjust to taste)
|
|
66
|
+
|
|
67
|
+
| Element | Size |
|
|
68
|
+
| ---------------- | ---------- |
|
|
69
|
+
| Hero title | 140–200px |
|
|
70
|
+
| Section heading | 80–120px |
|
|
71
|
+
| Page heading | 56–80px |
|
|
72
|
+
| Body text | 32–44px |
|
|
73
|
+
| Caption / label | 22–28px |
|
|
74
|
+
|
|
75
|
+
### Spacing
|
|
76
|
+
|
|
77
|
+
- Content padding: **100–160px** from canvas edges. Never let text touch the edge.
|
|
78
|
+
- Line-height: 1.2 for headings, 1.5–1.7 for body.
|
|
79
|
+
- Breathing room between elements: 32–64px.
|
|
80
|
+
|
|
81
|
+
### Vertical budget — content MUST fit 1080px
|
|
82
|
+
|
|
83
|
+
The canvas does **not** scroll. Anything below 1080px is silently cropped. Before writing JSX, do the math on paper and confirm the page fits. This is the #1 cause of broken slides — assume you will overflow unless you've checked.
|
|
84
|
+
|
|
85
|
+
**Usable height** = `1080 − top_padding − bottom_padding`. With 120px padding on each side that's **840px**. With 160px each side, **760px**. Pick the padding first, then design within that budget.
|
|
86
|
+
|
|
87
|
+
**Element height** = `font_size × line_height × number_of_lines`. A bullet that wraps to 2 lines counts as 2 lines. Add the gap below it (32–64px) before summing the next element.
|
|
88
|
+
|
|
89
|
+
**Worked example — single content page, 120px padding (budget = 840px):**
|
|
90
|
+
|
|
91
|
+
| Element | Height |
|
|
92
|
+
| ---------------------------------------- | ----------------------- |
|
|
93
|
+
| Heading: 80px × 1.2 × 1 line | 96px |
|
|
94
|
+
| Gap | 64px |
|
|
95
|
+
| Body paragraph: 40px × 1.6 × 3 lines | 192px |
|
|
96
|
+
| Gap | 48px |
|
|
97
|
+
| 5 bullets: 40px × 1.6 × 1 line each | 320px (5 × 64px) |
|
|
98
|
+
| 4 gaps between bullets: 24px each | 96px |
|
|
99
|
+
| **Total** | **816px ✅ fits in 840** |
|
|
100
|
+
|
|
101
|
+
Swap the heading to 120px or add a 6th bullet and you're over. **Verify every page like this before you write it.**
|
|
102
|
+
|
|
103
|
+
**Page-level rules:**
|
|
104
|
+
|
|
105
|
+
- One heading + body OR one heading + ≤5 short bullets. Not both blocks of body copy *and* a long bullet list.
|
|
106
|
+
- A bullet should fit on one line at the chosen font size. If it wraps, either shorten the copy or move it to its own page.
|
|
107
|
+
- Hero title pages (140–200px) carry a title + 1 subtitle + maybe an eyebrow — nothing else.
|
|
108
|
+
- Section headings (80–120px) need almost nothing else on the page.
|
|
109
|
+
- If you find yourself raising padding, shrinking type below the scale's lower bound, or tightening line-height under 1.4 to make things fit — **split into two pages instead**. Splitting is always the right answer when the budget is tight.
|
|
110
|
+
|
|
111
|
+
**Never** use `overflow: auto/scroll`, negative margins, or transforms to hide overflow. The canvas is fixed; cropped content is gone.
|
|
112
|
+
|
|
113
|
+
## Visual direction
|
|
114
|
+
|
|
115
|
+
Pick a coherent look and hold it across every page:
|
|
116
|
+
|
|
117
|
+
- **Palette** — 1 background, 1 primary text, 1 accent, 1 muted. Define as constants at the top of the file.
|
|
118
|
+
- **Typography** — one display font + one body font. System stack unless the user specifies. Heavy weight for headlines (800–900), normal for body (400–500).
|
|
119
|
+
- **Layout grid** — pick a single content padding (e.g. 120px) and stick to it. Left-aligned content feels editorial; centered feels ceremonial.
|
|
120
|
+
- **Aesthetic commitment** — choose ONE: minimal, maximalist, editorial, retro, brutalist, soft/pastel, neon, paper/print. Don't mix.
|
|
121
|
+
|
|
122
|
+
Consult the `frontend-design` skill for deeper aesthetic guidance if the user wants something bold.
|
|
123
|
+
|
|
124
|
+
## Webfonts
|
|
125
|
+
|
|
126
|
+
The default is a system font stack — prefer it. When a deck genuinely needs a webfont (a brand font, or CJK / Thai / Arabic where system coverage is poor):
|
|
127
|
+
|
|
128
|
+
- **Load the stylesheet once, in `<head>` — never inside a per-page component.** Every page is mounted live at the same time (thumbnail rail, overview grid, and the PDF print root), so a `<style>@import>` / `<link>` rendered inside a `Page` registers the whole `@font-face` set once *per page*. Inject it once, idempotently:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const FONT_HREF = 'https://fonts.googleapis.com/css2?family=...&display=swap';
|
|
132
|
+
if (typeof document !== 'undefined' && !document.getElementById('osd-webfont')) {
|
|
133
|
+
const link = document.createElement('link');
|
|
134
|
+
link.id = 'osd-webfont';
|
|
135
|
+
link.rel = 'stylesheet';
|
|
136
|
+
link.href = FONT_HREF;
|
|
137
|
+
document.head.appendChild(link);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- **List only the weights you actually use.** Each extra weight multiplies the number of `@font-face` rules.
|
|
142
|
+
- **CJK fonts are large.** A Google Fonts CJK family registers hundreds of `unicode-range` subset faces (Noto Sans TC ≈ 105 subsets × each weight), and PDF export waits on fonts before printing. If the deck's text is fixed, add `&text=<unique chars>` to the URL to request a tiny single-face subset instead of the full set.
|
|
143
|
+
|
|
144
|
+
## Themes
|
|
145
|
+
|
|
146
|
+
If `themes/<id>.md` exists at the project root and the slide is meant to follow it, **the theme file overrides the defaults in this skill** — its palette, typography, layout padding, and Title/Footer components are authoritative. Read the theme file before applying anything else in this section.
|
|
147
|
+
|
|
148
|
+
Themes are produced by the `create-theme` skill and are pure documentation: copy the palette and the paste-ready Title / Footer / Eyebrow components straight into your slide. If the theme's frontmatter has `mode: dark` or `mode: light`, treat that as the slide's background mode (e.g. when picking which logo variant to import).
|
|
149
|
+
|
|
150
|
+
## Design system (opt-in, per-slide)
|
|
151
|
+
|
|
152
|
+
A slide can declare its own typed design tokens at the top of `index.tsx`:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import type { DesignSystem, Page } from '@open-aippt/core';
|
|
156
|
+
|
|
157
|
+
export const design: DesignSystem = {
|
|
158
|
+
palette: { bg: '#f7f5f0', text: '#1a1814', accent: '#6d4cff' },
|
|
159
|
+
fonts: {
|
|
160
|
+
display: 'Georgia, "Times New Roman", serif',
|
|
161
|
+
body: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
|
|
162
|
+
},
|
|
163
|
+
typeScale: { hero: 168, body: 36 },
|
|
164
|
+
radius: 12,
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`export` it (rather than plain `const`) so the framework can read the object and inject CSS variables at the canvas root automatically.
|
|
169
|
+
|
|
170
|
+
The shape is intentionally minimal — it only covers what the Design panel can currently tweak. Anything outside this set (heading sizes, spacing, motion, extra palette colors) belongs as plain hard-coded constants in the slide file.
|
|
171
|
+
|
|
172
|
+
There are **two consumption surfaces**, and you should mix them inside the same slide:
|
|
173
|
+
|
|
174
|
+
- **`var(--osd-X)` for visual properties (color, font, font-size, radius)** — these get instant updates while the user drags a slider in the Design panel, before any file write.
|
|
175
|
+
```tsx
|
|
176
|
+
<div style={{ background: 'var(--osd-bg)', color: 'var(--osd-text)', borderRadius: 'var(--osd-radius)', fontFamily: 'var(--osd-font-body)', fontSize: 'var(--osd-size-body)' }}>
|
|
177
|
+
```
|
|
178
|
+
Available vars: `--osd-bg`, `--osd-text`, `--osd-accent`, `--osd-font-display`, `--osd-font-body`, `--osd-size-hero`, `--osd-size-body`, `--osd-radius`.
|
|
179
|
+
|
|
180
|
+
- **Direct `design.X` reads** — when you need a JS number for arithmetic or to label something in the UI. These update via HMR after the panel commits the file, not while dragging.
|
|
181
|
+
```tsx
|
|
182
|
+
<p>{design.typeScale.hero}px</p>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The dev UI has a **Design** button in the slide header (next to Inspect). Edits update an in-memory draft and the live-preview overlay; a floating Save / Discard bar at the bottom of the canvas commits or reverts. The const stays the single source of truth — production builds bake the saved values.
|
|
186
|
+
|
|
187
|
+
**Default to using it.** Every new slide should declare a `design` const so it stays tweakable from the panel after generation — this is the expected baseline. Only fall back to the local `palette` constants pattern (see starter template) for a one-off slide whose palette is intentionally locked and not meant to be re-themed. Both styles can coexist across slides — the panel only operates on the *currently viewed* slide.
|
|
188
|
+
|
|
189
|
+
Format constraints (for the panel's AST writer):
|
|
190
|
+
- Must be `[export] const design: DesignSystem = { … }` (or `as DesignSystem` / `satisfies DesignSystem`) at module top level.
|
|
191
|
+
- Object initializer must be a literal — no spreads, no helper calls. Plain values only.
|
|
192
|
+
- `DesignSystem` must be imported from `@open-aippt/core` (the panel adds the import automatically when creating a fresh block).
|
|
193
|
+
|
|
194
|
+
## Starter template
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import type { DesignSystem, Page, SlideMeta } from '@open-aippt/core';
|
|
198
|
+
|
|
199
|
+
export const design: DesignSystem = {
|
|
200
|
+
palette: { bg: '#0f172a', text: '#f8fafc', accent: '#fbbf24' },
|
|
201
|
+
fonts: {
|
|
202
|
+
display: 'system-ui, -apple-system, sans-serif',
|
|
203
|
+
body: 'system-ui, -apple-system, sans-serif',
|
|
204
|
+
},
|
|
205
|
+
typeScale: { hero: 180, body: 40 },
|
|
206
|
+
radius: 12,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Extra colors / sizes outside the DesignSystem shape stay as plain consts.
|
|
210
|
+
const muted = '#94a3b8';
|
|
211
|
+
|
|
212
|
+
const fill = {
|
|
213
|
+
width: '100%',
|
|
214
|
+
height: '100%',
|
|
215
|
+
fontFamily: 'var(--osd-font-body)',
|
|
216
|
+
} as const;
|
|
217
|
+
|
|
218
|
+
const Cover: Page = () => (
|
|
219
|
+
<div
|
|
220
|
+
style={{
|
|
221
|
+
...fill,
|
|
222
|
+
background: 'var(--osd-bg)',
|
|
223
|
+
color: 'var(--osd-text)',
|
|
224
|
+
display: 'flex',
|
|
225
|
+
flexDirection: 'column',
|
|
226
|
+
justifyContent: 'center',
|
|
227
|
+
padding: '0 160px',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<div style={{ fontSize: 28, color: 'var(--osd-accent)', letterSpacing: '0.2em' }}>
|
|
231
|
+
CHAPTER 01
|
|
232
|
+
</div>
|
|
233
|
+
<h1
|
|
234
|
+
style={{
|
|
235
|
+
fontFamily: 'var(--osd-font-display)',
|
|
236
|
+
fontSize: 'var(--osd-size-hero)',
|
|
237
|
+
fontWeight: 900,
|
|
238
|
+
margin: '32px 0',
|
|
239
|
+
lineHeight: 1.05,
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
The Big Idea
|
|
243
|
+
</h1>
|
|
244
|
+
<p style={{ fontSize: 'var(--osd-size-body)', color: muted, maxWidth: 1200 }}>
|
|
245
|
+
A short subtitle that explains what this slide is about.
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const Content: Page = () => (
|
|
251
|
+
<div style={{ ...fill, background: 'var(--osd-bg)', color: 'var(--osd-text)', padding: 120 }}>
|
|
252
|
+
<h2 style={{ fontFamily: 'var(--osd-font-display)', fontSize: 80, fontWeight: 800, margin: 0 }}>
|
|
253
|
+
Section heading
|
|
254
|
+
</h2>
|
|
255
|
+
<ul style={{ fontSize: 'var(--osd-size-body)', lineHeight: 1.6, marginTop: 64, paddingLeft: 48 }}>
|
|
256
|
+
<li>One clear point per line</li>
|
|
257
|
+
<li>Keep to 3–5 bullets</li>
|
|
258
|
+
<li>Let the space breathe</li>
|
|
259
|
+
</ul>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
export const meta: SlideMeta = {
|
|
264
|
+
title: 'The Big Idea',
|
|
265
|
+
createdAt: '2026-05-16T12:00:00Z',
|
|
266
|
+
};
|
|
267
|
+
export default [Cover, Content] satisfies Page[];
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Assets
|
|
271
|
+
|
|
272
|
+
**Slide-local assets** live under `slides/<id>/assets/` — anything one-off to a single slide. Import them as ES modules:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import hero from './assets/hero.jpg';
|
|
276
|
+
// …
|
|
277
|
+
<img src={hero} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
For URL-only access:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
const videoUrl = new URL('./assets/intro.mp4', import.meta.url).href;
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Global assets** — anything reused across decks or themes (company logos, presenter avatars, recurring icons) — live in the project root `assets/` folder. Import them via the `@assets` alias:
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
import logo from '@assets/logos/acme.svg';
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
A `themes/*.md` file may name an asset path in its prose (e.g. "use `@assets/logos/acme.svg` in the title slot"); the slide imports it explicitly.
|
|
293
|
+
|
|
294
|
+
Skip the `assets/` folder entirely for pure-text slides.
|
|
295
|
+
|
|
296
|
+
## Image placeholders
|
|
297
|
+
|
|
298
|
+
When a page genuinely needs a real image **the user has to provide** — a product screenshot, a team photo, a chart from their data — leave a typed placeholder instead of inventing a stand-in:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { ImagePlaceholder } from '@open-aippt/core';
|
|
302
|
+
|
|
303
|
+
<ImagePlaceholder hint="Product hero screenshot" width={1280} height={720} />
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The user uploads the real file via the Assets panel, then clicks the placeholder in the inspector and picks "Replace…" — the JSX is rewritten to a real `<img>` with the import added.
|
|
307
|
+
|
|
308
|
+
**Use a placeholder only when** a specific concrete image is required by the deck's topic. Examples that warrant one: a product-intro deck (product screenshot per feature), an offsite recap (team photo), a case study (customer logo, dashboard screenshot).
|
|
309
|
+
|
|
310
|
+
**Do not use a placeholder** for decoration, generic "stock photo" filler, hero imagery on a text-heavy slide, or anywhere a typographic / iconographic / illustrative solution would do. If you can carry the page with type, layout, and color — do that. Empty placeholders the user has to fill are friction; only spend that friction when the alternative is worse.
|
|
311
|
+
|
|
312
|
+
Size the placeholder to the slot it occupies. Pass `width`/`height` when the layout has a fixed image box; omit them when the placeholder fills a flex/grid cell. The `hint` should describe the *content* the user needs ("Q3 revenue chart") not the *role* ("hero image").
|
|
313
|
+
|
|
314
|
+
## Page numbers
|
|
315
|
+
|
|
316
|
+
If a footer shows the current page (`03 / 12`, `Page 3`, etc.), read it from `useSlidePageNumber()` — **never hardcode** `n` / `TOTAL`. Inserting, reordering, or deleting a page would otherwise force you to retouch every footer.
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
import { useSlidePageNumber } from '@open-aippt/core';
|
|
320
|
+
|
|
321
|
+
const Footer = () => {
|
|
322
|
+
const { current, total } = useSlidePageNumber();
|
|
323
|
+
return (
|
|
324
|
+
<span>{String(current).padStart(2, '0')} / {String(total).padStart(2, '0')}</span>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`current` is 1-indexed (matches what readers see) and `total` is the slide's page count. The hook works in every render context (main viewer, thumbnails, overview grid, present mode, presenter window, HTML/PDF export) — the same `<Footer />` JSX is correct everywhere. Call the hook inside a component that's used **per page**; don't try to call it at module top level.
|
|
330
|
+
|
|
331
|
+
## Stepped reveals (`<Steps>` / `<Step>`)
|
|
332
|
+
|
|
333
|
+
Reveal a page one beat at a time instead of showing everything at once. Wrap the deferred parts in `<Step>`, wrap the group in `<Steps>`. Each `→` reveals the next `<Step>`; `→` after the last one advances to the next page. `←` peels the last reveal back. Use it to stage attention — show framing first, then the consequence, then the turn — so the audience reads at the speaker's pace, not ahead.
|
|
334
|
+
|
|
335
|
+
`slides/build-on-reveal/` is the canonical worked example; study it before authoring a stepped page.
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import { Step, Steps } from '@open-aippt/core';
|
|
339
|
+
|
|
340
|
+
<Steps>
|
|
341
|
+
<Step><div style={BULLET_ROW}>An audience reads faster than a presenter speaks.</div></Step>
|
|
342
|
+
<Step><div style={BULLET_ROW}>Showing every bullet at once invites pre-reading.</div></Step>
|
|
343
|
+
<Step><div style={BULLET_ROW}>Revealing in time stages attention.</div></Step>
|
|
344
|
+
</Steps>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Rules
|
|
348
|
+
|
|
349
|
+
- **`<Step>` must be a *direct* child of `<Steps>`.** A `<Step>` nested deeper (or used without a `<Steps>` parent) renders fully revealed and defers nothing.
|
|
350
|
+
- **Non-`Step` children render immediately.** Put a headline or intro paragraph *inside* `<Steps>` as a plain element and it shows from the start; only the `<Step>` blocks wait. This is the "headline always, body in turn" pattern:
|
|
351
|
+
```tsx
|
|
352
|
+
<Steps>
|
|
353
|
+
<h2>Not everything has to wait.</h2>{/* visible immediately */}
|
|
354
|
+
<Step><p>First, set the stage…</p></Step>
|
|
355
|
+
<Step><p>Then, layer the consequence…</p></Step>
|
|
356
|
+
</Steps>
|
|
357
|
+
```
|
|
358
|
+
- **Multiple `<Steps>` blocks on one page compose in document order.** The first block reveals all its steps before the second begins; `←` unwinds in reverse. Use this for two columns that build left-then-right, each column owning its own `<Steps>`:
|
|
359
|
+
```tsx
|
|
360
|
+
<div style={COL}><Steps><Step>…</Step><Step>…</Step></Steps></div>{/* finishes first */}
|
|
361
|
+
<div style={COL}><Steps><Step>…</Step><Step>…</Step></Steps></div>{/* then this */}
|
|
362
|
+
```
|
|
363
|
+
- **Entry direction decides the starting state — same content, two rhythms.** Entering forward (`→` from the previous page) starts empty and builds up. Jumping in via the overview grid, or arriving backward from a later page, shows the page **fully composed** with every step already revealed. Design the page to read well both ways: a thumbnail or overview jump should look complete, not blank.
|
|
364
|
+
- **`<Step>` fades in over `duration` ms (default 180).** Pass `<Step duration={...}>` to adjust. `prefers-reduced-motion: reduce` collapses it to an instant cut automatically — don't write a fallback.
|
|
365
|
+
|
|
366
|
+
### When to reach for it
|
|
367
|
+
|
|
368
|
+
Use stepped reveals when the *order* of ideas is the point — a list whose payoff is the last item, a build-up to a conclusion, a before/after. Don't wrap every page's content in `<Step>` reflexively: a page the audience should take in at a glance (a hero title, a single quote, a diagram) is stronger shown whole. Reveals are timing, not decoration — same restraint as transitions.
|
|
369
|
+
|
|
370
|
+
## Page transitions
|
|
371
|
+
|
|
372
|
+
The framework can run an enter/exit animation between every slide change. There's **no default** — pages snap unless you declare a `SlideTransition`. Snap-swap is a perfectly tasteful default; only opt in when motion adds something.
|
|
373
|
+
|
|
374
|
+
`prefers-reduced-motion: reduce` is honored automatically. You don't write a fallback.
|
|
375
|
+
|
|
376
|
+
### Contract
|
|
377
|
+
|
|
378
|
+
Module-level for the whole deck; per-page to override. The **incoming page wins**: navigating A → B uses `pages[B].transition ?? module.transition`. Its `exit` plays on A, its `enter` plays on B. Going back B → A uses A's transition.
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
import type { Page, SlideTransition } from '@open-aippt/core';
|
|
382
|
+
|
|
383
|
+
const Cover: Page = () => <section>…</section>;
|
|
384
|
+
const Body: Page = () => <section>…</section>;
|
|
385
|
+
|
|
386
|
+
// Module-level default — every page inherits unless it overrides.
|
|
387
|
+
export const transition: SlideTransition = { /* … */ };
|
|
388
|
+
|
|
389
|
+
// Per-page override.
|
|
390
|
+
Cover.transition = { /* … */ };
|
|
391
|
+
|
|
392
|
+
export default [Cover, Body];
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
type TransitionPhase = {
|
|
397
|
+
keyframes: Keyframe[] | PropertyIndexedKeyframes; // WAAPI keyframes
|
|
398
|
+
duration?: number; // ms (falls back to top-level duration)
|
|
399
|
+
easing?: string; // CSS easing
|
|
400
|
+
delay?: number; // ms — use to overlap exit + enter
|
|
401
|
+
};
|
|
402
|
+
type SlideTransition = {
|
|
403
|
+
duration: number; // top-level fallback
|
|
404
|
+
easing?: string; // top-level fallback
|
|
405
|
+
enter?: TransitionPhase; // runs on incoming page
|
|
406
|
+
exit?: TransitionPhase; // runs on outgoing page
|
|
407
|
+
};
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The framework also exposes `--osd-dir` (`1` forward, `-1` backward) and `data-osd-dir` on the wrapper, so a single keyframe can mirror direction without a JS callback.
|
|
411
|
+
|
|
412
|
+
### Design principles (hold the line)
|
|
413
|
+
|
|
414
|
+
The single loudest signal of "made in PowerPoint" is six different transitions in one deck. Restraint is the rhythm.
|
|
415
|
+
|
|
416
|
+
- **Pick one DNA, hold it across the deck.** Same duration band, same easing pair, same out-then-in stagger. Variation lives only in *which property* gets the small nudge — Y, X, opacity, scale, blur.
|
|
417
|
+
- **Duration: 140–280 ms.** Exit 140–180 ms, enter 200–280 ms, enter delayed ~80 ms so they overlap but don't fight. Past 350 ms is video-editor territory; reserve for genuine state changes.
|
|
418
|
+
- **Magnitude ceiling: 12 px or 3% scale.** A 6 px Y-rise reads as "next thought." A 1920 px translateX reads as "different document." Premium tools move barely enough to register.
|
|
419
|
+
- **Opacity is always part of it.** Pure-transform transitions look stiff; pure-opacity transitions are the safest possible default.
|
|
420
|
+
- **Easing: ease-in for exit, ease-out for enter.** `cubic-bezier(0.4, 0, 1, 1)` going out, `cubic-bezier(0, 0, 0.2, 1)` coming in. Never `linear` (feels like a slideshow). Reserve symmetric `ease-in-out` for state-anchored morphs only.
|
|
421
|
+
|
|
422
|
+
### Tasteful family — six members, one DNA
|
|
423
|
+
|
|
424
|
+
Use this set as a starting point. Pick one as the deck's house transition; optionally reserve a second for hero/cover slides and a third for genuine section breaks. The CSS-`calc` + `--osd-dir` trick lets a single definition mirror itself on backward navigation when needed.
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
const EASE_OUT = 'cubic-bezier(0, 0, 0.2, 1)';
|
|
428
|
+
const EASE_IN = 'cubic-bezier(0.4, 0, 1, 1)';
|
|
429
|
+
|
|
430
|
+
// RISE — house quiet. 6 px Y. Use as module default.
|
|
431
|
+
export const transition: SlideTransition = {
|
|
432
|
+
duration: 200,
|
|
433
|
+
exit: { duration: 140, easing: EASE_IN,
|
|
434
|
+
keyframes: [
|
|
435
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
436
|
+
{ opacity: 0, transform: 'translateY(-4px)' },
|
|
437
|
+
] },
|
|
438
|
+
enter: { duration: 200, delay: 80, easing: EASE_OUT,
|
|
439
|
+
keyframes: [
|
|
440
|
+
{ opacity: 0, transform: 'translateY(6px)' },
|
|
441
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
442
|
+
] },
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// DISSOLVE — pure opacity. The quietest possible.
|
|
446
|
+
const dissolve: SlideTransition = {
|
|
447
|
+
duration: 240,
|
|
448
|
+
exit: { duration: 200, easing: EASE_IN,
|
|
449
|
+
keyframes: [{ opacity: 1 }, { opacity: 0 }] },
|
|
450
|
+
enter: { duration: 240, delay: 40, easing: EASE_OUT,
|
|
451
|
+
keyframes: [{ opacity: 0 }, { opacity: 1 }] },
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// SETTLE — cover-grade. Rise + a hair of blur on enter only.
|
|
455
|
+
Cover.transition = {
|
|
456
|
+
duration: 280,
|
|
457
|
+
exit: { duration: 160, easing: EASE_IN,
|
|
458
|
+
keyframes: [
|
|
459
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
460
|
+
{ opacity: 0, transform: 'translateY(-6px)' },
|
|
461
|
+
] },
|
|
462
|
+
enter: { duration: 280, delay: 100, easing: EASE_OUT,
|
|
463
|
+
keyframes: [
|
|
464
|
+
{ opacity: 0, transform: 'translateY(12px)', filter: 'blur(4px)' },
|
|
465
|
+
{ opacity: 1, transform: 'translateY(0)', filter: 'blur(0)' },
|
|
466
|
+
] },
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// BLOOM — scale 0.97 → 1, no translate. Materializes in place.
|
|
470
|
+
const bloom: SlideTransition = {
|
|
471
|
+
duration: 240,
|
|
472
|
+
exit: { duration: 160, easing: EASE_IN,
|
|
473
|
+
keyframes: [
|
|
474
|
+
{ opacity: 1, transform: 'scale(1)' },
|
|
475
|
+
{ opacity: 0, transform: 'scale(1.01)' },
|
|
476
|
+
] },
|
|
477
|
+
enter: { duration: 240, delay: 80, easing: EASE_OUT,
|
|
478
|
+
keyframes: [
|
|
479
|
+
{ opacity: 0, transform: 'scale(0.97)' },
|
|
480
|
+
{ opacity: 1, transform: 'scale(1)' },
|
|
481
|
+
] },
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// FALL — mirrored Rise. Incoming page comes down from above.
|
|
485
|
+
const fall: SlideTransition = {
|
|
486
|
+
duration: 200,
|
|
487
|
+
exit: { duration: 140, easing: EASE_IN,
|
|
488
|
+
keyframes: [
|
|
489
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
490
|
+
{ opacity: 0, transform: 'translateY(4px)' },
|
|
491
|
+
] },
|
|
492
|
+
enter: { duration: 200, delay: 80, easing: EASE_OUT,
|
|
493
|
+
keyframes: [
|
|
494
|
+
{ opacity: 0, transform: 'translateY(-6px)' },
|
|
495
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
496
|
+
] },
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// BREATH — section break. Exit fully, hold 120 ms, then enter.
|
|
500
|
+
// Reserve for genuine chapter dividers; use at most 1–2× per deck.
|
|
501
|
+
const breath: SlideTransition = {
|
|
502
|
+
duration: 460,
|
|
503
|
+
exit: { duration: 180, easing: EASE_IN,
|
|
504
|
+
keyframes: [{ opacity: 1 }, { opacity: 0 }] },
|
|
505
|
+
enter: { duration: 240, delay: 300, easing: EASE_OUT,
|
|
506
|
+
keyframes: [
|
|
507
|
+
{ opacity: 0, transform: 'translateY(8px)' },
|
|
508
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
509
|
+
] },
|
|
510
|
+
};
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
All six share the same DNA — they only differ in which property carries the small nudge. The reader perceives variety; the eye still reads one consistent hand.
|
|
514
|
+
|
|
515
|
+
### Direction-aware keyframes (use sparingly)
|
|
516
|
+
|
|
517
|
+
Most tasteful tools don't mirror on backward navigation. When you genuinely need to — e.g. a horizontal slide that should reverse — use `--osd-dir` inside `calc()`:
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
{ transform: 'translateX(calc(var(--osd-dir, 1) * 8px))' },
|
|
521
|
+
{ transform: 'translateX(0)' },
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
If you find yourself reaching for this on every transition, you're probably over-designing. Forward = backward is the more refined default.
|
|
525
|
+
|
|
526
|
+
### Transition anti-patterns
|
|
527
|
+
|
|
528
|
+
- ❌ Six different transitions across six pages — the single loudest "made in PowerPoint" tell.
|
|
529
|
+
- ❌ `translateX(100%)` slide-from-side — iOS modal / PowerPoint Push; not a slide change.
|
|
530
|
+
- ❌ Aggressive scale-pop (e.g. `0.85 → 1`) + blur — lightbox / photo-viewer vocabulary; implies zooming *into* something.
|
|
531
|
+
- ❌ `clip-path: inset(…)` reveals — After Effects vocabulary; theatrical.
|
|
532
|
+
- ❌ Parallel blur on both layers at once — visual mush; the eye can't fixate.
|
|
533
|
+
- ❌ Duration > 350 ms for a standard slide change — drags.
|
|
534
|
+
- ❌ Translate > 12 px or scale > 3% — reads as rupture, not continuity.
|
|
535
|
+
- ❌ `linear` easing — feels like a slideshow, not a product.
|
|
536
|
+
- ❌ Declaring a transition on every deck. **If you don't have a clear reason, omit it.** Snap-swap is fine.
|
|
537
|
+
|
|
538
|
+
## Repeated elements: component, not `map`
|
|
539
|
+
|
|
540
|
+
When a page has visually repeated items — cards, logo rows, gallery tiles, list rows, step indicators — **define a small component and instantiate it once per item**. Do **not** render the group with `array.map` over a data array.
|
|
541
|
+
|
|
542
|
+
Define the component **in the same `index.tsx`**, alongside the `Page` components. Never split it into a sibling file like `Card.tsx` — a slide is always a single `index.tsx` plus its `assets/`.
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
// ✅ Each card is its own JSX node — inspector edits one at a time.
|
|
546
|
+
const Card = ({ src, label }: { src: string; label: string }) => (
|
|
547
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
548
|
+
<img src={src} style={{ width: 320, height: 320, objectFit: 'cover', borderRadius: 12 }} />
|
|
549
|
+
<p style={{ fontSize: 32 }}>{label}</p>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 64 }}>
|
|
554
|
+
<Card src={alpha} label="Alpha" />
|
|
555
|
+
<Card src={beta} label="Beta" />
|
|
556
|
+
<Card src={gamma} label="Gamma" />
|
|
557
|
+
</div>
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
```tsx
|
|
561
|
+
// ❌ One shared template — replacing the image or text in the inspector
|
|
562
|
+
// changes every rendered card at once.
|
|
563
|
+
const items = [
|
|
564
|
+
{ src: alpha, label: 'Alpha' },
|
|
565
|
+
{ src: beta, label: 'Beta' },
|
|
566
|
+
{ src: gamma, label: 'Gamma' },
|
|
567
|
+
];
|
|
568
|
+
items.map((item) => (
|
|
569
|
+
<div>
|
|
570
|
+
<img src={item.src} />
|
|
571
|
+
<p>{item.label}</p>
|
|
572
|
+
</div>
|
|
573
|
+
));
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
The inspector edits source JSX in place. A `map` body is **one source location** shared by every rendered instance, so when the user replaces an image or tweaks a label there, every card mutates together. Explicit instances give each card its own JSX node and its own props — the unit the inspector can target.
|
|
577
|
+
|
|
578
|
+
The component definition stays the single source of truth for layout/styling (change it once → all cards update). Only the per-instance data — `src`, `label`, accent color — lives at the call site.
|
|
579
|
+
|
|
580
|
+
This applies whenever the *visual element* repeats, not whenever the *data* does. Pure-text lists (`<ul><li>` bullets) are fine: each `<li>` is already its own JSX node, so plain literal markup is the correct shape — no need to wrap them in a component.
|
|
581
|
+
|
|
582
|
+
## Runtime behavior you get for free
|
|
583
|
+
|
|
584
|
+
- Home page lists every folder under `slides/`.
|
|
585
|
+
- Clicking a slide shows a left thumbnail rail, main page, prev/next, page counter.
|
|
586
|
+
- Arrow keys / PageUp / PageDown navigate. `F` enters fullscreen play mode.
|
|
587
|
+
- In play mode: Space/→ next, ← prev, Esc exit.
|
|
588
|
+
- Hot reload: edit `index.tsx` and the browser updates live.
|
|
589
|
+
|
|
590
|
+
## Self-review before finishing
|
|
591
|
+
|
|
592
|
+
- [ ] `slides/<id>/index.tsx` `export default`s a non-empty `Page[]`.
|
|
593
|
+
- [ ] Every page's root fills `100% × 100%`.
|
|
594
|
+
- [ ] Content lives inside padding (no text kisses the edge).
|
|
595
|
+
- [ ] **For every page, sum (font_size × line_height × lines) + gaps + 2×padding ≤ 1080px.** If close, split the page. No `overflow: auto` escape hatches.
|
|
596
|
+
- [ ] No bullet wraps to a second line at the chosen font size.
|
|
597
|
+
- [ ] One coherent visual direction across every page (palette + type scale).
|
|
598
|
+
- [ ] Slide declares a top-level `export const design: DesignSystem = { … }` and references the values via `var(--osd-X)` (use `design.X` only when you need a JS number for arithmetic). Only omit the `design` const for a one-off slide whose palette is intentionally locked.
|
|
599
|
+
- [ ] One idea per page.
|
|
600
|
+
- [ ] Visually repeated elements (cards, tiles, logo rows) are rendered as explicit `<Component />` instances, not via `array.map` over a data list.
|
|
601
|
+
- [ ] All imported assets exist on disk — slide-local under `slides/<id>/assets/`, or global under `assets/` (imported via `@assets/...`).
|
|
602
|
+
- [ ] Every `<ImagePlaceholder>` corresponds to a real image the user must supply — not decorative filler. If it could be replaced by typography or layout, it should be.
|
|
603
|
+
- [ ] If a page uses `<Steps>`/`<Step>`, every `<Step>` is a direct child of a `<Steps>`, and the page still reads as complete when jumped to via the overview grid (entering forward builds up; jumping in shows it fully revealed).
|
|
604
|
+
- [ ] If a `SlideTransition` is declared, every page sits in one family — same duration band (140–280 ms), same easing pair, same out-then-in stagger, magnitude under 12 px / 3%. No six-different-vocabularies decks. When in doubt, omit transitions entirely.
|
|
605
|
+
- [ ] Nothing outside `slides/<id>/` was edited.
|
|
606
|
+
|
|
607
|
+
## Anti-patterns
|
|
608
|
+
|
|
609
|
+
- ❌ Walls of text. If a page has more than ~40 words, split it.
|
|
610
|
+
- ❌ Using the full canvas for body copy. Respect 100–160px padding.
|
|
611
|
+
- ❌ Overflowing 1080px vertically. Cropped content is invisible — split the page.
|
|
612
|
+
- ❌ `overflow: auto` / `overflow: scroll` / `overflow: hidden` to "hide" too much content. The canvas doesn't scroll; you've just hidden the bug.
|
|
613
|
+
- ❌ Shrinking type below the scale's lower bound, or padding below 100px, to cram more in. Split instead.
|
|
614
|
+
- ❌ Bullets that wrap to a second line — either shorten or move to its own page.
|
|
615
|
+
- ❌ Body type under 28px — unreadable on a projector.
|
|
616
|
+
- ❌ Inconsistent palette across pages.
|
|
617
|
+
- ❌ Installing packages. Only `react` and standard web APIs are available.
|
|
618
|
+
- ❌ Writing CSS to a shared file. Inline styles or scoped classnames only.
|
|
619
|
+
- ❌ Creating `README.md` or other prose files inside the slide folder.
|
|
620
|
+
- ❌ Editing `package.json`, `open-aippt.config.ts`, or other slides.
|
|
621
|
+
- ❌ Sprinkling `<ImagePlaceholder>` across pages "for visual interest". Placeholders are for content the user owns; they're not stock-photo slots.
|
|
622
|
+
- ❌ Using a placeholder for an icon or decorative shape — those are typography/SVG problems, not asset problems.
|
|
623
|
+
- ❌ Rendering visually repeated elements with `array.map(...)` over a data array. Define a component and instantiate it explicitly per item (`<Card />`, `<Card />`, `<Card />`) so the inspector can edit each independently — a shared `map` body mutates every instance at once.
|
|
624
|
+
- ❌ Wrapping every page's content in `<Step>` reflexively. Stepped reveals are for content whose *order* is the point; a glance-and-get-it page (hero title, single quote, diagram) is stronger shown whole.
|
|
625
|
+
- ❌ A `<Step>` that isn't a direct child of `<Steps>` (nested deeper, or with no `<Steps>` parent). It renders fully revealed and defers nothing — the reveal silently does nothing.
|