@open-slide/core 0.0.11 → 0.0.13

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.
Files changed (89) hide show
  1. package/dist/{build-DHiRlpjn.js → build-DC3FTpWO.js} +2 -1
  2. package/dist/cli/bin.js +43 -4
  3. package/dist/{config-LZM903FE.js → config-Cuw0mC5h.js} +592 -63
  4. package/dist/design-BUML7uvZ.js +35 -0
  5. package/dist/{dev-B3JzCYn7.js → dev-BuWsdYvn.js} +2 -1
  6. package/dist/index.d.ts +55 -4
  7. package/dist/index.js +110 -1
  8. package/dist/{preview-UikovHEt.js → preview-CIcG-lP3.js} +2 -1
  9. package/dist/sync-3oqN1WyK.js +139 -0
  10. package/dist/sync-B4eLo2H6.js +3 -0
  11. package/dist/vite/index.d.ts +1 -1
  12. package/dist/vite/index.js +2 -1
  13. package/package.json +2 -1
  14. package/skills/apply-comments/SKILL.md +83 -0
  15. package/skills/create-slide/SKILL.md +81 -0
  16. package/skills/create-theme/SKILL.md +194 -0
  17. package/skills/slide-authoring/SKILL.md +288 -0
  18. package/src/app/{App.tsx → app.tsx} +8 -6
  19. package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
  20. package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
  21. package/src/app/components/history-provider.tsx +120 -0
  22. package/src/app/components/image-placeholder.tsx +121 -0
  23. package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
  24. package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
  25. package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
  26. package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
  27. package/src/app/components/inspector/save-bar.tsx +47 -0
  28. package/src/app/components/panel/panel-fields.tsx +60 -0
  29. package/src/app/components/panel/panel-shell.tsx +78 -0
  30. package/src/app/components/panel/save-card.tsx +139 -0
  31. package/src/app/components/pdf-progress-toast.tsx +25 -0
  32. package/src/app/components/player.tsx +341 -0
  33. package/src/app/components/present/blackout-overlay.tsx +18 -0
  34. package/src/app/components/present/control-bar.tsx +204 -0
  35. package/src/app/components/present/help-overlay.tsx +56 -0
  36. package/src/app/components/present/jump-input.tsx +74 -0
  37. package/src/app/components/present/laser-pointer.tsx +40 -0
  38. package/src/app/components/present/overview-grid.tsx +184 -0
  39. package/src/app/components/present/progress-bar.tsx +26 -0
  40. package/src/app/components/present/use-idle.ts +44 -0
  41. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  42. package/src/app/components/present/use-presenter-channel.ts +71 -0
  43. package/src/app/components/present/use-touch-swipe.ts +63 -0
  44. package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
  45. package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
  46. package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
  47. package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
  48. package/src/app/components/style-panel/design-provider.tsx +139 -0
  49. package/src/app/components/style-panel/style-panel.tsx +326 -0
  50. package/src/app/components/style-panel/use-design.ts +112 -0
  51. package/src/app/components/theme-toggle.tsx +57 -0
  52. package/src/app/components/thumbnail-rail.tsx +151 -0
  53. package/src/app/components/ui/button.tsx +51 -19
  54. package/src/app/components/ui/card.tsx +1 -1
  55. package/src/app/components/ui/dialog.tsx +25 -9
  56. package/src/app/components/ui/dropdown-menu.tsx +29 -12
  57. package/src/app/components/ui/input.tsx +13 -9
  58. package/src/app/components/ui/popover.tsx +5 -2
  59. package/src/app/components/ui/progress.tsx +2 -2
  60. package/src/app/components/ui/select.tsx +11 -5
  61. package/src/app/components/ui/separator.tsx +1 -1
  62. package/src/app/components/ui/slider.tsx +4 -4
  63. package/src/app/components/ui/sonner.tsx +11 -1
  64. package/src/app/components/ui/tabs.tsx +6 -6
  65. package/src/app/components/ui/textarea.tsx +11 -7
  66. package/src/app/components/ui/toggle-group.tsx +2 -2
  67. package/src/app/components/ui/toggle.tsx +6 -6
  68. package/src/app/components/ui/tooltip.tsx +5 -2
  69. package/src/app/lib/design.ts +64 -0
  70. package/src/app/lib/export-html.ts +10 -1
  71. package/src/app/lib/export-pdf.ts +7 -0
  72. package/src/app/lib/folders.ts +1 -1
  73. package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
  74. package/src/app/lib/sdk.ts +5 -0
  75. package/src/app/lib/slides.ts +1 -1
  76. package/src/app/lib/utils.ts +1 -1
  77. package/src/app/main.tsx +5 -2
  78. package/src/app/routes/{Home.tsx → home.tsx} +266 -97
  79. package/src/app/routes/presenter.tsx +400 -0
  80. package/src/app/routes/slide.tsx +519 -0
  81. package/src/app/styles.css +338 -67
  82. package/src/app/components/PdfProgressToast.tsx +0 -23
  83. package/src/app/components/Player.tsx +0 -100
  84. package/src/app/components/ThumbnailRail.tsx +0 -68
  85. package/src/app/components/inspector/SaveBar.tsx +0 -77
  86. package/src/app/routes/Slide.tsx +0 -478
  87. /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
  88. /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
  89. /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
@@ -0,0 +1,194 @@
1
+ ---
2
+ name: create-theme
3
+ description: Use this skill when the user wants to create, draft, author, or extract a slide theme in this open-slide repo. Triggers on phrases like "create a theme", "make a theme called X", "extract a theme from <slide>", "build a theme from these images". Produces a single markdown file under `themes/<id>.md` describing palette, typography, layout, fixed Title/Footer components, and motion philosophy. Do NOT use for editing slides themselves — only for authoring `themes/<id>.md`.
4
+ ---
5
+
6
+ # Create a slide theme
7
+
8
+ This skill produces one markdown file under `themes/<id>.md` that describes a reusable visual identity for slides — palette, typography, layout, fixed Title/Footer/Eyebrow components, motion. Themes are agent-facing documentation, not executable runtime: a slide author reads the theme markdown and applies it when writing `slides/<id>/index.tsx`.
9
+
10
+ A theme is **distinct from a slide's `design` const**. The theme markdown is authoring-time aesthetic direction (read by `create-slide`, copied into slide source). A per-slide `const design: DesignSystem = { … }` (declared at the top of `slides/<id>/index.tsx`) is the runtime tokens object the user can tweak from the Design panel in the dev UI. Both can coexist: the markdown theme commits the *direction*, the per-slide `design` const makes the slide *tweakable*. This skill only writes the markdown.
11
+
12
+ You only write a single file under `themes/`. Never modify slides or other configuration. The canvas / type-scale defaults that themes can override live in the **`slide-authoring`** skill — read it before writing the theme so your overrides are stated explicitly.
13
+
14
+ ## Step 1 — Identify the input source
15
+
16
+ A theme can be derived from any combination of three input shapes:
17
+
18
+ - **Image references** — paths or URLs to slide screenshots, mood-board images, brand assets.
19
+ - **Free-text description** — prose describing the desired palette, fonts, feel.
20
+ - **An existing slide** — `slides/<id>/index.tsx` whose visual identity should be lifted out into a reusable theme.
21
+
22
+ If the user's original message already specifies the inputs unambiguously, skip the question and proceed. Otherwise call `AskUserQuestion` (multi-select) so they can pick one or more sources, and ask follow-ups (paths, slide id, prose) only as needed.
23
+
24
+ ## Step 2 — Gather raw inputs
25
+
26
+ - **Images**: read each path with the `Read` tool (it accepts images). Note dominant colors as hex, type weight/style, layout rhythm, decorative motifs, and any recurring chrome (header bar, footer line, page numbers).
27
+ - **Text**: extract explicit tokens (hex codes, font names, motion verbs) and implicit tone words ("editorial", "playful", "brutalist"). Resolve vague language into concrete decisions before writing.
28
+ - **Existing slide**: read `slides/<id>/index.tsx` and pull:
29
+ - The `palette` object → Palette section.
30
+ - Font constants and any `font-size` patterns → Typography section.
31
+ - Padding / alignment patterns → Layout section.
32
+ - Recurring components (TrafficLights, Eyebrow, Footer-style helpers, WindowShell, …) → Fixed components section.
33
+ - `@keyframes` blocks and the shared `styles` string → Motion section.
34
+ - The aesthetic feel implied by the design → Aesthetic paragraph.
35
+
36
+ When inputs disagree (e.g. images use blue but the description says green), ask the user which to honor.
37
+
38
+ ## Step 3 — Pick a theme id
39
+
40
+ Use **kebab-case**, short, descriptive. Examples: `editorial-noir`, `brutalist-mono`, `pastel-soft`, `dev-terminal`. Check `themes/` to avoid collisions.
41
+
42
+ ## Step 4 — Write `themes/<id>.md`
43
+
44
+ Produce a file with this exact section order. Section bodies adapt to the theme; the headings stay consistent across all themes.
45
+
46
+ ````markdown
47
+ ---
48
+ name: <Human title, e.g. "Editorial Noir">
49
+ description: <one-line elevator pitch>
50
+ mode: light | dark | system
51
+ ---
52
+
53
+ # <Theme name>
54
+
55
+ ## Palette
56
+
57
+ | Role | Value | Notes |
58
+ | ------ | --------- | ------------------------------ |
59
+ | bg | `#0f172a` | page background |
60
+ | text | `#f8fafc` | primary copy |
61
+ | accent | `#fbbf24` | callouts, eyebrow, key numbers |
62
+ | muted | `#94a3b8` | secondary copy, dividers |
63
+ | ... | ... | extend as the theme requires |
64
+
65
+ ## Typography
66
+
67
+ - Display font: `<stack>` — weight 800–900 for headlines.
68
+ - Body font: `<stack>` — weight 400–500.
69
+ - Type-scale overrides (only list what differs from `slide-authoring` defaults):
70
+ - Hero title: 180 px (default 140–200 ✓)
71
+ - Body text: 36 px
72
+
73
+ ## Layout
74
+
75
+ - Content padding: 120 px from canvas edges (1920 × 1080).
76
+ - Alignment: left-aligned, single column.
77
+ - Grid notes: optional 12-column overlay at 80 px gutter for content pages.
78
+
79
+ ## Fixed components
80
+
81
+ These are paste-ready. Copy them verbatim into a slide that uses this theme.
82
+
83
+ ### Title
84
+
85
+ ```tsx
86
+ const Title = ({ children }: { children: React.ReactNode }) => (
87
+ <h1
88
+ style={{
89
+ fontSize: 140,
90
+ fontWeight: 900,
91
+ lineHeight: 1.05,
92
+ letterSpacing: '-0.02em',
93
+ margin: 0,
94
+ color: '#f8fafc',
95
+ }}
96
+ >
97
+ {children}
98
+ </h1>
99
+ );
100
+ ```
101
+
102
+ ### Footer
103
+
104
+ ```tsx
105
+ const Footer = ({ pageNum, total }: { pageNum: number; total: number }) => (
106
+ <div
107
+ style={{
108
+ position: 'absolute',
109
+ left: 120,
110
+ right: 120,
111
+ bottom: 60,
112
+ display: 'flex',
113
+ justifyContent: 'space-between',
114
+ fontSize: 24,
115
+ color: '#94a3b8',
116
+ }}
117
+ >
118
+ <span>EDITORIAL NOIR · 2026</span>
119
+ <span>{pageNum} / {total}</span>
120
+ </div>
121
+ );
122
+ ```
123
+
124
+ ### Eyebrow / accents (optional)
125
+
126
+ ```tsx
127
+ const Eyebrow = ({ children }: { children: React.ReactNode }) => (
128
+ <div style={{ fontSize: 26, letterSpacing: '0.2em', color: '#fbbf24' }}>
129
+ {children}
130
+ </div>
131
+ );
132
+ ```
133
+
134
+ ## Motion
135
+
136
+ - Philosophy: static / subtle / rich — pick one and explain in one sentence.
137
+ - Reusable keyframes (paste-ready, only if the philosophy is subtle or rich):
138
+
139
+ ```css
140
+ @keyframes fadeUp {
141
+ from { opacity: 0; transform: translateY(24px); }
142
+ to { opacity: 1; transform: translateY(0); }
143
+ }
144
+ ```
145
+
146
+ ## Aesthetic
147
+
148
+ One paragraph. What it feels like, the references it draws on, what to avoid (e.g. "no rounded corners; no gradients; no decorative emoji"). Commit to a single direction — minimal, maximalist, editorial, retro, brutalist, soft/pastel, neon, paper/print.
149
+
150
+ ## Example usage
151
+
152
+ ```tsx
153
+ const Cover: Page = () => (
154
+ <div style={{ width: '100%', height: '100%', background: '#0f172a', color: '#f8fafc', padding: 120, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
155
+ <Eyebrow>CHAPTER 01</Eyebrow>
156
+ <Title>The Big Idea</Title>
157
+ <p style={{ fontSize: 36, color: '#94a3b8', maxWidth: 1200, marginTop: 32 }}>
158
+ A short subtitle that explains what this slide is about.
159
+ </p>
160
+ <Footer pageNum={1} total={5} />
161
+ </div>
162
+ );
163
+ ```
164
+ ````
165
+
166
+ ## Step 5 — Self-review
167
+
168
+ Run this checklist before finishing:
169
+
170
+ - [ ] Palette covers `bg` / `text` / `accent` / `muted` at minimum, all as hex.
171
+ - [ ] Type scale specifies hero, heading, body, caption sizes (or explicitly defers to `slide-authoring` defaults).
172
+ - [ ] At least Title and Footer are defined as paste-ready React with concrete inline styles.
173
+ - [ ] Motion section commits to one of static / subtle / rich.
174
+ - [ ] Aesthetic paragraph names a single coherent direction.
175
+ - [ ] File lives at `themes/<id>.md` and only that file was created — no slide changes, no config changes.
176
+ - [ ] Frontmatter `mode` is one of `light`, `dark`, `system`.
177
+
178
+ ## Step 6 — Hand off
179
+
180
+ Tell the user:
181
+
182
+ - The theme id and file path.
183
+ - That `/create-slide` will list it as a picker option on its next run.
184
+ - A one-line summary of the look (palette + aesthetic).
185
+
186
+ Do not run the dev server. Do not modify slides — even to demonstrate the theme; that is the user's next move.
187
+
188
+ ## Anti-patterns
189
+
190
+ - ❌ Writing executable code in `themes/<id>.md` outside the labeled component snippets — the file is documentation.
191
+ - ❌ Producing more than one file. One theme = one `themes/<id>.md`.
192
+ - ❌ Inventing palette / fonts when the user supplied images or an existing slide. Extract, don't fabricate.
193
+ - ❌ Editing `slides/`, `packages/`, `package.json`, or `open-slide.config.ts`.
194
+ - ❌ Skipping the Fixed components section. Title and Footer are the most common reuse target — they must be paste-ready.
@@ -0,0 +1,288 @@
1
+ ---
2
+ name: slide-authoring
3
+ description: Technical reference for writing or editing open-slide 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-slide 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
+ - Any ad-hoc slide edit (manual tweak, one-off fix) should also consult this skill before touching the file.
13
+
14
+ 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.
15
+
16
+ ## Hard rules
17
+
18
+ - Put the slide under `slides/<kebab-case-id>/`.
19
+ - Entry is `slides/<id>/index.tsx`. Images/videos/fonts go under `slides/<id>/assets/`.
20
+ - Do **not** touch `package.json`, `open-slide.config.ts`, or other slides.
21
+ - Do not add dependencies. Only `react` and standard web APIs are available.
22
+ - Do not create `README.md` or other prose files inside the slide folder — just `index.tsx` + `assets/`.
23
+
24
+ ## File contract
25
+
26
+ ```tsx
27
+ // slides/<id>/index.tsx
28
+ import type { Page, SlideMeta } from '@open-slide/core';
29
+
30
+ const Cover: Page = () => <div>…</div>;
31
+ const Body: Page = () => <div>…</div>;
32
+
33
+ export const meta: SlideMeta = { title: 'My slide' };
34
+ export default [Cover, Body] satisfies Page[];
35
+ ```
36
+
37
+ - `export default` is a **non-empty array of zero-prop React components**, one per page, in order.
38
+ - `meta.title` (optional) shows in the slide header. Default is the folder name.
39
+ - The slide id is the kebab-case folder name. Pick something short and descriptive (`q2-roadmap`, `team-offsite-2026`).
40
+
41
+ ## Canvas
42
+
43
+ Every page renders into a fixed **1920 × 1080** canvas. The framework scales it; you design as if the viewport is literally 1920×1080.
44
+
45
+ - Use **absolute pixel values** for `font-size`, padding, positioning. No `rem`, no `vw`/`vh`, no `%` for type.
46
+ - The root element of each page should fill the canvas: `width: '100%'; height: '100%'`.
47
+ - Prefer inline `style={{ … }}`. Any CSS you load is global — scope classnames carefully.
48
+
49
+ ### Type scale (start here, adjust to taste)
50
+
51
+ | Element | Size |
52
+ | ---------------- | ---------- |
53
+ | Hero title | 140–200px |
54
+ | Section heading | 80–120px |
55
+ | Page heading | 56–80px |
56
+ | Body text | 32–44px |
57
+ | Caption / label | 22–28px |
58
+
59
+ ### Spacing
60
+
61
+ - Content padding: **100–160px** from canvas edges. Never let text touch the edge.
62
+ - Line-height: 1.2 for headings, 1.5–1.7 for body.
63
+ - Breathing room between elements: 32–64px.
64
+
65
+ ### Vertical budget — content MUST fit 1080px
66
+
67
+ 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.
68
+
69
+ **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.
70
+
71
+ **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.
72
+
73
+ **Worked example — single content page, 120px padding (budget = 840px):**
74
+
75
+ | Element | Height |
76
+ | ---------------------------------------- | ----------------------- |
77
+ | Heading: 80px × 1.2 × 1 line | 96px |
78
+ | Gap | 64px |
79
+ | Body paragraph: 40px × 1.6 × 3 lines | 192px |
80
+ | Gap | 48px |
81
+ | 5 bullets: 40px × 1.6 × 1 line each | 320px (5 × 64px) |
82
+ | 4 gaps between bullets: 24px each | 96px |
83
+ | **Total** | **816px ✅ fits in 840** |
84
+
85
+ Swap the heading to 120px or add a 6th bullet and you're over. **Verify every page like this before you write it.**
86
+
87
+ **Page-level rules:**
88
+
89
+ - One heading + body OR one heading + ≤5 short bullets. Not both blocks of body copy *and* a long bullet list.
90
+ - 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.
91
+ - Hero title pages (140–200px) carry a title + 1 subtitle + maybe an eyebrow — nothing else.
92
+ - Section headings (80–120px) need almost nothing else on the page.
93
+ - 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.
94
+
95
+ **Never** use `overflow: auto/scroll`, negative margins, or transforms to hide overflow. The canvas is fixed; cropped content is gone.
96
+
97
+ ## Visual direction
98
+
99
+ Pick a coherent look and hold it across every page:
100
+
101
+ - **Palette** — 1 background, 1 primary text, 1 accent, 1 muted. Define as constants at the top of the file.
102
+ - **Typography** — one display font + one body font. System stack unless the user specifies. Heavy weight for headlines (800–900), normal for body (400–500).
103
+ - **Layout grid** — pick a single content padding (e.g. 120px) and stick to it. Left-aligned content feels editorial; centered feels ceremonial.
104
+ - **Aesthetic commitment** — choose ONE: minimal, maximalist, editorial, retro, brutalist, soft/pastel, neon, paper/print. Don't mix.
105
+
106
+ Consult the `frontend-design` skill for deeper aesthetic guidance if the user wants something bold.
107
+
108
+ ## Themes
109
+
110
+ 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.
111
+
112
+ 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).
113
+
114
+ ## Design system (opt-in, per-slide)
115
+
116
+ A slide can declare its own typed design tokens at the top of `index.tsx`:
117
+
118
+ ```tsx
119
+ import type { DesignSystem, Page } from '@open-slide/core';
120
+
121
+ export const design: DesignSystem = {
122
+ palette: { bg: '#f7f5f0', text: '#1a1814', accent: '#6d4cff' },
123
+ fonts: {
124
+ display: 'Georgia, "Times New Roman", serif',
125
+ body: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
126
+ },
127
+ typeScale: { hero: 168, body: 36 },
128
+ radius: { md: 12 },
129
+ };
130
+ ```
131
+
132
+ `export` it (rather than plain `const`) so the framework can read the object and inject CSS variables at the canvas root automatically.
133
+
134
+ 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.
135
+
136
+ There are **two consumption surfaces**, and you should mix them inside the same slide:
137
+
138
+ - **`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.
139
+ ```tsx
140
+ <div style={{ background: 'var(--osd-bg)', color: 'var(--osd-text)', borderRadius: 'var(--osd-radius-md)', fontFamily: 'var(--osd-font-body)', fontSize: 'var(--osd-size-body)' }}>
141
+ ```
142
+ Available vars: `--osd-bg`, `--osd-text`, `--osd-accent`, `--osd-font-display`, `--osd-font-body`, `--osd-size-hero`, `--osd-size-body`, `--osd-radius-md`.
143
+
144
+ - **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.
145
+ ```tsx
146
+ <p>{design.typeScale.hero}px</p>
147
+ ```
148
+
149
+ 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.
150
+
151
+ When to use it: any time the slide should remain tweakable from the panel after generation. When to *not* use it: one-off slides whose palette is intentionally fixed (then use the local `palette` constants pattern from the starter template). Both styles can coexist across slides — the panel only operates on the *currently viewed* slide.
152
+
153
+ Format constraints (for the panel's AST writer):
154
+ - Must be `[export] const design: DesignSystem = { … }` (or `as DesignSystem` / `satisfies DesignSystem`) at module top level.
155
+ - Object initializer must be a literal — no spreads, no helper calls. Plain values only.
156
+ - `DesignSystem` must be imported from `@open-slide/core` (the panel adds the import automatically when creating a fresh block).
157
+
158
+ ## Starter template
159
+
160
+ ```tsx
161
+ import type { Page, SlideMeta } from '@open-slide/core';
162
+
163
+ const palette = {
164
+ bg: '#0f172a',
165
+ text: '#f8fafc',
166
+ accent: '#fbbf24',
167
+ muted: '#94a3b8',
168
+ };
169
+
170
+ const fill = {
171
+ width: '100%',
172
+ height: '100%',
173
+ fontFamily: 'system-ui, -apple-system, sans-serif',
174
+ } as const;
175
+
176
+ const Cover: Page = () => (
177
+ <div
178
+ style={{
179
+ ...fill,
180
+ background: palette.bg,
181
+ color: palette.text,
182
+ display: 'flex',
183
+ flexDirection: 'column',
184
+ justifyContent: 'center',
185
+ padding: '0 160px',
186
+ }}
187
+ >
188
+ <div style={{ fontSize: 28, color: palette.accent, letterSpacing: '0.2em' }}>
189
+ CHAPTER 01
190
+ </div>
191
+ <h1 style={{ fontSize: 180, fontWeight: 900, margin: '32px 0', lineHeight: 1.05 }}>
192
+ The Big Idea
193
+ </h1>
194
+ <p style={{ fontSize: 40, color: palette.muted, maxWidth: 1200 }}>
195
+ A short subtitle that explains what this slide is about.
196
+ </p>
197
+ </div>
198
+ );
199
+
200
+ const Content: Page = () => (
201
+ <div style={{ ...fill, background: palette.bg, color: palette.text, padding: 120 }}>
202
+ <h2 style={{ fontSize: 80, fontWeight: 800, margin: 0 }}>Section heading</h2>
203
+ <ul style={{ fontSize: 40, lineHeight: 1.6, marginTop: 64, paddingLeft: 48 }}>
204
+ <li>One clear point per line</li>
205
+ <li>Keep to 3–5 bullets</li>
206
+ <li>Let the space breathe</li>
207
+ </ul>
208
+ </div>
209
+ );
210
+
211
+ export const meta: SlideMeta = { title: 'The Big Idea' };
212
+ export default [Cover, Content] satisfies Page[];
213
+ ```
214
+
215
+ ## Assets
216
+
217
+ Place files under `slides/<id>/assets/`. Import them as ES modules:
218
+
219
+ ```tsx
220
+ import hero from './assets/hero.jpg';
221
+ // …
222
+ <img src={hero} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
223
+ ```
224
+
225
+ For URL-only access:
226
+
227
+ ```tsx
228
+ const videoUrl = new URL('./assets/intro.mp4', import.meta.url).href;
229
+ ```
230
+
231
+ Skip the `assets/` folder entirely for pure-text slides.
232
+
233
+ ## Image placeholders
234
+
235
+ 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:
236
+
237
+ ```tsx
238
+ import { ImagePlaceholder } from '@open-slide/core';
239
+
240
+ <ImagePlaceholder hint="Product hero screenshot" width={1280} height={720} />
241
+ ```
242
+
243
+ 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.
244
+
245
+ **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).
246
+
247
+ **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.
248
+
249
+ 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").
250
+
251
+ ## Runtime behavior you get for free
252
+
253
+ - Home page lists every folder under `slides/`.
254
+ - Clicking a slide shows a left thumbnail rail, main page, prev/next, page counter.
255
+ - Arrow keys / PageUp / PageDown navigate. `F` enters fullscreen play mode.
256
+ - In play mode: Space/→ next, ← prev, Esc exit.
257
+ - Hot reload: edit `index.tsx` and the browser updates live.
258
+
259
+ ## Self-review before finishing
260
+
261
+ - [ ] `slides/<id>/index.tsx` `export default`s a non-empty `Page[]`.
262
+ - [ ] Every page's root fills `100% × 100%`.
263
+ - [ ] Content lives inside padding (no text kisses the edge).
264
+ - [ ] **For every page, sum (font_size × line_height × lines) + gaps + 2×padding ≤ 1080px.** If close, split the page. No `overflow: auto` escape hatches.
265
+ - [ ] No bullet wraps to a second line at the chosen font size.
266
+ - [ ] One coherent visual direction across every page (palette + type scale).
267
+ - [ ] If the slide should be tweakable from the Design panel, it declares a top-level `const design: DesignSystem = { … }` and references `design.X` from inline styles.
268
+ - [ ] One idea per page.
269
+ - [ ] All imported assets exist on disk under `slides/<id>/assets/`.
270
+ - [ ] 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.
271
+ - [ ] Nothing outside `slides/<id>/` was edited.
272
+
273
+ ## Anti-patterns
274
+
275
+ - ❌ Walls of text. If a page has more than ~40 words, split it.
276
+ - ❌ Using the full canvas for body copy. Respect 100–160px padding.
277
+ - ❌ Overflowing 1080px vertically. Cropped content is invisible — split the page.
278
+ - ❌ `overflow: auto` / `overflow: scroll` / `overflow: hidden` to "hide" too much content. The canvas doesn't scroll; you've just hidden the bug.
279
+ - ❌ Shrinking type below the scale's lower bound, or padding below 100px, to cram more in. Split instead.
280
+ - ❌ Bullets that wrap to a second line — either shorten or move to its own page.
281
+ - ❌ Body type under 28px — unreadable on a projector.
282
+ - ❌ Inconsistent palette across pages.
283
+ - ❌ Installing packages. Only `react` and standard web APIs are available.
284
+ - ❌ Writing CSS to a shared file. Inline styles or scoped classnames only.
285
+ - ❌ Creating `README.md` or other prose files inside the slide folder.
286
+ - ❌ Editing `package.json`, `open-slide.config.ts`, or other slides.
287
+ - ❌ Sprinkling `<ImagePlaceholder>` across pages "for visual interest". Placeholders are for content the user owns; they're not stock-photo slots.
288
+ - ❌ Using a placeholder for an icon or decorative shape — those are typography/SVG problems, not asset problems.
@@ -1,8 +1,9 @@
1
- import { BrowserRouter, Route, Routes } from 'react-router-dom';
2
1
  import config from 'virtual:open-slide/config';
2
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
3
3
  import { Toaster } from './components/ui/sonner';
4
- import { Home } from './routes/Home';
5
- import { Slide } from './routes/Slide';
4
+ import { Home } from './routes/home';
5
+ import { Presenter } from './routes/presenter';
6
+ import { Slide } from './routes/slide';
6
7
 
7
8
  export function App() {
8
9
  return (
@@ -10,6 +11,7 @@ export function App() {
10
11
  <Routes>
11
12
  <Route path="/" element={config.build.showSlideBrowser ? <Home /> : <NotFound />} />
12
13
  <Route path="/s/:slideId" element={<Slide />} />
14
+ <Route path="/s/:slideId/presenter" element={<Presenter />} />
13
15
  <Route path="*" element={<NotFound />} />
14
16
  </Routes>
15
17
  <Toaster />
@@ -19,10 +21,10 @@ export function App() {
19
21
 
20
22
  function NotFound() {
21
23
  return (
22
- <div className="grid h-screen place-items-center bg-background px-6 text-center">
24
+ <div className="grid h-screen place-items-center bg-background px-6 text-center text-foreground">
23
25
  <div>
24
- <p className="font-mono text-sm tracking-widest text-muted-foreground uppercase">404</p>
25
- <h1 className="mt-2 text-2xl font-semibold tracking-tight">Page not found</h1>
26
+ <p className="folio">404 · not found</p>
27
+ <h1 className="mt-2 font-heading text-2xl font-semibold tracking-tight">Page not found</h1>
26
28
  </div>
27
29
  </div>
28
30
  );