@tleblancureta/prez
v0.4.2
Published
Framework-agnostic React presentation system. Author slide decks as TSX, render with a built-in viewer (keyboard nav, fullscreen, grid, PDF export). Pairs with the `prez` Claude skill for guided integration into any React app.
Downloads
1,270
Maintainers
Readme
@tleblancureta/prez
Framework-agnostic React presentation system. Author slide decks as TSX, render with a built-in viewer (keyboard nav, fullscreen, grid view, PDF export).
- 🎯 Pure React + Tailwind. No Next.js, no router coupling. Wire it to App Router, Pages Router, Vite, Remix, react-router — anything.
- 🧱 17 slide layouts out of the box (cover, agenda, story, bullets, comparison, table, timeline, calendar, checklist, takeaway, …) plus building blocks (
Callout,IconCard,NumberCard). - ⌨️ Keyboard nav (arrows / space /
f/g/?/l), fullscreen, grid thumbnail view, deep-linkable?slide=N. - 🖨️ PDF export via
html2canvas-pro+jspdf(both optional peer deps; only loaded when the user clicks download). - 🔒 Optional password gate with client-side or server-verified modes.
- 🤖 Pairs with the
prezClaude skill so an AI agent can scaffold the integration into any host app.
Status: 0.2.0 — usable but iterating. Public API is stable; skill recipes evolve.
Install
npm install @tleblancureta/prez lucide-react
# Optional, only needed if you use PDF export:
npm install html2canvas-pro jspdfRequired peer deps: react ≥ 18, react-dom ≥ 18, lucide-react, tailwindcss ≥ 3.4.
Quickstart (60 seconds)
1. Create a registry and your first deck
// src/lib/prez.ts
import { createRegistry, type Presentation } from "@tleblancureta/prez";
import { CoverSlide, BulletSlide, TakeawaySlide } from "@tleblancureta/prez";
export const prez = createRegistry();
prez.register({
slug: "intro",
title: "Hola mundo",
description: "Mi primera deck con @tleblancureta/prez",
createdAt: "2026-04-28",
slides: [
{
id: "cover",
content: <CoverSlide title="Hola mundo" subtitle="Una deck de prueba" />,
},
{
id: "bullets",
content: (
<BulletSlide
title="Tres puntos"
items={[
{ text: "Prez es framework-agnóstico" },
{ text: "Cada slide es un componente React" },
{ text: "Exporta a PDF con un click" },
]}
/>
),
},
{
id: "cta",
content: (
<TakeawaySlide
title="Listo!"
items={["Crear", "Compartir", "Iterar"]}
cta={{ label: "Empezar", url: "https://fizko.ai" }}
/>
),
},
],
});2. Render the viewer
// In any client-rendered page / route
import { SlideViewer } from "@tleblancureta/prez";
import { prez } from "@/lib/prez";
export function PrezPage({ slug }: { slug: string }) {
const presentation = prez.get(slug);
if (!presentation) return <div>Not found</div>;
return <SlideViewer slides={presentation.slides} title={presentation.title} />;
}That's it. URL deep-linking (?slide=2), keyboard nav, grid view, fullscreen, and PDF export work out of the box.
API reference
createRegistry(): Registry
Creates an isolated presentation registry. Each registry is independent — useful for tests or multi-tenant apps.
const registry = createRegistry();
registry.register(presentation); // throws on invalid shape
registry.get(slug); // → Presentation | undefined
registry.list({ includeUnlisted }); // → PresentationSummary[] sorted newest-first<SlideViewer>
Main viewer. Pure React — no router imports. Manages URL state via window.history.replaceState by default.
<SlideViewer
slides={presentation.slides}
title={presentation.title}
// Optional — for framework-controlled URL state:
currentSlide={current}
onSlideChange={setCurrent}
// Optional — branded logo top-right of every slide:
logo="/my-logo.png"
/>When currentSlide and onSlideChange are both provided, the host owns URL state (use it with useSearchParams / useRouter / etc.). Otherwise, the viewer manages it internally.
<PasswordGate>
Two modes — client-side (password baked into bundle, fine for low-stakes gating) or server-verified (call your own endpoint).
{/* Client-side */}
<PasswordGate
expectedPassword="mypassword"
onUnlock={() => setUnlocked(true)}
/>
{/* Server-verified */}
<PasswordGate
verify={async (input) => {
const res = await fetch("/api/verify-prez", {
method: "POST",
body: JSON.stringify({ password: input, slug }),
});
return res.ok;
}}
onUnlock={() => setUnlocked(true)}
/>useSlideUrlState(totalSlides, paramName?)
Optional helper for hosts that want ?slide=N URL sync without wiring their framework's router. Returns [currentSlide, setCurrentSlide].
const [current, setCurrent] = useSlideUrlState(slides.length);
return <SlideViewer slides={slides} title={title} currentSlide={current} onSlideChange={setCurrent} />;Slide formats (16:9, 4:5, 1:1, 9:16, custom)
By default, slides render at 1920×1080 (16:9) — the traditional projector / webinar / pitch-deck format. To author Instagram posts, stories, square cards, or any custom canvas, set format on the Presentation:
prez.register({
slug: "ig-launch-card",
title: "Lanzamiento — feed",
description: "Instagram feed post",
format: "4:5", // 1080×1350 (Instagram feed)
createdAt: "2026-04-28",
slides: [/* ... */],
});
prez.register({
slug: "ig-story",
title: "Story de lanzamiento",
format: "9:16", // 1080×1920 (story / reel / TikTok)
// ...
});
prez.register({
slug: "linkedin-square",
title: "Card cuadrada",
format: "1:1", // 1080×1080 (square)
// ...
});
prez.register({
slug: "billboard",
title: "Cartel afuera",
format: { width: 2400, height: 800 }, // custom — anything goes
// ...
});Behavior driven by format:
- Viewer scales to fit the slide aspect on screen (portrait, square, ultrawide all work).
- PDF export uses the exact dimensions; orientation is auto-detected (
landscapeifwidth ≥ height, elseportrait). - Grid thumbnails keep a constant ~288px width; portrait formats get taller thumbnail cards.
- Slide layout components (
CoverSlide,BulletSlide, etc.) useh-full w-fullso they adapt to any aspect — but for very different aspect ratios you may want to author custom JSX viaContentSlidefor tighter control.
If you pass currentSlide + onSlideChange (controlled mode), also pass format to <SlideViewer> directly:
<SlideViewer slides={...} title={...} format="4:5" />The package exports SLIDE_FORMATS (preset map), resolveDimensions(format), and the types SlideFormat, SlideFormatName, SlideDimensions for advanced use cases (e.g. computing scaled thumbnail sizes or building a format picker).
Mobile preview frame (Instagram chrome)
For decks meant to ship as social-media posts, set chrome: "instagram" to wrap the live viewer in an iPhone + Instagram-post mockup (status bar, account header with avatar/username, action bar, caption). Useful for previewing 4:5 / 1:1 / 9:16 decks the way they'll actually appear in-feed before exporting:
prez.register({
slug: "ig-launch",
format: "4:5",
chrome: "instagram",
chromeUsername: "miempresa",
chromeCaption: "Una caption opcional que se muestra abajo del post.",
// chromeAvatar: "/avatar.png", // optional, falls back to a gradient initial
/* ... */
});Notes:
- The chrome only affects the live viewer. PDF export always captures the bare slide so you can publish the asset directly.
- Works with any
format, but is designed for mobile-first ratios (4:5,1:1,9:16). - The phone frame has fixed dimensions (430×880) and is itself scaled to fit the viewport — so the slide looks the same regardless of screen size.
- Available chromes:
"none"(default) and"instagram". More device frames can be added later (e.g."twitter","linkedin", custom phone shells).
You can also use <InstagramFrame> directly outside the viewer if you want to embed a single mocked-up slide somewhere else (a landing page, a portfolio, etc.):
import { InstagramFrame } from "@tleblancureta/prez";
<InstagramFrame
slideContent={<MyHookSlide />}
slideWidth={1080}
slideHeight={1350}
username="miempresa"
caption="..."
/>Slide layouts
All exported from @tleblancureta/prez:
| Layout | Use case |
|---|---|
| WaitSlide | Pre-event lobby |
| CoverSlide | Title + subtitle + author + date |
| SectionSlide | Section divider with big number |
| AgendaSlide | Numbered TOC |
| StorySlide | Narrative with stat + author + quote |
| BulletSlide | Bullets + optional myth-buster |
| ContentSlide | Generic title + custom JSX children |
| TableSlide | Comparison table with colored headers |
| TimelineSlide | Horizontal timeline with active step |
| CalendarSlide | Multi-row calendar grid |
| ErrorListSlide | Numbered errors + fixes |
| ChecklistSlide | Multi-phase color-coded checklist |
| ComparisonSlide | Before/after side-by-side |
| TakeawaySlide | Key takeaways + CTA |
Building blocks (use inside ContentSlide):
Callout— green/yellow/red/blue/neutral note boxIconCard— icon + title + description cardNumberCard— big number + label
Integration recipes
Recommended: pair with the prez Claude skill. Run Claude in your project, ask it to "set up @tleblancureta/prez", and it scaffolds the right wiring for whatever stack you're on (Next App Router, Next Pages, Vite, Remix, etc.).
Manual integration recipes:
Next.js App Router
// app/prez/[slug]/page.tsx
import { notFound } from "next/navigation";
import { prez } from "@/lib/prez";
import { PrezClient } from "./PrezClient";
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const p = prez.get(slug);
if (!p) notFound();
return <PrezClient presentation={p} />;
}
// app/prez/[slug]/PrezClient.tsx
"use client";
import { useState } from "react";
import { SlideViewer, PasswordGate, type Presentation } from "@tleblancureta/prez";
export function PrezClient({ presentation }: { presentation: Presentation }) {
const [unlocked, setUnlocked] = useState(!presentation.password);
if (!unlocked) {
return (
<PasswordGate
expectedPassword={presentation.password!}
onUnlock={() => setUnlocked(true)}
/>
);
}
return <SlideViewer slides={presentation.slides} title={presentation.title} />;
}Vite + react-router
// src/routes/PrezRoute.tsx
import { useParams } from "react-router-dom";
import { SlideViewer } from "@tleblancureta/prez";
import { prez } from "@/lib/prez";
export function PrezRoute() {
const { slug } = useParams();
const p = slug ? prez.get(slug) : undefined;
if (!p) return <div>Not found</div>;
return <SlideViewer slides={p.slides} title={p.title} />;
}PDF export
html2canvas-pro + jspdf are optional peer deps. The viewer renders fine without them — you'll just get a console error if the user clicks the download button without them installed.
npm install html2canvas-pro jspdfEach slide is captured at 1920 × 1080 (the design canvas). The PDF is generated client-side and triggered as a direct download — no server round-trip needed.
Tailwind setup
Slides use Tailwind utility classes directly. Make sure your Tailwind content scan includes the package:
// tailwind.config.js (Tailwind 3)
export default {
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/@tleblancureta/prez/dist/**/*.{js,cjs}",
],
};For Tailwind 4 (CSS-config), no extra setup is needed — the JIT scans imports automatically.
License
MIT © Akashi Labs
