@open-slide/core 0.0.1

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.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>open-slide</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,8 @@
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
+ }
@@ -0,0 +1,39 @@
1
+ export type SlideSourceHit = {
2
+ line: number;
3
+ column: number;
4
+ anchor: HTMLElement;
5
+ };
6
+
7
+ type FiberLike = {
8
+ return: FiberLike | null;
9
+ stateNode?: unknown;
10
+ _debugSource?: { fileName?: string; lineNumber?: number; columnNumber?: number };
11
+ memoizedProps?: { __source?: { fileName?: string; lineNumber?: number; columnNumber?: number } };
12
+ };
13
+
14
+ function getFiber(el: Element): FiberLike | null {
15
+ const key = Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
16
+ if (!key) return null;
17
+ return (el as unknown as Record<string, FiberLike>)[key] ?? null;
18
+ }
19
+
20
+ function getSource(fiber: FiberLike) {
21
+ return fiber._debugSource ?? fiber.memoizedProps?.__source;
22
+ }
23
+
24
+ export function findSlideSource(el: HTMLElement, deckId: string): SlideSourceHit | null {
25
+ const needle = `/slides/${deckId}/index.tsx`;
26
+ let fiber = getFiber(el);
27
+ let anchor: HTMLElement = el;
28
+ while (fiber) {
29
+ const src = getSource(fiber);
30
+ if (src?.fileName?.endsWith(needle) && src.lineNumber) {
31
+ return { line: src.lineNumber, column: src.columnNumber ?? 0, anchor };
32
+ }
33
+ if (fiber.stateNode instanceof HTMLElement) {
34
+ anchor = fiber.stateNode;
35
+ }
36
+ fiber = fiber.return;
37
+ }
38
+ return null;
39
+ }
@@ -0,0 +1,74 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ export type SlideComment = {
4
+ id: string;
5
+ line: number;
6
+ ts: string;
7
+ note: string;
8
+ hint?: string;
9
+ };
10
+
11
+ type ListResponse = { comments: SlideComment[] };
12
+
13
+ export function useComments(deckId: string) {
14
+ const [comments, setComments] = useState<SlideComment[]>([]);
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ const refetch = useCallback(async () => {
18
+ if (!deckId) return;
19
+ try {
20
+ const res = await fetch(`/__comments?deckId=${encodeURIComponent(deckId)}`);
21
+ if (!res.ok) {
22
+ setError(`GET /__comments → ${res.status}`);
23
+ return;
24
+ }
25
+ const data = (await res.json()) as ListResponse;
26
+ setComments(data.comments);
27
+ setError(null);
28
+ } catch (e) {
29
+ setError(String((e as Error).message ?? e));
30
+ }
31
+ }, [deckId]);
32
+
33
+ const add = useCallback(
34
+ async (line: number, column: number, text: string) => {
35
+ const res = await fetch('/__comments/add', {
36
+ method: 'POST',
37
+ headers: { 'content-type': 'application/json' },
38
+ body: JSON.stringify({ deckId, line, column, text }),
39
+ });
40
+ if (!res.ok) {
41
+ const body = (await res.json().catch(() => ({}))) as { error?: string };
42
+ throw new Error(body.error ?? `POST /__comments/add → ${res.status}`);
43
+ }
44
+ await refetch();
45
+ },
46
+ [deckId, refetch],
47
+ );
48
+
49
+ const remove = useCallback(
50
+ async (id: string) => {
51
+ const res = await fetch(`/__comments/${id}?deckId=${encodeURIComponent(deckId)}`, {
52
+ method: 'DELETE',
53
+ });
54
+ if (!res.ok) throw new Error(`DELETE /__comments/${id} → ${res.status}`);
55
+ await refetch();
56
+ },
57
+ [deckId, refetch],
58
+ );
59
+
60
+ useEffect(() => {
61
+ refetch();
62
+ }, [refetch]);
63
+
64
+ useEffect(() => {
65
+ if (!import.meta.hot) return;
66
+ const handler = () => refetch();
67
+ import.meta.hot.on('vite:afterUpdate', handler);
68
+ return () => {
69
+ import.meta.hot?.off('vite:afterUpdate', handler);
70
+ };
71
+ }, [refetch]);
72
+
73
+ return { comments, error, refetch, add, remove };
74
+ }
@@ -0,0 +1,16 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ export type SlidePage = ComponentType;
4
+
5
+ export type DeckMeta = {
6
+ title?: string;
7
+ theme?: 'light' | 'dark';
8
+ };
9
+
10
+ export type DeckModule = {
11
+ default: SlidePage[];
12
+ meta?: DeckMeta;
13
+ };
14
+
15
+ export const CANVAS_WIDTH = 1920;
16
+ export const CANVAS_HEIGHT = 1080;
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App';
4
+ import './styles.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,185 @@
1
+ import { ChevronLeft, ChevronRight, Play } from 'lucide-react';
2
+ import { useCallback, useEffect, useMemo, 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 { Player } from '../components/Player';
10
+ import { SlideCanvas } from '../components/SlideCanvas';
11
+ import { ThumbnailRail } from '../components/ThumbnailRail';
12
+ import { loadDeck } from '../lib/decks';
13
+ import type { DeckModule } from '../lib/sdk';
14
+
15
+ export function Deck() {
16
+ const { deckId = '' } = useParams();
17
+ const [searchParams, setSearchParams] = useSearchParams();
18
+ const [deck, setDeck] = useState<DeckModule | null>(null);
19
+ const [error, setError] = useState<string | null>(null);
20
+ const [playing, setPlaying] = useState(false);
21
+
22
+ useEffect(() => {
23
+ let cancelled = false;
24
+ setDeck(null);
25
+ setError(null);
26
+ loadDeck(deckId)
27
+ .then((mod) => {
28
+ if (!cancelled) setDeck(mod);
29
+ })
30
+ .catch((e) => {
31
+ if (!cancelled) setError(String(e?.message ?? e));
32
+ });
33
+ return () => {
34
+ cancelled = true;
35
+ };
36
+ }, [deckId]);
37
+
38
+ const pages = useMemo(() => deck?.default ?? [], [deck]);
39
+ const pageCount = pages.length;
40
+ const rawIndex = Number(searchParams.get('p') ?? '1') - 1;
41
+ const index = Number.isFinite(rawIndex) ? Math.max(0, Math.min(pageCount - 1, rawIndex)) : 0;
42
+
43
+ const goTo = useCallback(
44
+ (i: number) => {
45
+ const clamped = Math.max(0, Math.min(pageCount - 1, i));
46
+ setSearchParams(
47
+ (prev) => {
48
+ const next = new URLSearchParams(prev);
49
+ next.set('p', String(clamped + 1));
50
+ return next;
51
+ },
52
+ { replace: true },
53
+ );
54
+ },
55
+ [pageCount, setSearchParams],
56
+ );
57
+
58
+ useEffect(() => {
59
+ if (playing) return;
60
+ const onKey = (e: KeyboardEvent) => {
61
+ if (e.target instanceof HTMLElement && e.target.matches('input, textarea')) return;
62
+ if (e.key === 'ArrowRight' || e.key === 'PageDown') {
63
+ e.preventDefault();
64
+ goTo(index + 1);
65
+ } else if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
66
+ e.preventDefault();
67
+ goTo(index - 1);
68
+ } else if (e.key === 'f' || e.key === 'F') {
69
+ setPlaying(true);
70
+ }
71
+ };
72
+ window.addEventListener('keydown', onKey);
73
+ return () => window.removeEventListener('keydown', onKey);
74
+ }, [index, goTo, playing]);
75
+
76
+ if (error) {
77
+ return (
78
+ <div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
79
+ <Link to="/" className="text-sm font-medium text-primary hover:underline">
80
+ ← Home
81
+ </Link>
82
+ <h2 className="mt-4 text-xl font-semibold text-foreground">Failed to load deck</h2>
83
+ <pre className="mt-4 overflow-auto rounded-md border bg-card p-4 text-xs whitespace-pre-wrap shadow-sm">
84
+ {error}
85
+ </pre>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ if (!deck) {
91
+ return (
92
+ <div className="mx-auto max-w-3xl px-8 py-16 text-sm text-muted-foreground">
93
+ Loading {deckId}…
94
+ </div>
95
+ );
96
+ }
97
+
98
+ if (pageCount === 0) {
99
+ return (
100
+ <div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
101
+ <Link to="/" className="text-sm font-medium text-primary hover:underline">
102
+ ← Home
103
+ </Link>
104
+ <h2 className="mt-4 text-xl font-semibold text-foreground">Empty deck</h2>
105
+ <p className="mt-2 text-sm">
106
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
107
+ slides/{deckId}/index.tsx
108
+ </code>{' '}
109
+ must{' '}
110
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">export default</code> a
111
+ non-empty array of components.
112
+ </p>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ if (playing) {
118
+ return (
119
+ <Player pages={pages} index={index} onIndexChange={goTo} onExit={() => setPlaying(false)} />
120
+ );
121
+ }
122
+
123
+ const CurrentPage = pages[index];
124
+ const title = deck.meta?.title ?? deckId;
125
+
126
+ return (
127
+ <InspectorProvider deckId={deckId}>
128
+ <div className="flex h-screen flex-col overflow-hidden bg-background">
129
+ <header className="flex shrink-0 items-center gap-4 border-b bg-card px-5 py-3">
130
+ <Button asChild variant="ghost" size="sm">
131
+ <Link to="/">
132
+ <ChevronLeft className="size-4" />
133
+ Home
134
+ </Link>
135
+ </Button>
136
+ <Separator orientation="vertical" className="h-5" />
137
+ <h1 className="flex-1 text-center text-sm font-semibold tracking-tight">{title}</h1>
138
+ <InspectToggleButton />
139
+ <Button size="sm" onClick={() => setPlaying(true)}>
140
+ <Play className="size-4" />
141
+ Play <kbd className="ml-1 rounded bg-primary-foreground/20 px-1 text-[10px]">F</kbd>
142
+ </Button>
143
+ </header>
144
+
145
+ <div className="flex min-h-0 flex-1">
146
+ <div className="w-60 shrink-0">
147
+ <ThumbnailRail pages={pages} current={index} onSelect={goTo} />
148
+ </div>
149
+ <main className="relative min-h-0 min-w-0 flex-1 bg-background p-8">
150
+ <SlideCanvas>
151
+ <CurrentPage />
152
+ </SlideCanvas>
153
+ <InspectOverlay />
154
+ </main>
155
+ </div>
156
+
157
+ <footer className="flex shrink-0 items-center justify-center gap-4 border-t bg-card p-3">
158
+ <Button
159
+ variant="outline"
160
+ size="sm"
161
+ onClick={() => goTo(index - 1)}
162
+ disabled={index === 0}
163
+ >
164
+ <ChevronLeft className="size-4" />
165
+ Prev
166
+ </Button>
167
+ <span className="min-w-16 text-center text-sm text-muted-foreground tabular-nums">
168
+ {index + 1} / {pageCount}
169
+ </span>
170
+ <Button
171
+ variant="outline"
172
+ size="sm"
173
+ onClick={() => goTo(index + 1)}
174
+ disabled={index === pageCount - 1}
175
+ >
176
+ Next
177
+ <ChevronRight className="size-4" />
178
+ </Button>
179
+ </footer>
180
+
181
+ <CommentWidget />
182
+ </div>
183
+ </InspectorProvider>
184
+ );
185
+ }
@@ -0,0 +1,98 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { FolderPlus } from 'lucide-react';
4
+ import { deckIds, loadDeck } from '../lib/decks';
5
+ import type { DeckModule } from '../lib/sdk';
6
+ import { SlideCanvas } from '../components/SlideCanvas';
7
+ import { Card, CardContent } from '@/components/ui/card';
8
+
9
+ export function Home() {
10
+ return (
11
+ <div className="mx-auto max-w-6xl px-8 py-16">
12
+ <header className="mb-10 flex items-end justify-between gap-6">
13
+ <div>
14
+ <h1 className="font-heading text-3xl font-bold tracking-tight">open-slide</h1>
15
+ <p className="mt-1 text-sm text-muted-foreground">
16
+ {deckIds.length} deck{deckIds.length === 1 ? '' : 's'} · start with any agent using the{' '}
17
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">create-slide</code>{' '}
18
+ skill
19
+ </p>
20
+ </div>
21
+ </header>
22
+
23
+ {deckIds.length === 0 ? (
24
+ <Card className="border-dashed">
25
+ <CardContent className="flex flex-col items-center gap-3 py-16 text-center text-muted-foreground">
26
+ <FolderPlus className="size-8 opacity-50" />
27
+ <p>No decks yet.</p>
28
+ <p className="text-sm">
29
+ Create{' '}
30
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
31
+ slides/my-deck/index.tsx
32
+ </code>{' '}
33
+ with{' '}
34
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
35
+ export default [Page1, Page2]
36
+ </code>
37
+ .
38
+ </p>
39
+ </CardContent>
40
+ </Card>
41
+ ) : (
42
+ <ul className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-5">
43
+ {deckIds.map((id) => (
44
+ <li key={id}>
45
+ <DeckCard id={id} />
46
+ </li>
47
+ ))}
48
+ </ul>
49
+ )}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ function DeckCard({ id }: { id: string }) {
55
+ const [deck, setDeck] = useState<DeckModule | null>(null);
56
+ useEffect(() => {
57
+ let cancelled = false;
58
+ loadDeck(id)
59
+ .then((mod) => {
60
+ if (!cancelled) setDeck(mod);
61
+ })
62
+ .catch(() => {});
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }, [id]);
67
+
68
+ const FirstPage = deck?.default[0];
69
+ const title = deck?.meta?.title ?? id;
70
+ const pageCount = deck?.default.length ?? 0;
71
+
72
+ return (
73
+ <Link
74
+ to={`/d/${id}`}
75
+ className="group block overflow-hidden rounded-xl bg-card text-card-foreground ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:ring-foreground/20 hover:shadow-lg"
76
+ >
77
+ <div className="relative aspect-video overflow-hidden bg-gradient-to-br from-indigo-50 to-violet-50">
78
+ {FirstPage ? (
79
+ <SlideCanvas flat>
80
+ <FirstPage />
81
+ </SlideCanvas>
82
+ ) : (
83
+ <div className="grid h-full w-full place-items-center text-xs tracking-widest uppercase text-muted-foreground/60">
84
+ Loading
85
+ </div>
86
+ )}
87
+ </div>
88
+ <div className="flex items-baseline justify-between gap-3 px-4 py-3">
89
+ <span className="truncate text-sm font-medium">{title}</span>
90
+ {pageCount > 0 && (
91
+ <span className="shrink-0 text-xs text-muted-foreground tabular-nums">
92
+ {pageCount} page{pageCount === 1 ? '' : 's'}
93
+ </span>
94
+ )}
95
+ </div>
96
+ </Link>
97
+ );
98
+ }
@@ -0,0 +1,130 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @import "@fontsource-variable/geist";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ @theme inline {
9
+ --font-heading: var(--font-sans);
10
+ --font-sans: "Geist Variable", sans-serif;
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --color-foreground: var(--foreground);
41
+ --color-background: var(--background);
42
+ --radius-sm: calc(var(--radius) * 0.6);
43
+ --radius-md: calc(var(--radius) * 0.8);
44
+ --radius-lg: var(--radius);
45
+ --radius-xl: calc(var(--radius) * 1.4);
46
+ --radius-2xl: calc(var(--radius) * 1.8);
47
+ --radius-3xl: calc(var(--radius) * 2.2);
48
+ --radius-4xl: calc(var(--radius) * 2.6);
49
+ }
50
+
51
+ :root {
52
+ --background: oklch(1 0 0);
53
+ --foreground: oklch(0.145 0 0);
54
+ --card: oklch(1 0 0);
55
+ --card-foreground: oklch(0.145 0 0);
56
+ --popover: oklch(1 0 0);
57
+ --popover-foreground: oklch(0.145 0 0);
58
+ --primary: oklch(0.205 0 0);
59
+ --primary-foreground: oklch(0.985 0 0);
60
+ --secondary: oklch(0.97 0 0);
61
+ --secondary-foreground: oklch(0.205 0 0);
62
+ --muted: oklch(0.97 0 0);
63
+ --muted-foreground: oklch(0.556 0 0);
64
+ --accent: oklch(0.97 0 0);
65
+ --accent-foreground: oklch(0.205 0 0);
66
+ --destructive: oklch(0.577 0.245 27.325);
67
+ --border: oklch(0.922 0 0);
68
+ --input: oklch(0.922 0 0);
69
+ --ring: oklch(0.708 0 0);
70
+ --chart-1: oklch(0.87 0 0);
71
+ --chart-2: oklch(0.556 0 0);
72
+ --chart-3: oklch(0.439 0 0);
73
+ --chart-4: oklch(0.371 0 0);
74
+ --chart-5: oklch(0.269 0 0);
75
+ --radius: 0.625rem;
76
+ --sidebar: oklch(0.985 0 0);
77
+ --sidebar-foreground: oklch(0.145 0 0);
78
+ --sidebar-primary: oklch(0.205 0 0);
79
+ --sidebar-primary-foreground: oklch(0.985 0 0);
80
+ --sidebar-accent: oklch(0.97 0 0);
81
+ --sidebar-accent-foreground: oklch(0.205 0 0);
82
+ --sidebar-border: oklch(0.922 0 0);
83
+ --sidebar-ring: oklch(0.708 0 0);
84
+ }
85
+
86
+ .dark {
87
+ --background: oklch(0.145 0 0);
88
+ --foreground: oklch(0.985 0 0);
89
+ --card: oklch(0.205 0 0);
90
+ --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.205 0 0);
92
+ --popover-foreground: oklch(0.985 0 0);
93
+ --primary: oklch(0.922 0 0);
94
+ --primary-foreground: oklch(0.205 0 0);
95
+ --secondary: oklch(0.269 0 0);
96
+ --secondary-foreground: oklch(0.985 0 0);
97
+ --muted: oklch(0.269 0 0);
98
+ --muted-foreground: oklch(0.708 0 0);
99
+ --accent: oklch(0.269 0 0);
100
+ --accent-foreground: oklch(0.985 0 0);
101
+ --destructive: oklch(0.704 0.191 22.216);
102
+ --border: oklch(1 0 0 / 10%);
103
+ --input: oklch(1 0 0 / 15%);
104
+ --ring: oklch(0.556 0 0);
105
+ --chart-1: oklch(0.87 0 0);
106
+ --chart-2: oklch(0.556 0 0);
107
+ --chart-3: oklch(0.439 0 0);
108
+ --chart-4: oklch(0.371 0 0);
109
+ --chart-5: oklch(0.269 0 0);
110
+ --sidebar: oklch(0.205 0 0);
111
+ --sidebar-foreground: oklch(0.985 0 0);
112
+ --sidebar-primary: oklch(0.488 0.243 264.376);
113
+ --sidebar-primary-foreground: oklch(0.985 0 0);
114
+ --sidebar-accent: oklch(0.269 0 0);
115
+ --sidebar-accent-foreground: oklch(0.985 0 0);
116
+ --sidebar-border: oklch(1 0 0 / 10%);
117
+ --sidebar-ring: oklch(0.556 0 0);
118
+ }
119
+
120
+ @layer base {
121
+ * {
122
+ @apply border-border outline-ring/50;
123
+ }
124
+ body {
125
+ @apply bg-background text-foreground;
126
+ }
127
+ html {
128
+ @apply font-sans;
129
+ }
130
+ }
@@ -0,0 +1,14 @@
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>;
5
+ }
6
+
7
+ declare module 'virtual:open-slide/config' {
8
+ const config: {
9
+ title?: string;
10
+ slidesDir?: string;
11
+ port?: number;
12
+ };
13
+ export default config;
14
+ }