@slidev/client 52.4.0 → 52.5.0

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.
@@ -1,20 +1,68 @@
1
1
  import { useInterval } from '@vueuse/core'
2
- import { computed } from 'vue'
2
+ import { computed, toRef } from 'vue'
3
+ import { sharedState } from '../state/shared'
3
4
 
4
5
  export function useTimer() {
5
- const { counter, isActive, reset, pause, resume } = useInterval(1000, { controls: true })
6
+ const interval = useInterval(100, { controls: true })
6
7
 
8
+ const state = toRef(sharedState, 'timerStatus')
7
9
  const timer = computed(() => {
8
- const passed = counter.value
9
- const sec = Math.floor(passed % 60).toString().padStart(2, '0')
10
- const min = Math.floor(passed / 60).toString().padStart(2, '0')
11
- return `${min}:${sec}`
10
+ if (sharedState.timerStatus === 'stopped' && sharedState.timerStartedAt === 0)
11
+ return { h: '', m: '-', s: '--', ms: '-' }
12
+ // eslint-disable-next-line ts/no-unused-expressions
13
+ interval.counter.value
14
+ const passed = (Date.now() - sharedState.timerStartedAt)
15
+ let h = Math.floor(passed / 1000 / 60 / 60).toString()
16
+ if (h === '0')
17
+ h = ''
18
+ let min = Math.floor(passed / 1000 / 60).toString()
19
+ if (h)
20
+ min = min.padStart(2, '0')
21
+ const sec = Math.floor(passed / 1000 % 60).toString().padStart(2, '0')
22
+ const ms = Math.floor(passed % 1000 / 100).toString()
23
+ return { h, m: min, s: sec, ms }
12
24
  })
13
25
 
26
+ function reset() {
27
+ interval.pause()
28
+ sharedState.timerStatus = 'stopped'
29
+ sharedState.timerStartedAt = 0
30
+ sharedState.timerPausedAt = 0
31
+ }
32
+
33
+ function resume() {
34
+ if (sharedState.timerStatus === 'stopped') {
35
+ sharedState.timerStatus = 'running'
36
+ sharedState.timerStartedAt = Date.now()
37
+ }
38
+ else if (sharedState.timerStatus === 'paused') {
39
+ sharedState.timerStatus = 'running'
40
+ sharedState.timerStartedAt = Date.now() - (sharedState.timerPausedAt - sharedState.timerStartedAt)
41
+ }
42
+ interval.resume()
43
+ }
44
+
45
+ function pause() {
46
+ sharedState.timerStatus = 'paused'
47
+ sharedState.timerPausedAt = Date.now()
48
+ interval.pause()
49
+ }
50
+
51
+ function toggle() {
52
+ if (sharedState.timerStatus === 'running') {
53
+ pause()
54
+ }
55
+ else {
56
+ resume()
57
+ }
58
+ }
59
+
14
60
  return {
61
+ state,
15
62
  timer,
16
- isTimerActive: isActive,
17
- resetTimer: reset,
18
- toggleTimer: () => (isActive.value ? pause() : resume()),
63
+ reset,
64
+ toggle,
65
+ resume,
66
+ pause,
19
67
  }
20
68
  }
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import type { ClicksContext } from '@slidev/types'
3
+ import { computed } from 'vue'
4
+ import { useNav } from '../composables/useNav'
5
+
6
+ const props = defineProps<{
7
+ clicksContext?: ClicksContext
8
+ current?: number
9
+ }>()
10
+
11
+ const nav = useNav()
12
+ const clicksContext = computed(() => props.clicksContext ?? nav.clicksContext.value)
13
+ const current = computed(() => props.current ?? nav.currentSlideNo.value)
14
+ const { total } = nav
15
+ </script>
16
+
17
+ <template>
18
+ <div class="relative flex gap-px">
19
+ <div
20
+ v-for="i of total - 1"
21
+ :key="i" class="border-x border-b border-main h-4px transition-all"
22
+ :style="{ width: `${(1 / (total - 1) * 100)}%` }"
23
+ :class="i < current ? 'bg-primary border-primary' : ''"
24
+ >
25
+ <Transition name="fade">
26
+ <div
27
+ v-if="i === current"
28
+ class="h-full bg-primary op75 transition-all"
29
+ :style="{ width: `${clicksContext.total === 0 ? 0 : clicksContext.current / (clicksContext.total + 1) * 100}%` }"
30
+ />
31
+ </Transition>
32
+ </div>
33
+ </div>
34
+ </template>
@@ -178,11 +178,9 @@ if (__SLIDEV_FEATURE_RECORD__)
178
178
 
179
179
  <VerticalDivider v-if="!isEmbedded" />
180
180
 
181
- <div class="h-40px flex" p="l-1 t-0.5 r-2" text="sm leading-2">
182
- <div class="my-auto">
183
- {{ currentSlideNo }}
184
- <span class="opacity-50">/ {{ total }}</span>
185
- </div>
181
+ <div class="px2 my-auto">
182
+ <span class="text-lg">{{ currentSlideNo }}</span>
183
+ <span class="opacity-50 text-sm"> / {{ total }}</span>
186
184
  </div>
187
185
 
188
186
  <CustomNavControls />
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import { useTimer } from '../composables/useTimer'
3
+
4
+ const { state, timer, reset, toggle } = useTimer()
5
+ </script>
6
+
7
+ <template>
8
+ <div
9
+ class="group flex items-center justify-center pl-4 select-none"
10
+ :class="{ running: 'text-green6 dark:text-green3', paused: 'text-orange6 dark:text-orange3', stopped: 'op50' }[state]"
11
+ >
12
+ <div class="w-22px cursor-pointer">
13
+ <div class="i-carbon:time group-hover:hidden text-xl" />
14
+ <div class="group-not-hover:hidden flex flex-col items-center">
15
+ <div class="relative op-80 hover:op-100" @click="toggle">
16
+ <div v-if="state === 'running'" class="i-carbon:pause text-lg" />
17
+ <div v-else class="i-carbon:play" />
18
+ </div>
19
+ <div class="op-80 hover:op-100" @click="reset">
20
+ <div class="i-carbon:renew" />
21
+ </div>
22
+ </div>
23
+ </div>
24
+ <div class="text-3xl px-3 my-auto font-mono">
25
+ <template v-if="timer.h">
26
+ <span>{{ timer.h }}</span>
27
+ <span op50>:</span>
28
+ </template>
29
+ <span>{{ timer.m }}</span>
30
+ <span op50>:</span>
31
+ <span>{{ timer.s }}</span>
32
+ <span class="text-base op50">.{{ timer.ms }}</span>
33
+ </div>
34
+ </div>
35
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import { parseTimesplits } from '@slidev/parser/utils'
3
+ import { computed } from 'vue'
4
+ import { useNav } from '../composables/useNav'
5
+ // import { useTimer } from '../composables/useTimer'
6
+
7
+ const { slides } = useNav()
8
+
9
+ // const timer = useTimer()
10
+ const slidesWithTimesplits = computed(() => slides.value.filter(i => i.meta.slide?.frontmatter.timesplit))
11
+
12
+ const timesplits = computed(() => {
13
+ const parsed = parseTimesplits(
14
+ slidesWithTimesplits.value
15
+ .map(i => ({ no: i.no, timesplit: i.meta.slide?.frontmatter.timesplit as string })),
16
+ )
17
+ return parsed
18
+ })
19
+ </script>
20
+
21
+ <template>
22
+ <div v-if="false" class="border-b border-main relative flex">
23
+ {{ timesplits }}
24
+ </div>
25
+ </template>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "52.4.0",
4
+ "version": "52.5.0",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -61,8 +61,8 @@
61
61
  "vue": "^3.5.22",
62
62
  "vue-router": "^4.6.3",
63
63
  "yaml": "^2.8.1",
64
- "@slidev/parser": "52.4.0",
65
- "@slidev/types": "52.4.0"
64
+ "@slidev/parser": "52.5.0",
65
+ "@slidev/types": "52.5.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "vite": "^7.1.10"
package/pages/notes.vue CHANGED
@@ -6,6 +6,7 @@ import { createClicksContextBase } from '../composables/useClicks'
6
6
  import { useNav } from '../composables/useNav'
7
7
  import { slidesTitle } from '../env'
8
8
  import ClicksSlider from '../internals/ClicksSlider.vue'
9
+ import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
9
10
  import IconButton from '../internals/IconButton.vue'
10
11
  import Modal from '../internals/Modal.vue'
11
12
  import NoteDisplay from '../internals/NoteDisplay.vue'
@@ -58,14 +59,11 @@ const clicksContext = computed(() => {
58
59
  </button>
59
60
  </div>
60
61
  </Modal>
61
- <div
62
- class="fixed top-0 left-0 h-3px bg-primary transition-all duration-500"
63
- :style="{ width: `${(pageNo - 1) / (total - 1) * 100 + 1}%` }"
64
- />
65
- <div class="h-full pt-2 flex flex-col">
62
+ <div class="h-full flex flex-col">
63
+ <CurrentProgressBar :clicks-context="clicksContext" :current="pageNo" />
66
64
  <div
67
65
  ref="scroller"
68
- class="px-5 flex-auto h-full overflow-auto"
66
+ class="px-5 py-3 flex-auto h-full overflow-auto"
69
67
  :style="{ fontSize: `${fontSize}px` }"
70
68
  >
71
69
  <NoteDisplay
@@ -98,8 +96,9 @@ const clicksContext = computed(() => {
98
96
  <div class="i-carbon:help" />
99
97
  </IconButton>
100
98
  <div class="flex-auto" />
101
- <div class="p2 text-center">
102
- {{ pageNo }} / {{ total }}
99
+ <div class="px2 my-auto">
100
+ <span class="text-lg">{{ pageNo }}</span>
101
+ <span class="opacity-50 text-sm"> / {{ total }}</span>
103
102
  </div>
104
103
  </div>
105
104
  </div>
@@ -6,11 +6,11 @@ import { createClicksContextBase } from '../composables/useClicks'
6
6
  import { useDrawings } from '../composables/useDrawings'
7
7
  import { useNav } from '../composables/useNav'
8
8
  import { useSwipeControls } from '../composables/useSwipeControls'
9
- import { useTimer } from '../composables/useTimer'
10
9
  import { useWakeLock } from '../composables/useWakeLock'
11
10
  import { slidesTitle } from '../env'
12
11
  import ClicksSlider from '../internals/ClicksSlider.vue'
13
12
  import ContextMenu from '../internals/ContextMenu.vue'
13
+ import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
14
14
  import DrawingControls from '../internals/DrawingControls.vue'
15
15
  import Goto from '../internals/Goto.vue'
16
16
  import IconButton from '../internals/IconButton.vue'
@@ -23,6 +23,7 @@ import SegmentControl from '../internals/SegmentControl.vue'
23
23
  import SlideContainer from '../internals/SlideContainer.vue'
24
24
  import SlidesShow from '../internals/SlidesShow.vue'
25
25
  import SlideWrapper from '../internals/SlideWrapper.vue'
26
+ import TimerInlined from '../internals/TimerInlined.vue'
26
27
  import { onContextMenu } from '../logic/contextMenu'
27
28
  import { registerShortcuts } from '../logic/shortcuts'
28
29
  import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state'
@@ -44,7 +45,6 @@ const {
44
45
  nextRoute,
45
46
  slides,
46
47
  getPrimaryClicks,
47
- total,
48
48
  } = useNav()
49
49
  const { isDrawing } = useDrawings()
50
50
 
@@ -52,8 +52,6 @@ useHead({ title: `Presenter - ${slidesTitle}` })
52
52
 
53
53
  const notesEditing = ref(false)
54
54
 
55
- const { timer, isTimerActive, resetTimer, toggleTimer } = useTimer()
56
-
57
55
  const clicksCtxMap = computed(() => slides.value.map((route) => {
58
56
  const clicks = ref(0)
59
57
  return {
@@ -116,7 +114,10 @@ onMounted(() => {
116
114
  </script>
117
115
 
118
116
  <template>
119
- <div class="bg-main h-full slidev-presenter" pt-2px>
117
+ <div class="bg-main h-full slidev-presenter grid grid-rows-[max-content_1fr] of-hidden">
118
+ <div>
119
+ <CurrentProgressBar />
120
+ </div>
120
121
  <div class="grid-container" :class="`layout${presenterLayout}`">
121
122
  <div ref="main" class="relative grid-section main flex flex-col">
122
123
  <div flex="~ gap-4 items-center" border="b main" p1>
@@ -210,32 +211,10 @@ onMounted(() => {
210
211
  <div class="grid-section bottom flex">
211
212
  <NavControls :persist="true" class="transition" :class="inFocus ? '' : 'op25'" />
212
213
  <div flex-auto />
213
- <div class="group flex items-center justify-center pl-4 select-none">
214
- <div class="w-22px cursor-pointer">
215
- <div class="i-carbon:time group-hover:hidden text-xl" />
216
- <div class="group-not-hover:hidden flex flex-col items-center">
217
- <div class="relative op-80 hover:op-100" @click="toggleTimer">
218
- <div v-if="isTimerActive" class="i-carbon:pause text-lg" />
219
- <div v-else class="i-carbon:play" />
220
- </div>
221
- <div class="op-80 hover:op-100" @click="resetTimer">
222
- <div class="i-carbon:renew" />
223
- </div>
224
- </div>
225
- </div>
226
- <div class="text-2xl px-3 my-auto font-mono">
227
- {{ timer }}
228
- </div>
229
- </div>
214
+ <TimerInlined />
230
215
  </div>
231
216
  <DrawingControls v-if="__SLIDEV_FEATURE_DRAWINGS__" />
232
217
  </div>
233
- <div class="progress-bar">
234
- <div
235
- class="progress h-3px bg-primary transition-all"
236
- :style="{ width: `${(currentSlideNo - 1) / (total - 1) * 100 + 1}%` }"
237
- />
238
- </div>
239
218
  </div>
240
219
  <Goto />
241
220
  <QuickOverview />
@@ -248,9 +227,7 @@ onMounted(() => {
248
227
  }
249
228
 
250
229
  .grid-container {
251
- --uno: bg-gray/20;
252
- height: 100%;
253
- width: 100%;
230
+ --uno: bg-gray/20 flex-1 of-hidden;
254
231
  display: grid;
255
232
  gap: 1px 1px;
256
233
  }
@@ -305,14 +282,9 @@ onMounted(() => {
305
282
  }
306
283
  }
307
284
 
308
- .progress-bar {
309
- --uno: fixed left-0 right-0 top-0;
310
- }
311
-
312
285
  .grid-section {
313
286
  --uno: bg-main;
314
287
  }
315
-
316
288
  .grid-section.top {
317
289
  grid-area: top;
318
290
  }
package/state/shared.ts CHANGED
@@ -6,6 +6,9 @@ export interface SharedState {
6
6
  page: number
7
7
  clicks: number
8
8
  clicksTotal: number
9
+ timerStatus: 'stopped' | 'running' | 'paused'
10
+ timerStartedAt: number
11
+ timerPausedAt: number
9
12
 
10
13
  cursor?: {
11
14
  x: number
@@ -23,6 +26,9 @@ const { init, onPatch, onUpdate, patch, state } = createSyncState<SharedState>(s
23
26
  page: 1,
24
27
  clicks: 0,
25
28
  clicksTotal: 0,
29
+ timerStatus: 'stopped',
30
+ timerStartedAt: 0,
31
+ timerPausedAt: 0,
26
32
  })
27
33
 
28
34
  export {