@slidev-react/client 0.2.5
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/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +44 -0
- package/src/addons/AddonProvider.tsx +25 -0
- package/src/addons/g2/G2Chart.tsx +370 -0
- package/src/addons/g2/chartPresets.ts +43 -0
- package/src/addons/g2/chartThemeTokens.ts +124 -0
- package/src/addons/g2/index.ts +36 -0
- package/src/addons/g2/style.css +31 -0
- package/src/addons/insight/Insight.tsx +10 -0
- package/src/addons/insight/InsightAddonProvider.tsx +20 -0
- package/src/addons/insight/SpotlightLayout.tsx +11 -0
- package/src/addons/insight/index.ts +17 -0
- package/src/addons/insight/style.css +34 -0
- package/src/addons/mermaid/MermaidDiagram.tsx +379 -0
- package/src/addons/mermaid/index.ts +10 -0
- package/src/addons/registry.test.ts +28 -0
- package/src/addons/registry.ts +61 -0
- package/src/addons/types.ts +6 -0
- package/src/app/App.tsx +125 -0
- package/src/app/README.md +18 -0
- package/src/app/providers/SlidesNavigationProvider.tsx +82 -0
- package/src/app/usePresentationBootstrap.ts +85 -0
- package/src/features/presentation/PresentationStatus.tsx +514 -0
- package/src/features/presentation/PrintSlidesView.tsx +350 -0
- package/src/features/presentation/browser.ts +5 -0
- package/src/features/presentation/draw/DrawOverlay.tsx +170 -0
- package/src/features/presentation/draw/DrawProvider.tsx +394 -0
- package/src/features/presentation/draw/persistence.test.ts +80 -0
- package/src/features/presentation/draw/persistence.ts +54 -0
- package/src/features/presentation/exportArtifacts.test.ts +48 -0
- package/src/features/presentation/exportArtifacts.ts +6 -0
- package/src/features/presentation/location.test.ts +73 -0
- package/src/features/presentation/location.ts +113 -0
- package/src/features/presentation/navigation/KeyboardController.tsx +73 -0
- package/src/features/presentation/navigation/PresentationNavbar.tsx +162 -0
- package/src/features/presentation/navigation/ShortcutsHelpOverlay.test.tsx +24 -0
- package/src/features/presentation/navigation/ShortcutsHelpOverlay.tsx +111 -0
- package/src/features/presentation/navigation/keyboardShortcuts.test.ts +74 -0
- package/src/features/presentation/navigation/keyboardShortcuts.ts +221 -0
- package/src/features/presentation/navigation/useSlidesNavigation.ts +15 -0
- package/src/features/presentation/overview/NotesOverview.tsx +200 -0
- package/src/features/presentation/overview/QuickOverview.tsx +126 -0
- package/src/features/presentation/path.ts +137 -0
- package/src/features/presentation/presenter/FlowTimelinePreview.test.tsx +54 -0
- package/src/features/presentation/presenter/FlowTimelinePreview.tsx +274 -0
- package/src/features/presentation/presenter/PresenterModeView.tsx +93 -0
- package/src/features/presentation/presenter/PresenterShell.tsx +286 -0
- package/src/features/presentation/presenter/PresenterSidePreview.tsx +68 -0
- package/src/features/presentation/presenter/PresenterTopProgress.tsx +28 -0
- package/src/features/presentation/presenter/SpeakerNotesPanel.tsx +51 -0
- package/src/features/presentation/presenter/StandaloneModeView.tsx +36 -0
- package/src/features/presentation/presenter/persistence.test.ts +26 -0
- package/src/features/presentation/presenter/persistence.ts +31 -0
- package/src/features/presentation/presenter/presentationSyncBridge.test.ts +87 -0
- package/src/features/presentation/presenter/presentationSyncBridge.ts +82 -0
- package/src/features/presentation/presenter/stage.ts +15 -0
- package/src/features/presentation/presenter/types.ts +30 -0
- package/src/features/presentation/presenter/useFullscreen.ts +58 -0
- package/src/features/presentation/presenter/useIdleCursor.ts +37 -0
- package/src/features/presentation/presenter/usePresentationFlowRuntime.ts +238 -0
- package/src/features/presentation/presenter/usePresenterChromeRuntime.ts +358 -0
- package/src/features/presentation/presenter/usePresenterSessionState.ts +226 -0
- package/src/features/presentation/presenter/useWakeLock.ts +110 -0
- package/src/features/presentation/recordingFilename.test.ts +46 -0
- package/src/features/presentation/recordingFilename.ts +56 -0
- package/src/features/presentation/reveal/Reveal.tsx +119 -0
- package/src/features/presentation/reveal/RevealContext.tsx +29 -0
- package/src/features/presentation/reveal/useRevealStep.ts +35 -0
- package/src/features/presentation/session.test.ts +122 -0
- package/src/features/presentation/session.ts +124 -0
- package/src/features/presentation/stage/SlidePreviewSurface.tsx +92 -0
- package/src/features/presentation/stage/SlideStage.tsx +159 -0
- package/src/features/presentation/stage/slideSurface.ts +71 -0
- package/src/features/presentation/stage/slideViewport.tsx +47 -0
- package/src/features/presentation/sync/adapters/broadcastChannelTransport.ts +40 -0
- package/src/features/presentation/sync/adapters/websocketTransport.ts +128 -0
- package/src/features/presentation/sync/model/presence.test.ts +42 -0
- package/src/features/presentation/sync/model/presence.ts +33 -0
- package/src/features/presentation/sync/model/replication.test.ts +72 -0
- package/src/features/presentation/sync/model/replication.ts +113 -0
- package/src/features/presentation/sync/model/status.test.ts +52 -0
- package/src/features/presentation/sync/model/status.ts +33 -0
- package/src/features/presentation/types.ts +1 -0
- package/src/features/presentation/usePresentationRecorder.ts +194 -0
- package/src/features/presentation/usePresentationSync.ts +423 -0
- package/src/index.ts +7 -0
- package/src/main.tsx +12 -0
- package/src/theme/ThemeProvider.test.ts +36 -0
- package/src/theme/ThemeProvider.tsx +79 -0
- package/src/theme/__mocks__/active-theme.ts +3 -0
- package/src/theme/base.css +14 -0
- package/src/theme/components.css +231 -0
- package/src/theme/index.css +11 -0
- package/src/theme/layouts/center.tsx +9 -0
- package/src/theme/layouts/cover.tsx +9 -0
- package/src/theme/layouts/default.tsx +5 -0
- package/src/theme/layouts/defaultLayouts.ts +20 -0
- package/src/theme/layouts/helpers.tsx +12 -0
- package/src/theme/layouts/image-right.tsx +21 -0
- package/src/theme/layouts/immersive.tsx +9 -0
- package/src/theme/layouts/resolveLayout.ts +9 -0
- package/src/theme/layouts/section.tsx +9 -0
- package/src/theme/layouts/statement.tsx +9 -0
- package/src/theme/layouts/two-cols.tsx +21 -0
- package/src/theme/layouts/types.ts +1 -0
- package/src/theme/layouts.css +133 -0
- package/src/theme/mark.css +379 -0
- package/src/theme/print.css +106 -0
- package/src/theme/prose.css +263 -0
- package/src/theme/registry.test.ts +21 -0
- package/src/theme/registry.ts +40 -0
- package/src/theme/tokens.css +148 -0
- package/src/theme/transitions.css +141 -0
- package/src/theme/types.ts +9 -0
- package/src/theme/useResolvedLayout.ts +24 -0
- package/src/types/generated-slides.d.ts +7 -0
- package/src/types/mdx-components.ts +7 -0
- package/src/types/plantuml-encoder.d.ts +7 -0
- package/src/ui/diagrams/PlantUmlDiagram.tsx +33 -0
- package/src/ui/mdx/MagicMoveDemo.tsx +114 -0
- package/src/ui/mdx/index.ts +21 -0
- package/src/ui/primitives/Annotate.test.tsx +64 -0
- package/src/ui/primitives/Annotate.tsx +82 -0
- package/src/ui/primitives/Badge.tsx +5 -0
- package/src/ui/primitives/Callout.tsx +24 -0
- package/src/ui/primitives/ChromeIconButton.tsx +58 -0
- package/src/ui/primitives/ChromePanel.tsx +79 -0
- package/src/ui/primitives/ChromeTag.tsx +70 -0
- package/src/ui/primitives/FormSelect.tsx +51 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { useCallback } from "react"
|
|
2
|
+
import type { CompiledSlide, SlidesConfig } from './types'
|
|
3
|
+
import { DrawProvider } from "../draw/DrawProvider"
|
|
4
|
+
import { KeyboardController } from "../navigation/KeyboardController"
|
|
5
|
+
import { ShortcutsHelpOverlay } from "../navigation/ShortcutsHelpOverlay"
|
|
6
|
+
import { NotesOverview } from "../overview/NotesOverview"
|
|
7
|
+
import { PresentationNavbar } from "../navigation/PresentationNavbar"
|
|
8
|
+
import { useSlidesNavigation } from "../navigation/useSlidesNavigation"
|
|
9
|
+
import { QuickOverview } from "../overview/QuickOverview"
|
|
10
|
+
import { PresentationStatus } from "../PresentationStatus"
|
|
11
|
+
import { buildPrintExportUrl } from "@slidev-react/core/presentation/export/urls"
|
|
12
|
+
import { buildPresentationEntryUrl, type PresentationSession } from "../session"
|
|
13
|
+
import type { PresentationSyncMode } from "../types"
|
|
14
|
+
import { RevealProvider } from "../reveal/RevealContext"
|
|
15
|
+
import { FlowTimelinePreview } from "./FlowTimelinePreview"
|
|
16
|
+
import { PresenterTopProgress } from "./PresenterTopProgress"
|
|
17
|
+
import { usePresentationFlowRuntime } from "./usePresentationFlowRuntime"
|
|
18
|
+
import { usePresenterChromeRuntime } from "./usePresenterChromeRuntime"
|
|
19
|
+
import { usePresenterSessionState } from "./usePresenterSessionState"
|
|
20
|
+
import { useWakeLock } from "./useWakeLock"
|
|
21
|
+
import { useFullscreen } from "./useFullscreen"
|
|
22
|
+
import { PresenterModeView } from "./PresenterModeView"
|
|
23
|
+
import { StandaloneModeView } from "./StandaloneModeView"
|
|
24
|
+
|
|
25
|
+
function canControlNavigation(session: PresentationSession) {
|
|
26
|
+
return !session.enabled || session.role === "presenter"
|
|
27
|
+
}
|
|
28
|
+
const PRESENTER_BOTTOM_BAR_CLEARANCE = 72
|
|
29
|
+
|
|
30
|
+
export function PresenterShell({
|
|
31
|
+
slides,
|
|
32
|
+
slidesTitle,
|
|
33
|
+
slidesConfig,
|
|
34
|
+
slidesExportFilename,
|
|
35
|
+
slidesSessionSeed,
|
|
36
|
+
drawStorageKey,
|
|
37
|
+
session,
|
|
38
|
+
onSyncModeChange,
|
|
39
|
+
}: {
|
|
40
|
+
slides: CompiledSlide[]
|
|
41
|
+
slidesTitle?: string
|
|
42
|
+
slidesConfig: SlidesConfig
|
|
43
|
+
slidesExportFilename?: string
|
|
44
|
+
slidesSessionSeed: string
|
|
45
|
+
drawStorageKey: string
|
|
46
|
+
session: PresentationSession
|
|
47
|
+
onSyncModeChange: (mode: PresentationSyncMode) => void
|
|
48
|
+
}) {
|
|
49
|
+
const navigation = useSlidesNavigation()
|
|
50
|
+
const currentSlide = slides[navigation.currentIndex]
|
|
51
|
+
const nextSlide = slides[navigation.currentIndex + 1] ?? null
|
|
52
|
+
const canControl = canControlNavigation(session)
|
|
53
|
+
const isPresenterRole = session.role === "presenter"
|
|
54
|
+
const canOpenOverview = canControl || session.role === "viewer"
|
|
55
|
+
|
|
56
|
+
const flow = usePresentationFlowRuntime({ slides, navigation })
|
|
57
|
+
const chrome = usePresenterChromeRuntime({ canControl, canOpenOverview, isPresenterRole })
|
|
58
|
+
const wakeLock = useWakeLock()
|
|
59
|
+
const fullscreen = useFullscreen()
|
|
60
|
+
|
|
61
|
+
const sessionState = usePresenterSessionState({
|
|
62
|
+
slides,
|
|
63
|
+
session,
|
|
64
|
+
navigation,
|
|
65
|
+
flow,
|
|
66
|
+
canControl,
|
|
67
|
+
slidesExportFilename,
|
|
68
|
+
slidesTitle,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const handleViewerAdvance = useCallback(() => {
|
|
72
|
+
sessionState.detachFromPresenter()
|
|
73
|
+
flow.advanceReveal()
|
|
74
|
+
}, [sessionState.detachFromPresenter, flow.advanceReveal])
|
|
75
|
+
|
|
76
|
+
const handleViewerRetreat = useCallback(() => {
|
|
77
|
+
sessionState.detachFromPresenter()
|
|
78
|
+
flow.retreatReveal()
|
|
79
|
+
}, [sessionState.detachFromPresenter, flow.retreatReveal])
|
|
80
|
+
|
|
81
|
+
const handleViewerFirst = useCallback(() => {
|
|
82
|
+
sessionState.detachFromPresenter()
|
|
83
|
+
flow.goToSlideAtStart(0)
|
|
84
|
+
}, [sessionState.detachFromPresenter, flow.goToSlideAtStart])
|
|
85
|
+
|
|
86
|
+
const handleViewerLast = useCallback(() => {
|
|
87
|
+
sessionState.detachFromPresenter()
|
|
88
|
+
flow.goToSlideAtStart(Math.max(navigation.total - 1, 0))
|
|
89
|
+
}, [sessionState.detachFromPresenter, flow.goToSlideAtStart, navigation.total])
|
|
90
|
+
|
|
91
|
+
const handleEnterPresenterMode = useCallback(() => {
|
|
92
|
+
const entryUrl = buildPresentationEntryUrl("presenter", slidesSessionSeed)
|
|
93
|
+
if (!entryUrl) return
|
|
94
|
+
|
|
95
|
+
window.location.assign(entryUrl)
|
|
96
|
+
}, [slidesSessionSeed])
|
|
97
|
+
|
|
98
|
+
const handleOpenPrintExport = useCallback(() => {
|
|
99
|
+
const exportUrl = buildPrintExportUrl(window.location.href)
|
|
100
|
+
const exportWindow = window.open(exportUrl, "_blank")
|
|
101
|
+
if (exportWindow) {
|
|
102
|
+
exportWindow.opener = null
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
window.location.assign(exportUrl)
|
|
107
|
+
}, [])
|
|
108
|
+
|
|
109
|
+
const handleOpenMirrorStage = useCallback(() => {
|
|
110
|
+
const targetUrl = session.viewerUrl
|
|
111
|
+
if (!targetUrl) return
|
|
112
|
+
|
|
113
|
+
const mirrorWindow = window.open(targetUrl, "_blank", "noopener,noreferrer")
|
|
114
|
+
if (mirrorWindow) {
|
|
115
|
+
mirrorWindow.opener = null
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
window.location.assign(targetUrl)
|
|
120
|
+
}, [session.viewerUrl])
|
|
121
|
+
|
|
122
|
+
const progressPercent =
|
|
123
|
+
navigation.total > 0 ? ((navigation.currentIndex + 1) / navigation.total) * 100 : 0
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
<RevealProvider value={flow.revealContextValue}>
|
|
128
|
+
<KeyboardController
|
|
129
|
+
enabled={canControl || session.role === "viewer"}
|
|
130
|
+
overlayOpen={Boolean(chrome.activeOverlay)}
|
|
131
|
+
onAdvance={!canControl ? handleViewerAdvance : undefined}
|
|
132
|
+
onRetreat={!canControl ? handleViewerRetreat : undefined}
|
|
133
|
+
onFirst={!canControl ? handleViewerFirst : undefined}
|
|
134
|
+
onLast={!canControl ? handleViewerLast : undefined}
|
|
135
|
+
/>
|
|
136
|
+
</RevealProvider>
|
|
137
|
+
<DrawProvider
|
|
138
|
+
currentSlideId={currentSlide.id}
|
|
139
|
+
storageKey={drawStorageKey}
|
|
140
|
+
readOnly={!canControl}
|
|
141
|
+
overlayOpen={Boolean(chrome.activeOverlay)}
|
|
142
|
+
remoteStrokes={canControl ? null : sessionState.remoteDrawings}
|
|
143
|
+
onStrokesChange={sessionState.onStrokesChange}
|
|
144
|
+
>
|
|
145
|
+
<div
|
|
146
|
+
className={`relative grid h-dvh max-h-dvh grid-cols-1 grid-rows-[minmax(0,1fr)] overflow-hidden ${
|
|
147
|
+
isPresenterRole ? "bg-slate-50" : "bg-black"
|
|
148
|
+
} ${chrome.hideCursor ? "cursor-none" : ""}`}
|
|
149
|
+
>
|
|
150
|
+
{isPresenterRole && (
|
|
151
|
+
<>
|
|
152
|
+
<div className="pointer-events-none absolute inset-0 bg-slate-50" />
|
|
153
|
+
<PresenterTopProgress total={navigation.total} progressPercent={progressPercent} />
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
{isPresenterRole && (
|
|
157
|
+
<PresentationStatus
|
|
158
|
+
slideId={currentSlide.id}
|
|
159
|
+
session={session}
|
|
160
|
+
sync={sessionState.sync}
|
|
161
|
+
recorder={sessionState.recorder}
|
|
162
|
+
wakeLock={wakeLock}
|
|
163
|
+
fullscreen={fullscreen}
|
|
164
|
+
chrome={{
|
|
165
|
+
stageScale: chrome.stageScale,
|
|
166
|
+
cursorMode: chrome.cursorMode,
|
|
167
|
+
timelinePreviewOpen: chrome.timelinePreviewOpen,
|
|
168
|
+
overviewOpen: chrome.overviewOpen,
|
|
169
|
+
notesOpen: chrome.notesOverviewOpen,
|
|
170
|
+
shortcutsOpen: chrome.shortcutsHelpOpen,
|
|
171
|
+
canOpenOverview,
|
|
172
|
+
onToggleTimelinePreview: chrome.toggleTimelinePreview,
|
|
173
|
+
onToggleOverview: chrome.toggleOverview,
|
|
174
|
+
onToggleNotes: chrome.toggleNotes,
|
|
175
|
+
onToggleShortcuts: chrome.toggleShortcuts,
|
|
176
|
+
onStageScaleChange: chrome.handleStageScaleChange,
|
|
177
|
+
onCursorModeChange: chrome.handleCursorModeChange,
|
|
178
|
+
}}
|
|
179
|
+
sessionTimerSeconds={canControl ? sessionState.localTimer : sessionState.remoteTimer}
|
|
180
|
+
canRecord={canControl}
|
|
181
|
+
onOpenMirrorStage={handleOpenMirrorStage}
|
|
182
|
+
onOpenPrintExport={handleOpenPrintExport}
|
|
183
|
+
onSyncModeChange={onSyncModeChange}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
<div
|
|
187
|
+
style={
|
|
188
|
+
isPresenterRole ? { paddingBottom: `${PRESENTER_BOTTOM_BAR_CLEARANCE}px` } : undefined
|
|
189
|
+
}
|
|
190
|
+
className={`relative min-h-0 min-w-0 size-full ${isPresenterRole ? "px-0 pb-0 pt-0 lg:px-0" : ""}`}
|
|
191
|
+
>
|
|
192
|
+
{isPresenterRole ? (
|
|
193
|
+
<PresenterModeView
|
|
194
|
+
currentSlide={currentSlide}
|
|
195
|
+
nextSlide={nextSlide}
|
|
196
|
+
slidesConfig={slidesConfig}
|
|
197
|
+
canControl={canControl}
|
|
198
|
+
remoteCursor={sessionState.remoteCursor}
|
|
199
|
+
localCursor={sessionState.localCursor}
|
|
200
|
+
setLocalCursor={sessionState.setLocalCursor}
|
|
201
|
+
flow={flow}
|
|
202
|
+
chrome={chrome}
|
|
203
|
+
navigation={navigation}
|
|
204
|
+
/>
|
|
205
|
+
) : (
|
|
206
|
+
<StandaloneModeView
|
|
207
|
+
currentSlide={currentSlide}
|
|
208
|
+
slidesConfig={slidesConfig}
|
|
209
|
+
canControl={canControl}
|
|
210
|
+
remoteCursor={sessionState.remoteCursor}
|
|
211
|
+
setLocalCursor={sessionState.setLocalCursor}
|
|
212
|
+
flow={flow}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
{isPresenterRole && chrome.timelinePreviewOpen && (
|
|
217
|
+
<div
|
|
218
|
+
className="absolute inset-x-4 z-30 flex justify-center"
|
|
219
|
+
style={{ bottom: `${PRESENTER_BOTTOM_BAR_CLEARANCE + 16}px` }}
|
|
220
|
+
>
|
|
221
|
+
<FlowTimelinePreview
|
|
222
|
+
slide={currentSlide}
|
|
223
|
+
currentClicks={flow.currentClicks}
|
|
224
|
+
currentClicksTotal={flow.currentClicksTotal}
|
|
225
|
+
slidesConfig={slidesConfig}
|
|
226
|
+
onJumpToCue={(cueIndex) => flow.setSlideClicks(currentSlide.id, cueIndex)}
|
|
227
|
+
onClose={chrome.closeOverlay}
|
|
228
|
+
className="w-full max-w-[min(920px,calc(100vw-2rem))] max-h-[min(60vh,700px)]"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
{!isPresenterRole && (
|
|
233
|
+
<PresentationNavbar
|
|
234
|
+
slideTitle={currentSlide.meta.title}
|
|
235
|
+
currentIndex={navigation.currentIndex}
|
|
236
|
+
total={navigation.total}
|
|
237
|
+
canPrev={flow.canPrev}
|
|
238
|
+
canNext={flow.canNext}
|
|
239
|
+
showPresenterModeButton={session.role !== "presenter"}
|
|
240
|
+
overviewOpen={chrome.overviewOpen}
|
|
241
|
+
notesOpen={chrome.notesOverviewOpen}
|
|
242
|
+
shortcutsOpen={chrome.shortcutsHelpOpen}
|
|
243
|
+
canOpenOverview={canOpenOverview}
|
|
244
|
+
onEnterPresenterMode={
|
|
245
|
+
session.role !== "presenter" ? handleEnterPresenterMode : undefined
|
|
246
|
+
}
|
|
247
|
+
onToggleOverview={chrome.toggleOverview}
|
|
248
|
+
onToggleNotes={chrome.toggleNotes}
|
|
249
|
+
onToggleShortcuts={chrome.toggleShortcuts}
|
|
250
|
+
onPrev={flow.retreatReveal}
|
|
251
|
+
onNext={flow.advanceReveal}
|
|
252
|
+
canControl={canControl}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
<QuickOverview
|
|
256
|
+
open={chrome.overviewOpen && canOpenOverview}
|
|
257
|
+
slides={slides}
|
|
258
|
+
currentIndex={navigation.currentIndex}
|
|
259
|
+
slidesConfig={slidesConfig}
|
|
260
|
+
onClose={chrome.closeOverlay}
|
|
261
|
+
onSelect={(index) => {
|
|
262
|
+
if (!canControl) sessionState.detachFromPresenter()
|
|
263
|
+
flow.goToSlideAtStart(index)
|
|
264
|
+
chrome.closeOverlay()
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
<NotesOverview
|
|
268
|
+
open={chrome.notesOverviewOpen && canControl}
|
|
269
|
+
slides={slides}
|
|
270
|
+
currentIndex={navigation.currentIndex}
|
|
271
|
+
onClose={chrome.closeOverlay}
|
|
272
|
+
onSelect={(index) => {
|
|
273
|
+
flow.goToSlideAtStart(index)
|
|
274
|
+
chrome.closeOverlay()
|
|
275
|
+
}}
|
|
276
|
+
/>
|
|
277
|
+
<ShortcutsHelpOverlay
|
|
278
|
+
open={chrome.shortcutsHelpOpen}
|
|
279
|
+
sections={chrome.shortcutHelpSections}
|
|
280
|
+
onClose={chrome.closeOverlay}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
</DrawProvider>
|
|
284
|
+
</>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { CompiledSlide, SlidesConfig } from "./types";
|
|
2
|
+
import { ChromePanel, chromePanelClassName } from "../../../ui/primitives/ChromePanel";
|
|
3
|
+
import { ChromeTag } from "../../../ui/primitives/ChromeTag";
|
|
4
|
+
import { SlidePreviewSurface } from "../stage/SlidePreviewSurface";
|
|
5
|
+
|
|
6
|
+
export function PresenterSidePreview({
|
|
7
|
+
title,
|
|
8
|
+
indexLabel,
|
|
9
|
+
slide,
|
|
10
|
+
slidesConfig,
|
|
11
|
+
}: {
|
|
12
|
+
title: string;
|
|
13
|
+
indexLabel: string;
|
|
14
|
+
slide: CompiledSlide | null;
|
|
15
|
+
slidesConfig: Pick<SlidesConfig, "slidesViewport" | "slidesLayout" | "slidesBackground">;
|
|
16
|
+
}) {
|
|
17
|
+
if (!slide) {
|
|
18
|
+
return (
|
|
19
|
+
<ChromePanel className="flex h-full min-h-0 min-w-0 flex-col gap-3">
|
|
20
|
+
<div className="flex items-center justify-between gap-2">
|
|
21
|
+
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700">
|
|
22
|
+
{title}
|
|
23
|
+
</p>
|
|
24
|
+
<ChromeTag tone="active" weight="semibold">
|
|
25
|
+
{indexLabel}
|
|
26
|
+
</ChromeTag>
|
|
27
|
+
</div>
|
|
28
|
+
<ChromePanel
|
|
29
|
+
as="div"
|
|
30
|
+
tone="dashed"
|
|
31
|
+
radius="frame"
|
|
32
|
+
className="grid min-h-[180px] min-w-0 w-full flex-1 place-items-center overflow-hidden text-sm"
|
|
33
|
+
>
|
|
34
|
+
End of slides
|
|
35
|
+
</ChromePanel>
|
|
36
|
+
</ChromePanel>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ChromePanel className="flex h-full min-h-0 min-w-0 flex-col gap-3">
|
|
42
|
+
<div className="flex items-center justify-between gap-2">
|
|
43
|
+
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700">
|
|
44
|
+
{title}
|
|
45
|
+
</p>
|
|
46
|
+
<ChromeTag tone="active" weight="semibold">
|
|
47
|
+
{indexLabel}
|
|
48
|
+
</ChromeTag>
|
|
49
|
+
</div>
|
|
50
|
+
<SlidePreviewSurface
|
|
51
|
+
Slide={slide.component}
|
|
52
|
+
meta={slide.meta}
|
|
53
|
+
slidesConfig={slidesConfig}
|
|
54
|
+
alignment="top-left"
|
|
55
|
+
viewportClassName={chromePanelClassName({
|
|
56
|
+
tone: "frame",
|
|
57
|
+
radius: "frame",
|
|
58
|
+
padding: "none",
|
|
59
|
+
className: "relative min-h-[180px] min-w-0 w-full flex-1 overflow-hidden",
|
|
60
|
+
})}
|
|
61
|
+
stageClassName="text-black"
|
|
62
|
+
/>
|
|
63
|
+
<p className="shrink-0 truncate text-base font-semibold text-slate-900">
|
|
64
|
+
{slide.meta.title ?? "Untitled"}
|
|
65
|
+
</p>
|
|
66
|
+
</ChromePanel>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function PresenterTopProgress({
|
|
2
|
+
total,
|
|
3
|
+
progressPercent,
|
|
4
|
+
}: {
|
|
5
|
+
total: number;
|
|
6
|
+
progressPercent: number;
|
|
7
|
+
}) {
|
|
8
|
+
const segmentCount = Math.max(total, 1);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="pointer-events-none absolute inset-x-0 top-0 z-30">
|
|
12
|
+
<div className="relative h-[4px] w-full overflow-hidden bg-white/30">
|
|
13
|
+
<div
|
|
14
|
+
className="absolute inset-y-0 left-0 bg-[linear-gradient(90deg,#86efac_0%,#22c55e_46%,#15803d_100%)] transition-[width] duration-300"
|
|
15
|
+
style={{ width: `${progressPercent}%` }}
|
|
16
|
+
/>
|
|
17
|
+
<div
|
|
18
|
+
className="absolute inset-0 grid gap-px bg-white/12"
|
|
19
|
+
style={{ gridTemplateColumns: `repeat(${segmentCount}, minmax(0, 1fr))` }}
|
|
20
|
+
>
|
|
21
|
+
{Array.from({ length: segmentCount }, (_, index) => (
|
|
22
|
+
<span key={index} aria-hidden className="bg-transparent" />
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ChromePanel } from "../../../ui/primitives/ChromePanel";
|
|
2
|
+
import { ChromeTag } from "../../../ui/primitives/ChromeTag";
|
|
3
|
+
|
|
4
|
+
function renderNotes(notes: string) {
|
|
5
|
+
const sections = notes
|
|
6
|
+
.split(/\n\s*\n/g)
|
|
7
|
+
.map((section) => section.trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
|
|
10
|
+
return sections.map((section, index) => (
|
|
11
|
+
<p key={`${index}-${section.slice(0, 24)}`} className="whitespace-pre-wrap">
|
|
12
|
+
{section}
|
|
13
|
+
</p>
|
|
14
|
+
));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SpeakerNotesPanel({
|
|
18
|
+
currentClicks,
|
|
19
|
+
currentClicksTotal,
|
|
20
|
+
notes,
|
|
21
|
+
}: {
|
|
22
|
+
currentClicks: number;
|
|
23
|
+
currentClicksTotal: number;
|
|
24
|
+
notes?: string;
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<ChromePanel className="flex flex-col">
|
|
28
|
+
<div className="mb-3 flex items-center justify-between gap-3">
|
|
29
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-500">
|
|
30
|
+
Notes
|
|
31
|
+
</p>
|
|
32
|
+
<ChromeTag>
|
|
33
|
+
{currentClicksTotal > 0 ? `Clicks ${currentClicks}/${currentClicksTotal}` : "Slide cue"}
|
|
34
|
+
</ChromeTag>
|
|
35
|
+
</div>
|
|
36
|
+
<ChromePanel tone="inset" radius="frame" className="flex-1 p-4 text-sm leading-7">
|
|
37
|
+
{notes ? (
|
|
38
|
+
<div className="space-y-4 text-slate-600">{renderNotes(notes)}</div>
|
|
39
|
+
) : (
|
|
40
|
+
<>
|
|
41
|
+
<p className="font-medium text-slate-900">No notes yet.</p>
|
|
42
|
+
<p className="mt-3 text-slate-500">
|
|
43
|
+
Add slide-level frontmatter with <code>notes: |</code> to keep your phrasing,
|
|
44
|
+
punchlines, and handoff lines close to the slide.
|
|
45
|
+
</p>
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
</ChromePanel>
|
|
49
|
+
</ChromePanel>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PresentationCursorState } from "../types"
|
|
2
|
+
import { RevealProvider } from "../reveal/RevealContext"
|
|
3
|
+
import { SlideStage } from "../stage/SlideStage"
|
|
4
|
+
import type { CompiledSlide, SlidesConfig } from "./types"
|
|
5
|
+
import type { PresentationFlowRuntime } from "./usePresentationFlowRuntime"
|
|
6
|
+
|
|
7
|
+
export function StandaloneModeView({
|
|
8
|
+
currentSlide,
|
|
9
|
+
slidesConfig,
|
|
10
|
+
canControl,
|
|
11
|
+
remoteCursor,
|
|
12
|
+
setLocalCursor,
|
|
13
|
+
flow,
|
|
14
|
+
}: {
|
|
15
|
+
currentSlide: CompiledSlide
|
|
16
|
+
slidesConfig: SlidesConfig
|
|
17
|
+
canControl: boolean
|
|
18
|
+
remoteCursor: PresentationCursorState | null
|
|
19
|
+
setLocalCursor: (cursor: PresentationCursorState | null) => void
|
|
20
|
+
flow: PresentationFlowRuntime
|
|
21
|
+
}) {
|
|
22
|
+
const CurrentSlide = currentSlide.component
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<RevealProvider value={flow.revealContextValue}>
|
|
26
|
+
<SlideStage
|
|
27
|
+
Slide={CurrentSlide}
|
|
28
|
+
slideId={currentSlide.id}
|
|
29
|
+
meta={currentSlide.meta}
|
|
30
|
+
slidesConfig={slidesConfig}
|
|
31
|
+
remoteCursor={canControl ? null : remoteCursor}
|
|
32
|
+
onCursorChange={canControl ? setLocalCursor : undefined}
|
|
33
|
+
/>
|
|
34
|
+
</RevealProvider>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parsePersistedPresenterCursorMode,
|
|
4
|
+
parsePersistedPresenterSidebarWidth,
|
|
5
|
+
parsePersistedPresenterStageScale,
|
|
6
|
+
} from "./persistence";
|
|
7
|
+
|
|
8
|
+
describe("presenter persistence", () => {
|
|
9
|
+
it("parses allowed stage scale values", () => {
|
|
10
|
+
expect(parsePersistedPresenterStageScale("1.08")).toBe(1.08);
|
|
11
|
+
expect(parsePersistedPresenterStageScale("1")).toBe(1);
|
|
12
|
+
expect(parsePersistedPresenterStageScale("0.95")).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("parses cursor mode values", () => {
|
|
16
|
+
expect(parsePersistedPresenterCursorMode("always")).toBe("always");
|
|
17
|
+
expect(parsePersistedPresenterCursorMode("idle-hide")).toBe("idle-hide");
|
|
18
|
+
expect(parsePersistedPresenterCursorMode("hidden")).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("parses finite sidebar widths only", () => {
|
|
22
|
+
expect(parsePersistedPresenterSidebarWidth("320")).toBe(320);
|
|
23
|
+
expect(parsePersistedPresenterSidebarWidth("Infinity")).toBeNull();
|
|
24
|
+
expect(parsePersistedPresenterSidebarWidth("abc")).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { PresenterCursorMode } from "./usePresenterChromeRuntime";
|
|
3
|
+
|
|
4
|
+
export const PRESENTER_STAGE_SCALE_STORAGE_KEY = "slide-react:presenter-stage-scale";
|
|
5
|
+
export const PRESENTER_CURSOR_MODE_STORAGE_KEY = "slide-react:presenter-cursor-mode";
|
|
6
|
+
export const PRESENTER_SIDEBAR_WIDTH_STORAGE_KEY = "slide-react:presenter-sidebar-width";
|
|
7
|
+
|
|
8
|
+
const presenterStageScaleSchema = z.union([z.literal(0.9), z.literal(1), z.literal(1.08)]);
|
|
9
|
+
const presenterCursorModeSchema = z.enum(["always", "idle-hide"]);
|
|
10
|
+
const presenterSidebarWidthSchema = z.number().finite();
|
|
11
|
+
|
|
12
|
+
export function parsePersistedPresenterStageScale(raw: string | null) {
|
|
13
|
+
if (!raw) return null;
|
|
14
|
+
|
|
15
|
+
const value = Number(raw);
|
|
16
|
+
const result = presenterStageScaleSchema.safeParse(value);
|
|
17
|
+
return result.success ? result.data : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parsePersistedPresenterCursorMode(raw: string | null): PresenterCursorMode | null {
|
|
21
|
+
const result = presenterCursorModeSchema.safeParse(raw);
|
|
22
|
+
return result.success ? result.data : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parsePersistedPresenterSidebarWidth(raw: string | null) {
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
|
|
28
|
+
const value = Number(raw);
|
|
29
|
+
const result = presenterSidebarWidthSchema.safeParse(value);
|
|
30
|
+
return result.success ? result.data : null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { buildPresentationSharedState, mapRemotePresentationPatch } from "./presentationSyncBridge";
|
|
3
|
+
|
|
4
|
+
describe("presentationSyncBridge", () => {
|
|
5
|
+
it("builds a shared state payload without mutating timestamps", () => {
|
|
6
|
+
expect(
|
|
7
|
+
buildPresentationSharedState({
|
|
8
|
+
page: 2,
|
|
9
|
+
cue: 1,
|
|
10
|
+
cueTotal: 3,
|
|
11
|
+
timer: 9,
|
|
12
|
+
cursor: { x: 10, y: 20 },
|
|
13
|
+
drawings: {
|
|
14
|
+
"slide-1": [],
|
|
15
|
+
},
|
|
16
|
+
drawingsRevision: 4,
|
|
17
|
+
}),
|
|
18
|
+
).toEqual({
|
|
19
|
+
page: 2,
|
|
20
|
+
cue: 1,
|
|
21
|
+
cueTotal: 3,
|
|
22
|
+
timer: 9,
|
|
23
|
+
cursor: { x: 10, y: 20 },
|
|
24
|
+
drawings: {
|
|
25
|
+
"slide-1": [],
|
|
26
|
+
},
|
|
27
|
+
drawingsRevision: 4,
|
|
28
|
+
lastUpdate: 0,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("maps remote patches back to presenter-side effects", () => {
|
|
33
|
+
vi.useFakeTimers();
|
|
34
|
+
vi.setSystemTime(new Date("2026-03-08T10:00:00.000Z"));
|
|
35
|
+
|
|
36
|
+
expect(
|
|
37
|
+
mapRemotePresentationPatch({
|
|
38
|
+
patch: {
|
|
39
|
+
timer: 12,
|
|
40
|
+
cursor: { x: 4, y: 8 },
|
|
41
|
+
cue: 2,
|
|
42
|
+
cueTotal: 5,
|
|
43
|
+
drawings: {
|
|
44
|
+
"slide-2": [],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
remotePage: 1,
|
|
48
|
+
currentPage: 1,
|
|
49
|
+
resolveSlideId: (index) => `slide-${index + 1}`,
|
|
50
|
+
}),
|
|
51
|
+
).toEqual({
|
|
52
|
+
remoteTimer: 12,
|
|
53
|
+
remoteCursor: { x: 4, y: 8 },
|
|
54
|
+
slideClicks: {
|
|
55
|
+
slideId: "slide-2",
|
|
56
|
+
clicks: 2,
|
|
57
|
+
},
|
|
58
|
+
slideClicksTotal: {
|
|
59
|
+
slideId: "slide-2",
|
|
60
|
+
clicksTotal: 5,
|
|
61
|
+
},
|
|
62
|
+
remoteDrawings: {
|
|
63
|
+
revision: new Date("2026-03-08T10:00:00.000Z").getTime(),
|
|
64
|
+
strokesBySlideId: {
|
|
65
|
+
"slide-2": [],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
vi.useRealTimers();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("clears remote cursors when the remote page differs from the current page", () => {
|
|
74
|
+
expect(
|
|
75
|
+
mapRemotePresentationPatch({
|
|
76
|
+
patch: {
|
|
77
|
+
cursor: { x: 1, y: 1 },
|
|
78
|
+
},
|
|
79
|
+
remotePage: 2,
|
|
80
|
+
currentPage: 1,
|
|
81
|
+
resolveSlideId: () => null,
|
|
82
|
+
}),
|
|
83
|
+
).toEqual({
|
|
84
|
+
remoteCursor: null,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|