presi-js
v0.0.11
Published
A modern presentation framework
Readme
Presi
Presi is a presentation framework built as a single npm package from local source.
The core idea is that presentations are normal frontend apps. Presi provides the presentation runtime, React bindings, and a framework-agnostic dev/build server around Vite.
Packages
This repo is a pnpm workspace for development only.
- The single public package is
presi-js, built topackages/presi-js. - Core runtime is imported from
presi-js/core. - React bindings are imported from
presi-js/react. - Server/dev/build APIs are imported from
presi-js/server. @presi/slides-testlives inslides/testand is the local example presentation app.
Everything under packages/ is generated build output. Do not edit it manually.
The npm package ships only dist and skills from packages/presi-js. Repository-only files like AGENTS.md and the local slides/ examples are not shipped.
Commands
Install dependencies:
pnpm installCreate a new React presentation with the single-package CLI:
npx presi-js create react my-talkShortcut:
npx presi-js react my-talkBuild the Presi packages:
pnpm buildRun library watch mode and the test presentation dev server:
pnpm devBuild the test presentation:
pnpm --filter @presi/slides-test buildType-check the test presentation:
pnpm --filter @presi/slides-test exec tsc --noEmitArchitecture
presi-js/core
presi-js/core owns the browser presentation runtime.
Responsibilities:
- Reads the slide DOM.
- Builds a per-slide timeline of steps.
- Handles hash navigation like
/#/0/2. - Handles keyboard navigation.
- Handles fullscreen shortcuts.
- Shows and hides slides.
- Shows and hides fragments.
- Runs JS step effects.
- Runs cleanup functions when effects become inactive.
- Exposes state and metadata for UI bindings.
Important concepts:
slideIndexis zero-based.stepIndexis the current step within the active slide.- Internally, some older code still uses the name
fragmentIndexforstepIndex. - Step
0is the initial state of a slide. - Fragments are elements with the
fragmentclass. - Fragments can use
data-step-indexto choose a specific step. data-fragment-indexstill works as a legacy alias.- JS effects are registered through the step registry and referenced in the DOM by
data-presi-step-id.
presi-js/react
presi-js/react provides React bindings for Presi.
Primary imports:
import { Wrapper, Slide, Step, usePresi } from "presi-js/react";Wrapper creates the core Presi instance:
<Wrapper aspectRatio="16:9">
<Slide title="Intro">Hello</Slide>
</Wrapper>Slide renders a slide <section>:
<Slide title="Intro" className="p-8">
<p>Hello</p>
</Slide>className is forwarded to the actual slide surface.
Fragments use the fragment class:
<Slide title="Fragments">
<p className="fragment" data-step-index="1">
First fragment
</p>
<p className="fragment" data-step-index="2">
Second fragment
</p>
</Slide>Step runs JS at a specific step:
<Slide title="JS Step">
<p className="fragment" data-step-index="1">
First fragment
</p>
<Step stepIndex={2} run={() => console.log("hello")} />
<p className="fragment" data-step-index="3">
Second fragment
</p>
</Slide>Slide supports lifecycle callbacks tied to slide visibility:
function IntroSlide() {
return (
<Slide
title="Intro"
onMount={() => console.log("slide active")}
onUnmount={() => console.log("slide inactive")}
>
Hello
</Slide>
);
}Internally these callbacks are registered as a step-0 effect. onUnmount runs when the slide is no longer active or when the presentation unmounts.
Transitions are opt-in through data attributes. Slides and step elements have no transition by default.
<Slide title="Intro" data-transition-out="fade-left">
<h2 className="fragment" data-step-index="1" data-transition-in="fade-up">
First point
</h2>
<p
className="fragment"
data-step-index="1"
data-transition-in="fade-up"
data-transition-in-order="2"
>
Second point
</p>
</Slide>Supported transition values:
fadefade-upfade-leftfade-rightfade-downfade-growfade-up-growfade-left-growfade-right-growfade-down-growpop
Transitions use the configured timing by default. pop runs for 400ms with cubic-bezier(.34, 1.45, .5, 1). Multiple elements entering or leaving in the same step are staggered in DOM order. Override order with data-transition-in-order or data-transition-out-order.
The transition constants and attribute names are exported from presi-js/core as PRESI_TRANSITION_CONFIG.
Override transition timing or attribute names when creating Presi:
new Presi(wrapper, {
transition: {
duration: 300,
delay: 75,
easing: "ease-out",
attributes: {
in: "data-enter",
out: "data-leave",
inOrder: "data-enter-order",
outOrder: "data-leave-order",
},
},
});React users can pass the same transition options to Wrapper:
<Wrapper
aspectRatio="16:9"
transition={{
duration: 300,
delay: 75,
}}
>
<Slide title="Intro">Hello</Slide>
</Wrapper>usePresi exposes presentation state:
const { slideIndex, stepIndex, totalSlides, totalSteps, currentSlide } =
usePresi();Current shape:
{
slideIndex: number;
stepIndex: number;
totalSlides: number;
totalSteps: number;
currentSlide: {
title: string;
};
}presi-js/server
presi-js/server provides a framework-agnostic dev/build layer on top of Vite.
It does not know about React, Vue, Svelte, or any specific UI framework. Framework support is passed through Vite plugins in the Presi config.
Supported commands for now:
presi-js devpresi-js build
PDF export is planned but not implemented yet.
Presi Server Config
A presentation app can define presi.config.ts:
import react from "@vitejs/plugin-react";
import { defineConfig } from "presi-js/server";
export default defineConfig({
entry: "Slides.tsx",
title: "Presi Test Slides",
resolveMountElement: () => document.getElementById("app"),
vite: {
plugins: [react()],
},
dev: {
port: 3000,
includeNotes: true,
},
build: {
outDir: "dist",
includeNotes: false,
},
});Config fields:
root: presentation root directory. Defaults to..entry: browser entry module. Defaults toSlides.tsx.title: HTML document title.resolveMountElement: browser function that returns the mount element.vite: Vite config merged into Presi's internal Vite config.dev.port: dev server port.dev.host: dev server host.dev.includeNotes: whether notes render in dev.build.outDir: production output directory.build.includeNotes: whether notes render in production.
If no index.html exists, Presi generates a default HTML shell with:
<div id="presi"></div>The default resolver is:
() => document.getElementById("presi")If a presentation provides its own index.html, Presi preserves it.
Presentation Entry
The presentation entry should default-export a render function.
React example:
import React from "react";
import ReactDOM from "react-dom/client";
import { Wrapper, Slide } from "presi-js/react";
const App = () => (
<Wrapper aspectRatio="16:9">
<Slide title="Hello">Hello world</Slide>
</Wrapper>
);
export default function render(mountElement: HTMLElement) {
ReactDOM.createRoot(mountElement).render(<App />);
}This keeps presi-js/server framework agnostic. Vue, Svelte, Solid, vanilla JS, or any other frontend stack can use the same shape as long as the entry exports a render function.
Notes
Speaker notes are controlled through a runtime build convention.
presi-js/server defines:
PRESI_INCLUDE_NOTESBy default:
- Dev builds include notes.
- Production builds exclude notes.
In React, Slide only renders <aside> notes when notes are enabled:
<Slide title="Intro" notes={["Speaker note"]}>
Hello
</Slide>slides/test
slides/test is the local example presentation app.
Important files:
slides/test/presi.config.ts: Presi server config.slides/test/index.html: custom HTML shell using#app.slides/test/Slides.tsx: app entry; exports the render function.slides/test/slides/: individual slide files.slides/test/theme/Slide.tsx: themed wrapper aroundpresi-js/react'sSlide.slides/test/style.css: defines local variable fonts and Tailwind layers.slides/test/fonts/: locally hosted variable font files.slides/test/tailwind.config.js: Tailwind content/theme config.slides/test/postcss.config.js: enables Tailwind and Autoprefixer.
The app is intentionally a consumer of the built packages:
import { Wrapper, Slide } from "presi-js/react";It should behave like an external app using Presi.
Tailwind And Fonts
The test app imports styles from slides/test/style.css:
@font-face {
font-family: "Nunito Sans";
src: url("./fonts/NunitoSans-Variable.ttf") format("truetype");
font-display: block;
}Tailwind scans:
slides/test/index.htmlslides/test/Slides.tsxslides/test/slides/**/*.{ts,tsx}slides/test/theme/**/*.{ts,tsx}
The test app self-hosts variable fonts under slides/test/fonts and references them from slides/test/style.css with font-display: block to avoid a flash of fallback text.
Available Tailwind font families:
className="font-heading" // Nunito variable, headings
className="font-body" // Nunito Sans variable, body textDevelopment Flow
pnpm dev runs an initial package build, then starts:
node scripts/build.mjs --watchpnpm --filter @presi/slides-test dev
When files under library/ change, esbuild rebuilds packages/presi-js. The test presentation runs through the generated CLI while its Vite config aliases imports back to local source for development.
Build Outputs
scripts/build.mjs bundles library sources with esbuild:
library/core/index.tstopackages/presi-js/dist/core.jslibrary/react/index.tstopackages/presi-js/dist/react.jslibrary/server/index.tstopackages/presi-js/dist/server.jslibrary/server/cli.tstopackages/presi-js/dist/cli.js
The script also writes .d.ts files for public package types.
Verification
Run these before committing larger changes:
pnpm build
pnpm --filter @presi/slides-test build
pnpm --filter @presi/slides-test exec tsc --noEmit
pnpm install --frozen-lockfileThe slides build may print a stale Browserslist caniuse-lite warning. That warning is unrelated to Presi.
Current Limitations
- PDF export is not implemented yet.
presi-js/servercurrently supports dev and production SPA builds only.stepIndexis still namedfragmentIndexin parts of core internals for historical reasons.- Public type declarations are currently generated by
scripts/build.mjsrather than emitted bytsc.
Skills
Consumer-facing skills are authored in root skills/ and shipped in the npm package under skills/.
presi-core: framework-agnostic Presi concepts, config, CLI, steps, notes, and builds.presi-react: authoring, styling, and debugging React presentations usingpresi-js/react.
AGENTS.md is intentionally not part of the package. It is only for contributors working on this repository.
