nextjs-slides 0.4.0 → 0.5.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 +87 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/parse-speaker-notes.d.ts +41 -0
- package/dist/parse-speaker-notes.js +10 -0
- package/dist/parse-speaker-notes.js.map +1 -0
- package/dist/primitives.js +3 -3
- package/dist/primitives.js.map +1 -1
- package/dist/slide-deck.d.ts +1 -1
- package/dist/slide-deck.js +13 -3
- package/dist/slide-deck.js.map +1 -1
- package/dist/slide-notes-view.d.ts +34 -0
- package/dist/slide-notes-view.js +107 -0
- package/dist/slide-notes-view.js.map +1 -0
- package/dist/sync.d.ts +18 -0
- package/dist/sync.js +16 -0
- package/dist/sync.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -120,15 +120,17 @@ That's it. Navigate to `/slides` and you have a full slide deck.
|
|
|
120
120
|
|
|
121
121
|
## `<SlideDeck>` Props
|
|
122
122
|
|
|
123
|
-
| Prop | Type
|
|
124
|
-
| -------------- |
|
|
125
|
-
| `slides` | `ReactNode[]`
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
123
|
+
| Prop | Type | Default | Description |
|
|
124
|
+
| -------------- | --------------------------------- | ------------ | ------------------------------------------------------- |
|
|
125
|
+
| `slides` | `ReactNode[]` | **required** | Your slides array |
|
|
126
|
+
| `speakerNotes` | `(string \| ReactNode \| null)[]` | — | Notes per slide (same index). See Speaker Notes below. |
|
|
127
|
+
| `syncEndpoint` | `string` | — | API route for presenter ↔ phone sync. |
|
|
128
|
+
| `basePath` | `string` | `"/slides"` | URL prefix for slide routes |
|
|
129
|
+
| `exitUrl` | `string` | — | URL for exit button (×). Shows in top-right when set. |
|
|
130
|
+
| `showProgress` | `boolean` | `true` | Show dot progress indicator |
|
|
131
|
+
| `showCounter` | `boolean` | `true` | Show "3 / 10" counter |
|
|
132
|
+
| `className` | `string` | — | Additional class for the deck container |
|
|
133
|
+
| `children` | `React.ReactNode` | **required** | Route content (from Next.js) |
|
|
132
134
|
|
|
133
135
|
## Primitives
|
|
134
136
|
|
|
@@ -171,6 +173,82 @@ That's it. Navigate to `/slides` and you have a full slide deck.
|
|
|
171
173
|
|
|
172
174
|
Keyboard events are ignored inside `<SlideDemo>`, inputs, and textareas so you can interact without advancing slides.
|
|
173
175
|
|
|
176
|
+
## Speaker Notes
|
|
177
|
+
|
|
178
|
+
Write notes in a markdown file — one section per slide, separated by `---`. Empty sections mean no notes for that slide:
|
|
179
|
+
|
|
180
|
+
```md
|
|
181
|
+
Welcome everyone. This is the opening slide.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
Talk about the base container here.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
Slide 4 notes. Slide 3 had none.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Parse the file and pass it to `SlideDeck`:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// app/slides/layout.tsx
|
|
198
|
+
import fs from "fs";
|
|
199
|
+
import path from "path";
|
|
200
|
+
import { SlideDeck, parseSpeakerNotes } from "nextjs-slides";
|
|
201
|
+
import { slides } from "./slides";
|
|
202
|
+
|
|
203
|
+
const notes = parseSpeakerNotes(
|
|
204
|
+
fs.readFileSync(path.join(process.cwd(), "app/slides/notes.md"), "utf-8")
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
export default function SlidesLayout({
|
|
208
|
+
children,
|
|
209
|
+
}: {
|
|
210
|
+
children: React.ReactNode;
|
|
211
|
+
}) {
|
|
212
|
+
return (
|
|
213
|
+
<SlideDeck slides={slides} speakerNotes={notes} syncEndpoint="/api/nxs-sync">
|
|
214
|
+
{children}
|
|
215
|
+
</SlideDeck>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Phone sync (presenter notes on your phone)
|
|
221
|
+
|
|
222
|
+
Open `/notes` on your phone while presenting on your laptop. The phone shows the current slide's notes and follows along as you navigate with the keyboard.
|
|
223
|
+
|
|
224
|
+
**1. Create the sync API route:**
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// app/api/nxs-sync/route.ts
|
|
228
|
+
export { GET, POST } from "nextjs-slides/sync";
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**2. Create the notes page:**
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
// app/notes/page.tsx
|
|
235
|
+
import fs from "fs";
|
|
236
|
+
import path from "path";
|
|
237
|
+
import { parseSpeakerNotes, SlideNotesView } from "nextjs-slides";
|
|
238
|
+
|
|
239
|
+
const notes = parseSpeakerNotes(
|
|
240
|
+
fs.readFileSync(path.join(process.cwd(), "app/slides/notes.md"), "utf-8")
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
export default function NotesPage() {
|
|
244
|
+
return <SlideNotesView notes={notes} syncEndpoint="/api/nxs-sync" />;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
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
|
+
|
|
250
|
+
> **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
|
+
|
|
174
252
|
## Custom Base Path & Multiple Decks
|
|
175
253
|
|
|
176
254
|
Use `basePath` for a different URL, `exitUrl` for an exit button (×), and `className` for scoped font/syntax overrides:
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,7 @@ export { SlideDeck } from './slide-deck.js';
|
|
|
2
2
|
export { generateSlideParams, getSlide } from './get-slide.js';
|
|
3
3
|
export { Slide, SlideBadge, SlideCode, SlideDemo, SlideHeaderBadge, SlideList, SlideListItem, SlideNote, SlideSpeaker, SlideSpeakerGrid, SlideSpeakerList, SlideSplitLayout, SlideStatement, SlideStatementList, SlideSubtitle, SlideTitle } from './primitives.js';
|
|
4
4
|
export { SlideLink } from './slide-link.js';
|
|
5
|
+
export { parseSpeakerNotes } from './parse-speaker-notes.js';
|
|
6
|
+
export { SlideNotesView } from './slide-notes-view.js';
|
|
5
7
|
export { SlideAlign, SlideDeckConfig, SlideLinkVariant } from './types.js';
|
|
6
8
|
import 'react/jsx-runtime';
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { SlideCode, SlideList, SlideListItem, SlideDemo } from "./primitives";
|
|
|
6
6
|
import { SlideStatementList, SlideStatement } from "./primitives";
|
|
7
7
|
import { SlideSpeaker, SlideSpeakerGrid, SlideSpeakerList } from "./primitives";
|
|
8
8
|
import { SlideLink } from "./slide-link";
|
|
9
|
+
import { parseSpeakerNotes } from "./parse-speaker-notes";
|
|
10
|
+
import { SlideNotesView } from "./slide-notes-view";
|
|
9
11
|
export {
|
|
10
12
|
Slide,
|
|
11
13
|
SlideBadge,
|
|
@@ -17,6 +19,7 @@ export {
|
|
|
17
19
|
SlideList,
|
|
18
20
|
SlideListItem,
|
|
19
21
|
SlideNote,
|
|
22
|
+
SlideNotesView,
|
|
20
23
|
SlideSpeaker,
|
|
21
24
|
SlideSpeakerGrid,
|
|
22
25
|
SlideSpeakerList,
|
|
@@ -26,6 +29,7 @@ export {
|
|
|
26
29
|
SlideSubtitle,
|
|
27
30
|
SlideTitle,
|
|
28
31
|
generateSlideParams,
|
|
29
|
-
getSlide
|
|
32
|
+
getSlide,
|
|
33
|
+
parseSpeakerNotes
|
|
30
34
|
};
|
|
31
35
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Provider\nexport { SlideDeck } from './slide-deck';\n\n// Routing helpers\nexport { getSlide, generateSlideParams } from './get-slide';\n\n// Primitives — Layout\nexport { Slide, SlideSplitLayout } from './primitives';\n\n// Primitives — Typography\nexport { SlideTitle, SlideSubtitle, SlideBadge, SlideHeaderBadge, SlideNote } from './primitives';\n\n// Primitives — Content\nexport { SlideCode, SlideList, SlideListItem, SlideDemo } from './primitives';\n\n// Primitives — Statements\nexport { SlideStatementList, SlideStatement } from './primitives';\n\n// Primitives — Speakers\nexport { SlideSpeaker, SlideSpeakerGrid, SlideSpeakerList } from './primitives';\n\n// Navigation\nexport { SlideLink } from './slide-link';\n\n// Types\nexport type { SlideAlign, SlideLinkVariant, SlideDeckConfig } from './types';\n"],"mappings":"AACA,SAAS,iBAAiB;AAG1B,SAAS,UAAU,2BAA2B;AAG9C,SAAS,OAAO,wBAAwB;AAGxC,SAAS,YAAY,eAAe,YAAY,kBAAkB,iBAAiB;AAGnF,SAAS,WAAW,WAAW,eAAe,iBAAiB;AAG/D,SAAS,oBAAoB,sBAAsB;AAGnD,SAAS,cAAc,kBAAkB,wBAAwB;AAGjE,SAAS,iBAAiB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Provider\nexport { SlideDeck } from './slide-deck';\n\n// Routing helpers\nexport { getSlide, generateSlideParams } from './get-slide';\n\n// Primitives — Layout\nexport { Slide, SlideSplitLayout } from './primitives';\n\n// Primitives — Typography\nexport { SlideTitle, SlideSubtitle, SlideBadge, SlideHeaderBadge, SlideNote } from './primitives';\n\n// Primitives — Content\nexport { SlideCode, SlideList, SlideListItem, SlideDemo } from './primitives';\n\n// Primitives — Statements\nexport { SlideStatementList, SlideStatement } from './primitives';\n\n// Primitives — Speakers\nexport { SlideSpeaker, SlideSpeakerGrid, SlideSpeakerList } from './primitives';\n\n// Navigation\nexport { SlideLink } from './slide-link';\n\n// Utilities\nexport { parseSpeakerNotes } from './parse-speaker-notes';\n\n// Speaker notes\nexport { SlideNotesView } from './slide-notes-view';\n\n// Types\nexport type { SlideAlign, SlideLinkVariant, SlideDeckConfig } from './types';\n"],"mappings":"AACA,SAAS,iBAAiB;AAG1B,SAAS,UAAU,2BAA2B;AAG9C,SAAS,OAAO,wBAAwB;AAGxC,SAAS,YAAY,eAAe,YAAY,kBAAkB,iBAAiB;AAGnF,SAAS,WAAW,WAAW,eAAe,iBAAiB;AAG/D,SAAS,oBAAoB,sBAAsB;AAGnD,SAAS,cAAc,kBAAkB,wBAAwB;AAGjE,SAAS,iBAAiB;AAG1B,SAAS,yBAAyB;AAGlC,SAAS,sBAAsB;","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse speaker notes from a markdown string.
|
|
3
|
+
*
|
|
4
|
+
* Format: one section per slide, separated by `---` on its own line.
|
|
5
|
+
* Empty sections produce `null` (no notes for that slide).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```md
|
|
9
|
+
* Welcome everyone. This is the opening slide.
|
|
10
|
+
*
|
|
11
|
+
* ---
|
|
12
|
+
*
|
|
13
|
+
* Talk about the base container here.
|
|
14
|
+
*
|
|
15
|
+
* ---
|
|
16
|
+
*
|
|
17
|
+
* ---
|
|
18
|
+
*
|
|
19
|
+
* Slide 4 notes. Slide 3 had none.
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* // slides/layout.tsx (server component — can use fs)
|
|
25
|
+
* import fs from 'fs';
|
|
26
|
+
* import path from 'path';
|
|
27
|
+
* import { SlideDeck, parseSpeakerNotes } from 'nextjs-slides';
|
|
28
|
+
* import { slides } from './slides';
|
|
29
|
+
*
|
|
30
|
+
* const notes = parseSpeakerNotes(
|
|
31
|
+
* fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8'),
|
|
32
|
+
* );
|
|
33
|
+
*
|
|
34
|
+
* export default function SlidesLayout({ children }: { children: React.ReactNode }) {
|
|
35
|
+
* return <SlideDeck slides={slides} speakerNotes={notes}>{children}</SlideDeck>;
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function parseSpeakerNotes(markdown: string): (string | null)[];
|
|
40
|
+
|
|
41
|
+
export { parseSpeakerNotes };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function parseSpeakerNotes(markdown) {
|
|
2
|
+
return markdown.split(/^---$/m).map((section) => {
|
|
3
|
+
const trimmed = section.trim();
|
|
4
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
parseSpeakerNotes
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=parse-speaker-notes.js.map
|
|
@@ -0,0 +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":[]}
|
package/dist/primitives.js
CHANGED
|
@@ -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-
|
|
38
|
+
"relative z-10 flex min-w-0 max-w-4xl flex-col gap-10",
|
|
39
39
|
align === "center" && "items-center",
|
|
40
40
|
align === "left" && "items-start"
|
|
41
41
|
),
|
|
@@ -91,7 +91,7 @@ function SlideCode({ children, className, title }) {
|
|
|
91
91
|
const html = highlightCode(children, lang);
|
|
92
92
|
return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0 w-full max-w-2xl", 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 overflow-x-auto border p-
|
|
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 } }) })
|
|
95
95
|
] });
|
|
96
96
|
}
|
|
97
97
|
function SlideList({ children, className }) {
|
|
@@ -113,7 +113,7 @@ function SlideDemo({
|
|
|
113
113
|
}) {
|
|
114
114
|
return /* @__PURE__ */ jsxs("div", { "data-slide-interactive": true, className: cn("min-w-0 w-full max-w-2xl", className), children: [
|
|
115
115
|
label && /* @__PURE__ */ jsx("div", { className: "text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase", children: label }),
|
|
116
|
-
/* @__PURE__ */ jsx("div", { className: "border-foreground/10 bg-foreground/[0.03] border p-6", children: /* @__PURE__ */ jsx(SlideDemoContent, { children }) })
|
|
116
|
+
/* @__PURE__ */ jsx("div", { className: "border-foreground/10 bg-foreground/[0.03] min-w-0 w-full max-w-full border p-4 sm:p-6", children: /* @__PURE__ */ jsx(SlideDemoContent, { children }) })
|
|
117
117
|
] });
|
|
118
118
|
}
|
|
119
119
|
function SlideStatementList({ children, className }) {
|
package/dist/primitives.js.map
CHANGED
|
@@ -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-6',\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 overflow-x-auto border p-6 text-left font-mono text-[13px] leading-[1.7] sm:text-sm\">\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] border 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 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":[]}
|
package/dist/slide-deck.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { SlideDeckConfig } from './types.js';
|
|
3
3
|
|
|
4
|
-
declare function SlideDeck({ children, slides, basePath, exitUrl, showProgress, showCounter, className, }: SlideDeckConfig & {
|
|
4
|
+
declare function SlideDeck({ children, slides, basePath, exitUrl, showProgress, showCounter, syncEndpoint, className, }: SlideDeckConfig & {
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
}): react_jsx_runtime.JSX.Element;
|
|
7
7
|
|
package/dist/slide-deck.js
CHANGED
|
@@ -11,6 +11,7 @@ function SlideDeck({
|
|
|
11
11
|
exitUrl,
|
|
12
12
|
showProgress = true,
|
|
13
13
|
showCounter = true,
|
|
14
|
+
syncEndpoint,
|
|
14
15
|
className
|
|
15
16
|
}) {
|
|
16
17
|
const router = useRouter();
|
|
@@ -64,17 +65,26 @@ function SlideDeck({
|
|
|
64
65
|
document.body.style.overflow = prev;
|
|
65
66
|
};
|
|
66
67
|
}, []);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!syncEndpoint || !isSlideRoute) return;
|
|
70
|
+
fetch(syncEndpoint, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ slide: current + 1, total })
|
|
74
|
+
}).catch(() => {
|
|
75
|
+
});
|
|
76
|
+
}, [syncEndpoint, current, total, isSlideRoute]);
|
|
67
77
|
return /* @__PURE__ */ jsx(ViewTransition, { default: "none", exit: "deck-unveil", children: /* @__PURE__ */ jsxs(
|
|
68
78
|
"div",
|
|
69
79
|
{
|
|
70
80
|
id: "slide-deck",
|
|
71
81
|
className: cn(
|
|
72
|
-
"bg-background text-foreground fixed inset-0 z-50 overflow-hidden font-sans select-none",
|
|
82
|
+
"bg-background text-foreground fixed inset-0 z-50 flex flex-col overflow-hidden font-sans select-none",
|
|
73
83
|
className
|
|
74
84
|
),
|
|
75
85
|
"data-pending": isPending ? "" : void 0,
|
|
76
86
|
children: [
|
|
77
|
-
/* @__PURE__ */ jsx(
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
78
88
|
ViewTransition,
|
|
79
89
|
{
|
|
80
90
|
default: "none",
|
|
@@ -91,7 +101,7 @@ function SlideDeck({
|
|
|
91
101
|
children: /* @__PURE__ */ jsx("div", { children })
|
|
92
102
|
},
|
|
93
103
|
pathname
|
|
94
|
-
),
|
|
104
|
+
) }),
|
|
95
105
|
isSlideRoute && showProgress && /* @__PURE__ */ jsx(
|
|
96
106
|
"div",
|
|
97
107
|
{
|
package/dist/slide-deck.js.map
CHANGED
|
@@ -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 { addTransitionType, useCallback, useEffect, useTransition, ViewTransition } from 'react';\nimport { cn } from './cn';\nimport type { SlideDeckConfig } from './types';\n\nexport function SlideDeck({\n children,\n slides,\n basePath = '/slides',\n exitUrl,\n showProgress = true,\n showCounter = true,\n className,\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 = new RegExp(`^${basePath}/(\\\\d+)$`);\n const isSlideRoute = slideRoutePattern.test(pathname);\n const current = (() => {\n const match = pathname.match(slideRoutePattern);\n return match ? Number(match[1]) - 1 : 0;\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 startTransition(() => {\n addTransitionType(clamped > current ? 'slide-forward' : 'slide-back');\n router.push(`${basePath}/${clamped + 1}`);\n });\n },\n [basePath, current, router, startTransition, 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 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 overflow-hidden font-sans select-none',\n className,\n )}\n data-pending={isPending ? '' : undefined}\n >\n <ViewTransition\n key={pathname}\n default=\"none\"\n enter={{\n default: 'slide-from-right',\n 'slide-back': 'slide-from-left',\n 'slide-forward': 'slide-from-right',\n }}\n exit={{\n default: 'slide-to-left',\n 'slide-back': 'slide-to-right',\n 'slide-forward': 'slide-to-left',\n }}\n >\n <div>{children}</div>\n </ViewTransition>\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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </Link>\n )}\n </div>\n </ViewTransition>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/slide-deck.tsx"],"sourcesContent":["'use client';\n\nimport Link from 'next/link';\nimport { usePathname, useRouter } from 'next/navigation';\nimport { addTransitionType, useCallback, useEffect, useTransition, ViewTransition } from 'react';\nimport { cn } from './cn';\nimport type { SlideDeckConfig } from './types';\n\nexport function SlideDeck({\n children,\n slides,\n basePath = '/slides',\n exitUrl,\n showProgress = true,\n showCounter = true,\n syncEndpoint,\n className,\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 = new RegExp(`^${basePath}/(\\\\d+)$`);\n const isSlideRoute = slideRoutePattern.test(pathname);\n const current = (() => {\n const match = pathname.match(slideRoutePattern);\n return match ? Number(match[1]) - 1 : 0;\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 startTransition(() => {\n addTransitionType(clamped > current ? 'slide-forward' : 'slide-back');\n router.push(`${basePath}/${clamped + 1}`);\n });\n },\n [basePath, current, router, startTransition, 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 (!syncEndpoint || !isSlideRoute) return;\n fetch(syncEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ slide: current + 1, total }),\n }).catch(() => {});\n }, [syncEndpoint, current, total, isSlideRoute]);\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 'slide-back': 'slide-from-left',\n 'slide-forward': 'slide-from-right',\n }}\n exit={{\n default: 'slide-to-left',\n 'slide-back': 'slide-to-right',\n 'slide-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 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n </Link>\n )}\n </div>\n </ViewTransition>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phone-friendly speaker notes view that stays in sync with the presenter.
|
|
5
|
+
*
|
|
6
|
+
* Polls the sync endpoint to track the current slide and displays the
|
|
7
|
+
* corresponding note. Open this page on your phone while presenting.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // app/slides/notes/page.tsx
|
|
12
|
+
* import fs from 'fs';
|
|
13
|
+
* import path from 'path';
|
|
14
|
+
* import { parseSpeakerNotes, SlideNotesView } from 'nextjs-slides';
|
|
15
|
+
*
|
|
16
|
+
* const notes = parseSpeakerNotes(
|
|
17
|
+
* fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8'),
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* export default function NotesPage() {
|
|
21
|
+
* return <SlideNotesView notes={notes} syncEndpoint="/api/nxs-sync" />;
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare function SlideNotesView({ notes, syncEndpoint, pollInterval, }: {
|
|
26
|
+
/** Speaker notes array (same index as slides). Typically from `parseSpeakerNotes()`. */
|
|
27
|
+
notes: (string | null)[];
|
|
28
|
+
/** API endpoint created with the sync route handlers. */
|
|
29
|
+
syncEndpoint: string;
|
|
30
|
+
/** Polling interval in ms. Defaults to 500. */
|
|
31
|
+
pollInterval?: number;
|
|
32
|
+
}): react_jsx_runtime.JSX.Element;
|
|
33
|
+
|
|
34
|
+
export { SlideNotesView };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
function SlideNotesView({
|
|
5
|
+
notes,
|
|
6
|
+
syncEndpoint,
|
|
7
|
+
pollInterval = 500
|
|
8
|
+
}) {
|
|
9
|
+
const [currentSlide, setCurrentSlide] = useState(1);
|
|
10
|
+
const [totalSlides, setTotalSlides] = useState(1);
|
|
11
|
+
const [connected, setConnected] = useState(false);
|
|
12
|
+
const poll = useCallback(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(syncEndpoint, { cache: "no-store" });
|
|
15
|
+
if (!res.ok) return;
|
|
16
|
+
const data = await res.json();
|
|
17
|
+
setCurrentSlide(data.slide);
|
|
18
|
+
setTotalSlides(data.total);
|
|
19
|
+
setConnected(true);
|
|
20
|
+
} catch {
|
|
21
|
+
setConnected(false);
|
|
22
|
+
}
|
|
23
|
+
}, [syncEndpoint]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
poll();
|
|
26
|
+
const id = setInterval(poll, pollInterval);
|
|
27
|
+
return () => clearInterval(id);
|
|
28
|
+
}, [poll, pollInterval]);
|
|
29
|
+
const noteIndex = currentSlide - 1;
|
|
30
|
+
const currentNote = noteIndex >= 0 && noteIndex < notes.length ? notes[noteIndex] : null;
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
style: {
|
|
35
|
+
minHeight: "100dvh",
|
|
36
|
+
display: "flex",
|
|
37
|
+
flexDirection: "column",
|
|
38
|
+
backgroundColor: "#0a0a0a",
|
|
39
|
+
color: "#e5e5e5",
|
|
40
|
+
fontFamily: "system-ui, -apple-system, sans-serif"
|
|
41
|
+
},
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsxs(
|
|
44
|
+
"div",
|
|
45
|
+
{
|
|
46
|
+
style: {
|
|
47
|
+
display: "flex",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
justifyContent: "space-between",
|
|
50
|
+
padding: "12px 20px",
|
|
51
|
+
borderBottom: "1px solid #262626",
|
|
52
|
+
fontSize: "13px",
|
|
53
|
+
color: "#737373"
|
|
54
|
+
},
|
|
55
|
+
children: [
|
|
56
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
57
|
+
"Slide ",
|
|
58
|
+
currentSlide,
|
|
59
|
+
" / ",
|
|
60
|
+
totalSlides
|
|
61
|
+
] }),
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"span",
|
|
64
|
+
{
|
|
65
|
+
style: {
|
|
66
|
+
width: 8,
|
|
67
|
+
height: 8,
|
|
68
|
+
borderRadius: "50%",
|
|
69
|
+
backgroundColor: connected ? "#22c55e" : "#ef4444"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
/* @__PURE__ */ jsx(
|
|
77
|
+
"div",
|
|
78
|
+
{
|
|
79
|
+
style: {
|
|
80
|
+
flex: 1,
|
|
81
|
+
display: "flex",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
padding: "24px 20px"
|
|
85
|
+
},
|
|
86
|
+
children: currentNote ? /* @__PURE__ */ jsx(
|
|
87
|
+
"p",
|
|
88
|
+
{
|
|
89
|
+
style: {
|
|
90
|
+
fontSize: "clamp(18px, 4vw, 28px)",
|
|
91
|
+
lineHeight: 1.6,
|
|
92
|
+
maxWidth: "640px",
|
|
93
|
+
whiteSpace: "pre-wrap"
|
|
94
|
+
},
|
|
95
|
+
children: currentNote
|
|
96
|
+
}
|
|
97
|
+
) : /* @__PURE__ */ jsx("p", { style: { fontSize: "18px", color: "#525252", fontStyle: "italic" }, children: "No notes for this slide." })
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
SlideNotesView
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=slide-notes-view.js.map
|
|
@@ -0,0 +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":[]}
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory slide state for presenter ↔ notes-viewer sync.
|
|
3
|
+
*
|
|
4
|
+
* Re-export these as your API route handlers:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // app/api/nxs-sync/route.ts
|
|
9
|
+
* export { GET, POST } from 'nextjs-slides/sync';
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Works with `syncEndpoint="/api/nxs-sync"` on `SlideDeck` and `SlideNotesView`.
|
|
13
|
+
* State lives in server memory — designed for `next dev` / single-server deployments.
|
|
14
|
+
*/
|
|
15
|
+
declare function GET(): Promise<Response>;
|
|
16
|
+
declare function POST(request: Request): Promise<Response>;
|
|
17
|
+
|
|
18
|
+
export { GET, POST };
|
package/dist/sync.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let currentSlide = 1;
|
|
2
|
+
let totalSlides = 1;
|
|
3
|
+
async function GET() {
|
|
4
|
+
return Response.json({ slide: currentSlide, total: totalSlides });
|
|
5
|
+
}
|
|
6
|
+
async function POST(request) {
|
|
7
|
+
const body = await request.json();
|
|
8
|
+
if (typeof body.slide === "number") currentSlide = body.slide;
|
|
9
|
+
if (typeof body.total === "number") totalSlides = body.total;
|
|
10
|
+
return Response.json({ slide: currentSlide, total: totalSlides });
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
GET,
|
|
14
|
+
POST
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=sync.js.map
|
package/dist/sync.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sync.ts"],"sourcesContent":["/**\n * In-memory slide state for presenter ↔ notes-viewer sync.\n *\n * Re-export these as your API route handlers:\n *\n * @example\n * ```ts\n * // app/api/nxs-sync/route.ts\n * export { GET, POST } from 'nextjs-slides/sync';\n * ```\n *\n * Works with `syncEndpoint=\"/api/nxs-sync\"` on `SlideDeck` and `SlideNotesView`.\n * State lives in server memory — designed for `next dev` / single-server deployments.\n */\n\nlet currentSlide = 1;\nlet totalSlides = 1;\n\nexport async function GET() {\n return Response.json({ slide: currentSlide, total: totalSlides });\n}\n\nexport async function POST(request: Request) {\n const body = await request.json();\n if (typeof body.slide === 'number') currentSlide = body.slide;\n if (typeof body.total === 'number') totalSlides = body.total;\n return Response.json({ slide: currentSlide, total: totalSlides });\n}\n"],"mappings":"AAeA,IAAI,eAAe;AACnB,IAAI,cAAc;AAElB,eAAsB,MAAM;AAC1B,SAAO,SAAS,KAAK,EAAE,OAAO,cAAc,OAAO,YAAY,CAAC;AAClE;AAEA,eAAsB,KAAK,SAAkB;AAC3C,QAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,MAAI,OAAO,KAAK,UAAU,SAAU,gBAAe,KAAK;AACxD,MAAI,OAAO,KAAK,UAAU,SAAU,eAAc,KAAK;AACvD,SAAO,SAAS,KAAK,EAAE,OAAO,cAAc,OAAO,YAAY,CAAC;AAClE;","names":[]}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ type SlideAlign = 'center' | 'left';
|
|
|
2
2
|
type SlideLinkVariant = 'primary' | 'ghost';
|
|
3
3
|
interface SlideDeckConfig {
|
|
4
4
|
slides: React.ReactNode[];
|
|
5
|
+
/** Speaker notes per slide (same index as slides). Use `parseSpeakerNotes()` to load from a markdown file. */
|
|
6
|
+
speakerNotes?: (string | React.ReactNode | null)[];
|
|
5
7
|
/** Base path for slide URLs. Defaults to "/slides" */
|
|
6
8
|
basePath?: string;
|
|
7
9
|
/** URL to navigate to when exiting the deck. When set, shows an exit button. */
|
|
@@ -10,6 +12,8 @@ interface SlideDeckConfig {
|
|
|
10
12
|
showProgress?: boolean;
|
|
11
13
|
/** Show slide counter (e.g. "3 / 10"). Defaults to true */
|
|
12
14
|
showCounter?: boolean;
|
|
15
|
+
/** API endpoint for presenter ↔ phone sync. See `SlideNotesView` and the sync route handlers. */
|
|
16
|
+
syncEndpoint?: string;
|
|
13
17
|
/** Additional className for the deck container */
|
|
14
18
|
className?: string;
|
|
15
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextjs-slides",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"import": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
+
"./sync": {
|
|
13
|
+
"types": "./dist/sync.d.ts",
|
|
14
|
+
"import": "./dist/sync.js"
|
|
15
|
+
},
|
|
12
16
|
"./styles.css": "./dist/slides.css"
|
|
13
17
|
},
|
|
14
18
|
"main": "./dist/index.js",
|