@slidev/client 0.50.0-beta.10 → 0.50.0-beta.12

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.
@@ -0,0 +1,61 @@
1
+ import { computed, ref } from 'vue'
2
+
3
+ export async function startScreenshotSession(width: number, height: number) {
4
+ const canvas = document.createElement('canvas')
5
+ canvas.width = width
6
+ canvas.height = height
7
+ const context = canvas.getContext('2d')!
8
+ const video = document.createElement('video')
9
+ video.width = width
10
+ video.height = height
11
+
12
+ const captureStream = ref<MediaStream | null>(await navigator.mediaDevices.getDisplayMedia({
13
+ video: {
14
+ // Use a rather small frame rate
15
+ frameRate: 10,
16
+ // @ts-expect-error missing types
17
+ cursor: 'never',
18
+ },
19
+ selfBrowserSurface: 'include',
20
+ preferCurrentTab: true,
21
+ }))
22
+ captureStream.value!.addEventListener('inactive', dispose)
23
+
24
+ video.srcObject = captureStream.value!
25
+ video.play()
26
+
27
+ function screenshot(element: HTMLElement) {
28
+ if (!captureStream.value)
29
+ throw new Error('captureStream inactive')
30
+ context.clearRect(0, 0, width, height)
31
+ const { left, top, width: elWidth } = element.getBoundingClientRect()
32
+ context.drawImage(
33
+ video,
34
+ left * window.devicePixelRatio,
35
+ top * window.devicePixelRatio,
36
+ elWidth * window.devicePixelRatio,
37
+ elWidth / width * height * window.devicePixelRatio,
38
+ 0,
39
+ 0,
40
+ width,
41
+ height,
42
+ )
43
+ return canvas.toDataURL('image/png')
44
+ }
45
+
46
+ function dispose() {
47
+ captureStream.value?.getTracks().forEach(track => track.stop())
48
+ captureStream.value = null
49
+ }
50
+
51
+ return {
52
+ isActive: computed(() => !!captureStream.value),
53
+ screenshot,
54
+ dispose,
55
+ }
56
+ }
57
+
58
+ export type ScreenshotSession = Awaited<ReturnType<typeof startScreenshotSession>>
59
+
60
+ const chromeVersion = window.navigator.userAgent.match(/Chrome\/(\d+)/)?.[1]
61
+ export const isScreenshotSupported = chromeVersion ? Number(chromeVersion) >= 94 : false
@@ -4,46 +4,15 @@ import type { Ref } from 'vue'
4
4
  import { onKeyStroke } from '@vueuse/core'
5
5
  import { and, not } from '@vueuse/math'
6
6
  import { watch } from 'vue'
7
+ import { useNav } from '../composables/useNav'
7
8
  import setupShortcuts from '../setup/shortcuts'
8
9
  import { fullscreen, isInputting, isOnFocus, magicKeys, shortcutsEnabled } from '../state'
9
10
 
10
- const _shortcut = and(not(isInputting), not(isOnFocus), shortcutsEnabled)
11
-
12
- export function shortcut(key: string | Ref<boolean>, fn: Fn, autoRepeat = false) {
13
- if (typeof key === 'string')
14
- key = magicKeys[key]
15
-
16
- const source = and(key, _shortcut)
17
- let count = 0
18
- let timer: any
19
- const trigger = () => {
20
- clearTimeout(timer)
21
- if (!source.value) {
22
- count = 0
23
- return
24
- }
25
- if (autoRepeat) {
26
- timer = setTimeout(trigger, Math.max(1000 - count * 250, 150))
27
- count++
28
- }
29
- fn()
30
- }
31
-
32
- return watch(source, trigger, { flush: 'sync' })
33
- }
34
-
35
- export function strokeShortcut(key: KeyFilter, fn: Fn) {
36
- return onKeyStroke(key, (ev) => {
37
- if (!_shortcut.value)
38
- return
39
- if (!ev.repeat)
40
- fn()
41
- })
42
- }
43
-
44
11
  export function registerShortcuts() {
45
- const allShortcuts = setupShortcuts()
12
+ const { isPrintMode } = useNav()
13
+ const enabled = and(not(isInputting), not(isOnFocus), not(isPrintMode), shortcutsEnabled)
46
14
 
15
+ const allShortcuts = setupShortcuts()
47
16
  const shortcuts = new Map<string | Ref<boolean>, ShortcutOptions>(
48
17
  allShortcuts.map((options: ShortcutOptions) => [options.key, options]),
49
18
  )
@@ -54,4 +23,36 @@ export function registerShortcuts() {
54
23
  })
55
24
 
56
25
  strokeShortcut('f', () => fullscreen.toggle())
26
+
27
+ function shortcut(key: string | Ref<boolean>, fn: Fn, autoRepeat = false) {
28
+ if (typeof key === 'string')
29
+ key = magicKeys[key]
30
+
31
+ const source = and(key, enabled)
32
+ let count = 0
33
+ let timer: any
34
+ const trigger = () => {
35
+ clearTimeout(timer)
36
+ if (!source.value) {
37
+ count = 0
38
+ return
39
+ }
40
+ if (autoRepeat) {
41
+ timer = setTimeout(trigger, Math.max(1000 - count * 250, 150))
42
+ count++
43
+ }
44
+ fn()
45
+ }
46
+
47
+ return watch(source, trigger, { flush: 'sync' })
48
+ }
49
+
50
+ function strokeShortcut(key: KeyFilter, fn: Fn) {
51
+ return onKeyStroke(key, (ev) => {
52
+ if (!enabled.value)
53
+ return
54
+ if (!ev.repeat)
55
+ fn()
56
+ })
57
+ }
57
58
  }
package/logic/slides.ts CHANGED
@@ -15,11 +15,12 @@ export function getSlide(no: number | string) {
15
15
  export function getSlidePath(
16
16
  route: SlideRoute | number | string,
17
17
  presenter: boolean,
18
+ exporting: boolean = false,
18
19
  ) {
19
20
  if (typeof route === 'number' || typeof route === 'string')
20
21
  route = getSlide(route)!
21
22
  const no = route.meta.slide?.frontmatter.routeAlias ?? route.no
22
- return presenter ? `/presenter/${no}` : `/${no}`
23
+ return exporting ? `/export/${no}` : presenter ? `/presenter/${no}` : `/${no}`
23
24
  }
24
25
 
25
26
  export function useIsSlideActive() {
package/modules/v-mark.ts CHANGED
@@ -3,6 +3,7 @@ import type { RawAtValue } from '@slidev/types'
3
3
  import type { App } from 'vue'
4
4
  import { annotate } from '@slidev/rough-notation'
5
5
  import { computed, watchEffect } from 'vue'
6
+ import { useNav } from '../composables/useNav'
6
7
  import { resolveClick } from './v-click'
7
8
 
8
9
  export interface RoughDirectiveOptions extends Partial<RoughAnnotationConfig> {
@@ -78,6 +79,8 @@ export function createVMarkDirective() {
78
79
  name: 'v-mark',
79
80
 
80
81
  mounted: (el, binding) => {
82
+ const { isPrintMode } = useNav()
83
+
81
84
  const options = computed(() => {
82
85
  const bindingOptions = (typeof binding.value === 'object' && !Array.isArray(binding.value))
83
86
  ? { ...binding.value }
@@ -109,6 +112,9 @@ export function createVMarkDirective() {
109
112
  }
110
113
  options.type ||= 'underline'
111
114
 
115
+ if (isPrintMode.value)
116
+ options.animationDuration = 1 /* millisecond */
117
+
112
118
  return options
113
119
  })
114
120
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.50.0-beta.10",
4
+ "version": "0.50.0-beta.12",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -32,12 +32,12 @@
32
32
  "@iconify-json/carbon": "^1.2.4",
33
33
  "@iconify-json/ph": "^1.2.1",
34
34
  "@iconify-json/svg-spinners": "^1.2.1",
35
- "@shikijs/monaco": "^1.24.0",
36
- "@shikijs/vitepress-twoslash": "^1.24.0",
35
+ "@shikijs/monaco": "^1.24.2",
36
+ "@shikijs/vitepress-twoslash": "^1.24.2",
37
37
  "@slidev/rough-notation": "^0.1.0",
38
38
  "@typescript/ata": "^0.9.7",
39
- "@unhead/vue": "^1.11.13",
40
- "@unocss/reset": "^0.65.0",
39
+ "@unhead/vue": "^1.11.14",
40
+ "@unocss/reset": "^0.65.1",
41
41
  "@vueuse/core": "^12.0.0",
42
42
  "@vueuse/math": "^12.0.0",
43
43
  "@vueuse/motion": "^2.2.6",
@@ -46,23 +46,25 @@
46
46
  "floating-vue": "^5.2.2",
47
47
  "fuse.js": "^7.0.0",
48
48
  "html-to-image": "^1.11.11",
49
- "katex": "^0.16.11",
49
+ "katex": "^0.16.15",
50
50
  "lz-string": "^1.5.0",
51
51
  "mermaid": "^11.4.1",
52
52
  "monaco-editor": "0.51.0",
53
- "prettier": "^3.4.1",
53
+ "nanotar": "^0.1.1",
54
+ "pptxgenjs": "^3.12.0",
55
+ "prettier": "^3.4.2",
54
56
  "recordrtc": "^5.6.2",
55
- "shiki": "^1.24.0",
56
- "shiki-magic-move": "^0.5.0",
57
+ "shiki": "^1.24.2",
58
+ "shiki-magic-move": "^0.5.2",
57
59
  "typescript": "5.6.3",
58
- "unocss": "^0.65.0",
60
+ "unocss": "^0.65.1",
59
61
  "vue": "^3.5.13",
60
62
  "vue-router": "^4.5.0",
61
63
  "yaml": "^2.6.1",
62
- "@slidev/types": "0.50.0-beta.10",
63
- "@slidev/parser": "0.50.0-beta.10"
64
+ "@slidev/parser": "0.50.0-beta.12",
65
+ "@slidev/types": "0.50.0-beta.12"
64
66
  },
65
67
  "devDependencies": {
66
- "vite": "^6.0.2"
68
+ "vite": "^6.0.3"
67
69
  }
68
70
  }
@@ -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 bg-main px2 py0 shadow z-1000 border="~ main rounded">
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>
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"