@open-slide/core 0.0.2 → 0.0.4

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.
Files changed (35) hide show
  1. package/bin.js +2 -0
  2. package/dist/{build-DJGuOT6x.js → build-CuoESF2g.js} +1 -1
  3. package/dist/cli/bin.js +5 -5
  4. package/dist/config-DF58h0l4.js +641 -0
  5. package/dist/{dev-0SG0ArzD.js → dev-rlOZacWo.js} +1 -1
  6. package/dist/index.d.ts +7 -9
  7. package/dist/{preview-61Aawrlg.js → preview-DCrD9X36.js} +1 -1
  8. package/dist/vite/index.js +1 -1
  9. package/package.json +7 -4
  10. package/src/app/App.tsx +2 -2
  11. package/src/app/components/ClickNavZones.tsx +34 -0
  12. package/src/app/components/Player.tsx +26 -7
  13. package/src/app/components/ThumbnailRail.tsx +5 -5
  14. package/src/app/components/inspector/CommentPopover.tsx +3 -11
  15. package/src/app/components/inspector/InspectOverlay.tsx +15 -4
  16. package/src/app/components/inspector/InspectorProvider.tsx +12 -5
  17. package/src/app/components/sidebar/FolderItem.tsx +188 -0
  18. package/src/app/components/sidebar/IconPicker.tsx +59 -0
  19. package/src/app/components/sidebar/Sidebar.tsx +118 -0
  20. package/src/app/components/ui/dialog.tsx +141 -0
  21. package/src/app/components/ui/dropdown-menu.tsx +228 -0
  22. package/src/app/components/ui/popover.tsx +72 -0
  23. package/src/app/components/ui/tabs.tsx +79 -0
  24. package/src/app/lib/export-html.ts +313 -0
  25. package/src/app/lib/folders.ts +166 -0
  26. package/src/app/lib/inspector/fiber.ts +2 -2
  27. package/src/app/lib/inspector/useComments.ts +8 -8
  28. package/src/app/lib/sdk.ts +18 -5
  29. package/src/app/lib/slides.ts +8 -0
  30. package/src/app/routes/Home.tsx +540 -63
  31. package/src/app/routes/Slide.tsx +298 -0
  32. package/src/app/virtual.d.ts +4 -4
  33. package/dist/config-Opp2R1Jf.js +0 -335
  34. package/src/app/lib/decks.ts +0 -8
  35. package/src/app/routes/Deck.tsx +0 -185
@@ -0,0 +1,298 @@
1
+ import { ChevronLeft, Download, Loader2, Pencil, Play } from 'lucide-react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Link, useParams, useSearchParams } from 'react-router-dom';
4
+ import { CommentWidget } from '@/components/inspector/CommentWidget';
5
+ import { InspectOverlay } from '@/components/inspector/InspectOverlay';
6
+ import { InspectorProvider, InspectToggleButton } from '@/components/inspector/InspectorProvider';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Separator } from '@/components/ui/separator';
9
+ import { useFolders } from '@/lib/folders';
10
+ import { cn } from '@/lib/utils';
11
+ import { ClickNavZones } from '../components/ClickNavZones';
12
+ import { Player } from '../components/Player';
13
+ import { SlideCanvas } from '../components/SlideCanvas';
14
+ import { ThumbnailRail } from '../components/ThumbnailRail';
15
+ import { exportSlideAsHtml } from '../lib/export-html';
16
+ import type { SlideModule } from '../lib/sdk';
17
+ import { loadSlide } from '../lib/slides';
18
+
19
+ export function Slide() {
20
+ const { slideId = '' } = useParams();
21
+ const [searchParams, setSearchParams] = useSearchParams();
22
+ const [slide, setSlide] = useState<SlideModule | null>(null);
23
+ const [error, setError] = useState<string | null>(null);
24
+ const [playing, setPlaying] = useState(false);
25
+ const [exporting, setExporting] = useState(false);
26
+ const { renameSlide } = useFolders();
27
+
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+ setSlide(null);
31
+ setError(null);
32
+ loadSlide(slideId)
33
+ .then((mod) => {
34
+ if (!cancelled) setSlide(mod);
35
+ })
36
+ .catch((e) => {
37
+ if (!cancelled) setError(String(e?.message ?? e));
38
+ });
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [slideId]);
43
+
44
+ const pages = useMemo(() => slide?.default ?? [], [slide]);
45
+ const pageCount = pages.length;
46
+ const rawIndex = Number(searchParams.get('p') ?? '1') - 1;
47
+ const index = Number.isFinite(rawIndex) ? Math.max(0, Math.min(pageCount - 1, rawIndex)) : 0;
48
+
49
+ const goTo = useCallback(
50
+ (i: number) => {
51
+ const clamped = Math.max(0, Math.min(pageCount - 1, i));
52
+ setSearchParams(
53
+ (prev) => {
54
+ const next = new URLSearchParams(prev);
55
+ next.set('p', String(clamped + 1));
56
+ return next;
57
+ },
58
+ { replace: true },
59
+ );
60
+ },
61
+ [pageCount, setSearchParams],
62
+ );
63
+
64
+ useEffect(() => {
65
+ if (playing) return;
66
+ const onKey = (e: KeyboardEvent) => {
67
+ if (e.target instanceof HTMLElement && e.target.matches('input, textarea')) return;
68
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'PageDown') {
69
+ e.preventDefault();
70
+ goTo(index + 1);
71
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
72
+ e.preventDefault();
73
+ goTo(index - 1);
74
+ } else if (e.key === 'f' || e.key === 'F') {
75
+ setPlaying(true);
76
+ }
77
+ };
78
+ window.addEventListener('keydown', onKey);
79
+ return () => window.removeEventListener('keydown', onKey);
80
+ }, [index, goTo, playing]);
81
+
82
+ if (error) {
83
+ return (
84
+ <div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
85
+ <Link to="/" className="text-sm font-medium text-primary hover:underline">
86
+ ← Home
87
+ </Link>
88
+ <h2 className="mt-4 text-xl font-semibold text-foreground">Failed to load slide</h2>
89
+ <pre className="mt-4 overflow-auto rounded-md border bg-card p-4 text-xs whitespace-pre-wrap shadow-sm">
90
+ {error}
91
+ </pre>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ if (!slide) {
97
+ return (
98
+ <div className="mx-auto max-w-3xl px-8 py-16 text-sm text-muted-foreground">
99
+ Loading {slideId}…
100
+ </div>
101
+ );
102
+ }
103
+
104
+ if (pageCount === 0) {
105
+ return (
106
+ <div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
107
+ <Link to="/" className="text-sm font-medium text-primary hover:underline">
108
+ ← Home
109
+ </Link>
110
+ <h2 className="mt-4 text-xl font-semibold text-foreground">Empty slide</h2>
111
+ <p className="mt-2 text-sm">
112
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
113
+ slides/{slideId}/index.tsx
114
+ </code>{' '}
115
+ must{' '}
116
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">export default</code> a
117
+ non-empty array of components.
118
+ </p>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ if (playing) {
124
+ return (
125
+ <Player pages={pages} index={index} onIndexChange={goTo} onExit={() => setPlaying(false)} />
126
+ );
127
+ }
128
+
129
+ const CurrentPage = pages[index];
130
+ const title = slide.meta?.title ?? slideId;
131
+
132
+ return (
133
+ <InspectorProvider slideId={slideId}>
134
+ <div className="flex h-screen flex-col overflow-hidden bg-background">
135
+ <header className="flex shrink-0 items-center gap-2 border-b bg-card px-3 py-2 md:gap-4 md:px-5 md:py-3">
136
+ <Button asChild variant="ghost" size="sm" className="px-2 md:px-3">
137
+ <Link to="/">
138
+ <ChevronLeft className="size-4" />
139
+ <span className="hidden md:inline">Home</span>
140
+ </Link>
141
+ </Button>
142
+ <Separator orientation="vertical" className="hidden h-5 md:block" />
143
+ <InlineTitleEditor title={title} onSubmit={(next) => renameSlide(slideId, next)} />
144
+ <Button
145
+ variant="ghost"
146
+ size="sm"
147
+ className="px-2 md:px-3"
148
+ disabled={exporting}
149
+ onClick={async () => {
150
+ if (!slide || exporting) return;
151
+ setExporting(true);
152
+ try {
153
+ await exportSlideAsHtml(slide, slideId);
154
+ } catch (err) {
155
+ console.error('[open-slide] export failed', err);
156
+ } finally {
157
+ setExporting(false);
158
+ }
159
+ }}
160
+ title="Download as HTML"
161
+ >
162
+ {exporting ? (
163
+ <Loader2 className="size-4 animate-spin" />
164
+ ) : (
165
+ <Download className="size-4" />
166
+ )}
167
+ <span className="hidden md:inline">Download</span>
168
+ </Button>
169
+ <InspectToggleButton />
170
+ <Button size="sm" onClick={() => setPlaying(true)} className="px-2 md:px-3">
171
+ <Play className="size-4" />
172
+ <span className="hidden md:inline">Play</span>
173
+ <kbd className="ml-1 hidden rounded bg-primary-foreground/20 px-1 text-[10px] md:inline">
174
+ F
175
+ </kbd>
176
+ </Button>
177
+ </header>
178
+
179
+ <div className="flex min-h-0 flex-1">
180
+ <div className="hidden w-[17rem] shrink-0 md:block">
181
+ <ThumbnailRail pages={pages} current={index} onSelect={goTo} />
182
+ </div>
183
+ <main
184
+ data-inspector-root
185
+ className="relative min-h-0 min-w-0 flex-1 bg-background p-2 md:p-8"
186
+ >
187
+ <SlideCanvas>
188
+ <CurrentPage />
189
+ </SlideCanvas>
190
+ <ClickNavZones
191
+ onPrev={() => goTo(index - 1)}
192
+ onNext={() => goTo(index + 1)}
193
+ canPrev={index > 0}
194
+ canNext={index < pageCount - 1}
195
+ />
196
+ <InspectOverlay />
197
+ <div className="pointer-events-none absolute bottom-3 left-1/2 z-10 -translate-x-1/2 rounded-full bg-black/50 px-2.5 py-0.5 text-[11px] font-medium tabular-nums text-white backdrop-blur md:hidden">
198
+ {index + 1} / {pageCount}
199
+ </div>
200
+ </main>
201
+ </div>
202
+
203
+ <CommentWidget />
204
+ </div>
205
+ </InspectorProvider>
206
+ );
207
+ }
208
+
209
+ function InlineTitleEditor({
210
+ title,
211
+ onSubmit,
212
+ }: {
213
+ title: string;
214
+ onSubmit: (name: string) => Promise<void> | void;
215
+ }) {
216
+ const [editing, setEditing] = useState(false);
217
+ const [value, setValue] = useState(title);
218
+ const [saving, setSaving] = useState(false);
219
+ const inputRef = useRef<HTMLInputElement | null>(null);
220
+
221
+ useEffect(() => {
222
+ if (!editing) setValue(title);
223
+ }, [title, editing]);
224
+
225
+ useEffect(() => {
226
+ if (editing) {
227
+ queueMicrotask(() => {
228
+ inputRef.current?.focus();
229
+ inputRef.current?.select();
230
+ });
231
+ }
232
+ }, [editing]);
233
+
234
+ const commit = async () => {
235
+ const trimmed = value.trim();
236
+ if (!trimmed || trimmed === title) {
237
+ setValue(title);
238
+ setEditing(false);
239
+ return;
240
+ }
241
+ setSaving(true);
242
+ try {
243
+ await onSubmit(trimmed);
244
+ setEditing(false);
245
+ } finally {
246
+ setSaving(false);
247
+ }
248
+ };
249
+
250
+ const cancel = () => {
251
+ setValue(title);
252
+ setEditing(false);
253
+ };
254
+
255
+ if (editing) {
256
+ return (
257
+ <div className="flex flex-1 items-center justify-center">
258
+ <input
259
+ ref={inputRef}
260
+ value={value}
261
+ disabled={saving}
262
+ onChange={(e) => setValue(e.target.value)}
263
+ onBlur={() => {
264
+ if (!saving) commit();
265
+ }}
266
+ onKeyDown={(e) => {
267
+ if (e.key === 'Enter') {
268
+ e.preventDefault();
269
+ commit();
270
+ } else if (e.key === 'Escape') {
271
+ e.preventDefault();
272
+ cancel();
273
+ }
274
+ }}
275
+ maxLength={80}
276
+ className="min-w-0 max-w-[min(32rem,90%)] rounded-md border bg-background px-2 py-0.5 text-center text-xs font-semibold tracking-tight outline-none ring-ring/40 focus:ring-2 md:text-sm"
277
+ />
278
+ </div>
279
+ );
280
+ }
281
+
282
+ return (
283
+ <div className="group/title flex flex-1 items-center justify-center gap-1.5 min-w-0">
284
+ <h1 className="truncate text-xs font-semibold tracking-tight md:text-sm">{title}</h1>
285
+ <button
286
+ type="button"
287
+ onClick={() => setEditing(true)}
288
+ aria-label="Rename slide"
289
+ className={cn(
290
+ 'flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-opacity hover:bg-muted hover:text-foreground',
291
+ 'opacity-0 group-hover/title:opacity-100 focus-visible:opacity-100',
292
+ )}
293
+ >
294
+ <Pencil className="size-3.5" />
295
+ </button>
296
+ </div>
297
+ );
298
+ }
@@ -1,7 +1,7 @@
1
- declare module 'virtual:open-slide/decks' {
2
- import type { DeckModule } from './lib/sdk';
3
- export const deckIds: string[];
4
- export function loadDeck(id: string): Promise<DeckModule>;
1
+ declare module 'virtual:open-slide/slides' {
2
+ import type { SlideModule } from './lib/sdk';
3
+ export const slideIds: string[];
4
+ export function loadSlide(id: string): Promise<SlideModule>;
5
5
  }
6
6
 
7
7
  declare module 'virtual:open-slide/config' {
@@ -1,335 +0,0 @@
1
- import fs, { readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { existsSync } from "node:fs";
5
- import tailwindcss from "@tailwindcss/vite";
6
- import react from "@vitejs/plugin-react";
7
- import { randomUUID } from "node:crypto";
8
- import fg from "fast-glob";
9
-
10
- //#region src/vite/comments-plugin.ts
11
- const MARKER_RE = /\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
12
- const DECK_ID_RE = /^[a-z0-9_-]+$/i;
13
- function b64urlEncode(s) {
14
- return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
15
- }
16
- function b64urlDecode(s) {
17
- const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
18
- return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
19
- }
20
- async function readBody(req) {
21
- return await new Promise((resolve, reject) => {
22
- const chunks = [];
23
- req.on("data", (c) => chunks.push(c));
24
- req.on("end", () => {
25
- const raw = Buffer.concat(chunks).toString("utf8");
26
- if (!raw) return resolve({});
27
- try {
28
- resolve(JSON.parse(raw));
29
- } catch (e) {
30
- reject(e);
31
- }
32
- });
33
- req.on("error", reject);
34
- });
35
- }
36
- function json(res, status, body) {
37
- res.statusCode = status;
38
- res.setHeader("content-type", "application/json");
39
- res.end(JSON.stringify(body));
40
- }
41
- function resolveSlidePath(userCwd, slidesDir, deckId) {
42
- if (!DECK_ID_RE.test(deckId)) return null;
43
- const slidesRoot = path.resolve(userCwd, slidesDir);
44
- const full = path.resolve(slidesRoot, deckId, "index.tsx");
45
- if (!full.startsWith(slidesRoot + path.sep)) return null;
46
- return full;
47
- }
48
- function parseMarkers(source) {
49
- const comments = [];
50
- const lines = source.split("\n");
51
- for (let i = 0; i < lines.length; i++) {
52
- const line = lines[i];
53
- MARKER_RE.lastIndex = 0;
54
- const m = MARKER_RE.exec(line);
55
- if (!m) continue;
56
- const [, id, ts, textB64] = m;
57
- try {
58
- const payload = JSON.parse(b64urlDecode(textB64));
59
- comments.push({
60
- id,
61
- line: i + 1,
62
- ts,
63
- note: payload.note,
64
- hint: payload.hint
65
- });
66
- } catch {}
67
- }
68
- return comments;
69
- }
70
- function newId() {
71
- return `c-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
72
- }
73
- function isJsxOpeningLine(line) {
74
- const t = line.trimStart();
75
- if (!t.startsWith("<")) return false;
76
- if (t.startsWith("</")) return false;
77
- if (t.startsWith("<!")) return false;
78
- return true;
79
- }
80
- /**
81
- * Find the line index to insert a JSX comment above.
82
- *
83
- * Babel's `_debugSource.lineNumber/columnNumber` points at the `<` of a JSX
84
- * opening tag, but the value can go stale (HMR races) or, per reports, point
85
- * at a line that's not actually a JSX boundary — e.g. inside an inline style
86
- * object. Verify with the source of truth before committing.
87
- */
88
- function findSafeInsertLine(lines, line, column) {
89
- const idx = line - 1;
90
- if (idx < 0 || idx >= lines.length) return null;
91
- if (column !== void 0 && lines[idx].charAt(column) === "<") return idx;
92
- if (isJsxOpeningLine(lines[idx])) return idx;
93
- const WINDOW = 30;
94
- for (let i = idx - 1; i >= Math.max(0, idx - WINDOW); i--) if (isJsxOpeningLine(lines[i])) return i;
95
- for (let i = idx + 1; i < Math.min(lines.length, idx + WINDOW); i++) if (isJsxOpeningLine(lines[i])) return i;
96
- return null;
97
- }
98
- function commentsPlugin(opts) {
99
- const userCwd = opts.userCwd;
100
- const slidesDir = opts.slidesDir ?? "slides";
101
- return {
102
- name: "open-slide:comments",
103
- apply: "serve",
104
- configureServer(server) {
105
- server.middlewares.use("/__comments", async (req, res, next) => {
106
- const url = new URL(req.url ?? "/", "http://local");
107
- const method = req.method ?? "GET";
108
- try {
109
- if (method === "GET" && url.pathname === "/") {
110
- const deckId = url.searchParams.get("deckId") ?? "";
111
- const file = resolveSlidePath(userCwd, slidesDir, deckId);
112
- if (!file) return json(res, 400, { error: "invalid deckId" });
113
- let source;
114
- try {
115
- source = await fs.readFile(file, "utf8");
116
- } catch {
117
- return json(res, 404, { error: "deck not found" });
118
- }
119
- return json(res, 200, { comments: parseMarkers(source) });
120
- }
121
- if (method === "POST" && url.pathname === "/add") {
122
- const body = await readBody(req);
123
- const deckId = body.deckId ?? "";
124
- const file = resolveSlidePath(userCwd, slidesDir, deckId);
125
- if (!file) return json(res, 400, { error: "invalid deckId" });
126
- if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
127
- if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
128
- let source;
129
- try {
130
- source = await fs.readFile(file, "utf8");
131
- } catch {
132
- return json(res, 404, { error: "deck not found" });
133
- }
134
- const lines = source.split("\n");
135
- const idx = findSafeInsertLine(lines, body.line, body.column);
136
- if (idx === null) return json(res, 422, { error: `could not find a safe JSX boundary near line ${body.line}. Try clicking a different element.` });
137
- const indent = lines[idx].match(/^\s*/)?.[0] ?? "";
138
- const id = newId();
139
- const ts = new Date().toISOString();
140
- const payload = b64urlEncode(JSON.stringify({
141
- note: body.text,
142
- hint: body.hint
143
- }));
144
- const marker = `${indent}{/* @slide-comment id="${id}" ts="${ts}" text="${payload}" */}`;
145
- lines.splice(idx, 0, marker);
146
- await fs.writeFile(file, lines.join("\n"), "utf8");
147
- return json(res, 200, {
148
- id,
149
- line: idx + 1
150
- });
151
- }
152
- if (method === "DELETE" && url.pathname.startsWith("/")) {
153
- const id = url.pathname.slice(1);
154
- if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
155
- const deckId = url.searchParams.get("deckId") ?? "";
156
- const file = resolveSlidePath(userCwd, slidesDir, deckId);
157
- if (!file) return json(res, 400, { error: "invalid deckId" });
158
- let source;
159
- try {
160
- source = await fs.readFile(file, "utf8");
161
- } catch {
162
- return json(res, 404, { error: "deck not found" });
163
- }
164
- const lines = source.split("\n");
165
- const idRe = new RegExp(`\\{\\/\\*\\s*@slide-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
166
- const hit = lines.findIndex((l) => idRe.test(l));
167
- if (hit === -1) return json(res, 404, { error: "marker not found" });
168
- lines.splice(hit, 1);
169
- await fs.writeFile(file, lines.join("\n"), "utf8");
170
- return json(res, 200, { ok: true });
171
- }
172
- next();
173
- } catch (err) {
174
- json(res, 500, { error: String(err.message ?? err) });
175
- }
176
- });
177
- }
178
- };
179
- }
180
-
181
- //#endregion
182
- //#region src/vite/open-slide-plugin.ts
183
- const DECKS_VMOD = "virtual:open-slide/decks";
184
- const CONFIG_VMOD = "virtual:open-slide/config";
185
- function resolved(id) {
186
- return `\0${id}`;
187
- }
188
- async function findDecks(userCwd, slidesDir) {
189
- const abs = path.resolve(userCwd, slidesDir);
190
- if (!existsSync(abs)) return [];
191
- const hits = await fg("*/index.{tsx,jsx,ts,js}", {
192
- cwd: abs,
193
- absolute: true,
194
- onlyFiles: true
195
- });
196
- return hits.sort();
197
- }
198
- function toId(absFile, slidesRoot) {
199
- const rel = path.relative(slidesRoot, absFile);
200
- return rel.split(path.sep)[0];
201
- }
202
- function generateDecksModule(files, slidesRoot, isDev) {
203
- const entries = files.map((abs) => {
204
- const id = toId(abs, slidesRoot);
205
- const importPath = isDev ? `/@fs${abs}` : abs;
206
- return {
207
- id,
208
- importPath
209
- };
210
- });
211
- const ids = JSON.stringify(entries.map((e) => e.id).sort());
212
- const cases = entries.map((e) => ` case ${JSON.stringify(e.id)}: return import(${JSON.stringify(e.importPath)});`).join("\n");
213
- return `// virtual:open-slide/decks — generated
214
- export const deckIds = ${ids};
215
-
216
- export async function loadDeck(id) {
217
- switch (id) {
218
- ${cases}
219
- default: throw new Error('Deck not found: ' + id);
220
- }
221
- }
222
- `;
223
- }
224
- function openSlidePlugin(opts) {
225
- const { userCwd, config } = opts;
226
- const slidesDir = config.slidesDir ?? "slides";
227
- const slidesRoot = path.resolve(userCwd, slidesDir);
228
- let isDev = false;
229
- return {
230
- name: "open-slide",
231
- config(_c, env) {
232
- isDev = env.command === "serve";
233
- return { server: { fs: { allow: [userCwd] } } };
234
- },
235
- resolveId(id) {
236
- if (id === DECKS_VMOD) return resolved(DECKS_VMOD);
237
- if (id === CONFIG_VMOD) return resolved(CONFIG_VMOD);
238
- return null;
239
- },
240
- async load(id) {
241
- if (id === resolved(DECKS_VMOD)) {
242
- const files = await findDecks(userCwd, slidesDir);
243
- return generateDecksModule(files, slidesRoot, isDev);
244
- }
245
- if (id === resolved(CONFIG_VMOD)) return `export default ${JSON.stringify(config)};\n`;
246
- return null;
247
- },
248
- configureServer(server) {
249
- const reload = () => {
250
- const mod = server.moduleGraph.getModuleById(resolved(DECKS_VMOD));
251
- if (mod) server.moduleGraph.invalidateModule(mod);
252
- server.ws.send({ type: "full-reload" });
253
- };
254
- server.watcher.add(path.join(slidesRoot, "*"));
255
- server.watcher.on("add", (p) => {
256
- if (p.startsWith(slidesRoot)) reload();
257
- });
258
- server.watcher.on("unlink", (p) => {
259
- if (p.startsWith(slidesRoot)) reload();
260
- });
261
- server.middlewares.use("/__open-slide/title", (_req, res) => {
262
- res.setHeader("content-type", "application/json");
263
- res.end(JSON.stringify({ title: config.title ?? null }));
264
- });
265
- }
266
- };
267
- }
268
- async function loadUserConfig(userCwd) {
269
- const file = path.join(userCwd, "open-slide.json");
270
- if (!existsSync(file)) return {};
271
- const raw = await readFile(file, "utf8");
272
- return JSON.parse(raw);
273
- }
274
-
275
- //#endregion
276
- //#region src/vite/config.ts
277
- function findPackageRoot(fromFile) {
278
- let dir = path.dirname(fromFile);
279
- while (dir !== path.dirname(dir)) {
280
- if (existsSync(path.join(dir, "package.json"))) return dir;
281
- dir = path.dirname(dir);
282
- }
283
- throw new Error(`Could not find package.json walking up from ${fromFile}`);
284
- }
285
- const PKG_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
286
- const APP_ROOT = path.join(PKG_ROOT, "src", "app");
287
- async function createViteConfig(opts) {
288
- const userCwd = path.resolve(opts.userCwd);
289
- const config = opts.config ?? await loadUserConfig(userCwd);
290
- const slidesDir = config.slidesDir ?? "slides";
291
- const slidesAbs = path.resolve(userCwd, slidesDir);
292
- return {
293
- root: APP_ROOT,
294
- configFile: false,
295
- plugins: [
296
- react(),
297
- tailwindcss(),
298
- openSlidePlugin({
299
- userCwd,
300
- config
301
- }),
302
- commentsPlugin({
303
- userCwd,
304
- slidesDir
305
- })
306
- ],
307
- resolve: { alias: { "@": APP_ROOT } },
308
- optimizeDeps: {
309
- entries: [path.join(APP_ROOT, "main.tsx")],
310
- include: [
311
- "react-router-dom",
312
- "radix-ui",
313
- "lucide-react",
314
- "clsx",
315
- "tailwind-merge",
316
- "class-variance-authority"
317
- ]
318
- },
319
- server: {
320
- port: config.port ?? 5173,
321
- fs: { allow: [
322
- APP_ROOT,
323
- userCwd,
324
- slidesAbs
325
- ] }
326
- },
327
- build: {
328
- outDir: path.resolve(userCwd, "dist"),
329
- emptyOutDir: true
330
- }
331
- };
332
- }
333
-
334
- //#endregion
335
- export { createViteConfig };
@@ -1,8 +0,0 @@
1
- import type { DeckModule } from './sdk';
2
- import { deckIds as ids, loadDeck as load } from 'virtual:open-slide/decks';
3
-
4
- export const deckIds: string[] = ids;
5
-
6
- export async function loadDeck(id: string): Promise<DeckModule> {
7
- return load(id);
8
- }