@slidev/client 52.4.0 → 52.6.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,103 @@
1
+ import { parseTimeString } from '@slidev/parser/utils'
1
2
  import { useInterval } from '@vueuse/core'
2
- import { computed } from 'vue'
3
+ import { computed, toRef } from 'vue'
4
+ import { configs } from '../env'
5
+ import { sharedState } from '../state/shared'
3
6
 
4
7
  export function useTimer() {
5
- const { counter, isActive, reset, pause, resume } = useInterval(1000, { controls: true })
8
+ const mode = computed(() => configs.timer || 'stopwatch')
9
+ const duration = computed(() => parseTimeString(configs.duration).seconds)
10
+ const interval = useInterval(100, { controls: true })
11
+
12
+ const state = toRef(sharedState, 'timer')
13
+ const status = computed(() => state.value?.status)
14
+ const passedMs = computed(() => {
15
+ // eslint-disable-next-line ts/no-unused-expressions
16
+ interval.counter.value
17
+ if (state.value.status === 'stopped' || !state.value.startedAt)
18
+ return 0
19
+ return Date.now() - state.value.startedAt
20
+ })
21
+ const passed = computed(() => passedMs.value / 1000)
22
+ const percentage = computed(() => passed.value / duration.value * 100)
6
23
 
7
24
  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}`
25
+ if (mode.value === 'stopwatch') {
26
+ if (state.value.status === 'stopped' || !state.value.startedAt)
27
+ return { h: '', m: '-', s: '--', ms: '-' }
28
+ }
29
+
30
+ const total = mode.value === 'countdown'
31
+ ? duration.value * 1000 - passedMs.value
32
+ : passedMs.value
33
+
34
+ let h = Math.floor(total / 1000 / 60 / 60).toString()
35
+ if (h === '0')
36
+ h = ''
37
+ let min = Math.floor(total / 1000 / 60 % 60).toString()
38
+ if (h)
39
+ min = min.padStart(2, '0')
40
+ const sec = Math.floor(total / 1000 % 60).toString().padStart(2, '0')
41
+ const ms = Math.floor(total % 1000 / 100).toString()
42
+
43
+ return {
44
+ h,
45
+ m: min,
46
+ s: sec,
47
+ ms,
48
+ }
12
49
  })
13
50
 
51
+ function reset() {
52
+ interval.pause()
53
+ state.value = {
54
+ status: 'stopped',
55
+ slides: {},
56
+ startedAt: 0,
57
+ pausedAt: 0,
58
+ }
59
+ }
60
+
61
+ function resume() {
62
+ if (!state.value)
63
+ return
64
+ if (state.value?.status === 'stopped') {
65
+ state.value.status = 'running'
66
+ state.value.startedAt = Date.now()
67
+ }
68
+ else if (state.value.status === 'paused') {
69
+ state.value.status = 'running'
70
+ state.value.startedAt = Date.now() - (state.value.pausedAt - state.value.startedAt)
71
+ }
72
+ interval.resume()
73
+ }
74
+
75
+ function pause() {
76
+ state.value.status = 'paused'
77
+ state.value.pausedAt = Date.now()
78
+ interval.pause()
79
+ }
80
+
81
+ function toggle() {
82
+ if (state.value.status === 'running') {
83
+ pause()
84
+ }
85
+ else {
86
+ resume()
87
+ }
88
+ }
89
+
14
90
  return {
91
+ state,
92
+ status,
15
93
  timer,
16
- isTimerActive: isActive,
17
- resetTimer: reset,
18
- toggleTimer: () => (isActive.value ? pause() : resume()),
94
+ reset,
95
+ toggle,
96
+ resume,
97
+ pause,
98
+ passed,
99
+ percentage,
100
+ duration,
101
+ mode,
19
102
  }
20
103
  }
@@ -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,49 @@
1
+ <script setup lang="ts">
2
+ // import { parseTimesplits } from '@slidev/parser/utils'
3
+ import { computed, reactive } from 'vue'
4
+ // import { useNav } from '../composables/useNav'
5
+ import { useTimer } from '../composables/useTimer'
6
+
7
+ // const { slides } = useNav()
8
+
9
+ const timer = reactive(useTimer())
10
+ // TODO: timesplit
11
+ // const slidesWithTimesplits = computed(() => slides.value.filter(i => i.meta.slide?.frontmatter.timesplit))
12
+
13
+ // const _timesplits = computed(() => {
14
+ // const parsed = parseTimesplits(
15
+ // slidesWithTimesplits.value
16
+ // .map(i => ({ no: i.no, timesplit: i.meta.slide?.frontmatter.timesplit as string })),
17
+ // )
18
+ // return parsed
19
+ // })
20
+
21
+ // TODO: maybe make it configurable, or somehow more smart
22
+ const color = computed(() => {
23
+ if (timer.status === 'stopped')
24
+ return 'op50'
25
+ if (timer.status === 'paused')
26
+ return 'bg-blue'
27
+
28
+ if (timer.percentage > 80)
29
+ return 'bg-yellow'
30
+ else if (timer.percentage > 100)
31
+ return 'bg-red'
32
+ else
33
+ return 'bg-green'
34
+ })
35
+ </script>
36
+
37
+ <template>
38
+ <div
39
+ class="border-b mt-px border-main relative flex h-4px"
40
+ >
41
+ <div
42
+ v-if="timer.status !== 'stopped'"
43
+ class="h-4px"
44
+ :class="color"
45
+ :style="{ width: `${timer.percentage}%` }"
46
+ />
47
+ <!-- {{ timesplits }} -->
48
+ </div>
49
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useTimer } from '../composables/useTimer'
4
+
5
+ const { status, percentage, mode, timer, reset, toggle } = useTimer()
6
+
7
+ const color = computed(() => {
8
+ if (status.value === 'stopped')
9
+ return 'op50'
10
+ if (status.value === 'paused')
11
+ return 'text-blue6 dark:text-blue3'
12
+
13
+ if (percentage.value > 80)
14
+ return 'text-yellow6 dark:text-yellow3'
15
+ else if (percentage.value > 100)
16
+ return 'text-red6 dark:text-red3'
17
+ else
18
+ return 'text-green6 dark:text-green3'
19
+ })
20
+ </script>
21
+
22
+ <template>
23
+ <div
24
+ class="group flex items-center justify-center pl-4 select-none"
25
+ :class="color"
26
+ >
27
+ <div class="w-22px cursor-pointer">
28
+ <div
29
+ class="group-hover:hidden text-2xl"
30
+ :class="mode === 'countdown' ? 'i-carbon:timer' : 'i-carbon:time'"
31
+ />
32
+ <div class="group-not-hover:hidden flex flex-col items-center">
33
+ <div class="relative op-80 hover:op-100" @click="toggle">
34
+ <div v-if="status === 'running'" class="i-carbon:pause text-lg" />
35
+ <div v-else class="i-carbon:play" />
36
+ </div>
37
+ <div class="op-80 hover:op-100" @click="reset">
38
+ <div class="i-carbon:renew" />
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <div class="text-3xl px-3 my-auto font-mono">
43
+ <template v-if="timer.h">
44
+ <span>{{ timer.h }}</span>
45
+ <span op50>:</span>
46
+ </template>
47
+ <span>{{ timer.m }}</span>
48
+ <span op50>:</span>
49
+ <span>{{ timer.s }}</span>
50
+ <span class="text-base op50">.{{ timer.ms }}</span>
51
+ </div>
52
+ </div>
53
+ </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.6.0",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -39,8 +39,8 @@
39
39
  "@typescript/ata": "^0.9.8",
40
40
  "@unhead/vue": "^2.0.19",
41
41
  "@unocss/reset": "^66.5.4",
42
- "@vueuse/core": "^13.9.0",
43
- "@vueuse/math": "^13.9.0",
42
+ "@vueuse/core": "^14.0.0",
43
+ "@vueuse/math": "^14.0.0",
44
44
  "@vueuse/motion": "^3.0.3",
45
45
  "drauu": "^0.4.3",
46
46
  "file-saver": "^2.0.5",
@@ -61,10 +61,10 @@
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.6.0",
65
+ "@slidev/types": "52.6.0"
66
66
  },
67
67
  "devDependencies": {
68
- "vite": "^7.1.10"
68
+ "vite": "^7.1.12"
69
69
  }
70
70
  }
package/pages/notes.vue CHANGED
@@ -6,9 +6,11 @@ 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'
13
+ import TimerBar from '../internals/TimerBar.vue'
12
14
  import { fullscreen } from '../state'
13
15
  import { sharedState } from '../state/shared'
14
16
 
@@ -58,14 +60,12 @@ const clicksContext = computed(() => {
58
60
  </button>
59
61
  </div>
60
62
  </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">
63
+ <div class="h-full flex flex-col">
64
+ <CurrentProgressBar :clicks-context="clicksContext" :current="pageNo" />
65
+ <TimerBar />
66
66
  <div
67
67
  ref="scroller"
68
- class="px-5 flex-auto h-full overflow-auto"
68
+ class="px-5 py-3 flex-auto h-full overflow-auto"
69
69
  :style="{ fontSize: `${fontSize}px` }"
70
70
  >
71
71
  <NoteDisplay
@@ -98,8 +98,9 @@ const clicksContext = computed(() => {
98
98
  <div class="i-carbon:help" />
99
99
  </IconButton>
100
100
  <div class="flex-auto" />
101
- <div class="p2 text-center">
102
- {{ pageNo }} / {{ total }}
101
+ <div class="px2 my-auto">
102
+ <span class="text-lg">{{ pageNo }}</span>
103
+ <span class="opacity-50 text-sm"> / {{ total }}</span>
103
104
  </div>
104
105
  </div>
105
106
  </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,8 @@ 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 TimerBar from '../internals/TimerBar.vue'
27
+ import TimerInlined from '../internals/TimerInlined.vue'
26
28
  import { onContextMenu } from '../logic/contextMenu'
27
29
  import { registerShortcuts } from '../logic/shortcuts'
28
30
  import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state'
@@ -44,7 +46,6 @@ const {
44
46
  nextRoute,
45
47
  slides,
46
48
  getPrimaryClicks,
47
- total,
48
49
  } = useNav()
49
50
  const { isDrawing } = useDrawings()
50
51
 
@@ -52,8 +53,6 @@ useHead({ title: `Presenter - ${slidesTitle}` })
52
53
 
53
54
  const notesEditing = ref(false)
54
55
 
55
- const { timer, isTimerActive, resetTimer, toggleTimer } = useTimer()
56
-
57
56
  const clicksCtxMap = computed(() => slides.value.map((route) => {
58
57
  const clicks = ref(0)
59
58
  return {
@@ -116,7 +115,11 @@ onMounted(() => {
116
115
  </script>
117
116
 
118
117
  <template>
119
- <div class="bg-main h-full slidev-presenter" pt-2px>
118
+ <div class="bg-main h-full slidev-presenter grid grid-rows-[max-content_1fr] of-hidden">
119
+ <div>
120
+ <CurrentProgressBar />
121
+ <TimerBar />
122
+ </div>
120
123
  <div class="grid-container" :class="`layout${presenterLayout}`">
121
124
  <div ref="main" class="relative grid-section main flex flex-col">
122
125
  <div flex="~ gap-4 items-center" border="b main" p1>
@@ -210,32 +213,10 @@ onMounted(() => {
210
213
  <div class="grid-section bottom flex">
211
214
  <NavControls :persist="true" class="transition" :class="inFocus ? '' : 'op25'" />
212
215
  <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>
216
+ <TimerInlined />
230
217
  </div>
231
218
  <DrawingControls v-if="__SLIDEV_FEATURE_DRAWINGS__" />
232
219
  </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
220
  </div>
240
221
  <Goto />
241
222
  <QuickOverview />
@@ -248,9 +229,7 @@ onMounted(() => {
248
229
  }
249
230
 
250
231
  .grid-container {
251
- --uno: bg-gray/20;
252
- height: 100%;
253
- width: 100%;
232
+ --uno: bg-gray/20 flex-1 of-hidden;
254
233
  display: grid;
255
234
  gap: 1px 1px;
256
235
  }
@@ -305,14 +284,9 @@ onMounted(() => {
305
284
  }
306
285
  }
307
286
 
308
- .progress-bar {
309
- --uno: fixed left-0 right-0 top-0;
310
- }
311
-
312
287
  .grid-section {
313
288
  --uno: bg-main;
314
289
  }
315
-
316
290
  .grid-section.top {
317
291
  grid-area: top;
318
292
  }
package/state/shared.ts CHANGED
@@ -7,6 +7,16 @@ export interface SharedState {
7
7
  clicks: number
8
8
  clicksTotal: number
9
9
 
10
+ timer: {
11
+ status: 'stopped' | 'running' | 'paused'
12
+ slides: Record<number, {
13
+ start?: number
14
+ end?: number
15
+ }>
16
+ startedAt: number
17
+ pausedAt: number
18
+ }
19
+
10
20
  cursor?: {
11
21
  x: number
12
22
  y: number
@@ -23,6 +33,12 @@ const { init, onPatch, onUpdate, patch, state } = createSyncState<SharedState>(s
23
33
  page: 1,
24
34
  clicks: 0,
25
35
  clicksTotal: 0,
36
+ timer: {
37
+ status: 'stopped',
38
+ slides: {},
39
+ startedAt: 0,
40
+ pausedAt: 0,
41
+ },
26
42
  })
27
43
 
28
44
  export {