@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.
- package/builtin/KaTexBlockWrapper.vue +1 -1
- package/builtin/Monaco.vue +1 -0
- package/builtin/TocList.vue +4 -1
- package/builtin/Tweet.vue +1 -1
- package/composables/useClicks.ts +6 -5
- package/composables/useDragElements.ts +31 -25
- package/composables/useNav.ts +17 -15
- package/composables/usePrintStyles.ts +28 -0
- package/composables/useTimer.ts +20 -0
- package/composables/useViewTransition.ts +1 -2
- package/constants.ts +2 -0
- package/internals/ClicksSlider.vue +1 -1
- package/internals/CodeRunner.vue +1 -1
- package/internals/ContextMenu.vue +6 -4
- package/internals/DrawingControls.vue +12 -12
- package/internals/ExportPdfTip.vue +90 -0
- package/internals/FormCheckbox.vue +16 -0
- package/internals/FormItem.vue +42 -0
- package/internals/IconButton.vue +7 -2
- package/internals/NavControls.vue +22 -16
- package/internals/PrintContainer.vue +2 -21
- package/internals/PrintSlide.vue +4 -3
- package/internals/PrintSlideClick.vue +11 -3
- package/internals/QuickOverview.vue +2 -2
- package/internals/RecordingControls.vue +4 -4
- package/internals/RecordingDialog.vue +1 -1
- package/internals/SelectList.vue +5 -3
- package/internals/Settings.vue +5 -2
- package/internals/SideEditor.vue +6 -6
- package/internals/SlideContainer.vue +6 -7
- package/internals/SlideWrapper.vue +1 -0
- package/internals/SlidesShow.vue +7 -3
- package/layouts/error.vue +5 -1
- package/logic/screenshot.ts +61 -0
- package/logic/shortcuts.ts +36 -35
- package/logic/slides.ts +2 -1
- package/logic/utils.ts +0 -23
- package/main.ts +7 -3
- package/modules/v-mark.ts +6 -0
- package/package.json +28 -26
- package/pages/entry.vue +4 -4
- package/pages/export.vue +369 -0
- package/pages/notes.vue +4 -4
- package/pages/overview.vue +3 -3
- package/pages/play.vue +5 -6
- package/pages/presenter.vue +30 -22
- package/pages/print.vue +0 -2
- package/setup/context-menu.ts +12 -21
- package/setup/monaco.ts +24 -28
- package/setup/root.ts +6 -2
- package/setup/routes.ts +23 -12
- package/setup/shortcuts.ts +1 -2
- package/state/index.ts +2 -1
- package/styles/index.css +8 -3
- package/uno.config.ts +3 -0
- 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'
|
package/builtin/Monaco.vue
CHANGED
package/builtin/TocList.vue
CHANGED
|
@@ -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>
|
package/composables/useClicks.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
},
|
package/composables/useNav.ts
CHANGED
|
@@ -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:
|
|
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 =
|
|
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.
|
|
286
|
-
const isPresenter = computed(() => currentRoute.
|
|
287
|
-
const isNotesViewer = computed(() => currentRoute.
|
|
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 =
|
|
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>
|
package/internals/CodeRunner.vue
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
122
|
-
<
|
|
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
|
-
<
|
|
131
|
-
<
|
|
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>
|
package/internals/IconButton.vue
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
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="
|
|
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" />
|