@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.
@@ -1,16 +1,20 @@
1
- import { ChevronLeft, ChevronRight, Play } from 'lucide-react';
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 { loadSlide } from '../lib/slides';
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-4 border-b bg-card px-5 py-3">
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
- <h1 className="flex-1 text-center text-sm font-semibold tracking-tight">{title}</h1>
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
- Play <kbd className="ml-1 rounded bg-primary-foreground/20 px-1 text-[10px]">F</kbd>
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 className="relative min-h-0 min-w-0 flex-1 bg-background p-8">
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
+ }
@@ -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 };
@@ -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 };
@@ -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 };