@slidev/client 52.16.0 → 52.17.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.
package/App.vue CHANGED
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { watchEffect } from 'vue'
3
3
  import { themeVars } from './env'
4
+ import PwaIndicator from './internals/PwaIndicator.vue'
4
5
  import setupRoot from './setup/root'
5
6
 
6
7
  setupRoot()
@@ -13,4 +14,5 @@ watchEffect(() => {
13
14
 
14
15
  <template>
15
16
  <RouterView />
17
+ <PwaIndicator v-if="__SLIDEV_FEATURE_PWA__" />
16
18
  </template>
@@ -1,9 +1,8 @@
1
1
  <!--
2
- [Experimental]
3
-
4
- Think this component as the TextBox you that will see
2
+ Think of this component as the TextBox that you will see
5
3
  in PowerPoint or Keynote. It will automatically resize
6
- the font size based on it's content to fit them in.
4
+ the font size based on its content to fit them in,
5
+ powered by fitty (https://github.com/rikschennink/fitty).
7
6
 
8
7
  Usage:
9
8
 
@@ -11,12 +10,14 @@ Usage:
11
10
 
12
11
  or
13
12
 
14
- <AutoFitText :max="80" :min="100" v-model="text"/>
13
+ <AutoFitText :max="100" :min="30" v-model="text"/>
15
14
  -->
16
15
 
17
16
  <script setup lang="ts">
18
- import { useElementSize, useVModel } from '@vueuse/core'
19
- import { computed, ref, watch } from 'vue'
17
+ import type { FittyInstance } from 'fitty'
18
+ import { useResizeObserver, useVModel } from '@vueuse/core'
19
+ import fitty from 'fitty'
20
+ import { onMounted, onUnmounted, ref, watch } from 'vue'
20
21
 
21
22
  const props = defineProps({
22
23
  modelValue: {
@@ -28,40 +29,59 @@ const props = defineProps({
28
29
  min: {
29
30
  default: 30,
30
31
  },
32
+ multiLine: {
33
+ type: Boolean,
34
+ default: true,
35
+ },
31
36
  })
32
37
 
33
- const emit = defineEmits<{
34
- (e: any): void
35
- }>()
36
- const container = ref<HTMLDivElement>()
37
- const inner = ref<HTMLDivElement>()
38
- const size = ref(100)
39
- const fontSize = computed(() => `${size.value}px`)
38
+ const emit = defineEmits(['update:modelValue'])
39
+
40
40
  const value = useVModel(props, 'modelValue', emit)
41
41
 
42
- const containerSize = useElementSize(container)
43
- const innerSize = useElementSize(inner)
42
+ const container = ref<HTMLDivElement>()
43
+ const inner = ref<HTMLDivElement>()
44
44
 
45
- const wrapLen = ref(0)
46
- const wrap = ref('nowrap')
45
+ let instance: FittyInstance | undefined
47
46
 
48
- watch([container, value, containerSize.width, innerSize.width], async () => {
49
- if (!container.value || innerSize.width.value <= 0)
47
+ function init() {
48
+ instance?.unsubscribe()
49
+ instance = undefined
50
+ if (!inner.value)
50
51
  return
51
- const ratio = containerSize.width.value / innerSize.width.value
52
- if (Number.isNaN(ratio) || ratio <= 0)
52
+ instance = fitty(inner.value, {
53
+ minSize: props.min,
54
+ maxSize: props.max,
55
+ multiLine: props.multiLine,
56
+ })
57
+ }
58
+
59
+ onMounted(() => {
60
+ init()
61
+ // Refit once web fonts are loaded, as glyph metrics may change
62
+ document.fonts?.ready.then(() => instance?.fit())
63
+ })
64
+
65
+ onUnmounted(() => {
66
+ instance?.unsubscribe()
67
+ instance = undefined
68
+ })
69
+
70
+ watch(() => [props.min, props.max, props.multiLine], init)
71
+
72
+ // Fitty observes content mutations and window resizes on its own,
73
+ // but not resizes of the container itself
74
+ useResizeObserver(container, () => {
75
+ if (!container.value || container.value.clientWidth <= 0)
53
76
  return
54
- let newSize = size.value * (containerSize.width.value / innerSize.width.value)
55
- if (newSize < props.min) {
56
- wrapLen.value = value.value.length
57
- wrap.value = ''
58
- }
59
- else {
60
- if (value.value.length < wrapLen.value)
61
- wrap.value = 'nowrap'
62
- }
63
- newSize = Math.max(props.min, Math.min(props.max, newSize))
64
- size.value = newSize
77
+ // If fitty was initialized while the element was hidden (e.g. on a
78
+ // preloaded slide), its internal state is invalid and refitting can
79
+ // never recover, so reinitialize it once the container is visible.
80
+ // A successful fit always leaves an inline font-size on the element.
81
+ if (inner.value && !inner.value.style.fontSize)
82
+ init()
83
+ else
84
+ instance?.fit()
65
85
  })
66
86
  </script>
67
87
 
@@ -78,11 +98,10 @@ watch([container, value, containerSize.width, innerSize.width], async () => {
78
98
  <style scoped>
79
99
  .slidev-auto-fit-text {
80
100
  overflow: auto;
81
- font-size: v-bind(fontSize);
82
- white-space: v-bind(wrap);
83
101
  }
84
102
 
85
103
  .slidev-auto-fit-text-inner {
86
104
  display: inline-block;
105
+ white-space: nowrap;
87
106
  }
88
107
  </style>
@@ -1,6 +1,6 @@
1
1
  import type { SlideRoute } from '@slidev/types'
2
2
  import type { ComputedRef, Ref } from 'vue'
3
- import { watchEffect } from 'vue'
3
+ import { computed, ref, watchEffect } from 'vue'
4
4
  import configs from '#slidev/configs'
5
5
 
6
6
  const loaded = new Set<string>()
@@ -14,6 +14,23 @@ function resolveUrl(url: string): string {
14
14
  return `${base}${url.startsWith('/') ? url : `/${url}`}`
15
15
  }
16
16
 
17
+ const RETRY_LIMIT = 2
18
+ const retries = new Map<string, number>()
19
+
20
+ const preloadedCount = ref(0)
21
+ const totalImagesCount = ref(0)
22
+
23
+ /**
24
+ * Progress of slide-image preloading, from 0 to 1 (1 when every preloadable
25
+ * image in the deck has loaded, or when the deck has no images).
26
+ */
27
+ export const preloadProgress = computed(() =>
28
+ totalImagesCount.value === 0 ? 1 : preloadedCount.value / totalImagesCount.value,
29
+ )
30
+
31
+ /** Whether all preloadable slide images have finished loading. */
32
+ export const preloadComplete = computed(() => preloadProgress.value >= 1)
33
+
17
34
  function preloadImage(url: string): void {
18
35
  const resolved = resolveUrl(url)
19
36
  if (loaded.has(resolved) || loading.has(resolved))
@@ -23,9 +40,18 @@ function preloadImage(url: string): void {
23
40
  img.onload = () => {
24
41
  loading.delete(resolved)
25
42
  loaded.add(resolved)
43
+ retries.delete(resolved)
44
+ preloadedCount.value = loaded.size
26
45
  }
27
46
  img.onerror = () => {
28
47
  loading.delete(resolved)
48
+ // Retry transient preload failures (e.g. flaky network) instead of silently
49
+ // giving up; bounded so genuinely-missing assets aren't hammered.
50
+ const attempts = retries.get(resolved) ?? 0
51
+ if (attempts < RETRY_LIMIT) {
52
+ retries.set(resolved, attempts + 1)
53
+ setTimeout(preloadImage, 1000 * (attempts + 1), url)
54
+ }
29
55
  }
30
56
  img.src = resolved
31
57
  }
@@ -50,6 +76,19 @@ export function usePreloadImages(
50
76
 
51
77
  const ahead = (typeof config === 'object' && config?.ahead) || 3
52
78
 
79
+ // Track total preloadable images across the deck for progress reporting
80
+ watchEffect(() => {
81
+ const all = slides.value
82
+ if (!all?.length)
83
+ return
84
+ const urls = new Set<string>()
85
+ for (const route of all) {
86
+ for (const url of route.meta?.slide?.images ?? [])
87
+ urls.add(resolveUrl(url))
88
+ }
89
+ totalImagesCount.value = urls.size
90
+ })
91
+
53
92
  // Preload current + prev + next + look-ahead window
54
93
  watchEffect(() => {
55
94
  const current = currentRoute.value
package/constants.ts CHANGED
@@ -86,6 +86,7 @@ export const HEADMATTER_FIELDS = [
86
86
  'comark',
87
87
  'contextMenu',
88
88
  'wakeLock',
89
+ 'pwa',
89
90
  'seoMeta',
90
91
  'notesAutoRuby',
91
92
  'magicMoveDuration',
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { pwaCaching, pwaReady } from '../setup/pwa'
3
+ </script>
4
+
5
+ <template>
6
+ <Transition name="slidev-pwa-fade">
7
+ <div
8
+ v-if="pwaCaching || pwaReady"
9
+ class="slidev-pwa-indicator"
10
+ :class="{ ready: pwaReady }"
11
+ >
12
+ <span v-if="pwaCaching">Caching for offline…</span>
13
+ <span v-else-if="pwaReady">Ready offline</span>
14
+ </div>
15
+ </Transition>
16
+ </template>
17
+
18
+ <style scoped>
19
+ .slidev-pwa-indicator {
20
+ position: fixed;
21
+ bottom: 1rem;
22
+ right: 1rem;
23
+ z-index: 9999;
24
+ padding: 0.4em 0.75em;
25
+ border-radius: 0.4em;
26
+ font-size: 0.75rem;
27
+ font-family: sans-serif;
28
+ line-height: 1.4;
29
+ background: rgba(0, 0, 0, 0.72);
30
+ color: #ccc;
31
+ pointer-events: none;
32
+ user-select: none;
33
+ backdrop-filter: blur(4px);
34
+ border: 1px solid rgba(255, 255, 255, 0.1);
35
+ }
36
+
37
+ .slidev-pwa-indicator.ready {
38
+ color: #7dce82;
39
+ }
40
+
41
+ .slidev-pwa-fade-enter-active,
42
+ .slidev-pwa-fade-leave-active {
43
+ transition: opacity 0.35s ease;
44
+ }
45
+
46
+ .slidev-pwa-fade-enter-from,
47
+ .slidev-pwa-fade-leave-to {
48
+ opacity: 0;
49
+ }
50
+ </style>
@@ -0,0 +1,29 @@
1
+ import type { SlideRoute } from '@slidev/types'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { getSlideRoutePath } from './slidePath'
4
+
5
+ function route(no: number, routeAlias?: string): SlideRoute {
6
+ return {
7
+ no,
8
+ meta: {
9
+ slide: {
10
+ frontmatter: {
11
+ routeAlias,
12
+ },
13
+ },
14
+ },
15
+ } as unknown as SlideRoute
16
+ }
17
+
18
+ describe('getSlideRoutePath', () => {
19
+ it('returns router paths without Vite base', () => {
20
+ expect(getSlideRoutePath(route(2), false)).toBe('/2')
21
+ expect(getSlideRoutePath(route(2), true)).toBe('/presenter/2')
22
+ expect(getSlideRoutePath(route(2), false, true)).toBe('/export/2')
23
+ })
24
+
25
+ it('uses route aliases', () => {
26
+ expect(getSlideRoutePath(route(2, 'intro'), false)).toBe('/intro')
27
+ expect(getSlideRoutePath(route(2, 'intro'), true)).toBe('/presenter/intro')
28
+ })
29
+ })
@@ -0,0 +1,10 @@
1
+ import type { SlideRoute } from '@slidev/types'
2
+
3
+ export function getSlideRoutePath(
4
+ route: SlideRoute,
5
+ presenter: boolean,
6
+ exporting: boolean = false,
7
+ ) {
8
+ const no = route.meta.slide?.frontmatter.routeAlias ?? route.no
9
+ return exporting ? `/export/${no}` : presenter ? `/presenter/${no}` : `/${no}`
10
+ }
package/logic/slides.ts CHANGED
@@ -3,6 +3,7 @@ import { tryOnMounted } from '@vueuse/core'
3
3
  import { computed, watch } from 'vue'
4
4
  import { slides } from '#slidev/slides'
5
5
  import { useSlideContext } from '../context'
6
+ import { getSlideRoutePath } from './slidePath'
6
7
 
7
8
  export { slides }
8
9
 
@@ -19,9 +20,7 @@ export function getSlidePath(
19
20
  ) {
20
21
  if (typeof route === 'number' || typeof route === 'string')
21
22
  route = getSlide(route)!
22
- const no = route.meta.slide?.frontmatter.routeAlias ?? route.no
23
- const path = exporting ? `export/${no}` : presenter ? `presenter/${no}` : `${no}`
24
- return `${import.meta.env.BASE_URL}${path}`
23
+ return getSlideRoutePath(route, presenter, exporting)
25
24
  }
26
25
 
27
26
  export function useIsSlideActive() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "52.16.0",
4
+ "version": "52.17.0",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -30,44 +30,45 @@
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.22",
33
+ "@iconify-json/carbon": "^1.2.24",
34
34
  "@iconify-json/ph": "^1.2.2",
35
35
  "@iconify-json/svg-spinners": "^1.2.4",
36
- "@shikijs/engine-javascript": "^4.2.0",
37
- "@shikijs/magic-move": "^4.2.0",
38
- "@shikijs/monaco": "^4.2.0",
39
- "@shikijs/vitepress-twoslash": "^4.2.0",
36
+ "@shikijs/engine-javascript": "^4.3.1",
37
+ "@shikijs/magic-move": "^4.3.1",
38
+ "@shikijs/monaco": "^4.3.1",
39
+ "@shikijs/vitepress-twoslash": "^4.3.1",
40
40
  "@slidev/rough-notation": "^0.1.0",
41
41
  "@typescript/ata": "^0.9.8",
42
- "@unhead/vue": "^3.1.1",
43
- "@unocss/extractor-mdc": "^66.7.0",
44
- "@unocss/preset-mini": "^66.7.0",
45
- "@unocss/reset": "^66.7.0",
42
+ "@unhead/vue": "^3.1.7",
43
+ "@unocss/extractor-mdc": "^66.7.5",
44
+ "@unocss/preset-mini": "^66.7.5",
45
+ "@unocss/reset": "^66.7.5",
46
46
  "@vueuse/core": "^14.3.0",
47
47
  "@vueuse/math": "^14.3.0",
48
48
  "@vueuse/motion": "^3.0.3",
49
49
  "ansis": "^4.3.1",
50
50
  "drauu": "^1.0.0",
51
51
  "file-saver": "^2.0.5",
52
+ "fitty": "^2.4.2",
52
53
  "floating-vue": "^5.2.2",
53
- "fuse.js": "^7.4.1",
54
+ "fuse.js": "^7.4.2",
54
55
  "katex": "^0.17.0",
55
56
  "lz-string": "^1.5.0",
56
- "mermaid": "^11.15.0",
57
+ "mermaid": "^11.16.0",
57
58
  "monaco-editor": "^0.55.1",
58
59
  "nanotar": "^0.3.0",
59
60
  "pptxgenjs": "^4.0.1",
60
61
  "recordrtc": "^5.6.2",
61
- "shiki": "^4.2.0",
62
+ "shiki": "^4.3.1",
62
63
  "typescript": "^6.0.3",
63
- "unocss": "^66.7.0",
64
- "vue": "^3.5.35",
64
+ "unocss": "^66.7.5",
65
+ "vue": "^3.5.39",
65
66
  "vue-router": "^5.1.0",
66
67
  "yaml": "^2.9.0",
67
- "@slidev/parser": "52.16.0",
68
- "@slidev/types": "52.16.0"
68
+ "@slidev/parser": "52.17.0",
69
+ "@slidev/types": "52.17.0"
69
70
  },
70
71
  "devDependencies": {
71
- "vite": "^8.0.16"
72
+ "vite": "^8.1.3"
72
73
  }
73
74
  }
package/setup/main.ts CHANGED
@@ -2,12 +2,13 @@ import type { AppContext } from '@slidev/types'
2
2
  import type { App } from 'vue'
3
3
  import TwoSlashFloatingVue from '@shikijs/vitepress-twoslash/client'
4
4
  import { createHead } from '@unhead/vue/client'
5
- import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
5
+ import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
6
6
  import setups from '#slidev/setups/main'
7
7
  import { createVClickDirectives } from '../modules/v-click'
8
8
  import { createVDragDirective } from '../modules/v-drag'
9
9
  import { createVMarkDirective } from '../modules/v-mark'
10
10
  import { createVMotionDirectives } from '../modules/v-motion'
11
+ import { setupPWA } from '../setup/pwa'
11
12
  import setupRoutes from '../setup/routes'
12
13
  import '../styles'
13
14
 
@@ -21,9 +22,11 @@ export default async function setupMain(app: App) {
21
22
  window.addEventListener('resize', setMaxHeight)
22
23
 
23
24
  const router = createRouter({
24
- history: __SLIDEV_HASH_ROUTE__
25
- ? createWebHashHistory(import.meta.env.BASE_URL)
26
- : createWebHistory(import.meta.env.BASE_URL),
25
+ history: __SLIDEV_MEMORY_ROUTE__
26
+ ? createMemoryHistory(import.meta.env.BASE_URL)
27
+ : __SLIDEV_HASH_ROUTE__
28
+ ? createWebHashHistory(import.meta.env.BASE_URL)
29
+ : createWebHistory(import.meta.env.BASE_URL),
27
30
  routes: setupRoutes(),
28
31
  })
29
32
 
@@ -42,4 +45,7 @@ export default async function setupMain(app: App) {
42
45
 
43
46
  for (const setup of setups)
44
47
  await setup(context)
48
+
49
+ if (__SLIDEV_FEATURE_PWA__)
50
+ setupPWA()
45
51
  }
package/setup/pwa.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { ref } from 'vue'
2
+
3
+ export const pwaCaching = ref(false)
4
+ export const pwaReady = ref(false)
5
+
6
+ export async function setupPWA() {
7
+ if (!__SLIDEV_FEATURE_PWA__)
8
+ return
9
+
10
+ const { registerSW } = await import('virtual:pwa-register')
11
+
12
+ pwaCaching.value = true
13
+
14
+ registerSW({
15
+ immediate: true,
16
+ onOfflineReady() {
17
+ pwaCaching.value = false
18
+ pwaReady.value = true
19
+ // Auto-dismiss "ready" indicator after 4 seconds
20
+ setTimeout(() => {
21
+ pwaReady.value = false
22
+ }, 4000)
23
+ },
24
+ onRegisteredSW(_swUrl, _registration) {
25
+ // SW registered; caching is in progress until onOfflineReady fires
26
+ },
27
+ })
28
+ }
package/shim.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /// <reference types="vite-plugin-pwa/client" />
2
+
1
3
  declare module '*.md' {
2
4
  // with unplugin-vue-markdown, markdowns can be treat as Vue components
3
5
  import type { ComponentOptions } from 'vue'