@open-aippt/core 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/bin.js +2 -0
  4. package/dist/build-DxTqmvsO.js +17 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +86 -0
  7. package/dist/config-CjzqjrEA.js +4280 -0
  8. package/dist/config-DIC-yVPp.d.ts +23 -0
  9. package/dist/design-cpzS8aud.js +35 -0
  10. package/dist/dev-BYuTeJbA.js +20 -0
  11. package/dist/format-BCeKbTOM.js +1605 -0
  12. package/dist/index.d.ts +134 -0
  13. package/dist/index.js +467 -0
  14. package/dist/locale/index.d.ts +24 -0
  15. package/dist/locale/index.js +3 -0
  16. package/dist/preview-DlQvnJPq.js +18 -0
  17. package/dist/sync-BPZ0m27m.js +139 -0
  18. package/dist/sync-EsYusbbL.js +3 -0
  19. package/dist/types-CHmFPIG_.d.ts +430 -0
  20. package/dist/vite/index.d.ts +14 -0
  21. package/dist/vite/index.js +4 -0
  22. package/env.d.ts +59 -0
  23. package/package.json +103 -0
  24. package/skills/apply-comments/SKILL.md +83 -0
  25. package/skills/create-slide/SKILL.md +91 -0
  26. package/skills/create-theme/SKILL.md +250 -0
  27. package/skills/current-slide/SKILL.md +110 -0
  28. package/skills/slide-authoring/SKILL.md +625 -0
  29. package/src/app/app.tsx +47 -0
  30. package/src/app/components/asset-view.tsx +966 -0
  31. package/src/app/components/history-provider.tsx +120 -0
  32. package/src/app/components/image-placeholder.tsx +243 -0
  33. package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
  34. package/src/app/components/inspector/comment-widget.tsx +93 -0
  35. package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
  36. package/src/app/components/inspector/inspect-overlay.tsx +387 -0
  37. package/src/app/components/inspector/inspector-panel.tsx +1115 -0
  38. package/src/app/components/inspector/inspector-provider.tsx +1218 -0
  39. package/src/app/components/inspector/save-bar.tsx +48 -0
  40. package/src/app/components/language-toggle.tsx +39 -0
  41. package/src/app/components/notes-drawer.tsx +120 -0
  42. package/src/app/components/overview-grid.tsx +363 -0
  43. package/src/app/components/panel/panel-fields.tsx +60 -0
  44. package/src/app/components/panel/panel-shell.tsx +80 -0
  45. package/src/app/components/panel/save-card.tsx +142 -0
  46. package/src/app/components/pdf-progress-toast.tsx +32 -0
  47. package/src/app/components/player.tsx +466 -0
  48. package/src/app/components/pptx-progress-toast.tsx +32 -0
  49. package/src/app/components/present/blackout-overlay.tsx +18 -0
  50. package/src/app/components/present/control-bar.tsx +315 -0
  51. package/src/app/components/present/help-overlay.tsx +57 -0
  52. package/src/app/components/present/jump-input.tsx +74 -0
  53. package/src/app/components/present/laser-pointer.tsx +39 -0
  54. package/src/app/components/present/progress-bar.tsx +26 -0
  55. package/src/app/components/present/use-idle.ts +46 -0
  56. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  57. package/src/app/components/present/use-presenter-channel.ts +66 -0
  58. package/src/app/components/present/use-touch-swipe.ts +66 -0
  59. package/src/app/components/shared-element.tsx +48 -0
  60. package/src/app/components/sidebar/folder-item.tsx +258 -0
  61. package/src/app/components/sidebar/icon-picker.tsx +61 -0
  62. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  63. package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
  64. package/src/app/components/sidebar/sidebar.tsx +284 -0
  65. package/src/app/components/slide-canvas.tsx +102 -0
  66. package/src/app/components/slide-transition-layer.tsx +844 -0
  67. package/src/app/components/style-panel/design-provider.tsx +148 -0
  68. package/src/app/components/style-panel/style-panel.tsx +349 -0
  69. package/src/app/components/style-panel/use-design.ts +112 -0
  70. package/src/app/components/theme-toggle.tsx +59 -0
  71. package/src/app/components/themes/theme-detail.tsx +305 -0
  72. package/src/app/components/themes/themes-gallery.tsx +149 -0
  73. package/src/app/components/thumbnail-rail.tsx +805 -0
  74. package/src/app/components/ui/badge.tsx +45 -0
  75. package/src/app/components/ui/button.tsx +99 -0
  76. package/src/app/components/ui/card.tsx +92 -0
  77. package/src/app/components/ui/context-menu.tsx +237 -0
  78. package/src/app/components/ui/dialog.tsx +157 -0
  79. package/src/app/components/ui/dropdown-menu.tsx +245 -0
  80. package/src/app/components/ui/input.tsx +25 -0
  81. package/src/app/components/ui/label.tsx +24 -0
  82. package/src/app/components/ui/popover.tsx +75 -0
  83. package/src/app/components/ui/progress.tsx +31 -0
  84. package/src/app/components/ui/scroll-area.tsx +53 -0
  85. package/src/app/components/ui/select.tsx +196 -0
  86. package/src/app/components/ui/separator.tsx +28 -0
  87. package/src/app/components/ui/slider.tsx +61 -0
  88. package/src/app/components/ui/sonner.tsx +48 -0
  89. package/src/app/components/ui/tabs.tsx +79 -0
  90. package/src/app/components/ui/textarea.tsx +22 -0
  91. package/src/app/components/ui/toggle-group.tsx +83 -0
  92. package/src/app/components/ui/toggle.tsx +45 -0
  93. package/src/app/components/ui/tooltip.tsx +58 -0
  94. package/src/app/favicon.ico +0 -0
  95. package/src/app/index.html +13 -0
  96. package/src/app/lib/assets.ts +242 -0
  97. package/src/app/lib/design-presets.ts +94 -0
  98. package/src/app/lib/design.ts +58 -0
  99. package/src/app/lib/export-html.ts +326 -0
  100. package/src/app/lib/export-pdf.ts +298 -0
  101. package/src/app/lib/export-pptx.ts +284 -0
  102. package/src/app/lib/folders.ts +239 -0
  103. package/src/app/lib/inspector/fiber.test.ts +154 -0
  104. package/src/app/lib/inspector/fiber.ts +85 -0
  105. package/src/app/lib/inspector/use-comments.ts +74 -0
  106. package/src/app/lib/inspector/use-editor.ts +73 -0
  107. package/src/app/lib/inspector/use-notes.ts +134 -0
  108. package/src/app/lib/locale-store.ts +67 -0
  109. package/src/app/lib/page-context.tsx +38 -0
  110. package/src/app/lib/print-ready.test.ts +32 -0
  111. package/src/app/lib/print-ready.ts +51 -0
  112. package/src/app/lib/sdk.test.ts +13 -0
  113. package/src/app/lib/sdk.ts +37 -0
  114. package/src/app/lib/slides.ts +26 -0
  115. package/src/app/lib/step-context.tsx +261 -0
  116. package/src/app/lib/themes.ts +22 -0
  117. package/src/app/lib/transition.ts +30 -0
  118. package/src/app/lib/use-agent-socket.ts +18 -0
  119. package/src/app/lib/use-click-page-navigation.ts +60 -0
  120. package/src/app/lib/use-is-mobile.ts +21 -0
  121. package/src/app/lib/use-locale.ts +8 -0
  122. package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
  123. package/src/app/lib/use-slide-module.ts +48 -0
  124. package/src/app/lib/use-wheel-page-navigation.ts +99 -0
  125. package/src/app/lib/utils.test.ts +25 -0
  126. package/src/app/lib/utils.ts +6 -0
  127. package/src/app/main.tsx +14 -0
  128. package/src/app/routes/assets.tsx +9 -0
  129. package/src/app/routes/home-shell.tsx +213 -0
  130. package/src/app/routes/home.tsx +807 -0
  131. package/src/app/routes/presenter.tsx +418 -0
  132. package/src/app/routes/slide.tsx +1108 -0
  133. package/src/app/routes/themes.tsx +34 -0
  134. package/src/app/styles.css +429 -0
  135. package/src/app/virtual.d.ts +51 -0
  136. package/src/locale/en.ts +416 -0
  137. package/src/locale/format.ts +12 -0
  138. package/src/locale/index.ts +6 -0
  139. package/src/locale/ja.ts +422 -0
  140. package/src/locale/types.ts +443 -0
  141. package/src/locale/zh-cn.ts +414 -0
  142. package/src/locale/zh-tw.ts +414 -0
@@ -0,0 +1,418 @@
1
+ import { ChevronLeft, ChevronRight, RotateCcw, Square, Sun } from 'lucide-react';
2
+ import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useParams } from 'react-router-dom';
4
+ import { Button } from '@/components/ui/button';
5
+ import { format, useLocale } from '@/lib/use-locale';
6
+ import { cn } from '@/lib/utils';
7
+ import {
8
+ type PresenterState,
9
+ usePresenterChannel,
10
+ } from '../components/present/use-presenter-channel';
11
+ import { SlideCanvas } from '../components/slide-canvas';
12
+ import { SlidePageProvider } from '../lib/page-context';
13
+ import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
14
+ import { type StepController, StepHost } from '../lib/step-context';
15
+ import { useSlideModule } from '../lib/use-slide-module';
16
+
17
+ export function Presenter() {
18
+ const { slideId = '' } = useParams();
19
+ const { slide, error } = useSlideModule(slideId);
20
+
21
+ // Presenter view is a passive mirror of the projection window. It only
22
+ // tracks the index it last heard about; navigation buttons send commands
23
+ // back to the projection so both windows stay in lock-step.
24
+ const [state, setState] = useState<PresenterState | null>(null);
25
+ // Local timer fallback — counts up from when the presenter window opened
26
+ // until the projection window publishes its actual `startedAt`.
27
+ const [localStart] = useState(() => Date.now());
28
+ const [hasProjection, setHasProjection] = useState(false);
29
+ const requestedRef = useRef(false);
30
+ const t = useLocale();
31
+
32
+ const channel = usePresenterChannel(slideId, (msg) => {
33
+ if (msg.type === 'state') {
34
+ setState(msg.state);
35
+ setHasProjection(true);
36
+ }
37
+ });
38
+
39
+ // Hydrate from the projection window once.
40
+ useEffect(() => {
41
+ if (!channel.available || requestedRef.current) return;
42
+ requestedRef.current = true;
43
+ channel.send({ type: 'request-state' });
44
+ // If nothing answers within a beat, surface the "no projection" hint.
45
+ const t = setTimeout(() => setHasProjection((v) => v), 600);
46
+ return () => clearTimeout(t);
47
+ }, [channel]);
48
+
49
+ const send = channel.send;
50
+ const goPrev = useCallback(() => send({ type: 'prev' }), [send]);
51
+ const goNext = useCallback(() => send({ type: 'next' }), [send]);
52
+ const goTo = useCallback((i: number) => send({ type: 'goto', index: i }), [send]);
53
+ const toggleBlack = useCallback(() => send({ type: 'toggle-blackout', mode: 'black' }), [send]);
54
+ const toggleWhite = useCallback(() => send({ type: 'toggle-blackout', mode: 'white' }), [send]);
55
+
56
+ // Local-window key bindings mirror the projection's main shortcuts so the
57
+ // presenter can drive without the mouse.
58
+ useEffect(() => {
59
+ const onKey = (e: KeyboardEvent) => {
60
+ if (e.target instanceof HTMLElement && e.target.matches('input, textarea')) return;
61
+ if (e.altKey || e.ctrlKey || e.metaKey) return;
62
+ if (
63
+ e.key === 'ArrowRight' ||
64
+ e.key === 'ArrowDown' ||
65
+ e.key === ' ' ||
66
+ e.key === 'PageDown'
67
+ ) {
68
+ e.preventDefault();
69
+ goNext();
70
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
71
+ e.preventDefault();
72
+ goPrev();
73
+ } else if (e.key === 'b' || e.key === 'B') {
74
+ e.preventDefault();
75
+ toggleBlack();
76
+ } else if (e.key === 'w' || e.key === 'W') {
77
+ e.preventDefault();
78
+ toggleWhite();
79
+ }
80
+ };
81
+ window.addEventListener('keydown', onKey);
82
+ return () => window.removeEventListener('keydown', onKey);
83
+ }, [goNext, goPrev, toggleBlack, toggleWhite]);
84
+
85
+ if (error) {
86
+ return (
87
+ <div className="dark grid h-dvh place-items-center bg-background p-8 text-foreground">
88
+ <div className="max-w-md text-center">
89
+ <span className="eyebrow text-destructive/80">{t.common.loadFailed}</span>
90
+ <h2 className="mt-2 font-heading text-xl font-semibold">{t.common.failedToLoadSlide}</h2>
91
+ <pre className="mt-4 overflow-auto rounded-[6px] border border-border bg-card p-4 text-left text-[11.5px] whitespace-pre-wrap shadow-edge">
92
+ {error}
93
+ </pre>
94
+ </div>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ if (!slide) {
100
+ return (
101
+ <div className="dark grid h-dvh place-items-center bg-background text-muted-foreground">
102
+ <div className="flex flex-col items-center gap-4">
103
+ <div className="relative h-px w-56 overflow-hidden bg-border">
104
+ <span
105
+ aria-hidden
106
+ className="line-loader-bar absolute inset-y-[-0.5px] left-0 w-1/4 bg-foreground"
107
+ />
108
+ </div>
109
+ <div className="text-[11.5px]">{format(t.presenter.loadingSlide, { slideId })}</div>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ const pages = slide.default;
116
+ const total = pages.length;
117
+ const index = Math.max(0, Math.min(total - 1, state?.index ?? 0));
118
+ const note = slide.notes?.[index];
119
+ const blackout = state?.blackout ?? null;
120
+ const startedAt = state?.startedAt ?? localStart;
121
+ const stepIndex = Math.max(0, state?.stepIndex ?? 0);
122
+ const stepCount = Math.max(0, state?.stepCount ?? 0);
123
+
124
+ const stepsRemaining = stepIndex < stepCount;
125
+ const hasNextSlide = index < total - 1;
126
+ const hasNext = stepsRemaining || hasNextSlide;
127
+ const nextPageIndex = stepsRemaining ? index : Math.min(total - 1, index + 1);
128
+ const nextRevealed = stepsRemaining ? stepIndex + 1 : 0;
129
+
130
+ const CurrentPage = pages[index];
131
+ const NextPage = hasNext ? pages[nextPageIndex] : null;
132
+
133
+ return (
134
+ <div className="dark flex h-dvh w-screen flex-col overflow-hidden bg-background text-foreground">
135
+ <PresenterTopBar
136
+ index={index}
137
+ total={total}
138
+ startedAt={startedAt}
139
+ slideTitle={slide.meta?.title ?? slideId}
140
+ connected={hasProjection}
141
+ />
142
+
143
+ <div className="grid min-h-0 flex-1 grid-cols-1 gap-6 px-6 pb-4 lg:grid-cols-[2fr_1fr]">
144
+ {/* Now-showing */}
145
+ <section className="flex min-h-0 flex-col gap-3">
146
+ <SectionLabel>{t.presenter.nowShowing}</SectionLabel>
147
+ <div className="relative min-h-0 flex-1 overflow-hidden rounded-[8px] bg-black ring-1 ring-border">
148
+ <SlideCanvas flat design={slide.design}>
149
+ <SlidePageProvider index={index} total={total}>
150
+ <PreviewStepHost revealed={stepIndex}>
151
+ <CurrentPage />
152
+ </PreviewStepHost>
153
+ </SlidePageProvider>
154
+ </SlideCanvas>
155
+ {blackout && (
156
+ <div
157
+ aria-hidden
158
+ className={cn(
159
+ 'pointer-events-none absolute inset-0 grid place-items-center text-[11px] tracking-[0.16em] uppercase',
160
+ blackout === 'black' ? 'bg-black text-white/35' : 'bg-white text-black/35',
161
+ )}
162
+ >
163
+ {blackout === 'black' ? t.presenter.blackScreen : t.presenter.whiteScreen}
164
+ </div>
165
+ )}
166
+ </div>
167
+ </section>
168
+
169
+ {/* Next + notes */}
170
+ <aside className="flex min-h-0 flex-col gap-4">
171
+ <div className="flex flex-col gap-2">
172
+ <SectionLabel>{hasNext ? t.presenter.upNext : t.presenter.lastSlide}</SectionLabel>
173
+ <div
174
+ className="relative w-full overflow-hidden rounded-[8px] bg-black ring-1 ring-border"
175
+ style={{ aspectRatio: `${CANVAS_WIDTH}/${CANVAS_HEIGHT}` }}
176
+ >
177
+ {NextPage ? (
178
+ <SlideCanvas flat freezeMotion design={slide.design}>
179
+ <SlidePageProvider index={nextPageIndex} total={total}>
180
+ <PreviewStepHost revealed={nextRevealed}>
181
+ <NextPage />
182
+ </PreviewStepHost>
183
+ </SlidePageProvider>
184
+ </SlideCanvas>
185
+ ) : (
186
+ <div className="grid h-full place-items-center text-[11.5px] text-muted-foreground">
187
+ {t.presenter.endOfDeck}
188
+ </div>
189
+ )}
190
+ </div>
191
+ </div>
192
+
193
+ <div className="flex min-h-0 flex-1 flex-col gap-2">
194
+ <SectionLabel>{t.presenter.speakerNotes}</SectionLabel>
195
+ <div className="min-h-0 flex-1 overflow-y-auto rounded-[6px] border border-border bg-card p-3 text-[13.5px] leading-relaxed whitespace-pre-wrap text-card-foreground">
196
+ {note?.trim() ? (
197
+ note
198
+ ) : (
199
+ <span className="text-muted-foreground">
200
+ {t.presenter.noNotesPrefix}
201
+ <code className="rounded-[3px] bg-muted px-1 py-0.5 font-mono text-[12px]">
202
+ export const notes = […]
203
+ </code>
204
+ {t.presenter.noNotesSuffix}
205
+ </span>
206
+ )}
207
+ </div>
208
+ </div>
209
+
210
+ <PresenterJumpControl total={total} current={index} onJump={goTo} />
211
+ </aside>
212
+ </div>
213
+
214
+ <PresenterBottomBar
215
+ index={index}
216
+ total={total}
217
+ blackout={blackout}
218
+ onPrev={goPrev}
219
+ onNext={goNext}
220
+ onBlackout={toggleBlack}
221
+ onWhiteout={toggleWhite}
222
+ />
223
+ </div>
224
+ );
225
+ }
226
+
227
+ function PresenterTopBar({
228
+ index,
229
+ total,
230
+ startedAt,
231
+ slideTitle,
232
+ connected,
233
+ }: {
234
+ index: number;
235
+ total: number;
236
+ startedAt: number;
237
+ slideTitle: string;
238
+ connected: boolean;
239
+ }) {
240
+ const t = useLocale();
241
+ return (
242
+ <header className="flex h-12 shrink-0 items-center justify-between border-b border-hairline px-6">
243
+ <div className="flex items-baseline gap-3">
244
+ <span className="eyebrow text-white/45">{t.presenter.eyebrow}</span>
245
+ <span className="truncate font-heading text-[14px] font-semibold tracking-tight">
246
+ {slideTitle}
247
+ </span>
248
+ {!connected && (
249
+ <span className="rounded-[3px] border border-amber-300/30 bg-amber-300/10 px-1.5 py-0.5 font-mono text-[10px] tracking-[0.06em] uppercase text-amber-200/85">
250
+ {t.presenter.notLinked}
251
+ </span>
252
+ )}
253
+ </div>
254
+ <div className="flex items-center gap-6">
255
+ <Clock />
256
+ <ElapsedClock startedAt={startedAt} />
257
+ <div className="font-mono text-[18px] tabular-nums">
258
+ <span className="text-foreground">{(index + 1).toString().padStart(2, '0')}</span>
259
+ <span className="text-foreground/30"> / </span>
260
+ <span className="text-muted-foreground">{total.toString().padStart(2, '0')}</span>
261
+ </div>
262
+ </div>
263
+ </header>
264
+ );
265
+ }
266
+
267
+ function PresenterBottomBar({
268
+ index,
269
+ total,
270
+ blackout,
271
+ onPrev,
272
+ onNext,
273
+ onBlackout,
274
+ onWhiteout,
275
+ }: {
276
+ index: number;
277
+ total: number;
278
+ blackout: 'black' | 'white' | null;
279
+ onPrev: () => void;
280
+ onNext: () => void;
281
+ onBlackout: () => void;
282
+ onWhiteout: () => void;
283
+ }) {
284
+ const t = useLocale();
285
+ return (
286
+ <footer className="flex shrink-0 items-center justify-between gap-3 border-t border-hairline px-6 py-3">
287
+ <div className="flex items-center gap-2">
288
+ <Button variant="outline" onClick={onPrev} disabled={index === 0}>
289
+ <ChevronLeft className="size-4" /> {t.presenter.prev}
290
+ </Button>
291
+ <Button variant="outline" onClick={onNext} disabled={index >= total - 1}>
292
+ {t.presenter.next} <ChevronRight className="size-4" />
293
+ </Button>
294
+ </div>
295
+ <div className="flex items-center gap-2">
296
+ <Button
297
+ variant={blackout === 'black' ? 'brand' : 'outline'}
298
+ onClick={onBlackout}
299
+ aria-pressed={blackout === 'black'}
300
+ >
301
+ <Square className="size-4 fill-current" /> {t.presenter.black}
302
+ </Button>
303
+ <Button
304
+ variant={blackout === 'white' ? 'brand' : 'outline'}
305
+ onClick={onWhiteout}
306
+ aria-pressed={blackout === 'white'}
307
+ >
308
+ <Sun className="size-4" /> {t.presenter.white}
309
+ </Button>
310
+ <Button
311
+ variant="ghost"
312
+ onClick={() => window.location.reload()}
313
+ title={t.presenter.resetTimer}
314
+ >
315
+ <RotateCcw className="size-4" /> {t.presenter.reset}
316
+ </Button>
317
+ </div>
318
+ </footer>
319
+ );
320
+ }
321
+
322
+ function PresenterJumpControl({
323
+ total,
324
+ current,
325
+ onJump,
326
+ }: {
327
+ total: number;
328
+ current: number;
329
+ onJump: (index: number) => void;
330
+ }) {
331
+ const [value, setValue] = useState('');
332
+ const t = useLocale();
333
+ return (
334
+ <form
335
+ onSubmit={(e) => {
336
+ e.preventDefault();
337
+ const n = Number.parseInt(value, 10);
338
+ if (Number.isFinite(n) && n >= 1 && n <= total) {
339
+ onJump(n - 1);
340
+ setValue('');
341
+ }
342
+ }}
343
+ className="flex items-center gap-2"
344
+ >
345
+ <SectionLabel>{t.presenter.jump}</SectionLabel>
346
+ <input
347
+ type="number"
348
+ min={1}
349
+ max={total}
350
+ value={value}
351
+ onChange={(e) => setValue(e.target.value)}
352
+ placeholder={(current + 1).toString()}
353
+ className="h-8 w-20 rounded-[5px] border border-border bg-card px-2 font-mono text-[12px] tabular-nums outline-none focus-visible:border-foreground/30"
354
+ />
355
+ <span className="font-mono text-[11px] text-muted-foreground">/ {total}</span>
356
+ </form>
357
+ );
358
+ }
359
+
360
+ function SectionLabel({ children }: { children: React.ReactNode }) {
361
+ return <span className="eyebrow">{children}</span>;
362
+ }
363
+
364
+ function PreviewStepHost({ revealed, children }: { revealed: number; children: ReactNode }) {
365
+ const noopControllerRef = useRef<StepController | null>(null);
366
+ return (
367
+ <StepHost
368
+ isActivePage={false}
369
+ entryDirection="jump"
370
+ controllerRef={noopControllerRef}
371
+ controlledRevealed={revealed}
372
+ >
373
+ {children}
374
+ </StepHost>
375
+ );
376
+ }
377
+
378
+ function Clock() {
379
+ const [now, setNow] = useState(() => new Date());
380
+ const t = useLocale();
381
+ useEffect(() => {
382
+ const id = setInterval(() => setNow(new Date()), 1000);
383
+ return () => clearInterval(id);
384
+ }, []);
385
+ return (
386
+ <time
387
+ title={t.presenter.currentTime}
388
+ className="font-mono text-[12px] tabular-nums text-muted-foreground"
389
+ >
390
+ {now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
391
+ </time>
392
+ );
393
+ }
394
+
395
+ function ElapsedClock({ startedAt }: { startedAt: number }) {
396
+ const [now, setNow] = useState(() => Date.now());
397
+ const t = useLocale();
398
+ useEffect(() => {
399
+ const id = setInterval(() => setNow(Date.now()), 1000);
400
+ return () => clearInterval(id);
401
+ }, []);
402
+ const elapsed = Math.max(0, Math.floor((now - startedAt) / 1000));
403
+ const h = Math.floor(elapsed / 3600);
404
+ const m = Math.floor((elapsed % 3600) / 60);
405
+ const s = elapsed % 60;
406
+ const text =
407
+ h > 0
408
+ ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
409
+ : `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
410
+ return (
411
+ <time
412
+ title={t.presenter.elapsed}
413
+ className="font-mono text-[18px] tabular-nums text-foreground"
414
+ >
415
+ {text}
416
+ </time>
417
+ );
418
+ }