@slidev/client 52.19.1 → 52.19.2

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.
@@ -78,13 +78,19 @@ onMounted(() => {
78
78
  return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
79
79
  })
80
80
 
81
+ let hiddenByRange = false
82
+
81
83
  watchEffect(() => {
82
84
  if (!el.value)
83
85
  return
84
86
 
85
87
  let rangeStr = props.ranges[index.value] ?? finallyRange.value
86
88
  const hide = rangeStr === 'hide'
87
- el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
89
+ // Only toggle the class when a `hide` range set it, so a click directive on the same element keeps control
90
+ if (hide || hiddenByRange) {
91
+ el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
92
+ hiddenByRange = hide
93
+ }
88
94
  if (hide)
89
95
  rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
90
96
 
@@ -68,13 +68,19 @@ onMounted(() => {
68
68
  return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
69
69
  })
70
70
 
71
+ let hiddenByRange = false
72
+
71
73
  watchEffect(() => {
72
74
  if (!el.value)
73
75
  return
74
76
 
75
77
  let rangeStr = props.ranges[index.value] ?? finallyRange.value
76
78
  const hide = rangeStr === 'hide'
77
- el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
79
+ // Only toggle the class when a `hide` range set it, so a click directive on the same element keeps control
80
+ if (hide || hiddenByRange) {
81
+ el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
82
+ hiddenByRange = hide
83
+ }
78
84
  if (hide)
79
85
  rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
80
86
 
package/builtin/Tweet.vue CHANGED
@@ -4,14 +4,17 @@ A simple wrapper for embedded Tweet
4
4
  Usage:
5
5
 
6
6
  <Tweet id="20" />
7
+ <Tweet url="https://x.com/jack/status/20" />
7
8
  -->
8
9
 
9
10
  <script setup lang="ts">
10
11
  import { onMounted, ref } from 'vue'
11
12
  import { isDark } from '../logic/dark'
13
+ import { resolveTweetId } from '../logic/tweet'
12
14
 
13
15
  const props = defineProps<{
14
- id: string | number
16
+ id?: string | number
17
+ url?: string
15
18
  scale?: string | number
16
19
  conversation?: string
17
20
  cards?: 'hidden' | 'visible'
@@ -23,6 +26,14 @@ const loaded = ref(false)
23
26
  const tweetNotFound = ref(false)
24
27
 
25
28
  async function create(retries = 10) {
29
+ const tweetId = resolveTweetId(props.id, props.url)
30
+ if (!tweetId) {
31
+ loaded.value = true
32
+ tweetNotFound.value = true
33
+ console.error('Tweet requires a valid id or X post URL.')
34
+ return
35
+ }
36
+
26
37
  // @ts-expect-error global
27
38
  if (!window.twttr?.widgets?.createTweet) {
28
39
  if (retries <= 0)
@@ -32,7 +43,7 @@ async function create(retries = 10) {
32
43
  }
33
44
  // @ts-expect-error global
34
45
  const element = await window.twttr.widgets.createTweet(
35
- props.id.toString(),
46
+ tweetId,
36
47
  tweet.value,
37
48
  {
38
49
  theme: isDark.value ? 'dark' : 'light',
@@ -56,7 +67,7 @@ onMounted(() => {
56
67
  <div v-if="!loaded || tweetNotFound" class="w-30 h-30 my-10px bg-gray-400 bg-opacity-10 rounded-lg flex opacity-50">
57
68
  <div class="m-auto animate-pulse text-4xl">
58
69
  <div class="i-carbon:logo-twitter" />
59
- <span v-if="tweetNotFound">Could not load tweet with id="{{ props.id }}"</span>
70
+ <span v-if="tweetNotFound">Could not load tweet</span>
60
71
  </div>
61
72
  </div>
62
73
  </div>
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolveTweetId } from './tweet'
3
+
4
+ describe('resolveTweetId', () => {
5
+ it('keeps the existing id input', () => {
6
+ expect(resolveTweetId(123, undefined)).toBe('123')
7
+ expect(resolveTweetId(' 456 ', undefined)).toBe('456')
8
+ })
9
+
10
+ it('prefers id when both inputs are present', () => {
11
+ expect(resolveTweetId('123', 'https://x.com/slidevjs/status/456')).toBe('123')
12
+ })
13
+
14
+ it.each([
15
+ 'https://x.com/slidevjs/status/123',
16
+ 'https://www.x.com/slidevjs/status/123?s=20',
17
+ 'https://twitter.com/slidevjs/status/123',
18
+ 'https://mobile.twitter.com/slidevjs/status/123/photo/1',
19
+ 'https://x.com/i/web/status/123',
20
+ ])('extracts an id from %s', (url) => {
21
+ expect(resolveTweetId(undefined, url)).toBe('123')
22
+ })
23
+
24
+ it.each([
25
+ 'https://example.com/slidevjs/status/123',
26
+ 'https://x.com.example.com/slidevjs/status/123',
27
+ 'https://x.com/slidevjs/status/not-a-number',
28
+ 'javascript:alert(1)',
29
+ 'not a URL',
30
+ ])('rejects an unsupported source URL: %s', (url) => {
31
+ expect(resolveTweetId(undefined, url)).toBeUndefined()
32
+ })
33
+ })
package/logic/tweet.ts ADDED
@@ -0,0 +1,37 @@
1
+ const TWEET_HOSTS = new Set([
2
+ 'mobile.twitter.com',
3
+ 'mobile.x.com',
4
+ 'twitter.com',
5
+ 'www.twitter.com',
6
+ 'www.x.com',
7
+ 'x.com',
8
+ ])
9
+
10
+ export function resolveTweetId(
11
+ id: string | number | undefined,
12
+ sourceUrl: string | undefined,
13
+ ): string | undefined {
14
+ if (typeof id === 'number')
15
+ return id.toString()
16
+
17
+ if (id?.trim())
18
+ return id.trim()
19
+
20
+ if (!sourceUrl)
21
+ return
22
+
23
+ try {
24
+ const url = new URL(sourceUrl)
25
+ if (!['http:', 'https:'].includes(url.protocol) || !TWEET_HOSTS.has(url.hostname))
26
+ return
27
+
28
+ const segments = url.pathname.split('/').filter(Boolean)
29
+ const statusIndex = segments.lastIndexOf('status')
30
+ const tweetId = segments[statusIndex + 1]
31
+ if (statusIndex < 1 || !tweetId || !/^\d+$/.test(tweetId))
32
+ return
33
+
34
+ return tweetId
35
+ }
36
+ catch {}
37
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "52.19.1",
4
+ "version": "52.19.2",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -30,29 +30,31 @@
30
30
  "dependencies": {
31
31
  "@antfu/utils": "^9.3.0",
32
32
  "@fix-webm-duration/fix": "^1.0.1",
33
- "@iconify-json/carbon": "^1.2.25",
33
+ "@iconify-json/carbon": "^1.2.27",
34
34
  "@iconify-json/ph": "^1.2.2",
35
35
  "@iconify-json/svg-spinners": "^1.2.4",
36
36
  "@shikijs/engine-javascript": "^4.4.3",
37
37
  "@shikijs/magic-move": "^4.4.3",
38
38
  "@shikijs/monaco": "^4.4.3",
39
39
  "@shikijs/vitepress-twoslash": "^4.4.3",
40
+ "@slidev/parser": "52.19.2",
40
41
  "@slidev/rough-notation": "^0.1.0",
42
+ "@slidev/types": "52.19.2",
41
43
  "@typescript/ata": "^0.9.8",
42
- "@unhead/vue": "^3.3.2",
43
- "@unocss/extractor-mdc": "^66.7.5",
44
- "@unocss/preset-mini": "^66.7.5",
45
- "@unocss/reset": "^66.7.5",
44
+ "@unhead/vue": "^3.4.1",
45
+ "@unocss/extractor-mdc": "^66.10.3",
46
+ "@unocss/preset-mini": "^66.10.3",
47
+ "@unocss/reset": "^66.10.3",
46
48
  "@vueuse/core": "^14.4.0",
47
49
  "@vueuse/math": "^14.4.0",
48
50
  "@vueuse/motion": "^3.0.3",
49
- "ansis": "^4.3.1",
51
+ "ansis": "^4.4.0",
50
52
  "drauu": "^1.0.0",
51
53
  "file-saver": "^2.0.5",
52
54
  "fitty": "^2.4.2",
53
- "floating-vue": "^5.2.2",
55
+ "floating-vue": "^5.4.0",
54
56
  "fuse.js": "^7.5.0",
55
- "katex": "^0.18.4",
57
+ "katex": "^0.18.7",
56
58
  "lz-string": "^1.5.0",
57
59
  "mermaid": "^11.16.1",
58
60
  "monaco-editor": "^0.56.0",
@@ -61,14 +63,12 @@
61
63
  "recordrtc": "^5.6.2",
62
64
  "shiki": "^4.4.3",
63
65
  "typescript": "^6.0.3",
64
- "unocss": "^66.7.5",
65
- "vue": "^3.5.41",
66
- "vue-router": "^5.2.0",
67
- "yaml": "^2.9.0",
68
- "@slidev/parser": "52.19.1",
69
- "@slidev/types": "52.19.1"
66
+ "unocss": "^66.10.3",
67
+ "vue": "^3.5.42",
68
+ "vue-router": "^5.3.1",
69
+ "yaml": "^2.9.1"
70
70
  },
71
71
  "devDependencies": {
72
- "vite": "^8.2.1"
72
+ "vite": "~8.2.0"
73
73
  }
74
74
  }