@slidev/client 0.50.0-beta.1 → 0.50.0-beta.11

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 (56) hide show
  1. package/builtin/KaTexBlockWrapper.vue +1 -1
  2. package/builtin/Monaco.vue +1 -0
  3. package/builtin/TocList.vue +4 -1
  4. package/builtin/Tweet.vue +1 -1
  5. package/composables/useClicks.ts +6 -5
  6. package/composables/useDragElements.ts +31 -25
  7. package/composables/useNav.ts +17 -15
  8. package/composables/usePrintStyles.ts +28 -0
  9. package/composables/useTimer.ts +20 -0
  10. package/composables/useViewTransition.ts +1 -2
  11. package/constants.ts +2 -0
  12. package/internals/ClicksSlider.vue +1 -1
  13. package/internals/CodeRunner.vue +1 -1
  14. package/internals/ContextMenu.vue +6 -4
  15. package/internals/DrawingControls.vue +12 -12
  16. package/internals/ExportPdfTip.vue +90 -0
  17. package/internals/FormCheckbox.vue +16 -0
  18. package/internals/FormItem.vue +42 -0
  19. package/internals/IconButton.vue +7 -2
  20. package/internals/NavControls.vue +22 -16
  21. package/internals/PrintContainer.vue +2 -21
  22. package/internals/PrintSlide.vue +4 -3
  23. package/internals/PrintSlideClick.vue +11 -3
  24. package/internals/QuickOverview.vue +2 -2
  25. package/internals/RecordingControls.vue +4 -4
  26. package/internals/RecordingDialog.vue +1 -1
  27. package/internals/SelectList.vue +5 -3
  28. package/internals/Settings.vue +5 -2
  29. package/internals/SideEditor.vue +6 -6
  30. package/internals/SlideContainer.vue +6 -7
  31. package/internals/SlideWrapper.vue +1 -0
  32. package/internals/SlidesShow.vue +7 -3
  33. package/layouts/error.vue +5 -1
  34. package/logic/screenshot.ts +61 -0
  35. package/logic/shortcuts.ts +36 -35
  36. package/logic/slides.ts +2 -1
  37. package/logic/utils.ts +0 -23
  38. package/main.ts +7 -3
  39. package/modules/v-mark.ts +6 -0
  40. package/package.json +28 -26
  41. package/pages/entry.vue +4 -4
  42. package/pages/export.vue +369 -0
  43. package/pages/notes.vue +4 -4
  44. package/pages/overview.vue +3 -3
  45. package/pages/play.vue +5 -6
  46. package/pages/presenter.vue +30 -22
  47. package/pages/print.vue +0 -2
  48. package/setup/context-menu.ts +12 -21
  49. package/setup/monaco.ts +24 -28
  50. package/setup/root.ts +6 -2
  51. package/setup/routes.ts +23 -12
  52. package/setup/shortcuts.ts +1 -2
  53. package/state/index.ts +2 -1
  54. package/styles/index.css +8 -3
  55. package/uno.config.ts +3 -0
  56. package/internals/PrintStyle.vue +0 -16
@@ -21,7 +21,7 @@ Learn more: https://sli.dev/guide/syntax.html#latex-line-highlighting
21
21
 
22
22
  <script setup lang="ts">
23
23
  import type { PropType } from 'vue'
24
- import { parseRangeString } from '@slidev/parser'
24
+ import { parseRangeString } from '@slidev/parser/utils'
25
25
  import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
26
26
  import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'
27
27
  import { useSlideContext } from '../context'
@@ -197,6 +197,7 @@ onMounted(async () => {
197
197
  })
198
198
 
199
199
  nextTick(() => monaco.editor.remeasureFonts())
200
+ setTimeout(() => monaco.editor.remeasureFonts(), 1000)
200
201
  })
201
202
  </script>
202
203
 
@@ -11,6 +11,7 @@ import type { TocItem } from '@slidev/types'
11
11
  import TitleRenderer from '#slidev/title-renderer'
12
12
  import { toArray } from '@antfu/utils'
13
13
  import { computed } from 'vue'
14
+ import { useNav } from '../composables/useNav'
14
15
 
15
16
  const props = withDefaults(defineProps<{
16
17
  level: number
@@ -20,6 +21,8 @@ const props = withDefaults(defineProps<{
20
21
  listClass?: string | string[]
21
22
  }>(), { level: 1 })
22
23
 
24
+ const { isPresenter } = useNav()
25
+
23
26
  const classes = computed(() => {
24
27
  return [
25
28
  ...toArray(props.listClass || []),
@@ -47,7 +50,7 @@ const styles = computed(() => {
47
50
  :key="item.path" class="slidev-toc-item"
48
51
  :class="[{ 'slidev-toc-item-active': item.active }, { 'slidev-toc-item-parent-active': item.activeParent }]"
49
52
  >
50
- <Link :to="item.path">
53
+ <Link :to="isPresenter ? `/presenter${item.path}` : item.path">
51
54
  <TitleRenderer :no="item.no" />
52
55
  </Link>
53
56
  <TocList
package/builtin/Tweet.vue CHANGED
@@ -55,7 +55,7 @@ onMounted(() => {
55
55
  <div ref="tweet" class="tweet slidev-tweet">
56
56
  <div v-if="!loaded || tweetNotFound" class="w-30 h-30 my-10px bg-gray-400 bg-opacity-10 rounded-lg flex opacity-50">
57
57
  <div class="m-auto animate-pulse text-4xl">
58
- <carbon:logo-twitter />
58
+ <div class="i-carbon:logo-twitter" />
59
59
  <span v-if="tweetNotFound">Could not load tweet with id="{{ props.id }}"</span>
60
60
  </div>
61
61
  </div>
@@ -1,7 +1,7 @@
1
1
  import type { ClicksContext, NormalizedRangeClickValue, NormalizedSingleClickValue, RawAtValue, RawSingleAtValue, SlideRoute } from '@slidev/types'
2
- import type { Ref } from 'vue'
2
+ import type { MaybeRefOrGetter, Ref } from 'vue'
3
3
  import { clamp, sum } from '@antfu/utils'
4
- import { computed, onMounted, onUnmounted, ref, shallowReactive } from 'vue'
4
+ import { computed, isReadonly, onMounted, onUnmounted, ref, shallowReactive, toValue } from 'vue'
5
5
 
6
6
  export function normalizeSingleAtValue(at: RawSingleAtValue): NormalizedSingleClickValue {
7
7
  if (at === false || at === 'false')
@@ -59,7 +59,8 @@ export function createClicksContextBase(
59
59
  // Convert maxMap to reactive
60
60
  maxMap = shallowReactive(maxMap)
61
61
  // Make sure the query is not greater than the total
62
- context.current = current.value
62
+ if (!isReadonly(current))
63
+ context.current = current.value
63
64
  })
64
65
  onUnmounted(() => {
65
66
  isMounted.value = false
@@ -160,11 +161,11 @@ export function createClicksContextBase(
160
161
 
161
162
  export function createFixedClicks(
162
163
  route?: SlideRoute | undefined,
163
- currentInit = 0,
164
+ currentInit: MaybeRefOrGetter<number> = 0,
164
165
  ): ClicksContext {
165
166
  const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
166
167
  return createClicksContextBase(
167
- ref(Math.max(currentInit, clicksStart)),
168
+ computed(() => Math.max(toValue(currentInit), clicksStart)),
168
169
  clicksStart,
169
170
  route?.meta?.clicks,
170
171
  )
@@ -7,6 +7,7 @@ import { injectionCurrentPage, injectionFrontmatter, injectionRenderContext, inj
7
7
  import { makeId } from '../logic/utils'
8
8
  import { activeDragElement } from '../state'
9
9
  import { directiveInject } from '../utils'
10
+ import { useNav } from './useNav'
10
11
  import { useSlideBounds } from './useSlideBounds'
11
12
  import { useDynamicSlideInfo } from './useSlideInfo'
12
13
 
@@ -69,25 +70,25 @@ export function useDragElementsUpdater(no: number) {
69
70
 
70
71
  section = type === 'prop'
71
72
  // eslint-disable-next-line regexp/no-super-linear-backtracking
72
- ? section.replace(/<(v-?drag-?\w*)(.*?)(\/)?>/gi, (full, tag, attrs, selfClose, index) => {
73
- if (index === idx) {
74
- replaced = true
75
- const posMatch = attrs.match(/pos=".*?"/)
76
- if (!posMatch)
77
- return `<${tag}${ensureSuffix(' ', attrs)}pos="${posStr}"${selfClose}>`
78
- const start = posMatch.index
79
- const end = start + posMatch[0].length
80
- return `<${tag}${attrs.slice(0, start)}pos="${posStr}"${attrs.slice(end)}${selfClose}>`
81
- }
82
- return full
83
- })
73
+ ? section.replace(/<(v-?drag-?\w*)(.*?)(\/)?>/gi, (full, tag, attrs, selfClose = '', index) => {
74
+ if (index === idx) {
75
+ replaced = true
76
+ const posMatch = attrs.match(/pos=".*?"/)
77
+ if (!posMatch)
78
+ return `<${tag}${ensureSuffix(' ', attrs)}pos="${posStr}"${selfClose}>`
79
+ const start = posMatch.index
80
+ const end = start + posMatch[0].length
81
+ return `<${tag}${attrs.slice(0, start)}pos="${posStr}"${attrs.slice(end)}${selfClose}>`
82
+ }
83
+ return full
84
+ })
84
85
  : section.replace(/(?<![</\w])v-drag(?:=".*?")?/gi, (full, index) => {
85
- if (index === idx) {
86
- replaced = true
87
- return `v-drag="${posStr}"`
88
- }
89
- return full
90
- })
86
+ if (index === idx) {
87
+ replaced = true
88
+ return `v-drag="${posStr}"`
89
+ }
90
+ return full
91
+ })
91
92
 
92
93
  if (!replaced)
93
94
  throw new Error(`[Slidev] VDrag Element ${id} is not found in the markdown source`)
@@ -127,7 +128,8 @@ export function useDragElement(directive: DirectiveBinding | null, posRaw?: stri
127
128
  const scale = inject(injectionSlideScale) ?? ref(1)
128
129
  const zoom = inject(injectionSlideZoom) ?? ref(1)
129
130
  const { left: slideLeft, top: slideTop, stop: stopWatchBounds } = useSlideBounds(inject(injectionSlideElement) ?? ref())
130
- const enabled = ['slide', 'presenter'].includes(renderContext.value)
131
+ const { isPrintMode } = useNav()
132
+ const enabled = ['slide', 'presenter'].includes(renderContext.value) && !isPrintMode.value
131
133
 
132
134
  let dataSource: DragElementDataSource = directive ? 'directive' : 'prop'
133
135
  let dragId: string = makeId()
@@ -180,16 +182,16 @@ export function useDragElement(directive: DirectiveBinding | null, posRaw?: stri
180
182
  const configuredHeight = ref(pos[3] ?? 0)
181
183
  const height = autoHeight
182
184
  ? computed({
183
- get: () => (autoHeight ? actualHeight.value : configuredHeight.value) || 0,
184
- set: v => !autoHeight && (configuredHeight.value = v),
185
- })
185
+ get: () => (autoHeight ? actualHeight.value : configuredHeight.value) || 0,
186
+ set: v => !autoHeight && (configuredHeight.value = v),
187
+ })
186
188
  : configuredHeight
187
189
  const configuredY0 = autoHeight ? ref(pos[1]) : ref(pos[1] + pos[3] / 2)
188
190
  const y0 = autoHeight
189
191
  ? computed({
190
- get: () => configuredY0.value + height.value / 2,
191
- set: v => configuredY0.value = v - height.value / 2,
192
- })
192
+ get: () => configuredY0.value + height.value / 2,
193
+ set: v => configuredY0.value = v - height.value / 2,
194
+ })
193
195
  : configuredY0
194
196
 
195
197
  const containerStyle = computed(() => {
@@ -266,10 +268,14 @@ export function useDragElement(directive: DirectiveBinding | null, posRaw?: stri
266
268
  state.stopDragging()
267
269
  },
268
270
  startDragging(): void {
271
+ if (!enabled)
272
+ return
269
273
  updateBounds()
270
274
  activeDragElement.value = state
271
275
  },
272
276
  stopDragging(): void {
277
+ if (!enabled)
278
+ return
273
279
  if (activeDragElement.value === state)
274
280
  activeDragElement.value = null
275
281
  },
@@ -3,10 +3,10 @@ import type { ComputedRef, Ref, TransitionGroupProps, WritableComputedRef } from
3
3
  import type { RouteLocationNormalized, Router } from 'vue-router'
4
4
  import { slides } from '#slidev/slides'
5
5
  import { clamp } from '@antfu/utils'
6
+ import { parseRangeString } from '@slidev/parser/utils'
6
7
  import { createSharedComposable } from '@vueuse/core'
7
- import { logicOr } from '@vueuse/math'
8
8
  import { computed, ref, watch } from 'vue'
9
- import { useRouter } from 'vue-router'
9
+ import { useRoute, useRouter } from 'vue-router'
10
10
  import { CLICKS_MAX } from '../constants'
11
11
  import { configs } from '../env'
12
12
  import { skipTransition } from '../logic/hmr'
@@ -71,7 +71,7 @@ export interface SlidevContextNavState {
71
71
  router: Router
72
72
  currentRoute: ComputedRef<RouteLocationNormalized>
73
73
  isPrintMode: ComputedRef<boolean>
74
- isPrintWithClicks: ComputedRef<boolean>
74
+ isPrintWithClicks: Ref<boolean>
75
75
  isEmbedded: ComputedRef<boolean>
76
76
  isPlaying: ComputedRef<boolean>
77
77
  isPresenter: ComputedRef<boolean>
@@ -83,6 +83,7 @@ export interface SlidevContextNavState {
83
83
  clicksContext: ComputedRef<ClicksContext>
84
84
  queryClicksRaw: Ref<string>
85
85
  queryClicks: WritableComputedRef<number>
86
+ printRange: Ref<number[]>
86
87
  getPrimaryClicks: (route: SlideRoute) => ClicksContext
87
88
  }
88
89
 
@@ -113,7 +114,7 @@ export function useNavBase(
113
114
  const hasNext = computed(() => currentSlideNo.value < slides.value.length || clicks.value < clicksTotal.value)
114
115
  const hasPrev = computed(() => currentSlideNo.value > 1 || clicks.value > 0)
115
116
 
116
- const currentTransition = computed(() => getCurrentTransition(navDirection.value, currentSlideRoute.value, prevRoute.value))
117
+ const currentTransition = computed(() => isPrint.value ? undefined : getCurrentTransition(navDirection.value, currentSlideRoute.value, prevRoute.value))
117
118
 
118
119
  watch(currentSlideRoute, (next, prev) => {
119
120
  navDirection.value = next.no - prev.no
@@ -191,7 +192,7 @@ export function useNavBase(
191
192
  clicks = clamp(clicks, clicksStart, meta?.__clicksContext?.total ?? CLICKS_MAX)
192
193
  if (force || pageChanged || clicksChanged) {
193
194
  await router?.push({
194
- path: getSlidePath(no, isPresenter.value),
195
+ path: getSlidePath(no, isPresenter.value, router.currentRoute.value.name === 'export'),
195
196
  query: {
196
197
  ...router.currentRoute.value.query,
197
198
  clicks: clicks === 0 ? undefined : clicks.toString(),
@@ -272,24 +273,24 @@ export function useFixedNav(
272
273
 
273
274
  const useNavState = createSharedComposable((): SlidevContextNavState => {
274
275
  const router = useRouter()
276
+ const currentRoute = useRoute()
275
277
 
276
- const currentRoute = computed(() => router.currentRoute.value)
277
278
  const query = computed(() => {
278
279
  // eslint-disable-next-line ts/no-unused-expressions
279
280
  router.currentRoute.value.query
280
281
  return new URLSearchParams(location.search)
281
282
  })
282
- const isPrintMode = computed(() => query.value.has('print'))
283
- const isPrintWithClicks = computed(() => query.value.get('print') === 'clicks')
283
+ const isPrintMode = computed(() => query.value.has('print') || currentRoute.name === 'export')
284
+ const isPrintWithClicks = ref(query.value.get('print') === 'clicks')
284
285
  const isEmbedded = computed(() => query.value.has('embedded'))
285
- const isPlaying = computed(() => currentRoute.value.name === 'play')
286
- const isPresenter = computed(() => currentRoute.value.name === 'presenter')
287
- const isNotesViewer = computed(() => currentRoute.value.name === 'notes')
286
+ const isPlaying = computed(() => currentRoute.name === 'play')
287
+ const isPresenter = computed(() => currentRoute.name === 'presenter')
288
+ const isNotesViewer = computed(() => currentRoute.name === 'notes')
288
289
  const isPresenterAvailable = computed(() => !isPresenter.value && (!configs.remote || query.value.get('password') === configs.remote))
289
- const hasPrimarySlide = logicOr(isPlaying, isPresenter)
290
-
291
- const currentSlideNo = computed(() => hasPrimarySlide.value ? getSlide(currentRoute.value.params.no as string)?.no ?? 1 : 1)
290
+ const hasPrimarySlide = computed(() => !!currentRoute.params.no)
291
+ const currentSlideNo = computed(() => hasPrimarySlide.value ? getSlide(currentRoute.params.no as string)?.no ?? 1 : 1)
292
292
  const currentSlideRoute = computed(() => slides.value[currentSlideNo.value - 1])
293
+ const printRange = ref(parseRangeString(slides.value.length, currentRoute.query.range as string | undefined))
293
294
 
294
295
  const queryClicksRaw = useRouteQuery<string>('clicks', '0')
295
296
 
@@ -342,7 +343,7 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
342
343
 
343
344
  return {
344
345
  router,
345
- currentRoute,
346
+ currentRoute: computed(() => currentRoute),
346
347
  isPrintMode,
347
348
  isPrintWithClicks,
348
349
  isEmbedded,
@@ -356,6 +357,7 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
356
357
  clicksContext,
357
358
  queryClicksRaw,
358
359
  queryClicks,
360
+ printRange,
359
361
  getPrimaryClicks,
360
362
  }
361
363
  })
@@ -0,0 +1,28 @@
1
+ import { useStyleTag } from '@vueuse/core'
2
+ import { computed } from 'vue'
3
+ import { slideHeight, slideWidth } from '../env'
4
+ import { useNav } from './useNav'
5
+
6
+ export function usePrintStyles() {
7
+ const { isPrintMode } = useNav()
8
+
9
+ useStyleTag(computed(() => isPrintMode.value
10
+ ? `
11
+ @page {
12
+ size: ${slideWidth.value}px ${slideHeight.value}px;
13
+ margin: 0px;
14
+ }
15
+
16
+ * {
17
+ transition: none !important;
18
+ transition-duration: 0s !important;
19
+ }`
20
+ : ''))
21
+ }
22
+
23
+ // Monaco uses `<style media="screen" class="monaco-colors">` to apply colors, which will be ignored in print mode.
24
+ export function patchMonacoColors() {
25
+ document.querySelectorAll<HTMLStyleElement>('style.monaco-colors').forEach((el) => {
26
+ el.media = ''
27
+ })
28
+ }
@@ -0,0 +1,20 @@
1
+ import { useInterval } from '@vueuse/core'
2
+ import { computed } from 'vue'
3
+
4
+ export function useTimer() {
5
+ const { counter, isActive, reset, pause, resume } = useInterval(1000, { controls: true })
6
+
7
+ const timer = computed(() => {
8
+ const passed = counter.value
9
+ const sec = Math.floor(passed % 60).toString().padStart(2, '0')
10
+ const min = Math.floor(passed / 60).toString().padStart(2, '0')
11
+ return `${min}:${sec}`
12
+ })
13
+
14
+ return {
15
+ timer,
16
+ isTimerAvctive: isActive,
17
+ resetTimer: reset,
18
+ toggleTimer: () => (isActive.value ? pause() : resume()),
19
+ }
20
+ }
@@ -17,7 +17,7 @@ export function useViewTransition() {
17
17
  const toMeta = getSlide(to.params.no as string)?.meta
18
18
  const fromNo = fromMeta?.slide?.no
19
19
  const toNo = toMeta?.slide?.no
20
- const transitionType = fromNo != null && toNo != null
20
+ const transitionType = fromNo != null && toNo != null && fromNo !== toNo
21
21
  && ((fromNo < toNo ? fromMeta?.transition : toMeta?.transition) ?? configs.transition)
22
22
  if (transitionType !== 'view-transition') {
23
23
  isViewTransition.value = false
@@ -41,7 +41,6 @@ export function useViewTransition() {
41
41
 
42
42
  // Wait for `TransitionGroup` to become normal `div`
43
43
  setTimeout(() => {
44
- // @ts-expect-error missing types
45
44
  document.startViewTransition(() => {
46
45
  changeRoute()
47
46
  return promise
package/constants.ts CHANGED
@@ -45,6 +45,7 @@ export const FRONTMATTER_FIELDS = [
45
45
  'transition',
46
46
  'zoom',
47
47
  'dragPos',
48
+ 'lang',
48
49
  ]
49
50
 
50
51
  export const HEADMATTER_FIELDS = [
@@ -55,6 +56,7 @@ export const HEADMATTER_FIELDS = [
55
56
  'author',
56
57
  'keywords',
57
58
  'presenter',
59
+ 'browserExporter',
58
60
  'download',
59
61
  'exportFilename',
60
62
  'export',
@@ -42,7 +42,7 @@ function onMousedown() {
42
42
  :class="length && props.clicksContext.isMounted ? '' : 'op50'"
43
43
  >
44
44
  <div class="flex gap-0.2 items-center min-w-16 font-mono mr1">
45
- <carbon:cursor-1 text-sm op50 />
45
+ <div class="i-carbon:cursor-1 text-sm op50" />
46
46
  <template v-if="current >= 0 && current !== CLICKS_MAX && active">
47
47
  <div flex-auto />
48
48
  <span text-primary>{{ current }}</span>
@@ -119,7 +119,7 @@ else if (autorun)
119
119
  </div>
120
120
  <div v-if="code.trim()" class="absolute right-1 top-1 max-h-full flex gap-1">
121
121
  <IconButton class="w-8 h-8 max-h-full flex justify-center items-center" title="Run code" @click="triggerRun">
122
- <carbon:play />
122
+ <div class="i-carbon:play" />
123
123
  </IconButton>
124
124
  </div>
125
125
  </template>
@@ -69,12 +69,13 @@ const top = computed(() => {
69
69
  <div v-if="item === 'separator'" :key="index" class="w-full my1 border-t border-main" />
70
70
  <div
71
71
  v-else-if="item.small"
72
- class="p-2 w-[40px] h-[40px] inline-block text-center cursor-pointer rounded"
72
+ class="p-2 w-[40px] h-[40px] inline-block text-center cursor-pointer rounded flex"
73
73
  :class="item.disabled ? `op40` : `hover:bg-active`"
74
74
  :title="(item.label as string)"
75
75
  @click="item.action"
76
76
  >
77
- <component :is="item.icon" />
77
+ <div v-if="typeof item.icon === 'string'" :class="item.icon" class="text-1.2em ma" />
78
+ <component :is="item.icon" v-else />
78
79
  </div>
79
80
  <div
80
81
  v-else
@@ -82,8 +83,9 @@ const top = computed(() => {
82
83
  :class="item.disabled ? `op40` : `hover:bg-active`"
83
84
  @click="item.action"
84
85
  >
85
- <div class="mx-auto">
86
- <component :is="item.icon" />
86
+ <div class="mx-auto flex">
87
+ <div v-if="typeof item.icon === 'string'" :class="item.icon" class="text-1.2em ma" />
88
+ <component :is="item.icon" v-else />
87
89
  </div>
88
90
  <div v-if="typeof item.label === 'string'">
89
91
  {{ item.label }}
@@ -49,7 +49,7 @@ function setBrushColor(color: typeof brush.color) {
49
49
  :initial-y="10"
50
50
  >
51
51
  <IconButton title="Draw with stylus" :class="{ shallow: drawingMode !== 'stylus' }" @click="setDrawingMode('stylus')">
52
- <carbon:pen />
52
+ <div class="i-carbon:pen" />
53
53
  </IconButton>
54
54
  <IconButton title="Draw a line" :class="{ shallow: drawingMode !== 'line' }" @click="setDrawingMode('line')">
55
55
  <svg width="1em" height="1em" class="-mt-0.5" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
@@ -57,16 +57,16 @@ function setBrushColor(color: typeof brush.color) {
57
57
  </svg>
58
58
  </IconButton>
59
59
  <IconButton title="Draw an arrow" :class="{ shallow: drawingMode !== 'arrow' }" @click="setDrawingMode('arrow')">
60
- <carbon:arrow-up-right />
60
+ <div class="i-carbon:arrow-up-right" />
61
61
  </IconButton>
62
62
  <IconButton title="Draw an ellipse" :class="{ shallow: drawingMode !== 'ellipse' }" @click="setDrawingMode('ellipse')">
63
- <carbon:radio-button />
63
+ <div class="i-carbon:radio-button" />
64
64
  </IconButton>
65
65
  <IconButton title="Draw a rectangle" :class="{ shallow: drawingMode !== 'rectangle' }" @click="setDrawingMode('rectangle')">
66
- <carbon:checkbox />
66
+ <div class="i-carbon:checkbox" />
67
67
  </IconButton>
68
68
  <IconButton title="Erase" :class="{ shallow: drawingMode !== 'eraseLine' }" @click="setDrawingMode('eraseLine')">
69
- <carbon:erase />
69
+ <div class="i-carbon:erase" />
70
70
  </IconButton>
71
71
 
72
72
  <VerticalDivider />
@@ -107,19 +107,19 @@ function setBrushColor(color: typeof brush.color) {
107
107
  <VerticalDivider />
108
108
 
109
109
  <IconButton title="Undo" :class="{ disabled: !canUndo }" @click="undo()">
110
- <carbon:undo />
110
+ <div class="i-carbon:undo" />
111
111
  </IconButton>
112
112
  <IconButton title="Redo" :class="{ disabled: !canRedo }" @click="redo()">
113
- <carbon:redo />
113
+ <div class="i-carbon:redo" />
114
114
  </IconButton>
115
115
  <IconButton title="Delete" :class="{ disabled: !canClear }" @click="clear()">
116
- <carbon:trash-can />
116
+ <div class="i-carbon:trash-can" />
117
117
  </IconButton>
118
118
 
119
119
  <VerticalDivider />
120
120
  <IconButton :title="drawingPinned ? 'Unpin drawing' : 'Pin drawing'" :class="{ shallow: !drawingPinned }" @click="drawingPinned = !drawingPinned">
121
- <carbon:pin-filled v-show="drawingPinned" class="transform -rotate-45" />
122
- <carbon:pin v-show="!drawingPinned" />
121
+ <div v-show="drawingPinned" class="i-carbon:pin-filled transform -rotate-45" />
122
+ <div v-show="!drawingPinned" class="i-carbon:pin" />
123
123
  </IconButton>
124
124
  <IconButton
125
125
  v-if="drawingEnabled"
@@ -127,8 +127,8 @@ function setBrushColor(color: typeof brush.color) {
127
127
  :class="{ shallow: !drawingEnabled }"
128
128
  @click="drawingEnabled = !drawingEnabled"
129
129
  >
130
- <carbon:error v-show="drawingPinned" />
131
- <carbon:close-outline v-show="!drawingPinned" />
130
+ <div v-show="drawingPinned" class="i-carbon:error" />
131
+ <div v-show="!drawingPinned" class="i-carbon:close-outline" />
132
132
  </IconButton>
133
133
  </Draggable>
134
134
  </template>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import { useVModel } from '@vueuse/core'
3
+ import { skipExportPdfTip } from '../state'
4
+ import Modal from './Modal.vue'
5
+
6
+ const props = defineProps({
7
+ modelValue: {
8
+ default: false,
9
+ },
10
+ })
11
+
12
+ const emit = defineEmits(['update:modelValue', 'print'])
13
+ const value = useVModel(props, 'modelValue', emit)
14
+
15
+ function print() {
16
+ value.value = false
17
+ emit('print')
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <Modal v-model="value" class="px-6 py-4 flex flex-col gap-2">
23
+ <div class="flex gap-2 text-xl">
24
+ <div class="i-carbon:information my-auto" /> Tips
25
+ </div>
26
+ <div>
27
+ Slidev will open your browser's built-in print dialog to export the slides as PDF. <br>
28
+ In the print dialog, please:
29
+ <ul class="list-disc my-4 pl-4">
30
+ <li>
31
+ Choose "Save as PDF" as the Destination.
32
+ <span class="op-70 text-xs"> (Not "Microsoft Print to PDF") </span>
33
+ </li>
34
+ <li> Choose "Default" as the Margin. </li>
35
+ <li> Toggle on "Print backgrounds". </li>
36
+ </ul>
37
+ <div class="mb-2 op-70 text-sm">
38
+ If you're encountering problems, please try
39
+ <a href="https://sli.dev/builtin/cli#export"> the CLI </a>
40
+ or
41
+ <a href="https://github.com/slidevjs/slidev/issues/new"> open an issue</a>.
42
+ </div>
43
+ <div class="form-check op-70">
44
+ <input
45
+ v-model="skipExportPdfTip"
46
+ name="record-camera"
47
+ type="checkbox"
48
+ >
49
+ <label for="record-camera" @click="skipExportPdfTip = !skipExportPdfTip">Don't show this dialog next time.</label>
50
+ </div>
51
+ </div>
52
+ <div class="flex my-1">
53
+ <button class="cancel" @click="value = false">
54
+ Cancel
55
+ </button>
56
+ <div class="flex-auto" />
57
+ <button @click="print">
58
+ Start
59
+ </button>
60
+ </div>
61
+ </Modal>
62
+ </template>
63
+
64
+ <style scoped>
65
+ button {
66
+ @apply bg-blue-400 text-white px-4 py-1 rounded border-b-2 border-blue-600;
67
+ @apply hover:(bg-blue-500 border-blue-700);
68
+ }
69
+
70
+ button.cancel {
71
+ @apply bg-gray-400 bg-opacity-50 text-white px-4 py-1 rounded border-b-2 border-main;
72
+ @apply hover:(bg-opacity-75 border-opacity-75);
73
+ }
74
+
75
+ a {
76
+ @apply border-current border-b border-dashed hover:text-primary hover:border-solid;
77
+ }
78
+
79
+ .form-check {
80
+ @apply leading-5;
81
+
82
+ * {
83
+ @apply my-auto align-middle;
84
+ }
85
+
86
+ label {
87
+ @apply ml-1 text-sm select-none;
88
+ }
89
+ }
90
+ </style>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ disabled?: boolean
4
+ }>()
5
+
6
+ const value = defineModel<boolean>('modelValue', {
7
+ type: Boolean,
8
+ })
9
+ </script>
10
+
11
+ <template>
12
+ <div border="~ main rounded" flex="~ gap-2 items-center" relative h-5 w-5 p0.5 hover:bg-active p1>
13
+ <div i-ri-check-line :class="value ? '' : 'op0'" />
14
+ <input v-model="value" type="checkbox" absolute inset-0 z-10 opacity-0.1 :disabled="disabled">
15
+ </div>
16
+ </template>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import { Tooltip } from 'floating-vue'
3
+ import { defineEmits, defineProps } from 'vue'
4
+
5
+ defineProps<{
6
+ title: string
7
+ nested?: boolean | number
8
+ div?: boolean
9
+ description?: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ (event: 'reset'): void
14
+ }>()
15
+
16
+ function reset() {
17
+ emit('reset')
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <component :is="div ? 'div' : 'label'" flex="~ row gap-2 items-center" select-none>
23
+ <div w-30 h-10 flex="~ gap-1 items-center">
24
+ <div
25
+ v-if="nested" i-ri-corner-down-right-line op40
26
+ :style="typeof nested === 'number' ? { marginLeft: `${nested * 0.5 + 0.5}rem` } : { marginLeft: '0.25rem' }"
27
+ />
28
+ <div v-if="!description" op75 @dblclick="reset">
29
+ {{ title }}
30
+ </div>
31
+ <Tooltip v-else distance="10">
32
+ <div op75 text-right @dblclick="reset">
33
+ {{ title }}
34
+ </div>
35
+ <template #popper>
36
+ <div text-sm min-w-90 v-html="description" />
37
+ </template>
38
+ </Tooltip>
39
+ </div>
40
+ <slot />
41
+ </component>
42
+ </template>
@@ -1,13 +1,18 @@
1
1
  <script setup lang="ts">
2
- defineProps<{
2
+ import { computed } from 'vue'
3
+
4
+ const props = defineProps<{
3
5
  title: string
4
6
  icon?: string
5
7
  as?: string
8
+ to?: string
6
9
  }>()
10
+
11
+ const type = computed(() => props.as || (props.to ? 'router-link' : 'button'))
7
12
  </script>
8
13
 
9
14
  <template>
10
- <component :is="as || 'button'" class="slidev-icon-btn" :title="title">
15
+ <component :is="type" class="slidev-icon-btn" :title="title" :to="to">
11
16
  <span class="sr-only">{{ title }}</span>
12
17
  <slot>
13
18
  <div :class="icon" />