supaslidev 0.3.5 → 0.4.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.
- package/app/assets/css/main.css +5 -5
- package/app/components/AppHeader.vue +3 -3
- package/app/components/PresentationCard.vue +67 -0
- package/app/components/PresentationListItem.vue +60 -0
- package/app/composables/useServers.ts +13 -0
- package/app/layouts/default.vue +1 -1
- package/app/pages/index.vue +79 -1
- package/dist/cli/index.js +1414 -829
- package/dist/index.d.ts +1 -0
- package/package.json +11 -10
- package/server/api/thumbnail/[id].post.ts +115 -0
- package/server/routes/thumbnails/[...path].get.ts +27 -0
- package/server/utils/process-manager.ts +1 -1
- package/src/cli/commands/deploy.ts +87 -8
- package/src/cli/commands/thumbnail.ts +89 -0
- package/src/cli/index.ts +10 -0
- package/src/shared/optimize-thumbnail.ts +23 -0
- package/src/shared/presentations.ts +20 -1
- package/src/shared/types.ts +1 -0
package/app/assets/css/main.css
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
:root {
|
|
9
|
-
--
|
|
10
|
-
--
|
|
9
|
+
--supaslidev-accent: var(--color-primary-700);
|
|
10
|
+
--supaslidev-success: var(--color-green-700);
|
|
11
11
|
--supaslidev-border: #d1d5db;
|
|
12
12
|
--supaslidev-header-bg: rgba(0, 0, 0, 0.02);
|
|
13
13
|
--supaslidev-text-muted: #6b7280;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.dark {
|
|
17
|
-
--
|
|
18
|
-
--
|
|
17
|
+
--supaslidev-accent: var(--color-primary-500);
|
|
18
|
+
--supaslidev-success: var(--color-green-500);
|
|
19
19
|
--supaslidev-border: #4b5563;
|
|
20
20
|
--supaslidev-header-bg: rgba(255, 255, 255, 0.03);
|
|
21
21
|
--supaslidev-text-muted: #9ca3af;
|
|
@@ -37,7 +37,7 @@ summary {
|
|
|
37
37
|
|
|
38
38
|
.settings-btn:hover .settings-icon {
|
|
39
39
|
transform: rotate(45deg);
|
|
40
|
-
color: var(--
|
|
40
|
+
color: var(--supaslidev-accent);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
*,
|
|
@@ -305,7 +305,7 @@ defineExpose({ focusInput, inputRef });
|
|
|
305
305
|
.logo-cursor {
|
|
306
306
|
width: 8px;
|
|
307
307
|
height: 1.25rem;
|
|
308
|
-
background: var(--
|
|
308
|
+
background: var(--supaslidev-accent);
|
|
309
309
|
animation: blink 1s step-end infinite;
|
|
310
310
|
}
|
|
311
311
|
|
|
@@ -322,7 +322,7 @@ defineExpose({ focusInput, inputRef });
|
|
|
322
322
|
outline: none;
|
|
323
323
|
color: var(--ui-text);
|
|
324
324
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
325
|
-
caret-color: var(--
|
|
325
|
+
caret-color: var(--supaslidev-accent);
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
.terminal-input::placeholder {
|
|
@@ -399,7 +399,7 @@ defineExpose({ focusInput, inputRef });
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
.dropdown-item--selected {
|
|
402
|
-
color: var(--
|
|
402
|
+
color: var(--supaslidev-accent);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
.dropdown-item-label {
|
|
@@ -6,6 +6,7 @@ const props = defineProps<{
|
|
|
6
6
|
}>();
|
|
7
7
|
|
|
8
8
|
const { deployMode, showDeployDemoToast } = useDeployMode();
|
|
9
|
+
const toast = useToast();
|
|
9
10
|
const deployBasePath = computed(() => (import.meta.env.BASE_URL || '/').replace(/\/$/, ''));
|
|
10
11
|
|
|
11
12
|
const {
|
|
@@ -14,6 +15,7 @@ const {
|
|
|
14
15
|
startServer,
|
|
15
16
|
stopServer,
|
|
16
17
|
exportPresentation,
|
|
18
|
+
generateThumbnail,
|
|
17
19
|
openInEditor,
|
|
18
20
|
waitForServerReady,
|
|
19
21
|
} = useServers();
|
|
@@ -26,6 +28,7 @@ const emit = defineEmits<{
|
|
|
26
28
|
const loading = ref({
|
|
27
29
|
dev: false,
|
|
28
30
|
export: false,
|
|
31
|
+
thumbnail: false,
|
|
29
32
|
edit: false,
|
|
30
33
|
});
|
|
31
34
|
|
|
@@ -87,6 +90,43 @@ async function handleExport(event: Event) {
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
async function handleThumbnail(event: Event) {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
event.stopPropagation();
|
|
96
|
+
|
|
97
|
+
if (deployMode.value) {
|
|
98
|
+
showDeployDemoToast();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (loading.value.thumbnail) return;
|
|
103
|
+
|
|
104
|
+
loading.value.thumbnail = true;
|
|
105
|
+
try {
|
|
106
|
+
const result = await generateThumbnail(props.presentation.id);
|
|
107
|
+
if (result.success && result.thumbnailPath) {
|
|
108
|
+
toast.add({
|
|
109
|
+
title: 'Thumbnail ready',
|
|
110
|
+
description: `${props.presentation.title} thumbnail generated`,
|
|
111
|
+
color: 'success',
|
|
112
|
+
icon: 'i-lucide-image',
|
|
113
|
+
actions: [
|
|
114
|
+
{
|
|
115
|
+
label: 'Open',
|
|
116
|
+
onClick: () => window.open(result.thumbnailPath, '_blank'),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
emit('exportError', result.error || 'Thumbnail generation failed');
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
emit('exportError', 'Failed to generate thumbnail');
|
|
125
|
+
} finally {
|
|
126
|
+
loading.value.thumbnail = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
90
130
|
async function handleEdit(event: Event) {
|
|
91
131
|
event.preventDefault();
|
|
92
132
|
event.stopPropagation();
|
|
@@ -166,6 +206,17 @@ function handleCardClick() {
|
|
|
166
206
|
</div>
|
|
167
207
|
</template>
|
|
168
208
|
|
|
209
|
+
<div
|
|
210
|
+
v-if="presentation.thumbnail"
|
|
211
|
+
class="thumbnail-preview border-b border-[var(--supaslidev-border)]"
|
|
212
|
+
>
|
|
213
|
+
<img
|
|
214
|
+
:src="presentation.thumbnail"
|
|
215
|
+
:alt="`${presentation.title} first slide`"
|
|
216
|
+
class="w-full h-auto block"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
169
220
|
<div class="terminal-body p-5 space-y-5">
|
|
170
221
|
<div class="terminal-prompt">
|
|
171
222
|
<div class="flex items-start gap-2">
|
|
@@ -234,6 +285,22 @@ function handleCardClick() {
|
|
|
234
285
|
export
|
|
235
286
|
</UButton>
|
|
236
287
|
|
|
288
|
+
<UButton
|
|
289
|
+
color="info"
|
|
290
|
+
variant="soft"
|
|
291
|
+
size="sm"
|
|
292
|
+
class="flex-1 terminal-btn font-mono"
|
|
293
|
+
:loading="loading.thumbnail"
|
|
294
|
+
:disabled="loading.thumbnail"
|
|
295
|
+
loading-icon="i-lucide-loader-circle"
|
|
296
|
+
@click="handleThumbnail"
|
|
297
|
+
>
|
|
298
|
+
<template v-if="!loading.thumbnail" #leading>
|
|
299
|
+
<span class="terminal-prompt-symbol">$</span>
|
|
300
|
+
</template>
|
|
301
|
+
thumbnail
|
|
302
|
+
</UButton>
|
|
303
|
+
|
|
237
304
|
<UButton
|
|
238
305
|
color="neutral"
|
|
239
306
|
variant="soft"
|
|
@@ -6,6 +6,7 @@ const props = defineProps<{
|
|
|
6
6
|
}>();
|
|
7
7
|
|
|
8
8
|
const { deployMode, showDeployDemoToast } = useDeployMode();
|
|
9
|
+
const toast = useToast();
|
|
9
10
|
const deployBasePath = computed(() => (import.meta.env.BASE_URL || '/').replace(/\/$/, ''));
|
|
10
11
|
|
|
11
12
|
const {
|
|
@@ -14,6 +15,7 @@ const {
|
|
|
14
15
|
startServer,
|
|
15
16
|
stopServer,
|
|
16
17
|
exportPresentation,
|
|
18
|
+
generateThumbnail,
|
|
17
19
|
openInEditor,
|
|
18
20
|
waitForServerReady,
|
|
19
21
|
} = useServers();
|
|
@@ -26,6 +28,7 @@ const emit = defineEmits<{
|
|
|
26
28
|
const loading = ref({
|
|
27
29
|
dev: false,
|
|
28
30
|
export: false,
|
|
31
|
+
thumbnail: false,
|
|
29
32
|
edit: false,
|
|
30
33
|
});
|
|
31
34
|
|
|
@@ -87,6 +90,43 @@ async function handleExport(event: Event) {
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
async function handleThumbnail(event: Event) {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
event.stopPropagation();
|
|
96
|
+
|
|
97
|
+
if (deployMode.value) {
|
|
98
|
+
showDeployDemoToast();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (loading.value.thumbnail) return;
|
|
103
|
+
|
|
104
|
+
loading.value.thumbnail = true;
|
|
105
|
+
try {
|
|
106
|
+
const result = await generateThumbnail(props.presentation.id);
|
|
107
|
+
if (result.success && result.thumbnailPath) {
|
|
108
|
+
toast.add({
|
|
109
|
+
title: 'Thumbnail ready',
|
|
110
|
+
description: `${props.presentation.title} thumbnail generated`,
|
|
111
|
+
color: 'success',
|
|
112
|
+
icon: 'i-lucide-image',
|
|
113
|
+
actions: [
|
|
114
|
+
{
|
|
115
|
+
label: 'Open',
|
|
116
|
+
onClick: () => window.open(result.thumbnailPath, '_blank'),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
emit('exportError', result.error || 'Thumbnail generation failed');
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
emit('exportError', 'Failed to generate thumbnail');
|
|
125
|
+
} finally {
|
|
126
|
+
loading.value.thumbnail = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
90
130
|
async function handleEdit(event: Event) {
|
|
91
131
|
event.preventDefault();
|
|
92
132
|
event.stopPropagation();
|
|
@@ -144,6 +184,13 @@ function handleOpen(event: Event) {
|
|
|
144
184
|
:class="running ? 'bg-[var(--ui-success)] animate-pulse' : 'bg-[var(--ui-text-muted)]'"
|
|
145
185
|
/>
|
|
146
186
|
|
|
187
|
+
<img
|
|
188
|
+
v-if="presentation.thumbnail"
|
|
189
|
+
:src="presentation.thumbnail"
|
|
190
|
+
:alt="`${presentation.title} first slide`"
|
|
191
|
+
class="w-10 h-6 object-cover rounded shrink-0 border border-[var(--supaslidev-border)]"
|
|
192
|
+
/>
|
|
193
|
+
|
|
147
194
|
<span class="text-xs text-[var(--ui-text-muted)] shrink-0">~/{{ presentation.id }}</span>
|
|
148
195
|
|
|
149
196
|
<span class="text-sm text-[var(--ui-text)] truncate min-w-0 flex-1">
|
|
@@ -217,6 +264,19 @@ function handleOpen(event: Event) {
|
|
|
217
264
|
@click="handleExport"
|
|
218
265
|
/>
|
|
219
266
|
|
|
267
|
+
<UButton
|
|
268
|
+
color="info"
|
|
269
|
+
variant="ghost"
|
|
270
|
+
size="xs"
|
|
271
|
+
:icon="loading.thumbnail ? '' : 'i-lucide-image'"
|
|
272
|
+
:loading="loading.thumbnail"
|
|
273
|
+
:disabled="loading.thumbnail"
|
|
274
|
+
loading-icon="i-lucide-loader-circle"
|
|
275
|
+
class="action-btn"
|
|
276
|
+
title="Generate thumbnail"
|
|
277
|
+
@click="handleThumbnail"
|
|
278
|
+
/>
|
|
279
|
+
|
|
220
280
|
<UButton
|
|
221
281
|
color="neutral"
|
|
222
282
|
variant="ghost"
|
|
@@ -78,6 +78,18 @@ async function exportPresentation(
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
async function generateThumbnail(
|
|
82
|
+
presentationId: string,
|
|
83
|
+
): Promise<{ success: boolean; thumbnailPath?: string; error?: string }> {
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(`/api/thumbnail/${presentationId}`, { method: 'POST' });
|
|
86
|
+
const result = await response.json();
|
|
87
|
+
return result;
|
|
88
|
+
} catch {
|
|
89
|
+
return { success: false, error: 'Failed to connect to thumbnail service' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
81
93
|
async function openInEditor(presentationId: string): Promise<{ success: boolean; error?: string }> {
|
|
82
94
|
try {
|
|
83
95
|
const response = await fetch(`/api/open-editor/${presentationId}`, { method: 'POST' });
|
|
@@ -140,6 +152,7 @@ export function useServers() {
|
|
|
140
152
|
startPolling,
|
|
141
153
|
stopPolling,
|
|
142
154
|
exportPresentation,
|
|
155
|
+
generateThumbnail,
|
|
143
156
|
openInEditor,
|
|
144
157
|
waitForServerReady,
|
|
145
158
|
};
|
package/app/layouts/default.vue
CHANGED
package/app/pages/index.vue
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
stopAllServers,
|
|
9
9
|
startServer,
|
|
10
10
|
exportPresentation,
|
|
11
|
+
generateThumbnail,
|
|
11
12
|
openInEditor,
|
|
12
13
|
waitForServerReady,
|
|
13
14
|
} = useServers();
|
|
@@ -169,6 +170,42 @@ async function handleExportCommand(presentation: Presentation) {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
async function handleThumbnailCommand(presentation: Presentation) {
|
|
174
|
+
isCommandPaletteOpen.value = false;
|
|
175
|
+
if (deployMode.value) {
|
|
176
|
+
showDeployDemoToast();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
toast.add({
|
|
180
|
+
title: 'Generating thumbnail...',
|
|
181
|
+
description: `Creating thumbnail for ${presentation.title}`,
|
|
182
|
+
color: 'info',
|
|
183
|
+
icon: 'i-lucide-image',
|
|
184
|
+
});
|
|
185
|
+
const result = await generateThumbnail(presentation.id);
|
|
186
|
+
if (result.success && result.thumbnailPath) {
|
|
187
|
+
toast.add({
|
|
188
|
+
title: 'Thumbnail ready',
|
|
189
|
+
description: `${presentation.title} thumbnail generated`,
|
|
190
|
+
color: 'success',
|
|
191
|
+
icon: 'i-lucide-image',
|
|
192
|
+
actions: [
|
|
193
|
+
{
|
|
194
|
+
label: 'Open',
|
|
195
|
+
onClick: () => window.open(result.thumbnailPath, '_blank'),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
});
|
|
199
|
+
} else if (result.error) {
|
|
200
|
+
toast.add({
|
|
201
|
+
title: 'Thumbnail Failed',
|
|
202
|
+
description: result.error,
|
|
203
|
+
color: 'error',
|
|
204
|
+
icon: 'i-lucide-alert-circle',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
172
209
|
async function handleEditCommand(presentation: Presentation) {
|
|
173
210
|
isCommandPaletteOpen.value = false;
|
|
174
211
|
if (deployMode.value) {
|
|
@@ -284,6 +321,30 @@ function handleExecuteCommand(command: string) {
|
|
|
284
321
|
return;
|
|
285
322
|
}
|
|
286
323
|
|
|
324
|
+
if (action === 'thumbnail') {
|
|
325
|
+
if (!arg) {
|
|
326
|
+
toast.add({
|
|
327
|
+
title: 'Missing argument',
|
|
328
|
+
description: 'Usage: thumbnail <presentation-name>',
|
|
329
|
+
color: 'warning',
|
|
330
|
+
icon: 'i-lucide-alert-triangle',
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const presentation = findPresentationByName(arg);
|
|
335
|
+
if (!presentation) {
|
|
336
|
+
toast.add({
|
|
337
|
+
title: 'Presentation not found',
|
|
338
|
+
description: `No presentation found with name "${arg}"`,
|
|
339
|
+
color: 'warning',
|
|
340
|
+
icon: 'i-lucide-alert-triangle',
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
handleThumbnailCommand(presentation);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
287
348
|
if (action === 'edit') {
|
|
288
349
|
if (!arg) {
|
|
289
350
|
toast.add({
|
|
@@ -310,7 +371,7 @@ function handleExecuteCommand(command: string) {
|
|
|
310
371
|
|
|
311
372
|
toast.add({
|
|
312
373
|
title: 'Unknown command',
|
|
313
|
-
description: `"${action}" is not a recognized command. Try: new, import, present, export, edit`,
|
|
374
|
+
description: `"${action}" is not a recognized command. Try: new, import, present, export, thumbnail, edit`,
|
|
314
375
|
color: 'warning',
|
|
315
376
|
icon: 'i-lucide-alert-triangle',
|
|
316
377
|
});
|
|
@@ -370,6 +431,18 @@ const commandPaletteGroups = computed<CommandPaletteGroup[]>(() => {
|
|
|
370
431
|
}),
|
|
371
432
|
),
|
|
372
433
|
},
|
|
434
|
+
{
|
|
435
|
+
id: 'thumbnail',
|
|
436
|
+
label: 'Thumbnail',
|
|
437
|
+
items: presentations.value.map(
|
|
438
|
+
(p: Presentation): CommandPaletteItem => ({
|
|
439
|
+
label: `Thumbnail > ${p.title}`,
|
|
440
|
+
suffix: 'Generate PNG of first slide',
|
|
441
|
+
icon: 'i-lucide-image',
|
|
442
|
+
onSelect: () => handleThumbnailCommand(p),
|
|
443
|
+
}),
|
|
444
|
+
),
|
|
445
|
+
},
|
|
373
446
|
{
|
|
374
447
|
id: 'edit',
|
|
375
448
|
label: 'Edit',
|
|
@@ -424,6 +497,11 @@ const commandOptions = computed(() => {
|
|
|
424
497
|
description: 'Export to PDF',
|
|
425
498
|
onSelect: () => handleExportCommand(p),
|
|
426
499
|
});
|
|
500
|
+
options.push({
|
|
501
|
+
label: `Thumbnail > ${p.title}`,
|
|
502
|
+
description: 'Generate PNG of first slide',
|
|
503
|
+
onSelect: () => handleThumbnailCommand(p),
|
|
504
|
+
});
|
|
427
505
|
options.push({
|
|
428
506
|
label: `Edit > ${p.title}`,
|
|
429
507
|
description: 'Open in VS Code',
|