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

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/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,
@@ -5,6 +5,7 @@ import { 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
  /**
10
11
  * @internal
@@ -65,14 +66,14 @@ 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 thisPath = +(route?.path ?? CLICKS_MAX)
69
70
  const context = useClicksContextBase(
70
71
  () => {
71
- const currentPath = +(currentRoute.value?.path ?? 99999)
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
  },
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',
@@ -1,19 +1,70 @@
1
1
  <script setup lang="ts">
2
+ import { computed, defineEmits, defineProps, nextTick, onMounted, ref, watch } from 'vue'
3
+ import { CLICKS_MAX } from '../constants'
4
+
2
5
  const props = defineProps<{
3
6
  class?: string
4
7
  noteHtml?: string
5
8
  note?: string
6
9
  placeholder?: string
10
+ clicks?: number | string
7
11
  }>()
8
12
 
9
13
  defineEmits(['click'])
14
+
15
+ const withClicks = computed(() => props.clicks != null && props.noteHtml?.includes('slidev-note-click-mark'))
16
+ const noteDisplay = ref<HTMLElement | null>(null)
17
+
18
+ function highlightNote() {
19
+ if (!noteDisplay.value || !withClicks.value || props.clicks == null)
20
+ return
21
+
22
+ const children = Array.from(noteDisplay.value.querySelectorAll('*'))
23
+
24
+ const disabled = +props.clicks < 0 || +props.clicks >= CLICKS_MAX
25
+ if (disabled) {
26
+ children.forEach(el => el.classList.remove('slidev-note-fade'))
27
+ return
28
+ }
29
+
30
+ let count = 0
31
+
32
+ const groups = new Map<number, Element[]>()
33
+
34
+ for (const child of children) {
35
+ if (!groups.has(count))
36
+ groups.set(count, [])
37
+
38
+ groups.get(count)!.push(child)
39
+ if (child.classList.contains('slidev-note-click-mark'))
40
+ count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
41
+ }
42
+
43
+ for (const [count, els] of groups)
44
+ els.forEach(el => el.classList.toggle('slidev-note-fade', +count !== +props.clicks!))
45
+ }
46
+
47
+ watch(
48
+ () => [props.noteHtml, props.clicks],
49
+ () => {
50
+ nextTick(() => {
51
+ highlightNote()
52
+ })
53
+ },
54
+ { immediate: true },
55
+ )
56
+
57
+ onMounted(() => {
58
+ highlightNote()
59
+ })
10
60
  </script>
11
61
 
12
62
  <template>
13
63
  <div
14
64
  v-if="noteHtml"
15
- class="prose overflow-auto outline-none"
16
- :class="props.class"
65
+ ref="noteDisplay"
66
+ class="prose overflow-auto outline-none slidev-note"
67
+ :class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
17
68
  @click="$emit('click')"
18
69
  v-html="noteHtml"
19
70
  />
@@ -20,6 +20,9 @@ const props = defineProps({
20
20
  placeholder: {
21
21
  default: 'No notes for this slide',
22
22
  },
23
+ clicks: {
24
+ type: [Number, String],
25
+ },
23
26
  autoHeight: {
24
27
  default: false,
25
28
  },
@@ -100,6 +103,7 @@ watch(
100
103
  :style="props.style"
101
104
  :note="note || placeholder"
102
105
  :note-html="info?.noteHTML"
106
+ :clicks="props.clicks"
103
107
  />
104
108
  <textarea
105
109
  v-else
@@ -5,6 +5,7 @@ import NoteDisplay from './NoteDisplay.vue'
5
5
  const props = defineProps<{
6
6
  no?: number
7
7
  class?: string
8
+ clicks?: number | string
8
9
  }>()
9
10
 
10
11
  const { info } = useSlideInfo(props.no)
@@ -15,5 +16,6 @@ const { info } = useSlideInfo(props.no)
15
16
  :class="props.class"
16
17
  :note="info?.note"
17
18
  :note-html="info?.noteHTML"
19
+ :clicks="props.clicks"
18
20
  />
19
21
  </template>
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import type { ClicksContext } from '@slidev/types'
3
+ import type { Ref } from 'vue'
4
+ import { computed } from 'vue'
5
+ import { CLICKS_MAX } from '../constants'
6
+
7
+ const props = defineProps<{
8
+ clickContext: [Ref<number>, ClicksContext]
9
+ }>()
10
+
11
+ const total = computed(() => props.clickContext[1].total)
12
+ const current = computed({
13
+ get() {
14
+ return props.clickContext[0].value > total.value ? -1 : props.clickContext[0].value
15
+ },
16
+ set(value: number) {
17
+ // eslint-disable-next-line vue/no-mutating-props
18
+ props.clickContext[0].value = value
19
+ },
20
+ })
21
+
22
+ const range = computed(() => Array.from({ length: total.value + 1 }, (_, i) => i))
23
+
24
+ function onMousedown() {
25
+ if (current.value < 0 || current.value > total.value)
26
+ current.value = 0
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <div
32
+ class="flex gap-0.5 items-center select-none"
33
+ :title="`Clicks in this slide: ${total}`"
34
+ >
35
+ <div class="flex gap-1 items-center min-w-16">
36
+ <carbon:cursor-1 text-sm op50 />
37
+ <span v-if="current <= total && current >= 0" text-primary>{{ current }}/</span>
38
+ <span op50>{{ total }}</span>
39
+ </div>
40
+ <div
41
+ relative flex-auto h5 flex="~"
42
+ @dblclick="current = CLICKS_MAX"
43
+ >
44
+ <div
45
+ v-for="i of range" :key="i"
46
+ border="y main" of-hidden relative
47
+ :class="[
48
+ i === 0 ? 'rounded-l border-l' : '',
49
+ i === total ? 'rounded-r border-r' : '',
50
+ ]"
51
+ :style="{ width: `${1 / total * 100}%` }"
52
+ >
53
+ <div absolute inset-0 z--1 :class=" i <= current ? 'bg-primary op20' : ''" />
54
+ <div
55
+ :class="[
56
+ +i === +current ? 'text-primary font-bold op100 border-primary' : 'op30 border-main',
57
+ i === 0 ? 'rounded-l' : '',
58
+ i === total ? 'rounded-r' : 'border-r-2',
59
+ ]"
60
+ w-full h-full text-xs flex items-center justify-center
61
+ >
62
+ {{ i }}
63
+ </div>
64
+ </div>
65
+ <input
66
+ v-model="current"
67
+ class="range" absolute inset-0
68
+ type="range" :min="0" :max="total" :step="1" z-10 op0
69
+ :style="{ '--thumb-width': `${1 / (total + 1) * 100}%` }"
70
+ @mousedown="onMousedown"
71
+ >
72
+ </div>
73
+ </div>
74
+ </template>
75
+
76
+ <style scoped>
77
+ .range {
78
+ -webkit-appearance: none;
79
+ appearance: none;
80
+ background: transparent;
81
+ }
82
+ .range::-webkit-slider-thumb {
83
+ -webkit-appearance: none;
84
+ height: 100%;
85
+ width: var(--thumb-width, 0.5rem);
86
+ }
87
+
88
+ .range::-moz-range-thumb {
89
+ height: 100%;
90
+ width: var(--thumb-width, 0.5rem);
91
+ }
92
+ </style>
@@ -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)[1]"
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.12",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -54,8 +54,8 @@
54
54
  "unocss": "^0.58.5",
55
55
  "vue": "^3.4.19",
56
56
  "vue-router": "^4.3.0",
57
- "@slidev/parser": "0.48.0-beta.11",
58
- "@slidev/types": "0.48.0-beta.11"
57
+ "@slidev/parser": "0.48.0-beta.12",
58
+ "@slidev/types": "0.48.0-beta.12"
59
59
  },
60
60
  "devDependencies": {
61
61
  "vite": "^5.1.4"
@@ -1,11 +1,12 @@
1
1
  <script setup lang="ts">
2
+ import type { Ref } from 'vue'
2
3
  import { computed, nextTick, onMounted, reactive, ref } from 'vue'
3
4
  import { useHead } from '@unhead/vue'
4
5
  import type { RouteRecordRaw } from 'vue-router'
5
6
  import type { ClicksContext } from 'packages/types'
6
7
  import { themeVars } from '../env'
7
8
  import { rawRoutes } from '../logic/nav'
8
- import { useClicksContextBase } from '../composables/useClicks'
9
+ import { useFixedClicks } from '../composables/useClicks'
9
10
  import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
10
11
  import { getSlideClass } from '../utils'
11
12
  import SlideContainer from '../internals/SlideContainer.vue'
@@ -13,6 +14,8 @@ import SlideWrapper from '../internals/SlideWrapper'
13
14
  import DrawingPreview from '../internals/DrawingPreview.vue'
14
15
  import IconButton from '../internals/IconButton.vue'
15
16
  import NoteEditor from '../internals/NoteEditor.vue'
17
+ import OverviewClicksSlider from '../internals/OverviewClicksSlider.vue'
18
+ import { CLICKS_MAX } from '../constants'
16
19
 
17
20
  const cardWidth = 450
18
21
 
@@ -27,16 +30,16 @@ const wordCounts = computed(() => rawRoutes.map(route => wordCount(route.meta?.s
27
30
  const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
28
31
  const totalClicks = computed(() => rawRoutes.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
29
32
 
30
- const clicksContextMap = new WeakMap<RouteRecordRaw, ClicksContext>()
33
+ const clicksContextMap = new WeakMap<RouteRecordRaw, [Ref<number>, ClicksContext]>()
31
34
  function getClickContext(route: RouteRecordRaw) {
32
35
  // We create a local clicks context to calculate the total clicks of the slide
33
36
  if (!clicksContextMap.has(route))
34
- clicksContextMap.set(route, useClicksContextBase(() => 999999, route?.meta?.clicks))
37
+ clicksContextMap.set(route, useFixedClicks(route, CLICKS_MAX))
35
38
  return clicksContextMap.get(route)!
36
39
  }
37
40
 
38
41
  function getSlideClicks(route: RouteRecordRaw) {
39
- return route.meta?.clicks || getClickContext(route)?.total
42
+ return route.meta?.clicks || getClickContext(route)?.[1]?.total
40
43
  }
41
44
 
42
45
  function wordCount(str: string) {
@@ -133,36 +136,36 @@ onMounted(() => {
133
136
  <div class="text-3xl op20 mb2">
134
137
  {{ idx + 1 }}
135
138
  </div>
139
+ </div>
140
+ <div class="flex flex-col gap-2 my5">
136
141
  <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)}`"
142
+ class="border rounded border-main overflow-hidden bg-main select-none h-max"
143
+ :style="themeVars"
144
+ @dblclick="openSlideInNewTab(route.path)"
140
145
  >
141
- <carbon:cursor-1 text-sm />
142
- {{ getSlideClicks(route) }}
146
+ <SlideContainer
147
+ :key="route.path"
148
+ :width="cardWidth"
149
+ :clicks-disabled="true"
150
+ class="pointer-events-none important:[&_*]:select-none"
151
+ >
152
+ <SlideWrapper
153
+ :is="route.component"
154
+ v-if="route?.component"
155
+ :clicks-context="getClickContext(route)[1]"
156
+ :class="getSlideClass(route)"
157
+ :route="route"
158
+ render-context="overview"
159
+ />
160
+ <DrawingPreview :page="+route.path" />
161
+ </SlideContainer>
143
162
  </div>
144
- </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"
155
- >
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>
163
+ <OverviewClicksSlider
164
+ v-if="getSlideClicks(route)"
165
+ mt-2
166
+ :click-context="getClickContext(route)"
167
+ class="w-full"
168
+ />
166
169
  </div>
167
170
  <div class="py3 mt-0.5 mr--8 ml--4 op0 transition group-hover:op100">
168
171
  <IconButton
@@ -179,6 +182,7 @@ onMounted(() => {
179
182
  class="max-w-250 w-250 text-lg rounded p3"
180
183
  :auto-height="true"
181
184
  :editing="edittingNote === idx"
185
+ :clicks="getClickContext(route)[0].value"
182
186
  @dblclick="edittingNote !== idx ? edittingNote = idx : null"
183
187
  @update:editing="edittingNote = null"
184
188
  />
@@ -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="clicksContext.current"
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="clicksContext.current"
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/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
@@ -63,6 +63,39 @@ html {
63
63
  width: 100%;
64
64
  }
65
65
 
66
+ /* Note Clicks */
67
+
68
+ .slidev-note-with-clicks .slidev-note-fade {
69
+ color: #888888ab;
70
+ }
71
+
72
+ .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;
76
+ }
77
+
78
+ .slidev-note-click-mark::before {
79
+ content: '';
80
+ display: inline-block;
81
+ --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");
82
+ -webkit-mask: var(--un-icon) no-repeat;
83
+ mask: var(--un-icon) no-repeat;
84
+ -webkit-mask-size: 100% 100%;
85
+ mask-size: 100% 100%;
86
+ background-color: currentColor;
87
+ color: inherit;
88
+ width: 1.2em;
89
+ height: 1.2em;
90
+ opacity: 0.8;
91
+ }
92
+
93
+ .slidev-note-click-mark::after {
94
+ content: attr(data-clicks);
95
+ display: inline-block;
96
+ transform: translateY(0.1em);
97
+ }
98
+
66
99
  /* Transform the position back for Rough Notation (v-mark) */
67
100
  .rough-annotation {
68
101
  transform: scale(calc(1 / var(--slidev-slide-scale)));