@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.
- package/dist/build-0xQdMJb7.js +14 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +58 -0
- package/dist/config-Dk8ASJ8X.js +324 -0
- package/dist/dev-BN2k5C-N.js +14 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +6 -0
- package/dist/preview-B-xUqFKf.js +12 -0
- package/dist/vite/index.d.ts +18 -0
- package/dist/vite/index.js +3 -0
- package/package.json +67 -0
- package/src/app/App.tsx +14 -0
- package/src/app/components/Player.tsx +61 -0
- package/src/app/components/SlideCanvas.tsx +70 -0
- package/src/app/components/ThumbnailRail.tsx +57 -0
- package/src/app/components/inspector/CommentPopover.tsx +102 -0
- package/src/app/components/inspector/CommentWidget.tsx +63 -0
- package/src/app/components/inspector/InspectOverlay.tsx +94 -0
- package/src/app/components/inspector/InspectorProvider.tsx +75 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +67 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/index.html +12 -0
- package/src/app/lib/decks.ts +8 -0
- package/src/app/lib/inspector/fiber.ts +39 -0
- package/src/app/lib/inspector/useComments.ts +74 -0
- package/src/app/lib/sdk.ts +16 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +10 -0
- package/src/app/routes/Deck.tsx +185 -0
- package/src/app/routes/Home.tsx +98 -0
- package/src/app/styles.css +130 -0
- package/src/app/virtual.d.ts +14 -0
|
@@ -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,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;
|
package/src/app/main.tsx
ADDED
|
@@ -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
|
+
}
|