@open-slide/core 0.0.3 → 0.0.5
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-BCORlVF3.js +16 -0
- package/dist/cli/bin.js +25 -36
- package/dist/{config-g-uy_P5U.js → config-DF58h0l4.js} +132 -5
- package/dist/dev-h-rxb3xY.js +19 -0
- package/dist/preview-lskE0s8A.js +17 -0
- package/dist/vite/index.js +1 -1
- package/package.json +5 -2
- package/src/app/components/ClickNavZones.tsx +34 -0
- package/src/app/components/Player.tsx +25 -3
- package/src/app/components/ThumbnailRail.tsx +8 -0
- package/src/app/components/inspector/CommentPopover.tsx +3 -11
- package/src/app/components/inspector/InspectOverlay.tsx +16 -4
- package/src/app/components/inspector/InspectorProvider.tsx +8 -1
- package/src/app/components/sidebar/FolderItem.tsx +1 -4
- package/src/app/components/ui/dialog.tsx +141 -0
- package/src/app/components/ui/dropdown-menu.tsx +41 -70
- package/src/app/components/ui/popover.tsx +22 -37
- package/src/app/components/ui/tabs.tsx +26 -36
- package/src/app/lib/export-html.ts +320 -0
- package/src/app/lib/folders.ts +40 -4
- package/src/app/lib/sdk.ts +1 -3
- package/src/app/routes/Home.tsx +453 -65
- package/src/app/routes/Slide.tsx +151 -38
- package/dist/build-Cav2jYyI.js +0 -14
- package/dist/dev-CFmlBbLh.js +0 -14
- package/dist/preview-CotwHU_d.js +0 -12
package/src/app/routes/Slide.tsx
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { ChevronLeft,
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import { ChevronLeft, Download, Loader2, Pencil, Play } from 'lucide-react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
4
4
|
import { CommentWidget } from '@/components/inspector/CommentWidget';
|
|
5
5
|
import { InspectOverlay } from '@/components/inspector/InspectOverlay';
|
|
6
6
|
import { InspectorProvider, InspectToggleButton } from '@/components/inspector/InspectorProvider';
|
|
7
7
|
import { Button } from '@/components/ui/button';
|
|
8
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';
|
|
9
12
|
import { Player } from '../components/Player';
|
|
10
13
|
import { SlideCanvas } from '../components/SlideCanvas';
|
|
11
14
|
import { ThumbnailRail } from '../components/ThumbnailRail';
|
|
12
|
-
import {
|
|
15
|
+
import { exportSlideAsHtml } from '../lib/export-html';
|
|
13
16
|
import type { SlideModule } from '../lib/sdk';
|
|
17
|
+
import { loadSlide } from '../lib/slides';
|
|
14
18
|
|
|
15
19
|
export function Slide() {
|
|
16
20
|
const { slideId = '' } = useParams();
|
|
@@ -18,6 +22,8 @@ export function Slide() {
|
|
|
18
22
|
const [slide, setSlide] = useState<SlideModule | null>(null);
|
|
19
23
|
const [error, setError] = useState<string | null>(null);
|
|
20
24
|
const [playing, setPlaying] = useState(false);
|
|
25
|
+
const [exporting, setExporting] = useState(false);
|
|
26
|
+
const { renameSlide } = useFolders();
|
|
21
27
|
|
|
22
28
|
useEffect(() => {
|
|
23
29
|
let cancelled = false;
|
|
@@ -59,10 +65,10 @@ export function Slide() {
|
|
|
59
65
|
if (playing) return;
|
|
60
66
|
const onKey = (e: KeyboardEvent) => {
|
|
61
67
|
if (e.target instanceof HTMLElement && e.target.matches('input, textarea')) return;
|
|
62
|
-
if (e.key === 'ArrowRight' || e.key === 'PageDown') {
|
|
68
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'PageDown') {
|
|
63
69
|
e.preventDefault();
|
|
64
70
|
goTo(index + 1);
|
|
65
|
-
} else if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
|
71
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
|
|
66
72
|
e.preventDefault();
|
|
67
73
|
goTo(index - 1);
|
|
68
74
|
} else if (e.key === 'f' || e.key === 'F') {
|
|
@@ -126,60 +132,167 @@ export function Slide() {
|
|
|
126
132
|
return (
|
|
127
133
|
<InspectorProvider slideId={slideId}>
|
|
128
134
|
<div className="flex h-screen flex-col overflow-hidden bg-background">
|
|
129
|
-
<header className="flex shrink-0 items-center gap-
|
|
130
|
-
<Button asChild variant="ghost" size="sm">
|
|
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">
|
|
131
137
|
<Link to="/">
|
|
132
138
|
<ChevronLeft className="size-4" />
|
|
133
|
-
Home
|
|
139
|
+
<span className="hidden md:inline">Home</span>
|
|
134
140
|
</Link>
|
|
135
141
|
</Button>
|
|
136
|
-
<Separator orientation="vertical" className="h-5" />
|
|
137
|
-
<
|
|
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>
|
|
138
169
|
<InspectToggleButton />
|
|
139
|
-
<Button size="sm" onClick={() => setPlaying(true)}>
|
|
170
|
+
<Button size="sm" onClick={() => setPlaying(true)} className="px-2 md:px-3">
|
|
140
171
|
<Play className="size-4" />
|
|
141
|
-
|
|
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>
|
|
142
176
|
</Button>
|
|
143
177
|
</header>
|
|
144
178
|
|
|
145
179
|
<div className="flex min-h-0 flex-1">
|
|
146
|
-
<div className="w-[17rem] shrink-0">
|
|
180
|
+
<div className="hidden w-[17rem] shrink-0 md:block">
|
|
147
181
|
<ThumbnailRail pages={pages} current={index} onSelect={goTo} />
|
|
148
182
|
</div>
|
|
149
|
-
<main
|
|
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
|
+
>
|
|
150
187
|
<SlideCanvas>
|
|
151
188
|
<CurrentPage />
|
|
152
189
|
</SlideCanvas>
|
|
190
|
+
<ClickNavZones
|
|
191
|
+
onPrev={() => goTo(index - 1)}
|
|
192
|
+
onNext={() => goTo(index + 1)}
|
|
193
|
+
canPrev={index > 0}
|
|
194
|
+
canNext={index < pageCount - 1}
|
|
195
|
+
/>
|
|
153
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>
|
|
154
200
|
</main>
|
|
155
201
|
</div>
|
|
156
202
|
|
|
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
203
|
<CommentWidget />
|
|
182
204
|
</div>
|
|
183
205
|
</InspectorProvider>
|
|
184
206
|
);
|
|
185
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
|
+
}
|
package/dist/build-Cav2jYyI.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { createViteConfig } from "./config-g-uy_P5U.js";
|
|
2
|
-
import { build as build$1 } from "vite";
|
|
3
|
-
|
|
4
|
-
//#region src/cli/build.ts
|
|
5
|
-
async function build() {
|
|
6
|
-
const config = await createViteConfig({
|
|
7
|
-
userCwd: process.cwd(),
|
|
8
|
-
mode: "build"
|
|
9
|
-
});
|
|
10
|
-
await build$1(config);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
//#endregion
|
|
14
|
-
export { build };
|
package/dist/dev-CFmlBbLh.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { createViteConfig } from "./config-g-uy_P5U.js";
|
|
2
|
-
import { createServer } from "vite";
|
|
3
|
-
|
|
4
|
-
//#region src/cli/dev.ts
|
|
5
|
-
async function dev() {
|
|
6
|
-
const config = await createViteConfig({ userCwd: process.cwd() });
|
|
7
|
-
const server = await createServer(config);
|
|
8
|
-
await server.listen();
|
|
9
|
-
server.printUrls();
|
|
10
|
-
server.bindCLIShortcuts({ print: true });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
//#endregion
|
|
14
|
-
export { dev };
|
package/dist/preview-CotwHU_d.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createViteConfig } from "./config-g-uy_P5U.js";
|
|
2
|
-
import { preview as preview$1 } from "vite";
|
|
3
|
-
|
|
4
|
-
//#region src/cli/preview.ts
|
|
5
|
-
async function preview() {
|
|
6
|
-
const config = await createViteConfig({ userCwd: process.cwd() });
|
|
7
|
-
const server = await preview$1(config);
|
|
8
|
-
server.printUrls();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
//#endregion
|
|
12
|
-
export { preview };
|