nextjs-slides 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -126,6 +126,7 @@ That's it. Navigate to `/slides` and you have a full slide deck.
126
126
  | `exitUrl` | `string` | — | URL for exit button (×). Shows in top-right when set. |
127
127
  | `showProgress` | `boolean` | `true` | Show dot progress indicator |
128
128
  | `showCounter` | `boolean` | `true` | Show "3 / 10" counter |
129
+ | `transition` | `boolean` | `true` | Enable default slide animations. Set `false` for custom morphs. |
129
130
  | `className` | `string` | — | Additional class for the deck container |
130
131
  | `children` | `React.ReactNode` | **required** | Route content (from Next.js) |
131
132
 
@@ -309,6 +310,29 @@ app/slides/
309
310
  demo/page.tsx ← Breakout page (no deck chrome)
310
311
  ```
311
312
 
313
+ ## Project structure
314
+
315
+ You can put all slide-related routes in a **route group** like `(demo)` or `(internal)` to keep them separate from your main app. Route groups use parentheses and do not affect the URL. You can have two `api` folders — one for your main app, one inside the group:
316
+
317
+ ```
318
+ app/
319
+ api/
320
+ ...your main app routes
321
+ (demo)/
322
+ api/
323
+ nxs-sync/
324
+ route.ts
325
+ slides/
326
+ layout.tsx
327
+ slides.tsx
328
+ notes.md
329
+ [page]/page.tsx
330
+ notes/
331
+ page.tsx
332
+ ```
333
+
334
+ URLs stay `/slides`, `/notes`, `/api/nxs-sync` — the `(demo)` segment is invisible. Update paths in `parseSpeakerNotes` and `basePath` if you move things.
335
+
312
336
  ## Styling & CSS
313
337
 
314
338
  The library **inherits** your app's theme. Primitives use Tailwind utilities that resolve to CSS variables: `--foreground`, `--background`, `--muted-foreground`, `--primary`, `--primary-foreground`, `--border`, `--muted`. Compatible with shadcn/ui and any Tailwind v4 setup that defines these.
@@ -354,6 +378,44 @@ Use `className="font-pixel"` on primitives where you want the pixel display font
354
378
 
355
379
  Slide transitions use the React 19 `<ViewTransition>` component with `addTransitionType()`. The CSS in `nextjs-slides/styles.css` defines the `::view-transition-*` animations. Override them in your own CSS to customize.
356
380
 
381
+ ### Magic Move (custom morphs)
382
+
383
+ Set `transition={false}` on `<SlideDeck>` to disable the default directional slide animation. Then add inline `viewTransitionName` styles to elements that should morph between slides:
384
+
385
+ ```tsx
386
+ function CycleBox({ children, name }: { children: React.ReactNode; name: string }) {
387
+ return (
388
+ <div
389
+ className="rounded-xl border px-7 py-3.5 text-xl font-semibold"
390
+ style={{ viewTransitionName: name }}
391
+ >
392
+ {children}
393
+ </div>
394
+ );
395
+ }
396
+
397
+ export const slides = [
398
+ <Slide key="basic">
399
+ <SlideTitle>Render Cycle</SlideTitle>
400
+ <div className="flex gap-4">
401
+ <CycleBox name="box-a">Event</CycleBox>
402
+ <CycleBox name="box-b">Commit</CycleBox>
403
+ </div>
404
+ </Slide>,
405
+
406
+ <Slide key="expanded">
407
+ <SlideTitle>Render Cycle</SlideTitle>
408
+ <div className="flex gap-4">
409
+ <CycleBox name="box-a">Event</CycleBox>
410
+ <span>loading</span>
411
+ <CycleBox name="box-b">Commit</CycleBox>
412
+ </div>
413
+ </Slide>,
414
+ ];
415
+ ```
416
+
417
+ Elements with matching `viewTransitionName` values across consecutive slides morph their position, size, and opacity automatically. New elements fade in, removed elements fade out. Requires `viewTransition: true` in your `next.config`.
418
+
357
419
  ## Troubleshooting
358
420
 
359
421
  **SlideCode syntax highlighting looks broken or colorless** — Ensure you import `nextjs-slides/styles.css` in your root layout or global CSS (see Quick Start). The `--sh-*` variables must be in scope for highlight.js tokens to display correctly.
@@ -30,7 +30,7 @@ import { SlideDeckConfig } from './types.js';
30
30
  * }
31
31
  * ```
32
32
  */
33
- declare function SlideDeck({ children, slides, basePath, exitUrl, showProgress, showCounter, syncEndpoint, className, speakerNotes: _speakerNotes, }: SlideDeckConfig & {
33
+ declare function SlideDeck({ children, slides, basePath, exitUrl, showProgress, showCounter, syncEndpoint, className, transition, speakerNotes: _speakerNotes, }: SlideDeckConfig & {
34
34
  children: React.ReactNode;
35
35
  }): react_jsx_runtime.JSX.Element;
36
36
 
@@ -27,6 +27,7 @@ function SlideDeck({
27
27
  showCounter = true,
28
28
  syncEndpoint,
29
29
  className,
30
+ transition = true,
30
31
  speakerNotes: _speakerNotes
31
32
  }) {
32
33
  const router = useRouter();
@@ -118,16 +119,16 @@ function SlideDeck({
118
119
  ViewTransition,
119
120
  {
120
121
  default: "none",
121
- enter: {
122
+ enter: transition ? {
122
123
  default: "slide-from-right",
123
124
  [TRANSITION_BACK]: "slide-from-left",
124
125
  [TRANSITION_FORWARD]: "slide-from-right"
125
- },
126
- exit: {
126
+ } : void 0,
127
+ exit: transition ? {
127
128
  default: "slide-to-left",
128
129
  [TRANSITION_BACK]: "slide-to-right",
129
130
  [TRANSITION_FORWARD]: "slide-to-left"
130
- },
131
+ } : void 0,
131
132
  children: /* @__PURE__ */ jsx("div", { children })
132
133
  },
133
134
  pathname
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/slide-deck.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { usePathname, useRouter } from 'next/navigation';\nimport {\n addTransitionType,\n useCallback,\n useEffect,\n useMemo,\n useTransition,\n ViewTransition,\n} from 'react';\nimport { cn } from './cn';\nimport { ExitIcon } from './icons';\nimport type { SlideDeckConfig } from './types';\n\nconst TRANSITION_FORWARD = 'slide-forward';\nconst TRANSITION_BACK = 'slide-back';\n\nfunction getSlideIndex(pathname: string, pattern: RegExp): number {\n const match = pathname.match(pattern);\n return match ? Number(match[1]) - 1 : 0;\n}\n\n/**\n * Top-level slide deck provider that wraps the current slide's content.\n *\n * Place this in your slides layout (e.g. `app/slides/layout.tsx`). It provides:\n * - **Keyboard navigation** — Arrow keys and Space to navigate slides.\n * - **ViewTransition animations** — Slide-in/out with directional awareness.\n * - **Progress UI** — Dots and a counter at the bottom of the viewport.\n * - **Exit button** — When `exitUrl` is set, shows an × in the top-right corner.\n * - **Presenter sync** — When `syncEndpoint` is set, POSTs the current slide\n * on navigation for `SlideNotesView` to poll.\n *\n * `SlideDeck` must be the **direct child** of the layout (no wrapper div)\n * for the deck-unveil exit animation to work correctly.\n *\n * @example\n * ```tsx\n * // app/slides/layout.tsx\n * import { SlideDeck } from 'nextjs-slides';\n * import { slides } from './slides';\n *\n * export default function SlidesLayout({ children }: { children: React.ReactNode }) {\n * return (\n * <SlideDeck slides={slides} exitUrl=\"/\" syncEndpoint=\"/api/nxs-sync\">\n * {children}\n * </SlideDeck>\n * );\n * }\n * ```\n */\nexport function SlideDeck({\n children,\n slides,\n basePath = '/slides',\n exitUrl,\n showProgress = true,\n showCounter = true,\n syncEndpoint,\n className,\n speakerNotes: _speakerNotes,\n}: SlideDeckConfig & { children: React.ReactNode }) {\n const router = useRouter();\n const pathname = usePathname();\n const [isPending, startTransition] = useTransition();\n\n const total = slides.length;\n const slideRoutePattern = useMemo(\n () =>\n new RegExp(`^${basePath.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}/(\\\\d+)$`),\n [basePath]\n );\n const isSlideRoute = slideRoutePattern.test(pathname);\n const current = useMemo(\n () => getSlideIndex(pathname, slideRoutePattern),\n [pathname, slideRoutePattern]\n );\n\n const syncSlide = useCallback(\n (slide: number) => {\n if (!syncEndpoint || !isSlideRoute) return;\n fetch(syncEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ slide, total }),\n }).catch(() => {});\n },\n [isSlideRoute, syncEndpoint, total]\n );\n\n const goTo = useCallback(\n (index: number) => {\n const clamped = Math.max(0, Math.min(index, total - 1));\n if (clamped === current) return;\n const targetSlide = clamped + 1; // 1-based for sync API\n syncSlide(targetSlide); // Immediate feedback for phone sync\n startTransition(() => {\n addTransitionType(\n clamped > current ? TRANSITION_FORWARD : TRANSITION_BACK\n );\n router.push(`${basePath}/${targetSlide}`);\n });\n },\n [basePath, current, router, startTransition, syncSlide, total]\n );\n\n useEffect(() => {\n if (!isSlideRoute) return;\n if (current > 0) router.prefetch(`${basePath}/${current}`);\n if (current < total - 1) router.prefetch(`${basePath}/${current + 2}`);\n }, [basePath, current, isSlideRoute, router, total]);\n\n useEffect(() => {\n if (!isSlideRoute) return;\n function onKeyDown(e: KeyboardEvent) {\n const target = e.target as HTMLElement;\n if (\n target.closest('[data-slide-interactive]') ||\n target.matches('input, textarea, select, [contenteditable=\"true\"]')\n ) {\n return;\n }\n if (e.key === 'ArrowRight' || e.key === ' ') {\n e.preventDefault();\n goTo(current + 1);\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n goTo(current - 1);\n }\n }\n window.addEventListener('keydown', onKeyDown);\n return () => window.removeEventListener('keydown', onKeyDown);\n }, [current, goTo, isSlideRoute]);\n\n useEffect(() => {\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, []);\n\n useEffect(() => {\n if (!isPending && isSlideRoute) {\n syncSlide(current + 1);\n }\n }, [current, isPending, isSlideRoute, syncSlide]);\n\n return (\n <ViewTransition default=\"none\" exit=\"deck-unveil\">\n <div\n id=\"slide-deck\"\n className={cn(\n 'bg-background text-foreground fixed inset-0 z-50 flex flex-col overflow-hidden font-sans select-none',\n className\n )}\n data-pending={isPending ? '' : undefined}\n >\n <div className=\"flex-1 overflow-hidden\">\n <ViewTransition\n key={pathname}\n default=\"none\"\n enter={{\n default: 'slide-from-right',\n [TRANSITION_BACK]: 'slide-from-left',\n [TRANSITION_FORWARD]: 'slide-from-right',\n }}\n exit={{\n default: 'slide-to-left',\n [TRANSITION_BACK]: 'slide-to-right',\n [TRANSITION_FORWARD]: 'slide-to-left',\n }}\n >\n <div>{children}</div>\n </ViewTransition>\n </div>\n\n {isSlideRoute && showProgress && (\n <div\n className=\"fixed bottom-8 left-1/2 z-50 flex -translate-x-1/2 items-center gap-1.5\"\n aria-label=\"Slide progress\"\n >\n {Array.from({ length: total }).map((_, i) => (\n <div\n key={i}\n className={cn(\n 'h-1 transition-all duration-300',\n i === current ? 'bg-foreground w-6' : 'bg-foreground/20 w-1'\n )}\n />\n ))}\n </div>\n )}\n\n {isSlideRoute && showCounter && (\n <div className=\"text-foreground/30 fixed right-8 bottom-8 z-50 font-mono text-xs tracking-wider\">\n {current + 1} / {total}\n </div>\n )}\n\n {isSlideRoute && exitUrl && (\n <Link\n href={exitUrl}\n className=\"text-foreground/50 hover:text-foreground fixed top-6 right-8 z-50 flex h-10 w-10 items-center justify-center rounded-md transition-colors hover:bg-foreground/10\"\n aria-label=\"Exit presentation\"\n >\n <ExitIcon />\n </Link>\n )}\n </div>\n </ViewTransition>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/slide-deck.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { usePathname, useRouter } from 'next/navigation';\nimport {\n addTransitionType,\n useCallback,\n useEffect,\n useMemo,\n useTransition,\n ViewTransition,\n} from 'react';\nimport { cn } from './cn';\nimport { ExitIcon } from './icons';\nimport type { SlideDeckConfig } from './types';\n\nconst TRANSITION_FORWARD = 'slide-forward';\nconst TRANSITION_BACK = 'slide-back';\n\nfunction getSlideIndex(pathname: string, pattern: RegExp): number {\n const match = pathname.match(pattern);\n return match ? Number(match[1]) - 1 : 0;\n}\n\n/**\n * Top-level slide deck provider that wraps the current slide's content.\n *\n * Place this in your slides layout (e.g. `app/slides/layout.tsx`). It provides:\n * - **Keyboard navigation** — Arrow keys and Space to navigate slides.\n * - **ViewTransition animations** — Slide-in/out with directional awareness.\n * - **Progress UI** — Dots and a counter at the bottom of the viewport.\n * - **Exit button** — When `exitUrl` is set, shows an × in the top-right corner.\n * - **Presenter sync** — When `syncEndpoint` is set, POSTs the current slide\n * on navigation for `SlideNotesView` to poll.\n *\n * `SlideDeck` must be the **direct child** of the layout (no wrapper div)\n * for the deck-unveil exit animation to work correctly.\n *\n * @example\n * ```tsx\n * // app/slides/layout.tsx\n * import { SlideDeck } from 'nextjs-slides';\n * import { slides } from './slides';\n *\n * export default function SlidesLayout({ children }: { children: React.ReactNode }) {\n * return (\n * <SlideDeck slides={slides} exitUrl=\"/\" syncEndpoint=\"/api/nxs-sync\">\n * {children}\n * </SlideDeck>\n * );\n * }\n * ```\n */\nexport function SlideDeck({\n children,\n slides,\n basePath = '/slides',\n exitUrl,\n showProgress = true,\n showCounter = true,\n syncEndpoint,\n className,\n transition = true,\n speakerNotes: _speakerNotes,\n}: SlideDeckConfig & { children: React.ReactNode }) {\n const router = useRouter();\n const pathname = usePathname();\n const [isPending, startTransition] = useTransition();\n\n const total = slides.length;\n const slideRoutePattern = useMemo(\n () =>\n new RegExp(`^${basePath.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}/(\\\\d+)$`),\n [basePath]\n );\n const isSlideRoute = slideRoutePattern.test(pathname);\n const current = useMemo(\n () => getSlideIndex(pathname, slideRoutePattern),\n [pathname, slideRoutePattern]\n );\n\n const syncSlide = useCallback(\n (slide: number) => {\n if (!syncEndpoint || !isSlideRoute) return;\n fetch(syncEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ slide, total }),\n }).catch(() => {});\n },\n [isSlideRoute, syncEndpoint, total]\n );\n\n const goTo = useCallback(\n (index: number) => {\n const clamped = Math.max(0, Math.min(index, total - 1));\n if (clamped === current) return;\n const targetSlide = clamped + 1; // 1-based for sync API\n syncSlide(targetSlide); // Immediate feedback for phone sync\n startTransition(() => {\n addTransitionType(\n clamped > current ? TRANSITION_FORWARD : TRANSITION_BACK\n );\n router.push(`${basePath}/${targetSlide}`);\n });\n },\n [basePath, current, router, startTransition, syncSlide, total]\n );\n\n useEffect(() => {\n if (!isSlideRoute) return;\n if (current > 0) router.prefetch(`${basePath}/${current}`);\n if (current < total - 1) router.prefetch(`${basePath}/${current + 2}`);\n }, [basePath, current, isSlideRoute, router, total]);\n\n useEffect(() => {\n if (!isSlideRoute) return;\n function onKeyDown(e: KeyboardEvent) {\n const target = e.target as HTMLElement;\n if (\n target.closest('[data-slide-interactive]') ||\n target.matches('input, textarea, select, [contenteditable=\"true\"]')\n ) {\n return;\n }\n if (e.key === 'ArrowRight' || e.key === ' ') {\n e.preventDefault();\n goTo(current + 1);\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n goTo(current - 1);\n }\n }\n window.addEventListener('keydown', onKeyDown);\n return () => window.removeEventListener('keydown', onKeyDown);\n }, [current, goTo, isSlideRoute]);\n\n useEffect(() => {\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, []);\n\n useEffect(() => {\n if (!isPending && isSlideRoute) {\n syncSlide(current + 1);\n }\n }, [current, isPending, isSlideRoute, syncSlide]);\n\n return (\n <ViewTransition default=\"none\" exit=\"deck-unveil\">\n <div\n id=\"slide-deck\"\n className={cn(\n 'bg-background text-foreground fixed inset-0 z-50 flex flex-col overflow-hidden font-sans select-none',\n className\n )}\n data-pending={isPending ? '' : undefined}\n >\n <div className=\"flex-1 overflow-hidden\">\n <ViewTransition\n key={pathname}\n default=\"none\"\n enter={transition ? {\n default: 'slide-from-right',\n [TRANSITION_BACK]: 'slide-from-left',\n [TRANSITION_FORWARD]: 'slide-from-right',\n } : undefined}\n exit={transition ? {\n default: 'slide-to-left',\n [TRANSITION_BACK]: 'slide-to-right',\n [TRANSITION_FORWARD]: 'slide-to-left',\n } : undefined}\n >\n <div>{children}</div>\n </ViewTransition>\n </div>\n\n {isSlideRoute && showProgress && (\n <div\n className=\"fixed bottom-8 left-1/2 z-50 flex -translate-x-1/2 items-center gap-1.5\"\n aria-label=\"Slide progress\"\n >\n {Array.from({ length: total }).map((_, i) => (\n <div\n key={i}\n className={cn(\n 'h-1 transition-all duration-300',\n i === current ? 'bg-foreground w-6' : 'bg-foreground/20 w-1'\n )}\n />\n ))}\n </div>\n )}\n\n {isSlideRoute && showCounter && (\n <div className=\"text-foreground/30 fixed right-8 bottom-8 z-50 font-mono text-xs tracking-wider\">\n {current + 1} / {total}\n </div>\n )}\n\n {isSlideRoute && exitUrl && (\n <Link\n href={exitUrl}\n className=\"text-foreground/50 hover:text-foreground fixed top-6 right-8 z-50 flex h-10 w-10 items-center justify-center rounded-md transition-colors hover:bg-foreground/10\"\n aria-label=\"Exit presentation\"\n >\n <ExitIcon />\n </Link>\n )}\n </div>\n </ViewTransition>\n );\n}\n"],"mappings":"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","names":[]}
package/dist/slides.css CHANGED
@@ -158,13 +158,6 @@
158
158
  }
159
159
  }
160
160
 
161
- /* Reset default view transition animations */
162
- ::view-transition-old(*) {
163
- animation: none;
164
- }
165
- ::view-transition-new(*) {
166
- animation: none;
167
- }
168
161
 
169
162
  /* Slide forward */
170
163
  ::view-transition-new(.slide-from-right) {
package/dist/types.d.ts CHANGED
@@ -17,6 +17,10 @@ interface SlideDeckConfig {
17
17
  showCounter?: boolean;
18
18
  /** API endpoint for presenter ↔ phone sync. See `SlideNotesView` and the sync route handlers. */
19
19
  syncEndpoint?: string;
20
+ /** Control the slide transition animation.
21
+ * - `true` (default) — directional slide-in/out
22
+ * - `false` — no wrapper animation (use with inline `viewTransitionName` for magic-move morphs) */
23
+ transition?: boolean;
20
24
  /** Additional className for the deck container */
21
25
  className?: string;
22
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-slides",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "description": "Composable slide deck primitives for Next.js — powered by React 19 ViewTransitions, Tailwind CSS, and highlight.js syntax highlighting.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -78,6 +78,10 @@
78
78
  ],
79
79
  "repository": {
80
80
  "type": "git",
81
- "url": "https://github.com/aurorascharff/nextjs-slides"
81
+ "url": "git+https://github.com/aurorascharff/nextjs-slides.git"
82
+ },
83
+ "homepage": "https://github.com/aurorascharff/nextjs-slides#readme",
84
+ "bugs": {
85
+ "url": "https://github.com/aurorascharff/nextjs-slides/issues"
82
86
  }
83
87
  }