@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,514 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Circle,
|
|
3
|
+
CircleDot,
|
|
4
|
+
Copy,
|
|
5
|
+
Eraser,
|
|
6
|
+
Expand,
|
|
7
|
+
Keyboard,
|
|
8
|
+
LayoutGrid,
|
|
9
|
+
List,
|
|
10
|
+
Link2,
|
|
11
|
+
NotebookText,
|
|
12
|
+
PenLine,
|
|
13
|
+
Printer,
|
|
14
|
+
Radio,
|
|
15
|
+
RectangleHorizontal,
|
|
16
|
+
RotateCcw,
|
|
17
|
+
Square,
|
|
18
|
+
SunMedium,
|
|
19
|
+
Trash2,
|
|
20
|
+
Wifi,
|
|
21
|
+
WifiOff,
|
|
22
|
+
} from "lucide-react";
|
|
23
|
+
import { useMemo, useState } from "react";
|
|
24
|
+
import { useDraw } from "./draw/DrawProvider";
|
|
25
|
+
import type { PresentationSession } from "./session";
|
|
26
|
+
import type { PresentationSyncMode } from "./types";
|
|
27
|
+
import type { PresentationSyncStatus, UsePresentationSyncResult } from "./usePresentationSync";
|
|
28
|
+
import type { PresentationRecorderRuntime } from "./usePresentationRecorder";
|
|
29
|
+
import type { WakeLockRuntime } from "./presenter/useWakeLock";
|
|
30
|
+
import type { FullscreenRuntime } from "./presenter/useFullscreen";
|
|
31
|
+
import { ChromeIconButton } from "../../ui/primitives/ChromeIconButton";
|
|
32
|
+
import { ChromeTag } from "../../ui/primitives/ChromeTag";
|
|
33
|
+
import { FormSelect } from "../../ui/primitives/FormSelect";
|
|
34
|
+
|
|
35
|
+
const DRAW_COLORS = ["#ef4444", "#3b82f6", "#22c55e", "#f59e0b", "#111827"];
|
|
36
|
+
const DRAW_WIDTHS = [3, 5, 8];
|
|
37
|
+
|
|
38
|
+
interface PresenterChromeProps {
|
|
39
|
+
stageScale: number;
|
|
40
|
+
cursorMode: "always" | "idle-hide";
|
|
41
|
+
timelinePreviewOpen: boolean;
|
|
42
|
+
overviewOpen: boolean;
|
|
43
|
+
notesOpen: boolean;
|
|
44
|
+
shortcutsOpen: boolean;
|
|
45
|
+
canOpenOverview: boolean;
|
|
46
|
+
onToggleTimelinePreview: () => void;
|
|
47
|
+
onToggleOverview: () => void;
|
|
48
|
+
onToggleNotes: () => void;
|
|
49
|
+
onToggleShortcuts: () => void;
|
|
50
|
+
onStageScaleChange: (value: number) => void;
|
|
51
|
+
onCursorModeChange: (value: "always" | "idle-hide") => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PresentationStatusProps {
|
|
55
|
+
slideId: string;
|
|
56
|
+
session: PresentationSession;
|
|
57
|
+
sync: UsePresentationSyncResult;
|
|
58
|
+
recorder: PresentationRecorderRuntime;
|
|
59
|
+
wakeLock: WakeLockRuntime;
|
|
60
|
+
fullscreen: FullscreenRuntime;
|
|
61
|
+
chrome: PresenterChromeProps;
|
|
62
|
+
sessionTimerSeconds: number;
|
|
63
|
+
canRecord: boolean;
|
|
64
|
+
onOpenMirrorStage?: () => void;
|
|
65
|
+
onOpenPrintExport?: () => void;
|
|
66
|
+
onSyncModeChange?: (mode: PresentationSyncMode) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function badgeClassName(status: PresentationSyncStatus) {
|
|
70
|
+
switch (status) {
|
|
71
|
+
case "connected":
|
|
72
|
+
return "border-emerald-200 bg-emerald-50 text-emerald-700";
|
|
73
|
+
case "degraded":
|
|
74
|
+
return "border-amber-200 bg-amber-50 text-amber-700";
|
|
75
|
+
case "connecting":
|
|
76
|
+
return "border-green-200 bg-green-50 text-green-700";
|
|
77
|
+
default:
|
|
78
|
+
return "border-slate-200 bg-slate-50 text-slate-700";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function statusDotClassName(status: PresentationSyncStatus) {
|
|
83
|
+
switch (status) {
|
|
84
|
+
case "connected":
|
|
85
|
+
return "bg-emerald-400";
|
|
86
|
+
case "degraded":
|
|
87
|
+
return "bg-amber-400";
|
|
88
|
+
case "connecting":
|
|
89
|
+
return "bg-green-400";
|
|
90
|
+
default:
|
|
91
|
+
return "bg-slate-400";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatTimer(seconds: number) {
|
|
96
|
+
const minutes = String(Math.floor(seconds / 60)).padStart(2, "0");
|
|
97
|
+
const restSeconds = String(seconds % 60).padStart(2, "0");
|
|
98
|
+
return `${minutes}:${restSeconds}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function PresentationStatus({
|
|
102
|
+
slideId,
|
|
103
|
+
session,
|
|
104
|
+
sync,
|
|
105
|
+
recorder,
|
|
106
|
+
wakeLock,
|
|
107
|
+
fullscreen,
|
|
108
|
+
chrome,
|
|
109
|
+
sessionTimerSeconds,
|
|
110
|
+
canRecord,
|
|
111
|
+
onOpenMirrorStage,
|
|
112
|
+
onOpenPrintExport,
|
|
113
|
+
onSyncModeChange,
|
|
114
|
+
}: PresentationStatusProps) {
|
|
115
|
+
const draw = useDraw();
|
|
116
|
+
const [copiedTarget, setCopiedTarget] = useState<"viewer" | null>(null);
|
|
117
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
118
|
+
const strokeCount = draw.strokesBySlideId[slideId]?.length ?? 0;
|
|
119
|
+
const hasStrokes = strokeCount > 0;
|
|
120
|
+
|
|
121
|
+
const statusLabel = useMemo(() => {
|
|
122
|
+
switch (sync.status) {
|
|
123
|
+
case "connected":
|
|
124
|
+
return "Connected";
|
|
125
|
+
case "connecting":
|
|
126
|
+
return "Connecting";
|
|
127
|
+
case "degraded":
|
|
128
|
+
return "Degraded";
|
|
129
|
+
default:
|
|
130
|
+
return "Disabled";
|
|
131
|
+
}
|
|
132
|
+
}, [sync.status]);
|
|
133
|
+
|
|
134
|
+
if (!session.enabled || !session.sessionId) return null;
|
|
135
|
+
|
|
136
|
+
const canCopyViewerLink = session.role === "presenter" && !!session.viewerUrl;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<aside className="pointer-events-none absolute inset-x-0 bottom-0 z-40">
|
|
140
|
+
<div className="relative">
|
|
141
|
+
{detailsOpen && (
|
|
142
|
+
<div className="pointer-events-auto absolute inset-x-0 bottom-full mb-2 border-t border-slate-200/80 bg-slate-50/72 px-3 py-3 text-slate-800 ring-1 ring-white/45 backdrop-blur-xl">
|
|
143
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
144
|
+
<ChromeTag size="md" weight="semibold" className="uppercase tracking-[0.18em]">
|
|
145
|
+
<span className={`size-2.5 rounded-full ${statusDotClassName(sync.status)}`} />
|
|
146
|
+
Live
|
|
147
|
+
<span
|
|
148
|
+
className={`rounded-[4px] border px-2 py-0.5 text-[10px] font-medium normal-case tracking-normal ${badgeClassName(sync.status)}`}
|
|
149
|
+
>
|
|
150
|
+
{statusLabel}
|
|
151
|
+
</span>
|
|
152
|
+
</ChromeTag>
|
|
153
|
+
<FormSelect
|
|
154
|
+
label="sync"
|
|
155
|
+
size="sm"
|
|
156
|
+
value={session.syncMode}
|
|
157
|
+
onChange={(event) => {
|
|
158
|
+
onSyncModeChange?.(event.target.value as PresentationSyncMode);
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<option value="send">send</option>
|
|
162
|
+
<option value="receive">receive</option>
|
|
163
|
+
<option value="both">both</option>
|
|
164
|
+
<option value="off">off</option>
|
|
165
|
+
</FormSelect>
|
|
166
|
+
{canCopyViewerLink && (
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={async () => {
|
|
170
|
+
if (!session.viewerUrl) return;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await navigator.clipboard.writeText(session.viewerUrl);
|
|
174
|
+
setCopiedTarget("viewer");
|
|
175
|
+
window.setTimeout(
|
|
176
|
+
() => setCopiedTarget((value) => (value === "viewer" ? null : value)),
|
|
177
|
+
1200,
|
|
178
|
+
);
|
|
179
|
+
} catch {
|
|
180
|
+
setCopiedTarget(null);
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
className="inline-flex items-center justify-center gap-1.5 rounded-md border border-slate-200 bg-white/88 px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:bg-white"
|
|
184
|
+
>
|
|
185
|
+
{copiedTarget === "viewer" ? <Copy size={12} /> : <Link2 size={12} />}
|
|
186
|
+
{copiedTarget === "viewer" ? "Viewer copied" : "Copy viewer link"}
|
|
187
|
+
</button>
|
|
188
|
+
)}
|
|
189
|
+
{onOpenMirrorStage && (
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={onOpenMirrorStage}
|
|
193
|
+
className="inline-flex items-center justify-center gap-1.5 rounded-md border border-slate-200 bg-white/88 px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:bg-white"
|
|
194
|
+
>
|
|
195
|
+
<Link2 size={12} />
|
|
196
|
+
Open mirror stage
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
<div className="mb-3 grid gap-2 sm:grid-cols-2">
|
|
201
|
+
<FormSelect
|
|
202
|
+
label="stage scale"
|
|
203
|
+
size="sm"
|
|
204
|
+
value={String(chrome.stageScale)}
|
|
205
|
+
onChange={(event) => {
|
|
206
|
+
chrome.onStageScaleChange(Number(event.target.value));
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<option value="0.9">90%</option>
|
|
210
|
+
<option value="1">100%</option>
|
|
211
|
+
<option value="1.08">108%</option>
|
|
212
|
+
</FormSelect>
|
|
213
|
+
<FormSelect
|
|
214
|
+
label="cursor"
|
|
215
|
+
size="sm"
|
|
216
|
+
value={chrome.cursorMode}
|
|
217
|
+
onChange={(event) => {
|
|
218
|
+
chrome.onCursorModeChange(event.target.value as "always" | "idle-hide");
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<option value="always">always visible</option>
|
|
222
|
+
<option value="idle-hide">hide when idle</option>
|
|
223
|
+
</FormSelect>
|
|
224
|
+
<span
|
|
225
|
+
className={`inline-flex items-center gap-2 rounded-md border px-3 py-2 text-xs ${
|
|
226
|
+
fullscreen.supported
|
|
227
|
+
? fullscreen.active
|
|
228
|
+
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
|
229
|
+
: "border-slate-200 bg-white/82 text-slate-600"
|
|
230
|
+
: "border-amber-200 bg-amber-50 text-amber-700"
|
|
231
|
+
}`}
|
|
232
|
+
>
|
|
233
|
+
fullscreen:{" "}
|
|
234
|
+
{fullscreen.supported ? (fullscreen.active ? "active" : "off") : "unsupported"}
|
|
235
|
+
</span>
|
|
236
|
+
<span
|
|
237
|
+
className={`inline-flex items-center gap-2 rounded-md border px-3 py-2 text-xs ${
|
|
238
|
+
wakeLock.supported
|
|
239
|
+
? wakeLock.active
|
|
240
|
+
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
|
241
|
+
: "border-slate-200 bg-white/82 text-slate-600"
|
|
242
|
+
: "border-amber-200 bg-amber-50 text-amber-700"
|
|
243
|
+
}`}
|
|
244
|
+
>
|
|
245
|
+
wake lock:{" "}
|
|
246
|
+
{wakeLock.supported
|
|
247
|
+
? wakeLock.requested || wakeLock.active
|
|
248
|
+
? wakeLock.active
|
|
249
|
+
? "active"
|
|
250
|
+
: "requesting"
|
|
251
|
+
: "off"
|
|
252
|
+
: "unsupported"}
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
256
|
+
<ChromeTag tone="muted" size="md" className="py-2 text-xs">
|
|
257
|
+
{sync.broadcastConnected ? <Wifi size={12} /> : <WifiOff size={12} />}
|
|
258
|
+
{sync.broadcastConnected ? "Broadcast connected" : "Broadcast unavailable"}
|
|
259
|
+
</ChromeTag>
|
|
260
|
+
<ChromeTag tone="muted" size="md" className="py-2 text-xs">
|
|
261
|
+
ws: {sync.wsConnected ? "connected" : "idle"}
|
|
262
|
+
</ChromeTag>
|
|
263
|
+
<ChromeTag tone="muted" size="md" className="py-2 text-xs tabular-nums">
|
|
264
|
+
peers: {sync.peerCount}
|
|
265
|
+
</ChromeTag>
|
|
266
|
+
<ChromeTag
|
|
267
|
+
tone={sync.remoteActive ? "success" : "warning"}
|
|
268
|
+
size="md"
|
|
269
|
+
className="py-2 text-xs"
|
|
270
|
+
>
|
|
271
|
+
remote: {sync.remoteActive ? "active" : "stale"}
|
|
272
|
+
</ChromeTag>
|
|
273
|
+
<ChromeTag tone="muted" size="md" className="py-2 text-xs">
|
|
274
|
+
role: {session.role}
|
|
275
|
+
</ChromeTag>
|
|
276
|
+
<ChromeTag tone="muted" size="md" className="py-2 font-mono text-[11px]">
|
|
277
|
+
{session.sessionId}
|
|
278
|
+
</ChromeTag>
|
|
279
|
+
</div>
|
|
280
|
+
{sync.lastSyncedAt && (
|
|
281
|
+
<p className="mt-3 text-right text-[11px] text-slate-500">
|
|
282
|
+
last sync {new Date(sync.lastSyncedAt).toLocaleTimeString()}
|
|
283
|
+
</p>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
<div className="pointer-events-auto w-full overflow-hidden rounded-t-[6px] border border-b-0 border-slate-200/80 bg-white/82 text-slate-800 ring-1 ring-white/45 backdrop-blur-xl">
|
|
288
|
+
<div className="flex flex-wrap items-center justify-between gap-3 px-3 py-3">
|
|
289
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
|
290
|
+
{canRecord && (
|
|
291
|
+
<>
|
|
292
|
+
<ChromeIconButton
|
|
293
|
+
onClick={draw.toggleEnabled}
|
|
294
|
+
title="Toggle draw (D)"
|
|
295
|
+
aria-label="Toggle draw mode"
|
|
296
|
+
tone={draw.enabled ? "active" : "default"}
|
|
297
|
+
>
|
|
298
|
+
<PenLine size={16} />
|
|
299
|
+
</ChromeIconButton>
|
|
300
|
+
{draw.enabled && (
|
|
301
|
+
<>
|
|
302
|
+
<ChromeIconButton
|
|
303
|
+
onClick={() => draw.setTool("pen")}
|
|
304
|
+
title="Pen (P)"
|
|
305
|
+
aria-label="Use pen tool"
|
|
306
|
+
tone={draw.tool === "pen" ? "active" : "default"}
|
|
307
|
+
>
|
|
308
|
+
<PenLine size={15} />
|
|
309
|
+
</ChromeIconButton>
|
|
310
|
+
<ChromeIconButton
|
|
311
|
+
onClick={() => draw.setTool("circle")}
|
|
312
|
+
title="Circle (B)"
|
|
313
|
+
aria-label="Use circle tool"
|
|
314
|
+
tone={draw.tool === "circle" ? "active" : "default"}
|
|
315
|
+
>
|
|
316
|
+
<Circle size={15} />
|
|
317
|
+
</ChromeIconButton>
|
|
318
|
+
<ChromeIconButton
|
|
319
|
+
onClick={() => draw.setTool("rectangle")}
|
|
320
|
+
title="Rectangle (R)"
|
|
321
|
+
aria-label="Use rectangle tool"
|
|
322
|
+
tone={draw.tool === "rectangle" ? "active" : "default"}
|
|
323
|
+
>
|
|
324
|
+
<RectangleHorizontal size={15} />
|
|
325
|
+
</ChromeIconButton>
|
|
326
|
+
<ChromeIconButton
|
|
327
|
+
onClick={() => draw.setTool("eraser")}
|
|
328
|
+
title="Eraser (E)"
|
|
329
|
+
aria-label="Use eraser tool"
|
|
330
|
+
tone={draw.tool === "eraser" ? "active" : "default"}
|
|
331
|
+
>
|
|
332
|
+
<Eraser size={15} />
|
|
333
|
+
</ChromeIconButton>
|
|
334
|
+
<div className="mx-1 h-6 w-px bg-slate-200" aria-hidden />
|
|
335
|
+
{DRAW_COLORS.map((color) => (
|
|
336
|
+
<button
|
|
337
|
+
key={color}
|
|
338
|
+
type="button"
|
|
339
|
+
onClick={() => {
|
|
340
|
+
draw.setColor(color);
|
|
341
|
+
draw.setTool("pen");
|
|
342
|
+
}}
|
|
343
|
+
title={`Set draw color ${color}`}
|
|
344
|
+
aria-label={`Set draw color ${color}`}
|
|
345
|
+
className={`inline-flex size-5 items-center justify-center rounded-full border shadow-sm transition ${draw.color === color ? "border-slate-700 ring-2 ring-emerald-300" : "border-slate-300 opacity-90 hover:opacity-100"}`}
|
|
346
|
+
style={{ backgroundColor: color }}
|
|
347
|
+
/>
|
|
348
|
+
))}
|
|
349
|
+
<div className="mx-1 h-6 w-px bg-slate-200" aria-hidden />
|
|
350
|
+
{DRAW_WIDTHS.map((value) => (
|
|
351
|
+
<ChromeIconButton
|
|
352
|
+
key={value}
|
|
353
|
+
onClick={() => {
|
|
354
|
+
draw.setWidth(value);
|
|
355
|
+
draw.setTool("pen");
|
|
356
|
+
}}
|
|
357
|
+
title={`Set brush size ${value}`}
|
|
358
|
+
aria-label={`Set brush size ${value}`}
|
|
359
|
+
tone={draw.width === value ? "active" : "default"}
|
|
360
|
+
>
|
|
361
|
+
<span
|
|
362
|
+
className="rounded-full bg-current"
|
|
363
|
+
style={{
|
|
364
|
+
width: `${Math.max(value + 2, 6)}px`,
|
|
365
|
+
height: `${Math.max(value + 2, 6)}px`,
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
</ChromeIconButton>
|
|
369
|
+
))}
|
|
370
|
+
<div className="mx-1 h-6 w-px bg-slate-200" aria-hidden />
|
|
371
|
+
<ChromeIconButton
|
|
372
|
+
onClick={() => draw.undo(slideId)}
|
|
373
|
+
disabled={!hasStrokes}
|
|
374
|
+
title="Undo last stroke (Cmd/Ctrl+Z)"
|
|
375
|
+
aria-label="Undo last stroke"
|
|
376
|
+
>
|
|
377
|
+
<RotateCcw size={15} />
|
|
378
|
+
</ChromeIconButton>
|
|
379
|
+
<ChromeIconButton
|
|
380
|
+
onClick={() => draw.clear(slideId)}
|
|
381
|
+
disabled={!hasStrokes}
|
|
382
|
+
title="Clear page strokes (C)"
|
|
383
|
+
aria-label="Clear page strokes"
|
|
384
|
+
>
|
|
385
|
+
<Trash2 size={15} />
|
|
386
|
+
</ChromeIconButton>
|
|
387
|
+
</>
|
|
388
|
+
)}
|
|
389
|
+
</>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
393
|
+
<ChromeTag
|
|
394
|
+
tone="defaultStrong"
|
|
395
|
+
size="md"
|
|
396
|
+
weight="semibold"
|
|
397
|
+
className="h-9 px-3.5 text-sm tabular-nums"
|
|
398
|
+
>
|
|
399
|
+
<Radio size={13} />
|
|
400
|
+
{formatTimer(sessionTimerSeconds)}
|
|
401
|
+
</ChromeTag>
|
|
402
|
+
{canRecord && recorder.supported && (
|
|
403
|
+
<ChromeIconButton
|
|
404
|
+
onClick={() => {
|
|
405
|
+
if (recorder.isRecording) void recorder.stop();
|
|
406
|
+
else void recorder.start();
|
|
407
|
+
}}
|
|
408
|
+
title={recorder.isRecording ? "Stop recording" : "Start recording"}
|
|
409
|
+
aria-label={recorder.isRecording ? "Stop recording" : "Start recording"}
|
|
410
|
+
tone={recorder.isRecording ? "danger" : "default"}
|
|
411
|
+
>
|
|
412
|
+
{recorder.isRecording ? <Square size={12} /> : <CircleDot size={12} />}
|
|
413
|
+
</ChromeIconButton>
|
|
414
|
+
)}
|
|
415
|
+
{canRecord && onOpenPrintExport && (
|
|
416
|
+
<ChromeIconButton
|
|
417
|
+
onClick={onOpenPrintExport}
|
|
418
|
+
title="Print / PDF"
|
|
419
|
+
aria-label="Print / PDF"
|
|
420
|
+
>
|
|
421
|
+
<Printer size={12} />
|
|
422
|
+
</ChromeIconButton>
|
|
423
|
+
)}
|
|
424
|
+
<ChromeIconButton
|
|
425
|
+
onClick={chrome.onToggleNotes}
|
|
426
|
+
title="Notes Workspace (N)"
|
|
427
|
+
aria-label="Toggle notes workspace"
|
|
428
|
+
tone={chrome.notesOpen ? "active" : "default"}
|
|
429
|
+
>
|
|
430
|
+
<NotebookText size={13} />
|
|
431
|
+
</ChromeIconButton>
|
|
432
|
+
<ChromeIconButton
|
|
433
|
+
onClick={chrome.onToggleOverview}
|
|
434
|
+
disabled={!chrome.canOpenOverview}
|
|
435
|
+
title="Quick Overview (O)"
|
|
436
|
+
aria-label="Toggle quick overview"
|
|
437
|
+
tone={chrome.overviewOpen ? "active" : "default"}
|
|
438
|
+
>
|
|
439
|
+
<LayoutGrid size={13} />
|
|
440
|
+
</ChromeIconButton>
|
|
441
|
+
<ChromeIconButton
|
|
442
|
+
onClick={chrome.onToggleShortcuts}
|
|
443
|
+
title="Keyboard shortcuts (?)"
|
|
444
|
+
aria-label="Toggle keyboard shortcuts"
|
|
445
|
+
tone={chrome.shortcutsOpen ? "active" : "default"}
|
|
446
|
+
>
|
|
447
|
+
<Keyboard size={13} />
|
|
448
|
+
</ChromeIconButton>
|
|
449
|
+
<ChromeIconButton
|
|
450
|
+
onClick={chrome.onToggleTimelinePreview}
|
|
451
|
+
title={chrome.timelinePreviewOpen ? "Hide timeline preview" : "Show timeline preview"}
|
|
452
|
+
aria-label={chrome.timelinePreviewOpen ? "Hide timeline preview" : "Show timeline preview"}
|
|
453
|
+
tone={chrome.timelinePreviewOpen ? "violet" : "default"}
|
|
454
|
+
>
|
|
455
|
+
<List size={13} />
|
|
456
|
+
</ChromeIconButton>
|
|
457
|
+
<ChromeIconButton
|
|
458
|
+
onClick={() => {
|
|
459
|
+
void wakeLock.toggle();
|
|
460
|
+
}}
|
|
461
|
+
disabled={!wakeLock.supported}
|
|
462
|
+
title={
|
|
463
|
+
wakeLock.supported
|
|
464
|
+
? wakeLock.active
|
|
465
|
+
? "Wake lock on"
|
|
466
|
+
: "Turn on wake lock"
|
|
467
|
+
: "Wake lock unsupported"
|
|
468
|
+
}
|
|
469
|
+
aria-label={
|
|
470
|
+
wakeLock.supported
|
|
471
|
+
? wakeLock.active
|
|
472
|
+
? "Wake lock on"
|
|
473
|
+
: "Turn on wake lock"
|
|
474
|
+
: "Wake lock unsupported"
|
|
475
|
+
}
|
|
476
|
+
tone={wakeLock.active ? "success" : "default"}
|
|
477
|
+
>
|
|
478
|
+
<SunMedium size={13} />
|
|
479
|
+
</ChromeIconButton>
|
|
480
|
+
<ChromeIconButton
|
|
481
|
+
onClick={() => {
|
|
482
|
+
void fullscreen.toggle();
|
|
483
|
+
}}
|
|
484
|
+
disabled={!fullscreen.supported}
|
|
485
|
+
title={fullscreen.active ? "Fullscreen on" : "Fullscreen"}
|
|
486
|
+
aria-label={fullscreen.active ? "Fullscreen on" : "Fullscreen"}
|
|
487
|
+
tone={fullscreen.active ? "info" : "default"}
|
|
488
|
+
>
|
|
489
|
+
<Expand size={12} />
|
|
490
|
+
</ChromeIconButton>
|
|
491
|
+
<ChromeIconButton
|
|
492
|
+
onClick={() => setDetailsOpen((value) => !value)}
|
|
493
|
+
title={detailsOpen ? "Hide live details" : "Show live details"}
|
|
494
|
+
aria-label={detailsOpen ? "Hide live details" : "Show live details"}
|
|
495
|
+
>
|
|
496
|
+
<span className="relative inline-flex items-center justify-center">
|
|
497
|
+
<Radio size={13} />
|
|
498
|
+
<span
|
|
499
|
+
className={`absolute right-0 bottom-0 size-2 rounded-full ring-2 ring-white ${statusDotClassName(sync.status)}`}
|
|
500
|
+
/>
|
|
501
|
+
</span>
|
|
502
|
+
</ChromeIconButton>
|
|
503
|
+
</div>
|
|
504
|
+
{canRecord && !recorder.supported && (
|
|
505
|
+
<span className="text-xs text-amber-700">Recording unsupported in this browser.</span>
|
|
506
|
+
)}
|
|
507
|
+
{recorder.error && <span className="text-xs text-rose-700">{recorder.error}</span>}
|
|
508
|
+
{wakeLock.error && <span className="text-xs text-amber-700">{wakeLock.error}</span>}
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
</aside>
|
|
513
|
+
);
|
|
514
|
+
}
|