@slidev/client 51.7.1 → 51.8.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.
@@ -12,12 +12,13 @@ Learn more: https://sli.dev/guide/syntax.html#line-highlighting
12
12
  -->
13
13
 
14
14
  <script setup lang="ts">
15
- import type { PropType } from 'vue'
15
+ import type { PropType, Ref } from 'vue'
16
16
  import { useClipboard } from '@vueuse/core'
17
- import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
17
+ import { computed, inject, onMounted, onUnmounted, ref, watchEffect } from 'vue'
18
18
  import { CLASS_VCLICK_HIDDEN, CLICKS_MAX } from '../constants'
19
19
  import { useSlideContext } from '../context'
20
20
  import { configs } from '../env'
21
+ import TitleIcon from '../internals/TitleIcon.vue'
21
22
  import { makeId, updateCodeHighlightRange } from '../logic/utils'
22
23
 
23
24
  const props = defineProps({
@@ -45,6 +46,10 @@ const props = defineProps({
45
46
  type: String,
46
47
  default: undefined,
47
48
  },
49
+ title: {
50
+ type: String,
51
+ default: undefined,
52
+ },
48
53
  })
49
54
 
50
55
  const { $clicksContext: clicks } = useSlideContext()
@@ -115,6 +120,13 @@ function copyCode() {
115
120
  if (code)
116
121
  copy(code)
117
122
  }
123
+
124
+ // code block title
125
+ const activeTitle = inject<Ref<string> | null>('activeTitle', null)
126
+
127
+ const isBlockTitleShow = computed(() => {
128
+ return activeTitle === null && props.title
129
+ })
118
130
  </script>
119
131
 
120
132
  <template>
@@ -123,17 +135,26 @@ function copyCode() {
123
135
  class="slidev-code-wrapper relative group"
124
136
  :class="{
125
137
  'slidev-code-line-numbers': props.lines,
138
+ 'active': activeTitle === title,
126
139
  }"
127
140
  :style="{
128
141
  'max-height': props.maxHeight,
129
142
  'overflow-y': props.maxHeight ? 'scroll' : undefined,
130
143
  '--start': props.startLine,
131
144
  }"
145
+ :data-title="title"
132
146
  >
147
+ <div v-if="isBlockTitleShow" class="slidev-code-block-title">
148
+ <TitleIcon :title="title" />
149
+ <div class="leading-1em">
150
+ {{ title.replace(/~([^~]+)~/g, '').trim() }}
151
+ </div>
152
+ </div>
133
153
  <slot />
134
154
  <button
135
155
  v-if="configs.codeCopy"
136
- class="slidev-code-copy absolute top-0 right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
156
+ class="slidev-code-copy absolute right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
157
+ :class="isBlockTitleShow ? 'top-10' : 'top-0'"
137
158
  :title="copied ? 'Copied' : 'Copy'" @click="copyCode()"
138
159
  >
139
160
  <ph-check-circle v-if="copied" class="p-2 w-8 h-8" />
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, provide, ref, useTemplateRef } from 'vue'
3
+ import TitleIcon from '../internals/TitleIcon.vue'
4
+
5
+ const codeGroupBlocksRef = useTemplateRef('codeGroupBlocksRef')
6
+ const activeTitle = ref('')
7
+
8
+ provide('activeTitle', activeTitle)
9
+ const tabs = ref<string[]>([])
10
+
11
+ onMounted(() => {
12
+ const codeGroupBlocks = codeGroupBlocksRef.value
13
+ let isActiveSet = false
14
+
15
+ codeGroupBlocks?.querySelectorAll('.slidev-code-wrapper')?.forEach((block) => {
16
+ const title = block.getAttribute('data-title') || ''
17
+ if (title) {
18
+ if (!isActiveSet) {
19
+ activeTitle.value = title
20
+ isActiveSet = true
21
+ }
22
+ tabs.value.push(title)
23
+ }
24
+ })
25
+ })
26
+ </script>
27
+
28
+ <template>
29
+ <div class="slidev-code-group">
30
+ <div class="slidev-code-group-tabs">
31
+ <div v-for="tab in tabs" :key="tab" class="flex items-center">
32
+ <div
33
+ class="slidev-code-tab"
34
+ :style="{
35
+ borderColor: activeTitle === tab ? 'var(--slidev-theme-primary)' : 'transparent',
36
+ color: activeTitle === tab ? 'var(--slidev-code-tab-active-text-color)' : 'var(--slidev-code-tab-text-color)',
37
+ }"
38
+ @click="activeTitle = tab"
39
+ >
40
+ <TitleIcon :title="tab" />
41
+
42
+ <div>
43
+ {{ tab.replace(/~([^~]+)~/g, '').trim() }}
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ <div ref="codeGroupBlocksRef" class="slidev-code-group-blocks">
49
+ <slot />
50
+ </div>
51
+ </div>
52
+ </template>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useVModel } from '@vueuse/core'
3
3
  import { nextTick } from 'vue'
4
- import { getFilename, mimeType, recordCamera, recorder, recordingName } from '../logic/recording'
4
+ import { bitRate, frameRate, getFilename, mimeType, recordCamera, recorder, recordingName, resolution } from '../logic/recording'
5
5
  import DevicesSelectors from './DevicesSelectors.vue'
6
6
  import Modal from './Modal.vue'
7
7
 
@@ -18,6 +18,14 @@ const value = useVModel(props, 'modelValue', emit)
18
18
 
19
19
  const { startRecording } = recorder
20
20
 
21
+ const frameRateOptions = [15, 24, 30, 60]
22
+ const resolutionOptions = [
23
+ { value: '1280x720', label: '720p (1280x720)' },
24
+ { value: '1920x1080', label: '1080p (1920x1080)' },
25
+ { value: '2560x1440', label: '1440p (2560x1440)' },
26
+ { value: '3840x2160', label: '2160p (3840x2160)' },
27
+ ]
28
+
21
29
  function close() {
22
30
  value.value = false
23
31
  }
@@ -27,6 +35,7 @@ async function start() {
27
35
  await nextTick()
28
36
  startRecording({
29
37
  mimeType: mimeType.value,
38
+ bitsPerSecond: bitRate.value * 1024,
30
39
  })
31
40
  }
32
41
  </script>
@@ -51,6 +60,48 @@ async function start() {
51
60
  <div>This will be used in the output filename that might <br>help you better organize your recording chips.</div>
52
61
  </div>
53
62
  </div>
63
+
64
+ <div class="form-text">
65
+ <label for="framerate">Frame Rate</label>
66
+ <select
67
+ v-model="frameRate"
68
+ class="bg-transparent text-current border border-main rounded px-2 py-1"
69
+ name="framerate"
70
+ >
71
+ <option v-for="rate in frameRateOptions" :key="rate" :value="rate">
72
+ {{ rate }} fps
73
+ </option>
74
+ </select>
75
+ </div>
76
+
77
+ <div class="form-text">
78
+ <label for="resolution">Resolution</label>
79
+ <select
80
+ v-model="resolution"
81
+ class="bg-transparent text-current border border-main rounded px-2 py-1"
82
+ name="resolution"
83
+ >
84
+ <option v-for="res in resolutionOptions" :key="res.value" :value="res.value">
85
+ {{ res.label }}
86
+ </option>
87
+ </select>
88
+ </div>
89
+
90
+ <div class="form-text">
91
+ <label for="bitrate">Bitrate</label>
92
+ <div class="relative">
93
+ <input
94
+ v-model.number="bitRate"
95
+ type="number"
96
+ min="1000"
97
+ step="1000"
98
+ class="bg-transparent text-current border border-main rounded px-2 py-1 w-full pr-12"
99
+ name="bitrate"
100
+ >
101
+ <span class="absolute right-3 top-1/2 transform -translate-y-1/2 text-sm opacity-50">kbps</span>
102
+ </div>
103
+ </div>
104
+
54
105
  <div class="form-check">
55
106
  <input
56
107
  v-model="recordCamera"
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title: string
4
+ }>()
5
+
6
+ const builtinIcons: Record<string, string> = {
7
+ // package managers
8
+ 'pnpm': 'i-vscode-icons:file-type-light-pnpm',
9
+ 'npm': 'i-vscode-icons:file-type-npm',
10
+ 'yarn': 'i-vscode-icons:file-type-yarn',
11
+ 'bun': 'i-vscode-icons:file-type-bun',
12
+ 'deno': 'i-vscode-icons:file-type-deno',
13
+ // frameworks
14
+ 'vue': 'i-vscode-icons:file-type-vue',
15
+ 'svelte': 'i-vscode-icons:file-type-svelte',
16
+ 'angular': 'i-vscode-icons:file-type-angular',
17
+ 'react': 'i-vscode-icons:file-type-reactjs',
18
+ 'next': 'i-vscode-icons:file-type-light-next',
19
+ 'nuxt': 'i-vscode-icons:file-type-nuxt',
20
+ 'solid': 'logos:solidjs-icon',
21
+ 'astro': 'i-vscode-icons:file-type-light-astro',
22
+ // bundlers
23
+ 'rollup': 'i-vscode-icons:file-type-rollup',
24
+ 'webpack': 'i-vscode-icons:file-type-webpack',
25
+ 'vite': 'i-vscode-icons:file-type-vite',
26
+ 'esbuild': 'i-vscode-icons:file-type-esbuild',
27
+ // configuration files
28
+ 'package.json': 'i-vscode-icons:file-type-node',
29
+ 'tsconfig.json': 'i-vscode-icons:file-type-tsconfig',
30
+ '.npmrc': 'i-vscode-icons:file-type-npm',
31
+ '.editorconfig': 'i-vscode-icons:file-type-editorconfig',
32
+ '.eslintrc': 'i-vscode-icons:file-type-eslint',
33
+ '.eslintignore': 'i-vscode-icons:file-type-eslint',
34
+ 'eslint.config': 'i-vscode-icons:file-type-eslint',
35
+ '.gitignore': 'i-vscode-icons:file-type-git',
36
+ '.gitattributes': 'i-vscode-icons:file-type-git',
37
+ '.env': 'i-vscode-icons:file-type-dotenv',
38
+ '.env.example': 'i-vscode-icons:file-type-dotenv',
39
+ '.vscode': 'i-vscode-icons:file-type-vscode',
40
+ 'tailwind.config': 'vscode-icons:file-type-tailwind',
41
+ 'uno.config': 'i-vscode-icons:file-type-unocss',
42
+ 'unocss.config': 'i-vscode-icons:file-type-unocss',
43
+ '.oxlintrc': 'i-vscode-icons:file-type-oxlint',
44
+ 'vue.config': 'i-vscode-icons:file-type-vueconfig',
45
+ // filename extensions
46
+ '.mts': 'i-vscode-icons:file-type-typescript',
47
+ '.cts': 'i-vscode-icons:file-type-typescript',
48
+ '.ts': 'i-vscode-icons:file-type-typescript',
49
+ '.tsx': 'i-vscode-icons:file-type-typescript',
50
+ '.mjs': 'i-vscode-icons:file-type-js',
51
+ '.cjs': 'i-vscode-icons:file-type-js',
52
+ '.json': 'i-vscode-icons:file-type-json',
53
+ '.js': 'i-vscode-icons:file-type-js',
54
+ '.jsx': 'i-vscode-icons:file-type-js',
55
+ '.md': 'i-vscode-icons:file-type-markdown',
56
+ '.py': 'i-vscode-icons:file-type-python',
57
+ '.ico': 'i-vscode-icons:file-type-favicon',
58
+ '.html': 'i-vscode-icons:file-type-html',
59
+ '.css': 'i-vscode-icons:file-type-css',
60
+ '.scss': 'i-vscode-icons:file-type-scss',
61
+ '.yml': 'i-vscode-icons:file-type-light-yaml',
62
+ '.yaml': 'i-vscode-icons:file-type-light-yaml',
63
+ '.php': 'i-vscode-icons:file-type-php',
64
+ }
65
+
66
+ function matchIcon(title: string) {
67
+ const colonMatch = title.match(/~([^~]+)~/g)
68
+ if (colonMatch && colonMatch.length > 0) {
69
+ const icon = colonMatch[0].slice(1, -1)
70
+ return icon
71
+ }
72
+
73
+ const sortedKeys = Object.keys(builtinIcons).sort((a, b) => b.length - a.length)
74
+ for (const key of sortedKeys) {
75
+ if (title.toLowerCase().includes(key.toLowerCase())) {
76
+ return builtinIcons[key]
77
+ }
78
+ }
79
+ return ''
80
+ }
81
+ </script>
82
+
83
+ <template>
84
+ <div v-if="matchIcon(title)" :class="`${matchIcon(title)} w-3.5 h-3.5 relative`" />
85
+ </template>
@@ -12,6 +12,9 @@ type MimeType = Defined<RecorderOptions['mimeType']>
12
12
  export const recordingName = ref('')
13
13
  export const recordCamera = ref(true)
14
14
  export const mimeType = useLocalStorage<MimeType>('slidev-record-mimetype', 'video/webm')
15
+ export const frameRate = useLocalStorage<number>('slidev-record-framerate', 30)
16
+ export const bitRate = useLocalStorage<number>('slidev-record-bitrate', 8192)
17
+ export const resolution = useLocalStorage<string>('slidev-record-resolution', '1920x1080')
15
18
 
16
19
  export const mimeExtMap: Record<string, string> = {
17
20
  'video/webm': 'webm',
@@ -78,7 +81,6 @@ export function useRecording() {
78
81
 
79
82
  const config: RecorderOptions = {
80
83
  type: 'video',
81
- bitsPerSecond: 4 * 256 * 8 * 1024,
82
84
  // Extending recording limit as default is only 1h (see https://github.com/muaz-khan/RecordRTC/issues/144)
83
85
  timeSlice: 24 * 60 * 60 * 1000,
84
86
  }
@@ -141,12 +143,13 @@ export function useRecording() {
141
143
  const { default: Recorder } = await import('recordrtc')
142
144
  await startCameraStream()
143
145
 
146
+ const [width, height] = resolution.value.split('x').map(Number)
144
147
  streamCapture.value = await navigator.mediaDevices.getDisplayMedia({
145
148
  video: {
146
149
  // aspectRatio: 1.6,
147
- frameRate: 15,
148
- width: 3840,
149
- height: 2160,
150
+ frameRate: frameRate.value,
151
+ width,
152
+ height,
150
153
  // @ts-expect-error missing types
151
154
  cursor: 'motion',
152
155
  resizeMode: 'crop-and-scale',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "51.7.1",
4
+ "version": "51.8.0",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -36,8 +36,8 @@
36
36
  "@shikijs/monaco": "^3.4.2",
37
37
  "@shikijs/vitepress-twoslash": "^3.4.2",
38
38
  "@slidev/rough-notation": "^0.1.0",
39
- "@typescript/ata": "^0.9.7",
40
- "@unhead/vue": "^2.0.9",
39
+ "@typescript/ata": "^0.9.8",
40
+ "@unhead/vue": "^2.0.10",
41
41
  "@unocss/reset": "^66.1.2",
42
42
  "@vueuse/core": "^13.2.0",
43
43
  "@vueuse/math": "^13.2.0",
@@ -61,8 +61,8 @@
61
61
  "vue": "^3.5.14",
62
62
  "vue-router": "^4.5.1",
63
63
  "yaml": "^2.8.0",
64
- "@slidev/parser": "51.7.1",
65
- "@slidev/types": "51.7.1"
64
+ "@slidev/parser": "51.8.0",
65
+ "@slidev/types": "51.8.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "vite": "^6.3.5"
package/styles/code.css CHANGED
@@ -44,6 +44,64 @@ html:not(.dark) .shiki span {
44
44
  overflow: auto;
45
45
  }
46
46
 
47
+ .slidev-code-block-title,
48
+ .slidev-code-group-tabs {
49
+ background: var(--slidev-code-background);
50
+ color: var(--slidev-code-tab-text-color);
51
+ padding-left: var(--slidev-code-padding);
52
+ padding-right: var(--slidev-code-padding);
53
+ font-size: var(--slidev-code-tab-font-size);
54
+ border-radius: var(--slidev-code-radius) var(--slidev-code-radius) 0 0;
55
+ box-shadow: inset 0 -1px var(--slidev-code-tab-divider);
56
+ display: flex;
57
+ gap: 8px;
58
+ align-items: center;
59
+ }
60
+
61
+ .slidev-code-block-title {
62
+ padding: var(--slidev-code-padding);
63
+ }
64
+
65
+ .slidev-code-tab {
66
+ font-size: var(--slidev-code-tab-font-size);
67
+ white-space: nowrap;
68
+ cursor: pointer;
69
+ transition: color 0.25s;
70
+ padding: var(--slidev-code-padding);
71
+ border-bottom: 2px solid transparent;
72
+ color: var(--slidev-code-tab-text-color);
73
+ position: relative;
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 8px;
77
+ }
78
+
79
+ .slidev-code-tab:hover {
80
+ color: var(--slidev-code-tab-active-text-color) !important;
81
+ }
82
+
83
+ .slidev-code-group-blocks .slidev-code-wrapper {
84
+ margin: 0 !important;
85
+ }
86
+
87
+ .slidev-code-group-blocks .slidev-code {
88
+ border-radius: 0 0 var(--slidev-code-radius) var(--slidev-code-radius) !important;
89
+ }
90
+
91
+ .slidev-code-group-blocks .slidev-code-wrapper.active {
92
+ display: block;
93
+ }
94
+
95
+ .slidev-code-group-blocks .slidev-code-wrapper {
96
+ display: none;
97
+ }
98
+
99
+ .slidev-code-block-title + .slidev-code,
100
+ .slidev-code-group-tabs + .slidev-code {
101
+ border-top-left-radius: 0 !important;
102
+ border-top-right-radius: 0 !important;
103
+ }
104
+
47
105
  .slidev-code .slidev-code-highlighted {
48
106
  }
49
107
  .slidev-code .slidev-code-dishonored {
package/styles/vars.css CHANGED
@@ -12,9 +12,17 @@
12
12
  --slidev-transition-duration: 0.5s;
13
13
  --slidev-slide-container-background: black;
14
14
  --slidev-controls-foreground: white;
15
+
16
+ --slidev-code-tab-divider: #e5e5e5;
17
+ --slidev-code-tab-text-color: #67676c;
18
+ --slidev-code-tab-font-size: 12px;
19
+ --slidev-code-tab-active-text-color: #3c3c43;
15
20
  }
16
21
 
17
22
  html.dark {
18
23
  --slidev-code-background: #1b1b1b;
19
24
  --slidev-code-foreground: #eee;
25
+ --slidev-code-tab-divider: #222222;
26
+ --slidev-code-tab-text-color: #98989f;
27
+ --slidev-code-tab-active-text-color: #dfdfd6;
20
28
  }