# Getting started (/docs/getting-started) ## Install [#install] npm pnpm yarn bun ```bash npx @open-slide/cli init my-deck cd my-deck npm run dev ``` ```bash pnpm dlx @open-slide/cli init my-deck cd my-deck pnpm run dev ``` ```bash yarn dlx @open-slide/cli init my-deck cd my-deck yarn dev ``` ```bash bun x @open-slide/cli init my-deck cd my-deck bun run dev ``` The scaffolder generates a workspace (abridged — see [open-slide init](/docs/cli/init) for the full tree): ```text my-deck/ ├── slides/ │ └── getting-started/ # your first slide ├── themes/ # shared design systems ├── .agents/skills/ # bundled agent skills ├── open-slide.config.ts # framework config ├── AGENTS.md # agent rules (CLAUDE.md symlinks to it) └── package.json ``` `npm run dev` boots the dev server on `http://localhost:5173` (or the next free port) with hot module reload, the inspector, and the assets panel. ## Author with an agent [#author-with-an-agent] Open the workspace in any agent-aware editor — Claude Code, Codex, Cursor, Windsurf, Zed, etc. From the agent prompt: ```text /create-slide for "Q2 launch — 3 chapters, mixed text density, subtle motion, dark aesthetic" ``` The skill scopes the deck (theme, aesthetic, page count, density, motion), picks an id, plans the structure, and writes the pages under `slides//index.tsx`. ## Iterate visually [#iterate-visually] 1. Click any element in the browser preview. 2. Pick **Comment** and type a note — *"use the accent colour on this title"*, *"shrink to 88px"*, *"swap to assets/cover.png"*. 3. Save. Comments are persisted as `@slide-comment` markers in the source. 4. Run `/apply-comments` in your agent. It rewrites exactly the flagged regions and clears the markers. You can also edit text, typography, and colour directly from the inspector property panel — see [Inspector](/docs/core-feature/inspector). ## Build & deploy [#build--deploy] npm pnpm yarn bun ```bash npm run build # static build to dist/ npm run preview # preview the static build locally ``` ```bash pnpm run build # static build to dist/ pnpm run preview # preview the static build locally ``` ```bash yarn build # static build to dist/ yarn preview # preview the static build locally ``` ```bash bun run build # static build to dist/ bun run preview # preview the static build locally ``` The output is a plain static site — deploy to Vercel, Cloudflare Pages, Zeabur, Netlify, or any static host. ## What's next [#whats-next] # Introduction (/docs) open-slide editor showing a deck with the inspector and assets panel docked alongside the canvas **open-slide** is a slide framework for the agent era. You describe a deck in natural language; your coding agent writes the React. open-slide handles the canvas, scaling, navigation, hot reload, present mode, and export so the agent can focus on content. Every page renders into a fixed **1920 × 1080** canvas. Slides are arbitrary React components — not a constrained DSL. npm pnpm yarn bun ```bash npx @open-slide/cli init ``` ```bash pnpm dlx @open-slide/cli init ``` ```bash yarn dlx @open-slide/cli init ``` ```bash bun x @open-slide/cli init ``` ## Why open-slide [#why-open-slide] * **Agent-native authoring.** Ships with skills that any Claude Code, Codex, or Cursor session can call. * **In-browser inspector.** Click any element in the dev server to tweak it directly in the visual editor, or leave a comment for your agent to apply. * **Assets manager + svgl.** Drop in images, search 1500+ brand logos through the integrated [svgl](https://svgl.app/) catalogue. * **Professional present mode.** Fullscreen playback, presenter view, speaker notes, timer. * **Export to static HTML or PDF.** Self-contained output — share without a server. ## A slide is a file [#a-slide-is-a-file] Each deck lives under `slides//`. The entry is `slides//index.tsx`. Assets sit alongside in `slides//assets/`. ```tsx title="slides/getting-started/index.tsx" import type { Page, SlideMeta } from '@open-slide/core'; const Cover: Page = () => (

Hello, open-slide.

); export const meta: SlideMeta = { title: 'Getting started', theme: 'corporate' }; export default [Cover] satisfies Page[]; ``` Two rules: the default export is an array of components (one per page), and each component fills its container (`width: '100%'` / `height: '100%'`). ## The 1920 × 1080 canvas [#the-1920--1080-canvas] The runtime scales the canvas uniformly to fit the viewport, so the same slide looks identical on a phone, a 4K monitor, and a beamer — every pixel coordinate you write is the coordinate that ships, in preview, present mode, and export. ## What's next [#whats-next] # Migrate from v1 to v2 (/docs/migrate-to-v2) v2 moves the whole toolchain forward: the runtime now ships **React 19** and builds with **Vite 8** (Rolldown-powered). Slides you wrote for v1 need no changes — the migration is confined to `package.json`, your Node version, and your deploy config. ## Requirements [#requirements] * **Node.js 20.19+** (or 22.12+). Vite 8 no longer supports Node 18, so `open-slide dev` and `open-slide build` won't start on older runtimes. ## Update your workspace [#update-your-workspace] Point your dependencies at the new majors: ```json title="package.json" { "dependencies": { "@open-slide/core": "^2.0.0", "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { "@types/react": "^19.2.18", "@types/react-dom": "^19.2.5" } } ``` Then reinstall and pull the latest agent skills: npm pnpm yarn bun ```bash npm install npm run sync:skills ``` ```bash pnpm install pnpm run sync:skills ``` ```bash yarn install yarn sync:skills ``` ```bash bun install bun run sync:skills ``` ### What if I keep React 18 in package.json? [#what-if-i-keep-react-18-in-packagejson] The app still runs. Core dedupes `react` and `react-dom` to its own React 19 copy at runtime, so a stale React 18 entry never loads a second copy (two copies would crash the production bundle with React error #525). Bump anyway: your editor otherwise typechecks slides against React 18 types while the runtime is React 19. ## Remove the `vite` devDependency [#remove-the-vite-devdependency] v1 scaffolds carried `"vite"` in `devDependencies` purely so hosting providers would detect a Vite project. In v2 it has to go. Core depends on Vite 8, and a stale Vite 5 entry makes package managers that hoist without checking peer ranges (bun, yarn classic) place core's Vite plugins next to the wrong copy, so `open-slide dev`, `open-slide build`, and `open-slide preview` fail with: ``` Package subpath './internal' is not defined by "exports" in .../node_modules/vite/package.json ``` npm and pnpm happen to nest the plugins correctly, but don't rely on it. `open-slide` checks for a mismatched `vite` on startup and refuses to run until it is removed, pointing back to this page. Remove `vite` from `devDependencies`, reinstall, and declare the build settings explicitly so your host still knows how to build the project: ```json title="vercel.json" { "buildCommand": "npm run build", "outputDirectory": "dist", "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } ``` ```toml title="netlify.toml" [build] command = "npm run build" publish = "dist" [[redirects]] from = "/*" to = "/index.html" status = 200 ``` ## Slide code [#slide-code] Slides are plain React components, so React 19's removals apply: `defaultProps` on function components, string refs, and `propTypes` are gone. Typical agent-authored slides use none of these — if yours do, the dev server's error overlay will point at the offending page. # open-slide build (/docs/cli/build) npm pnpm yarn bun ```bash open-slide build # or npm run build ``` ```bash open-slide build # or pnpm run build ``` ```bash open-slide build # or yarn build ``` ```bash open-slide build # or bun run build ``` Builds every deck under `slides/` into a self-contained static site at `dist/`. The output has no server requirement — drop it on any static host. ## Flags [#flags] | Flag | Default | Description | | ----------------- | ------- | ------------------------------------------ | | `--out-dir ` | `dist` | Write the static build to a custom folder. | ## Output [#output] ```text dist/ ├── index.html ├── assets/ │ └── ...hashed files... └── ... ``` ## Configuration [#configuration] Two `open-slide.config.ts` fields matter at build time: * For a public share, hide the slide browser and on-canvas UI with the `build` toggles — see [Export](/docs/core-feature/export). * If the build won't sit at the domain root (GitHub Pages project sites, a reverse-proxy folder), set `base` so asset URLs and navigation resolve against the deployed path — see [Config](/docs/reference/config). # open-slide dev (/docs/cli/dev) npm pnpm yarn bun ```bash open-slide dev # or, via the generated script npm run dev ``` ```bash open-slide dev # or, via the generated script pnpm run dev ``` ```bash open-slide dev # or, via the generated script yarn dev ``` ```bash open-slide dev # or, via the generated script bun run dev ``` Boots a Vite-powered dev server on `http://localhost:5173` (or the next free port). Hot module reload is wired up for both the runtime and your slide files — saving an `index.tsx` updates the canvas without losing slide state. ## What you get [#what-you-get] * **Slide browser** — list of every deck under `slides/`. * **Canvas** — the active page, scaled to fit the viewport. * **Inspector** — click any element to edit it visually, drop a `@slide-comment`, or both. * **Assets panel** — drag, rename, replace, plus svgl logo search. * **Present mode** — fullscreen playback with presenter view. ## Flags [#flags] | Flag | Default | Description | | ------------------- | -------------- | -------------------------------------------------------- | | `-p, --port ` | from config | Override the listening port. | | `--host [host]` | localhost only | Expose the dev server on the network. Optional host pin. | | `--open` | off | Open the browser on start. | | `--no-skills-check` | off | Skip the built-in skills drift check. | The skills check also obeys the `OPEN_SLIDE_SKIP_SKILLS_CHECK=1` environment variable — useful in CI or container builds where stdin isn't a TTY. ## Configuration [#configuration] Override the port (and a few other knobs) in `open-slide.config.ts`: ```ts title="open-slide.config.ts" import type { OpenSlideConfig } from '@open-slide/core'; const config: OpenSlideConfig = { port: 5174, slidesDir: 'slides', }; export default config; ``` Accessing the dev server through a proxied domain (Coder, Codespaces, a reverse proxy) and hitting `Blocked request. This host is not allowed.`? Set [`allowedHosts`](/docs/reference/config#remote-ide--proxied-dev-server) in the config. # open-slide init (/docs/cli/init) npm pnpm yarn bun ```bash npx @open-slide/cli init [directory] ``` ```bash pnpm dlx @open-slide/cli init [directory] ``` ```bash yarn dlx @open-slide/cli init [directory] ``` ```bash bun x @open-slide/cli init [directory] ``` If `directory` is omitted, an interactive run prompts for one (defaulting to the current folder); non-interactive runs use the current folder. If it's provided and doesn't exist, it's created. ## Flags [#flags] | Flag | Default | Description | | ------------------- | ------------- | -------------------------------------------------------------------- | | `-f, --force` | off | Scaffold into the target even if it's not empty. | | `-n, --name ` | folder name | Override the generated `package.json` name. | | `--use-npm` | auto-detected | Install with npm. | | `--use-pnpm` | auto-detected | Install with pnpm. | | `--use-yarn` | auto-detected | Install with yarn. | | `--use-bun` | auto-detected | Install with bun. | | `--no-install` | off | Skip dependency installation. You'll have to run `install` yourself. | | `--no-git` | off | Skip `git init` and the initial commit. | When no `--use-*` flag is passed, the scaffolder detects the package manager that launched it (via `npm_config_user_agent`) and falls back to `npm`. Pass an explicit flag to force one. The runtime UI language is picked in-app, not at init. ## What it generates [#what-it-generates] ```text my-deck/ ├── slides/ │ └── getting-started/ # starter slide + its assets ├── themes/ # empty — shared design systems ├── assets/ # global assets (@assets/* alias) ├── .agents/ │ └── skills/ # bundled agent skills ├── .claude/ │ └── skills/ # symlinks into .agents/skills/ ├── open-slide.config.ts # framework config ├── AGENTS.md # agent rules ├── CLAUDE.md # symlink to AGENTS.md ├── package.json ├── tsconfig.json └── netlify.toml / vercel.json # one-click deploy configs ``` ## After init [#after-init] npm pnpm yarn bun ```bash cd my-deck npm install npm run dev ``` ```bash cd my-deck pnpm install pnpm run dev ``` ```bash cd my-deck yarn install yarn dev ``` ```bash cd my-deck bun install bun run dev ``` The dev server boots, opens the slide browser, and you're ready for your agent to take over. # Overview (/docs/cli/overview) open-slide ships two CLIs: * **`@open-slide/cli`** — the scaffolder. One command (`init`) to bootstrap a new workspace. * **`@open-slide/core`** — the runtime CLI. `dev`, `build`, `preview`, and `sync:skills` for an existing workspace. After `init`, `package.json` exposes the runtime CLI under standard scripts: ```json { "scripts": { "dev": "open-slide dev", "build": "open-slide build", "preview": "open-slide preview", "sync:skills": "open-slide sync:skills" } } ``` So in day-to-day use you'll just type `npm run dev`, `npm run build`, etc. # open-slide preview (/docs/cli/preview) npm pnpm yarn bun ```bash open-slide preview # or npm run preview ``` ```bash open-slide preview # or pnpm run preview ``` ```bash open-slide preview # or yarn preview ``` ```bash open-slide preview # or bun run preview ``` Serves `dist/` on a local port so you can rehearse the deck the way it will ship — same scaling, same routing, no dev-only chrome. Run `open-slide build` first; `preview` doesn't rebuild on its own. ## Flags [#flags] | Flag | Default | Description | | ------------------- | -------------- | ------------------------------------------------------------------------ | | `-p, --port ` | `4173` | Override the listening port. The config `port` field only affects `dev`. | | `--host [host]` | localhost only | Expose the preview on the network. Optional host pin. | | `--open` | off | Open the browser on start. | # open-slide sync:skills (/docs/cli/sync-skills) npm pnpm yarn bun ```bash open-slide sync:skills # or npm run sync:skills ``` ```bash open-slide sync:skills # or pnpm run sync:skills ``` ```bash open-slide sync:skills # or yarn sync:skills ``` ```bash open-slide sync:skills # or bun run sync:skills ``` Copies the latest skill definitions from your installed `@open-slide/core` into the workspace under `.agents/skills/`, and refreshes the `.claude/skills/` symlinks so Claude Code picks them up too. Run this after upgrading `@open-slide/core` — that's how you pull in skill fixes and new skills without re-scaffolding. ## Flags [#flags] | Flag | Default | Description | | ----------- | ------- | -------------------------------------------------------- | | `--dry-run` | off | Print the add / update / unchanged plan without writing. | ## What it touches [#what-it-touches] ```text .agents/ └── skills/ └── / # canonical copy from @open-slide/core .claude/ └── skills/ └── # symlink to ../../.agents/skills/ ``` Each skill is hashed before copy, so unchanged skills are left alone and the symlink is the only thing refreshed. On platforms where symlinks fail (e.g. some Windows configurations), the command falls back to a recursive copy. ## Drift detection on `dev` [#drift-detection-on-dev] `open-slide dev` runs the same drift check at startup and offers to sync interactively — see [open-slide dev](/docs/cli/dev) for how to skip it. # Assets manager (/docs/core-feature/assets-manager) Open-slide assets panel listing slide files with the svgl logo search open Assets come in two scopes: * **Slide-local** — one-off files only used by a single deck. They live next to the deck that uses them and are imported with a relative path. * **Global** — files reused across decks or themes (company logos, presenter avatars, recurring icons). They live in a top-level `assets/` folder and are imported through the `@assets/*` Vite alias. ```text project-root/ ├── assets/ # global — shared across decks │ ├── acme.svg │ └── avatar.png └── slides/ └── q2-launch/ ├── index.tsx └── assets/ # slide-local — only this deck ├── cover.png ├── chart.svg └── intro.mp4 ``` ```tsx import cover from './assets/cover.png'; // slide-local import chart from './assets/chart.svg'; // slide-local import logo from '@assets/acme.svg'; // global const Cover = () => ( <> ); ``` Both paths run through Vite's normal asset pipeline — files get hashed and emitted on `build` like any other import. ## In-browser asset manager [#in-browser-asset-manager] The dev server has an **Assets** panel with a **Slide / Global** scope toggle at the top: * **Slide** scope reads and writes `slides//assets/`. * **Global** scope reads and writes the project root `assets/` folder. Drag files in to upload them. Rename and replace from the same pane the inspector uses to swap an element's `src` — the Replace picker carries the same scope toggle, and the rewritten `import` uses `./assets/...` or `@assets/...` to match the source. The home sidebar also exposes the global manager directly: click **Assets** under Themes to open the locked-global view at `/assets`. ## svgl logo search [#svgl-logo-search] Open-slide svgl logo search in the Assets panel, showing brand logo results for a search query Need a brand logo? The Assets panel has an **svgl** search powered by [svgl.app](https://svgl.app/). Search, pick, drop — the SVG lands in whichever scope is active (slide-local or global) and you can `import` it immediately. # Export (/docs/core-feature/export) `open-slide build` produces a plain static site under `dist/` — everything renders on the client, so no server is needed. ## Static HTML [#static-html] npm pnpm yarn bun ```bash npm run build npm run preview # serves dist/ for sanity-checking ``` ```bash pnpm run build pnpm run preview # serves dist/ for sanity-checking ``` ```bash yarn build yarn preview # serves dist/ for sanity-checking ``` ```bash bun run build bun run preview # serves dist/ for sanity-checking ``` `dist/` is portable. Drop it on Vercel, Cloudflare Pages, Netlify, Zeabur, GitHub Pages — anything that hosts files. ## From the toolbar [#from-the-toolbar] The **Export** menu in the slide toolbar exports the active deck directly: * **HTML** — a static HTML snapshot of the deck. Decks that reference assets download as a zip (HTML file + assets folder). * **PDF** — one 1920 × 1080 landscape PDF page per deck page. Not supported in Safari. * **PPTX (images)** — each page rendered as an image on a PowerPoint slide. Native editable PPTX export is not available yet. ## What gets shipped [#what-gets-shipped] The build pipeline only includes: * The decks under `slides/`. * The assets each deck imports. * The runtime — viewer, scaler, present mode (configurable; see below). You can trim the surface area for a public share by toggling fields in [`open-slide.config.ts`](/docs/reference/config): ```ts import type { OpenSlideConfig } from '@open-slide/core'; const config: OpenSlideConfig = { build: { showSlideBrowser: false, // hide the deck index showSlideUi: false, // hide on-canvas UI (toolbar etc.) allowHtmlDownload: false, // hide the export menu (HTML / PDF / PPTX) }, }; export default config; ``` # Inspector (/docs/core-feature/inspector) Open-slide inspector with the property panel docked beside the canvas, editing a selected element's typography and colour The visual editor is available during development. Slides open in edit mode: click an object to select it, drag to move it, and double-click text to edit it. Press `Enter` to edit a selected text object. Press `Escape` to return to object selection, then again to clear the selection. The **Format** toolbar button (or `i`) shows and hides the property panel. On desktop, the panel stays open while you select different objects or clear the selection, keeping the canvas in the same position. Choose **Preview** to interact with the slide and play its animations, then **Edit** to resume editing. **Present** opens presentation mode. ## Arrange objects [#arrange-objects] Click an element to select it. Drag the selection to move it, use its handles to resize it, or drag the rotation handle. Smart guides appear when an edge or center lines up with another element or the slide. Positions and sizes use slide pixels, so the same movement works at any canvas zoom. Rotated elements scale proportionally when their width and height cannot be adjusted independently without distorting the shape. The **Arrange** tab provides exact position, size, and rotation inputs, plus alignment and layer controls: * **Align** — align left, center, right, top, middle, or bottom. Choose the slide or the combined selection as the reference. * **Distribute** — select at least three elements to give them equal gaps horizontally or vertically. * **Layer** — bring forward, send backward, bring to front, or send to back within the element's CSS stacking context. * **Smart guides** — toggle snapping, or hold `Alt` to bypass it temporarily. * **Select parent** — move up to the containing element to arrange a whole component, card, or text block. Hold `Cmd` / `Ctrl` while clicking to select a child inside an already selected container. Hold `Shift` while clicking to add or remove elements from the selection. Drag the combined selection to move all selected elements together. Drag on empty canvas to select elements with a marquee. **Select all** selects the slide's editable elements without selecting both parents and their children. Width, height, and rotation inputs apply to one element at a time. | Shortcut | Action | | ----------------------------------- | ----------------------------------- | | `Enter` with a text object selected | Edit its text | | `Escape` while editing text | Return to object selection | | `Escape` with an object selected | Clear the selection | | `I` | Show or hide the Format panel | | Arrow keys | Move the selection by 1 slide pixel | | `Shift` + arrow keys | Move by 10 slide pixels | | `Shift` while dragging | Constrain movement to one axis | | `Shift` while resizing | Preserve the aspect ratio | | `Alt` while dragging | Temporarily bypass smart guides | | `Shift` while rotating | Snap to 15-degree increments | | `Escape` during a drag | Cancel the current gesture | | `Cmd` / `Ctrl` + `A` | Select all editable objects | | `Cmd` / `Ctrl` + `Z` | Undo | | `Cmd` / `Ctrl` + `Shift` + `Z` | Redo | | `Cmd` / `Ctrl` + `S` | Save pending edits | Inline text spans inherit their containing block's layout. Use **Select parent** to move that block. Elements rendered repeatedly from the same JSX definition also share source styles; select a unique parent container to arrange an instance independently. The panel explains when a selected element cannot be transformed. Layer changes preserve nested content's position and size. If shared nested content prevents this, select a positioned parent container to change layers. ## Format text and images [#format-text-and-images] The Format panel shows controls for the selected object. Use the **Text** tab for typography, text colour, and paragraph alignment; the **Image** tab for replacement and cropping; and **Style** for a shape's appearance. Switch to **Arrange** for geometry, alignment, and layers. Select a word or phrase directly on the canvas, then change its size, weight, style, or colour in the panel. The selection stays active so you can continue typing or apply another format. `Cmd` / `Ctrl` + `B` and `Cmd` / `Ctrl` + `I` toggle bold and italic while editing text. When the panel is hidden, a compact toolbar provides the same text controls beside the selection. Line height and letter spacing sit in the **Typography** section, and **Content** lets you edit the object's complete text in a text area. Two expandable sections follow: * **Comment** — leave a note for your agent. * **Source** — inspect the element tag and agent connection. Image replacement lets you choose any file in the deck's or the global assets folder; see the [assets manager](/docs/core-feature/assets-manager) for how the two scopes work. Edits buffer in memory. Press **Save** or `Cmd` / `Ctrl` + `S` to write them to source. Switching to Preview or leaving the deck also saves pending edits. Use **Undo**, **Redo**, or **Discard** to revise changes before saving. ## Comments [#comments] Sometimes the change is easier to describe than to perform. Click an element, expand **Comment** in the Format panel, and type a note: > use the accent colour on this title The inspector pins the note as an `@slide-comment` marker in the source. The marker travels with the element and is invisible at runtime. Run `/apply-comments` in your agent to turn the notes into edits — see [`/apply-comments`](/docs/skills/apply-comments). ## Selection is visible to your agent [#selection-is-visible-to-your-agent] Whenever you click, the inspector publishes the picked element to a file your agent reads via [`/current-slide`](/docs/skills/current-slide) — so *"make this bigger"* resolves to the exact JSX node with no "which one?" round-trip. # Present mode (/docs/core-feature/present-mode) Open-slide presenter view with the current slide, next-slide preview, speaker notes, and timer ## Fullscreen playback [#fullscreen-playback] From the dev server, hit **Present** (or press `f`). The current deck goes fullscreen, scaled uniformly to fit the screen: * Arrow / space / `PgDn` to advance. * `←` / `↑` / `PgUp` to go back. * `Home` / `End` to jump to first / last. * `Esc` to exit. ## Presenter view [#presenter-view] Press `p` (or the presenter button in the present control bar) to open the presenter view in a second window: * **Current slide** at the size you'll show it. * **Next slide** preview. * **Speaker notes** for the current page. * **Timer** — elapsed time since you entered present mode. Keep it on the laptop screen while the deck mirrors to the projector. ## Speaker notes [#speaker-notes] Add notes alongside the page array — an array of strings, index-aligned with the pages (use `undefined` to skip a page): ```tsx export const notes = [ 'Open with a smile. Mention the analyst quote from yesterday.', undefined, 'Pause here for questions. The chart is a hand-off.', ]; ``` # Themes (/docs/core-feature/themes) Open-slide theme editor showing a theme's palette and typography beside its live demo slide A theme is a **bundle of two paired files** under `themes/`: * `themes/.md` — agent-facing recipe: palette, typography, layout, fixed components, motion. `create-slide` reads this before authoring. * `themes/.demo.tsx` — a runnable mini-slide that demonstrates the theme. The dev UI's **Themes** panel imports it and renders it as the theme's live preview card. Both files share the same stem so the runtime can pair them automatically. If you author more than two decks you'll want them to look related — a theme captures the recipe in one place any deck can reference, and the demo makes it browsable in the dev UI before you pick one. ## File layout [#file-layout] ```text themes/ ├── corporate.md ├── corporate.demo.tsx ├── editorial.md ├── editorial.demo.tsx ├── retro-print.md └── retro-print.demo.tsx ``` The markdown is free-form. A useful template: ```md title="themes/corporate.md" --- name: Corporate description: Calm corporate look with a single accent. mode: dark --- # Corporate ## Palette - ink: #0a0a0c - accent: #4a52b5 - mint: #1f8458 ## Typography - display: 'Söhne', system-ui - mono: 'JetBrains Mono' ## Voice - Tight, declarative, never coy. - One concept per slide. ``` The demo is a normal slide module — same shape as `slides//index.tsx`, just placed under `themes/` so the runtime treats it as preview-only (it does not appear in the slides list): ```tsx title="themes/corporate.demo.tsx" import type { Page } from '@open-slide/core'; const Cover: Page = () => (/* …inline Title / Footer using theme tokens… */); const Content: Page = () => (/* … */); export default [Cover, Content]; ``` Keep the demo's inline Title/Footer/Eyebrow components verbatim in sync with the snippets in the markdown — what authors see in the Themes panel should match what `create-slide` will paste into a real slide. ## Using a theme [#using-a-theme] When you call `/create-slide`, mention the theme by name: ```text /create-slide for "Q3 board update — use the corporate theme" ``` The agent loads `themes/corporate.md`, applies the palette and type stack, and writes pages that match. ## Extracting a theme [#extracting-a-theme] Already have a deck whose look you like? Use [`/create-theme`](/docs/skills/create-theme) to extract its design tokens into a reusable theme file. # MorphElement (/docs/primitive/morph-element) `MorphElement` is the primitive for making one visual object move between two pages — a Morph Transition (Keynote calls the effect "Magic Move"). Wrap the same object on both pages and the runtime morphs it across the cut instead of fading it out and back in. ```tsx title="slides/morph/index.tsx" import { MorphElement, type Page, type SlideTransition } from '@open-slide/core'; const Token = ({ x, y, size }: { x: number; y: number; size: number }) => (
); const Start: Page = () => ; const End: Page = () => ; export const transition: SlideTransition = { duration: 700, morph: { duration: 700, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', }, }; export default [Start, End] satisfies Page[]; ``` ## Matching contract [#matching-contract] The `id` is the contract. When the outgoing and incoming pages both contain a `MorphElement` with the same `id`, the runtime measures both DOM nodes, hides the originals, animates a clone across the 1920 × 1080 canvas, then restores the incoming element. If a marked element exists only on the outgoing page, it fades out. If it exists only on the incoming page, it fades in. The runtime does not guess similar objects; unmatched ids are separate objects. ## What to wrap [#what-to-wrap] Wrap the same visual object in a new position or size: a logo, diagram node, timeline marker, product card, badge, or highlight. Keep changing copy outside the `MorphElement`. If the content changes while moving, the audience reads it as one object turning into another, not continuity. `MorphElement` works best with a single DOM child. If there is exactly one DOM child, the runtime writes the `data-osd-morph` marker onto that child. Otherwise it wraps the content in a `div`. ## Transition required [#transition-required] A Morph Transition only runs when the selected transition enables `morph`. ```ts export const transition: SlideTransition = { duration: 600, morph: true, }; ``` Use the object form when you need separate timing from the page enter/exit animation. # Page (/docs/primitive/page) `Page` is the base primitive in open-slide. A deck is just an ordered array of zero-prop React components, and each component is one page in the runtime, presenter view, overview grid, thumbnails, and exports. ```tsx title="slides/q2-launch/index.tsx" import { type Page, type SlideMeta, useSlidePageNumber } from '@open-slide/core'; const Cover: Page = () => { const { current, total } = useSlidePageNumber(); return (

{current} / {total}

Q2 launch

); }; export const meta: SlideMeta = { title: 'Q2 launch', }; export default [Cover] satisfies Page[]; ``` ## Contract [#contract] * A `Page` takes no props. Pull page context from hooks such as `useSlidePageNumber()`. * The module default export is the display order: `export default [Cover, Body] satisfies Page[]`. * The root element should fill the canvas with `width: '100%'` and `height: '100%'`. * Every page renders into a fixed `1920 × 1080` canvas. The runtime scales the whole canvas uniformly for preview, present mode, and export. ## Layout model [#layout-model] Write pages as fixed-format compositions, not responsive web pages. Pixel coordinates are stable because the canvas size never changes. Use absolute positioning when exact placement matters, and use flex/grid when a section needs internal rhythm. The runtime may mount a page in more than one surface at the same time: current canvas, thumbnail rail, overview grid, theme preview, or export. Keep render output deterministic, avoid depending on the browser viewport, and guard browser-only side effects when a page also needs to export cleanly. ## Per-page overrides [#per-page-overrides] `Page` can carry a `transition` property. Use it only when one page needs a different animation than the module-level default. ```tsx import type { Page, SlideTransition } from '@open-slide/core'; const Cover: Page = () =>
; const coverTransition: SlideTransition = { duration: 240, enter: { keyframes: [ { opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'translateY(0)' }, ], }, }; Cover.transition = coverTransition; ``` See [Transition](/docs/primitive/transition) for the animation model. # Step (/docs/primitive/step) `Steps` and `Step` let one `Page` reveal content one beat at a time. The page index does not change while stepping; the navigation controller consumes forward/backward input until the page has no more local steps. ```tsx title="slides/product-story/index.tsx" import { Step, Steps, type Page } from '@open-slide/core'; const Problem: Page = () => (

The audience reads faster than you speak.

Showing every bullet at once invites pre-reading.

Reveal the setup, then the consequence, then the turn.

The final page state still reads as one complete composition.

); ``` ## Reveal rules [#reveal-rules] * `Step` must be a direct child of `Steps`. A nested `Step`, or a `Step` without a `Steps` parent, renders fully revealed. * Non-`Step` children inside `Steps` render immediately. * Multiple `Steps` groups on one page compose in document order. The first group finishes before the second starts, and backward navigation unwinds in reverse. * `Step` fades in over `duration` milliseconds. The default is `180`, and reduced motion collapses it to an instant reveal. ```tsx

Always visible

First reveal Second reveal
``` ## Entry state [#entry-state] The starting state depends on how the audience lands on the page: | Entry | Initial step state | | ------------------------------ | ------------------------------------- | | Forward from the previous page | Empty, then builds one step at a time | | Backward from the next page | Fully revealed | | Overview / jump navigation | Fully revealed | Design the final composed state first, then choose which pieces deserve to be withheld. A thumbnail, overview jump, or backward navigation should not look like a broken blank page. ## When to use it [#when-to-use-it] Use `Step` when order changes comprehension: a payoff list, a before/after, a diagram build, or a narrative reveal. Avoid wrapping every page by habit. A hero title, quote, chart, or image-led page often reads better when shown whole. # Transition (/docs/primitive/transition) `SlideTransition` describes what happens when the active `Page` changes. There is no default animation: pages snap unless the deck exports a transition or an incoming page defines its own override. ```tsx title="slides/q2-launch/index.tsx" import type { Page, SlideTransition } from '@open-slide/core'; const Cover: Page = () =>
; const Agenda: Page = () =>
; export const transition: SlideTransition = { duration: 260, exit: { keyframes: [{ opacity: 1 }, { opacity: 1 }], }, enter: { easing: 'cubic-bezier(0, 0, 0.2, 1)', keyframes: [ { opacity: 0, transform: 'translateY(6px)' }, { opacity: 1, transform: 'translateY(0)' }, ], }, }; export default [Cover, Agenda] satisfies Page[]; ``` `prefers-reduced-motion: reduce` is honored automatically. The outgoing page stays opaque for the whole cut and the incoming page fades in on top of it, so a page with its own background never dips through the deck background. The runtime ignores `opacity` in `exit` keyframes unless the transition sets `throughBackground: true` — see [SlideTransition](/docs/reference/slide-transitions#background). ## Where it lives [#where-it-lives] There are two declaration surfaces: * `export const transition` in the slide module sets the deck default. * `Page.transition = …` sets an override for one page. The incoming page wins. Navigating `Cover → Agenda` uses `Agenda.transition ?? module.transition`; the selected `exit` runs on `Cover`, and `enter` runs on `Agenda`. Navigating backward uses the page you are returning to. ```tsx const Cover: Page = () =>
; const Agenda: Page = () =>
; export const transition: SlideTransition = { duration: 220, enter: { keyframes: [{ opacity: 0 }, { opacity: 1 }], }, }; Agenda.transition = { duration: 320, enter: { keyframes: [ { opacity: 0, transform: 'scale(0.98)' }, { opacity: 1, transform: 'scale(1)' }, ], }, }; ``` ## Direction [#direction] During a page transition, the wrapper exposes `--osd-dir` (`1` / `-1`) and `data-osd-dir` (`forward` / `backward`) so a keyframe can mirror itself on backward navigation. See [SlideTransition](/docs/reference/slide-transitions) for the details. ## Morph [#morph] `SlideTransition.morph` enables [MorphElement](/docs/primitive/morph-element) matching for objects that should visually continue across two pages. ```ts export const transition: SlideTransition = { duration: 600, morph: { duration: 600, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', }, }; ``` For the full schema and a reusable family of examples, see [SlideTransition](/docs/reference/slide-transitions). # open-slide.config.ts (/docs/reference/config) The config lives at the workspace root and is fully optional — every field has a sensible default. ```ts title="open-slide.config.ts" import type { OpenSlideConfig } from '@open-slide/core'; const config: OpenSlideConfig = {}; export default config; ``` ## Schema [#schema] ### `build` [#build] Every toggle is on by default and only takes effect in the static build; the dev server always shows the full UI. ## Common recipes [#common-recipes] ### Public share [#public-share] Hide the slide browser, on-canvas UI, and export menu with the `build` toggles — see [Export](/docs/core-feature/export) for the annotated example. ### Host under a subpath [#host-under-a-subpath] Set `base` to deploy the build under a sub-directory instead of the domain root — GitHub Pages project sites, a reverse-proxy folder, an intranet path. Use a leading and trailing slash: ```ts const config: OpenSlideConfig = { base: '/my-slides/', }; ``` The value is passed straight to Vite's `base` and to React Router's `basename`, so client-side navigation matches the deployed path. ### Remote IDE / proxied dev server [#remote-ide--proxied-dev-server] Cloud workspaces (Coder, Codespaces, Gitpod, …) reach the dev server through a generated domain, so Vite blocks the request with `Blocked request. This host (…) is not allowed.` Allow your workspace domain — a leading dot matches every subdomain: ```ts const config: OpenSlideConfig = { allowedHosts: ['.my-workspace.example.com'], }; ``` Or set `allowedHosts: true` to accept any host. Only do this on a trusted network — it disables Vite's DNS-rebinding protection. The value is passed straight to Vite's [`server.allowedHosts`](https://vite.dev/config/server-options#server-allowedhosts) and also applies to `open-slide preview`. ### Organizing many decks [#organizing-many-decks] `slidesDir` is a single path and decks must sit directly inside it (`slides//index.tsx` — nested folders are not discovered). To group decks, use the in-app sidebar folders (stored in `slides/.folders.json`). # ImagePlaceholder (/docs/reference/image-placeholder) `ImagePlaceholder` is a small runtime component that renders a sized, dashed box with a hint label. Drop it into a slide whenever the layout calls for a real image — a product screenshot, a team photo, a customer dashboard — that you don't have on disk yet. ```tsx title="slides/q2-launch/index.tsx" import { ImagePlaceholder } from '@open-slide/core'; const Hero = () => ( ); export default [Hero]; ``` The user uploads the real file via the [Assets panel](/docs/core-feature/assets-manager), clicks the placeholder in the [inspector](/docs/core-feature/inspector), and picks **Replace…** — the JSX is rewritten to a real `` and the asset import is added at the top of the file. The replacement keeps the placeholder's `width` and `height` so the image lands in exactly the same slot. ## Props [#props] Any other `
` attribute is passed through too (except `children`, `style`, and `className`, which the component owns). When both `width` and `height` are set, the placeholder renders the dimensions under the hint so you can eyeball aspect ratios while drafting. ## Sizing [#sizing] Pick one of two modes: * **Fixed box** — pass `width` and `height` when the layout has a hard image slot (a hero card, a logo lockup at a known size). * **Fill the parent** — omit both when the placeholder sits inside a flex or grid cell that already controls its size. The component defaults to `width: 100%; height: 100%`. ```tsx // Fixed // Fill a flex/grid cell
``` The `hint` should describe the *content* the user has to supply ("Q3 revenue chart"), not its layout role ("hero image"). ## Replacement workflow [#replacement-workflow] The inspector's **Replace…** action is wired to a Vite-plugin op called `replace-placeholder-with-image`. Given an asset path, it: 1. Confirms the targeted JSX is an ``. 2. Adds `import from './assets/'` if the asset isn't already imported. 3. Rewrites the element to: ```tsx } alt="" style={{ width, height, objectFit: 'cover', objectPosition: '50% 50%' }} /> ``` The asset path must start with `./assets/` (slide-local) or `@assets/` (global). If you skip the inspector and want to swap the placeholder by hand, the snippet above is exactly what to write. ## Cropping [#cropping] After replacement the image is a regular `` with `objectFit: 'cover'`, which means the asset's aspect ratio rarely matches the slot exactly. To reframe the visible region: * **Double-click** the image while the inspector is active, or * Click it once and choose **Crop** in the property panel. The crop dialog writes `objectFit` (Fill = `cover`, Fit = `contain`) and stores the crop rectangle as an `objectViewBox: inset(…)` value on the same inline `style` block — no asset is mutated, so you can reopen the dialog later and adjust. ## When to use it [#when-to-use-it] Use a placeholder only when a *specific concrete* image is required by the deck's topic — a product screenshot, a team photo, a customer logo. Don't use it for decoration or generic stock-photo filler; if a typographic or iconographic solution would do, prefer that. # Slide module exports (/docs/reference/slide-meta) ```tsx title="slides//index.tsx" import type { Page, SlideMeta } from '@open-slide/core'; const Cover: Page = () =>
{/* ... */}
; export const meta: SlideMeta = { title: 'Cover', theme: 'corporate', }; export const notes = ['Open with the analyst quote.']; export default [Cover] satisfies Page[]; ``` ## Default export [#default-export] `Page[]` — the array of components, in display order. Each component fills its container; the runtime renders one at a time onto the 1920×1080 canvas. ```ts type Page = ComponentType & { transition?: SlideTransition; }; ``` ## `meta` (optional) [#meta-optional] ## `notes` (optional) [#notes-optional] Plain-text strings (line breaks preserved), index-aligned with the page array. Surfaced in the presenter view — see [Present mode](/docs/core-feature/present-mode). ```ts export const notes: (string | undefined)[] = [ 'Open with a smile.', undefined, 'Pause for questions.', ]; ``` ## `transition` (optional) [#transition-optional] A module-level `SlideTransition` becomes the default animation between every page in the deck. Per-page overrides are assigned on the `Page` component itself. See [SlideTransition](/docs/reference/slide-transitions) for the full schema. ```ts import type { SlideTransition } from '@open-slide/core'; export const transition: SlideTransition = { duration: 200, exit: { /* … */ }, enter: { /* … */ }, }; ``` ## Imports from `@open-slide/core` [#imports-from-open-slidecore] ```ts import { CANVAS_WIDTH, // 1920 CANVAS_HEIGHT, // 1080 ImagePlaceholder, // sized empty placeholder — see /docs/reference/image-placeholder MorphElement, // match an element across pages — see /docs/primitive/morph-element } from '@open-slide/core'; import type { MorphElementProps, Page, SlideMeta, SlideModule, SlideTransition, // per-page / module-level animations — see /docs/reference/slide-transitions MorphTransition, TransitionPhase, ImagePlaceholderProps, } from '@open-slide/core'; ``` # SlideTransition (/docs/reference/slide-transitions) The framework can run an enter/exit animation between every page change. There is **no default** — pages snap unless you declare a `SlideTransition`. For where to declare one and how precedence works, see [Transition](/docs/primitive/transition); this page is the schema plus a reusable set of examples. `prefers-reduced-motion: reduce` is honored automatically — you don't write a fallback. ## Schema [#schema] ### `SlideTransition` [#slidetransition] ### `TransitionPhase` [#transitionphase] ### `MorphTransition` [#morphtransition] ## Background [#background] The outgoing and incoming pages are stacked in one container painted with the deck background (`--osd-bg`), incoming on top. Every page paints its own background, so while the outgoing page stays opaque the incoming page fades in over it and the container colour is never visible. An exit that fades to `opacity: 0` would expose the container before the enter has covered it — a dip to black on any page whose background differs from `--osd-bg`. The runtime therefore drops `opacity` from `exit` keyframes; `transform` and `filter` still apply. Write the fade on `enter` and keep the exit as a hold: ```ts export const transition: SlideTransition = { duration: 260, exit: { keyframes: [{ opacity: 1 }, { opacity: 1 }] }, enter: { keyframes: [{ opacity: 0 }, { opacity: 1 }] }, }; ``` Set `throughBackground: true` when the dip is the point — a section break that goes to black between two pages that share the deck background. Exit keyframes then run exactly as written. ```ts const breath: SlideTransition = { duration: 460, throughBackground: true, exit: { duration: 180, keyframes: [{ opacity: 1 }, { opacity: 0 }] }, enter: { duration: 240, delay: 300, keyframes: [{ opacity: 0 }, { opacity: 1 }] }, }; ``` ## Direction hook [#direction-hook] The framework writes two values on the transition wrapper so a single keyframe can mirror itself on backward navigation: | Surface | Forward | Backward | | -------------- | --------- | ---------- | | `--osd-dir` | `1` | `-1` | | `data-osd-dir` | `forward` | `backward` | Use `--osd-dir` inside `calc()`: ```tsx { transform: 'translateX(calc(var(--osd-dir, 1) * 8px))' }, { transform: 'translateX(0)' }, ``` Most decks shouldn't mirror on backward navigation — reach for the direction hook only when the motion has a literal direction (a horizontal advance, a chapter sweep). ## Morph [#morph] `morph` enables [MorphElement](/docs/primitive/morph-element) matching: wrap the same visual object on two pages with the same `id` and the runtime animates it across the cut instead of fading it. Marked elements that exist on only one of the two pages fade in or out automatically. Use the object form when the morph needs its own timing: ```ts export const transition: SlideTransition = { duration: 900, enter: { keyframes: [{ opacity: 0 }, { opacity: 1 }] }, exit: { keyframes: [{ opacity: 1 }, { opacity: 1 }] }, morph: { duration: 900, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', }, }; ``` # /apply-comments (/docs/skills/apply-comments) `/apply-comments` closes the visual feedback loop. You drop comments through the inspector; the skill resolves them into code edits. ## What it does [#what-it-does] 1. Scans the workspace for `@slide-comment` markers. 2. For each marker, reads the surrounding code and the comment text. 3. Edits the file to satisfy the comment. 4. Removes the marker. ```text › /apply-comments ✓ 3 markers · slides/q2-launch/index.tsx ✓ 1 marker · slides/intro/index.tsx ``` ## Marker semantics [#marker-semantics] A `@slide-comment` marker is a normal JSX/JS comment that the inspector inserts next to the element you commented on. It's invisible at runtime — the rendered output never changes. The marker carries: * The user's note (the message you typed). * A pointer to the element (so an agent reading the file knows what *this* meant). ## When to use it [#when-to-use-it] After a batch of inspector comments — typically right before saving the deck for the day or right before a rehearsal. Refreshing the dev server after `/apply-comments` shows the new state with the markers cleared. If a comment is ambiguous, the agent applies the smallest reasonable interpretation and notes the assumption in its summary. Comments it can't resolve at all are left in place and reported as skipped — refine the wording and re-run. # /create-slide (/docs/skills/create-slide) `/create-slide` is the entry-point skill for any new deck. ## What it does [#what-it-does] 1. **Theme** — if `themes/` contains any themes, asks you to pick one (or none). A picked theme settles the visual direction. See [Themes](/docs/core-feature/themes). 2. **Scoping** — asks four questions: aesthetic direction (skipped when a theme is picked), page count, text density, motion vs. static. 3. **ID & structure** — picks a kebab-case id, plans the page list. 4. **Authoring** — writes `slides//index.tsx` page by page, deferring to [`/slide-authoring`](/docs/skills/slide-authoring) for the *how*. ## When to use it [#when-to-use-it] For any new deck. Even if you plan to hand-author, kick off with `/create-slide` to seed the folder, the page array, and the meta block. ```text /create-slide for "Q2 launch — 3 chapters, mixed text density, subtle motion, dark aesthetic" ``` ## What you get [#what-you-get] A working `slides//index.tsx` with one component per page, a `meta` export, and assets imported from `slides//assets/`. Drop into the dev server and iterate from there. # /create-theme (/docs/skills/create-theme) `/create-theme` codifies a visual recipe into a reusable theme file. Run it once your decks start sharing a visual language. ## What it does [#what-it-does] Writes a **theme bundle** under `themes/` — two paired files that share a stem, always produced together: * `themes/.md` — palette, type stack, layout vocabulary, fixed components, motion, and voice notes that `/create-slide` reads on its next run. * `themes/.demo.tsx` — a runnable 2–3 page mini-slide that the dev UI's **Themes** panel renders as the theme's live preview. See [Themes](/docs/core-feature/themes) for the file format and how a theme is consumed. The input can be: * **An existing deck.** The skill scans `slides//index.tsx`, distils the recurring tokens, and writes them out. * **A brief.** Describe the system in chat (*"warm editorial, serif display, high-contrast palette"*) and the skill bootstraps the bundle from scratch. * **Image references.** Pass screenshots or mood-board images and the skill extracts palette, type, and layout cues from them. Once the bundle exists, `/create-slide` offers the theme on its next run — see [Themes](/docs/core-feature/themes#using-a-theme). # /current-slide (/docs/skills/current-slide) `/current-slide` is what makes deictic prompts work. *"This page"*, *"this heading"*, *"the slide I'm on"* — the skill teaches the agent how to resolve them to a concrete slide id, page index, and (when set) the JSX element you've picked in the [inspector](/docs/core-feature/inspector). ## How it works [#how-it-works] The dev server writes the user's current view to `node_modules/.open-slide/current.json` on every navigation and selection change: * `slideId` / `pageIndex` — the deck folder name and the active page. * `pagePath` — the source file of that page. * `selection` — line/column, tag name, and a text snippet of the JSX node picked in the inspector (`null` when nothing is picked). The skill reads that file before asking *which slide?* ## When it fires [#when-it-fires] You don't usually invoke it directly. `/slide-authoring` cross-references it, so any "fix this" prompt routes through it automatically. It collapses the most common round-trip in agent-driven editing: ```text You: make this title smaller Agent: which title? You: the one on slide 3 ``` With `/current-slide`, the agent already knows. See [`/apply-comments`](/docs/skills/apply-comments) for how this folds into the broader inspector loop. # Overview (/docs/skills/overview) open-slide treats your coding agent as a first-class user: every workspace ships with agent rules and a set of skills the agent invokes by name. ## What ships in the workspace [#what-ships-in-the-workspace] `npx @open-slide/cli init` generates an `AGENTS.md` (and `CLAUDE.md`) at the root with the framework's hard rules, plus five skills under `.agents/skills/` (with `.claude/skills/` symlinks for Claude Code): * [`/create-slide`](/docs/skills/create-slide) * [`/slide-authoring`](/docs/skills/slide-authoring) * [`/apply-comments`](/docs/skills/apply-comments) * [`/create-theme`](/docs/skills/create-theme) * [`/current-slide`](/docs/skills/current-slide) The skills are kept inside the workspace (not the agent's global config) so they version with the project. `open-slide sync:skills` re-syncs them from the package if you upgrade. ## The hard rules (`AGENTS.md`) [#the-hard-rules-agentsmd] * Slides go under `slides//`. * Entry is `slides//index.tsx`. * Assets sit in `slides//assets/`. * Don't touch `package.json`, `open-slide.config.ts`, or other slides. * Don't add dependencies. Use only React and standard web APIs. These constraints keep an agent from accidentally installing a UI library, rewriting global config, or stomping on another deck. ## Bring your own agent [#bring-your-own-agent] The framework is agent-agnostic. Any tool that can edit React files in a workspace can author slides — Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Windsurf, and Zed all work. To author cleanly, the agent needs to: 1. **Read the rules.** `AGENTS.md` (and `CLAUDE.md`) at the workspace root define the file contract and constraints. 2. **Find the skills.** The five skills above live under `.agents/skills/` (or wherever that agent looks for skills). 3. **Edit files.** That's it. There is no per-agent SDK. If your agent of choice doesn't have a skills system, paste the `slide-authoring` skill into context manually and ask the agent to follow its rules. `AGENTS.md` is the canonical rules file — it follows the cross-tool convention the agents above already look for. `CLAUDE.md` is a symlink to it (a copy on Windows) so Claude Code picks up the same rules automatically. # /slide-authoring (/docs/skills/slide-authoring) `/slide-authoring` is the canonical spec for everything inside `slides//` — the file contract, the 1920×1080 canvas, type scale, spacing, palette rules, and per-primitive references. `/create-slide` and `/apply-comments` defer to it for the *how*. You don't usually invoke `slide-authoring` directly. Other skills do. ## What it ships [#what-it-ships] The defaults below are opinionated so an agent doesn't have to reinvent the look of every deck. Override any of it per-deck, or codify your overrides in a [theme](/docs/core-feature/themes). ### Type scale [#type-scale] A 1920×1080 stage pairs well with bigger type than the web is used to: | Role | Size (px) | | --------------- | --------- | | Hero title | 140 – 200 | | Section heading | 80 – 120 | | Page heading | 56 – 80 | | Body | 32 – 44 | | Caption / label | 22 – 28 | ### Spacing [#spacing] * Content padding: 100–160 px from the canvas edges. * Line-height: 1.2 for headings, 1.5–1.7 for body. * The canvas doesn't scroll — the skill makes the agent budget every page's vertical space against the 1080 px height before writing JSX, and split into two pages rather than shrink type to fit. ### Visual direction [#visual-direction] * **Palette** — 1 background, 1 primary text, 1 accent, 1 muted. * **Typography** — one display font + one body font; system stack unless the user specifies. * **One grid per deck** — a single content padding held across every page. * **One aesthetic** — minimal, editorial, retro, brutalist, … — chosen up front and never mixed. For a recipe-driven design system, write a theme — see [Themes](/docs/core-feature/themes).