@slidev/client 0.48.0-beta.11 → 0.48.0-beta.13

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>
package/builtin/VClick.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { PropType, VNode } from 'vue'
8
8
  import { Text, defineComponent, h } from 'vue'
9
+ import { CLICKS_MAX } from '../constants'
9
10
  import VClicks from './VClicks'
10
11
 
11
12
  export default defineComponent({
@@ -31,7 +32,7 @@ export default defineComponent({
31
32
  return h(
32
33
  VClicks,
33
34
  {
34
- every: 99999,
35
+ every: CLICKS_MAX,
35
36
  at: this.at,
36
37
  hide: this.hide,
37
38
  fade: this.fade,
@@ -1,15 +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
+ import { CLICKS_MAX } from '../constants'
8
9
 
9
- /**
10
- * @internal
11
- */
12
- export function useClicksContextBase(getCurrent: () => number, clicksOverrides?: number): ClicksContext {
10
+ function useClicksContextBase(current: Ref<number>, clicksOverrides?: number): ClicksContext {
13
11
  const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
14
12
  const map: ClicksContext['map'] = shallowReactive(new Map())
15
13
 
@@ -18,7 +16,10 @@ export function useClicksContextBase(getCurrent: () => number, clicksOverrides?:
18
16
  return isPrintMode.value && !isPrintWithClicks.value
19
17
  },
20
18
  get current() {
21
- return getCurrent()
19
+ return current.value
20
+ },
21
+ set current(value) {
22
+ current.value = value
22
23
  },
23
24
  relativeOffsets,
24
25
  map,
@@ -65,17 +66,25 @@ export function useClicksContextBase(getCurrent: () => number, clicksOverrides?:
65
66
  export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksContext {
66
67
  if (route?.meta?.__clicksContext)
67
68
  return route.meta.__clicksContext
68
- const thisPath = +(route?.path ?? 99999)
69
- const context = useClicksContextBase(
70
- () => {
71
- const currentPath = +(currentRoute.value?.path ?? 99999)
69
+ const thisPath = +(route?.path ?? CLICKS_MAX)
70
+ const current = computed({
71
+ get() {
72
+ const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
72
73
  if (currentPath === thisPath)
73
74
  return queryClicks.value
74
75
  else if (currentPath > thisPath)
75
- return 99999
76
+ return CLICKS_MAX
76
77
  else
77
78
  return 0
78
79
  },
80
+ set(v) {
81
+ const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
82
+ if (currentPath === thisPath)
83
+ queryClicks.value = v
84
+ },
85
+ })
86
+ const context = useClicksContextBase(
87
+ current,
79
88
  route?.meta?.clicks,
80
89
  )
81
90
  if (route?.meta)
@@ -83,7 +92,6 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
83
92
  return context
84
93
  }
85
94
 
86
- export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0): [Ref<number>, ClicksContext] {
87
- const current = ref(currentInit)
88
- return [current, useClicksContextBase(() => current.value, route?.meta?.clicks)]
95
+ export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0): ClicksContext {
96
+ return useClicksContextBase(ref(currentInit), route?.meta?.clicks)
89
97
  }
package/constants.ts CHANGED
@@ -22,6 +22,8 @@ export const CLASS_VCLICK_HIDDEN_EXP = 'slidev-vclick-hidden-explicitly'
22
22
  export const CLASS_VCLICK_CURRENT = 'slidev-vclick-current'
23
23
  export const CLASS_VCLICK_PRIOR = 'slidev-vclick-prior'
24
24
 
25
+ export const CLICKS_MAX = 999999
26
+
25
27
  export const TRUST_ORIGINS = [
26
28
  'localhost',
27
29
  '127.0.0.1',
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
 
@@ -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(() => {
@@ -1,19 +1,125 @@
1
1
  <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, ref, watch } from 'vue'
3
+ import type { ClicksContext } from '@slidev/types'
4
+ import { CLICKS_MAX } from '../constants'
5
+
2
6
  const props = defineProps<{
3
7
  class?: string
4
8
  noteHtml?: string
5
9
  note?: string
6
10
  placeholder?: string
11
+ clicksContext?: ClicksContext
7
12
  }>()
8
13
 
9
14
  defineEmits(['click'])
15
+
16
+ const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
17
+ const noteDisplay = ref<HTMLElement | null>(null)
18
+
19
+ const CLASS_FADE = 'slidev-note-fade'
20
+ const CLASS_MARKER = 'slidev-note-click-mark'
21
+
22
+ function highlightNote() {
23
+ if (!noteDisplay.value || !withClicks.value || props.clicksContext?.current == null)
24
+ return
25
+
26
+ const current = +props.clicksContext?.current ?? CLICKS_MAX
27
+ const disabled = current < 0 || current >= CLICKS_MAX
28
+ if (disabled) {
29
+ Array.from(noteDisplay.value.querySelectorAll('*'))
30
+ .forEach(el => el.classList.remove(CLASS_FADE))
31
+ return
32
+ }
33
+
34
+ const nodeToIgnores = new Set<Element>()
35
+ function ignoreParent(node: Element) {
36
+ if (!node || node === noteDisplay.value)
37
+ return
38
+ nodeToIgnores.add(node)
39
+ if (node.parentElement)
40
+ ignoreParent(node.parentElement)
41
+ }
42
+
43
+ const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
44
+ const markersMap = new Map<number, HTMLElement>()
45
+
46
+ // Convert all sibling text nodes to spans, so we attach classes to them
47
+ for (const marker of markers) {
48
+ const parent = marker.parentElement!
49
+ const clicks = Number(marker.dataset!.clicks)
50
+ markersMap.set(clicks, marker)
51
+ // Ignore the parents of the marker, so the class only applies to the children
52
+ ignoreParent(parent)
53
+ Array.from(parent!.childNodes)
54
+ .forEach((node) => {
55
+ if (node.nodeType === 3) { // text node
56
+ const span = document.createElement('span')
57
+ span.textContent = node.textContent
58
+ parent.insertBefore(span, node)
59
+ node.remove()
60
+ }
61
+ })
62
+ }
63
+ const children = Array.from(noteDisplay.value.querySelectorAll('*'))
64
+
65
+ let count = 0
66
+
67
+ // Segmenting notes by clicks
68
+ const segments = new Map<number, Element[]>()
69
+ for (const child of children) {
70
+ if (!segments.has(count))
71
+ segments.set(count, [])
72
+ segments.get(count)!.push(child)
73
+ // Update count when reach marker
74
+ if (child.classList.contains(CLASS_MARKER))
75
+ count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
76
+ }
77
+
78
+ // Apply
79
+ for (const [count, els] of segments) {
80
+ els.forEach(el => el.classList.toggle(
81
+ CLASS_FADE,
82
+ nodeToIgnores.has(el)
83
+ ? false
84
+ : count !== current,
85
+ ))
86
+ }
87
+
88
+ for (const [clicks, marker] of markersMap) {
89
+ marker.classList.remove(CLASS_FADE)
90
+ marker.classList.toggle(`${CLASS_MARKER}-past`, clicks < current)
91
+ marker.classList.toggle(`${CLASS_MARKER}-active`, clicks === current)
92
+ marker.classList.toggle(`${CLASS_MARKER}-next`, clicks === current + 1)
93
+ marker.classList.toggle(`${CLASS_MARKER}-future`, clicks > current + 1)
94
+ marker.addEventListener('dblclick', (e) => {
95
+ props.clicksContext!.current = clicks
96
+ e.stopPropagation()
97
+ e.stopImmediatePropagation()
98
+ })
99
+ }
100
+ }
101
+
102
+ watch(
103
+ () => [props.noteHtml, props.clicksContext?.current],
104
+ () => {
105
+ nextTick(() => {
106
+ highlightNote()
107
+ })
108
+ },
109
+ { immediate: true },
110
+ )
111
+
112
+ onMounted(() => {
113
+ highlightNote()
114
+ })
10
115
  </script>
11
116
 
12
117
  <template>
13
118
  <div
14
119
  v-if="noteHtml"
15
- class="prose overflow-auto outline-none"
16
- :class="props.class"
120
+ ref="noteDisplay"
121
+ class="prose overflow-auto outline-none slidev-note"
122
+ :class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
17
123
  @click="$emit('click')"
18
124
  v-html="noteHtml"
19
125
  />
@@ -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,6 +22,9 @@ const props = defineProps({
20
22
  placeholder: {
21
23
  default: 'No notes for this slide',
22
24
  },
25
+ clicksContext: {
26
+ type: Object as PropType<ClicksContext>,
27
+ },
23
28
  autoHeight: {
24
29
  default: false,
25
30
  },
@@ -100,6 +105,7 @@ watch(
100
105
  :style="props.style"
101
106
  :note="note || placeholder"
102
107
  :note-html="info?.noteHTML"
108
+ :clicks-context="clicksContext"
103
109
  />
104
110
  <textarea
105
111
  v-else
@@ -1,10 +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
9
+ clicksContext?: ClicksContext
8
10
  }>()
9
11
 
10
12
  const { info } = useSlideInfo(props.no)
@@ -15,5 +17,6 @@ const { info } = useSlideInfo(props.no)
15
17
  :class="props.class"
16
18
  :note="info?.note"
17
19
  :note-html="info?.noteHTML"
20
+ :clicks-context="clicksContext"
18
21
  />
19
22
  </template>
@@ -0,0 +1,91 @@
1
+ <script setup lang="ts">
2
+ import type { ClicksContext } from '@slidev/types'
3
+ import { computed } from 'vue'
4
+ import { CLICKS_MAX } from '../constants'
5
+
6
+ const props = defineProps<{
7
+ clicksContext: ClicksContext
8
+ }>()
9
+
10
+ const total = computed(() => props.clicksContext.total)
11
+ const current = computed({
12
+ get() {
13
+ return props.clicksContext.current > total.value ? -1 : props.clicksContext.current
14
+ },
15
+ set(value: number) {
16
+ // eslint-disable-next-line vue/no-mutating-props
17
+ props.clicksContext.current = value
18
+ },
19
+ })
20
+
21
+ const range = computed(() => Array.from({ length: total.value + 1 }, (_, i) => i))
22
+
23
+ function onMousedown() {
24
+ if (current.value < 0 || current.value > total.value)
25
+ current.value = 0
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <div
31
+ class="flex gap-0.5 items-center select-none"
32
+ :title="`Clicks in this slide: ${total}`"
33
+ >
34
+ <div class="flex gap-1 items-center min-w-16">
35
+ <carbon:cursor-1 text-sm op50 />
36
+ <span v-if="current <= total && current >= 0" text-primary>{{ current }}/</span>
37
+ <span op50>{{ total }}</span>
38
+ </div>
39
+ <div
40
+ relative flex-auto h5 flex="~"
41
+ @dblclick="current = CLICKS_MAX"
42
+ >
43
+ <div
44
+ v-for="i of range" :key="i"
45
+ border="y main" of-hidden relative
46
+ :class="[
47
+ i === 0 ? 'rounded-l border-l' : '',
48
+ i === total ? 'rounded-r border-r' : '',
49
+ ]"
50
+ :style="{ width: `${1 / total * 100}%` }"
51
+ >
52
+ <div absolute inset-0 z--1 :class=" i <= current ? 'bg-primary op20' : ''" />
53
+ <div
54
+ :class="[
55
+ +i === +current ? 'text-primary font-bold op100 border-primary' : 'op30 border-main',
56
+ i === 0 ? 'rounded-l' : '',
57
+ i === total ? 'rounded-r' : 'border-r-2',
58
+ ]"
59
+ w-full h-full text-xs flex items-center justify-center
60
+ >
61
+ {{ i }}
62
+ </div>
63
+ </div>
64
+ <input
65
+ v-model="current"
66
+ class="range" absolute inset-0
67
+ type="range" :min="0" :max="total" :step="1" z-10 op0
68
+ :style="{ '--thumb-width': `${1 / (total + 1) * 100}%` }"
69
+ @mousedown="onMousedown"
70
+ >
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <style scoped>
76
+ .range {
77
+ -webkit-appearance: none;
78
+ appearance: none;
79
+ background: transparent;
80
+ }
81
+ .range::-webkit-slider-thumb {
82
+ -webkit-appearance: none;
83
+ height: 100%;
84
+ width: var(--thumb-width, 0.5rem);
85
+ }
86
+
87
+ .range::-moz-range-thumb {
88
+ height: 100%;
89
+ width: var(--thumb-width, 0.5rem);
90
+ }
91
+ </style>
@@ -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>
@@ -7,6 +7,7 @@ import { currentPage, go as goSlide, rawRoutes } from '../logic/nav'
7
7
  import { currentOverviewPage, overviewRowCount } from '../logic/overview'
8
8
  import { useFixedClicks } from '../composables/useClicks'
9
9
  import { getSlideClass } from '../utils'
10
+ import { CLICKS_MAX } from '../constants'
10
11
  import SlideContainer from './SlideContainer.vue'
11
12
  import SlideWrapper from './SlideWrapper'
12
13
  import DrawingPreview from './DrawingPreview.vue'
@@ -139,7 +140,7 @@ watchEffect(() => {
139
140
  <SlideWrapper
140
141
  :is="route.component"
141
142
  v-if="route?.component"
142
- :clicks-context="useFixedClicks(route, 99999)[1]"
143
+ :clicks-context="useFixedClicks(route, CLICKS_MAX)"
143
144
  :class="getSlideClass(route)"
144
145
  :route="route"
145
146
  render-context="overview"
package/logic/nav.ts CHANGED
@@ -7,6 +7,7 @@ import { rawRoutes, router } from '../routes'
7
7
  import { configs } from '../env'
8
8
  import { skipTransition } from '../composables/hmr'
9
9
  import { usePrimaryClicks } from '../composables/useClicks'
10
+ import { CLICKS_MAX } from '../constants'
10
11
  import { useRouteQuery } from './route'
11
12
  import { isDrawing } from './drawings'
12
13
 
@@ -39,7 +40,7 @@ export const queryClicks = computed({
39
40
  get() {
40
41
  // eslint-disable-next-line ts/no-use-before-define
41
42
  if (clicksContext.value.disabled)
42
- return 99999
43
+ return CLICKS_MAX
43
44
  let v = +(queryClicksRaw.value || 0)
44
45
  if (Number.isNaN(v))
45
46
  v = 0
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.11",
4
+ "version": "0.48.0-beta.13",
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.11",
58
- "@slidev/types": "0.48.0-beta.11"
58
+ "@slidev/types": "0.48.0-beta.13",
59
+ "@slidev/parser": "0.48.0-beta.13"
59
60
  },
60
61
  "devDependencies": {
61
62
  "vite": "^5.1.4"
package/pages/notes.vue CHANGED
@@ -51,6 +51,7 @@ 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"
54
55
  />
55
56
  </div>
56
57
  <div class="flex-none border-t border-main">
@@ -4,8 +4,8 @@ import { useHead } from '@unhead/vue'
4
4
  import type { RouteRecordRaw } from 'vue-router'
5
5
  import type { ClicksContext } from 'packages/types'
6
6
  import { themeVars } from '../env'
7
- import { rawRoutes } from '../logic/nav'
8
- import { useClicksContextBase } from '../composables/useClicks'
7
+ import { openInEditor, rawRoutes } from '../logic/nav'
8
+ import { useFixedClicks } from '../composables/useClicks'
9
9
  import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
10
10
  import { getSlideClass } from '../utils'
11
11
  import SlideContainer from '../internals/SlideContainer.vue'
@@ -13,6 +13,8 @@ import SlideWrapper from '../internals/SlideWrapper'
13
13
  import DrawingPreview from '../internals/DrawingPreview.vue'
14
14
  import IconButton from '../internals/IconButton.vue'
15
15
  import NoteEditor from '../internals/NoteEditor.vue'
16
+ import OverviewClicksSlider from '../internals/OverviewClicksSlider.vue'
17
+ import { CLICKS_MAX } from '../constants'
16
18
 
17
19
  const cardWidth = 450
18
20
 
@@ -28,15 +30,15 @@ const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
28
30
  const totalClicks = computed(() => rawRoutes.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
29
31
 
30
32
  const clicksContextMap = new WeakMap<RouteRecordRaw, ClicksContext>()
31
- function getClickContext(route: RouteRecordRaw) {
33
+ function getClicksContext(route: RouteRecordRaw) {
32
34
  // We create a local clicks context to calculate the total clicks of the slide
33
35
  if (!clicksContextMap.has(route))
34
- clicksContextMap.set(route, useClicksContextBase(() => 999999, route?.meta?.clicks))
36
+ clicksContextMap.set(route, useFixedClicks(route, CLICKS_MAX))
35
37
  return clicksContextMap.get(route)!
36
38
  }
37
39
 
38
40
  function getSlideClicks(route: RouteRecordRaw) {
39
- return route.meta?.clicks || getClickContext(route)?.total
41
+ return route.meta?.clicks || getClicksContext(route)?.total
40
42
  }
41
43
 
42
44
  function wordCount(str: string) {
@@ -129,40 +131,55 @@ onMounted(() => {
129
131
  :ref="el => blocks.set(idx, el as any)"
130
132
  class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
131
133
  >
132
- <div class="select-none w-13 text-right my4">
134
+ <div class="select-none w-13 text-right my4 flex flex-col gap-1 items-end">
133
135
  <div class="text-3xl op20 mb2">
134
136
  {{ idx + 1 }}
135
137
  </div>
136
- <div
137
- v-if="getSlideClicks(route)"
138
- class="flex gap-0.5 op50 items-center justify-end"
139
- :title="`Clicks in this slide: ${getSlideClicks(route)}`"
138
+ <IconButton
139
+ class="mr--3 op0 group-hover:op80"
140
+ title="Play in new tab"
141
+ @click="openSlideInNewTab(route.path)"
140
142
  >
141
- <carbon:cursor-1 text-sm />
142
- {{ getSlideClicks(route) }}
143
- </div>
143
+ <carbon:presentation-file />
144
+ </IconButton>
145
+ <IconButton
146
+ v-if="route.meta?.slide"
147
+ class="mr--3 op0 group-hover:op80"
148
+ title="Open in editor"
149
+ @click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
150
+ >
151
+ <carbon:cics-program />
152
+ </IconButton>
144
153
  </div>
145
- <div
146
- class="border rounded border-main overflow-hidden bg-main my5 select-none h-max"
147
- :style="themeVars"
148
- @dblclick="openSlideInNewTab(route.path)"
149
- >
150
- <SlideContainer
151
- :key="route.path"
152
- :width="cardWidth"
153
- :clicks-disabled="true"
154
- class="pointer-events-none important:[&_*]:select-none"
154
+ <div class="flex flex-col gap-2 my5">
155
+ <div
156
+ class="border rounded border-main overflow-hidden bg-main select-none h-max"
157
+ :style="themeVars"
158
+ @dblclick="openSlideInNewTab(route.path)"
155
159
  >
156
- <SlideWrapper
157
- :is="route.component"
158
- v-if="route?.component"
159
- :clicks-context="getClickContext(route)"
160
- :class="getSlideClass(route)"
161
- :route="route"
162
- render-context="overview"
163
- />
164
- <DrawingPreview :page="+route.path" />
165
- </SlideContainer>
160
+ <SlideContainer
161
+ :key="route.path"
162
+ :width="cardWidth"
163
+ :clicks-disabled="true"
164
+ class="pointer-events-none important:[&_*]:select-none"
165
+ >
166
+ <SlideWrapper
167
+ :is="route.component"
168
+ v-if="route?.component"
169
+ :clicks-context="getClicksContext(route)"
170
+ :class="getSlideClass(route)"
171
+ :route="route"
172
+ render-context="overview"
173
+ />
174
+ <DrawingPreview :page="+route.path" />
175
+ </SlideContainer>
176
+ </div>
177
+ <OverviewClicksSlider
178
+ v-if="getSlideClicks(route)"
179
+ mt-2
180
+ :clicks-context="getClicksContext(route)"
181
+ class="w-full"
182
+ />
166
183
  </div>
167
184
  <div class="py3 mt-0.5 mr--8 ml--4 op0 transition group-hover:op100">
168
185
  <IconButton
@@ -179,6 +196,7 @@ onMounted(() => {
179
196
  class="max-w-250 w-250 text-lg rounded p3"
180
197
  :auto-height="true"
181
198
  :editing="edittingNote === idx"
199
+ :clicks-context="getClicksContext(route)"
182
200
  @dblclick="edittingNote !== idx ? edittingNote = idx : null"
183
201
  @update:editing="edittingNote = null"
184
202
  />
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 />
@@ -49,12 +49,12 @@ const nextFrameClicksCtx = computed(() => {
49
49
  return nextFrame.value && clicksCtxMap[+nextFrame.value[0].path - 1]
50
50
  })
51
51
  watch([currentRoute, queryClicks], () => {
52
- nextFrameClicksCtx.value && (nextFrameClicksCtx.value[0].value = nextFrame.value![1])
52
+ nextFrameClicksCtx.value && (nextFrameClicksCtx.value.current = nextFrame.value![1])
53
53
  }, { immediate: true })
54
54
 
55
- const Editor = shallowRef<any>()
55
+ const SideEditor = shallowRef<any>()
56
56
  if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
57
- import('../internals/Editor.vue').then(v => Editor.value = v.default)
57
+ import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
58
58
 
59
59
  // sync presenter cursor
60
60
  onMounted(() => {
@@ -121,9 +121,9 @@ onMounted(() => {
121
121
  class="h-full w-full"
122
122
  >
123
123
  <SlideWrapper
124
- :is="nextFrame[0].component as any"
124
+ :is="(nextFrame[0].component as any)"
125
125
  :key="nextFrame[0].path"
126
- :clicks-context="nextFrameClicksCtx[1]"
126
+ :clicks-context="nextFrameClicksCtx"
127
127
  :class="getSlideClass(nextFrame[0])"
128
128
  :route="nextFrame[0]"
129
129
  render-context="previewNext"
@@ -134,8 +134,8 @@ onMounted(() => {
134
134
  </div>
135
135
  </div>
136
136
  <!-- Notes -->
137
- <div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && Editor && showEditor" class="grid-section note of-auto">
138
- <Editor />
137
+ <div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && SideEditor && showEditor" class="grid-section note of-auto">
138
+ <SideEditor />
139
139
  </div>
140
140
  <div v-else class="grid-section note grid grid-rows-[1fr_min-content] overflow-hidden">
141
141
  <NoteEditor
@@ -144,6 +144,7 @@ onMounted(() => {
144
144
  :no="currentSlideId"
145
145
  class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
146
146
  :editing="notesEditing"
147
+ :clicks-context="clicksContext"
147
148
  :style="{ fontSize: `${presenterNotesFontSize}em` }"
148
149
  />
149
150
  <NoteStatic
@@ -152,6 +153,7 @@ onMounted(() => {
152
153
  :no="currentSlideId"
153
154
  class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
154
155
  :style="{ fontSize: `${presenterNotesFontSize}em` }"
156
+ :clicks-context="clicksContext"
155
157
  />
156
158
  <div class="border-t border-main py-1 px-2 text-sm">
157
159
  <IconButton title="Increase font size" @click="increasePresenterFontSize">
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/code.css CHANGED
@@ -61,7 +61,7 @@ html:not(.dark) .shiki span {
61
61
  counter-increment: step;
62
62
  display: inline-block;
63
63
  text-align: right;
64
- --uno: w-4 mr-6 text-gray-400 dark:text-gray-600;
64
+ --uno: w-4 mr-6 text-gray-400 dark-text-gray-600;
65
65
  }
66
66
 
67
67
  /* Inline Code */
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;
@@ -63,6 +67,51 @@ html {
63
67
  width: 100%;
64
68
  }
65
69
 
70
+ /* Note Clicks */
71
+
72
+ .slidev-note-with-clicks .slidev-note-fade {
73
+ color: #888888ab;
74
+ }
75
+
76
+ .slidev-note-click-mark {
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;
92
+ }
93
+
94
+ .slidev-note-click-mark::before {
95
+ content: '';
96
+ display: inline-block;
97
+ --un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 32 32' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M23 28a1 1 0 0 1-.71-.29l-6.13-6.14l-3.33 5a1 1 0 0 1-1 .44a1 1 0 0 1-.81-.7l-6-20A1 1 0 0 1 6.29 5l20 6a1 1 0 0 1 .7.81a1 1 0 0 1-.44 1l-5 3.33l6.14 6.13a1 1 0 0 1 0 1.42l-4 4A1 1 0 0 1 23 28m0-2.41L25.59 23l-7.16-7.15l5.25-3.5L7.49 7.49l4.86 16.19l3.5-5.25Z'/%3E%3C/svg%3E");
98
+ -webkit-mask: var(--un-icon) no-repeat;
99
+ mask: var(--un-icon) no-repeat;
100
+ -webkit-mask-size: 100% 100%;
101
+ mask-size: 100% 100%;
102
+ background-color: currentColor;
103
+ color: inherit;
104
+ width: 1.2em;
105
+ height: 1.2em;
106
+ opacity: 0.8;
107
+ }
108
+
109
+ .slidev-note-click-mark::after {
110
+ content: attr(data-clicks);
111
+ display: inline-block;
112
+ transform: translateY(0.1em);
113
+ }
114
+
66
115
  /* Transform the position back for Rough Notation (v-mark) */
67
116
  .rough-annotation {
68
117
  transform: scale(calc(1 / var(--slidev-slide-scale)));
File without changes