@slidev/client 52.15.2 → 52.17.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 (43) hide show
  1. package/App.vue +2 -0
  2. package/builtin/AutoFitText.vue +54 -35
  3. package/builtin/KaTexBlockWrapper.test.ts +41 -0
  4. package/builtin/KaTexBlockWrapper.vue +4 -19
  5. package/builtin/ShikiMagicMove.vue +2 -2
  6. package/builtin/TocList.vue +1 -1
  7. package/builtin/katex-lines.ts +22 -0
  8. package/composables/useEmbeddedCtrl.ts +4 -2
  9. package/composables/useNav.ts +1 -1
  10. package/composables/usePreloadImages.ts +40 -1
  11. package/constants.ts +1 -0
  12. package/env.ts +1 -1
  13. package/internals/ClicksSlider.vue +99 -48
  14. package/internals/CodeRunner.vue +2 -2
  15. package/internals/Goto.vue +2 -2
  16. package/internals/NavControls.vue +1 -1
  17. package/internals/NoteDisplay.vue +5 -0
  18. package/internals/NoteEditable.vue +2 -3
  19. package/internals/PrintSlideClick.vue +1 -1
  20. package/internals/PwaIndicator.vue +50 -0
  21. package/internals/SideEditor.vue +2 -7
  22. package/internals/SlideWrapper.vue +1 -1
  23. package/internals/SlidesShow.vue +1 -1
  24. package/logic/sideEditor.ts +14 -0
  25. package/logic/slidePath.test.ts +29 -0
  26. package/logic/slidePath.ts +10 -0
  27. package/logic/slides.ts +3 -3
  28. package/modules/mermaid.ts +1 -1
  29. package/package.json +26 -25
  30. package/pages/overview.vue +293 -64
  31. package/pages/presenter.vue +1 -4
  32. package/setup/code-runners.ts +2 -2
  33. package/setup/context-menu.ts +1 -1
  34. package/setup/main.ts +11 -5
  35. package/setup/mermaid.ts +1 -1
  36. package/setup/monaco.ts +4 -4
  37. package/setup/pwa.ts +28 -0
  38. package/setup/root.ts +1 -1
  39. package/setup/shiki.ts +1 -1
  40. package/setup/shortcuts.ts +1 -1
  41. package/shim-vue.d.ts +1 -0
  42. package/shim.d.ts +2 -0
  43. package/styles/index.ts +1 -1
@@ -1,11 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import type { ClicksContext, SlideRoute } from '@slidev/types'
3
3
  import { useHead } from '@unhead/vue'
4
- import { computed, nextTick, onMounted, reactive, ref, shallowRef } from 'vue'
4
+ import { computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue'
5
+ import { useRoute, useRouter } from 'vue-router'
5
6
  import { createFixedClicks } from '../composables/useClicks'
6
7
  import { useNav } from '../composables/useNav'
7
8
  import { CLICKS_MAX } from '../constants'
8
- import { pathPrefix, slidesTitle } from '../env'
9
+ import { pathPrefix, slideAspect, slidesTitle } from '../env'
9
10
  import ClicksSlider from '../internals/ClicksSlider.vue'
10
11
  import DrawingPreview from '../internals/DrawingPreview.vue'
11
12
  import IconButton from '../internals/IconButton.vue'
@@ -14,19 +15,38 @@ import SlideContainer from '../internals/SlideContainer.vue'
14
15
  import SlideWrapper from '../internals/SlideWrapper.vue'
15
16
  import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
16
17
  import { getSlidePath } from '../logic/slides'
18
+ import { windowSize } from '../state'
17
19
 
18
20
  const cardWidth = 450
19
21
 
20
22
  useHead({ title: `Overview - ${slidesTitle}` })
21
23
 
22
- const { openInEditor, slides } = useNav()
24
+ const currentRoute = useRoute()
25
+ const router = useRouter()
26
+ const { openInEditor, slides, isEmbedded } = useNav()
27
+ const isPreviewMode = computed(() => currentRoute.query.mode === 'preview')
28
+ const isEmbeddedPreviewMode = computed(() => isPreviewMode.value && isEmbedded.value)
29
+ const overviewCardWidth = computed(() => {
30
+ if (!isPreviewMode.value)
31
+ return cardWidth
32
+ if (isEmbeddedPreviewMode.value)
33
+ return Math.max(0, windowSize.width.value - 16)
34
+ return Math.min(900, Math.max(320, windowSize.width.value - 160))
35
+ })
36
+ const overviewSlideHeight = computed(() => overviewCardWidth.value / slideAspect.value)
23
37
 
24
38
  const blocks: Map<number, HTMLElement> = reactive(new Map())
39
+ const slidePreviews: Map<number, HTMLElement> = reactive(new Map())
25
40
  const activeBlocks = ref<number[]>([])
41
+ const scroller = ref<HTMLElement>()
26
42
  const edittingNote = ref<number | null>(null)
43
+ let ignoreOverviewScrollUntil = 0
44
+ let pendingOverviewScrollNo: number | undefined
45
+ let overviewScrollTimer: ReturnType<typeof setTimeout> | undefined
27
46
  const wordCounts = computed(() => slides.value.map(route => wordCount(route.meta?.slide?.note || '')))
28
47
  const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
29
48
  const totalClicks = computed(() => slides.value.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
49
+ const slideNoDigits = computed(() => String(Math.max(1, slides.value.length)).length)
30
50
 
31
51
  const activeSlide = shallowRef<SlideRoute>()
32
52
  const clicksContextMap = new WeakMap<SlideRoute, ClicksContext>()
@@ -65,25 +85,23 @@ function wordCount(str: string) {
65
85
  return count
66
86
  }
67
87
 
68
- function isElementInViewport(el: HTMLElement) {
69
- const rect = el.getBoundingClientRect()
70
- const delta = 20
71
- return (
72
- rect.top >= 0 - delta
73
- && rect.left >= 0 - delta
74
- && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + delta
75
- && rect.right <= (window.innerWidth || document.documentElement.clientWidth) + delta
76
- )
77
- }
78
-
79
88
  function checkActiveBlocks() {
80
- const active: number[] = []
81
- Array.from(blocks.entries())
82
- .forEach(([idx, el]) => {
83
- if (isElementInViewport(el))
84
- active.push(idx)
85
- })
86
- activeBlocks.value = active
89
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight
90
+ let active: { idx: number, visibleHeight: number } | undefined
91
+ const fullyVisible: number[] = []
92
+
93
+ for (const [idx, el] of blocks.entries()) {
94
+ const rect = el.getBoundingClientRect()
95
+ const visibleHeight = Math.max(0, Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0))
96
+ if (visibleHeight === 0)
97
+ continue
98
+ if (visibleHeight >= rect.height)
99
+ fullyVisible.push(idx)
100
+ if (!active || visibleHeight > active.visibleHeight)
101
+ active = { idx, visibleHeight }
102
+ }
103
+
104
+ activeBlocks.value = fullyVisible.length ? fullyVisible : active ? [active.idx] : []
87
105
  }
88
106
 
89
107
  function openSlideInNewTab(path: string) {
@@ -93,12 +111,142 @@ function openSlideInNewTab(path: string) {
93
111
  a.click()
94
112
  }
95
113
 
114
+ function openSlideInBrowser(path: string) {
115
+ const url = new URL(pathPrefix + path.slice(1), location.href).href
116
+ if (isEmbedded.value) {
117
+ window.parent.postMessage({
118
+ target: 'slidev',
119
+ sender: 'slidev',
120
+ type: 'open-external',
121
+ url,
122
+ }, '*')
123
+ return
124
+ }
125
+ openSlideInNewTab(path)
126
+ }
127
+
96
128
  function scrollToSlide(idx: number) {
97
129
  const el = blocks.get(idx)
98
130
  if (el)
99
131
  el.scrollIntoView({ behavior: 'smooth', block: 'start' })
100
132
  }
101
133
 
134
+ function getSlidePreviewTop(idx: number) {
135
+ const el = slidePreviews.get(idx) || blocks.get(idx)
136
+ if (!el || !scroller.value)
137
+ return null
138
+ const scrollerRect = scroller.value.getBoundingClientRect()
139
+ const elRect = el.getBoundingClientRect()
140
+ return elRect.top - scrollerRect.top + scroller.value.scrollTop
141
+ }
142
+
143
+ function scrollSlideNoIntoCenter(no: number) {
144
+ if (!scroller.value || slides.value.length === 0)
145
+ return
146
+ const clamped = Math.min(Math.max(no, 1), slides.value.length)
147
+ const idx = Math.floor(clamped) - 1
148
+ const progress = clamped - (idx + 1)
149
+ const start = getSlidePreviewTop(idx)
150
+ const end = getSlidePreviewTop(idx + 1)
151
+ if (start == null)
152
+ return
153
+ const top = start
154
+ + (end == null ? 0 : (end - start) * progress)
155
+ - scroller.value.clientHeight * 0.5
156
+ if (Math.abs(scroller.value.scrollTop - top) < 1)
157
+ return
158
+ scroller.value.scrollTo({ top })
159
+ }
160
+
161
+ function getInitialSlideNo() {
162
+ const value = currentRoute.query.slideNo
163
+ const no = Number(Array.isArray(value) ? value[0] : value)
164
+ return Number.isFinite(no) && no > 0 ? no : undefined
165
+ }
166
+
167
+ function updateSlideNoQuery(no: number) {
168
+ if (!isEmbeddedPreviewMode.value)
169
+ return
170
+ const slideNo = Number(no.toFixed(3)).toString()
171
+ if (currentRoute.query.slideNo === slideNo)
172
+ return
173
+ router.replace({
174
+ query: {
175
+ ...currentRoute.query,
176
+ slideNo,
177
+ },
178
+ })
179
+ }
180
+
181
+ function getCenteredSlideNo() {
182
+ if (!scroller.value || slides.value.length === 0)
183
+ return null
184
+ const center = scroller.value.scrollTop + scroller.value.clientHeight * 0.5
185
+ const tops = slides.value
186
+ .map((_, idx) => getSlidePreviewTop(idx))
187
+ .filter((top): top is number => top != null)
188
+ if (tops.length === 0)
189
+ return null
190
+ if (tops.length === 1 || center <= tops[0])
191
+ return 1
192
+ for (let i = 1; i < tops.length; i++) {
193
+ if (center <= tops[i]) {
194
+ const span = Math.max(1, tops[i] - tops[i - 1])
195
+ return i + (center - tops[i - 1]) / span
196
+ }
197
+ }
198
+ return slides.value.length
199
+ }
200
+
201
+ function postOverviewScroll(no: number) {
202
+ pendingOverviewScrollNo = no
203
+ if (overviewScrollTimer)
204
+ return
205
+ overviewScrollTimer = setTimeout(() => {
206
+ overviewScrollTimer = undefined
207
+ const no = pendingOverviewScrollNo
208
+ pendingOverviewScrollNo = undefined
209
+ if (no == null)
210
+ return
211
+ updateSlideNoQuery(no)
212
+ if (Date.now() < ignoreOverviewScrollUntil)
213
+ return
214
+ window.parent.postMessage({
215
+ target: 'slidev',
216
+ sender: 'slidev',
217
+ type: 'overview-scroll',
218
+ no,
219
+ }, '*')
220
+ }, 50)
221
+ }
222
+
223
+ function onOverviewScroll() {
224
+ checkActiveBlocks()
225
+ if (!isEmbeddedPreviewMode.value || Date.now() < ignoreOverviewScrollUntil)
226
+ return
227
+ const no = getCenteredSlideNo()
228
+ if (no != null)
229
+ postOverviewScroll(no)
230
+ }
231
+
232
+ function onOverviewMessage({ data }: MessageEvent) {
233
+ if (
234
+ !isEmbeddedPreviewMode.value
235
+ || data?.target !== 'slidev'
236
+ || data.sender !== 'vscode'
237
+ || data.type !== 'overview-scroll'
238
+ ) {
239
+ return
240
+ }
241
+ const no = Number(data.no)
242
+ if (no > 0) {
243
+ ignoreOverviewScrollUntil = Date.now() + 300
244
+ pendingOverviewScrollNo = undefined
245
+ updateSlideNoQuery(no)
246
+ scrollSlideNoIntoCenter(no)
247
+ }
248
+ }
249
+
102
250
  function onMarkerClick(e: MouseEvent, clicks: number, route: SlideRoute) {
103
251
  const ctx = getClicksContext(route)
104
252
  if (ctx.current === clicks)
@@ -108,18 +256,52 @@ function onMarkerClick(e: MouseEvent, clicks: number, route: SlideRoute) {
108
256
  e.preventDefault()
109
257
  }
110
258
 
259
+ function openOverviewSlideSource(e: MouseEvent, route: SlideRoute) {
260
+ if (e.ctrlKey || e.metaKey) {
261
+ e.preventDefault()
262
+ openSlideInBrowser(getSlidePath(route, false))
263
+ return
264
+ }
265
+ const slide = route.meta?.slide
266
+ if (!slide)
267
+ return
268
+ window.parent.postMessage({
269
+ target: 'slidev',
270
+ type: 'command',
271
+ command: 'goto',
272
+ args: [slide.filepath, slide.sourceIndex],
273
+ }, '*')
274
+ }
275
+
111
276
  onMounted(() => {
277
+ window.addEventListener('message', onOverviewMessage)
278
+ const initialSlideNo = isEmbeddedPreviewMode.value ? getInitialSlideNo() : undefined
279
+ if (initialSlideNo != null) {
280
+ ignoreOverviewScrollUntil = Date.now() + 300
281
+ scrollSlideNoIntoCenter(initialSlideNo)
282
+ }
112
283
  nextTick(() => {
284
+ if (initialSlideNo != null)
285
+ scrollSlideNoIntoCenter(initialSlideNo)
113
286
  checkActiveBlocks()
114
287
  })
115
288
  })
289
+
290
+ onUnmounted(() => {
291
+ window.removeEventListener('message', onOverviewMessage)
292
+ if (overviewScrollTimer)
293
+ clearTimeout(overviewScrollTimer)
294
+ })
116
295
  </script>
117
296
 
118
297
  <template>
119
298
  <div class="h-screen w-screen of-hidden flex">
120
- <nav class="grid grid-rows-[auto_max-content] border-r border-main select-none max-h-full h-full">
299
+ <nav
300
+ v-if="!isEmbedded"
301
+ class="grid grid-rows-[auto_max-content] border-r border-main select-none max-h-full h-full"
302
+ >
121
303
  <div class="relative">
122
- <div class="absolute left-0 top-0 bottom-0 w-200 flex flex-col flex-auto items-end group p2 gap-1 max-h-full of-x-visible of-y-auto" style="direction:rtl">
304
+ <div class="absolute left-0 top-0 bottom-0 w-200 flex flex-col flex-auto items-end group p-6px md:p-10px gap-1 max-h-full of-x-visible of-y-auto" style="direction:rtl">
123
305
  <div
124
306
  v-for="(route, idx) of slides"
125
307
  :key="route.no"
@@ -163,46 +345,86 @@ onMounted(() => {
163
345
  </div>
164
346
  </nav>
165
347
  <main
348
+ ref="scroller"
166
349
  class="flex-1 h-full of-auto"
167
350
  :style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`"
168
- @scroll="checkActiveBlocks"
351
+ @scroll="onOverviewScroll"
169
352
  >
170
353
  <div
171
354
  v-for="(route, idx) of slides"
172
355
  :key="route.no"
173
356
  :ref="el => blocks.set(idx, el as any)"
174
- class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
175
- :class="idx === 0 ? 'pt5' : ''"
357
+ class="overview-slide-block relative of-hidden flex gap-4 min-h-50"
358
+ :class="[idx === 0 && !isEmbeddedPreviewMode ? 'pt2' : '', isEmbeddedPreviewMode ? 'justify-center' : 'border-t border-main']"
176
359
  >
177
- <div class="select-none w-13 text-right my4 flex flex-col gap-1 items-end">
178
- <div class="text-3xl op20 mb2">
360
+ <div
361
+ v-if="!isEmbeddedPreviewMode"
362
+ class="select-none text-right my5 flex flex-col justify-between items-end"
363
+ :class="isPreviewMode ? 'w-9' : 'w-13'"
364
+ :style="{ height: `${overviewSlideHeight}px` }"
365
+ >
366
+ <div class="self-center text-3xl op20 mb2 text-center mr--14px tabular-nums" :style="{ width: `${slideNoDigits}ch` }">
179
367
  {{ idx + 1 }}
180
368
  </div>
181
- <IconButton
182
- class="mr--3 op0 group-hover:op80"
183
- title="Play in new tab"
184
- @click="openSlideInNewTab(getSlidePath(route, false))"
185
- >
186
- <div class="i-carbon:presentation-file" />
187
- </IconButton>
188
- <IconButton
189
- v-if="__DEV__ && route.meta?.slide"
190
- class="mr--3 op0 group-hover:op80"
191
- title="Open in editor"
192
- @click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
193
- >
194
- <div class="i-carbon:cics-program" />
195
- </IconButton>
369
+ <div class="flex flex-col gap-1 mx-1 items-end">
370
+ <IconButton
371
+ class="overview-slide-action mr--4 op0"
372
+ :class="isPreviewMode ? 'text-lg' : ''"
373
+ title="Play in new tab"
374
+ @click="openSlideInNewTab(getSlidePath(route, false))"
375
+ >
376
+ <div class="i-carbon:presentation-file" />
377
+ </IconButton>
378
+ <IconButton
379
+ v-if="__DEV__ && route.meta?.slide"
380
+ class="overview-slide-action mr--4 op0"
381
+ :class="isPreviewMode ? 'text-lg' : ''"
382
+ title="Open in editor"
383
+ @click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
384
+ >
385
+ <div class="i-carbon:edit" />
386
+ </IconButton>
387
+ </div>
196
388
  </div>
197
- <div class="flex flex-col gap-2 my5" :style="{ width: `${cardWidth}px` }">
389
+ <div
390
+ class="flex flex-col"
391
+ :class="isEmbeddedPreviewMode ? 'my1 gap-0' : 'my5 gap-2'"
392
+ :style="{ width: `${overviewCardWidth}px` }"
393
+ >
394
+ <div
395
+ v-if="isEmbeddedPreviewMode"
396
+ class="flex items-end gap-2"
397
+ >
398
+ <button
399
+ type="button"
400
+ class="select-none pl-1 text-lg leading-tight op60 tabular-nums hover:op90 hover:underline underline-offset-2"
401
+ @click="openOverviewSlideSource($event, route)"
402
+ >
403
+ {{ idx + 1 }}
404
+ </button>
405
+ <ClicksSlider
406
+ v-if="getSlideClicks(route)"
407
+ :active="activeSlide === route"
408
+ :clicks-context="getClicksContext(route)"
409
+ resettable
410
+ compact
411
+ attached
412
+ class="ml-auto w-88 min-w-[70%] max-w-[calc(100%-3rem)]"
413
+ @dblclick="toggleRoute(route)"
414
+ @activate="activeSlide = route"
415
+ @reset="activeSlide = undefined"
416
+ />
417
+ </div>
198
418
  <div
199
- class="border rounded border-main overflow-hidden bg-main select-none h-max"
200
- @dblclick="openSlideInNewTab(getSlidePath(route, false))"
419
+ :ref="el => slidePreviews.set(idx, el as any)"
420
+ class="border rounded border-main overflow-hidden bg-main h-max"
421
+ :class="[isEmbeddedPreviewMode && getSlideClicks(route) ? 'rounded-tr-0' : '', isEmbeddedPreviewMode ? '' : 'select-none']"
422
+ @dblclick="!isEmbeddedPreviewMode && openSlideInNewTab(getSlidePath(route, false))"
201
423
  >
202
424
  <SlideContainer
203
425
  :key="route.no"
204
- :width="cardWidth"
205
- class="pointer-events-none important:[&_*]:select-none"
426
+ :width="overviewCardWidth"
427
+ :class="isEmbeddedPreviewMode ? '' : 'pointer-events-none important:[&_*]:select-none'"
206
428
  >
207
429
  <SlideWrapper
208
430
  :clicks-context="getClicksContext(route)"
@@ -213,27 +435,21 @@ onMounted(() => {
213
435
  </SlideContainer>
214
436
  </div>
215
437
  <ClicksSlider
216
- v-if="getSlideClicks(route)"
438
+ v-if="getSlideClicks(route) && !isEmbeddedPreviewMode"
217
439
  :active="activeSlide === route"
218
440
  :clicks-context="getClicksContext(route)"
219
- class="w-full mt-2"
441
+ resettable
442
+ class="ml-1 w-[calc(100%-0.25rem)]"
443
+ :class="isPreviewMode ? '' : 'mt-2'"
220
444
  @dblclick="toggleRoute(route)"
221
- @click="activeSlide = route"
445
+ @activate="activeSlide = route"
446
+ @reset="activeSlide = undefined"
222
447
  />
223
448
  </div>
224
- <div class="py3 mt-0.5 mr--8 ml--4 op0 transition group-hover:op100">
225
- <IconButton
226
- title="Edit Note"
227
- class="rounded-full w-9 h-9 text-sm"
228
- :class="edittingNote === route.no ? 'important:op0' : ''"
229
- @click="edittingNote = route.no"
230
- >
231
- <div class="i-carbon:pen" />
232
- </IconButton>
233
- </div>
234
449
  <NoteEditable
450
+ v-if="!isPreviewMode"
235
451
  :no="route.no"
236
- class="max-w-250 w-250 text-lg rounded p3"
452
+ class="relative z-1 max-w-250 w-250 text-lg rounded p3"
237
453
  :auto-height="true"
238
454
  :highlight="activeSlide === route"
239
455
  :editing="edittingNote === route.no"
@@ -243,14 +459,17 @@ onMounted(() => {
243
459
  @marker-click="(e, clicks) => onMarkerClick(e, clicks, route)"
244
460
  />
245
461
  <div
246
- v-if="wordCounts[idx] > 0"
462
+ v-if="!isPreviewMode && wordCounts[idx] > 0"
247
463
  class="select-none absolute bottom-0 right-0 bg-main rounded-tl p2 op35 text-xs"
248
464
  >
249
465
  {{ wordCounts[idx] }} words
250
466
  </div>
251
467
  </div>
252
468
  </main>
253
- <div class="absolute top-0 right-0 px3 py1.5 border-b border-l rounded-lb bg-main border-main select-none">
469
+ <div
470
+ v-if="!isEmbedded"
471
+ class="absolute z-2 top-0 right-0 px3 py1.5 border-b border-l rounded-lb bg-main/80 backdrop-blur border-main select-none"
472
+ >
254
473
  <div class="text-xs op50">
255
474
  {{ slides.length }} slides ·
256
475
  {{ totalClicks + slides.length - 1 }} clicks ·
@@ -259,3 +478,13 @@ onMounted(() => {
259
478
  </div>
260
479
  </div>
261
480
  </template>
481
+
482
+ <style scoped>
483
+ .overview-slide-block:hover .overview-slide-action {
484
+ opacity: 0.6;
485
+ }
486
+
487
+ .overview-slide-action:hover {
488
+ opacity: 0.8;
489
+ }
490
+ </style>
@@ -436,6 +436,7 @@ onMounted(() => {
436
436
  --slidev-presenter-notes-width: 360px;
437
437
  --slidev-presenter-notes-row-size: 280px;
438
438
  --uno: bg-gray/20 flex-1 of-hidden;
439
+ position: relative;
439
440
  display: grid;
440
441
  gap: 1px 1px;
441
442
  }
@@ -572,8 +573,6 @@ onMounted(() => {
572
573
  top: 0;
573
574
  width: 1px;
574
575
  height: 100%;
575
- background-color: currentColor;
576
- opacity: 0.2;
577
576
  transform: translateX(-50%);
578
577
  }
579
578
 
@@ -596,8 +595,6 @@ onMounted(() => {
596
595
  top: 0;
597
596
  width: 1px;
598
597
  height: 100%;
599
- background-color: currentColor;
600
- opacity: 0.2;
601
598
  transform: translateX(-50%);
602
599
  }
603
600
  </style>
@@ -1,10 +1,10 @@
1
1
  import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputs, CodeRunnerOutputText } from '@slidev/types'
2
2
  import type { CodeToHastOptions } from 'shiki'
3
3
  import type ts from 'typescript'
4
- import deps from '#slidev/monaco-run-deps'
5
- import setups from '#slidev/setups/code-runners'
6
4
  import { createSingletonPromise } from '@antfu/utils'
7
5
  import { ref } from 'vue'
6
+ import deps from '#slidev/monaco-run-deps'
7
+ import setups from '#slidev/setups/code-runners'
8
8
  import { configs } from '../env'
9
9
 
10
10
  export default createSingletonPromise(async () => {
@@ -2,8 +2,8 @@
2
2
 
3
3
  import type { ContextMenuItem } from '@slidev/types'
4
4
  import type { ComputedRef } from 'vue'
5
- import setups from '#slidev/setups/context-menu'
6
5
  import { computed } from 'vue'
6
+ import setups from '#slidev/setups/context-menu'
7
7
  import { useDrawings } from '../composables/useDrawings'
8
8
  import { useNav } from '../composables/useNav'
9
9
  import { fullscreen, showEditor, toggleOverview } from '../state'
package/setup/main.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import type { AppContext } from '@slidev/types'
2
2
  import type { App } from 'vue'
3
- import setups from '#slidev/setups/main'
4
3
  import TwoSlashFloatingVue from '@shikijs/vitepress-twoslash/client'
5
4
  import { createHead } from '@unhead/vue/client'
6
- import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
5
+ import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
6
+ import setups from '#slidev/setups/main'
7
7
  import { createVClickDirectives } from '../modules/v-click'
8
8
  import { createVDragDirective } from '../modules/v-drag'
9
9
  import { createVMarkDirective } from '../modules/v-mark'
10
10
  import { createVMotionDirectives } from '../modules/v-motion'
11
+ import { setupPWA } from '../setup/pwa'
11
12
  import setupRoutes from '../setup/routes'
12
13
  import '../styles'
13
14
 
@@ -21,9 +22,11 @@ export default async function setupMain(app: App) {
21
22
  window.addEventListener('resize', setMaxHeight)
22
23
 
23
24
  const router = createRouter({
24
- history: __SLIDEV_HASH_ROUTE__
25
- ? createWebHashHistory(import.meta.env.BASE_URL)
26
- : createWebHistory(import.meta.env.BASE_URL),
25
+ history: __SLIDEV_MEMORY_ROUTE__
26
+ ? createMemoryHistory(import.meta.env.BASE_URL)
27
+ : __SLIDEV_HASH_ROUTE__
28
+ ? createWebHashHistory(import.meta.env.BASE_URL)
29
+ : createWebHistory(import.meta.env.BASE_URL),
27
30
  routes: setupRoutes(),
28
31
  })
29
32
 
@@ -42,4 +45,7 @@ export default async function setupMain(app: App) {
42
45
 
43
46
  for (const setup of setups)
44
47
  await setup(context)
48
+
49
+ if (__SLIDEV_FEATURE_PWA__)
50
+ setupPWA()
45
51
  }
package/setup/mermaid.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { MermaidConfig } from 'mermaid'
2
- import setups from '#slidev/setups/mermaid'
3
2
  import { createSingletonPromise } from '@antfu/utils'
3
+ import setups from '#slidev/setups/mermaid'
4
4
 
5
5
  export default createSingletonPromise(async () => {
6
6
  const setupReturn: MermaidConfig = {
package/setup/monaco.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  import type { MonacoSetupReturn } from '@slidev/types'
2
- import configs from '#slidev/configs'
3
- import setups from '#slidev/setups/monaco'
4
2
  import { createSingletonPromise } from '@antfu/utils'
5
3
  import { shikiToMonaco } from '@shikijs/monaco'
6
4
  import { setupTypeAcquisition } from '@typescript/ata'
7
5
  import * as monaco from 'monaco-editor'
8
-
9
6
  import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
10
7
  // @ts-expect-error missing types
11
8
  import { StandaloneServices } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneServices'
9
+
12
10
  import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
13
11
  import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
14
12
  import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
@@ -17,9 +15,11 @@ import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
17
15
  import { ContextViewService } from 'monaco-editor/esm/vs/platform/contextview/browser/contextViewService'
18
16
  // @ts-expect-error missing types
19
17
  import { SyncDescriptor } from 'monaco-editor/esm/vs/platform/instantiation/common/descriptors'
20
-
21
18
  import ts from 'typescript'
22
19
  import { watchEffect } from 'vue'
20
+
21
+ import configs from '#slidev/configs'
22
+ import setups from '#slidev/setups/monaco'
23
23
  import { isDark } from '../logic/dark'
24
24
  import { lockShortcuts } from '../state'
25
25
 
package/setup/pwa.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { ref } from 'vue'
2
+
3
+ export const pwaCaching = ref(false)
4
+ export const pwaReady = ref(false)
5
+
6
+ export async function setupPWA() {
7
+ if (!__SLIDEV_FEATURE_PWA__)
8
+ return
9
+
10
+ const { registerSW } = await import('virtual:pwa-register')
11
+
12
+ pwaCaching.value = true
13
+
14
+ registerSW({
15
+ immediate: true,
16
+ onOfflineReady() {
17
+ pwaCaching.value = false
18
+ pwaReady.value = true
19
+ // Auto-dismiss "ready" indicator after 4 seconds
20
+ setTimeout(() => {
21
+ pwaReady.value = false
22
+ }, 4000)
23
+ },
24
+ onRegisteredSW(_swUrl, _registration) {
25
+ // SW registered; caching is in progress until onOfflineReady fires
26
+ },
27
+ })
28
+ }
package/setup/root.ts CHANGED
@@ -1,7 +1,7 @@
1
- import setups from '#slidev/setups/root'
2
1
  import { useHead } from '@unhead/vue'
3
2
  import { computed, getCurrentInstance, reactive, ref, shallowRef, watch } from 'vue'
4
3
  import { useRouter } from 'vue-router'
4
+ import setups from '#slidev/setups/root'
5
5
  import { createFixedClicks } from '../composables/useClicks'
6
6
  import { useEmbeddedControl } from '../composables/useEmbeddedCtrl'
7
7
  import { useNav } from '../composables/useNav'
package/setup/shiki.ts CHANGED
@@ -1,7 +1,7 @@
1
- import setups from '#slidev/setups/shiki'
2
1
  import { createSingletonPromise } from '@antfu/utils'
3
2
  import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'
4
3
  import { createBundledHighlighter, createSingletonShorthands } from 'shiki/core'
4
+ import setups from '#slidev/setups/shiki'
5
5
  import { resolveShikiOptions, shikiContext } from './shiki-options'
6
6
 
7
7
  export default createSingletonPromise(async () => {
@@ -1,6 +1,6 @@
1
1
  import type { NavOperations, ShortcutOptions } from '@slidev/types'
2
- import setups from '#slidev/setups/shortcuts'
3
2
  import { and, not, or } from '@vueuse/math'
3
+ import setups from '#slidev/setups/shortcuts'
4
4
  import { useDrawings } from '../composables/useDrawings'
5
5
  import { useNav } from '../composables/useNav'
6
6
  import { toggleDark } from '../logic/dark'
package/shim-vue.d.ts CHANGED
@@ -23,6 +23,7 @@ declare module 'vue-router' {
23
23
  noteHTML: string
24
24
  filepath: string
25
25
  start: number
26
+ sourceIndex: number
26
27
  id: number
27
28
  no: number
28
29
  }
package/shim.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /// <reference types="vite-plugin-pwa/client" />
2
+
1
3
  declare module '*.md' {
2
4
  // with unplugin-vue-markdown, markdowns can be treat as Vue components
3
5
  import type { ComponentOptions } from 'vue'