@slidev/client 0.50.0-beta.8 → 0.50.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 (44) hide show
  1. package/composables/useClicks.ts +6 -5
  2. package/composables/useDragElements.ts +30 -24
  3. package/composables/useNav.ts +17 -15
  4. package/composables/usePrintStyles.ts +28 -0
  5. package/constants.ts +1 -0
  6. package/internals/ClicksSlider.vue +1 -1
  7. package/internals/ContextMenu.vue +1 -1
  8. package/internals/DragControl.vue +1 -1
  9. package/internals/DrawingControls.vue +1 -1
  10. package/internals/ExportPdfTip.vue +90 -0
  11. package/internals/FormCheckbox.vue +16 -0
  12. package/internals/FormItem.vue +41 -0
  13. package/internals/IconButton.vue +7 -2
  14. package/internals/MenuButton.vue +1 -1
  15. package/internals/Modal.vue +1 -1
  16. package/internals/NavControls.vue +10 -4
  17. package/internals/PrintContainer.vue +2 -21
  18. package/internals/PrintSlide.vue +4 -3
  19. package/internals/PrintSlideClick.vue +11 -3
  20. package/internals/QuickOverview.vue +2 -2
  21. package/internals/Settings.vue +5 -2
  22. package/internals/SideEditor.vue +1 -1
  23. package/internals/SlidesShow.vue +7 -3
  24. package/internals/WebCamera.vue +2 -2
  25. package/layouts/error.vue +5 -1
  26. package/logic/dark.ts +11 -0
  27. package/logic/screenshot.ts +61 -0
  28. package/logic/shortcuts.ts +36 -35
  29. package/logic/slides.ts +2 -1
  30. package/main.ts +7 -3
  31. package/modules/v-mark.ts +6 -0
  32. package/package.json +18 -16
  33. package/pages/export.vue +369 -0
  34. package/pages/overview.vue +1 -1
  35. package/pages/play.vue +1 -4
  36. package/pages/presenter.vue +9 -0
  37. package/pages/print.vue +0 -2
  38. package/setup/monaco.ts +14 -14
  39. package/setup/root.ts +6 -2
  40. package/setup/routes.ts +23 -12
  41. package/state/index.ts +2 -1
  42. package/styles/index.css +9 -4
  43. package/uno.config.ts +14 -0
  44. package/internals/PrintStyle.vue +0 -16
@@ -0,0 +1,369 @@
1
+ <script setup lang="ts">
2
+ import type { ScreenshotSession } from '../logic/screenshot'
3
+ import { sleep } from '@antfu/utils'
4
+ import { parseRangeString } from '@slidev/parser/utils'
5
+ import { useHead } from '@unhead/vue'
6
+ import { provideLocal, useElementSize, useLocalStorage, useStyleTag, watchDebounced } from '@vueuse/core'
7
+
8
+ import { computed, ref, useTemplateRef, watch } from 'vue'
9
+ import { useRouter } from 'vue-router'
10
+ import { useDarkMode } from '../composables/useDarkMode'
11
+ import { useNav } from '../composables/useNav'
12
+ import { patchMonacoColors } from '../composables/usePrintStyles'
13
+ import { injectionSlideScale } from '../constants'
14
+ import { configs, slideHeight, slidesTitle, slideWidth } from '../env'
15
+ import ExportPdfTip from '../internals/ExportPdfTip.vue'
16
+ import FormCheckbox from '../internals/FormCheckbox.vue'
17
+ import FormItem from '../internals/FormItem.vue'
18
+ import PrintSlide from '../internals/PrintSlide.vue'
19
+ import { isScreenshotSupported, startScreenshotSession } from '../logic/screenshot'
20
+ import { skipExportPdfTip } from '../state'
21
+ import Play from './play.vue'
22
+
23
+ const { slides, isPrintWithClicks, hasNext, go, next, currentSlideNo, clicks, printRange } = useNav()
24
+ const router = useRouter()
25
+ const { isColorSchemaConfigured, isDark } = useDarkMode()
26
+ const { width: containerWidth } = useElementSize(useTemplateRef('export-container'))
27
+ const { height: contentHeight } = useElementSize(useTemplateRef('export-content'))
28
+ const scale = computed(() => containerWidth.value / slideWidth.value)
29
+ const contentMarginBottom = computed(() => `${contentHeight.value * (scale.value - 1)}px`)
30
+ const rangesRaw = ref('')
31
+ const initialWait = ref(1000)
32
+ const delay = useLocalStorage('slidev-export-capture-delay', 400, { listenToStorageChanges: false })
33
+ type ScreenshotResult = { slideIndex: number, clickIndex: number, dataUrl: string }[]
34
+ const screenshotSession = ref<ScreenshotSession | null>(null)
35
+ const capturedImages = ref<ScreenshotResult | null>(null)
36
+ const title = ref(configs.exportFilename || slidesTitle)
37
+
38
+ useHead({
39
+ title,
40
+ })
41
+
42
+ provideLocal(injectionSlideScale, scale)
43
+
44
+ const showExportPdfTip = ref(false)
45
+ function pdf() {
46
+ if (skipExportPdfTip.value) {
47
+ doPrint()
48
+ }
49
+ else {
50
+ showExportPdfTip.value = true
51
+ }
52
+ }
53
+
54
+ function doPrint() {
55
+ patchMonacoColors()
56
+ setTimeout(window.print, 100)
57
+ }
58
+
59
+ async function capturePngs() {
60
+ if (screenshotSession.value) {
61
+ screenshotSession.value.dispose()
62
+ screenshotSession.value = null
63
+ }
64
+ if (capturedImages.value)
65
+ return capturedImages.value
66
+ try {
67
+ const scale = 2
68
+ screenshotSession.value = await startScreenshotSession(slideWidth.value * scale, slideHeight.value * scale)
69
+ const result: ScreenshotResult = []
70
+
71
+ go(1, 0, true)
72
+
73
+ await sleep(initialWait.value + delay.value)
74
+ while (true) {
75
+ if (!screenshotSession.value) {
76
+ break
77
+ }
78
+ result.push({
79
+ slideIndex: currentSlideNo.value - 1,
80
+ clickIndex: clicks.value,
81
+ dataUrl: screenshotSession.value.screenshot(document.getElementById('slide-content')!),
82
+ })
83
+ if (hasNext.value) {
84
+ await sleep(delay.value)
85
+ next()
86
+ await sleep(delay.value)
87
+ }
88
+ else {
89
+ break
90
+ }
91
+ }
92
+
93
+ if (screenshotSession.value) {
94
+ screenshotSession.value.dispose()
95
+ capturedImages.value = result
96
+ screenshotSession.value = null
97
+ }
98
+ }
99
+ catch (e) {
100
+ console.error(e)
101
+ capturedImages.value = null
102
+ }
103
+ finally {
104
+ router.push('/export')
105
+ }
106
+ return capturedImages.value
107
+ }
108
+
109
+ async function pptx() {
110
+ const pngs = await capturePngs()
111
+ if (!pngs)
112
+ return
113
+ const pptx = await import('pptxgenjs')
114
+ .then(r => r.default)
115
+ .then(PptxGen => new PptxGen())
116
+
117
+ const layoutName = `${slideWidth.value}x${slideHeight.value}`
118
+ pptx.defineLayout({
119
+ name: layoutName,
120
+ width: slideWidth.value / 96,
121
+ height: slideHeight.value / 96,
122
+ })
123
+ pptx.layout = layoutName
124
+ if (configs.author)
125
+ pptx.author = configs.author
126
+ pptx.company = 'Created using Slidev'
127
+ pptx.title = title.value
128
+ if (typeof configs.info === 'string')
129
+ pptx.subject = configs.info
130
+
131
+ pngs.forEach(({ slideIndex, dataUrl }) => {
132
+ const slide = pptx.addSlide()
133
+ slide.background = {
134
+ data: dataUrl,
135
+ }
136
+
137
+ const note = slides.value[slideIndex].meta.slide.note
138
+ if (note)
139
+ slide.addNotes(note)
140
+ })
141
+
142
+ const blob = await pptx.write({
143
+ outputType: 'blob',
144
+ compression: true,
145
+ }) as Blob
146
+ const url = URL.createObjectURL(blob)
147
+ const a = document.createElement('a')
148
+ a.href = url
149
+ a.download = `${title.value}.pptx`
150
+ a.click()
151
+ }
152
+
153
+ async function pngsGz() {
154
+ const pngs = await capturePngs()
155
+ if (!pngs)
156
+ return
157
+ const { createTarGzip } = await import('nanotar')
158
+ const data = await createTarGzip(
159
+ pngs.map(({ slideIndex, dataUrl }) => ({
160
+ name: `${slideIndex}.png`,
161
+ data: new Uint8Array(atob(dataUrl.split(',')[1]).split('').map(char => char.charCodeAt(0))),
162
+ })),
163
+ )
164
+ const a = document.createElement('a')
165
+ const blob = new Blob([data], { type: 'application/gzip' })
166
+ a.href = URL.createObjectURL(blob)
167
+ a.download = `${title.value}.tar.gz`
168
+ a.click()
169
+ }
170
+
171
+ useStyleTag(computed(() => screenshotSession.value?.isActive
172
+ ? `
173
+ html {
174
+ cursor: none;
175
+ margin-bottom: 20px;
176
+ }
177
+ body {
178
+ pointer-events: none;
179
+ }`
180
+ : `
181
+ :root {
182
+ --slidev-slide-scale: ${scale.value};
183
+ }
184
+ `))
185
+
186
+ // clear captured images when settings changed
187
+ watch(
188
+ [
189
+ isDark,
190
+ printRange,
191
+ isPrintWithClicks,
192
+ ],
193
+ () => capturedImages.value = null,
194
+ )
195
+
196
+ watchDebounced(
197
+ [slides, rangesRaw],
198
+ () => printRange.value = parseRangeString(slides.value.length, rangesRaw.value),
199
+ { debounce: 300 },
200
+ )
201
+
202
+ // clear captured images when HMR
203
+ if (import.meta.hot) {
204
+ import.meta.hot.on('vite:beforeUpdate', () => {
205
+ capturedImages.value = null
206
+ })
207
+ }
208
+ </script>
209
+
210
+ <template>
211
+ <Play v-if="screenshotSession?.isActive" />
212
+ <div
213
+ v-else
214
+ class="fixed inset-0 flex flex-col md:flex-row md:gap-8 print:position-unset print:inset-0 print:block print:min-h-max justify-center of-hidden bg-main"
215
+ >
216
+ <div class="print:hidden min-w-fit flex flex-wrap md:flex-nowrap md:of-y-auto md:flex-col gap-2 p-6 max-w-100">
217
+ <h1 class="text-3xl md:my-4 flex items-center gap-2 w-full">
218
+ <RouterLink to="/" class="i-carbon:previous-outline op-70 hover:op-100" />
219
+ Browser Exporter
220
+ <sup op50 italic text-sm>Experimental</sup>
221
+ </h1>
222
+ <div flex="~ col gap-2">
223
+ <h2>Options</h2>
224
+ <FormItem title="Title">
225
+ <input v-model="title" type="text">
226
+ </FormItem>
227
+ <FormItem title="Range">
228
+ <input v-model="rangesRaw" type="text" :placeholder="`1-${slides.length}`">
229
+ </FormItem>
230
+ <FormItem title="Dark mode">
231
+ <FormCheckbox v-model="isDark" :disabled="isColorSchemaConfigured" />
232
+ </FormItem>
233
+ <FormItem title="With clicks">
234
+ <FormCheckbox v-model="isPrintWithClicks" />
235
+ </FormItem>
236
+ </div>
237
+ <div class="flex-grow" />
238
+ <div class="min-w-fit" flex="~ col gap-3">
239
+ <div border="~ main rounded-lg" p3 flex="~ col gap-2">
240
+ <h2>Export as Vector File</h2>
241
+ <div class="flex flex-col gap-2 items-start min-w-max">
242
+ <button @click="pdf">
243
+ PDF
244
+ </button>
245
+ </div>
246
+ </div>
247
+
248
+ <div border="~ main rounded-lg" p3 flex="~ col gap-2" :class="isScreenshotSupported ? '' : 'border-orange'">
249
+ <h2>Export as Images</h2>
250
+ <div v-if="!isScreenshotSupported" class="min-w-full w-0 text-orange/100 p-1 mb--4 bg-orange/10 rounded">
251
+ <span class="i-carbon:warning-alt inline-block mb--.5" />
252
+ Your browser may not support image capturing.
253
+ If you encounter issues, please use a modern Chromium-based browser,
254
+ or export via the CLI.
255
+ </div>
256
+ <div class="flex flex-col gap-2 items-start min-w-max">
257
+ <button @click="pptx">
258
+ PPTX
259
+ </button>
260
+ <button @click="pngsGz">
261
+ PNGs.gz
262
+ </button>
263
+ </div>
264
+ <div w-full h-1px border="t main" my2 />
265
+ <div class="relative flex flex-col gap-2 flex-nowrap">
266
+ <div class="flex flex-col gap-2 items-start min-w-max">
267
+ <button v-if="capturedImages" class="flex justify-center items-center gap-2" @click="capturedImages = null">
268
+ <span class="i-carbon:trash-can inline-block text-xl" />
269
+ Clear Captured Images
270
+ </button>
271
+ <button v-else class="flex justify-center items-center gap-2" @click="capturePngs">
272
+ <div class="i-carbon:camera-action inline-block text-xl" />
273
+ Pre-capture Slides as Images
274
+ </button>
275
+ <FormItem title="Delay" description="Delay between capturing each slide in milliseconds.<br>Increase this value if slides are captured incompletely. <br>(Not related to PDF export)">
276
+ <input v-model="delay" type="number" step="50" min="50">
277
+ </FormItem>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ <div id="export-container" ref="export-container" relative>
284
+ <div print:hidden fixed right-5 bottom-5 px2 py0 z-label slidev-glass-effect>
285
+ <span op75>Rendering as {{ capturedImages ? 'Captured Images' : 'DOM' }} </span>
286
+ </div>
287
+ <div v-show="!capturedImages" id="export-content" ref="export-content">
288
+ <PrintSlide v-for="route, index in slides" :key="index" :hidden="!printRange.includes(index + 1)" :route />
289
+ </div>
290
+ <div v-if="capturedImages" id="export-content-images" class="print:hidden grid">
291
+ <div v-for="png, i of capturedImages" :key="i" class="print-slide-container">
292
+ <img :src="png.dataUrl">
293
+ </div>
294
+ </div>
295
+ </div>
296
+ <div id="twoslash-container" />
297
+ <ExportPdfTip v-model="showExportPdfTip" @print="doPrint" />
298
+ </div>
299
+ </template>
300
+
301
+ <style scoped>
302
+ @media not print {
303
+ #export-container {
304
+ scrollbar-width: thin;
305
+ scroll-behavior: smooth;
306
+ --uno: w-full overflow-x-hidden overflow-y-auto max-h-full max-w-300 p-6;
307
+ }
308
+
309
+ #export-content {
310
+ transform: v-bind('`scale(${scale})`');
311
+ margin-bottom: v-bind('contentMarginBottom');
312
+ --uno: origin-tl;
313
+ }
314
+
315
+ #export-content,
316
+ #export-content-images {
317
+ --uno: flex flex-col gap-2;
318
+ }
319
+ }
320
+
321
+ @media print {
322
+ #export-content {
323
+ transform: scale(1);
324
+ display: block !important;
325
+ }
326
+ }
327
+
328
+ button {
329
+ --uno: 'w-full rounded bg-gray:10 px-4 py-2 hover:bg-gray/20';
330
+ }
331
+
332
+ label {
333
+ --uno: text-xl flex gap-2 items-center select-none;
334
+
335
+ span {
336
+ --uno: flex-grow;
337
+ }
338
+
339
+ input[type='text'],
340
+ input[type='number'] {
341
+ --uno: border border-main rounded px-2 py-1;
342
+ }
343
+ }
344
+
345
+ h2 {
346
+ --uno: font-500 op-70;
347
+ }
348
+
349
+ #export-content {
350
+ --uno: pointer-events-none;
351
+ }
352
+ </style>
353
+
354
+ <style>
355
+ @media print {
356
+ html,
357
+ body,
358
+ #app {
359
+ overflow: unset !important;
360
+ }
361
+ }
362
+
363
+ @media not print {
364
+ #export-content-images .print-slide-container,
365
+ #export-content .print-slide-container {
366
+ --uno: border border-main rounded-md shadow of-hidden;
367
+ }
368
+ }
369
+ </style>
@@ -135,7 +135,7 @@ onMounted(() => {
135
135
  </button>
136
136
  <div
137
137
  v-if="route.meta?.slide?.title"
138
- class="pointer-events-none select-none absolute left-110% backdrop-blur-8 top-50% translate-y--50% ws-nowrap z-10 px2 shadow-xl rounded border border-main transition duration-400 op0 group-hover:op100"
138
+ class="pointer-events-none select-none absolute left-110% top-50% translate-y--50% ws-nowrap z-label px2 slidev-glass-effect transition duration-400 op0 group-hover:op100"
139
139
  :class="activeBlocks.includes(idx) ? 'text-primary' : 'text-main important-text-op-50'"
140
140
  >
141
141
  {{ route.meta?.slide?.title }}
package/pages/play.vue CHANGED
@@ -8,12 +8,11 @@ import { useWakeLock } from '../composables/useWakeLock'
8
8
  import Controls from '../internals/Controls.vue'
9
9
  import NavControls from '../internals/NavControls.vue'
10
10
  import PresenterMouse from '../internals/PresenterMouse.vue'
11
- import PrintStyle from '../internals/PrintStyle.vue'
12
11
  import SlideContainer from '../internals/SlideContainer.vue'
13
12
  import SlidesShow from '../internals/SlidesShow.vue'
14
13
  import { onContextMenu } from '../logic/contextMenu'
15
14
  import { registerShortcuts } from '../logic/shortcuts'
16
- import { editorHeight, editorWidth, isEditorVertical, isScreenVertical, showEditor, windowSize } from '../state'
15
+ import { editorHeight, editorWidth, isEditorVertical, isScreenVertical, showEditor } from '../state'
17
16
 
18
17
  const { next, prev, isPrintMode } = useNav()
19
18
  const { isDrawing } = useDrawings()
@@ -63,14 +62,12 @@ if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
63
62
  </script>
64
63
 
65
64
  <template>
66
- <PrintStyle v-if="isPrintMode" />
67
65
  <div
68
66
  id="page-root" ref="root" class="grid"
69
67
  :class="isEditorVertical ? 'grid-rows-[1fr_max-content]' : 'grid-cols-[1fr_max-content]'"
70
68
  >
71
69
  <SlideContainer
72
70
  :style="{ background: 'var(--slidev-slide-container-background, black)' }"
73
- :width="isPrintMode ? windowSize.width.value : undefined"
74
71
  is-main
75
72
  @pointerdown="onClick"
76
73
  @contextmenu="onContextMenu"
@@ -247,6 +247,15 @@ onMounted(() => {
247
247
  'bottom bottom';
248
248
  }
249
249
 
250
+ .grid-container.layout3 {
251
+ grid-template-columns: 2fr 3fr;
252
+ grid-template-rows: 1fr 1fr min-content;
253
+ grid-template-areas:
254
+ 'note next'
255
+ 'main next'
256
+ 'bottom bottom';
257
+ }
258
+
250
259
  @media (max-aspect-ratio: 3/5) {
251
260
  .grid-container.layout1 {
252
261
  grid-template-columns: 1fr;
package/pages/print.vue CHANGED
@@ -3,7 +3,6 @@ import { recomputeAllPoppers } from 'floating-vue'
3
3
  import { onMounted, watchEffect } from 'vue'
4
4
  import { useNav } from '../composables/useNav'
5
5
  import PrintContainer from '../internals/PrintContainer.vue'
6
- import PrintStyle from '../internals/PrintStyle.vue'
7
6
  import { windowSize } from '../state'
8
7
 
9
8
  const { isPrintMode } = useNav()
@@ -21,7 +20,6 @@ onMounted(() => {
21
20
  </script>
22
21
 
23
22
  <template>
24
- <PrintStyle v-if="isPrintMode" />
25
23
  <div id="page-root" class="grid grid-cols-[1fr_max-content]">
26
24
  <PrintContainer
27
25
  class="w-full h-full"
package/setup/monaco.ts CHANGED
@@ -64,22 +64,22 @@ const setup = createSingletonPromise(async () => {
64
64
 
65
65
  const ata = configs.monacoTypesSource === 'cdn'
66
66
  ? setupTypeAcquisition({
67
- projectName: 'TypeScript Playground',
68
- typescript: ts as any, // Version mismatch. No problem found so far.
69
- logger: console,
70
- delegate: {
71
- receivedFile: (code: string, path: string) => {
72
- defaults.addExtraLib(code, `file://${path}`)
73
- const uri = monaco.Uri.file(path)
74
- if (monaco.editor.getModel(uri) === null)
75
- monaco.editor.createModel(code, 'javascript', uri)
76
- },
77
- progress: (downloaded: number, total: number) => {
67
+ projectName: 'TypeScript Playground',
68
+ typescript: ts as any, // Version mismatch. No problem found so far.
69
+ logger: console,
70
+ delegate: {
71
+ receivedFile: (code: string, path: string) => {
72
+ defaults.addExtraLib(code, `file://${path}`)
73
+ const uri = monaco.Uri.file(path)
74
+ if (monaco.editor.getModel(uri) === null)
75
+ monaco.editor.createModel(code, 'javascript', uri)
76
+ },
77
+ progress: (downloaded: number, total: number) => {
78
78
  // eslint-disable-next-line no-console
79
- console.debug(`[Typescript ATA] ${downloaded} / ${total}`)
79
+ console.debug(`[Typescript ATA] ${downloaded} / ${total}`)
80
+ },
80
81
  },
81
- },
82
- })
82
+ })
83
83
  : () => { }
84
84
 
85
85
  monaco.languages.register({ id: 'vue' })
package/setup/root.ts CHANGED
@@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'
5
5
  import { createFixedClicks } from '../composables/useClicks'
6
6
  import { useEmbeddedControl } from '../composables/useEmbeddedCtrl'
7
7
  import { useNav } from '../composables/useNav'
8
+ import { usePrintStyles } from '../composables/usePrintStyles'
8
9
  import { injectionClicksContext, injectionCurrentPage, injectionRenderContext, injectionSlidevContext, TRUST_ORIGINS } from '../constants'
9
10
  import { configs, slidesTitle } from '../env'
10
11
  import { skipTransition } from '../logic/hmr'
@@ -43,6 +44,7 @@ export default function setupRoot() {
43
44
  hasPrimarySlide,
44
45
  isNotesViewer,
45
46
  isPresenter,
47
+ isPrintMode,
46
48
  } = useNav()
47
49
 
48
50
  useHead({
@@ -50,6 +52,8 @@ export default function setupRoot() {
50
52
  htmlAttrs: configs.htmlAttrs,
51
53
  })
52
54
 
55
+ usePrintStyles()
56
+
53
57
  initSharedState(`${slidesTitle} - shared`)
54
58
  initDrawingState(`${slidesTitle} - drawings`)
55
59
 
@@ -57,7 +61,7 @@ export default function setupRoot() {
57
61
 
58
62
  // update shared state
59
63
  function updateSharedState() {
60
- if (isNotesViewer.value)
64
+ if (isNotesViewer.value || isPrintMode.value)
61
65
  return
62
66
 
63
67
  // we allow Presenter mode, or Viewer mode from trusted origins to update the shared state
@@ -86,7 +90,7 @@ export default function setupRoot() {
86
90
  watch(clicksContext, updateSharedState)
87
91
 
88
92
  onPatch((state) => {
89
- if (!hasPrimarySlide.value)
93
+ if (!hasPrimarySlide.value || isPrintMode.value)
90
94
  return
91
95
  if (state.lastUpdate?.type === 'presenter' && (+state.page !== +currentSlideNo.value || +clicksContext.value.current !== +state.clicks)) {
92
96
  skipTransition.value = false
package/setup/routes.ts CHANGED
@@ -5,21 +5,21 @@ import setups from '#slidev/setups/routes'
5
5
  export default function setupRoutes() {
6
6
  const routes: RouteRecordRaw[] = []
7
7
 
8
- if (__SLIDEV_FEATURE_PRESENTER__) {
9
- function passwordGuard(to: RouteLocationNormalized) {
10
- if (!configs.remote || configs.remote === to.query.password)
8
+ function passwordGuard(to: RouteLocationNormalized) {
9
+ if (!configs.remote || configs.remote === to.query.password)
10
+ return true
11
+ if (configs.remote && to.query.password === undefined) {
12
+ // eslint-disable-next-line no-alert
13
+ const password = prompt('Enter password')
14
+ if (configs.remote === password)
11
15
  return true
12
- if (configs.remote && to.query.password === undefined) {
13
- // eslint-disable-next-line no-alert
14
- const password = prompt('Enter password')
15
- if (configs.remote === password)
16
- return true
17
- }
18
- if (to.params.no)
19
- return { path: `/${to.params.no}` }
20
- return { path: '' }
21
16
  }
17
+ if (to.params.no)
18
+ return { path: `/${to.params.no}` }
19
+ return { path: '' }
20
+ }
22
21
 
22
+ if (__SLIDEV_FEATURE_PRESENTER__) {
23
23
  routes.push(
24
24
  {
25
25
  name: 'entry',
@@ -64,6 +64,17 @@ export default function setupRoutes() {
64
64
  )
65
65
  }
66
66
 
67
+ if (__SLIDEV_FEATURE_BROWSER_EXPORTER__) {
68
+ routes.push(
69
+ {
70
+ name: 'export',
71
+ path: '/export/:no?',
72
+ component: () => import('../pages/export.vue'),
73
+ beforeEnter: passwordGuard,
74
+ },
75
+ )
76
+ }
77
+
67
78
  routes.push(
68
79
  {
69
80
  name: 'play',
package/state/index.ts CHANGED
@@ -26,6 +26,7 @@ export const currentCamera = useLocalStorage<string>('slidev-camera', 'default',
26
26
  export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
27
27
  export const slideScale = useLocalStorage<number>('slidev-scale', 0)
28
28
  export const wakeLockEnabled = useLocalStorage('slidev-wake-lock', true)
29
+ export const skipExportPdfTip = useLocalStorage('slidev-skip-export-pdf-tip', false)
29
30
 
30
31
  export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
31
32
  export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
@@ -40,7 +41,7 @@ export const presenterLayout = useLocalStorage('slidev-presenter-layout', 1, { l
40
41
 
41
42
  export function togglePresenterLayout() {
42
43
  presenterLayout.value = presenterLayout.value + 1
43
- if (presenterLayout.value > 2)
44
+ if (presenterLayout.value > 3)
44
45
  presenterLayout.value = 1
45
46
  }
46
47
 
package/styles/index.css CHANGED
@@ -8,7 +8,9 @@ body,
8
8
  height: 100vh;
9
9
  height: calc(var(--vh, 1vh) * 100);
10
10
  overflow: hidden;
11
- @apply font-sans;
11
+ print-color-adjust: exact;
12
+ -webkit-print-color-adjust: exact;
13
+ --uno: font-sans bg-main;
12
14
  }
13
15
 
14
16
  html {
@@ -17,11 +19,10 @@ html {
17
19
 
18
20
  .slidev-icon-btn {
19
21
  aspect-ratio: 1;
20
- display: inline-block;
21
22
  user-select: none;
22
23
  outline: none;
23
24
  cursor: pointer;
24
- @apply opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
25
+ @apply inline-flex items-center justify-center opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
25
26
  @apply hover:(opacity-100 bg-gray-400 bg-opacity-10);
26
27
  @apply focus-visible:(opacity-100 outline outline-2 outline-offset-2 outline-black dark:outline-white);
27
28
  @apply md:p-2;
@@ -40,6 +41,10 @@ html {
40
41
  pointer-events: none;
41
42
  }
42
43
 
44
+ .slidev-layout a.slidev-icon-btn {
45
+ @apply border-none hover:border-none hover:text-white;
46
+ }
47
+
43
48
  .slidev-vclick-target {
44
49
  @apply transition-opacity duration-100;
45
50
  }
@@ -125,7 +130,7 @@ html {
125
130
  position: fixed;
126
131
  }
127
132
 
128
- #twoslash-container .v-popper__wrapper {
133
+ #twoslash-container .v-popper__wrapper:not(.no-slide-scale > *) {
129
134
  transform: scale(calc(1 * var(--slidev-slide-scale)));
130
135
  transform-origin: 30px top;
131
136
  }
package/uno.config.ts CHANGED
@@ -13,6 +13,9 @@ export default defineConfig({
13
13
  safelist: [
14
14
  '!opacity-0',
15
15
  'prose',
16
+ // See https://github.com/slidevjs/slidev/issues/1705
17
+ 'grid-rows-[1fr_max-content]',
18
+ 'grid-cols-[1fr_max-content]',
16
19
  ],
17
20
  shortcuts: {
18
21
  'bg-main': 'bg-white dark:bg-[#121212]',
@@ -27,6 +30,17 @@ export default defineConfig({
27
30
  'abs-b': 'absolute bottom-0 left-0 right-0',
28
31
  'abs-bl': 'absolute bottom-0 left-0',
29
32
  'abs-br': 'absolute bottom-0 right-0',
33
+
34
+ 'z-drawing': 'z-10',
35
+ 'z-camera': 'z-15',
36
+ 'z-dragging': 'z-18',
37
+ 'z-menu': 'z-20',
38
+ 'z-label': 'z-40',
39
+ 'z-nav': 'z-50',
40
+ 'z-context-menu': 'z-60',
41
+ 'z-modal': 'z-70',
42
+
43
+ 'slidev-glass-effect': 'shadow-xl backdrop-blur-8 border border-main bg-main bg-opacity-75!',
30
44
  },
31
45
  // Slidev Specific Variants, probably extrat to a preset later
32
46
  variants: [
@@ -1,16 +0,0 @@
1
- <script setup lang="ts">
2
- import type { Slots } from 'vue'
3
- import { h } from 'vue'
4
- import { slideHeight, slideWidth } from '../env'
5
-
6
- function vStyle<Props>(props: Props, { slots }: { slots: Slots }) {
7
- if (slots.default)
8
- return h('style', slots.default())
9
- }
10
- </script>
11
-
12
- <template>
13
- <vStyle>
14
- @page { size: {{ slideWidth }}px {{ slideHeight }}px; margin: 0px; }
15
- </vStyle>
16
- </template>