@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.
@@ -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>
@@ -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 getCurrent()
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 context = useClicksContextBase(
71
- () => {
72
- const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
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): [Ref<number>, ClicksContext] {
88
- const current = ref(currentInit)
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()[1])
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
 
@@ -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 SlidesOverview from './SlidesOverview.vue'
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
- <SlidesOverview v-model="showOverview" />
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 { injectionSlideScale } from '../constants'
4
+ import { useSlideContext } from '../context'
6
5
 
7
- const scale = injectLocal(injectionSlideScale)!
6
+ const scale = useSlideContext().$scale
8
7
  const svg = ref<SVGSVGElement>()
9
8
 
10
9
  onMounted(() => {
@@ -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
- </button>
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="<md:hidden"
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, defineEmits, defineProps, nextTick, onMounted, ref, watch } from 'vue'
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
- clicks?: number | string
11
+ clicksContext?: ClicksContext
12
+ autoScroll?: boolean
11
13
  }>()
12
14
 
13
15
  defineEmits(['click'])
14
16
 
15
- const withClicks = computed(() => props.clicks != null && props.noteHtml?.includes('slidev-note-click-mark'))
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.clicks == null)
24
+ if (!noteDisplay.value || !withClicks.value || props.clicksContext?.current == null)
20
25
  return
21
26
 
22
- const children = Array.from(noteDisplay.value.querySelectorAll('*'))
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
- children.forEach(el => el.classList.remove('slidev-note-fade'))
30
+ Array.from(noteDisplay.value.querySelectorAll('*'))
31
+ .forEach(el => el.classList.remove(CLASS_FADE))
27
32
  return
28
33
  }
29
34
 
30
- let count = 0
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 groups = new Map<number, Element[]>()
44
+ const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
45
+ const markersMap = new Map<number, HTMLElement>()
33
46
 
34
- for (const child of children) {
35
- if (!groups.has(count))
36
- groups.set(count, [])
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
- groups.get(count)!.push(child)
39
- if (child.classList.contains('slidev-note-click-mark'))
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
- for (const [count, els] of groups)
44
- els.forEach(el => el.classList.toggle('slidev-note-fade', +count !== +props.clicks!))
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.clicks],
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>
@@ -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
- clicks: {
24
- type: [Number, String],
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
- watchEffect(() => {
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="my--4 border-transparent border-2"
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="props.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=" editing = false"
118
- @focus="editing = true"
118
+ @keydown.esc="editing = false"
119
119
  />
120
120
  </template>
@@ -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
- clicks?: number | string
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="props.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
- clickContext: [Ref<number>, ClicksContext]
7
+ clicksContext: ClicksContext
9
8
  }>()
10
9
 
11
- const total = computed(() => props.clickContext[1].total)
10
+ const total = computed(() => props.clicksContext.total)
12
11
  const current = computed({
13
12
  get() {
14
- return props.clickContext[0].value > total.value ? -1 : props.clickContext[0].value
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.clickContext[0].value = value
17
+ props.clicksContext.current = value
19
18
  },
20
19
  })
21
20
 
@@ -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)[1]
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 v-for="i of clicks0.total" :key="i" :clicks-context="useFixedClicks(route, i)[1]" :nav="nav" :route="route" />
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)[1]"
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-4">
168
- <RouterLink
169
- v-if="__DEV__"
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
- to="/overview"
175
+ href="/overview"
172
176
  tab-index="-1"
173
- class="border-main border px3 py1 rounded hover:bg-gray/5 hover:text-primary"
177
+ class="text-2xl"
174
178
  >
175
- List overview
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.12",
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.19",
56
+ "vue": "^3.4.20",
56
57
  "vue-router": "^4.3.0",
57
- "@slidev/parser": "0.48.0-beta.12",
58
- "@slidev/types": "0.48.0-beta.12"
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">
@@ -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: 'List Overview',
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, [Ref<number>, ClicksContext]>()
34
- function getClickContext(route: RouteRecordRaw) {
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 || getClickContext(route)?.[1]?.total
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="getClickContext(route)[1]"
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
- :click-context="getClickContext(route)"
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="getClickContext(route)[0].value"
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 Editor = shallowRef<any>()
34
+ const SideEditor = shallowRef<any>()
35
35
  if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
36
- import('../internals/Editor.vue').then(v => Editor.value = v.default)
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__ && Editor && showEditor">
74
- <Editor :resize="true" />
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 :note-html="slide!.noteHTML" class="max-w-full" />
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>
@@ -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 SlidesOverview from '../internals/SlidesOverview.vue'
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
- const Editor = shallowRef<any>()
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/Editor.vue').then(v => Editor.value = v.default)
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="context">
114
- current
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[1]"
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="context">
133
- next
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__ && Editor && showEditor" class="grid-section note of-auto">
138
- <Editor />
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
- :editing="notesEditing"
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.current"
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
- <SlidesOverview v-model="showOverview" />
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-active;
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: min-content 2fr 1fr min-content;
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: min-content 2fr 1fr min-content;
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: min-content 1fr 1fr 1fr min-content;
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: min-content 1fr 2fr min-content;
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
- @apply inline-block cursor-pointer select-none !outline-none;
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
- font-size: 0.8em;
74
- --uno: text-violet bg-violet/10 mx1 px1 font-mono rounded flex flex-inline
75
- items-center align-middle;
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