@slidev/client 0.50.0-beta.9 → 0.51.0-beta.1

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 (57) 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/composables/useTimer.ts +1 -1
  6. package/constants.ts +1 -0
  7. package/internals/Badge.vue +48 -0
  8. package/internals/ClicksSlider.vue +1 -1
  9. package/internals/ContextMenu.vue +1 -1
  10. package/internals/{DevicesList.vue → DevicesSelectors.vue} +12 -4
  11. package/internals/DragControl.vue +1 -1
  12. package/internals/DrawingControls.vue +1 -1
  13. package/internals/ExportPdfTip.vue +90 -0
  14. package/internals/FormCheckbox.vue +16 -0
  15. package/internals/FormItem.vue +41 -0
  16. package/internals/IconButton.vue +7 -2
  17. package/internals/MenuButton.vue +2 -2
  18. package/internals/Modal.vue +1 -1
  19. package/internals/NavControls.vue +21 -9
  20. package/internals/PrintContainer.vue +2 -21
  21. package/internals/PrintSlide.vue +4 -3
  22. package/internals/PrintSlideClick.vue +11 -3
  23. package/internals/QuickOverview.vue +2 -2
  24. package/internals/RecordingControls.vue +2 -2
  25. package/internals/RecordingDialog.vue +4 -14
  26. package/internals/ScreenCaptureMirror.vue +45 -0
  27. package/internals/SegmentControl.vue +29 -0
  28. package/internals/SelectList.vue +1 -5
  29. package/internals/Settings.vue +16 -3
  30. package/internals/SideEditor.vue +1 -1
  31. package/internals/SlidesShow.vue +7 -3
  32. package/internals/SyncControls.vue +73 -0
  33. package/internals/WebCamera.vue +2 -2
  34. package/layouts/error.vue +5 -1
  35. package/logic/color.ts +62 -0
  36. package/logic/dark.ts +11 -0
  37. package/logic/screenshot.ts +61 -0
  38. package/logic/shortcuts.ts +36 -35
  39. package/logic/slides.ts +2 -1
  40. package/modules/v-mark.ts +6 -0
  41. package/package.json +20 -18
  42. package/pages/export.vue +365 -0
  43. package/pages/notes.vue +3 -3
  44. package/pages/overview.vue +1 -1
  45. package/pages/play.vue +1 -4
  46. package/pages/presenter.vue +46 -18
  47. package/pages/print.vue +0 -2
  48. package/setup/monaco.ts +14 -14
  49. package/setup/root.ts +37 -25
  50. package/setup/routes.ts +23 -12
  51. package/state/drawings.ts +5 -1
  52. package/state/index.ts +1 -55
  53. package/state/shared.ts +0 -7
  54. package/state/storage.ts +70 -0
  55. package/styles/index.css +15 -4
  56. package/uno.config.ts +15 -0
  57. package/internals/PrintStyle.vue +0 -16
@@ -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
+ ref(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
 
@@ -70,24 +71,24 @@ export function useDragElementsUpdater(no: number) {
70
71
  section = type === 'prop'
71
72
  // eslint-disable-next-line regexp/no-super-linear-backtracking
72
73
  ? 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
- })
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
+ }
@@ -13,7 +13,7 @@ export function useTimer() {
13
13
 
14
14
  return {
15
15
  timer,
16
- isTimerAvctive: isActive,
16
+ isTimerActive: isActive,
17
17
  resetTimer: reset,
18
18
  toggleTimer: () => (isActive.value ? pause() : resume()),
19
19
  }
package/constants.ts CHANGED
@@ -56,6 +56,7 @@ export const HEADMATTER_FIELDS = [
56
56
  'author',
57
57
  'keywords',
58
58
  'presenter',
59
+ 'browserExporter',
59
60
  'download',
60
61
  'exportFilename',
61
62
  'export',
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import {
4
+ getHashColorFromString,
5
+ getHsla,
6
+ } from '../logic/color'
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ text?: string
11
+ color?: boolean | number
12
+ as?: string
13
+ size?: string
14
+ }>(),
15
+ {
16
+ color: true,
17
+ },
18
+ )
19
+
20
+ const style = computed(() => {
21
+ if (!props.text || props.color === false)
22
+ return {}
23
+ return {
24
+ color: typeof props.color === 'number'
25
+ ? getHsla(props.color)
26
+ : getHashColorFromString(props.text),
27
+ background: typeof props.color === 'number'
28
+ ? getHsla(props.color, 0.1)
29
+ : getHashColorFromString(props.text, 0.1),
30
+ }
31
+ })
32
+
33
+ const sizeClasses = computed(() => {
34
+ switch (props.size || 'sm') {
35
+ case 'sm':
36
+ return 'px-1.5 text-11px leading-1.6em'
37
+ }
38
+ return ''
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <component :is="as || 'span'" ws-nowrap rounded :class="sizeClasses" :style>
44
+ <slot>
45
+ <span v-text="props.text" />
46
+ </slot>
47
+ </component>
48
+ </template>
@@ -87,7 +87,7 @@ function onMousedown() {
87
87
  v-model="current"
88
88
  class="range"
89
89
  type="range" :min="start" :max="total" :step="1"
90
- absolute inset-0 z-10 op0
90
+ absolute inset-0 z-label op0
91
91
  :class="readonly ? 'pointer-events-none' : ''"
92
92
  :style="{ '--thumb-width': `${1 / (length + 1) * 100}%` }"
93
93
  @mousedown="onMousedown"
@@ -61,7 +61,7 @@ const top = computed(() => {
61
61
  v-if="currentContextMenu"
62
62
  ref="container"
63
63
  :style="`left:${left}px;top:${top}px`"
64
- class="fixed z-100 w-60 flex flex-wrap justify-items-start p-1 animate-fade-in animate-duration-100 backdrop-blur bg-main bg-opacity-75! border border-main rounded-md shadow overflow-hidden select-none"
64
+ class="slidev-glass-effect fixed z-context-menu w-60 flex flex-wrap justify-items-start p-1 animate-fade-in animate-duration-100 rounded-md shadow overflow-hidden select-none"
65
65
  @contextmenu.prevent=""
66
66
  @click="closeContextMenu"
67
67
  >
@@ -43,13 +43,21 @@ ensureDevicesListPermissions()
43
43
  </script>
44
44
 
45
45
  <template>
46
- <div class="text-sm">
47
- <SelectList v-model="currentCamera" title="Camera" :items="camerasItems" />
48
- <SelectList v-model="currentMic" title="Microphone" :items="microphonesItems" />
46
+ <div text-sm flex="~ col gap-2">
47
+ <SelectList
48
+ v-model="currentCamera"
49
+ title="Camera"
50
+ :items="camerasItems"
51
+ />
52
+ <SelectList
53
+ v-model="currentMic"
54
+ title="Microphone"
55
+ :items="microphonesItems"
56
+ />
49
57
  <SelectList
50
58
  v-if="mimeTypeItems.length"
51
59
  v-model="mimeType"
52
- title="mimeType"
60
+ title="Video Format"
53
61
  :items="mimeTypeItems"
54
62
  />
55
63
  </div>
@@ -375,7 +375,7 @@ watchEffect(() => {
375
375
  @pointermove="onPointermove"
376
376
  @pointerup="onPointerup"
377
377
  >
378
- <div class="absolute inset-0 z-100 dark:b-gray-400" :class="isArrow ? '' : 'b b-dark'">
378
+ <div class="absolute inset-0 z-nav dark:b-gray-400" :class="isArrow ? '' : 'b b-dark'">
379
379
  <template v-if="!autoHeight">
380
380
  <div v-bind="getCornerProps(true, true)" />
381
381
  <div v-bind="getCornerProps(false, false)" />
@@ -42,7 +42,7 @@ function setBrushColor(color: typeof brush.color) {
42
42
  <template>
43
43
  <Draggable
44
44
  v-if="drawingEnabled || drawingPinned"
45
- class="flex flex-wrap text-xl p-2 gap-1 rounded-md bg-main shadow transition-opacity duration-200 z-20 border border-main"
45
+ class="flex flex-wrap text-xl p-2 gap-1 rounded-md bg-main shadow transition-opacity duration-200 z-nav border border-main"
46
46
  :class="!drawingEnabled && drawingPinned ? 'opacity-40 hover:opacity-90' : ''"
47
47
  storage-key="slidev-drawing-pos"
48
48
  :initial-x="10"
@@ -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 opacity-0.1 :disabled="disabled">
15
+ </div>
16
+ </template>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import { Tooltip } from 'floating-vue'
3
+
4
+ defineProps<{
5
+ title: string
6
+ nested?: boolean | number
7
+ div?: boolean
8
+ description?: string
9
+ }>()
10
+
11
+ const emit = defineEmits<{
12
+ (event: 'reset'): void
13
+ }>()
14
+
15
+ function reset() {
16
+ emit('reset')
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <component :is="div ? 'div' : 'label'" flex="~ row gap-2 items-center" select-none>
22
+ <div w-30 h-10 flex="~ gap-1 items-center">
23
+ <div
24
+ v-if="nested" i-ri-corner-down-right-line op40
25
+ :style="typeof nested === 'number' ? { marginLeft: `${nested * 0.5 + 0.5}rem` } : { marginLeft: '0.25rem' }"
26
+ />
27
+ <div v-if="!description" op75 @dblclick="reset">
28
+ {{ title }}
29
+ </div>
30
+ <Tooltip v-else distance="10">
31
+ <div op75 text-right @dblclick="reset">
32
+ {{ title }}
33
+ </div>
34
+ <template #popper>
35
+ <div text-sm min-w-90 v-html="description" />
36
+ </template>
37
+ </Tooltip>
38
+ </div>
39
+ <slot />
40
+ </component>
41
+ </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" />
@@ -30,8 +30,8 @@ onClickOutside(el, () => {
30
30
  <KeepAlive>
31
31
  <div
32
32
  v-if="value"
33
- class="rounded-md bg-main text-main shadow absolute bottom-10 left-0 z-20"
34
- dark:border="~ main"
33
+ class="bg-main text-main shadow-xl absolute bottom-10 left-0 z-menu"
34
+ border="~ main rounded-md"
35
35
  >
36
36
  <slot name="menu" />
37
37
  </div>
@@ -20,7 +20,7 @@ function onClick() {
20
20
 
21
21
  <template>
22
22
  <KeepAlive>
23
- <div v-if="value" class="fixed top-0 bottom-0 left-0 right-0 grid z-20">
23
+ <div v-if="value" class="fixed top-0 bottom-0 left-0 right-0 grid z-modal">
24
24
  <div
25
25
  bg="black opacity-80"
26
26
  class="absolute top-0 bottom-0 left-0 right-0 -z-1"
@@ -10,6 +10,7 @@ import { downloadPDF } from '../utils'
10
10
  import IconButton from './IconButton.vue'
11
11
  import MenuButton from './MenuButton.vue'
12
12
  import Settings from './Settings.vue'
13
+ import SyncControls from './SyncControls.vue'
13
14
 
14
15
  import VerticalDivider from './VerticalDivider.vue'
15
16
 
@@ -48,7 +49,7 @@ function onMouseLeave() {
48
49
 
49
50
  const barStyle = computed(() => props.persist
50
51
  ? 'text-$slidev-controls-foreground bg-transparent'
51
- : 'rounded-md bg-main shadow dark:border dark:border-main')
52
+ : 'rounded-md bg-main shadow-xl border border-main')
52
53
 
53
54
  const RecordingControls = shallowRef<any>()
54
55
  if (__SLIDEV_FEATURE_RECORD__)
@@ -58,7 +59,7 @@ if (__SLIDEV_FEATURE_RECORD__)
58
59
  <template>
59
60
  <nav ref="root" class="flex flex-col">
60
61
  <div
61
- class="flex flex-wrap-reverse text-xl gap-0.5 p-1 lg:gap-1 lg:p-2"
62
+ class="flex flex-wrap-reverse text-xl gap-0.5 p-1 lg:p-2"
62
63
  :class="barStyle"
63
64
  @mouseleave="onMouseLeave"
64
65
  >
@@ -130,18 +131,20 @@ if (__SLIDEV_FEATURE_RECORD__)
130
131
  >
131
132
  <div class="i-carbon:text-annotation-toggle" />
132
133
  </IconButton>
133
-
134
- <IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial" @click="togglePresenterLayout">
135
- <div class="i-carbon:template" />
136
- {{ presenterLayout }}
137
- </IconButton>
138
134
  </template>
135
+
139
136
  <template v-if="!__DEV__">
140
137
  <IconButton v-if="configs.download" title="Download as PDF" @click="downloadPDF">
141
138
  <div class="i-carbon:download" />
142
139
  </IconButton>
143
140
  </template>
144
141
 
142
+ <template v-if="__SLIDEV_FEATURE_BROWSER_EXPORTER__ && !isEmbedded && !isPresenter">
143
+ <IconButton title="Browser Exporter" to="/export">
144
+ <div class="i-carbon:document-pdf" />
145
+ </IconButton>
146
+ </template>
147
+
145
148
  <IconButton
146
149
  v-if="!isPresenter && configs.info && !isEmbedded"
147
150
  title="Show info"
@@ -150,10 +153,19 @@ if (__SLIDEV_FEATURE_RECORD__)
150
153
  <div class="i-carbon:information" />
151
154
  </IconButton>
152
155
 
153
- <template v-if="!isPresenter && !isEmbedded">
156
+ <template v-if="!isEmbedded">
157
+ <VerticalDivider />
158
+
159
+ <IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial flex items-center" @click="togglePresenterLayout">
160
+ <div class="i-carbon:template" />
161
+ {{ presenterLayout }}
162
+ </IconButton>
163
+
164
+ <SyncControls v-if="__SLIDEV_FEATURE_PRESENTER__" />
165
+
154
166
  <MenuButton>
155
167
  <template #button>
156
- <IconButton title="Adjust settings">
168
+ <IconButton title="More Options">
157
169
  <div class="i-carbon:settings-adjust" />
158
170
  </IconButton>
159
171
  </template>