Skip to content

Commit 667028e

Browse files
committed
feat: introduce list overview
1 parent b8f63cc commit 667028e

19 files changed

+194
-35
lines changed

demo/composable-vue/components/DarkToggle.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isDark, toggleDark } from '@slidev/client/logic/dark.ts'
44

55
<template>
66
<button
7-
class="bg-$slidev-theme-primary rounded border-b-2 border-green-900 text-sm px-2 pt-1.5 pb-1 inline-block !outline-none hover:bg-opacity-85"
7+
class="bg-primary rounded border-b-2 border-green-900 text-sm px-2 pt-1.5 pb-1 inline-block !outline-none hover:bg-opacity-85"
88
@click="toggleDark"
99
>
1010
<div class="flex">

demo/composable-vue/slides.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ layout: center
9292
<a class="!border-none" href="https://github.com/vueuse/vueuse" target="__blank"><img class="mt-2 h-4 inline mx-0.5" alt="GitHub stars" src="https://img.shields.io/github/stars/vueuse/vueuse?style=social"></a>
9393
</div>
9494
</div>
95-
<div class="border-l border-gray-400 border-opacity-25 !all:leading-12 !all:list-none my-auto">
95+
<div class="border-l border-main !all:leading-12 !all:list-none my-auto">
9696

9797
- Works for both Vue 2 and 3
9898
- Tree-shakeable ESM

packages/client/internals/DrawingControls.vue

+1-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ function setBrushColor(color: typeof brush.color) {
4040

4141
<template>
4242
<Draggable
43-
class="flex flex-wrap text-xl p-2 gap-1 rounded-md bg-main shadow transition-opacity duration-200 z-20"
44-
dark="border border-gray-400 border-opacity-10"
43+
class="flex flex-wrap text-xl p-2 gap-1 rounded-md bg-main shadow transition-opacity duration-200 z-20 border border-main"
4544
:class="drawingEnabled ? '' : drawingPinned ? 'opacity-40 hover:opacity-90' : 'opacity-0 pointer-events-none'"
4645
storage-key="slidev-drawing-pos"
4746
:initial-x="10"

packages/client/internals/Editor.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,13 @@ throttledWatch(
172172
<div class="flex pb-2 text-xl -mt-1">
173173
<div class="mr-4 rounded flex">
174174
<IconButton
175-
title="Switch to content tab" :class="tab === 'content' ? 'text-$slidev-theme-primary' : ''"
175+
title="Switch to content tab" :class="tab === 'content' ? 'text-primary' : ''"
176176
@click="switchTab('content')"
177177
>
178178
<carbon:account />
179179
</IconButton>
180180
<IconButton
181-
title="Switch to notes tab" :class="tab === 'note' ? 'text-$slidev-theme-primary' : ''"
181+
title="Switch to notes tab" :class="tab === 'note' ? 'text-primary' : ''"
182182
@click="switchTab('note')"
183183
>
184184
<carbon:align-box-bottom-right />
+4-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<script setup lang="ts">
22
defineProps<{
33
title: string
4+
icon?: string
45
}>()
56
</script>
67

78
<template>
89
<button class="slidev-icon-btn" :title="title" v-bind="$attrs">
910
<span class="sr-only">{{ title }}</span>
10-
<slot />
11+
<slot>
12+
<div :class="icon" />
13+
</slot>
1114
</button>
1215
</template>

packages/client/internals/NavControls.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function onMouseLeave() {
3434
3535
const barStyle = computed(() => props.persist
3636
? 'text-$slidev-controls-foreground bg-transparent'
37-
: 'rounded-md bg-main shadow dark:border dark:border-gray-400 dark:border-opacity-10')
37+
: 'rounded-md bg-main shadow dark:border dark:border-main')
3838
3939
const RecordingControls = shallowRef<any>()
4040
if (__SLIDEV_FEATURE_RECORD__)

packages/client/internals/NoteDisplay.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defineEmits(['click'])
2727
</div>
2828
<div
2929
v-else
30-
class="prose overflow-auto outline-none opacity-50 italic"
30+
class="prose overflow-auto outline-none opacity-50 italic select-none"
3131
:class="props.class"
3232
@click="$emit('click')"
3333
>

packages/client/internals/NoteEditor.vue

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<script setup lang="ts">
22
import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
33
import { ref, watch, watchEffect } from 'vue'
4-
import { currentSlideId } from '../logic/nav'
54
import { useDynamicSlideInfo } from '../logic/note'
65
import NoteDisplay from './NoteDisplay.vue'
76
87
const props = defineProps({
8+
no: {
9+
type: Number,
10+
},
911
class: {
1012
default: '',
1113
},
@@ -25,15 +27,15 @@ const emit = defineEmits([
2527
])
2628
const editing = useVModel(props, 'editing', emit, { passive: true })
2729
28-
const { info, update } = useDynamicSlideInfo(currentSlideId)
30+
const { info, update } = useDynamicSlideInfo(props.no)
2931
3032
const note = ref('')
3133
let timer: any
3234
3335
const { ignoreUpdates } = ignorableWatch(
3436
note,
3537
(v) => {
36-
const id = currentSlideId.value
38+
const id = props.no
3739
clearTimeout(timer)
3840
timer = setTimeout(() => {
3941
update({ note: v }, id)
+5-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
<script setup lang="ts">
2-
import { computed } from 'vue'
3-
import { currentRoute } from '../logic/nav'
2+
import { useSlideInfo } from '../logic/note'
43
import NoteDisplay from './NoteDisplay.vue'
54
65
const props = defineProps<{
6+
no?: number
77
class?: string
88
}>()
99
10-
const note = computed(() => currentRoute.value?.meta?.slide?.note)
11-
const noteHtml = computed(() => currentRoute.value?.meta?.slide?.noteHTML)
10+
const { info } = useSlideInfo(props.no)
1211
</script>
1312

1413
<template>
1514
<NoteDisplay
1615
:class="props.class"
17-
:note="note"
18-
:note-html="noteHtml"
16+
:note="info?.note"
17+
:note-html="info?.noteHTML"
1918
/>
2019
</template>

packages/client/internals/RecordingDialog.vue

+2-3
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async function start() {
109109
}
110110
111111
input[type="text"] {
112-
@apply border border-gray-400 rounded px-2 py-1;
112+
@apply border border-main rounded px-2 py-1;
113113
}
114114
115115
button {
@@ -118,8 +118,7 @@ async function start() {
118118
}
119119
120120
button.cancel {
121-
@apply bg-gray-400 text-white px-4 py-1 rounded border-b-2 border-gray-500;
122-
@apply bg-opacity-50 border-opacity-50;
121+
@apply bg-gray-400 bg-opacity-50 text-white px-4 py-1 rounded border-b-2 border-main;
123122
@apply hover:(bg-opacity-75 border-opacity-75)
124123
}
125124
}

packages/client/internals/SlidesOverview.vue

+17-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { currentPage, go as goSlide, rawRoutes } from '../logic/nav'
77
import { currentOverviewPage, overviewRowCount } from '../logic/overview'
88
import { useFixedClicks } from '../composables/useClicks'
99
import { getSlideClass } from '../utils'
10+
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
1011
import SlideContainer from './SlideContainer.vue'
1112
import SlideWrapper from './SlideWrapper'
1213
import DrawingPreview from './DrawingPreview.vue'
@@ -112,7 +113,7 @@ watchEffect(() => {
112113
>
113114
<div
114115
v-show="value"
115-
class="bg-main !bg-opacity-75 p-16 overflow-y-auto backdrop-blur-5px fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)]"
116+
class="bg-main !bg-opacity-75 p-16 py-20 overflow-y-auto backdrop-blur-5px fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)]"
116117
@click="close()"
117118
>
118119
<div
@@ -125,8 +126,8 @@ watchEffect(() => {
125126
class="relative"
126127
>
127128
<div
128-
class="inline-block border rounded border-opacity-50 overflow-hidden bg-main hover:border-$slidev-theme-primary transition"
129-
:class="(focus(idx + 1) || currentOverviewPage === idx + 1) ? 'border-$slidev-theme-primary' : 'border-gray-400'"
129+
class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition"
130+
:class="(focus(idx + 1) || currentOverviewPage === idx + 1) ? 'border-primary' : 'border-main'"
130131
:style="themeVars"
131132
@click="go(+route.path)"
132133
>
@@ -163,7 +164,17 @@ watchEffect(() => {
163164
</div>
164165
</div>
165166
</Transition>
166-
<IconButton v-if="value" title="Close" class="fixed text-2xl top-4 right-4 text-gray-400" @click="close">
167-
<carbon:close />
168-
</IconButton>
167+
<div class="fixed top-4 right-4 text-gray-400 flex items-center gap-4">
168+
<RouterLink
169+
v-if="__DEV__"
170+
to="/overview"
171+
tab-index="-1"
172+
class="border-main border px3 py1 rounded hover:bg-gray/5 hover:text-primary"
173+
>
174+
List overview
175+
</RouterLink>
176+
<IconButton v-if="value" title="Close" class="text-2xl" @click="close">
177+
<carbon:close />
178+
</IconButton>
179+
</div>
169180
</template>

packages/client/pages/notes.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function decreaseFontSize() {
5353
:placeholder="`No notes for Slide ${pageNo}.`"
5454
/>
5555
</div>
56-
<div class="flex-none border-t border-gray-400 border-opacity-20">
56+
<div class="flex-none border-t border-main">
5757
<div class="flex gap-1 items-center px-6 py-3">
5858
<IconButton :title="isFullscreen ? 'Close fullscreen' : 'Enter fullscreen'" @click="toggleFullscreen">
5959
<carbon:minimize v-if="isFullscreen" />

packages/client/pages/overview.vue

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<script setup lang="ts">
2+
import { nextTick, onMounted, reactive, ref } from 'vue'
3+
import { themeVars } from '../env'
4+
import { rawRoutes } from '../logic/nav'
5+
import { useFixedClicks } from '../composables/useClicks'
6+
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
7+
import { getSlideClass } from '../utils'
8+
import SlideContainer from '../internals/SlideContainer.vue'
9+
import SlideWrapper from '../internals/SlideWrapper'
10+
import DrawingPreview from '../internals/DrawingPreview.vue'
11+
import NoteDisplay from '../internals/NoteDisplay.vue'
12+
import IconButton from '../internals/IconButton.vue'
13+
14+
const cardWidth = 450
15+
16+
const blocks: Map<number, HTMLElement> = reactive(new Map())
17+
const activeBlocks = ref<number[]>([])
18+
19+
function isElementInViewport(el: HTMLElement) {
20+
const rect = el.getBoundingClientRect()
21+
const delta = 20
22+
return (
23+
rect.top >= 0 - delta
24+
&& rect.left >= 0 - delta
25+
&& rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + delta
26+
&& rect.right <= (window.innerWidth || document.documentElement.clientWidth) + delta
27+
)
28+
}
29+
30+
function checkActiveBlocks() {
31+
const active: number[] = []
32+
Array.from(blocks.entries())
33+
.forEach(([idx, el]) => {
34+
if (isElementInViewport(el))
35+
active.push(idx)
36+
})
37+
activeBlocks.value = active
38+
}
39+
40+
function openSlideInNewTab(path: string) {
41+
const a = document.createElement('a')
42+
a.target = '_blank'
43+
a.href = path
44+
a.click()
45+
}
46+
47+
function scrollToSlide(idx: number) {
48+
const el = blocks.get(idx)
49+
if (el)
50+
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
51+
}
52+
53+
onMounted(() => {
54+
nextTick(() => {
55+
checkActiveBlocks()
56+
})
57+
})
58+
</script>
59+
60+
<template>
61+
<div class="h-screen w-screen of-hidden flex">
62+
<nav class="h-full flex flex-col border-r border-main p2">
63+
<div class="of-auto flex flex-col flex-auto items-center">
64+
<button
65+
v-for="(route, idx) of rawRoutes"
66+
:key="route.path"
67+
class="relative transition duration-300 w-8 h-8 rounded hover:bg-gray:10 hover:op100"
68+
:class="[
69+
activeBlocks.includes(idx) ? 'op100 text-primary' : 'op20',
70+
]"
71+
@click="scrollToSlide(idx)"
72+
>
73+
<div>{{ idx + 1 }}</div>
74+
</button>
75+
</div>
76+
<IconButton
77+
v-if="!isColorSchemaConfigured"
78+
:title="isDark ? 'Switch to light mode theme' : 'Switch to dark mode theme'"
79+
@click="toggleDark()"
80+
>
81+
<carbon-moon v-if="isDark" />
82+
<carbon-sun v-else />
83+
</IconButton>
84+
</nav>
85+
<main
86+
class="flex-1 h-full of-auto"
87+
:style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`"
88+
@scroll="checkActiveBlocks"
89+
>
90+
<div class="px4 py2 text-orange bg-orange:5">
91+
<span font-bold>List Overview</span> is in beta, feedback is welcome!
92+
</div>
93+
<div
94+
v-for="(route, idx) of rawRoutes"
95+
:key="route.path"
96+
:ref="el => blocks.set(idx, el)"
97+
class="relative border-t border-main of-hidden flex gap-4 min-h-50"
98+
>
99+
<div class="select-none text-3xl op25 my4 w-13 text-right">
100+
{{ idx + 1 }}
101+
</div>
102+
<div
103+
class="border rounded border-main overflow-hidden bg-main my5"
104+
:style="themeVars"
105+
@dblclick="openSlideInNewTab(route.path)"
106+
>
107+
<SlideContainer
108+
:key="route.path"
109+
:width="cardWidth"
110+
:clicks-disabled="true"
111+
class="pointer-events-none"
112+
>
113+
<SlideWrapper
114+
:is="route.component"
115+
v-if="route?.component"
116+
:clicks-context="useFixedClicks(route, 99999)[1]"
117+
:class="getSlideClass(route)"
118+
:route="route"
119+
render-context="overview"
120+
/>
121+
<DrawingPreview :page="+route.path" />
122+
</SlideContainer>
123+
</div>
124+
<NoteDisplay
125+
:note="route.meta?.slide?.note"
126+
:note-html="route.meta?.slide?.noteHTML"
127+
/>
128+
</div>
129+
</main>
130+
</div>
131+
</template>

packages/client/pages/presenter.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { useHead } from '@unhead/vue'
33
import { computed, onMounted, reactive, ref, shallowRef, watch } from 'vue'
44
import { useMouse, useWindowFocus } from '@vueuse/core'
5-
import { clicksContext, currentPage, currentRoute, hasNext, nextRoute, queryClicks, rawRoutes, total, useSwipeControls } from '../logic/nav'
5+
import { clicksContext, currentPage, currentRoute, currentSlideId, hasNext, nextRoute, queryClicks, rawRoutes, total, useSwipeControls } from '../logic/nav'
66
import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showOverview, showPresenterCursor } from '../state'
77
import { configs, themeVars } from '../env'
88
import { sharedState } from '../state/shared'
@@ -140,12 +140,16 @@ onMounted(() => {
140140
<div v-else class="grid-section note grid grid-rows-[1fr_min-content] overflow-hidden">
141141
<NoteEditor
142142
v-if="__DEV__"
143+
:key="`edit-${currentSlideId}`"
144+
:no="currentSlideId"
143145
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
144146
:editing="notesEditing"
145147
:style="{ fontSize: `${presenterNotesFontSize}em` }"
146148
/>
147149
<NoteStatic
148150
v-else
151+
:key="`static-${currentSlideId}`"
152+
:no="currentSlideId"
149153
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
150154
:style="{ fontSize: `${presenterNotesFontSize}em` }"
151155
/>

packages/client/pages/presenter/print.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const slidesWithNote = computed(() => rawRoutes
5858
</h2>
5959
<NoteDisplay :note-html="slide!.noteHTML" class="max-w-full" />
6060
</div>
61-
<hr v-if="index < slidesWithNote.length - 1" class="border-gray-400/50 mb-8">
61+
<hr v-if="index < slidesWithNote.length - 1" class="border-main mb-8">
6262
</div>
6363
</div>
6464
</div>

packages/client/routes.ts

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ if (__SLIDEV_FEATURE_PRESENTER__) {
5757
path: '/entry',
5858
component: () => import('./pages/entry.vue'),
5959
})
60+
routes.push({
61+
name: 'overview',
62+
path: '/overview',
63+
component: () => import('./pages/overview.vue'),
64+
})
6065
routes.push({
6166
name: 'notes',
6267
path: '/notes',

0 commit comments

Comments
 (0)