@slidev/client 0.48.0-beta.10 → 0.48.0-beta.12

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.
@@ -10,7 +10,7 @@ Usage:
10
10
  import { computed } from 'vue'
11
11
  import { toArray } from '@antfu/utils'
12
12
  import type { TocItem } from '@slidev/types'
13
- import Titles from '/@slidev/titles.md'
13
+ import Titles from '#slidev/titles.md'
14
14
 
15
15
  const props = withDefaults(defineProps<{
16
16
  level: number
@@ -65,7 +65,8 @@ const styles = computed(() => {
65
65
  .slidev-layout .slidev-toc-item p {
66
66
  margin: 0;
67
67
  }
68
- .slidev-layout .slidev-toc-item div, .slidev-layout .slidev-toc-item div p {
68
+ .slidev-layout .slidev-toc-item div,
69
+ .slidev-layout .slidev-toc-item div p {
69
70
  display: initial;
70
71
  }
71
72
  </style>
package/builtin/VClick.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { PropType, VNode } from 'vue'
8
8
  import { Text, defineComponent, h } from 'vue'
9
+ import { CLICKS_MAX } from '../constants'
9
10
  import VClicks from './VClicks'
10
11
 
11
12
  export default defineComponent({
@@ -31,7 +32,7 @@ export default defineComponent({
31
32
  return h(
32
33
  VClicks,
33
34
  {
34
- every: 99999,
35
+ every: CLICKS_MAX,
35
36
  at: this.at,
36
37
  hide: this.hide,
37
38
  fade: this.fade,
@@ -5,8 +5,12 @@ import { ref, shallowReactive } from 'vue'
5
5
  import type { RouteRecordRaw } from 'vue-router'
6
6
  import { currentRoute, isPrintMode, isPrintWithClicks, queryClicks, routeForceRefresh } from '../logic/nav'
7
7
  import { normalizeAtProp } from '../logic/utils'
8
+ import { CLICKS_MAX } from '../constants'
8
9
 
9
- function useClicksContextBase(getCurrent: () => number, clicksOverrides?: number): ClicksContext {
10
+ /**
11
+ * @internal
12
+ */
13
+ export function useClicksContextBase(getCurrent: () => number, clicksOverrides?: number): ClicksContext {
10
14
  const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
11
15
  const map: ClicksContext['map'] = shallowReactive(new Map())
12
16
 
@@ -62,14 +66,14 @@ function useClicksContextBase(getCurrent: () => number, clicksOverrides?: number
62
66
  export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksContext {
63
67
  if (route?.meta?.__clicksContext)
64
68
  return route.meta.__clicksContext
65
- const thisPath = +(route?.path ?? 99999)
69
+ const thisPath = +(route?.path ?? CLICKS_MAX)
66
70
  const context = useClicksContextBase(
67
71
  () => {
68
- const currentPath = +(currentRoute.value?.path ?? 99999)
72
+ const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
69
73
  if (currentPath === thisPath)
70
74
  return queryClicks.value
71
75
  else if (currentPath > thisPath)
72
- return 99999
76
+ return CLICKS_MAX
73
77
  else
74
78
  return 0
75
79
  },
package/constants.ts CHANGED
@@ -22,6 +22,8 @@ export const CLASS_VCLICK_HIDDEN_EXP = 'slidev-vclick-hidden-explicitly'
22
22
  export const CLASS_VCLICK_CURRENT = 'slidev-vclick-current'
23
23
  export const CLASS_VCLICK_PRIOR = 'slidev-vclick-prior'
24
24
 
25
+ export const CLICKS_MAX = 999999
26
+
25
27
  export const TRUST_ORIGINS = [
26
28
  'localhost',
27
29
  '127.0.0.1',
package/env.ts CHANGED
@@ -1,15 +1,12 @@
1
- import type { SlidevConfig } from '@slidev/types'
2
- import type { UnwrapNestedRefs } from 'vue'
3
1
  import { computed } from 'vue'
4
2
  import { objectMap } from '@antfu/utils'
3
+ import configs from '#slidev/configs'
5
4
 
6
- // @ts-expect-error missing types
7
- import _configs from '/@slidev/configs'
8
- import type { SlidevContext } from './modules/context'
5
+ export { configs }
9
6
 
10
- export const configs = _configs as SlidevConfig
11
7
  export const slideAspect = configs.aspectRatio ?? (16 / 9)
12
8
  export const slideWidth = configs.canvasWidth ?? 980
9
+
13
10
  // To honor the aspect ratio more as possible, we need to approximate the height to the next integer.
14
11
  // Doing this, we will prevent on print, to create an additional empty white page after each page.
15
12
  export const slideHeight = Math.ceil(slideWidth / slideAspect)
@@ -17,9 +14,3 @@ export const slideHeight = Math.ceil(slideWidth / slideAspect)
17
14
  export const themeVars = computed(() => {
18
15
  return objectMap(configs.themeConfig || {}, (k, v) => [`--slidev-theme-${k}`, v])
19
16
  })
20
-
21
- declare module 'vue' {
22
- interface ComponentCustomProperties {
23
- $slidev: UnwrapNestedRefs<SlidevContext>
24
- }
25
- }
@@ -131,10 +131,8 @@ function setBrushColor(color: typeof brush.color) {
131
131
  </Draggable>
132
132
  </template>
133
133
 
134
- <style lang="postcss">
135
- .v-popper--theme-menu {
136
- .v-popper__arrow-inner {
137
- @apply border-main;
138
- }
134
+ <style>
135
+ .v-popper--theme-menu .v-popper__arrow-inner {
136
+ --uno: border-main;
139
137
  }
140
138
  </style>
@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
3
3
  import Fuse from 'fuse.js'
4
4
  import { go, rawRoutes } from '../logic/nav'
5
5
  import { activeElement, showGotoDialog } from '../state'
6
- import Titles from '/@slidev/titles.md'
6
+ import Titles from '#slidev/titles.md'
7
7
 
8
8
  const container = ref<HTMLDivElement>()
9
9
  const input = ref<HTMLInputElement>()
@@ -165,10 +165,11 @@ watch(activeElement, () => {
165
165
  </div>
166
166
  </template>
167
167
 
168
- <style scoped lang="postcss">
168
+ <style scoped>
169
169
  .autocomplete-list {
170
- @apply bg-main transform mt-1 overflow-auto;
171
- max-height: calc( 100vh - 100px );
170
+ --uno: bg-main mt-1;
171
+ overflow: auto;
172
+ max-height: calc(100vh - 100px);
172
173
  }
173
174
 
174
175
  .autocomplete {
@@ -10,8 +10,7 @@ import MenuButton from './MenuButton.vue'
10
10
  import VerticalDivider from './VerticalDivider.vue'
11
11
  import IconButton from './IconButton.vue'
12
12
 
13
- // @ts-expect-error virtual module
14
- import CustomNavControls from '/@slidev/custom-nav-controls'
13
+ import CustomNavControls from '#slidev/custom-nav-controls'
15
14
 
16
15
  const props = defineProps({
17
16
  persist: {
@@ -1,19 +1,70 @@
1
1
  <script setup lang="ts">
2
+ import { computed, defineEmits, defineProps, nextTick, onMounted, ref, watch } from 'vue'
3
+ import { CLICKS_MAX } from '../constants'
4
+
2
5
  const props = defineProps<{
3
6
  class?: string
4
7
  noteHtml?: string
5
8
  note?: string
6
9
  placeholder?: string
10
+ clicks?: number | string
7
11
  }>()
8
12
 
9
13
  defineEmits(['click'])
14
+
15
+ const withClicks = computed(() => props.clicks != null && props.noteHtml?.includes('slidev-note-click-mark'))
16
+ const noteDisplay = ref<HTMLElement | null>(null)
17
+
18
+ function highlightNote() {
19
+ if (!noteDisplay.value || !withClicks.value || props.clicks == null)
20
+ return
21
+
22
+ const children = Array.from(noteDisplay.value.querySelectorAll('*'))
23
+
24
+ const disabled = +props.clicks < 0 || +props.clicks >= CLICKS_MAX
25
+ if (disabled) {
26
+ children.forEach(el => el.classList.remove('slidev-note-fade'))
27
+ return
28
+ }
29
+
30
+ let count = 0
31
+
32
+ const groups = new Map<number, Element[]>()
33
+
34
+ for (const child of children) {
35
+ if (!groups.has(count))
36
+ groups.set(count, [])
37
+
38
+ groups.get(count)!.push(child)
39
+ if (child.classList.contains('slidev-note-click-mark'))
40
+ count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
41
+ }
42
+
43
+ for (const [count, els] of groups)
44
+ els.forEach(el => el.classList.toggle('slidev-note-fade', +count !== +props.clicks!))
45
+ }
46
+
47
+ watch(
48
+ () => [props.noteHtml, props.clicks],
49
+ () => {
50
+ nextTick(() => {
51
+ highlightNote()
52
+ })
53
+ },
54
+ { immediate: true },
55
+ )
56
+
57
+ onMounted(() => {
58
+ highlightNote()
59
+ })
10
60
  </script>
11
61
 
12
62
  <template>
13
63
  <div
14
64
  v-if="noteHtml"
15
- class="prose overflow-auto outline-none"
16
- :class="props.class"
65
+ ref="noteDisplay"
66
+ class="prose overflow-auto outline-none slidev-note"
67
+ :class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
17
68
  @click="$emit('click')"
18
69
  v-html="noteHtml"
19
70
  />
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { nextTick, ref, watch, watchEffect } from 'vue'
2
3
  import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
3
- import { ref, watch, watchEffect } from 'vue'
4
4
  import { useDynamicSlideInfo } from '../logic/note'
5
5
  import NoteDisplay from './NoteDisplay.vue'
6
6
 
@@ -20,6 +20,12 @@ const props = defineProps({
20
20
  placeholder: {
21
21
  default: 'No notes for this slide',
22
22
  },
23
+ clicks: {
24
+ type: [Number, String],
25
+ },
26
+ autoHeight: {
27
+ default: false,
28
+ },
23
29
  })
24
30
 
25
31
  const emit = defineEmits([
@@ -66,6 +72,27 @@ watchEffect(() => {
66
72
  onClickOutside(input, () => {
67
73
  editing.value = false
68
74
  })
75
+
76
+ function calculateHeight() {
77
+ if (!props.autoHeight || !input.value || !editing.value)
78
+ return
79
+ if (input.value.scrollHeight > input.value.clientHeight)
80
+ input.value.style.height = `${input.value.scrollHeight}px`
81
+ }
82
+
83
+ const inputHeight = ref<number | null>()
84
+ watchEffect(() => {
85
+ calculateHeight()
86
+ })
87
+ watch(
88
+ note,
89
+ () => {
90
+ nextTick(() => {
91
+ calculateHeight()
92
+ })
93
+ },
94
+ { flush: 'post' },
95
+ )
69
96
  </script>
70
97
 
71
98
  <template>
@@ -76,6 +103,7 @@ onClickOutside(input, () => {
76
103
  :style="props.style"
77
104
  :note="note || placeholder"
78
105
  :note-html="info?.noteHTML"
106
+ :clicks="props.clicks"
79
107
  />
80
108
  <textarea
81
109
  v-else
@@ -83,7 +111,7 @@ onClickOutside(input, () => {
83
111
  v-model="note"
84
112
  class="prose resize-none overflow-auto outline-none bg-transparent block border-primary border-2"
85
113
  style="line-height: 1.75;"
86
- :style="props.style"
114
+ :style="[props.style, inputHeight != null ? { height: `${inputHeight}px` } : {}]"
87
115
  :class="props.class"
88
116
  :placeholder="placeholder"
89
117
  @keydown.esc=" editing = false"
@@ -5,6 +5,7 @@ import NoteDisplay from './NoteDisplay.vue'
5
5
  const props = defineProps<{
6
6
  no?: number
7
7
  class?: string
8
+ clicks?: number | string
8
9
  }>()
9
10
 
10
11
  const { info } = useSlideInfo(props.no)
@@ -15,5 +16,6 @@ const { info } = useSlideInfo(props.no)
15
16
  :class="props.class"
16
17
  :note="info?.note"
17
18
  :note-html="info?.noteHTML"
19
+ :clicks="props.clicks"
18
20
  />
19
21
  </template>
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import type { ClicksContext } from '@slidev/types'
3
+ import type { Ref } from 'vue'
4
+ import { computed } from 'vue'
5
+ import { CLICKS_MAX } from '../constants'
6
+
7
+ const props = defineProps<{
8
+ clickContext: [Ref<number>, ClicksContext]
9
+ }>()
10
+
11
+ const total = computed(() => props.clickContext[1].total)
12
+ const current = computed({
13
+ get() {
14
+ return props.clickContext[0].value > total.value ? -1 : props.clickContext[0].value
15
+ },
16
+ set(value: number) {
17
+ // eslint-disable-next-line vue/no-mutating-props
18
+ props.clickContext[0].value = value
19
+ },
20
+ })
21
+
22
+ const range = computed(() => Array.from({ length: total.value + 1 }, (_, i) => i))
23
+
24
+ function onMousedown() {
25
+ if (current.value < 0 || current.value > total.value)
26
+ current.value = 0
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <div
32
+ class="flex gap-0.5 items-center select-none"
33
+ :title="`Clicks in this slide: ${total}`"
34
+ >
35
+ <div class="flex gap-1 items-center min-w-16">
36
+ <carbon:cursor-1 text-sm op50 />
37
+ <span v-if="current <= total && current >= 0" text-primary>{{ current }}/</span>
38
+ <span op50>{{ total }}</span>
39
+ </div>
40
+ <div
41
+ relative flex-auto h5 flex="~"
42
+ @dblclick="current = CLICKS_MAX"
43
+ >
44
+ <div
45
+ v-for="i of range" :key="i"
46
+ border="y main" of-hidden relative
47
+ :class="[
48
+ i === 0 ? 'rounded-l border-l' : '',
49
+ i === total ? 'rounded-r border-r' : '',
50
+ ]"
51
+ :style="{ width: `${1 / total * 100}%` }"
52
+ >
53
+ <div absolute inset-0 z--1 :class=" i <= current ? 'bg-primary op20' : ''" />
54
+ <div
55
+ :class="[
56
+ +i === +current ? 'text-primary font-bold op100 border-primary' : 'op30 border-main',
57
+ i === 0 ? 'rounded-l' : '',
58
+ i === total ? 'rounded-r' : 'border-r-2',
59
+ ]"
60
+ w-full h-full text-xs flex items-center justify-center
61
+ >
62
+ {{ i }}
63
+ </div>
64
+ </div>
65
+ <input
66
+ v-model="current"
67
+ class="range" absolute inset-0
68
+ type="range" :min="0" :max="total" :step="1" z-10 op0
69
+ :style="{ '--thumb-width': `${1 / (total + 1) * 100}%` }"
70
+ @mousedown="onMousedown"
71
+ >
72
+ </div>
73
+ </div>
74
+ </template>
75
+
76
+ <style scoped>
77
+ .range {
78
+ -webkit-appearance: none;
79
+ appearance: none;
80
+ background: transparent;
81
+ }
82
+ .range::-webkit-slider-thumb {
83
+ -webkit-appearance: none;
84
+ height: 100%;
85
+ width: var(--thumb-width, 0.5rem);
86
+ }
87
+
88
+ .range::-moz-range-thumb {
89
+ height: 100%;
90
+ width: var(--thumb-width, 0.5rem);
91
+ }
92
+ </style>
@@ -9,11 +9,8 @@ import { getSlideClass } from '../utils'
9
9
  import type { SlidevContextNav } from '../modules/context'
10
10
  import SlideWrapper from './SlideWrapper'
11
11
 
12
- // @ts-expect-error virtual module
13
- import GlobalTop from '/@slidev/global-components/top'
14
-
15
- // @ts-expect-error virtual module
16
- import GlobalBottom from '/@slidev/global-components/bottom'
12
+ import GlobalTop from '#slidev/global-components/top'
13
+ import GlobalBottom from '#slidev/global-components/bottom'
17
14
 
18
15
  const props = defineProps<{
19
16
  clicksContext: ClicksContext
@@ -108,18 +108,18 @@ async function start() {
108
108
  }
109
109
  }
110
110
 
111
- input[type="text"] {
111
+ input[type='text'] {
112
112
  @apply border border-main rounded px-2 py-1;
113
113
  }
114
114
 
115
115
  button {
116
116
  @apply bg-orange-400 text-white px-4 py-1 rounded border-b-2 border-orange-600;
117
- @apply hover:(bg-orange-500 border-orange-700)
117
+ @apply hover:(bg-orange-500 border-orange-700);
118
118
  }
119
119
 
120
120
  button.cancel {
121
121
  @apply bg-gray-400 bg-opacity-50 text-white px-4 py-1 rounded border-b-2 border-main;
122
- @apply hover:(bg-opacity-75 border-opacity-75)
122
+ @apply hover:(bg-opacity-75 border-opacity-75);
123
123
  }
124
124
  }
125
125
  </style>
@@ -3,7 +3,7 @@ import { provideLocal, useElementSize, useStyleTag } from '@vueuse/core'
3
3
  import { computed, ref, watchEffect } from 'vue'
4
4
  import { configs, slideAspect, slideHeight, slideWidth } from '../env'
5
5
  import { injectionSlideScale } from '../constants'
6
- import { isPrintMode } from '../logic/nav'
6
+ import { clicksDirection, isPrintMode } from '../logic/nav'
7
7
 
8
8
  const props = defineProps({
9
9
  width: {
@@ -55,6 +55,8 @@ const style = computed(() => ({
55
55
 
56
56
  const className = computed(() => ({
57
57
  'select-none': !configs.selectable,
58
+ 'slidev-nav-go-forward': clicksDirection.value > 0,
59
+ 'slidev-nav-go-backward': clicksDirection.value < 0,
58
60
  }))
59
61
 
60
62
  if (props.isMain) {
@@ -69,8 +71,8 @@ provideLocal(injectionSlideScale, scale as any)
69
71
  </script>
70
72
 
71
73
  <template>
72
- <div id="slide-container" ref="root" :class="className">
73
- <div id="slide-content" :style="style">
74
+ <div id="slide-container" ref="root" class="slidev-slides-container" :class="className">
75
+ <div id="slide-content" class="slidev-slide-content" :style="style">
74
76
  <slot />
75
77
  </div>
76
78
  <slot name="controls" />
@@ -7,6 +7,7 @@ import { currentPage, go as goSlide, rawRoutes } from '../logic/nav'
7
7
  import { currentOverviewPage, overviewRowCount } from '../logic/overview'
8
8
  import { useFixedClicks } from '../composables/useClicks'
9
9
  import { getSlideClass } from '../utils'
10
+ import { CLICKS_MAX } from '../constants'
10
11
  import SlideContainer from './SlideContainer.vue'
11
12
  import SlideWrapper from './SlideWrapper'
12
13
  import DrawingPreview from './DrawingPreview.vue'
@@ -139,7 +140,7 @@ watchEffect(() => {
139
140
  <SlideWrapper
140
141
  :is="route.component"
141
142
  v-if="route?.component"
142
- :clicks-context="useFixedClicks(route, 99999)[1]"
143
+ :clicks-context="useFixedClicks(route, CLICKS_MAX)[1]"
143
144
  :class="getSlideClass(route)"
144
145
  :route="route"
145
146
  render-context="overview"
@@ -6,14 +6,11 @@ import { useViewTransition } from '../composables/useViewTransition'
6
6
  import { skipTransition } from '../composables/hmr'
7
7
  import { usePrimaryClicks } from '../composables/useClicks'
8
8
  import SlideWrapper from './SlideWrapper'
9
-
10
- // @ts-expect-error virtual module
11
- import GlobalTop from '/@slidev/global-components/top'
12
-
13
- // @ts-expect-error virtual module
14
- import GlobalBottom from '/@slidev/global-components/bottom'
15
9
  import PresenterMouse from './PresenterMouse.vue'
16
10
 
11
+ import GlobalTop from '#slidev/global-components/top'
12
+ import GlobalBottom from '#slidev/global-components/bottom'
13
+
17
14
  defineProps<{
18
15
  renderContext: 'slide' | 'presenter'
19
16
  }>()
@@ -77,10 +74,12 @@ function onAfterLeave() {
77
74
 
78
75
  <style scoped>
79
76
  #slideshow {
80
- @apply h-full;
77
+ height: 100%;
81
78
  }
82
79
 
83
80
  #slideshow > div {
84
- @apply h-full w-full absolute;
81
+ position: absolute;
82
+ height: 100%;
83
+ width: 100%;
85
84
  }
86
85
  </style>
@@ -51,9 +51,15 @@ const props = defineProps({
51
51
  grid-template-rows: repeat(2, 1fr);
52
52
  }
53
53
 
54
- .col-header { grid-area: 1 / 1 / 2 / 3; }
55
- .col-left { grid-area: 2 / 1 / 3 / 2; }
56
- .col-right { grid-area: 2 / 2 / 3 / 3; }
54
+ .col-header {
55
+ grid-area: 1 / 1 / 2 / 3;
56
+ }
57
+ .col-left {
58
+ grid-area: 2 / 1 / 3 / 2;
59
+ }
60
+ .col-right {
61
+ grid-area: 2 / 2 / 3 / 3;
62
+ }
57
63
  .col-bottom {
58
64
  align-self: end;
59
65
  grid-area: 3 / 1 / 3 / 3;
package/logic/nav.ts CHANGED
@@ -7,6 +7,7 @@ import { rawRoutes, router } from '../routes'
7
7
  import { configs } from '../env'
8
8
  import { skipTransition } from '../composables/hmr'
9
9
  import { usePrimaryClicks } from '../composables/useClicks'
10
+ import { CLICKS_MAX } from '../constants'
10
11
  import { useRouteQuery } from './route'
11
12
  import { isDrawing } from './drawings'
12
13
 
@@ -22,6 +23,7 @@ nextTick(() => {
22
23
  })
23
24
 
24
25
  export const navDirection = ref(0)
26
+ export const clicksDirection = ref(0)
25
27
 
26
28
  export const route = computed(() => router.currentRoute.value)
27
29
 
@@ -38,7 +40,7 @@ export const queryClicks = computed({
38
40
  get() {
39
41
  // eslint-disable-next-line ts/no-use-before-define
40
42
  if (clicksContext.value.disabled)
41
- return 99999
43
+ return CLICKS_MAX
42
44
  let v = +(queryClicksRaw.value || 0)
43
45
  if (Number.isNaN(v))
44
46
  v = 0
@@ -84,6 +86,7 @@ watch(currentRoute, (next, prev) => {
84
86
  })
85
87
 
86
88
  export async function next() {
89
+ clicksDirection.value = 1
87
90
  if (clicksTotal.value <= queryClicks.value)
88
91
  await nextSlide()
89
92
  else
@@ -91,6 +94,7 @@ export async function next() {
91
94
  }
92
95
 
93
96
  export async function prev() {
97
+ clicksDirection.value = -1
94
98
  if (queryClicks.value <= 0)
95
99
  await prevSlide()
96
100
  else
@@ -102,11 +106,13 @@ export function getPath(no: number | string) {
102
106
  }
103
107
 
104
108
  export async function nextSlide() {
109
+ clicksDirection.value = 1
105
110
  if (currentPage.value < rawRoutes.length)
106
111
  await go(currentPage.value + 1)
107
112
  }
108
113
 
109
114
  export async function prevSlide(lastClicks = true) {
115
+ clicksDirection.value = -1
110
116
  const next = Math.max(1, currentPage.value - 1)
111
117
  await go(next)
112
118
  if (lastClicks && clicksTotal.value)
package/main.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /// <reference types="@slidev/types/client" />
2
+
1
3
  import { createApp } from 'vue'
2
4
  import { createHead } from '@unhead/vue'
3
5
  import App from './App.vue'
@@ -7,7 +9,7 @@ import { createVClickDirectives } from './modules/v-click'
7
9
  import { createVMarkDirective } from './modules/v-mark'
8
10
  import { createSlidevContext } from './modules/context'
9
11
 
10
- import '/@slidev/styles'
12
+ import '#slidev/styles'
11
13
 
12
14
  const app = createApp(App)
13
15
  app.use(router)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.48.0-beta.10",
4
+ "version": "0.48.0-beta.12",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -54,8 +54,8 @@
54
54
  "unocss": "^0.58.5",
55
55
  "vue": "^3.4.19",
56
56
  "vue-router": "^4.3.0",
57
- "@slidev/types": "0.48.0-beta.10",
58
- "@slidev/parser": "0.48.0-beta.10"
57
+ "@slidev/parser": "0.48.0-beta.12",
58
+ "@slidev/types": "0.48.0-beta.12"
59
59
  },
60
60
  "devDependencies": {
61
61
  "vite": "^5.1.4"
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
- import { nextTick, onMounted, reactive, ref } from 'vue'
2
+ import type { Ref } from 'vue'
3
+ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
3
4
  import { useHead } from '@unhead/vue'
5
+ import type { RouteRecordRaw } from 'vue-router'
6
+ import type { ClicksContext } from 'packages/types'
4
7
  import { themeVars } from '../env'
5
8
  import { rawRoutes } from '../logic/nav'
6
9
  import { useFixedClicks } from '../composables/useClicks'
@@ -11,6 +14,8 @@ import SlideWrapper from '../internals/SlideWrapper'
11
14
  import DrawingPreview from '../internals/DrawingPreview.vue'
12
15
  import IconButton from '../internals/IconButton.vue'
13
16
  import NoteEditor from '../internals/NoteEditor.vue'
17
+ import OverviewClicksSlider from '../internals/OverviewClicksSlider.vue'
18
+ import { CLICKS_MAX } from '../constants'
14
19
 
15
20
  const cardWidth = 450
16
21
 
@@ -21,6 +26,25 @@ useHead({
21
26
  const blocks: Map<number, HTMLElement> = reactive(new Map())
22
27
  const activeBlocks = ref<number[]>([])
23
28
  const edittingNote = ref<number | null>(null)
29
+ const wordCounts = computed(() => rawRoutes.map(route => wordCount(route.meta?.slide?.note || '')))
30
+ const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
31
+ const totalClicks = computed(() => rawRoutes.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
32
+
33
+ const clicksContextMap = new WeakMap<RouteRecordRaw, [Ref<number>, ClicksContext]>()
34
+ function getClickContext(route: RouteRecordRaw) {
35
+ // We create a local clicks context to calculate the total clicks of the slide
36
+ if (!clicksContextMap.has(route))
37
+ clicksContextMap.set(route, useFixedClicks(route, CLICKS_MAX))
38
+ return clicksContextMap.get(route)!
39
+ }
40
+
41
+ function getSlideClicks(route: RouteRecordRaw) {
42
+ return route.meta?.clicks || getClickContext(route)?.[1]?.total
43
+ }
44
+
45
+ function wordCount(str: string) {
46
+ return str.match(/[\w\d\’\'-]+/gi)?.length || 0
47
+ }
24
48
 
25
49
  function isElementInViewport(el: HTMLElement) {
26
50
  const rect = el.getBoundingClientRect()
@@ -109,31 +133,39 @@ onMounted(() => {
109
133
  class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
110
134
  >
111
135
  <div class="select-none w-13 text-right my4">
112
- <div class="text-3xl op20">
136
+ <div class="text-3xl op20 mb2">
113
137
  {{ idx + 1 }}
114
138
  </div>
115
139
  </div>
116
- <div
117
- class="border rounded border-main overflow-hidden bg-main my5 select-none h-max"
118
- :style="themeVars"
119
- @dblclick="openSlideInNewTab(route.path)"
120
- >
121
- <SlideContainer
122
- :key="route.path"
123
- :width="cardWidth"
124
- :clicks-disabled="true"
125
- class="pointer-events-none important:[&_*]:select-none"
140
+ <div class="flex flex-col gap-2 my5">
141
+ <div
142
+ class="border rounded border-main overflow-hidden bg-main select-none h-max"
143
+ :style="themeVars"
144
+ @dblclick="openSlideInNewTab(route.path)"
126
145
  >
127
- <SlideWrapper
128
- :is="route.component"
129
- v-if="route?.component"
130
- :clicks-context="useFixedClicks(route, 99999)[1]"
131
- :class="getSlideClass(route)"
132
- :route="route"
133
- render-context="overview"
134
- />
135
- <DrawingPreview :page="+route.path" />
136
- </SlideContainer>
146
+ <SlideContainer
147
+ :key="route.path"
148
+ :width="cardWidth"
149
+ :clicks-disabled="true"
150
+ class="pointer-events-none important:[&_*]:select-none"
151
+ >
152
+ <SlideWrapper
153
+ :is="route.component"
154
+ v-if="route?.component"
155
+ :clicks-context="getClickContext(route)[1]"
156
+ :class="getSlideClass(route)"
157
+ :route="route"
158
+ render-context="overview"
159
+ />
160
+ <DrawingPreview :page="+route.path" />
161
+ </SlideContainer>
162
+ </div>
163
+ <OverviewClicksSlider
164
+ v-if="getSlideClicks(route)"
165
+ mt-2
166
+ :click-context="getClickContext(route)"
167
+ class="w-full"
168
+ />
137
169
  </div>
138
170
  <div class="py3 mt-0.5 mr--8 ml--4 op0 transition group-hover:op100">
139
171
  <IconButton
@@ -148,10 +180,26 @@ onMounted(() => {
148
180
  <NoteEditor
149
181
  :no="idx"
150
182
  class="max-w-250 w-250 text-lg rounded p3"
183
+ :auto-height="true"
151
184
  :editing="edittingNote === idx"
185
+ :clicks="getClickContext(route)[0].value"
186
+ @dblclick="edittingNote !== idx ? edittingNote = idx : null"
152
187
  @update:editing="edittingNote = null"
153
188
  />
189
+ <div
190
+ v-if="wordCounts[idx] > 0"
191
+ class="select-none absolute bottom-0 right-0 bg-main rounded-tl p2 op35 text-xs"
192
+ >
193
+ {{ wordCounts[idx] }} words
194
+ </div>
154
195
  </div>
155
196
  </main>
197
+ <div class="absolute top-0 right-0 px3 py1.5 border-b border-l rounded-lb bg-main border-main select-none">
198
+ <div class="text-xs op50">
199
+ {{ rawRoutes.length }} slides ·
200
+ {{ totalClicks + rawRoutes.length - 1 }} clicks ·
201
+ {{ totalWords }} words
202
+ </div>
203
+ </div>
156
204
  </div>
157
205
  </template>
@@ -144,6 +144,7 @@ onMounted(() => {
144
144
  :no="currentSlideId"
145
145
  class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
146
146
  :editing="notesEditing"
147
+ :clicks="clicksContext.current"
147
148
  :style="{ fontSize: `${presenterNotesFontSize}em` }"
148
149
  />
149
150
  <NoteStatic
@@ -152,6 +153,7 @@ onMounted(() => {
152
153
  :no="currentSlideId"
153
154
  class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
154
155
  :style="{ fontSize: `${presenterNotesFontSize}em` }"
156
+ :clicks="clicksContext.current"
155
157
  />
156
158
  <div class="border-t border-main py-1 px-2 text-sm">
157
159
  <IconButton title="Increase font size" @click="increasePresenterFontSize">
@@ -185,26 +187,26 @@ onMounted(() => {
185
187
  <SlidesOverview v-model="showOverview" />
186
188
  </template>
187
189
 
188
- <style lang="postcss" scoped>
190
+ <style scoped>
189
191
  .slidev-presenter {
190
192
  --slidev-controls-foreground: current;
191
193
  }
192
194
 
193
- .timer-btn:hover {
194
- & > :first-child {
195
- @apply opacity-0;
196
- }
197
- & > :last-child {
198
- @apply opacity-100;
199
- }
195
+ .timer-btn:hover > :first-child {
196
+ opacity: 0;
197
+ }
198
+ .timer-btn:hover > :last-child {
199
+ opacity: 1;
200
200
  }
201
201
 
202
202
  .section-title {
203
- @apply px-4 py-2 text-xl;
203
+ --uno: px-4 py-2 text-xl;
204
204
  }
205
205
 
206
206
  .grid-container {
207
- @apply h-full w-full bg-gray-400 bg-opacity-15;
207
+ --uno: bg-active;
208
+ height: 100%;
209
+ width: 100%;
208
210
  display: grid;
209
211
  gap: 1px 1px;
210
212
  }
@@ -213,20 +215,20 @@ onMounted(() => {
213
215
  grid-template-columns: 1fr 1fr;
214
216
  grid-template-rows: min-content 2fr 1fr min-content;
215
217
  grid-template-areas:
216
- "top top"
217
- "main main"
218
- "note next"
219
- "bottom bottom";
218
+ 'top top'
219
+ 'main main'
220
+ 'note next'
221
+ 'bottom bottom';
220
222
  }
221
223
 
222
224
  .grid-container.layout2 {
223
225
  grid-template-columns: 3fr 2fr;
224
226
  grid-template-rows: min-content 2fr 1fr min-content;
225
227
  grid-template-areas:
226
- "top top"
227
- "note main"
228
- "note next"
229
- "bottom bottom";
228
+ 'top top'
229
+ 'note main'
230
+ 'note next'
231
+ 'bottom bottom';
230
232
  }
231
233
 
232
234
  @media (max-aspect-ratio: 3/5) {
@@ -234,11 +236,11 @@ onMounted(() => {
234
236
  grid-template-columns: 1fr;
235
237
  grid-template-rows: min-content 1fr 1fr 1fr min-content;
236
238
  grid-template-areas:
237
- "top"
238
- "main"
239
- "note"
240
- "next"
241
- "bottom";
239
+ 'top'
240
+ 'main'
241
+ 'note'
242
+ 'next'
243
+ 'bottom';
242
244
  }
243
245
  }
244
246
 
@@ -247,38 +249,40 @@ onMounted(() => {
247
249
  grid-template-columns: 1fr 1.1fr 0.9fr;
248
250
  grid-template-rows: min-content 1fr 2fr min-content;
249
251
  grid-template-areas:
250
- "top top top"
251
- "main main next"
252
- "main main note"
253
- "bottom bottom bottom";
252
+ 'top top top'
253
+ 'main main next'
254
+ 'main main note'
255
+ 'bottom bottom bottom';
254
256
  }
255
257
  }
256
258
 
257
259
  .progress-bar {
258
- @apply fixed left-0 right-0 bottom-0;
260
+ --uno: fixed left-0 right-0 bottom-0;
259
261
  }
260
262
 
261
263
  .grid-section {
262
- @apply bg-main;
263
-
264
- &.top {
265
- grid-area: top;
266
- }
267
- &.main {
268
- grid-area: main;
269
- }
270
- &.next {
271
- grid-area: next;
272
- }
273
- &.note {
274
- grid-area: note;
275
- }
276
- &.bottom {
277
- grid-area: bottom;
278
- }
264
+ --uno: bg-main;
279
265
  }
280
266
 
267
+ .grid-section.top {
268
+ grid-area: top;
269
+ }
270
+ .grid-section.main {
271
+ grid-area: main;
272
+ }
273
+ .grid-section.next {
274
+ grid-area: next;
275
+ }
276
+ .grid-section.note {
277
+ grid-area: note;
278
+ }
279
+ .grid-section.bottom {
280
+ grid-area: bottom;
281
+ }
281
282
  .context {
282
- @apply absolute top-0 left-0 px-1 text-xs bg-gray-400 bg-opacity-50 opacity-75 rounded-br-md;
283
+ position: absolute;
284
+ top: 0;
285
+ left: 0;
286
+ --uno: px-1 text-xs bg-gray-400 bg-opacity-50 opacity-75 rounded-br-md;
283
287
  }
284
288
  </style>
package/routes.ts CHANGED
@@ -1,15 +1,10 @@
1
1
  import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
2
2
  import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
3
- import type { TransitionGroupProps } from 'vue'
4
- import type { ClicksContext, SlideInfo } from '@slidev/types'
5
3
 
6
- // @ts-expect-error missing types
7
- import _rawRoutes, { redirects } from '/@slidev/routes'
4
+ import { rawRoutes, redirects } from '#slidev/routes'
5
+ import configs from '#slidev/configs'
8
6
 
9
- // @ts-expect-error missing types
10
- import _configs from '/@slidev/configs'
11
-
12
- export const rawRoutes = _rawRoutes as RouteRecordRaw[]
7
+ export { rawRoutes }
13
8
 
14
9
  export const routes: RouteRecordRaw[] = [
15
10
  {
@@ -34,12 +29,12 @@ export const routes: RouteRecordRaw[] = [
34
29
 
35
30
  if (__SLIDEV_FEATURE_PRESENTER__) {
36
31
  function passwordGuard(to: RouteLocationNormalized) {
37
- if (!_configs.remote || _configs.remote === to.query.password)
32
+ if (!configs.remote || configs.remote === to.query.password)
38
33
  return true
39
- if (_configs.remote && to.query.password === undefined) {
34
+ if (configs.remote && to.query.password === undefined) {
40
35
  // eslint-disable-next-line no-alert
41
36
  const password = prompt('Enter password')
42
- if (_configs.remote === password)
37
+ if (configs.remote === password)
43
38
  return true
44
39
  }
45
40
  if (to.params.no)
@@ -87,28 +82,3 @@ export const router = createRouter({
87
82
  : createWebHistory(import.meta.env.BASE_URL),
88
83
  routes,
89
84
  })
90
-
91
- declare module 'vue-router' {
92
- interface RouteMeta {
93
- // inherited from frontmatter
94
- layout: string
95
- name?: string
96
- class?: string
97
- clicks?: number
98
- transition?: string | TransitionGroupProps | undefined
99
- preload?: boolean
100
-
101
- // slide info
102
- slide?: Omit<SlideInfo, 'source'> & {
103
- noteHTML: string
104
- filepath: string
105
- start: number
106
- id: number
107
- no: number
108
- }
109
-
110
- // private fields
111
- __clicksContext: null | ClicksContext
112
- __preloaded?: boolean
113
- }
114
- }
package/shim-vue.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ declare module 'vue' {
2
+ import type { UnwrapNestedRefs } from 'vue'
3
+ import type { SlidevContext } from './modules/context'
4
+
5
+ interface ComponentCustomProperties {
6
+ $slidev: UnwrapNestedRefs<SlidevContext>
7
+ }
8
+ }
9
+
10
+ declare module 'vue-router' {
11
+ interface RouteMeta {
12
+ // inherited from frontmatter
13
+ layout: string
14
+ name?: string
15
+ class?: string
16
+ clicks?: number
17
+ transition?: string | TransitionGroupProps | undefined
18
+ preload?: boolean
19
+
20
+ // slide info
21
+ slide?: Omit<SlideInfo, 'source'> & {
22
+ noteHTML: string
23
+ filepath: string
24
+ start: number
25
+ id: number
26
+ no: number
27
+ }
28
+
29
+ // private fields
30
+ __clicksContext: null | ClicksContext
31
+ __preloaded?: boolean
32
+ }
33
+ }
34
+
35
+ export {}
package/shim.d.ts CHANGED
@@ -1,9 +1,3 @@
1
- declare interface Window {
2
- // extend the window
3
- }
4
-
5
- declare module '*.vue';
6
-
7
1
  // with unplugin-vue-markdown, markdowns can be treat as Vue components
8
2
  declare module '*.md' {
9
3
  import type { ComponentOptions } from 'vue'
@@ -12,12 +6,6 @@ declare module '*.md' {
12
6
  export default component
13
7
  }
14
8
 
15
- declare module '/@slidev/configs' {
16
- import { SlidevConfig } from '@slidev/types'
17
-
18
- export default SlidevConfig
19
- }
20
-
21
9
  declare module 'mermaid/dist/mermaid.esm.mjs' {
22
10
  import Mermaid from 'mermaid/dist/mermaid.d.ts'
23
11
 
package/styles/code.css CHANGED
@@ -59,7 +59,9 @@ html:not(.dark) .shiki span {
59
59
  .slidev-code-line-numbers .slidev-code code .line::before {
60
60
  content: counter(step);
61
61
  counter-increment: step;
62
- @apply w-4 mr-6 inline-block text-right text-gray-400 dark:text-gray-600;
62
+ display: inline-block;
63
+ text-align: right;
64
+ --uno: w-4 mr-6 text-gray-400 dark-text-gray-600;
63
65
  }
64
66
 
65
67
  /* Inline Code */
@@ -67,7 +69,7 @@ html:not(.dark) .shiki span {
67
69
  font-size: 0.9em;
68
70
  background: var(--slidev-code-background);
69
71
  border-radius: var(--slidev-code-radius);
70
- @apply font-light py-0.5 px-1.5;
72
+ --uno: font-light py-0.5 px-1.5;
71
73
  }
72
74
 
73
75
  .slidev-layout :not(pre) > code:before {
@@ -82,4 +84,6 @@ html:not(.dark) .shiki span {
82
84
  }
83
85
 
84
86
  /* CodeMirror */
85
- .CodeMirror pre.CodeMirror-placeholder { opacity: 0.4; }
87
+ .CodeMirror pre.CodeMirror-placeholder {
88
+ opacity: 0.4;
89
+ }
package/styles/index.css CHANGED
@@ -23,15 +23,16 @@ html {
23
23
  }
24
24
 
25
25
  .slidev-icon-btn.shallow {
26
- @apply opacity-30
26
+ opacity: 0.3;
27
27
  }
28
28
 
29
29
  .slidev-icon-btn.active {
30
- @apply opacity-100
30
+ opacity: 1;
31
31
  }
32
32
 
33
33
  .slidev-icon-btn.disabled {
34
- @apply opacity-25 pointer-events-none;
34
+ opacity: 0.25;
35
+ pointer-events: none;
35
36
  }
36
37
 
37
38
  .slidev-vclick-target {
@@ -39,11 +40,13 @@ html {
39
40
  }
40
41
 
41
42
  .slidev-vclick-hidden {
42
- @apply !opacity-0 !pointer-events-none;
43
+ opacity: 0 !important;
44
+ pointer-events: none !important;
45
+ user-select: none !important;
43
46
  }
44
47
 
45
48
  .slidev-vclick-fade {
46
- @apply opacity-50;
49
+ opacity: 0.5;
47
50
  }
48
51
 
49
52
  .slidev-icon {
@@ -53,7 +56,44 @@ html {
53
56
  }
54
57
 
55
58
  .slidev-page {
56
- @apply absolute top-0 left-0 right-0 w-full relative;
59
+ position: relative;
60
+ top: 0;
61
+ left: 0;
62
+ right: 0;
63
+ width: 100%;
64
+ }
65
+
66
+ /* Note Clicks */
67
+
68
+ .slidev-note-with-clicks .slidev-note-fade {
69
+ color: #888888ab;
70
+ }
71
+
72
+ .slidev-note-click-mark {
73
+ font-size: 0.8em;
74
+ --uno: text-violet bg-violet/10 mx1 px1 font-mono rounded flex flex-inline
75
+ items-center align-middle;
76
+ }
77
+
78
+ .slidev-note-click-mark::before {
79
+ content: '';
80
+ display: inline-block;
81
+ --un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 32 32' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M23 28a1 1 0 0 1-.71-.29l-6.13-6.14l-3.33 5a1 1 0 0 1-1 .44a1 1 0 0 1-.81-.7l-6-20A1 1 0 0 1 6.29 5l20 6a1 1 0 0 1 .7.81a1 1 0 0 1-.44 1l-5 3.33l6.14 6.13a1 1 0 0 1 0 1.42l-4 4A1 1 0 0 1 23 28m0-2.41L25.59 23l-7.16-7.15l5.25-3.5L7.49 7.49l4.86 16.19l3.5-5.25Z'/%3E%3C/svg%3E");
82
+ -webkit-mask: var(--un-icon) no-repeat;
83
+ mask: var(--un-icon) no-repeat;
84
+ -webkit-mask-size: 100% 100%;
85
+ mask-size: 100% 100%;
86
+ background-color: currentColor;
87
+ color: inherit;
88
+ width: 1.2em;
89
+ height: 1.2em;
90
+ opacity: 0.8;
91
+ }
92
+
93
+ .slidev-note-click-mark::after {
94
+ content: attr(data-clicks);
95
+ display: inline-block;
96
+ transform: translateY(0.1em);
57
97
  }
58
98
 
59
99
  /* Transform the position back for Rough Notation (v-mark) */
package/styles/katex.css CHANGED
@@ -2,4 +2,4 @@
2
2
  }
3
3
  .slidev-katex-wrapper .mord.dishonored {
4
4
  opacity: 0.3;
5
- }
5
+ }
@@ -1,7 +1,8 @@
1
1
  .slidev-layout {
2
2
  @apply px-14 py-10 text-[1.1rem] h-full;
3
3
 
4
- pre, code {
4
+ pre,
5
+ code {
5
6
  @apply select-text;
6
7
  }
7
8
 
@@ -79,11 +80,13 @@
79
80
  @apply border-current border-b border-dashed hover:text-primary hover:border-solid;
80
81
  }
81
82
 
82
- td, th {
83
+ td,
84
+ th {
83
85
  @apply p-2 py-3;
84
86
  }
85
87
 
86
- b, strong {
88
+ b,
89
+ strong {
87
90
  @apply font-600;
88
91
  }
89
92
 
@@ -94,8 +97,8 @@
94
97
  }
95
98
 
96
99
  .slidev-layout,
97
- [dir=ltr],
98
- .slidev-layout [dir=ltr] {
100
+ [dir='ltr'],
101
+ .slidev-layout [dir='ltr'] {
99
102
  h1 {
100
103
  @apply -ml-[0.05em] mr-0;
101
104
  }
@@ -109,10 +112,10 @@
109
112
  }
110
113
  }
111
114
 
112
- [dir=rtl],
113
- .slidev-layout [dir=rtl] {
115
+ [dir='rtl'],
116
+ .slidev-layout [dir='rtl'] {
114
117
  h1 {
115
- @apply -mr-[0.05em] ml-0;
118
+ @apply -mr-[0.05em] ml-0;
116
119
  }
117
120
 
118
121
  h6 {
package/styles/vars.css CHANGED
@@ -7,7 +7,7 @@
7
7
  --slidev-code-line-height: 18px;
8
8
  --slidev-code-radius: 4px;
9
9
  --slidev-code-margin: 4px 0;
10
- --slidev-theme-primary: #3AB9D5;
10
+ --slidev-theme-primary: #3ab9d5;
11
11
 
12
12
  --slidev-transition-duration: 0.5s;
13
13
  --slidev-slide-container-background: black;
package/uno.config.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  transformerDirectives,
9
9
  transformerVariantGroup,
10
10
  } from 'unocss'
11
+ import { variantMatcher } from '@unocss/preset-mini/utils'
11
12
 
12
13
  export default defineConfig({
13
14
  safelist: [
@@ -28,6 +29,13 @@ export default defineConfig({
28
29
  'abs-bl': 'absolute bottom-0 left-0',
29
30
  'abs-br': 'absolute bottom-0 right-0',
30
31
  },
32
+ // Slidev Specific Variants, probably extrat to a preset later
33
+ variants: [
34
+ // `forward:` and `backward:` variant to selectively apply styles based on the direction of the slide
35
+ // For example, `forward:text-red` will only apply to the slides that are navigated forward
36
+ variantMatcher('forward', input => ({ prefix: `.slidev-nav-go-forward ${input.prefix}` })),
37
+ variantMatcher('backward', input => ({ prefix: `.slidev-nav-go-backward ${input.prefix}` })),
38
+ ],
31
39
  presets: [
32
40
  presetUno(),
33
41
  presetAttributify(),