nextjs-slides 0.5.0 → 0.5.2

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
@@ -247,6 +247,24 @@ export default function NotesPage() {
247
247
 
248
248
  Open your phone on `http://<your-ip>:3000/notes` (same network). The deck POSTs the current slide to the sync endpoint on every navigation; the notes view polls it every 500ms.
249
249
 
250
+ ### Demo notes (extra sections after the slides)
251
+
252
+ Add more `---` sections after the last slide's notes — these become demo notes you can step through on your phone after the presentation ends:
253
+
254
+ ```md
255
+ ...last slide notes
256
+
257
+ ---
258
+
259
+ Open the counter demo. Show how useState drives the count.
260
+
261
+ ---
262
+
263
+ Switch to the editor. Walk through adding a new slide.
264
+ ```
265
+
266
+ The notes view auto-follows the deck during slides. Once you tap "Next" past the last slide, you enter demo notes territory (the header switches to "Demo 1 / 2") and the phone stops auto-syncing so you control it manually.
267
+
250
268
  > **Note:** The sync state lives in server memory — designed for `next dev` or single-server deployments. It won't persist across serverless function invocations.
251
269
 
252
270
  ## Custom Base Path & Multiple Decks
@@ -312,7 +330,6 @@ Use `className="font-pixel"` on primitives where you want the pixel display font
312
330
 
313
331
  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.
314
332
 
315
-
316
333
  ## Troubleshooting
317
334
 
318
335
  **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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/parse-speaker-notes.ts"],"sourcesContent":["/**\n * Parse speaker notes from a markdown string.\n *\n * Format: one section per slide, separated by `---` on its own line.\n * Empty sections produce `null` (no notes for that slide).\n *\n * @example\n * ```md\n * Welcome everyone. This is the opening slide.\n *\n * ---\n *\n * Talk about the base container here.\n *\n * ---\n *\n * ---\n *\n * Slide 4 notes. Slide 3 had none.\n * ```\n *\n * @example\n * ```tsx\n * // slides/layout.tsx (server component — can use fs)\n * import fs from 'fs';\n * import path from 'path';\n * import { SlideDeck, parseSpeakerNotes } from 'nextjs-slides';\n * import { slides } from './slides';\n *\n * const notes = parseSpeakerNotes(\n * fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8'),\n * );\n *\n * export default function SlidesLayout({ children }: { children: React.ReactNode }) {\n * return <SlideDeck slides={slides} speakerNotes={notes}>{children}</SlideDeck>;\n * }\n * ```\n */\nexport function parseSpeakerNotes(markdown: string): (string | null)[] {\n return markdown\n .split(/^---$/m)\n .map(section => {\n const trimmed = section.trim();\n return trimmed.length > 0 ? trimmed : null;\n });\n}\n"],"mappings":"AAsCO,SAAS,kBAAkB,UAAqC;AACrE,SAAO,SACJ,MAAM,QAAQ,EACd,IAAI,aAAW;AACd,UAAM,UAAU,QAAQ,KAAK;AAC7B,WAAO,QAAQ,SAAS,IAAI,UAAU;AAAA,EACxC,CAAC;AACL;","names":[]}
1
+ {"version":3,"sources":["../src/parse-speaker-notes.ts"],"sourcesContent":["/**\n * Parse speaker notes from a markdown string.\n *\n * Format: one section per slide, separated by `---` on its own line.\n * Empty sections produce `null` (no notes for that slide).\n *\n * @example\n * ```md\n * Welcome everyone. This is the opening slide.\n *\n * ---\n *\n * Talk about the base container here.\n *\n * ---\n *\n * ---\n *\n * Slide 4 notes. Slide 3 had none.\n * ```\n *\n * @example\n * ```tsx\n * // slides/layout.tsx (server component — can use fs)\n * import fs from 'fs';\n * import path from 'path';\n * import { SlideDeck, parseSpeakerNotes } from 'nextjs-slides';\n * import { slides } from './slides';\n *\n * const notes = parseSpeakerNotes(\n * fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8'),\n * );\n *\n * export default function SlidesLayout({ children }: { children: React.ReactNode }) {\n * return <SlideDeck slides={slides} speakerNotes={notes}>{children}</SlideDeck>;\n * }\n * ```\n */\nexport function parseSpeakerNotes(markdown: string): (string | null)[] {\n return markdown.split(/^---$/m).map((section) => {\n const trimmed = section.trim();\n return trimmed.length > 0 ? trimmed : null;\n });\n}\n"],"mappings":"AAsCO,SAAS,kBAAkB,UAAqC;AACrE,SAAO,SAAS,MAAM,QAAQ,EAAE,IAAI,CAAC,YAAY;AAC/C,UAAM,UAAU,QAAQ,KAAK;AAC7B,WAAO,QAAQ,SAAS,IAAI,UAAU;AAAA,EACxC,CAAC;AACH;","names":[]}
@@ -24,7 +24,7 @@ function Slide({
24
24
  "div",
25
25
  {
26
26
  className: cn(
27
- "nxs-slide relative flex h-dvh w-dvw flex-col justify-center gap-8 overflow-hidden px-12 py-20 sm:px-24 md:px-32 lg:px-40",
27
+ "nxs-slide relative flex h-dvh w-dvw flex-col justify-center gap-8 px-12 py-20 sm:px-24 md:px-32 lg:px-40",
28
28
  align === "center" && "items-center text-center",
29
29
  align === "left" && "items-start text-left",
30
30
  className
@@ -35,7 +35,7 @@ function Slide({
35
35
  "div",
36
36
  {
37
37
  className: cn(
38
- "relative z-10 flex min-w-0 max-w-4xl flex-col gap-10",
38
+ "relative z-10 flex max-w-4xl flex-col gap-10",
39
39
  align === "center" && "items-center",
40
40
  align === "left" && "items-start"
41
41
  ),
@@ -51,11 +51,11 @@ function SlideSplitLayout({
51
51
  right,
52
52
  className
53
53
  }) {
54
- return /* @__PURE__ */ jsxs("div", { className: cn("nxs-slide relative flex h-dvh w-dvw overflow-hidden", className), children: [
54
+ return /* @__PURE__ */ jsxs("div", { className: cn("nxs-slide relative flex h-dvh w-dvw", className), children: [
55
55
  /* @__PURE__ */ jsx("div", { className: "border-foreground/10 pointer-events-none absolute inset-4 border sm:inset-6", "aria-hidden": true }),
56
- /* @__PURE__ */ jsx("div", { className: "relative z-10 flex min-w-0 w-1/2 flex-col justify-center overflow-x-auto px-12 py-20 sm:px-16 md:px-20 lg:px-24", children: left }),
56
+ /* @__PURE__ */ jsx("div", { className: "nxs-slide-split-col relative z-10 flex w-1/2 flex-col justify-center px-12 py-20 sm:px-16 md:px-20 lg:px-24", children: left }),
57
57
  /* @__PURE__ */ jsx("div", { className: "bg-foreground/10 absolute top-4 bottom-4 left-1/2 z-10 w-px sm:top-6 sm:bottom-6", "aria-hidden": true }),
58
- /* @__PURE__ */ jsx("div", { className: "relative z-10 flex min-w-0 w-1/2 flex-col justify-center overflow-x-auto px-12 py-20 sm:px-16 md:px-20 lg:px-24", children: right })
58
+ /* @__PURE__ */ jsx("div", { className: "nxs-slide-split-col relative z-10 flex w-1/2 flex-col justify-center px-12 py-20 sm:px-16 md:px-20 lg:px-24", children: right })
59
59
  ] });
60
60
  }
61
61
  function SlideTitle({ children, className }) {
@@ -89,9 +89,9 @@ function SlideHeaderBadge({ children, className }) {
89
89
  function SlideCode({ children, className, title }) {
90
90
  const lang = title?.split(".").pop();
91
91
  const html = highlightCode(children, lang);
92
- return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0 w-full max-w-2xl", className), children: [
92
+ return /* @__PURE__ */ jsxs("div", { className: cn("nxs-code-wrapper", className), children: [
93
93
  title && /* @__PURE__ */ jsx("div", { className: "text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase", children: title }),
94
- /* @__PURE__ */ jsx("pre", { className: "nxs-code-block min-w-0 w-full max-w-full overflow-x-auto border p-4 text-left font-mono leading-[1.7] sm:p-6", style: { fontSize: "clamp(0.75rem, 1.5vw + 0.5rem, 0.875rem)" }, children: /* @__PURE__ */ jsx("code", { dangerouslySetInnerHTML: { __html: html } }) })
94
+ /* @__PURE__ */ jsx("pre", { className: "nxs-code-block", children: /* @__PURE__ */ jsx("code", { dangerouslySetInnerHTML: { __html: html } }) })
95
95
  ] });
96
96
  }
97
97
  function SlideList({ children, className }) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/primitives.tsx"],"sourcesContent":["import hljs from 'highlight.js/lib/core';\nimport javascript from 'highlight.js/lib/languages/javascript';\nimport typescript from 'highlight.js/lib/languages/typescript';\nimport xml from 'highlight.js/lib/languages/xml';\nimport { cn } from './cn';\n\nhljs.registerLanguage('javascript', javascript);\nhljs.registerLanguage('typescript', typescript);\nhljs.registerLanguage('xml', xml);\n\nfunction highlightCode(code: string, lang?: string): string {\n if (!lang) return hljs.highlight(code, { language: 'typescript' }).value;\n const language = lang === 'ts' || lang === 'tsx' ? 'typescript' : lang;\n const registered = hljs.getLanguage(language);\n if (!registered) return hljs.highlight(code, { language: 'typescript' }).value;\n return hljs.highlight(code, { language }).value;\n}\nimport { SlideDemoContent } from './slide-demo-content';\nimport type { SlideAlign } from './types';\n\nexport function Slide({\n children,\n align = 'center',\n className,\n}: {\n children: React.ReactNode;\n align?: SlideAlign;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n 'nxs-slide relative flex h-dvh w-dvw flex-col justify-center gap-8 overflow-hidden px-12 py-20 sm:px-24 md:px-32 lg:px-40',\n align === 'center' && 'items-center text-center',\n align === 'left' && 'items-start text-left',\n className,\n )}\n >\n <div className=\"border-foreground/10 pointer-events-none absolute inset-4 border sm:inset-6\" aria-hidden />\n <div\n className={cn(\n 'relative z-10 flex min-w-0 max-w-4xl flex-col gap-10',\n align === 'center' && 'items-center',\n align === 'left' && 'items-start',\n )}\n >\n {children}\n </div>\n </div>\n );\n}\n\nexport function SlideSplitLayout({\n left,\n right,\n className,\n}: {\n left: React.ReactNode;\n right: React.ReactNode;\n className?: string;\n}) {\n return (\n <div className={cn('nxs-slide relative flex h-dvh w-dvw overflow-hidden', className)}>\n <div className=\"border-foreground/10 pointer-events-none absolute inset-4 border sm:inset-6\" aria-hidden />\n <div className=\"relative z-10 flex min-w-0 w-1/2 flex-col justify-center overflow-x-auto px-12 py-20 sm:px-16 md:px-20 lg:px-24\">\n {left}\n </div>\n <div className=\"bg-foreground/10 absolute top-4 bottom-4 left-1/2 z-10 w-px sm:top-6 sm:bottom-6\" aria-hidden />\n <div className=\"relative z-10 flex min-w-0 w-1/2 flex-col justify-center overflow-x-auto px-12 py-20 sm:px-16 md:px-20 lg:px-24\">{right}</div>\n </div>\n );\n}\n\nexport function SlideTitle({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <h1\n className={cn('text-foreground text-4xl font-extrabold sm:text-5xl md:text-6xl lg:text-7xl', className)}\n style={{ letterSpacing: '-0.04em' }}\n >\n {children}\n </h1>\n );\n}\n\nexport function SlideSubtitle({ children, className }: { children: React.ReactNode; className?: string }) {\n return <p className={cn('text-muted-foreground text-lg sm:text-xl md:text-2xl', className)}>{children}</p>;\n}\n\nexport function SlideBadge({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <span\n className={cn(\n 'bg-foreground text-background inline-block w-fit shrink-0 rounded-full px-4 py-1.5 text-sm font-semibold tracking-wide',\n className,\n )}\n >\n {children}\n </span>\n );\n}\n\nexport function SlideHeaderBadge({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <div className={cn('flex items-center gap-3', className)}>\n <span className=\"text-foreground text-xl font-semibold tracking-tight italic sm:text-2xl\">{children}</span>\n </div>\n );\n}\n\nexport function SlideCode({ children, className, title }: { children: string; className?: string; title?: string }) {\n const lang = title?.split('.').pop();\n const html = highlightCode(children, lang);\n\n return (\n <div className={cn('min-w-0 w-full max-w-2xl', className)}>\n {title && <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase\">{title}</div>}\n <pre className=\"nxs-code-block min-w-0 w-full max-w-full overflow-x-auto border p-4 text-left font-mono leading-[1.7] sm:p-6\" style={{ fontSize: 'clamp(0.75rem, 1.5vw + 0.5rem, 0.875rem)' }}>\n <code dangerouslySetInnerHTML={{ __html: html }} />\n </pre>\n </div>\n );\n}\n\nexport function SlideList({ children, className }: { children: React.ReactNode; className?: string }) {\n return <ul className={cn('flex flex-col gap-4 text-left', className)}>{children}</ul>;\n}\n\nexport function SlideListItem({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <li className={cn('text-foreground/70 flex items-start gap-3 text-lg sm:text-xl', className)}>\n <span className=\"bg-foreground/40 mt-2 block h-1.5 w-1.5 shrink-0 rounded-full\" aria-hidden />\n <span>{children}</span>\n </li>\n );\n}\n\nexport function SlideNote({ children, className }: { children: React.ReactNode; className?: string }) {\n return <p className={cn('text-muted-foreground/50 mt-4 text-sm', className)}>{children}</p>;\n}\n\nexport function SlideDemo({\n children,\n className,\n label,\n}: {\n children: React.ReactNode;\n className?: string;\n label?: string;\n}) {\n return (\n <div data-slide-interactive className={cn('min-w-0 w-full max-w-2xl', className)}>\n {label && <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase\">{label}</div>}\n <div className=\"border-foreground/10 bg-foreground/[0.03] min-w-0 w-full max-w-full border p-4 sm:p-6\">\n <SlideDemoContent>{children}</SlideDemoContent>\n </div>\n </div>\n );\n}\n\nexport function SlideStatementList({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('flex min-w-0 w-full flex-col', className)}>{children}</div>;\n}\n\nexport function SlideStatement({\n title,\n description,\n className,\n}: {\n title: string;\n description?: string;\n className?: string;\n}) {\n return (\n <div className={cn('border-foreground/10 border-t px-8 py-8 last:border-b sm:px-12 md:px-16', className)}>\n <h3 className=\"text-foreground text-lg font-bold sm:text-xl md:text-2xl\">{title}</h3>\n {description && <p className=\"text-muted-foreground mt-1 text-sm sm:text-base\">{description}</p>}\n </div>\n );\n}\n\nexport function SlideSpeaker({\n name,\n title,\n avatar,\n className,\n}: {\n name: string;\n title: string;\n /** Image URL or path for the speaker avatar. Falls back to placeholder when omitted. */\n avatar?: string;\n className?: string;\n}) {\n return (\n <div className={cn('flex items-center gap-4', className)}>\n <div\n className={cn(\n 'h-12 w-12 shrink-0 overflow-hidden rounded-full',\n avatar ? 'relative' : 'bg-foreground/15 border-foreground/20 border',\n )}\n aria-hidden\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : null}\n </div>\n <div>\n <p className=\"text-foreground/90 text-sm font-medium tracking-widest uppercase\">{name}</p>\n <p className=\"text-muted-foreground text-sm tracking-wider uppercase\">{title}</p>\n </div>\n </div>\n );\n}\n\nexport function SlideSpeakerGrid({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('grid grid-cols-1 gap-6 sm:grid-cols-2', className)}>{children}</div>;\n}\n\nexport function SlideSpeakerList({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('flex flex-col gap-6', className)}>{children}</div>;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/primitives.tsx"],"sourcesContent":["import hljs from 'highlight.js/lib/core';\nimport javascript from 'highlight.js/lib/languages/javascript';\nimport typescript from 'highlight.js/lib/languages/typescript';\nimport xml from 'highlight.js/lib/languages/xml';\nimport { cn } from './cn';\n\nhljs.registerLanguage('javascript', javascript);\nhljs.registerLanguage('typescript', typescript);\nhljs.registerLanguage('xml', xml);\n\nfunction highlightCode(code: string, lang?: string): string {\n if (!lang) return hljs.highlight(code, { language: 'typescript' }).value;\n const language = lang === 'ts' || lang === 'tsx' ? 'typescript' : lang;\n const registered = hljs.getLanguage(language);\n if (!registered) return hljs.highlight(code, { language: 'typescript' }).value;\n return hljs.highlight(code, { language }).value;\n}\nimport { SlideDemoContent } from './slide-demo-content';\nimport type { SlideAlign } from './types';\n\nexport function Slide({\n children,\n align = 'center',\n className,\n}: {\n children: React.ReactNode;\n align?: SlideAlign;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n 'nxs-slide relative flex h-dvh w-dvw flex-col justify-center gap-8 px-12 py-20 sm:px-24 md:px-32 lg:px-40',\n align === 'center' && 'items-center text-center',\n align === 'left' && 'items-start text-left',\n className,\n )}\n >\n <div className=\"border-foreground/10 pointer-events-none absolute inset-4 border sm:inset-6\" aria-hidden />\n <div\n className={cn(\n 'relative z-10 flex max-w-4xl flex-col gap-10',\n align === 'center' && 'items-center',\n align === 'left' && 'items-start',\n )}\n >\n {children}\n </div>\n </div>\n );\n}\n\nexport function SlideSplitLayout({\n left,\n right,\n className,\n}: {\n left: React.ReactNode;\n right: React.ReactNode;\n className?: string;\n}) {\n return (\n <div className={cn('nxs-slide relative flex h-dvh w-dvw', className)}>\n <div className=\"border-foreground/10 pointer-events-none absolute inset-4 border sm:inset-6\" aria-hidden />\n <div className=\"nxs-slide-split-col relative z-10 flex w-1/2 flex-col justify-center px-12 py-20 sm:px-16 md:px-20 lg:px-24\">\n {left}\n </div>\n <div className=\"bg-foreground/10 absolute top-4 bottom-4 left-1/2 z-10 w-px sm:top-6 sm:bottom-6\" aria-hidden />\n <div className=\"nxs-slide-split-col relative z-10 flex w-1/2 flex-col justify-center px-12 py-20 sm:px-16 md:px-20 lg:px-24\">{right}</div>\n </div>\n );\n}\n\nexport function SlideTitle({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <h1\n className={cn('text-foreground text-4xl font-extrabold sm:text-5xl md:text-6xl lg:text-7xl', className)}\n style={{ letterSpacing: '-0.04em' }}\n >\n {children}\n </h1>\n );\n}\n\nexport function SlideSubtitle({ children, className }: { children: React.ReactNode; className?: string }) {\n return <p className={cn('text-muted-foreground text-lg sm:text-xl md:text-2xl', className)}>{children}</p>;\n}\n\nexport function SlideBadge({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <span\n className={cn(\n 'bg-foreground text-background inline-block w-fit shrink-0 rounded-full px-4 py-1.5 text-sm font-semibold tracking-wide',\n className,\n )}\n >\n {children}\n </span>\n );\n}\n\nexport function SlideHeaderBadge({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <div className={cn('flex items-center gap-3', className)}>\n <span className=\"text-foreground text-xl font-semibold tracking-tight italic sm:text-2xl\">{children}</span>\n </div>\n );\n}\n\nexport function SlideCode({ children, className, title }: { children: string; className?: string; title?: string }) {\n const lang = title?.split('.').pop();\n const html = highlightCode(children, lang);\n\n return (\n <div className={cn('nxs-code-wrapper', className)}>\n {title && <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase\">{title}</div>}\n <pre className=\"nxs-code-block\">\n <code dangerouslySetInnerHTML={{ __html: html }} />\n </pre>\n </div>\n );\n}\n\nexport function SlideList({ children, className }: { children: React.ReactNode; className?: string }) {\n return <ul className={cn('flex flex-col gap-4 text-left', className)}>{children}</ul>;\n}\n\nexport function SlideListItem({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <li className={cn('text-foreground/70 flex items-start gap-3 text-lg sm:text-xl', className)}>\n <span className=\"bg-foreground/40 mt-2 block h-1.5 w-1.5 shrink-0 rounded-full\" aria-hidden />\n <span>{children}</span>\n </li>\n );\n}\n\nexport function SlideNote({ children, className }: { children: React.ReactNode; className?: string }) {\n return <p className={cn('text-muted-foreground/50 mt-4 text-sm', className)}>{children}</p>;\n}\n\nexport function SlideDemo({\n children,\n className,\n label,\n}: {\n children: React.ReactNode;\n className?: string;\n label?: string;\n}) {\n return (\n <div data-slide-interactive className={cn('min-w-0 w-full max-w-2xl', className)}>\n {label && <div className=\"text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase\">{label}</div>}\n <div className=\"border-foreground/10 bg-foreground/[0.03] min-w-0 w-full max-w-full border p-4 sm:p-6\">\n <SlideDemoContent>{children}</SlideDemoContent>\n </div>\n </div>\n );\n}\n\nexport function SlideStatementList({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('flex min-w-0 w-full flex-col', className)}>{children}</div>;\n}\n\nexport function SlideStatement({\n title,\n description,\n className,\n}: {\n title: string;\n description?: string;\n className?: string;\n}) {\n return (\n <div className={cn('border-foreground/10 border-t px-8 py-8 last:border-b sm:px-12 md:px-16', className)}>\n <h3 className=\"text-foreground text-lg font-bold sm:text-xl md:text-2xl\">{title}</h3>\n {description && <p className=\"text-muted-foreground mt-1 text-sm sm:text-base\">{description}</p>}\n </div>\n );\n}\n\nexport function SlideSpeaker({\n name,\n title,\n avatar,\n className,\n}: {\n name: string;\n title: string;\n /** Image URL or path for the speaker avatar. Falls back to placeholder when omitted. */\n avatar?: string;\n className?: string;\n}) {\n return (\n <div className={cn('flex items-center gap-4', className)}>\n <div\n className={cn(\n 'h-12 w-12 shrink-0 overflow-hidden rounded-full',\n avatar ? 'relative' : 'bg-foreground/15 border-foreground/20 border',\n )}\n aria-hidden\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : null}\n </div>\n <div>\n <p className=\"text-foreground/90 text-sm font-medium tracking-widest uppercase\">{name}</p>\n <p className=\"text-muted-foreground text-sm tracking-wider uppercase\">{title}</p>\n </div>\n </div>\n );\n}\n\nexport function SlideSpeakerGrid({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('grid grid-cols-1 gap-6 sm:grid-cols-2', className)}>{children}</div>;\n}\n\nexport function SlideSpeakerList({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('flex flex-col gap-6', className)}>{children}</div>;\n}\n"],"mappings":"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","names":[]}
@@ -4,11 +4,12 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  * Phone-friendly speaker notes view that stays in sync with the presenter.
5
5
  *
6
6
  * Polls the sync endpoint to track the current slide and displays the
7
- * corresponding note. Open this page on your phone while presenting.
7
+ * corresponding note. Notes beyond the slide count are treated as "demo notes"
8
+ * — advance through them manually using the on-screen buttons.
8
9
  *
9
10
  * @example
10
11
  * ```tsx
11
- * // app/slides/notes/page.tsx
12
+ * // app/notes/page.tsx
12
13
  * import fs from 'fs';
13
14
  * import path from 'path';
14
15
  * import { parseSpeakerNotes, SlideNotesView } from 'nextjs-slides';
@@ -23,7 +24,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
23
24
  * ```
24
25
  */
25
26
  declare function SlideNotesView({ notes, syncEndpoint, pollInterval, }: {
26
- /** Speaker notes array (same index as slides). Typically from `parseSpeakerNotes()`. */
27
+ /** Speaker notes array. Indices 0…slides-1 match slides; extras are demo notes. */
27
28
  notes: (string | null)[];
28
29
  /** API endpoint created with the sync route handlers. */
29
30
  syncEndpoint: string;
@@ -1,22 +1,28 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useState } from "react";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
4
  function SlideNotesView({
5
5
  notes,
6
6
  syncEndpoint,
7
7
  pollInterval = 500
8
8
  }) {
9
- const [currentSlide, setCurrentSlide] = useState(1);
9
+ const [noteIndex, setNoteIndex] = useState(0);
10
10
  const [totalSlides, setTotalSlides] = useState(1);
11
11
  const [connected, setConnected] = useState(false);
12
+ const manualOverride = useRef(false);
12
13
  const poll = useCallback(async () => {
13
14
  try {
14
15
  const res = await fetch(syncEndpoint, { cache: "no-store" });
15
16
  if (!res.ok) return;
16
17
  const data = await res.json();
17
- setCurrentSlide(data.slide);
18
+ const syncIndex = data.slide - 1;
18
19
  setTotalSlides(data.total);
19
20
  setConnected(true);
21
+ setNoteIndex((prev) => {
22
+ if (manualOverride.current && prev >= data.total) return prev;
23
+ manualOverride.current = false;
24
+ return syncIndex;
25
+ });
20
26
  } catch {
21
27
  setConnected(false);
22
28
  }
@@ -26,8 +32,24 @@ function SlideNotesView({
26
32
  const id = setInterval(poll, pollInterval);
27
33
  return () => clearInterval(id);
28
34
  }, [poll, pollInterval]);
29
- const noteIndex = currentSlide - 1;
35
+ const goNext = useCallback(() => {
36
+ setNoteIndex((prev) => {
37
+ if (prev >= notes.length - 1) return prev;
38
+ manualOverride.current = true;
39
+ return prev + 1;
40
+ });
41
+ }, [notes.length]);
42
+ const goPrev = useCallback(() => {
43
+ setNoteIndex((prev) => {
44
+ if (prev <= 0) return prev;
45
+ manualOverride.current = true;
46
+ return prev - 1;
47
+ });
48
+ }, []);
30
49
  const currentNote = noteIndex >= 0 && noteIndex < notes.length ? notes[noteIndex] : null;
50
+ const inDemoNotes = noteIndex >= totalSlides;
51
+ const displayNumber = noteIndex + 1;
52
+ const label = inDemoNotes ? `Demo ${displayNumber - totalSlides} / ${notes.length - totalSlides}` : `Slide ${displayNumber} / ${totalSlides}`;
31
53
  return /* @__PURE__ */ jsxs(
32
54
  "div",
33
55
  {
@@ -53,12 +75,7 @@ function SlideNotesView({
53
75
  color: "#737373"
54
76
  },
55
77
  children: [
56
- /* @__PURE__ */ jsxs("span", { children: [
57
- "Slide ",
58
- currentSlide,
59
- " / ",
60
- totalSlides
61
- ] }),
78
+ /* @__PURE__ */ jsx("span", { children: label }),
62
79
  /* @__PURE__ */ jsx(
63
80
  "span",
64
81
  {
@@ -96,6 +113,59 @@ function SlideNotesView({
96
113
  }
97
114
  ) : /* @__PURE__ */ jsx("p", { style: { fontSize: "18px", color: "#525252", fontStyle: "italic" }, children: "No notes for this slide." })
98
115
  }
116
+ ),
117
+ /* @__PURE__ */ jsxs(
118
+ "div",
119
+ {
120
+ style: {
121
+ display: "flex",
122
+ gap: "12px",
123
+ padding: "16px 20px",
124
+ borderTop: "1px solid #262626"
125
+ },
126
+ children: [
127
+ /* @__PURE__ */ jsx(
128
+ "button",
129
+ {
130
+ onClick: goPrev,
131
+ disabled: noteIndex <= 0,
132
+ style: {
133
+ flex: 1,
134
+ padding: "14px",
135
+ fontSize: "16px",
136
+ fontWeight: 500,
137
+ border: "1px solid #333",
138
+ borderRadius: "10px",
139
+ backgroundColor: noteIndex <= 0 ? "#111" : "#1a1a1a",
140
+ color: noteIndex <= 0 ? "#444" : "#e5e5e5",
141
+ cursor: noteIndex <= 0 ? "default" : "pointer",
142
+ WebkitTapHighlightColor: "transparent"
143
+ },
144
+ children: "\u2190 Prev"
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsx(
148
+ "button",
149
+ {
150
+ onClick: goNext,
151
+ disabled: noteIndex >= notes.length - 1,
152
+ style: {
153
+ flex: 1,
154
+ padding: "14px",
155
+ fontSize: "16px",
156
+ fontWeight: 500,
157
+ border: "1px solid #333",
158
+ borderRadius: "10px",
159
+ backgroundColor: noteIndex >= notes.length - 1 ? "#111" : "#1a1a1a",
160
+ color: noteIndex >= notes.length - 1 ? "#444" : "#e5e5e5",
161
+ cursor: noteIndex >= notes.length - 1 ? "default" : "pointer",
162
+ WebkitTapHighlightColor: "transparent"
163
+ },
164
+ children: "Next \u2192"
165
+ }
166
+ )
167
+ ]
168
+ }
99
169
  )
100
170
  ]
101
171
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/slide-notes-view.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\n\n/**\n * Phone-friendly speaker notes view that stays in sync with the presenter.\n *\n * Polls the sync endpoint to track the current slide and displays the\n * corresponding note. Open this page on your phone while presenting.\n *\n * @example\n * ```tsx\n * // app/slides/notes/page.tsx\n * import fs from 'fs';\n * import path from 'path';\n * import { parseSpeakerNotes, SlideNotesView } from 'nextjs-slides';\n *\n * const notes = parseSpeakerNotes(\n * fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8'),\n * );\n *\n * export default function NotesPage() {\n * return <SlideNotesView notes={notes} syncEndpoint=\"/api/nxs-sync\" />;\n * }\n * ```\n */\nexport function SlideNotesView({\n notes,\n syncEndpoint,\n pollInterval = 500,\n}: {\n /** Speaker notes array (same index as slides). Typically from `parseSpeakerNotes()`. */\n notes: (string | null)[];\n /** API endpoint created with the sync route handlers. */\n syncEndpoint: string;\n /** Polling interval in ms. Defaults to 500. */\n pollInterval?: number;\n}) {\n const [currentSlide, setCurrentSlide] = useState(1);\n const [totalSlides, setTotalSlides] = useState(1);\n const [connected, setConnected] = useState(false);\n\n const poll = useCallback(async () => {\n try {\n const res = await fetch(syncEndpoint, { cache: 'no-store' });\n if (!res.ok) return;\n const data = await res.json();\n setCurrentSlide(data.slide);\n setTotalSlides(data.total);\n setConnected(true);\n } catch {\n setConnected(false);\n }\n }, [syncEndpoint]);\n\n useEffect(() => {\n poll();\n const id = setInterval(poll, pollInterval);\n return () => clearInterval(id);\n }, [poll, pollInterval]);\n\n const noteIndex = currentSlide - 1;\n const currentNote = noteIndex >= 0 && noteIndex < notes.length ? notes[noteIndex] : null;\n\n return (\n <div\n style={{\n minHeight: '100dvh',\n display: 'flex',\n flexDirection: 'column',\n backgroundColor: '#0a0a0a',\n color: '#e5e5e5',\n fontFamily: 'system-ui, -apple-system, sans-serif',\n }}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 20px',\n borderBottom: '1px solid #262626',\n fontSize: '13px',\n color: '#737373',\n }}\n >\n <span>\n Slide {currentSlide} / {totalSlides}\n </span>\n <span\n style={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n backgroundColor: connected ? '#22c55e' : '#ef4444',\n }}\n />\n </div>\n\n <div\n style={{\n flex: 1,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '24px 20px',\n }}\n >\n {currentNote ? (\n <p\n style={{\n fontSize: 'clamp(18px, 4vw, 28px)',\n lineHeight: 1.6,\n maxWidth: '640px',\n whiteSpace: 'pre-wrap',\n }}\n >\n {currentNote}\n </p>\n ) : (\n <p style={{ fontSize: '18px', color: '#525252', fontStyle: 'italic' }}>\n No notes for this slide.\n </p>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAsFQ,SAGA,KAHA;AApFR,SAAS,aAAa,WAAW,gBAAgB;AAwB1C,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,eAAe;AACjB,GAOG;AACD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,CAAC;AAClD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAEhD,QAAM,OAAO,YAAY,YAAY;AACnC,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,cAAc,EAAE,OAAO,WAAW,CAAC;AAC3D,UAAI,CAAC,IAAI,GAAI;AACb,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,sBAAgB,KAAK,KAAK;AAC1B,qBAAe,KAAK,KAAK;AACzB,mBAAa,IAAI;AAAA,IACnB,QAAQ;AACN,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,YAAU,MAAM;AACd,SAAK;AACL,UAAM,KAAK,YAAY,MAAM,YAAY;AACzC,WAAO,MAAM,cAAc,EAAE;AAAA,EAC/B,GAAG,CAAC,MAAM,YAAY,CAAC;AAEvB,QAAM,YAAY,eAAe;AACjC,QAAM,cAAc,aAAa,KAAK,YAAY,MAAM,SAAS,MAAM,SAAS,IAAI;AAEpF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,WAAW;AAAA,QACX,SAAS;AAAA,QACT,eAAe;AAAA,QACf,iBAAiB;AAAA,QACjB,OAAO;AAAA,QACP,YAAY;AAAA,MACd;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,cACL,SAAS;AAAA,cACT,YAAY;AAAA,cACZ,gBAAgB;AAAA,cAChB,SAAS;AAAA,cACT,cAAc;AAAA,cACd,UAAU;AAAA,cACV,OAAO;AAAA,YACT;AAAA,YAEA;AAAA,mCAAC,UAAK;AAAA;AAAA,gBACG;AAAA,gBAAa;AAAA,gBAAI;AAAA,iBAC1B;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAO;AAAA,oBACL,OAAO;AAAA,oBACP,QAAQ;AAAA,oBACR,cAAc;AAAA,oBACd,iBAAiB,YAAY,YAAY;AAAA,kBAC3C;AAAA;AAAA,cACF;AAAA;AAAA;AAAA,QACF;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,cACL,MAAM;AAAA,cACN,SAAS;AAAA,cACT,YAAY;AAAA,cACZ,gBAAgB;AAAA,cAChB,SAAS;AAAA,YACX;AAAA,YAEC,wBACC;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO;AAAA,kBACL,UAAU;AAAA,kBACV,YAAY;AAAA,kBACZ,UAAU;AAAA,kBACV,YAAY;AAAA,gBACd;AAAA,gBAEC;AAAA;AAAA,YACH,IAEA,oBAAC,OAAE,OAAO,EAAE,UAAU,QAAQ,OAAO,WAAW,WAAW,SAAS,GAAG,sCAEvE;AAAA;AAAA,QAEJ;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/slide-notes-view.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Phone-friendly speaker notes view that stays in sync with the presenter.\n *\n * Polls the sync endpoint to track the current slide and displays the\n * corresponding note. Notes beyond the slide count are treated as \"demo notes\"\n * — advance through them manually using the on-screen buttons.\n *\n * @example\n * ```tsx\n * // app/notes/page.tsx\n * import fs from 'fs';\n * import path from 'path';\n * import { parseSpeakerNotes, SlideNotesView } from 'nextjs-slides';\n *\n * const notes = parseSpeakerNotes(\n * fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8'),\n * );\n *\n * export default function NotesPage() {\n * return <SlideNotesView notes={notes} syncEndpoint=\"/api/nxs-sync\" />;\n * }\n * ```\n */\nexport function SlideNotesView({\n notes,\n syncEndpoint,\n pollInterval = 500,\n}: {\n /** Speaker notes array. Indices 0…slides-1 match slides; extras are demo notes. */\n notes: (string | null)[];\n /** API endpoint created with the sync route handlers. */\n syncEndpoint: string;\n /** Polling interval in ms. Defaults to 500. */\n pollInterval?: number;\n}) {\n const [noteIndex, setNoteIndex] = useState(0);\n const [totalSlides, setTotalSlides] = useState(1);\n const [connected, setConnected] = useState(false);\n const manualOverride = useRef(false);\n\n const poll = useCallback(async () => {\n try {\n const res = await fetch(syncEndpoint, { cache: 'no-store' });\n if (!res.ok) return;\n const data = await res.json();\n const syncIndex = (data.slide as number) - 1;\n setTotalSlides(data.total);\n setConnected(true);\n\n setNoteIndex(prev => {\n if (manualOverride.current && prev >= data.total) return prev;\n manualOverride.current = false;\n return syncIndex;\n });\n } catch {\n setConnected(false);\n }\n }, [syncEndpoint]);\n\n useEffect(() => {\n poll();\n const id = setInterval(poll, pollInterval);\n return () => clearInterval(id);\n }, [poll, pollInterval]);\n\n const goNext = useCallback(() => {\n setNoteIndex(prev => {\n if (prev >= notes.length - 1) return prev;\n manualOverride.current = true;\n return prev + 1;\n });\n }, [notes.length]);\n\n const goPrev = useCallback(() => {\n setNoteIndex(prev => {\n if (prev <= 0) return prev;\n manualOverride.current = true;\n return prev - 1;\n });\n }, []);\n\n const currentNote = noteIndex >= 0 && noteIndex < notes.length ? notes[noteIndex] : null;\n const inDemoNotes = noteIndex >= totalSlides;\n const displayNumber = noteIndex + 1;\n\n const label = inDemoNotes\n ? `Demo ${displayNumber - totalSlides} / ${notes.length - totalSlides}`\n : `Slide ${displayNumber} / ${totalSlides}`;\n\n return (\n <div\n style={{\n minHeight: '100dvh',\n display: 'flex',\n flexDirection: 'column',\n backgroundColor: '#0a0a0a',\n color: '#e5e5e5',\n fontFamily: 'system-ui, -apple-system, sans-serif',\n }}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 20px',\n borderBottom: '1px solid #262626',\n fontSize: '13px',\n color: '#737373',\n }}\n >\n <span>{label}</span>\n <span\n style={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n backgroundColor: connected ? '#22c55e' : '#ef4444',\n }}\n />\n </div>\n\n <div\n style={{\n flex: 1,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '24px 20px',\n }}\n >\n {currentNote ? (\n <p\n style={{\n fontSize: 'clamp(18px, 4vw, 28px)',\n lineHeight: 1.6,\n maxWidth: '640px',\n whiteSpace: 'pre-wrap',\n }}\n >\n {currentNote}\n </p>\n ) : (\n <p style={{ fontSize: '18px', color: '#525252', fontStyle: 'italic' }}>\n No notes for this slide.\n </p>\n )}\n </div>\n\n <div\n style={{\n display: 'flex',\n gap: '12px',\n padding: '16px 20px',\n borderTop: '1px solid #262626',\n }}\n >\n <button\n onClick={goPrev}\n disabled={noteIndex <= 0}\n style={{\n flex: 1,\n padding: '14px',\n fontSize: '16px',\n fontWeight: 500,\n border: '1px solid #333',\n borderRadius: '10px',\n backgroundColor: noteIndex <= 0 ? '#111' : '#1a1a1a',\n color: noteIndex <= 0 ? '#444' : '#e5e5e5',\n cursor: noteIndex <= 0 ? 'default' : 'pointer',\n WebkitTapHighlightColor: 'transparent',\n }}\n >\n ← Prev\n </button>\n <button\n onClick={goNext}\n disabled={noteIndex >= notes.length - 1}\n style={{\n flex: 1,\n padding: '14px',\n fontSize: '16px',\n fontWeight: 500,\n border: '1px solid #333',\n borderRadius: '10px',\n backgroundColor: noteIndex >= notes.length - 1 ? '#111' : '#1a1a1a',\n color: noteIndex >= notes.length - 1 ? '#444' : '#e5e5e5',\n cursor: noteIndex >= notes.length - 1 ? 'default' : 'pointer',\n WebkitTapHighlightColor: 'transparent',\n }}\n >\n Next →\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/dist/slides.css CHANGED
@@ -33,11 +33,43 @@
33
33
  --sh-sign: #ededed;
34
34
  }
35
35
 
36
+ /* Slide container overflow containment */
37
+ .nxs-slide {
38
+ overflow: hidden;
39
+ }
40
+
41
+ .nxs-slide-split-col {
42
+ min-width: 0;
43
+ overflow-x: auto;
44
+ }
45
+
46
+ /* Code block wrapper — constrains width so code doesn't blow out the slide */
47
+ .nxs-code-wrapper {
48
+ min-width: 0;
49
+ width: 100%;
50
+ max-width: 42rem;
51
+ }
52
+
36
53
  /* Map highlight.js to theme variables (all overwritable via --sh-*) */
37
54
  .nxs-code-block {
38
55
  background: var(--nxs-code-bg);
39
- border-color: var(--nxs-code-border);
56
+ border: 1px solid var(--nxs-code-border);
40
57
  color: var(--nxs-code-text);
58
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
59
+ font-size: clamp(0.75rem, 1.5vw + 0.5rem, 0.875rem);
60
+ line-height: 1.7;
61
+ text-align: left;
62
+ min-width: 0;
63
+ width: 100%;
64
+ max-width: 100%;
65
+ overflow-x: auto;
66
+ padding: 1rem;
67
+ }
68
+
69
+ @media (min-width: 640px) {
70
+ .nxs-code-block {
71
+ padding: 1.5rem;
72
+ }
41
73
  }
42
74
  .nxs-code-block .hljs-keyword,
43
75
  .nxs-code-block .hljs-literal { color: var(--sh-keyword); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-slides",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",