@slidev/client 52.15.2 → 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.
Files changed (43) hide show
  1. package/App.vue +2 -0
  2. package/builtin/AutoFitText.vue +54 -35
  3. package/builtin/KaTexBlockWrapper.test.ts +41 -0
  4. package/builtin/KaTexBlockWrapper.vue +4 -19
  5. package/builtin/ShikiMagicMove.vue +2 -2
  6. package/builtin/TocList.vue +1 -1
  7. package/builtin/katex-lines.ts +22 -0
  8. package/composables/useEmbeddedCtrl.ts +4 -2
  9. package/composables/useNav.ts +1 -1
  10. package/composables/usePreloadImages.ts +40 -1
  11. package/constants.ts +1 -0
  12. package/env.ts +1 -1
  13. package/internals/ClicksSlider.vue +99 -48
  14. package/internals/CodeRunner.vue +2 -2
  15. package/internals/Goto.vue +2 -2
  16. package/internals/NavControls.vue +1 -1
  17. package/internals/NoteDisplay.vue +5 -0
  18. package/internals/NoteEditable.vue +2 -3
  19. package/internals/PrintSlideClick.vue +1 -1
  20. package/internals/PwaIndicator.vue +50 -0
  21. package/internals/SideEditor.vue +2 -7
  22. package/internals/SlideWrapper.vue +1 -1
  23. package/internals/SlidesShow.vue +1 -1
  24. package/logic/sideEditor.ts +14 -0
  25. package/logic/slidePath.test.ts +29 -0
  26. package/logic/slidePath.ts +10 -0
  27. package/logic/slides.ts +3 -3
  28. package/modules/mermaid.ts +1 -1
  29. package/package.json +26 -25
  30. package/pages/overview.vue +293 -64
  31. package/pages/presenter.vue +1 -4
  32. package/setup/code-runners.ts +2 -2
  33. package/setup/context-menu.ts +1 -1
  34. package/setup/main.ts +11 -5
  35. package/setup/mermaid.ts +1 -1
  36. package/setup/monaco.ts +4 -4
  37. package/setup/pwa.ts +28 -0
  38. package/setup/root.ts +1 -1
  39. package/setup/shiki.ts +1 -1
  40. package/setup/shortcuts.ts +1 -1
  41. package/shim-vue.d.ts +1 -0
  42. package/shim.d.ts +2 -0
  43. package/styles/index.ts +1 -1
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>
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { collectKatexEquationLines } from './katex-lines'
3
+
4
+ function fakeElement(options: {
5
+ parent?: Element | null
6
+ closest?: Element | null
7
+ children?: Element[]
8
+ } = {}) {
9
+ const el = {
10
+ parentElement: options.parent ?? null,
11
+ closest: () => options.closest ?? null,
12
+ querySelectorAll: () => options.children ?? [],
13
+ }
14
+ return el as unknown as Element
15
+ }
16
+
17
+ describe('collectKatexEquationLines', () => {
18
+ it('ignores nested KaTeX tables when mapping equation lines', () => {
19
+ const rowA1 = fakeElement()
20
+ const rowA2 = fakeElement()
21
+ const rowB1 = fakeElement()
22
+ const rowB2 = fakeElement()
23
+ const nestedRow = fakeElement()
24
+
25
+ const outerLeftParent = fakeElement({ children: [rowA1, rowA2] })
26
+ const outerRightParent = fakeElement({ children: [rowB1, rowB2] })
27
+ const nestedParent = fakeElement({
28
+ parent: fakeElement({ closest: outerLeftParent }),
29
+ children: [nestedRow],
30
+ })
31
+
32
+ const root = fakeElement({
33
+ children: [outerLeftParent, nestedParent, outerRightParent],
34
+ })
35
+
36
+ expect(collectKatexEquationLines(root)).toEqual([
37
+ [rowA1, rowB1],
38
+ [rowA2, rowB2],
39
+ ])
40
+ })
41
+ })
@@ -26,6 +26,7 @@ import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
26
26
  import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'
27
27
  import { useSlideContext } from '../context'
28
28
  import { makeId } from '../logic/utils'
29
+ import { collectKatexEquationLines } from './katex-lines'
29
30
 
30
31
  const props = defineProps({
31
32
  ranges: {
@@ -77,26 +78,10 @@ onMounted(() => {
77
78
  if (hide)
78
79
  rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
79
80
 
80
- // KaTeX equations have col-align-XXX as parent
81
- const equationParents = el.value.querySelectorAll('.mtable > [class*=col-align]')
82
- if (!equationParents)
83
- return
84
-
85
- // For each row we extract the individual equation rows
86
- const equationRowsOfEachParent = Array.from(equationParents)
87
- .map(item => Array.from(item.querySelectorAll(':scope > .vlist-t > .vlist-r > .vlist > span > .mord')))
88
81
  // This list maps rows from different parents to line them up
89
- const lines: Element[][] = []
90
- for (const equationRowParent of equationRowsOfEachParent) {
91
- equationRowParent.forEach((equationRow, idx) => {
92
- if (!equationRow)
93
- return
94
- if (Array.isArray(lines[idx]))
95
- lines[idx].push(equationRow)
96
- else
97
- lines[idx] = [equationRow]
98
- })
99
- }
82
+ const lines = collectKatexEquationLines(el.value)
83
+ if (!lines.length)
84
+ return
100
85
 
101
86
  const startLine = props.startLine
102
87
  const highlights: number[] = parseRangeString(lines.length + startLine - 1, rangeStr)
@@ -1,10 +1,10 @@
1
1
  <script setup lang="ts">
2
- import type { KeyedTokensInfo } from 'shiki-magic-move/types'
2
+ import type { KeyedTokensInfo } from '@shikijs/magic-move/types'
3
3
  import type { PropType } from 'vue'
4
4
  import { sleep } from '@antfu/utils'
5
+ import { ShikiMagicMovePrecompiled } from '@shikijs/magic-move/vue'
5
6
  import { useClipboard } from '@vueuse/core'
6
7
  import lz from 'lz-string'
7
- import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
8
8
  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
9
9
  import { useNav } from '../composables/useNav'
10
10
  import { CLICKS_MAX } from '../constants'
@@ -8,9 +8,9 @@ Usage:
8
8
  -->
9
9
  <script setup lang="ts">
10
10
  import type { TocItem } from '@slidev/types'
11
- import TitleRenderer from '#slidev/title-renderer'
12
11
  import { toArray } from '@antfu/utils'
13
12
  import { computed } from 'vue'
13
+ import TitleRenderer from '#slidev/title-renderer'
14
14
  import { useNav } from '../composables/useNav'
15
15
 
16
16
  const props = withDefaults(
@@ -0,0 +1,22 @@
1
+ const KATEX_EQUATION_PARENT_SELECTOR = '.mtable > [class*=col-align]'
2
+ const KATEX_EQUATION_ROW_SELECTOR = ':scope > .vlist-t > .vlist-r > .vlist > span > .mord'
3
+
4
+ export function collectKatexEquationLines(el: Element): Element[][] {
5
+ const equationParents = Array.from(el.querySelectorAll(KATEX_EQUATION_PARENT_SELECTOR))
6
+ .filter(parent => !parent.parentElement?.closest(KATEX_EQUATION_PARENT_SELECTOR))
7
+
8
+ const lines: Element[][] = []
9
+ for (const equationRowParent of equationParents) {
10
+ const equationRows = Array.from(equationRowParent.querySelectorAll(KATEX_EQUATION_ROW_SELECTOR))
11
+ equationRows.forEach((equationRow, idx) => {
12
+ if (!equationRow)
13
+ return
14
+ if (Array.isArray(lines[idx]))
15
+ lines[idx].push(equationRow)
16
+ else
17
+ lines[idx] = [equationRow]
18
+ })
19
+ }
20
+
21
+ return lines
22
+ }
@@ -31,8 +31,10 @@ export function useEmbeddedControl() {
31
31
 
32
32
  if (nav.isEmbedded.value) {
33
33
  throttledWatch(
34
- [nav.currentSlideNo, nav.clicks, nav.hasNext, nav.hasPrev],
35
- ([no, clicks, hasNext, hasPrev]) => {
34
+ [nav.currentSlideNo, nav.clicks, nav.hasNext, nav.hasPrev, nav.hasPrimarySlide],
35
+ ([no, clicks, hasNext, hasPrev, hasPrimarySlide]) => {
36
+ if (!hasPrimarySlide)
37
+ return
36
38
  window.parent.postMessage(
37
39
  {
38
40
  target: 'slidev',
@@ -1,12 +1,12 @@
1
1
  import type { ClicksContext, SlideRoute, TocItem } from '@slidev/types'
2
2
  import type { ComputedRef, Ref, TransitionGroupProps, WritableComputedRef } from 'vue'
3
3
  import type { RouteLocationNormalized, Router } from 'vue-router'
4
- import { slides } from '#slidev/slides'
5
4
  import { clamp } from '@antfu/utils'
6
5
  import { parseRangeString } from '@slidev/parser/utils'
7
6
  import { createSharedComposable } from '@vueuse/core'
8
7
  import { computed, ref, watch } from 'vue'
9
8
  import { useRoute, useRouter } from 'vue-router'
9
+ import { slides } from '#slidev/slides'
10
10
  import { CLICKS_MAX } from '../constants'
11
11
  import { configs } from '../env'
12
12
  import { useRouteQuery } from '../logic/route'
@@ -1,7 +1,7 @@
1
1
  import type { SlideRoute } from '@slidev/types'
2
2
  import type { ComputedRef, Ref } from 'vue'
3
+ import { computed, ref, watchEffect } from 'vue'
3
4
  import configs from '#slidev/configs'
4
- import { watchEffect } from 'vue'
5
5
 
6
6
  const loaded = new Set<string>()
7
7
  const loading = 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',
package/env.ts CHANGED
@@ -1,6 +1,6 @@
1
- import configs from '#slidev/configs'
2
1
  import { objectMap } from '@antfu/utils'
3
2
  import { computed } from 'vue'
3
+ import configs from '#slidev/configs'
4
4
 
5
5
  export { configs }
6
6
 
@@ -1,70 +1,146 @@
1
1
  <script setup lang="ts">
2
2
  import type { ClicksContext } from '@slidev/types'
3
3
  import { clamp, range } from '@antfu/utils'
4
- import { computed } from 'vue'
4
+ import { computed, ref } from 'vue'
5
5
  import { CLICKS_MAX } from '../constants'
6
6
 
7
7
  const props = withDefaults(defineProps<{
8
8
  clicksContext: ClicksContext
9
9
  readonly?: boolean
10
10
  active?: boolean
11
+ resettable?: boolean
12
+ compact?: boolean
13
+ attached?: boolean
11
14
  }>(), {
12
15
  active: true,
13
16
  })
14
17
 
18
+ const emit = defineEmits<{
19
+ (type: 'activate'): void
20
+ (type: 'reset'): void
21
+ }>()
22
+
15
23
  const total = computed(() => props.clicksContext.total)
16
24
  const start = computed(() => clamp(0, props.clicksContext.clicksStart, total.value))
17
25
  const length = computed(() => total.value - start.value + 1)
18
26
  const current = computed({
19
27
  get() {
28
+ if (props.resettable && !props.active)
29
+ return -1
20
30
  return props.clicksContext.current > total.value ? -1 : props.clicksContext.current
21
31
  },
22
32
  set(value: number) {
33
+ if (props.resettable && value < 0) {
34
+ emit('reset')
35
+ // eslint-disable-next-line vue/no-mutating-props
36
+ props.clicksContext.current = CLICKS_MAX
37
+ return
38
+ }
39
+ emit('activate')
23
40
  // eslint-disable-next-line vue/no-mutating-props
24
41
  props.clicksContext.current = value
25
42
  },
26
43
  })
27
44
 
45
+ const isReset = computed(() => props.resettable && current.value < 0)
28
46
  const clicksRange = computed(() => range(start.value, total.value + 1))
47
+ const sliderEl = ref<HTMLElement>()
48
+ let pointerDown: { id: number, x: number, y: number } | undefined
49
+
50
+ function getPointerRatio(event: PointerEvent) {
51
+ const rect = sliderEl.value!.getBoundingClientRect()
52
+ return (event.clientX - rect.left) / Math.max(1, rect.width)
53
+ }
54
+
55
+ // Presses snap to a cell; drags switch only after crossing half a cell.
56
+ function setCurrentFromPointer(event: PointerEvent, snap: boolean) {
57
+ if (props.readonly || !sliderEl.value || (!snap && !(event.buttons & 1)))
58
+ return
59
+ const ratio = getPointerRatio(event)
60
+ // In resettable mode, dragging left of the rail restores the inactive state.
61
+ if (props.resettable && ratio < 0) {
62
+ current.value = -1
63
+ return
64
+ }
65
+ // Keep press-at-right-edge inside the last cell.
66
+ const position = clamp(0, ratio, snap ? 0.999999 : 1) * length.value
67
+ const currentOffset = clamp(0, current.value - start.value, length.value - 1)
68
+ let next = snap ? start.value + Math.floor(position) : current.value
69
+ if (!snap && position >= currentOffset + 1.5)
70
+ next = start.value + Math.floor(position - 0.5)
71
+ else if (!snap && position < currentOffset - 0.5)
72
+ next = start.value + Math.ceil(position - 0.5)
73
+ current.value = clamp(start.value, next, total.value)
74
+ }
29
75
 
30
- function onMousedown() {
76
+ function onPointerDown(event: PointerEvent) {
31
77
  if (props.readonly)
32
78
  return
33
- if (current.value < 0 || current.value > total.value)
34
- current.value = 0
79
+ sliderEl.value?.setPointerCapture(event.pointerId)
80
+ pointerDown = { id: event.pointerId, x: event.clientX, y: event.clientY }
81
+ setCurrentFromPointer(event, true)
82
+ }
83
+
84
+ function onPointerMove(event: PointerEvent) {
85
+ if (pointerDown?.id === event.pointerId) {
86
+ // Treat tiny movement after pointerdown as part of the click.
87
+ if (Math.abs(event.clientX - pointerDown.x) <= 3 && Math.abs(event.clientY - pointerDown.y) <= 3)
88
+ return
89
+ pointerDown = undefined
90
+ }
91
+ setCurrentFromPointer(event, false)
35
92
  }
36
93
  </script>
37
94
 
38
95
  <template>
39
96
  <div
40
- class="flex gap-1 items-center select-none"
97
+ class="flex gap-1 select-none"
41
98
  :title="`Clicks in this slide: ${length}`"
42
- :class="length && props.clicksContext.isMounted ? '' : 'op50'"
99
+ :class="[attached ? 'items-end' : 'items-center', length && props.clicksContext.isMounted ? '' : 'op50']"
43
100
  >
44
- <div class="flex gap-0.2 items-center min-w-16 font-mono mr1">
45
- <div class="i-carbon:cursor-1 text-sm op50" />
101
+ <div
102
+ class="flex items-center font-mono"
103
+ :class="[compact ? 'gap-1 min-w-0 mr0' : 'gap-0.2 min-w-16 mr1', attached ? 'h-[22px]' : '']"
104
+ >
105
+ <div class="i-carbon:cursor-1 text-sm op50" :class="compact ? 'ml-1' : ''" />
46
106
  <template v-if="current >= 0 && current !== CLICKS_MAX && active">
47
- <div flex-auto />
48
- <span text-primary>{{ current }}</span>
49
- <span op25 text-sm>/</span>
50
- <span op50 text-sm>{{ total }}</span>
107
+ <div v-if="!compact" flex-auto />
108
+ <span>
109
+ <span text-primary>{{ current }}</span>
110
+ <span op25 text-sm>/</span>
111
+ <span op50 text-sm>{{ total }}</span>
112
+ </span>
51
113
  </template>
52
114
  <div
53
115
  v-else
54
- op50 flex-auto pl1
116
+ op50
117
+ :class="compact ? '' : 'flex-auto pl1'"
55
118
  >
56
- {{ total }}
119
+ <span
120
+ :class="compact ? 'inline-block text-center' : ''"
121
+ :style="compact ? { width: `${String(total).length * 2 + 1}ch`, marginLeft: '-0.25ch' } : undefined"
122
+ >{{ total }}</span>
57
123
  </div>
58
124
  </div>
59
125
  <div
60
- relative flex-auto h5 font-mono flex="~"
126
+ ref="sliderEl"
127
+ relative flex-auto font-mono flex="~"
128
+ touch-none
129
+ :class="[attached ? 'h-[22px]' : 'h5', isReset ? 'op80' : '']"
130
+ @pointerdown.capture="onPointerDown"
131
+ @pointermove="onPointerMove"
132
+ @pointerup="pointerDown = undefined"
133
+ @pointercancel="pointerDown = undefined"
61
134
  >
62
135
  <div
63
136
  v-for="i of clicksRange" :key="i"
64
137
  border="y main" of-hidden relative
65
138
  :class="[
66
- i === 0 ? 'rounded-l border-l' : '',
67
- i === total ? 'rounded-r border-r' : '',
139
+ i === 0 ? 'border-l' : '',
140
+ i === 0 ? attached ? 'rounded-tl' : 'rounded-l' : '',
141
+ i === total ? 'border-r' : '',
142
+ i === total && +i !== +current ? attached ? 'rounded-tr' : 'rounded-r' : '',
143
+ attached ? 'border-b-0' : '',
68
144
  ]"
69
145
  :style="{ width: length > 0 ? `${1 / length * 100}%` : '100%' }"
70
146
  >
@@ -72,45 +148,20 @@ function onMousedown() {
72
148
  absolute inset-0
73
149
  :class="(i <= current && active) ? 'bg-primary op15' : ''"
74
150
  />
151
+ <div
152
+ v-if="+i === +current && active"
153
+ absolute inset-y-0 right-0 w-0.5 bg-primary z-1
154
+ />
75
155
  <div
76
156
  :class="[
77
- (+i === +current && active) ? 'text-primary font-bold op100 border-primary' : 'op30 border-main',
78
- i === 0 ? 'rounded-l' : '',
79
- i === total ? 'rounded-r' : 'border-r-2',
157
+ (+i === +current && active) ? 'text-primary font-bold op100' : 'op30',
158
+ i !== total ? 'border-r-2 border-main' : '',
80
159
  ]"
81
160
  w-full h-full text-xs flex items-center justify-center z-1
82
161
  >
83
162
  {{ i }}
84
163
  </div>
85
164
  </div>
86
- <input
87
- v-model="current"
88
- class="range"
89
- type="range" :min="start" :max="total" :step="1"
90
- absolute inset-0 z-label op0
91
- :class="readonly ? 'pointer-events-none' : ''"
92
- :style="{ '--thumb-width': `${1 / (length + 1) * 100}%` }"
93
- @mousedown="onMousedown"
94
- @focus="event => (event.currentTarget as HTMLElement)?.blur()"
95
- >
96
165
  </div>
97
166
  </div>
98
167
  </template>
99
-
100
- <style scoped>
101
- .range {
102
- -webkit-appearance: none;
103
- appearance: none;
104
- background: transparent;
105
- }
106
- .range::-webkit-slider-thumb {
107
- -webkit-appearance: none;
108
- height: 100%;
109
- width: var(--thumb-width, 0.5rem);
110
- }
111
-
112
- .range::-moz-range-thumb {
113
- height: 100%;
114
- width: var(--thumb-width, 0.5rem);
115
- }
116
- </style>
@@ -22,12 +22,12 @@ const props = defineProps<{
22
22
 
23
23
  const emit = defineEmits(['update:modelValue'])
24
24
 
25
- const { isPrintMode } = useNav()
25
+ const { isEmbedded, isPrintMode } = useNav()
26
26
 
27
27
  const code = useVModel(props, 'modelValue', emit)
28
28
 
29
29
  const { $renderContext, $clicksContext } = useSlideContext()
30
- const disabled = computed(() => !['slide', 'presenter'].includes($renderContext.value))
30
+ const disabled = computed(() => !['slide', 'presenter'].includes($renderContext.value) && !($renderContext.value === 'overview' && isEmbedded.value))
31
31
 
32
32
  const autorun = isPrintMode.value ? 'once' : props.autorun
33
33
  const isRunning = ref(autorun)
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import TitleRenderer from '#slidev/title-renderer'
3
2
  import Fuse from 'fuse.js'
4
3
  import { computed, ref, watch } from 'vue'
4
+ import TitleRenderer from '#slidev/title-renderer'
5
5
  import { useNav } from '../composables/useNav'
6
6
  import { activeElement, showGotoDialog } from '../state'
7
7
 
@@ -136,7 +136,7 @@ watch(activeElement, () => {
136
136
  >
137
137
  </div>
138
138
  <div
139
- v-if="result.length > 0"
139
+ v-if="showGotoDialog && result.length > 0"
140
140
  ref="list"
141
141
  class="autocomplete-list"
142
142
  shadow="~"
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import CustomNavControls from '#slidev/custom-nav-controls'
3
2
  import { computed, ref, shallowRef } from 'vue'
3
+ import CustomNavControls from '#slidev/custom-nav-controls'
4
4
  import { useDrawings } from '../composables/useDrawings'
5
5
  import { useNav } from '../composables/useNav'
6
6
  import { configs } from '../env'
@@ -172,4 +172,9 @@ watchEffect(() => {
172
172
  .slidev-note :first-child {
173
173
  margin-top: 0;
174
174
  }
175
+
176
+ .slidev-note {
177
+ overflow-wrap: anywhere;
178
+ word-break: break-word;
179
+ }
175
180
  </style>