@slidev/client 0.48.0-beta.12 → 0.48.0-beta.14
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/ShikiMagicMove.vue +48 -0
- package/composables/useClicks.ts +21 -12
- package/context.ts +5 -2
- package/internals/Controls.vue +2 -2
- package/internals/DrawingLayer.vue +2 -3
- package/internals/IconButton.vue +3 -2
- package/internals/NavControls.vue +2 -2
- package/internals/NoteDisplay.vue +85 -20
- package/internals/NoteEditor.vue +10 -10
- package/internals/NoteStatic.vue +3 -2
- package/internals/OverviewClicksSlider.vue +4 -5
- package/internals/PrintSlide.vue +8 -2
- package/internals/{SlidesOverview.vue → QuickOverview.vue} +11 -10
- package/internals/RecordingControls.vue +1 -1
- package/package.json +5 -4
- package/pages/notes.vue +2 -0
- package/pages/overview.vue +26 -11
- package/pages/play.vue +4 -4
- package/pages/presenter/print.vue +4 -1
- package/pages/presenter.vue +43 -49
- package/state/index.ts +1 -1
- package/styles/index.css +20 -4
- /package/internals/{Editor.vue → SideEditor.vue} +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
|
|
3
|
+
import type { KeyedTokensInfo } from 'shiki-magic-move/types'
|
|
4
|
+
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
|
5
|
+
import { useSlideContext } from '../context'
|
|
6
|
+
import { makeId } from '../logic/utils'
|
|
7
|
+
|
|
8
|
+
import 'shiki-magic-move/style.css'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
steps: KeyedTokensInfo[]
|
|
12
|
+
at?: string | number
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const { $clicksContext: clicks, $scale: scale } = useSlideContext()
|
|
16
|
+
const id = makeId()
|
|
17
|
+
const index = ref(0)
|
|
18
|
+
|
|
19
|
+
onUnmounted(() => {
|
|
20
|
+
clicks!.unregister(id)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
onMounted(() => {
|
|
24
|
+
if (!clicks || clicks.disabled)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
const { start, end, delta } = clicks.resolve(props.at || '+1', props.steps.length - 1)
|
|
28
|
+
clicks.register(id, { max: end, delta })
|
|
29
|
+
|
|
30
|
+
watchEffect(() => {
|
|
31
|
+
if (clicks.disabled)
|
|
32
|
+
index.value = props.steps.length - 1
|
|
33
|
+
else
|
|
34
|
+
index.value = Math.min(Math.max(0, clicks.current - start + 1), props.steps.length - 1)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="slidev-code-wrapper slidev-code-magic-move">
|
|
41
|
+
<ShikiMagicMovePrecompiled
|
|
42
|
+
class="slidev-code relative shiki"
|
|
43
|
+
:steps="steps"
|
|
44
|
+
:step="index"
|
|
45
|
+
:options="{ globalScale: scale }"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
package/composables/useClicks.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { sum } from '@antfu/utils'
|
|
2
2
|
import type { ClicksContext } from '@slidev/types'
|
|
3
3
|
import type { Ref } from 'vue'
|
|
4
|
-
import { ref, shallowReactive } from 'vue'
|
|
4
|
+
import { computed, ref, shallowReactive } from 'vue'
|
|
5
5
|
import type { RouteRecordRaw } from 'vue-router'
|
|
6
6
|
import { currentRoute, isPrintMode, isPrintWithClicks, queryClicks, routeForceRefresh } from '../logic/nav'
|
|
7
7
|
import { normalizeAtProp } from '../logic/utils'
|
|
8
8
|
import { CLICKS_MAX } from '../constants'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
* @internal
|
|
12
|
-
*/
|
|
13
|
-
export function useClicksContextBase(getCurrent: () => number, clicksOverrides?: number): ClicksContext {
|
|
10
|
+
function useClicksContextBase(current: Ref<number>, clicksOverrides?: number): ClicksContext {
|
|
14
11
|
const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
|
|
15
12
|
const map: ClicksContext['map'] = shallowReactive(new Map())
|
|
16
13
|
|
|
@@ -19,7 +16,10 @@ export function useClicksContextBase(getCurrent: () => number, clicksOverrides?:
|
|
|
19
16
|
return isPrintMode.value && !isPrintWithClicks.value
|
|
20
17
|
},
|
|
21
18
|
get current() {
|
|
22
|
-
return
|
|
19
|
+
return current.value
|
|
20
|
+
},
|
|
21
|
+
set current(value) {
|
|
22
|
+
current.value = value
|
|
23
23
|
},
|
|
24
24
|
relativeOffsets,
|
|
25
25
|
map,
|
|
@@ -67,9 +67,11 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
67
67
|
if (route?.meta?.__clicksContext)
|
|
68
68
|
return route.meta.__clicksContext
|
|
69
69
|
const thisPath = +(route?.path ?? CLICKS_MAX)
|
|
70
|
-
const
|
|
71
|
-
()
|
|
72
|
-
const currentPath = +(currentRoute.value?.path ??
|
|
70
|
+
const current = computed({
|
|
71
|
+
get() {
|
|
72
|
+
const currentPath = +(currentRoute.value?.path ?? Number.NaN)
|
|
73
|
+
if (!currentPath || Number.isNaN(currentPath))
|
|
74
|
+
return 0
|
|
73
75
|
if (currentPath === thisPath)
|
|
74
76
|
return queryClicks.value
|
|
75
77
|
else if (currentPath > thisPath)
|
|
@@ -77,6 +79,14 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
77
79
|
else
|
|
78
80
|
return 0
|
|
79
81
|
},
|
|
82
|
+
set(v) {
|
|
83
|
+
const currentPath = +(currentRoute.value?.path ?? Number.NaN)
|
|
84
|
+
if (currentPath === thisPath)
|
|
85
|
+
queryClicks.value = v
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
const context = useClicksContextBase(
|
|
89
|
+
current,
|
|
80
90
|
route?.meta?.clicks,
|
|
81
91
|
)
|
|
82
92
|
if (route?.meta)
|
|
@@ -84,7 +94,6 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
84
94
|
return context
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0):
|
|
88
|
-
|
|
89
|
-
return [current, useClicksContextBase(() => current.value, route?.meta?.clicks)]
|
|
97
|
+
export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0): ClicksContext {
|
|
98
|
+
return useClicksContextBase(ref(currentInit), route?.meta?.clicks)
|
|
90
99
|
}
|
package/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shallowRef, toRef } from 'vue'
|
|
1
|
+
import { ref, shallowRef, toRef } from 'vue'
|
|
2
2
|
import { injectLocal, objectOmit, provideLocal } from '@vueuse/core'
|
|
3
3
|
import { useFixedClicks } from './composables/useClicks'
|
|
4
4
|
import {
|
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
injectionFrontmatter,
|
|
10
10
|
injectionRenderContext,
|
|
11
11
|
injectionRoute,
|
|
12
|
+
injectionSlideScale,
|
|
12
13
|
injectionSlidevContext,
|
|
13
14
|
} from './constants'
|
|
14
15
|
|
|
15
|
-
const clicksContextFallback = shallowRef(useFixedClicks()
|
|
16
|
+
const clicksContextFallback = shallowRef(useFixedClicks())
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Get the current slide context, should be called inside the setup function of a component inside slide
|
|
@@ -26,6 +27,7 @@ export function useSlideContext() {
|
|
|
26
27
|
const $renderContext = injectLocal(injectionRenderContext)!
|
|
27
28
|
const $frontmatter = injectLocal(injectionFrontmatter, {})
|
|
28
29
|
const $route = injectLocal(injectionRoute, undefined)
|
|
30
|
+
const $scale = injectLocal(injectionSlideScale, ref(1))!
|
|
29
31
|
|
|
30
32
|
return {
|
|
31
33
|
$slidev,
|
|
@@ -36,6 +38,7 @@ export function useSlideContext() {
|
|
|
36
38
|
$route,
|
|
37
39
|
$renderContext,
|
|
38
40
|
$frontmatter,
|
|
41
|
+
$scale,
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
package/internals/Controls.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { shallowRef } from 'vue'
|
|
3
3
|
import { showInfoDialog, showOverview, showRecordingDialog } from '../state'
|
|
4
4
|
import { configs } from '../env'
|
|
5
|
-
import
|
|
5
|
+
import QuickOverview from './QuickOverview.vue'
|
|
6
6
|
import InfoDialog from './InfoDialog.vue'
|
|
7
7
|
import Goto from './Goto.vue'
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ if (__SLIDEV_FEATURE_RECORD__) {
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<
|
|
18
|
+
<QuickOverview v-model="showOverview" />
|
|
19
19
|
<Goto />
|
|
20
20
|
<WebCamera v-if="WebCamera" />
|
|
21
21
|
<RecordingDialog v-if="RecordingDialog" v-model="showRecordingDialog" />
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import { injectLocal } from '@vueuse/core'
|
|
4
3
|
import { drauu, drawingEnabled, loadCanvas } from '../logic/drawings'
|
|
5
|
-
import {
|
|
4
|
+
import { useSlideContext } from '../context'
|
|
6
5
|
|
|
7
|
-
const scale =
|
|
6
|
+
const scale = useSlideContext().$scale
|
|
8
7
|
const svg = ref<SVGSVGElement>()
|
|
9
8
|
|
|
10
9
|
onMounted(() => {
|
package/internals/IconButton.vue
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
defineProps<{
|
|
3
3
|
title: string
|
|
4
4
|
icon?: string
|
|
5
|
+
as?: string
|
|
5
6
|
}>()
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<template>
|
|
9
|
-
<button class="slidev-icon-btn" :title="title" v-bind="$attrs">
|
|
10
|
+
<component :is="as || 'button'" class="slidev-icon-btn" :title="title" v-bind="$attrs">
|
|
10
11
|
<span class="sr-only">{{ title }}</span>
|
|
11
12
|
<slot>
|
|
12
13
|
<div :class="icon" />
|
|
13
14
|
</slot>
|
|
14
|
-
</
|
|
15
|
+
</component>
|
|
15
16
|
</template>
|
|
@@ -114,13 +114,13 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
114
114
|
<IconButton
|
|
115
115
|
v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__"
|
|
116
116
|
:title="showEditor ? 'Hide editor' : 'Show editor'"
|
|
117
|
-
class="
|
|
117
|
+
class="lt-md:hidden"
|
|
118
118
|
@click="showEditor = !showEditor"
|
|
119
119
|
>
|
|
120
120
|
<carbon:text-annotation-toggle />
|
|
121
121
|
</IconButton>
|
|
122
122
|
|
|
123
|
-
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" @click="togglePresenterLayout">
|
|
123
|
+
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial" @click="togglePresenterLayout">
|
|
124
124
|
<carbon:template />
|
|
125
125
|
{{ presenterLayout }}
|
|
126
126
|
</IconButton>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import type { ClicksContext } from '@slidev/types'
|
|
3
4
|
import { CLICKS_MAX } from '../constants'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
@@ -7,45 +8,103 @@ const props = defineProps<{
|
|
|
7
8
|
noteHtml?: string
|
|
8
9
|
note?: string
|
|
9
10
|
placeholder?: string
|
|
10
|
-
|
|
11
|
+
clicksContext?: ClicksContext
|
|
12
|
+
autoScroll?: boolean
|
|
11
13
|
}>()
|
|
12
14
|
|
|
13
15
|
defineEmits(['click'])
|
|
14
16
|
|
|
15
|
-
const withClicks = computed(() => props.
|
|
17
|
+
const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
|
|
16
18
|
const noteDisplay = ref<HTMLElement | null>(null)
|
|
17
19
|
|
|
20
|
+
const CLASS_FADE = 'slidev-note-fade'
|
|
21
|
+
const CLASS_MARKER = 'slidev-note-click-mark'
|
|
22
|
+
|
|
18
23
|
function highlightNote() {
|
|
19
|
-
if (!noteDisplay.value || !withClicks.value || props.
|
|
24
|
+
if (!noteDisplay.value || !withClicks.value || props.clicksContext?.current == null)
|
|
20
25
|
return
|
|
21
26
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const disabled = +props.clicks < 0 || +props.clicks >= CLICKS_MAX
|
|
27
|
+
const current = +props.clicksContext?.current ?? CLICKS_MAX
|
|
28
|
+
const disabled = current < 0 || current >= CLICKS_MAX
|
|
25
29
|
if (disabled) {
|
|
26
|
-
|
|
30
|
+
Array.from(noteDisplay.value.querySelectorAll('*'))
|
|
31
|
+
.forEach(el => el.classList.remove(CLASS_FADE))
|
|
27
32
|
return
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
const nodeToIgnores = new Set<Element>()
|
|
36
|
+
function ignoreParent(node: Element) {
|
|
37
|
+
if (!node || node === noteDisplay.value)
|
|
38
|
+
return
|
|
39
|
+
nodeToIgnores.add(node)
|
|
40
|
+
if (node.parentElement)
|
|
41
|
+
ignoreParent(node.parentElement)
|
|
42
|
+
}
|
|
31
43
|
|
|
32
|
-
const
|
|
44
|
+
const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
|
|
45
|
+
const markersMap = new Map<number, HTMLElement>()
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
// Convert all sibling text nodes to spans, so we attach classes to them
|
|
48
|
+
for (const marker of markers) {
|
|
49
|
+
const parent = marker.parentElement!
|
|
50
|
+
const clicks = Number(marker.dataset!.clicks)
|
|
51
|
+
markersMap.set(clicks, marker)
|
|
52
|
+
// Ignore the parents of the marker, so the class only applies to the children
|
|
53
|
+
ignoreParent(parent)
|
|
54
|
+
Array.from(parent!.childNodes)
|
|
55
|
+
.forEach((node) => {
|
|
56
|
+
if (node.nodeType === 3) { // text node
|
|
57
|
+
const span = document.createElement('span')
|
|
58
|
+
span.textContent = node.textContent
|
|
59
|
+
parent.insertBefore(span, node)
|
|
60
|
+
node.remove()
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
const children = Array.from(noteDisplay.value.querySelectorAll('*'))
|
|
65
|
+
|
|
66
|
+
let count = 0
|
|
37
67
|
|
|
38
|
-
|
|
39
|
-
|
|
68
|
+
// Segmenting notes by clicks
|
|
69
|
+
const segments = new Map<number, Element[]>()
|
|
70
|
+
for (const child of children) {
|
|
71
|
+
if (!segments.has(count))
|
|
72
|
+
segments.set(count, [])
|
|
73
|
+
segments.get(count)!.push(child)
|
|
74
|
+
// Update count when reach marker
|
|
75
|
+
if (child.classList.contains(CLASS_MARKER))
|
|
40
76
|
count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
|
|
41
77
|
}
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
|
|
79
|
+
// Apply
|
|
80
|
+
for (const [count, els] of segments) {
|
|
81
|
+
els.forEach(el => el.classList.toggle(
|
|
82
|
+
CLASS_FADE,
|
|
83
|
+
nodeToIgnores.has(el)
|
|
84
|
+
? false
|
|
85
|
+
: count !== current,
|
|
86
|
+
))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const [clicks, marker] of markersMap) {
|
|
90
|
+
marker.classList.remove(CLASS_FADE)
|
|
91
|
+
marker.classList.toggle(`${CLASS_MARKER}-past`, clicks < current)
|
|
92
|
+
marker.classList.toggle(`${CLASS_MARKER}-active`, clicks === current)
|
|
93
|
+
marker.classList.toggle(`${CLASS_MARKER}-next`, clicks === current + 1)
|
|
94
|
+
marker.classList.toggle(`${CLASS_MARKER}-future`, clicks > current + 1)
|
|
95
|
+
marker.addEventListener('dblclick', (e) => {
|
|
96
|
+
props.clicksContext!.current = clicks
|
|
97
|
+
e.stopPropagation()
|
|
98
|
+
e.stopImmediatePropagation()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (props.autoScroll && clicks === current)
|
|
102
|
+
marker.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
103
|
+
}
|
|
45
104
|
}
|
|
46
105
|
|
|
47
106
|
watch(
|
|
48
|
-
() => [props.noteHtml, props.
|
|
107
|
+
() => [props.noteHtml, props.clicksContext?.current],
|
|
49
108
|
() => {
|
|
50
109
|
nextTick(() => {
|
|
51
110
|
highlightNote()
|
|
@@ -70,7 +129,7 @@ onMounted(() => {
|
|
|
70
129
|
/>
|
|
71
130
|
<div
|
|
72
131
|
v-else-if="note"
|
|
73
|
-
class="prose overflow-auto outline-none"
|
|
132
|
+
class="prose overflow-auto outline-none slidev-note"
|
|
74
133
|
:class="props.class"
|
|
75
134
|
@click="$emit('click')"
|
|
76
135
|
>
|
|
@@ -78,10 +137,16 @@ onMounted(() => {
|
|
|
78
137
|
</div>
|
|
79
138
|
<div
|
|
80
139
|
v-else
|
|
81
|
-
class="prose overflow-auto outline-none opacity-50 italic select-none"
|
|
140
|
+
class="prose overflow-auto outline-none opacity-50 italic select-none slidev-note"
|
|
82
141
|
:class="props.class"
|
|
83
142
|
@click="$emit('click')"
|
|
84
143
|
>
|
|
85
144
|
<p v-text="props.placeholder || 'No notes.'" />
|
|
86
145
|
</div>
|
|
87
146
|
</template>
|
|
147
|
+
|
|
148
|
+
<style>
|
|
149
|
+
.slidev-note :first-child {
|
|
150
|
+
margin-top: 0;
|
|
151
|
+
}
|
|
152
|
+
</style>
|
package/internals/NoteEditor.vue
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
2
3
|
import { nextTick, ref, watch, watchEffect } from 'vue'
|
|
3
4
|
import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
|
|
5
|
+
import type { ClicksContext } from '@slidev/types'
|
|
4
6
|
import { useDynamicSlideInfo } from '../logic/note'
|
|
5
7
|
import NoteDisplay from './NoteDisplay.vue'
|
|
6
8
|
|
|
@@ -20,8 +22,8 @@ const props = defineProps({
|
|
|
20
22
|
placeholder: {
|
|
21
23
|
default: 'No notes for this slide',
|
|
22
24
|
},
|
|
23
|
-
|
|
24
|
-
type:
|
|
25
|
+
clicksContext: {
|
|
26
|
+
type: Object as PropType<ClicksContext>,
|
|
25
27
|
},
|
|
26
28
|
autoHeight: {
|
|
27
29
|
default: false,
|
|
@@ -81,9 +83,7 @@ function calculateHeight() {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
const inputHeight = ref<number | null>()
|
|
84
|
-
|
|
85
|
-
calculateHeight()
|
|
86
|
-
})
|
|
86
|
+
|
|
87
87
|
watch(
|
|
88
88
|
note,
|
|
89
89
|
() => {
|
|
@@ -91,19 +91,20 @@ watch(
|
|
|
91
91
|
calculateHeight()
|
|
92
92
|
})
|
|
93
93
|
},
|
|
94
|
-
{ flush: 'post' },
|
|
94
|
+
{ flush: 'post', immediate: true },
|
|
95
95
|
)
|
|
96
96
|
</script>
|
|
97
97
|
|
|
98
98
|
<template>
|
|
99
99
|
<NoteDisplay
|
|
100
100
|
v-if="!editing"
|
|
101
|
-
class="
|
|
101
|
+
class="border-transparent border-2"
|
|
102
102
|
:class="[props.class, note ? '' : 'opacity-25 italic select-none']"
|
|
103
103
|
:style="props.style"
|
|
104
104
|
:note="note || placeholder"
|
|
105
105
|
:note-html="info?.noteHTML"
|
|
106
|
-
:clicks="
|
|
106
|
+
:clicks-context="clicksContext"
|
|
107
|
+
:auto-scroll="!autoHeight"
|
|
107
108
|
/>
|
|
108
109
|
<textarea
|
|
109
110
|
v-else
|
|
@@ -114,7 +115,6 @@ watch(
|
|
|
114
115
|
:style="[props.style, inputHeight != null ? { height: `${inputHeight}px` } : {}]"
|
|
115
116
|
:class="props.class"
|
|
116
117
|
:placeholder="placeholder"
|
|
117
|
-
@keydown.esc="
|
|
118
|
-
@focus="editing = true"
|
|
118
|
+
@keydown.esc="editing = false"
|
|
119
119
|
/>
|
|
120
120
|
</template>
|
package/internals/NoteStatic.vue
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { ClicksContext } from 'packages/types'
|
|
2
3
|
import { useSlideInfo } from '../logic/note'
|
|
3
4
|
import NoteDisplay from './NoteDisplay.vue'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
no?: number
|
|
7
8
|
class?: string
|
|
8
|
-
|
|
9
|
+
clicksContext?: ClicksContext
|
|
9
10
|
}>()
|
|
10
11
|
|
|
11
12
|
const { info } = useSlideInfo(props.no)
|
|
@@ -16,6 +17,6 @@ const { info } = useSlideInfo(props.no)
|
|
|
16
17
|
:class="props.class"
|
|
17
18
|
:note="info?.note"
|
|
18
19
|
:note-html="info?.noteHTML"
|
|
19
|
-
:clicks="
|
|
20
|
+
:clicks-context="clicksContext"
|
|
20
21
|
/>
|
|
21
22
|
</template>
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ClicksContext } from '@slidev/types'
|
|
3
|
-
import type { Ref } from 'vue'
|
|
4
3
|
import { computed } from 'vue'
|
|
5
4
|
import { CLICKS_MAX } from '../constants'
|
|
6
5
|
|
|
7
6
|
const props = defineProps<{
|
|
8
|
-
|
|
7
|
+
clicksContext: ClicksContext
|
|
9
8
|
}>()
|
|
10
9
|
|
|
11
|
-
const total = computed(() => props.
|
|
10
|
+
const total = computed(() => props.clicksContext.total)
|
|
12
11
|
const current = computed({
|
|
13
12
|
get() {
|
|
14
|
-
return props.
|
|
13
|
+
return props.clicksContext.current > total.value ? -1 : props.clicksContext.current
|
|
15
14
|
},
|
|
16
15
|
set(value: number) {
|
|
17
16
|
// eslint-disable-next-line vue/no-mutating-props
|
|
18
|
-
props.
|
|
17
|
+
props.clicksContext.current = value
|
|
19
18
|
},
|
|
20
19
|
})
|
|
21
20
|
|
package/internals/PrintSlide.vue
CHANGED
|
@@ -9,7 +9,7 @@ const props = defineProps<{ route: RouteRecordRaw }>()
|
|
|
9
9
|
|
|
10
10
|
const route = computed(() => props.route)
|
|
11
11
|
const nav = useNav(route)
|
|
12
|
-
const clicks0 = useFixedClicks(route.value, 0)
|
|
12
|
+
const clicks0 = useFixedClicks(route.value, 0)
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<template>
|
|
@@ -19,6 +19,12 @@ const clicks0 = useFixedClicks(route.value, 0)[1]
|
|
|
19
19
|
:route="route"
|
|
20
20
|
/>
|
|
21
21
|
<template v-if="!clicks0.disabled">
|
|
22
|
-
<PrintSlideClick
|
|
22
|
+
<PrintSlideClick
|
|
23
|
+
v-for="i of clicks0.total"
|
|
24
|
+
:key="i"
|
|
25
|
+
:clicks-context="useFixedClicks(route, i)"
|
|
26
|
+
:nav="nav"
|
|
27
|
+
:route="route"
|
|
28
|
+
/>
|
|
23
29
|
</template>
|
|
24
30
|
</template>
|
|
@@ -140,7 +140,7 @@ watchEffect(() => {
|
|
|
140
140
|
<SlideWrapper
|
|
141
141
|
:is="route.component"
|
|
142
142
|
v-if="route?.component"
|
|
143
|
-
:clicks-context="useFixedClicks(route, CLICKS_MAX)
|
|
143
|
+
:clicks-context="useFixedClicks(route, CLICKS_MAX)"
|
|
144
144
|
:class="getSlideClass(route)"
|
|
145
145
|
:route="route"
|
|
146
146
|
render-context="overview"
|
|
@@ -164,18 +164,19 @@ watchEffect(() => {
|
|
|
164
164
|
</div>
|
|
165
165
|
</div>
|
|
166
166
|
</Transition>
|
|
167
|
-
<div v-if="value" class="fixed top-4 right-4 text-gray-400 flex items-center gap-
|
|
168
|
-
<
|
|
169
|
-
|
|
167
|
+
<div v-if="value" class="fixed top-4 right-4 text-gray-400 flex flex-col items-center gap-2">
|
|
168
|
+
<IconButton title="Close" class="text-2xl" @click="close">
|
|
169
|
+
<carbon:close />
|
|
170
|
+
</IconButton>
|
|
171
|
+
<IconButton
|
|
172
|
+
as="a"
|
|
173
|
+
title="Slides Overview"
|
|
170
174
|
target="_blank"
|
|
171
|
-
|
|
175
|
+
href="/overview"
|
|
172
176
|
tab-index="-1"
|
|
173
|
-
class="
|
|
177
|
+
class="text-2xl"
|
|
174
178
|
>
|
|
175
|
-
|
|
176
|
-
</RouterLink>
|
|
177
|
-
<IconButton title="Close" class="text-2xl" @click="close">
|
|
178
|
-
<carbon:close />
|
|
179
|
+
<carbon:list-boxes />
|
|
179
180
|
</IconButton>
|
|
180
181
|
</div>
|
|
181
182
|
</template>
|
|
@@ -54,7 +54,7 @@ onMounted(() => {
|
|
|
54
54
|
</IconButton>
|
|
55
55
|
<MenuButton :disabled="recording">
|
|
56
56
|
<template #button>
|
|
57
|
-
<IconButton title="Select recording device" class="h-full !text-sm !px-0">
|
|
57
|
+
<IconButton title="Select recording device" class="h-full !text-sm !px-0 aspect-initial">
|
|
58
58
|
<carbon:chevron-up class="opacity-50" />
|
|
59
59
|
</IconButton>
|
|
60
60
|
</template>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.48.0-beta.
|
|
4
|
+
"version": "0.48.0-beta.14",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
"prettier": "^3.2.5",
|
|
52
52
|
"recordrtc": "^5.6.2",
|
|
53
53
|
"resolve": "^1.22.8",
|
|
54
|
+
"shiki-magic-move": "^0.1.0",
|
|
54
55
|
"unocss": "^0.58.5",
|
|
55
|
-
"vue": "^3.4.
|
|
56
|
+
"vue": "^3.4.20",
|
|
56
57
|
"vue-router": "^4.3.0",
|
|
57
|
-
"@slidev/
|
|
58
|
-
"@slidev/
|
|
58
|
+
"@slidev/types": "0.48.0-beta.14",
|
|
59
|
+
"@slidev/parser": "0.48.0-beta.14"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"vite": "^5.1.4"
|
package/pages/notes.vue
CHANGED
|
@@ -51,6 +51,8 @@ function decreaseFontSize() {
|
|
|
51
51
|
:note="currentRoute?.meta?.slide?.note"
|
|
52
52
|
:note-html="currentRoute?.meta?.slide?.noteHTML"
|
|
53
53
|
:placeholder="`No notes for Slide ${pageNo}.`"
|
|
54
|
+
:clicks-context="currentRoute?.meta?.__clicksContext"
|
|
55
|
+
:auto-scroll="true"
|
|
54
56
|
/>
|
|
55
57
|
</div>
|
|
56
58
|
<div class="flex-none border-t border-main">
|
package/pages/overview.vue
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Ref } from 'vue'
|
|
3
2
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
4
3
|
import { useHead } from '@unhead/vue'
|
|
5
4
|
import type { RouteRecordRaw } from 'vue-router'
|
|
6
5
|
import type { ClicksContext } from 'packages/types'
|
|
7
|
-
import { themeVars } from '../env'
|
|
8
|
-
import { rawRoutes } from '../logic/nav'
|
|
6
|
+
import { configs, themeVars } from '../env'
|
|
7
|
+
import { openInEditor, rawRoutes } from '../logic/nav'
|
|
9
8
|
import { useFixedClicks } from '../composables/useClicks'
|
|
10
9
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
11
10
|
import { getSlideClass } from '../utils'
|
|
@@ -19,8 +18,9 @@ import { CLICKS_MAX } from '../constants'
|
|
|
19
18
|
|
|
20
19
|
const cardWidth = 450
|
|
21
20
|
|
|
21
|
+
const slideTitle = configs.titleTemplate.replace('%s', configs.title || 'Slidev')
|
|
22
22
|
useHead({
|
|
23
|
-
title:
|
|
23
|
+
title: `Overview - ${slideTitle}`,
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
const blocks: Map<number, HTMLElement> = reactive(new Map())
|
|
@@ -30,8 +30,8 @@ const wordCounts = computed(() => rawRoutes.map(route => wordCount(route.meta?.s
|
|
|
30
30
|
const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
|
|
31
31
|
const totalClicks = computed(() => rawRoutes.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
|
|
32
32
|
|
|
33
|
-
const clicksContextMap = new WeakMap<RouteRecordRaw,
|
|
34
|
-
function
|
|
33
|
+
const clicksContextMap = new WeakMap<RouteRecordRaw, ClicksContext>()
|
|
34
|
+
function getClicksContext(route: RouteRecordRaw) {
|
|
35
35
|
// We create a local clicks context to calculate the total clicks of the slide
|
|
36
36
|
if (!clicksContextMap.has(route))
|
|
37
37
|
clicksContextMap.set(route, useFixedClicks(route, CLICKS_MAX))
|
|
@@ -39,7 +39,7 @@ function getClickContext(route: RouteRecordRaw) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function getSlideClicks(route: RouteRecordRaw) {
|
|
42
|
-
return route.meta?.clicks ||
|
|
42
|
+
return route.meta?.clicks || getClicksContext(route)?.total
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function wordCount(str: string) {
|
|
@@ -132,10 +132,25 @@ onMounted(() => {
|
|
|
132
132
|
:ref="el => blocks.set(idx, el as any)"
|
|
133
133
|
class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
|
|
134
134
|
>
|
|
135
|
-
<div class="select-none w-13 text-right my4">
|
|
135
|
+
<div class="select-none w-13 text-right my4 flex flex-col gap-1 items-end">
|
|
136
136
|
<div class="text-3xl op20 mb2">
|
|
137
137
|
{{ idx + 1 }}
|
|
138
138
|
</div>
|
|
139
|
+
<IconButton
|
|
140
|
+
class="mr--3 op0 group-hover:op80"
|
|
141
|
+
title="Play in new tab"
|
|
142
|
+
@click="openSlideInNewTab(route.path)"
|
|
143
|
+
>
|
|
144
|
+
<carbon:presentation-file />
|
|
145
|
+
</IconButton>
|
|
146
|
+
<IconButton
|
|
147
|
+
v-if="route.meta?.slide"
|
|
148
|
+
class="mr--3 op0 group-hover:op80"
|
|
149
|
+
title="Open in editor"
|
|
150
|
+
@click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
|
|
151
|
+
>
|
|
152
|
+
<carbon:cics-program />
|
|
153
|
+
</IconButton>
|
|
139
154
|
</div>
|
|
140
155
|
<div class="flex flex-col gap-2 my5">
|
|
141
156
|
<div
|
|
@@ -152,7 +167,7 @@ onMounted(() => {
|
|
|
152
167
|
<SlideWrapper
|
|
153
168
|
:is="route.component"
|
|
154
169
|
v-if="route?.component"
|
|
155
|
-
:clicks-context="
|
|
170
|
+
:clicks-context="getClicksContext(route)"
|
|
156
171
|
:class="getSlideClass(route)"
|
|
157
172
|
:route="route"
|
|
158
173
|
render-context="overview"
|
|
@@ -163,7 +178,7 @@ onMounted(() => {
|
|
|
163
178
|
<OverviewClicksSlider
|
|
164
179
|
v-if="getSlideClicks(route)"
|
|
165
180
|
mt-2
|
|
166
|
-
:
|
|
181
|
+
:clicks-context="getClicksContext(route)"
|
|
167
182
|
class="w-full"
|
|
168
183
|
/>
|
|
169
184
|
</div>
|
|
@@ -182,7 +197,7 @@ onMounted(() => {
|
|
|
182
197
|
class="max-w-250 w-250 text-lg rounded p3"
|
|
183
198
|
:auto-height="true"
|
|
184
199
|
:editing="edittingNote === idx"
|
|
185
|
-
:clicks="
|
|
200
|
+
:clicks-context="getClicksContext(route)"
|
|
186
201
|
@dblclick="edittingNote !== idx ? edittingNote = idx : null"
|
|
187
202
|
@update:editing="edittingNote = null"
|
|
188
203
|
/>
|
package/pages/play.vue
CHANGED
|
@@ -31,9 +31,9 @@ useSwipeControls(root)
|
|
|
31
31
|
|
|
32
32
|
const persistNav = computed(() => isScreenVertical.value || showEditor.value)
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const SideEditor = shallowRef<any>()
|
|
35
35
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
36
|
-
import('../internals/
|
|
36
|
+
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
37
37
|
|
|
38
38
|
const DrawingControls = shallowRef<any>()
|
|
39
39
|
if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
@@ -70,8 +70,8 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
70
70
|
</template>
|
|
71
71
|
</SlideContainer>
|
|
72
72
|
|
|
73
|
-
<template v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ &&
|
|
74
|
-
<
|
|
73
|
+
<template v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && SideEditor && showEditor">
|
|
74
|
+
<SideEditor :resize="true" />
|
|
75
75
|
</template>
|
|
76
76
|
</div>
|
|
77
77
|
<Controls />
|
|
@@ -56,7 +56,10 @@ const slidesWithNote = computed(() => rawRoutes
|
|
|
56
56
|
<div class="flex-auto" />
|
|
57
57
|
</div>
|
|
58
58
|
</h2>
|
|
59
|
-
<NoteDisplay
|
|
59
|
+
<NoteDisplay
|
|
60
|
+
:note-html="slide!.noteHTML"
|
|
61
|
+
class="max-w-full"
|
|
62
|
+
/>
|
|
60
63
|
</div>
|
|
61
64
|
<hr v-if="index < slidesWithNote.length - 1" class="border-main mb-8">
|
|
62
65
|
</div>
|
package/pages/presenter.vue
CHANGED
|
@@ -14,7 +14,7 @@ import { useFixedClicks } from '../composables/useClicks'
|
|
|
14
14
|
import SlideWrapper from '../internals/SlideWrapper'
|
|
15
15
|
import SlideContainer from '../internals/SlideContainer.vue'
|
|
16
16
|
import NavControls from '../internals/NavControls.vue'
|
|
17
|
-
import
|
|
17
|
+
import QuickOverview from '../internals/QuickOverview.vue'
|
|
18
18
|
import NoteEditor from '../internals/NoteEditor.vue'
|
|
19
19
|
import NoteStatic from '../internals/NoteStatic.vue'
|
|
20
20
|
import Goto from '../internals/Goto.vue'
|
|
@@ -45,16 +45,23 @@ const nextFrame = computed(() => {
|
|
|
45
45
|
else
|
|
46
46
|
return null
|
|
47
47
|
})
|
|
48
|
+
|
|
48
49
|
const nextFrameClicksCtx = computed(() => {
|
|
49
50
|
return nextFrame.value && clicksCtxMap[+nextFrame.value[0].path - 1]
|
|
50
51
|
})
|
|
51
|
-
watch([currentRoute, queryClicks], () => {
|
|
52
|
-
nextFrameClicksCtx.value && (nextFrameClicksCtx.value[0].value = nextFrame.value![1])
|
|
53
|
-
}, { immediate: true })
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
watch(
|
|
54
|
+
[currentRoute, queryClicks],
|
|
55
|
+
() => {
|
|
56
|
+
if (nextFrameClicksCtx.value)
|
|
57
|
+
nextFrameClicksCtx.value.current = nextFrame.value![1]
|
|
58
|
+
},
|
|
59
|
+
{ immediate: true },
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const SideEditor = shallowRef<any>()
|
|
56
63
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
57
|
-
import('../internals/
|
|
64
|
+
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
58
65
|
|
|
59
66
|
// sync presenter cursor
|
|
60
67
|
onMounted(() => {
|
|
@@ -86,21 +93,6 @@ onMounted(() => {
|
|
|
86
93
|
<template>
|
|
87
94
|
<div class="bg-main h-full slidev-presenter">
|
|
88
95
|
<div class="grid-container" :class="`layout${presenterLayout}`">
|
|
89
|
-
<div class="grid-section top flex">
|
|
90
|
-
<img src="../assets/logo-title-horizontal.png" class="ml-2 my-auto h-10 py-1 lg:h-14 lg:py-2" style="height: 3.5rem;" alt="Slidev logo">
|
|
91
|
-
<div class="flex-auto" />
|
|
92
|
-
<div
|
|
93
|
-
class="timer-btn my-auto relative w-22px h-22px cursor-pointer text-lg"
|
|
94
|
-
opacity="50 hover:100"
|
|
95
|
-
@click="resetTimer"
|
|
96
|
-
>
|
|
97
|
-
<carbon:time class="absolute" />
|
|
98
|
-
<carbon:renew class="absolute opacity-0" />
|
|
99
|
-
</div>
|
|
100
|
-
<div class="text-2xl pl-2 pr-6 my-auto tabular-nums">
|
|
101
|
-
{{ timer }}
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
96
|
<div ref="main" class="relative grid-section main flex flex-col p-2 lg:p-4" :style="themeVars">
|
|
105
97
|
<SlideContainer
|
|
106
98
|
key="main"
|
|
@@ -110,8 +102,8 @@ onMounted(() => {
|
|
|
110
102
|
<SlidesShow render-context="presenter" />
|
|
111
103
|
</template>
|
|
112
104
|
</SlideContainer>
|
|
113
|
-
<div class="
|
|
114
|
-
|
|
105
|
+
<div class="absolute left-0 top-0 bg-main border-b border-r border-main px2 py1 op50 text-sm">
|
|
106
|
+
Current
|
|
115
107
|
</div>
|
|
116
108
|
</div>
|
|
117
109
|
<div class="relative grid-section next flex flex-col p-2 lg:p-4" :style="themeVars">
|
|
@@ -121,30 +113,30 @@ onMounted(() => {
|
|
|
121
113
|
class="h-full w-full"
|
|
122
114
|
>
|
|
123
115
|
<SlideWrapper
|
|
124
|
-
:is="nextFrame[0].component as any"
|
|
116
|
+
:is="(nextFrame[0].component as any)"
|
|
125
117
|
:key="nextFrame[0].path"
|
|
126
|
-
:clicks-context="nextFrameClicksCtx
|
|
118
|
+
:clicks-context="nextFrameClicksCtx"
|
|
127
119
|
:class="getSlideClass(nextFrame[0])"
|
|
128
120
|
:route="nextFrame[0]"
|
|
129
121
|
render-context="previewNext"
|
|
130
122
|
/>
|
|
131
123
|
</SlideContainer>
|
|
132
|
-
<div class="
|
|
133
|
-
|
|
124
|
+
<div class="absolute left-0 top-0 bg-main border-b border-r border-main px2 py1 op50 text-sm">
|
|
125
|
+
Next
|
|
134
126
|
</div>
|
|
135
127
|
</div>
|
|
136
128
|
<!-- Notes -->
|
|
137
|
-
<div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ &&
|
|
138
|
-
<
|
|
129
|
+
<div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && SideEditor && showEditor" class="grid-section note of-auto">
|
|
130
|
+
<SideEditor />
|
|
139
131
|
</div>
|
|
140
132
|
<div v-else class="grid-section note grid grid-rows-[1fr_min-content] overflow-hidden">
|
|
141
133
|
<NoteEditor
|
|
142
134
|
v-if="__DEV__"
|
|
143
135
|
:key="`edit-${currentSlideId}`"
|
|
136
|
+
v-model:editing="notesEditing"
|
|
144
137
|
:no="currentSlideId"
|
|
145
138
|
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
|
|
146
|
-
:
|
|
147
|
-
:clicks="clicksContext.current"
|
|
139
|
+
:clicks-context="clicksContext"
|
|
148
140
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
149
141
|
/>
|
|
150
142
|
<NoteStatic
|
|
@@ -153,7 +145,7 @@ onMounted(() => {
|
|
|
153
145
|
:no="currentSlideId"
|
|
154
146
|
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
|
|
155
147
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
156
|
-
:clicks="clicksContext
|
|
148
|
+
:clicks-context="clicksContext"
|
|
157
149
|
/>
|
|
158
150
|
<div class="border-t border-main py-1 px-2 text-sm">
|
|
159
151
|
<IconButton title="Increase font size" @click="increasePresenterFontSize">
|
|
@@ -171,8 +163,20 @@ onMounted(() => {
|
|
|
171
163
|
</IconButton>
|
|
172
164
|
</div>
|
|
173
165
|
</div>
|
|
174
|
-
<div class="grid-section bottom">
|
|
166
|
+
<div class="grid-section bottom flex">
|
|
175
167
|
<NavControls :persist="true" />
|
|
168
|
+
<div flex-auto />
|
|
169
|
+
<div
|
|
170
|
+
class="timer-btn my-auto relative w-22px h-22px cursor-pointer text-lg"
|
|
171
|
+
opacity="50 hover:100"
|
|
172
|
+
@click="resetTimer"
|
|
173
|
+
>
|
|
174
|
+
<carbon:time class="absolute" />
|
|
175
|
+
<carbon:renew class="absolute opacity-0" />
|
|
176
|
+
</div>
|
|
177
|
+
<div class="text-2xl pl-2 pr-6 my-auto tabular-nums">
|
|
178
|
+
{{ timer }}
|
|
179
|
+
</div>
|
|
176
180
|
</div>
|
|
177
181
|
<DrawingControls v-if="__SLIDEV_FEATURE_DRAWINGS__" />
|
|
178
182
|
</div>
|
|
@@ -184,7 +188,7 @@ onMounted(() => {
|
|
|
184
188
|
</div>
|
|
185
189
|
</div>
|
|
186
190
|
<Goto />
|
|
187
|
-
<
|
|
191
|
+
<QuickOverview v-model="showOverview" />
|
|
188
192
|
</template>
|
|
189
193
|
|
|
190
194
|
<style scoped>
|
|
@@ -204,7 +208,7 @@ onMounted(() => {
|
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
.grid-container {
|
|
207
|
-
--uno: bg-
|
|
211
|
+
--uno: bg-gray/20;
|
|
208
212
|
height: 100%;
|
|
209
213
|
width: 100%;
|
|
210
214
|
display: grid;
|
|
@@ -213,9 +217,8 @@ onMounted(() => {
|
|
|
213
217
|
|
|
214
218
|
.grid-container.layout1 {
|
|
215
219
|
grid-template-columns: 1fr 1fr;
|
|
216
|
-
grid-template-rows:
|
|
220
|
+
grid-template-rows: 2fr 1fr min-content;
|
|
217
221
|
grid-template-areas:
|
|
218
|
-
'top top'
|
|
219
222
|
'main main'
|
|
220
223
|
'note next'
|
|
221
224
|
'bottom bottom';
|
|
@@ -223,9 +226,8 @@ onMounted(() => {
|
|
|
223
226
|
|
|
224
227
|
.grid-container.layout2 {
|
|
225
228
|
grid-template-columns: 3fr 2fr;
|
|
226
|
-
grid-template-rows:
|
|
229
|
+
grid-template-rows: 2fr 1fr min-content;
|
|
227
230
|
grid-template-areas:
|
|
228
|
-
'top top'
|
|
229
231
|
'note main'
|
|
230
232
|
'note next'
|
|
231
233
|
'bottom bottom';
|
|
@@ -234,9 +236,8 @@ onMounted(() => {
|
|
|
234
236
|
@media (max-aspect-ratio: 3/5) {
|
|
235
237
|
.grid-container.layout1 {
|
|
236
238
|
grid-template-columns: 1fr;
|
|
237
|
-
grid-template-rows:
|
|
239
|
+
grid-template-rows: 1fr 1fr 1fr min-content;
|
|
238
240
|
grid-template-areas:
|
|
239
|
-
'top'
|
|
240
241
|
'main'
|
|
241
242
|
'note'
|
|
242
243
|
'next'
|
|
@@ -247,9 +248,8 @@ onMounted(() => {
|
|
|
247
248
|
@media (min-aspect-ratio: 1/1) {
|
|
248
249
|
.grid-container.layout1 {
|
|
249
250
|
grid-template-columns: 1fr 1.1fr 0.9fr;
|
|
250
|
-
grid-template-rows:
|
|
251
|
+
grid-template-rows: 1fr 2fr min-content;
|
|
251
252
|
grid-template-areas:
|
|
252
|
-
'top top top'
|
|
253
253
|
'main main next'
|
|
254
254
|
'main main note'
|
|
255
255
|
'bottom bottom bottom';
|
|
@@ -279,10 +279,4 @@ onMounted(() => {
|
|
|
279
279
|
.grid-section.bottom {
|
|
280
280
|
grid-area: bottom;
|
|
281
281
|
}
|
|
282
|
-
.context {
|
|
283
|
-
position: absolute;
|
|
284
|
-
top: 0;
|
|
285
|
-
left: 0;
|
|
286
|
-
--uno: px-1 text-xs bg-gray-400 bg-opacity-50 opacity-75 rounded-br-md;
|
|
287
|
-
}
|
|
288
282
|
</style>
|
package/state/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { slideAspect } from '../env'
|
|
|
5
5
|
export const showRecordingDialog = ref(false)
|
|
6
6
|
export const showInfoDialog = ref(false)
|
|
7
7
|
export const showGotoDialog = ref(false)
|
|
8
|
+
export const showOverview = ref(false)
|
|
8
9
|
|
|
9
10
|
export const shortcutsEnabled = ref(true)
|
|
10
11
|
export const breakpoints = useBreakpoints({
|
|
@@ -24,7 +25,6 @@ export const currentCamera = useLocalStorage<string>('slidev-camera', 'default',
|
|
|
24
25
|
export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
|
|
25
26
|
export const slideScale = useLocalStorage<number>('slidev-scale', 0)
|
|
26
27
|
|
|
27
|
-
export const showOverview = useLocalStorage('slidev-show-overview', false, { listenToStorageChanges: false })
|
|
28
28
|
export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
|
|
29
29
|
export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
|
|
30
30
|
export const isEditorVertical = useLocalStorage('slidev-editor-vertical', false, { listenToStorageChanges: false })
|
package/styles/index.css
CHANGED
|
@@ -16,7 +16,11 @@ html {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
.slidev-icon-btn {
|
|
19
|
-
|
|
19
|
+
aspect-ratio: 1;
|
|
20
|
+
display: inline-block;
|
|
21
|
+
user-select: none;
|
|
22
|
+
outline: none;
|
|
23
|
+
cursor: pointer;
|
|
20
24
|
@apply opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
|
|
21
25
|
@apply hover:(opacity-100 bg-gray-400 bg-opacity-10);
|
|
22
26
|
@apply md:p-2;
|
|
@@ -70,9 +74,21 @@ html {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
.slidev-note-click-mark {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
user-select: none;
|
|
78
|
+
font-size: 0.7em;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
--uno: text-violet bg-violet/10 px1 font-mono rounded items-center border
|
|
81
|
+
border-transparent;
|
|
82
|
+
}
|
|
83
|
+
.slidev-note-click-mark.slidev-note-click-mark-active {
|
|
84
|
+
--uno: border border-violet;
|
|
85
|
+
}
|
|
86
|
+
.slidev-note-click-mark.slidev-note-click-mark-past {
|
|
87
|
+
filter: saturate(0);
|
|
88
|
+
opacity: 0.5;
|
|
89
|
+
}
|
|
90
|
+
.slidev-note-click-mark.slidev-note-click-mark-future {
|
|
91
|
+
opacity: 0.5;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
.slidev-note-click-mark::before {
|
|
File without changes
|