supaslidev 0.1.4 → 0.2.0

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 (67) hide show
  1. package/app/app.config.ts +9 -0
  2. package/app/assets/css/main.css +90 -0
  3. package/app/components/AppHeader.vue +429 -0
  4. package/app/components/CreatePresentationDialog.vue +236 -0
  5. package/app/components/EmptyState.vue +37 -0
  6. package/app/components/ImportPresentationDialog.vue +865 -0
  7. package/app/components/PresentationCard.vue +343 -0
  8. package/app/components/PresentationListItem.vue +242 -0
  9. package/app/composables/useServers.ts +148 -0
  10. package/app/layouts/default.vue +49 -0
  11. package/app/pages/index.vue +542 -0
  12. package/dist/cli/index.js +183751 -137
  13. package/dist/config.d.ts +8 -0
  14. package/dist/config.js +16 -0
  15. package/dist/index.d.ts +21 -0
  16. package/dist/index.js +3 -0
  17. package/dist/module.d.ts +6 -0
  18. package/dist/module.js +9168 -0
  19. package/dist/prompt.js +847 -0
  20. package/nuxt.config.ts +53 -0
  21. package/package.json +26 -19
  22. package/server/api/export/[id].post.ts +67 -0
  23. package/server/api/open-editor/[id].post.ts +28 -0
  24. package/server/api/presentations/import.post.ts +139 -0
  25. package/server/api/presentations/index.get.ts +18 -0
  26. package/server/api/presentations/index.post.ts +175 -0
  27. package/server/api/presentations/upload.post.ts +174 -0
  28. package/server/api/presentations/validate.post.ts +14 -0
  29. package/server/api/servers/[id].delete.ts +15 -0
  30. package/server/api/servers/[id].post.ts +17 -0
  31. package/server/api/servers/index.delete.ts +5 -0
  32. package/server/api/servers/index.get.ts +5 -0
  33. package/server/api/servers/stop-all.post.ts +5 -0
  34. package/server/plugins/generate.ts +12 -0
  35. package/server/plugins/shutdown.ts +16 -0
  36. package/server/routes/exports/[...path].get.ts +25 -0
  37. package/server/utils/config.ts +13 -0
  38. package/server/utils/process-manager.ts +119 -0
  39. package/src/cli/commands/create.ts +125 -0
  40. package/src/cli/commands/deploy.ts +90 -0
  41. package/src/cli/commands/dev.ts +116 -0
  42. package/src/cli/commands/export.ts +63 -0
  43. package/src/cli/commands/import.ts +178 -0
  44. package/src/cli/commands/present.ts +111 -0
  45. package/src/cli/index.ts +87 -0
  46. package/src/cli/utils.ts +94 -0
  47. package/src/config.ts +21 -0
  48. package/src/index.ts +2 -0
  49. package/src/module.ts +12 -0
  50. package/src/shared/catalog.ts +94 -0
  51. package/src/shared/copy.ts +28 -0
  52. package/src/shared/index.ts +29 -0
  53. package/{scripts/generate-presentations.mjs → src/shared/presentations.ts} +23 -46
  54. package/src/shared/types.ts +29 -0
  55. package/src/shared/validation.ts +111 -0
  56. package/dist/assets/index-BerY9FcI.js +0 -49
  57. package/dist/assets/index-CVzsY-on.css +0 -1
  58. package/dist/index.html +0 -24
  59. package/server/api.js +0 -1225
  60. /package/{dist → public}/apple-touch-icon.png +0 -0
  61. /package/{dist → public}/favicon-96x96.png +0 -0
  62. /package/{dist → public}/favicon.ico +0 -0
  63. /package/{dist → public}/favicon.svg +0 -0
  64. /package/{dist → public}/site.webmanifest +0 -0
  65. /package/{dist → public}/ssl-logo.png +0 -0
  66. /package/{dist → public}/web-app-manifest-192x192.png +0 -0
  67. /package/{dist → public}/web-app-manifest-512x512.png +0 -0
@@ -0,0 +1,542 @@
1
+ <script setup lang="ts">
2
+ import type { CommandPaletteGroup, CommandPaletteItem } from '@nuxt/ui';
3
+ import type { Presentation } from '../composables/useServers';
4
+
5
+ const {
6
+ startPolling,
7
+ stopPolling,
8
+ stopAllServers,
9
+ startServer,
10
+ exportPresentation,
11
+ openInEditor,
12
+ waitForServerReady,
13
+ } = useServers();
14
+ const toast = useToast();
15
+ const colorMode = useColorMode();
16
+
17
+ function handleExportError(message: string) {
18
+ toast.add({
19
+ title: 'Export Failed',
20
+ description: message,
21
+ color: 'error',
22
+ icon: 'i-lucide-alert-circle',
23
+ });
24
+ }
25
+
26
+ const isDialogOpen = ref(false);
27
+ const isImportDialogOpen = ref(false);
28
+ const isCommandPaletteOpen = ref(false);
29
+ const initialSearchQuery = ref('');
30
+ const appHeaderRef = ref<InstanceType<typeof AppHeader> | null>(null);
31
+
32
+ function handleKeydown(event: KeyboardEvent) {
33
+ if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
34
+ event.preventDefault();
35
+ isCommandPaletteOpen.value = !isCommandPaletteOpen.value;
36
+ return;
37
+ }
38
+
39
+ if (isCommandPaletteOpen.value || isDialogOpen.value || isImportDialogOpen.value) {
40
+ return;
41
+ }
42
+
43
+ const activeElement = document.activeElement;
44
+ const isInputFocused =
45
+ activeElement instanceof HTMLInputElement ||
46
+ activeElement instanceof HTMLTextAreaElement ||
47
+ activeElement?.getAttribute('contenteditable') === 'true';
48
+
49
+ if (isInputFocused) {
50
+ return;
51
+ }
52
+
53
+ if (event.metaKey || event.ctrlKey || event.altKey) {
54
+ return;
55
+ }
56
+
57
+ const isTypingKey = event.key.length === 1 && !event.repeat;
58
+
59
+ if (isTypingKey) {
60
+ event.preventDefault();
61
+ if (appHeaderRef.value?.inputRef) {
62
+ appHeaderRef.value.inputRef.value = event.key;
63
+ appHeaderRef.value.inputRef.dispatchEvent(new Event('input', { bubbles: true }));
64
+ appHeaderRef.value.focusInput();
65
+ }
66
+ }
67
+ }
68
+
69
+ function handlePresentationCreated(presentation: Presentation) {
70
+ presentations.value = [...presentations.value, presentation].sort((a, b) =>
71
+ a.title.localeCompare(b.title),
72
+ );
73
+ }
74
+
75
+ function handlePresentationImported(imported: Presentation[]) {
76
+ presentations.value = [...presentations.value, ...imported].sort((a, b) =>
77
+ a.title.localeCompare(b.title),
78
+ );
79
+ }
80
+
81
+ function handleBeforeUnload() {
82
+ navigator.sendBeacon('/api/servers/stop-all');
83
+ }
84
+
85
+ const presentations = ref<Presentation[]>([]);
86
+
87
+ onMounted(async () => {
88
+ startPolling();
89
+ window.addEventListener('beforeunload', handleBeforeUnload);
90
+ window.addEventListener('keydown', handleKeydown);
91
+
92
+ try {
93
+ const response = await fetch('/api/presentations');
94
+ if (response.ok) {
95
+ presentations.value = await response.json();
96
+ }
97
+ } catch {
98
+ // API not available yet
99
+ }
100
+ });
101
+
102
+ onUnmounted(() => {
103
+ stopPolling();
104
+ stopAllServers();
105
+ window.removeEventListener('beforeunload', handleBeforeUnload);
106
+ window.removeEventListener('keydown', handleKeydown);
107
+ });
108
+ const searchQuery = ref('');
109
+
110
+ const VIEW_MODE_STORAGE_KEY = 'supaslidev-view-mode';
111
+
112
+ function loadViewMode(): 'grid' | 'list' {
113
+ if (import.meta.server) return 'grid';
114
+ const saved = localStorage.getItem(VIEW_MODE_STORAGE_KEY);
115
+ return saved === 'list' ? 'list' : 'grid';
116
+ }
117
+
118
+ const viewMode = ref<'grid' | 'list'>(loadViewMode());
119
+
120
+ watch(viewMode, (newMode) => {
121
+ localStorage.setItem(VIEW_MODE_STORAGE_KEY, newMode);
122
+ });
123
+
124
+ async function handlePresentCommand(presentation: Presentation) {
125
+ isCommandPaletteOpen.value = false;
126
+ const result = await startServer(presentation.id);
127
+ if (result.success && result.port) {
128
+ const isReady = await waitForServerReady(result.port);
129
+ if (isReady) {
130
+ window.open(`http://localhost:${result.port}`, '_blank');
131
+ }
132
+ }
133
+ }
134
+
135
+ async function handleExportCommand(presentation: Presentation) {
136
+ isCommandPaletteOpen.value = false;
137
+ const result = await exportPresentation(presentation.id);
138
+ if (result.success && result.pdfPath) {
139
+ window.open(result.pdfPath, '_blank');
140
+ } else if (result.error) {
141
+ handleExportError(result.error);
142
+ }
143
+ }
144
+
145
+ async function handleEditCommand(presentation: Presentation) {
146
+ isCommandPaletteOpen.value = false;
147
+ const result = await openInEditor(presentation.id);
148
+ if (!result.success && result.error) {
149
+ toast.add({
150
+ title: 'Editor Error',
151
+ description: result.error,
152
+ color: 'error',
153
+ icon: 'i-lucide-alert-circle',
154
+ });
155
+ }
156
+ }
157
+
158
+ function handleCreateCommand() {
159
+ isCommandPaletteOpen.value = false;
160
+ isDialogOpen.value = true;
161
+ }
162
+
163
+ function handleImportCommand() {
164
+ isCommandPaletteOpen.value = false;
165
+ isImportDialogOpen.value = true;
166
+ }
167
+
168
+ function handleToggleThemeCommand() {
169
+ isCommandPaletteOpen.value = false;
170
+ colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';
171
+ }
172
+
173
+ function handleToggleViewModeCommand() {
174
+ isCommandPaletteOpen.value = false;
175
+ viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid';
176
+ }
177
+
178
+ function findPresentationByName(name: string): Presentation | undefined {
179
+ const normalizedName = name.toLowerCase().trim();
180
+ return presentations.value.find(
181
+ (p) => p.id.toLowerCase() === normalizedName || p.title.toLowerCase() === normalizedName,
182
+ );
183
+ }
184
+
185
+ function handleExecuteCommand(command: string) {
186
+ const parts = command.trim().split(/\s+/);
187
+ const action = parts[0]?.toLowerCase();
188
+ const arg = parts.slice(1).join(' ');
189
+
190
+ if (action === 'new') {
191
+ handleCreateCommand();
192
+ return;
193
+ }
194
+
195
+ if (action === 'import') {
196
+ handleImportCommand();
197
+ return;
198
+ }
199
+
200
+ if (action === 'present') {
201
+ if (!arg) {
202
+ toast.add({
203
+ title: 'Missing argument',
204
+ description: 'Usage: present <presentation-name>',
205
+ color: 'warning',
206
+ icon: 'i-lucide-alert-triangle',
207
+ });
208
+ return;
209
+ }
210
+ const presentation = findPresentationByName(arg);
211
+ if (!presentation) {
212
+ toast.add({
213
+ title: 'Presentation not found',
214
+ description: `No presentation found with name "${arg}"`,
215
+ color: 'warning',
216
+ icon: 'i-lucide-alert-triangle',
217
+ });
218
+ return;
219
+ }
220
+ handlePresentCommand(presentation);
221
+ return;
222
+ }
223
+
224
+ if (action === 'export') {
225
+ if (!arg) {
226
+ toast.add({
227
+ title: 'Missing argument',
228
+ description: 'Usage: export <presentation-name>',
229
+ color: 'warning',
230
+ icon: 'i-lucide-alert-triangle',
231
+ });
232
+ return;
233
+ }
234
+ const presentation = findPresentationByName(arg);
235
+ if (!presentation) {
236
+ toast.add({
237
+ title: 'Presentation not found',
238
+ description: `No presentation found with name "${arg}"`,
239
+ color: 'warning',
240
+ icon: 'i-lucide-alert-triangle',
241
+ });
242
+ return;
243
+ }
244
+ handleExportCommand(presentation);
245
+ return;
246
+ }
247
+
248
+ if (action === 'edit') {
249
+ if (!arg) {
250
+ toast.add({
251
+ title: 'Missing argument',
252
+ description: 'Usage: edit <presentation-name>',
253
+ color: 'warning',
254
+ icon: 'i-lucide-alert-triangle',
255
+ });
256
+ return;
257
+ }
258
+ const presentation = findPresentationByName(arg);
259
+ if (!presentation) {
260
+ toast.add({
261
+ title: 'Presentation not found',
262
+ description: `No presentation found with name "${arg}"`,
263
+ color: 'warning',
264
+ icon: 'i-lucide-alert-triangle',
265
+ });
266
+ return;
267
+ }
268
+ handleEditCommand(presentation);
269
+ return;
270
+ }
271
+
272
+ toast.add({
273
+ title: 'Unknown command',
274
+ description: `"${action}" is not a recognized command. Try: new, import, present, export, edit`,
275
+ color: 'warning',
276
+ icon: 'i-lucide-alert-triangle',
277
+ });
278
+ }
279
+
280
+ const commandPaletteGroups = computed<CommandPaletteGroup[]>(() => [
281
+ {
282
+ id: 'actions',
283
+ label: 'Actions',
284
+ items: [
285
+ {
286
+ label: 'New',
287
+ suffix: 'Create a new presentation',
288
+ icon: 'i-lucide-plus',
289
+ onSelect: handleCreateCommand,
290
+ },
291
+ {
292
+ label: 'Import',
293
+ suffix: 'Import existing Sli.dev presentation(s)',
294
+ icon: 'i-lucide-import',
295
+ onSelect: handleImportCommand,
296
+ },
297
+ {
298
+ label: 'Toggle theme',
299
+ suffix: colorMode.value === 'dark' ? 'Switch to light mode' : 'Switch to dark mode',
300
+ icon: colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon',
301
+ onSelect: handleToggleThemeCommand,
302
+ },
303
+ {
304
+ label: 'Toggle view',
305
+ suffix: viewMode.value === 'grid' ? 'Switch to list layout' : 'Switch to grid layout',
306
+ icon: viewMode.value === 'grid' ? 'i-lucide-list' : 'i-lucide-layout-grid',
307
+ onSelect: handleToggleViewModeCommand,
308
+ },
309
+ ],
310
+ },
311
+ {
312
+ id: 'presentations',
313
+ label: 'Present',
314
+ items: presentations.value.map(
315
+ (p): CommandPaletteItem => ({
316
+ label: `Present > ${p.title}`,
317
+ suffix: 'Start dev server and open in browser',
318
+ icon: 'i-lucide-play',
319
+ onSelect: () => handlePresentCommand(p),
320
+ }),
321
+ ),
322
+ },
323
+ {
324
+ id: 'export',
325
+ label: 'Export',
326
+ items: presentations.value.map(
327
+ (p): CommandPaletteItem => ({
328
+ label: `Export > ${p.title}`,
329
+ suffix: 'Export to PDF',
330
+ icon: 'i-lucide-download',
331
+ onSelect: () => handleExportCommand(p),
332
+ }),
333
+ ),
334
+ },
335
+ {
336
+ id: 'edit',
337
+ label: 'Edit',
338
+ items: presentations.value.map(
339
+ (p): CommandPaletteItem => ({
340
+ label: `Edit > ${p.title}`,
341
+ suffix: 'Open in VS Code',
342
+ icon: 'i-lucide-pencil',
343
+ onSelect: () => handleEditCommand(p),
344
+ }),
345
+ ),
346
+ },
347
+ ]);
348
+
349
+ const filteredPresentations = computed(() => {
350
+ if (!searchQuery.value.trim()) {
351
+ return presentations.value;
352
+ }
353
+ const query = searchQuery.value.toLowerCase();
354
+ return presentations.value.filter((p) => p.title.toLowerCase().includes(query));
355
+ });
356
+
357
+ const commandOptions = computed(() => {
358
+ const options: { label: string; description?: string; onSelect: () => void }[] = [
359
+ { label: 'New', description: 'Create a new presentation', onSelect: handleCreateCommand },
360
+ {
361
+ label: 'Import',
362
+ description: 'Import existing Sli.dev presentation(s)',
363
+ onSelect: handleImportCommand,
364
+ },
365
+ {
366
+ label: 'Toggle theme',
367
+ description: colorMode.value === 'dark' ? 'Switch to light mode' : 'Switch to dark mode',
368
+ onSelect: handleToggleThemeCommand,
369
+ },
370
+ {
371
+ label: 'Toggle view',
372
+ description: viewMode.value === 'grid' ? 'Switch to list layout' : 'Switch to grid layout',
373
+ onSelect: handleToggleViewModeCommand,
374
+ },
375
+ ];
376
+
377
+ presentations.value.forEach((p) => {
378
+ options.push({
379
+ label: `Present > ${p.title}`,
380
+ description: 'Start dev server and open in browser',
381
+ onSelect: () => handlePresentCommand(p),
382
+ });
383
+ options.push({
384
+ label: `Export > ${p.title}`,
385
+ description: 'Export to PDF',
386
+ onSelect: () => handleExportCommand(p),
387
+ });
388
+ options.push({
389
+ label: `Edit > ${p.title}`,
390
+ description: 'Open in VS Code',
391
+ onSelect: () => handleEditCommand(p),
392
+ });
393
+ });
394
+
395
+ return options;
396
+ });
397
+ </script>
398
+
399
+ <template>
400
+ <div>
401
+ <AppHeader
402
+ ref="appHeaderRef"
403
+ :commands="commandOptions"
404
+ @open-command-palette="isCommandPaletteOpen = true"
405
+ @execute-command="handleExecuteCommand"
406
+ />
407
+
408
+ <div class="flex items-center justify-between mb-6">
409
+ <p class="text-muted font-mono text-sm">
410
+ {{ filteredPresentations.length }} presentation{{
411
+ filteredPresentations.length !== 1 ? 's' : ''
412
+ }}
413
+ </p>
414
+ <div class="flex items-center gap-3">
415
+ <UButton variant="outline" class="font-mono" @click="isImportDialogOpen = true">
416
+ <template #leading>
417
+ <span class="opacity-70">$</span>
418
+ </template>
419
+ import
420
+ </UButton>
421
+ <UButton class="btn-new font-mono" @click="isDialogOpen = true">
422
+ <template #leading>
423
+ <span class="opacity-70">$</span>
424
+ </template>
425
+ new
426
+ </UButton>
427
+ </div>
428
+ </div>
429
+
430
+ <div class="mb-6 flex justify-center">
431
+ <UInput
432
+ v-model="searchQuery"
433
+ icon="i-lucide-search"
434
+ placeholder="Search presentations by title..."
435
+ class="filter-input max-w-md w-full"
436
+ size="lg"
437
+ />
438
+ </div>
439
+
440
+ <template v-if="presentations.length === 0">
441
+ <EmptyState
442
+ icon="i-lucide-presentation"
443
+ title="No presentations yet"
444
+ description="Create your first presentation to get started with supaslidev."
445
+ >
446
+ <UButton class="font-mono" @click="isDialogOpen = true">
447
+ <template #leading>
448
+ <span class="opacity-70">$</span>
449
+ </template>
450
+ create presentation
451
+ </UButton>
452
+ </EmptyState>
453
+ </template>
454
+
455
+ <template v-else>
456
+ <div v-if="filteredPresentations.length > 0" class="view-container">
457
+ <Transition name="view-fade" mode="out-in">
458
+ <div
459
+ v-if="viewMode === 'grid'"
460
+ key="grid"
461
+ class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6"
462
+ >
463
+ <TransitionGroup name="card" appear>
464
+ <PresentationCard
465
+ v-for="presentation in filteredPresentations"
466
+ :key="presentation.id"
467
+ :presentation="presentation"
468
+ @export-error="handleExportError"
469
+ />
470
+ </TransitionGroup>
471
+ </div>
472
+ <div v-else key="list" class="flex flex-col gap-2">
473
+ <TransitionGroup name="list" appear>
474
+ <PresentationListItem
475
+ v-for="presentation in filteredPresentations"
476
+ :key="presentation.id"
477
+ :presentation="presentation"
478
+ @export-error="handleExportError"
479
+ />
480
+ </TransitionGroup>
481
+ </div>
482
+ </Transition>
483
+ </div>
484
+
485
+ <EmptyState
486
+ v-else
487
+ icon="i-lucide-search-x"
488
+ title="No presentations found"
489
+ description="Try adjusting your search query."
490
+ >
491
+ <UButton variant="soft" class="font-mono" @click="searchQuery = ''">
492
+ <template #leading>
493
+ <span class="opacity-70">$</span>
494
+ </template>
495
+ clear search
496
+ </UButton>
497
+ </EmptyState>
498
+ </template>
499
+
500
+ <CreatePresentationDialog
501
+ :open="isDialogOpen"
502
+ @close="isDialogOpen = false"
503
+ @created="handlePresentationCreated"
504
+ />
505
+
506
+ <ImportPresentationDialog
507
+ :open="isImportDialogOpen"
508
+ @close="isImportDialogOpen = false"
509
+ @imported="handlePresentationImported"
510
+ />
511
+
512
+ <UModal v-model:open="isCommandPaletteOpen" @after-leave="initialSearchQuery = ''">
513
+ <template #body>
514
+ <UCommandPalette
515
+ v-model:search-term="initialSearchQuery"
516
+ :groups="commandPaletteGroups"
517
+ :fuse="{
518
+ fuseOptions: {
519
+ threshold: 0.4,
520
+ keys: ['label', 'suffix'],
521
+ ignoreLocation: true,
522
+ },
523
+ matchAllWhenSearchEmpty: true,
524
+ }"
525
+ placeholder="Search commands..."
526
+ class="h-80"
527
+ />
528
+ </template>
529
+ </UModal>
530
+ </div>
531
+ </template>
532
+
533
+ <style scoped>
534
+ .filter-input :deep(input) {
535
+ border: 1px solid var(--supaslidev-border);
536
+ border-radius: 0.5rem;
537
+ }
538
+
539
+ .filter-input :deep(input:focus) {
540
+ border-color: var(--ui-border-accented);
541
+ }
542
+ </style>