@open-slide/core 1.0.6 → 1.2.0

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 (37) hide show
  1. package/dist/{build-4wOJF1l4.js → build-6BeQ3cxb.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-evLWCV1-.js → config-AxZ5OE1u.js} +772 -201
  4. package/dist/{config-D2y1AXaN.d.ts → config-CtT8K4VF.d.ts} +1 -1
  5. package/dist/{dev-BUr0S-Ij.js → dev-C9eLmUEq.js} +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/locale/index.d.ts +1 -1
  8. package/dist/locale/index.js +136 -24
  9. package/dist/{preview-DP_gIphz.js → preview-Cunm-f4i.js} +1 -1
  10. package/dist/{types-BVvl_xup.d.ts → types-CRHIeoNq.d.ts} +37 -4
  11. package/dist/vite/index.d.ts +2 -2
  12. package/dist/vite/index.js +1 -1
  13. package/package.json +5 -1
  14. package/skills/current-slide/SKILL.md +110 -0
  15. package/skills/slide-authoring/SKILL.md +48 -1
  16. package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
  17. package/src/app/components/inspector/inspect-overlay.tsx +17 -2
  18. package/src/app/components/inspector/inspector-panel.tsx +90 -26
  19. package/src/app/components/inspector/inspector-provider.tsx +136 -1
  20. package/src/app/components/notes-drawer.tsx +117 -0
  21. package/src/app/components/player.tsx +26 -8
  22. package/src/app/components/present/overview-grid.tsx +2 -2
  23. package/src/app/components/present/use-idle.ts +6 -4
  24. package/src/app/components/style-panel/design-provider.tsx +13 -0
  25. package/src/app/components/style-panel/style-panel.tsx +23 -11
  26. package/src/app/components/thumbnail-rail.tsx +317 -55
  27. package/src/app/components/ui/context-menu.tsx +237 -0
  28. package/src/app/lib/design-presets.ts +94 -0
  29. package/src/app/lib/inspector/use-notes.ts +134 -0
  30. package/src/app/routes/home.tsx +34 -12
  31. package/src/app/routes/presenter.tsx +27 -24
  32. package/src/app/routes/slide.tsx +238 -51
  33. package/src/locale/en.ts +35 -4
  34. package/src/locale/ja.ts +35 -4
  35. package/src/locale/types.ts +38 -4
  36. package/src/locale/zh-cn.ts +35 -4
  37. package/src/locale/zh-tw.ts +35 -4
@@ -0,0 +1,94 @@
1
+ import { type DesignSystem, defaultDesign } from './design';
2
+
3
+ const SANS_SYSTEM = '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif';
4
+ const SANS_INTER = '"Inter", system-ui, sans-serif';
5
+ const SANS_HELV = '"Helvetica Neue", Helvetica, Arial, sans-serif';
6
+ const SERIF_GEORGIA = 'Georgia, "Times New Roman", serif';
7
+ const SERIF_TIMES = '"Times New Roman", Times, serif';
8
+ const MONO_SF = '"SF Mono", "JetBrains Mono", Menlo, monospace';
9
+
10
+ export const designPresets: DesignSystem[] = [
11
+ defaultDesign,
12
+ {
13
+ palette: { bg: '#0f1115', text: '#f5f3ee', accent: '#7cc4ff' },
14
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
15
+ typeScale: { hero: 192, body: 32 },
16
+ radius: 6,
17
+ },
18
+ {
19
+ palette: { bg: '#eef1f4', text: '#1c2733', accent: '#ff6a5b' },
20
+ fonts: { display: SANS_HELV, body: SANS_SYSTEM },
21
+ typeScale: { hero: 156, body: 30 },
22
+ radius: 8,
23
+ },
24
+ {
25
+ palette: { bg: '#fdf6e3', text: '#073642', accent: '#b58900' },
26
+ fonts: { display: SERIF_GEORGIA, body: SANS_INTER },
27
+ typeScale: { hero: 144, body: 28 },
28
+ radius: 14,
29
+ },
30
+ {
31
+ palette: { bg: '#ede2cc', text: '#3a2a1a', accent: '#2f6e3a' },
32
+ fonts: { display: SERIF_TIMES, body: SERIF_GEORGIA },
33
+ typeScale: { hero: 168, body: 32 },
34
+ radius: 4,
35
+ },
36
+ {
37
+ palette: { bg: '#ffffff', text: '#0a0a0a', accent: '#e11d48' },
38
+ fonts: { display: SANS_HELV, body: SANS_HELV },
39
+ typeScale: { hero: 200, body: 28 },
40
+ radius: 0,
41
+ },
42
+ {
43
+ palette: { bg: '#fde9d9', text: '#3a1f3d', accent: '#f97316' },
44
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
45
+ typeScale: { hero: 184, body: 36 },
46
+ radius: 24,
47
+ },
48
+ {
49
+ palette: { bg: '#e9f5ee', text: '#0f3324', accent: '#ec4899' },
50
+ fonts: { display: SANS_INTER, body: SANS_INTER },
51
+ typeScale: { hero: 160, body: 32 },
52
+ radius: 16,
53
+ },
54
+ {
55
+ palette: { bg: '#0a0a0a', text: '#f3edd9', accent: '#eab308' },
56
+ fonts: { display: SERIF_GEORGIA, body: SANS_HELV },
57
+ typeScale: { hero: 200, body: 32 },
58
+ radius: 2,
59
+ },
60
+ {
61
+ palette: { bg: '#ece2f5', text: '#2a1c4a', accent: '#facc15' },
62
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
63
+ typeScale: { hero: 168, body: 34 },
64
+ radius: 20,
65
+ },
66
+ {
67
+ palette: { bg: '#101418', text: '#a7f3d0', accent: '#fbbf24' },
68
+ fonts: { display: MONO_SF, body: MONO_SF },
69
+ typeScale: { hero: 144, body: 24 },
70
+ radius: 4,
71
+ },
72
+ {
73
+ palette: { bg: '#fafafa', text: '#0a0a0a', accent: '#facc15' },
74
+ fonts: { display: SANS_HELV, body: SANS_HELV },
75
+ typeScale: { hero: 220, body: 32 },
76
+ radius: 0,
77
+ },
78
+ ];
79
+
80
+ function pickRandom(): DesignSystem {
81
+ const idx = Math.floor(Math.random() * designPresets.length);
82
+ return designPresets[idx] ?? defaultDesign;
83
+ }
84
+
85
+ export function shuffleDesign(current?: DesignSystem | null): DesignSystem {
86
+ if (designPresets.length === 0) return defaultDesign;
87
+ if (designPresets.length === 1) return designPresets[0] ?? defaultDesign;
88
+ const currentJson = current ? JSON.stringify(current) : null;
89
+ for (let i = 0; i < 8; i++) {
90
+ const pick = pickRandom();
91
+ if (JSON.stringify(pick) !== currentJson) return pick;
92
+ }
93
+ return pickRandom();
94
+ }
@@ -0,0 +1,134 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ export type NoteSaveStatus =
4
+ | { kind: 'idle' }
5
+ | { kind: 'saving' }
6
+ | { kind: 'saved' }
7
+ | { kind: 'error'; message: string };
8
+
9
+ const DEBOUNCE_MS = 600;
10
+
11
+ type Target = { slideId: string; index: number };
12
+
13
+ // HMR is suppressed for our writes, so the cached slide module's `notes`
14
+ // stays stale across navigation. Cache last-saved text per target so
15
+ // switching slides and back doesn't surface the old value.
16
+ const sessionCache = new Map<string, string>();
17
+ const cacheKey = (slideId: string, index: number) => `${slideId}:${index}`;
18
+
19
+ // Remap the per-target cache after a reorder. `order[i]` is the original
20
+ // page index that lands at new position `i`, matching the contract used by
21
+ // the `/__slides/:id/reorder` endpoint.
22
+ export function remapNotesSessionCacheAfterReorder(slideId: string, order: number[]): void {
23
+ const prev = new Map<number, string>();
24
+ for (let i = 0; i < order.length; i++) {
25
+ const cached = sessionCache.get(cacheKey(slideId, i));
26
+ if (cached !== undefined) prev.set(i, cached);
27
+ sessionCache.delete(cacheKey(slideId, i));
28
+ }
29
+ for (let newIdx = 0; newIdx < order.length; newIdx++) {
30
+ const oldIdx = order[newIdx];
31
+ const text = prev.get(oldIdx);
32
+ if (text !== undefined) sessionCache.set(cacheKey(slideId, newIdx), text);
33
+ }
34
+ }
35
+
36
+ export function useNotes(slideId: string, index: number, initial: string | undefined) {
37
+ const initialText = sessionCache.get(cacheKey(slideId, index)) ?? initial ?? '';
38
+ const [value, setValueState] = useState(initialText);
39
+ const [status, setStatus] = useState<NoteSaveStatus>({ kind: 'idle' });
40
+
41
+ const lastSavedRef = useRef(initialText);
42
+ const dirtyRef = useRef(false);
43
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
44
+ const inflightRef = useRef<AbortController | null>(null);
45
+ const targetRef = useRef<Target>({ slideId, index });
46
+ const valueRef = useRef(value);
47
+ valueRef.current = value;
48
+
49
+ const cancelTimer = useCallback(() => {
50
+ if (timerRef.current != null) {
51
+ clearTimeout(timerRef.current);
52
+ timerRef.current = null;
53
+ }
54
+ }, []);
55
+
56
+ const persist = useCallback(async (target: Target, text: string) => {
57
+ inflightRef.current?.abort();
58
+ const ctl = new AbortController();
59
+ inflightRef.current = ctl;
60
+ setStatus({ kind: 'saving' });
61
+ try {
62
+ const res = await fetch('/__notes', {
63
+ method: 'PUT',
64
+ headers: { 'content-type': 'application/json' },
65
+ body: JSON.stringify({ slideId: target.slideId, index: target.index, text }),
66
+ signal: ctl.signal,
67
+ });
68
+ const body = (await res.json().catch(() => ({}))) as { error?: string };
69
+ if (!res.ok) throw new Error(body.error ?? `PUT /__notes → ${res.status}`);
70
+ sessionCache.set(cacheKey(target.slideId, target.index), text);
71
+ if (inflightRef.current !== ctl) return;
72
+ lastSavedRef.current = text;
73
+ dirtyRef.current = false;
74
+ setStatus({ kind: 'saved' });
75
+ } catch (err) {
76
+ if ((err as { name?: string }).name === 'AbortError') return;
77
+ setStatus({ kind: 'error', message: String((err as Error).message ?? err) });
78
+ } finally {
79
+ if (inflightRef.current === ctl) inflightRef.current = null;
80
+ }
81
+ }, []);
82
+
83
+ const flush = useCallback(async () => {
84
+ cancelTimer();
85
+ if (!dirtyRef.current) return;
86
+ const target = targetRef.current;
87
+ await persist(target, valueRef.current);
88
+ }, [cancelTimer, persist]);
89
+
90
+ // When the (slideId, index) target changes, flush pending edits for the
91
+ // previous target before adopting the new initial text.
92
+ useEffect(() => {
93
+ const prev = targetRef.current;
94
+ const targetChanged = prev.slideId !== slideId || prev.index !== index;
95
+ if (targetChanged && dirtyRef.current) {
96
+ cancelTimer();
97
+ const pending = valueRef.current;
98
+ if (lastSavedRef.current !== pending) void persist(prev, pending);
99
+ }
100
+ targetRef.current = { slideId, index };
101
+ cancelTimer();
102
+ setValueState(initialText);
103
+ lastSavedRef.current = initialText;
104
+ dirtyRef.current = false;
105
+ setStatus({ kind: 'idle' });
106
+ }, [slideId, index, initialText, persist, cancelTimer]);
107
+
108
+ useEffect(() => {
109
+ return () => {
110
+ cancelTimer();
111
+ inflightRef.current?.abort();
112
+ };
113
+ }, [cancelTimer]);
114
+
115
+ const setValue = useCallback(
116
+ (next: string) => {
117
+ setValueState(next);
118
+ dirtyRef.current = next !== lastSavedRef.current;
119
+ cancelTimer();
120
+ if (!dirtyRef.current) {
121
+ setStatus({ kind: 'idle' });
122
+ return;
123
+ }
124
+ const target = targetRef.current;
125
+ timerRef.current = setTimeout(() => {
126
+ timerRef.current = null;
127
+ void persist(target, next);
128
+ }, DEBOUNCE_MS);
129
+ },
130
+ [persist, cancelTimer],
131
+ );
132
+
133
+ return { value, setValue, status, flush };
134
+ }
@@ -27,7 +27,8 @@ import type { Folder, FolderIcon, SlideModule } from '../lib/sdk';
27
27
  import { loadSlide, slideIds } from '../lib/slides';
28
28
 
29
29
  export function Home() {
30
- const { manifest, create, update, remove, assign, renameSlide, deleteSlide } = useFolders();
30
+ const { manifest, loading, create, update, remove, assign, renameSlide, deleteSlide } =
31
+ useFolders();
31
32
  const [searchParams, setSearchParams] = useSearchParams();
32
33
  const selectedId = searchParams.get('f') ?? DRAFT_ID;
33
34
  const t = useLocale();
@@ -168,23 +169,27 @@ export function Home() {
168
169
  <h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
169
170
  {title}
170
171
  </h1>
171
- <span className="folio ml-1 self-end pb-2">
172
- {(isSearching ? filteredSlides.length : visibleSlides.length)
173
- .toString()
174
- .padStart(2, '0')}
175
- {isSearching && (
176
- <span className="opacity-40">
177
- /{visibleSlides.length.toString().padStart(2, '0')}
178
- </span>
179
- )}
180
- </span>
172
+ {!loading && (
173
+ <span className="folio ml-1 self-end pb-2">
174
+ {(isSearching ? filteredSlides.length : visibleSlides.length)
175
+ .toString()
176
+ .padStart(2, '0')}
177
+ {isSearching && (
178
+ <span className="opacity-40">
179
+ /{visibleSlides.length.toString().padStart(2, '0')}
180
+ </span>
181
+ )}
182
+ </span>
183
+ )}
181
184
  <div className="ml-auto w-full md:w-auto">
182
185
  <SearchInput value={query} onChange={setQuery} />
183
186
  </div>
184
187
  </div>
185
188
  </header>
186
189
 
187
- {visibleSlides.length === 0 ? (
190
+ {loading ? (
191
+ <HomeLoading />
192
+ ) : visibleSlides.length === 0 ? (
188
193
  <EmptyState isDraft={isDraft} folderName={selectedFolder?.name} />
189
194
  ) : filteredSlides.length === 0 ? (
190
195
  <NoResultsState query={query} onClear={() => setQuery('')} />
@@ -271,6 +276,23 @@ function SearchInput({ value, onChange }: { value: string; onChange: (value: str
271
276
  );
272
277
  }
273
278
 
279
+ function HomeLoading() {
280
+ const t = useLocale();
281
+ return (
282
+ <div className="grid place-items-center px-8 py-24 text-muted-foreground">
283
+ <div className="flex flex-col items-center gap-4">
284
+ <div className="relative h-px w-56 overflow-hidden bg-hairline">
285
+ <span
286
+ aria-hidden
287
+ className="line-loader-bar absolute inset-y-[-0.5px] left-0 w-1/4 bg-foreground"
288
+ />
289
+ </div>
290
+ <span className="eyebrow text-[11.5px]">{t.slide.loadingEyebrow}</span>
291
+ </div>
292
+ </div>
293
+ );
294
+ }
295
+
274
296
  function NoResultsState({ query, onClear }: { query: string; onClear: () => void }) {
275
297
  const t = useLocale();
276
298
  return (
@@ -100,11 +100,11 @@ export function Presenter() {
100
100
 
101
101
  if (error) {
102
102
  return (
103
- <div className="grid h-dvh place-items-center bg-zinc-950 p-8 text-zinc-300">
103
+ <div className="dark grid h-dvh place-items-center bg-background p-8 text-foreground">
104
104
  <div className="max-w-md text-center">
105
- <span className="eyebrow text-red-300/80">{t.common.loadFailed}</span>
105
+ <span className="eyebrow text-destructive/80">{t.common.loadFailed}</span>
106
106
  <h2 className="mt-2 font-heading text-xl font-semibold">{t.common.failedToLoadSlide}</h2>
107
- <pre className="mt-4 overflow-auto rounded-[6px] border border-white/10 bg-black/40 p-4 text-left text-[11.5px] whitespace-pre-wrap">
107
+ <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">
108
108
  {error}
109
109
  </pre>
110
110
  </div>
@@ -114,15 +114,15 @@ export function Presenter() {
114
114
 
115
115
  if (!slide) {
116
116
  return (
117
- <div className="grid h-dvh place-items-center bg-zinc-950 text-zinc-400">
117
+ <div className="dark grid h-dvh place-items-center bg-background text-muted-foreground">
118
118
  <div className="flex flex-col items-center gap-4">
119
- <div className="relative h-px w-56 overflow-hidden bg-white/10">
119
+ <div className="relative h-px w-56 overflow-hidden bg-border">
120
120
  <span
121
121
  aria-hidden
122
- className="line-loader-bar absolute inset-y-[-0.5px] left-0 w-1/4 bg-zinc-100"
122
+ className="line-loader-bar absolute inset-y-[-0.5px] left-0 w-1/4 bg-foreground"
123
123
  />
124
124
  </div>
125
- <div className="text-[12.5px]">{format(t.presenter.loadingSlide, { slideId })}</div>
125
+ <div className="text-[11.5px]">{format(t.presenter.loadingSlide, { slideId })}</div>
126
126
  </div>
127
127
  </div>
128
128
  );
@@ -141,7 +141,7 @@ export function Presenter() {
141
141
  const NextPage = hasNext ? pages[nextIndex] : null;
142
142
 
143
143
  return (
144
- <div className="flex h-dvh w-screen flex-col overflow-hidden bg-zinc-950 text-zinc-100">
144
+ <div className="dark flex h-dvh w-screen flex-col overflow-hidden bg-background text-foreground">
145
145
  <PresenterTopBar
146
146
  index={index}
147
147
  total={total}
@@ -154,7 +154,7 @@ export function Presenter() {
154
154
  {/* Now-showing */}
155
155
  <section className="flex min-h-0 flex-col gap-3">
156
156
  <SectionLabel>{t.presenter.nowShowing}</SectionLabel>
157
- <div className="relative min-h-0 flex-1 overflow-hidden rounded-[8px] bg-black ring-1 ring-white/10">
157
+ <div className="relative min-h-0 flex-1 overflow-hidden rounded-[8px] bg-black ring-1 ring-border">
158
158
  <SlideCanvas flat design={slide.design}>
159
159
  <CurrentPage />
160
160
  </SlideCanvas>
@@ -177,7 +177,7 @@ export function Presenter() {
177
177
  <div className="flex flex-col gap-2">
178
178
  <SectionLabel>{hasNext ? t.presenter.upNext : t.presenter.lastSlide}</SectionLabel>
179
179
  <div
180
- className="relative w-full overflow-hidden rounded-[6px] bg-black ring-1 ring-white/10"
180
+ className="relative w-full overflow-hidden rounded-[8px] bg-black ring-1 ring-border"
181
181
  style={{ aspectRatio: `${CANVAS_WIDTH}/${CANVAS_HEIGHT}` }}
182
182
  >
183
183
  {NextPage ? (
@@ -185,7 +185,7 @@ export function Presenter() {
185
185
  <NextPage />
186
186
  </SlideCanvas>
187
187
  ) : (
188
- <div className="grid h-full place-items-center text-[11.5px] text-white/40">
188
+ <div className="grid h-full place-items-center text-[11.5px] text-muted-foreground">
189
189
  {t.presenter.endOfDeck}
190
190
  </div>
191
191
  )}
@@ -194,13 +194,13 @@ export function Presenter() {
194
194
 
195
195
  <div className="flex min-h-0 flex-1 flex-col gap-2">
196
196
  <SectionLabel>{t.presenter.speakerNotes}</SectionLabel>
197
- <div className="min-h-0 flex-1 overflow-y-auto rounded-[6px] border border-white/10 bg-black/40 p-3 text-[13.5px] leading-relaxed whitespace-pre-wrap text-white/85">
197
+ <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">
198
198
  {note?.trim() ? (
199
199
  note
200
200
  ) : (
201
- <span className="text-white/40">
201
+ <span className="text-muted-foreground">
202
202
  {t.presenter.noNotesPrefix}
203
- <code className="rounded-[3px] bg-white/10 px-1 py-0.5 font-mono text-[12px]">
203
+ <code className="rounded-[3px] bg-muted px-1 py-0.5 font-mono text-[12px]">
204
204
  export const notes = […]
205
205
  </code>
206
206
  {t.presenter.noNotesSuffix}
@@ -241,7 +241,7 @@ function PresenterTopBar({
241
241
  }) {
242
242
  const t = useLocale();
243
243
  return (
244
- <header className="flex shrink-0 items-center justify-between border-b border-white/10 px-6 py-3">
244
+ <header className="flex h-12 shrink-0 items-center justify-between border-b border-hairline px-6">
245
245
  <div className="flex items-baseline gap-3">
246
246
  <span className="eyebrow text-white/45">{t.presenter.eyebrow}</span>
247
247
  <span className="truncate font-heading text-[14px] font-semibold tracking-tight">
@@ -257,9 +257,9 @@ function PresenterTopBar({
257
257
  <Clock />
258
258
  <ElapsedClock startedAt={startedAt} />
259
259
  <div className="font-mono text-[18px] tabular-nums">
260
- <span className="text-white">{(index + 1).toString().padStart(2, '0')}</span>
261
- <span className="text-white/35"> / </span>
262
- <span className="text-white/55">{total.toString().padStart(2, '0')}</span>
260
+ <span className="text-foreground">{(index + 1).toString().padStart(2, '0')}</span>
261
+ <span className="text-foreground/30"> / </span>
262
+ <span className="text-muted-foreground">{total.toString().padStart(2, '0')}</span>
263
263
  </div>
264
264
  </div>
265
265
  </header>
@@ -285,7 +285,7 @@ function PresenterBottomBar({
285
285
  }) {
286
286
  const t = useLocale();
287
287
  return (
288
- <footer className="flex shrink-0 items-center justify-between gap-3 border-t border-white/10 px-6 py-3">
288
+ <footer className="flex shrink-0 items-center justify-between gap-3 border-t border-hairline px-6 py-3">
289
289
  <div className="flex items-center gap-2">
290
290
  <Button variant="outline" onClick={onPrev} disabled={index === 0}>
291
291
  <ChevronLeft className="size-4" /> {t.presenter.prev}
@@ -352,15 +352,15 @@ function PresenterJumpControl({
352
352
  value={value}
353
353
  onChange={(e) => setValue(e.target.value)}
354
354
  placeholder={(current + 1).toString()}
355
- className="h-8 w-20 rounded-[5px] border border-white/15 bg-black/40 px-2 font-mono text-[12px] tabular-nums outline-none focus-visible:border-white/30"
355
+ 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"
356
356
  />
357
- <span className="font-mono text-[11px] text-white/45">/ {total}</span>
357
+ <span className="font-mono text-[11px] text-muted-foreground">/ {total}</span>
358
358
  </form>
359
359
  );
360
360
  }
361
361
 
362
362
  function SectionLabel({ children }: { children: React.ReactNode }) {
363
- return <span className="eyebrow text-white/45">{children}</span>;
363
+ return <span className="eyebrow">{children}</span>;
364
364
  }
365
365
 
366
366
  function Clock() {
@@ -373,7 +373,7 @@ function Clock() {
373
373
  return (
374
374
  <time
375
375
  title={t.presenter.currentTime}
376
- className="font-mono text-[12px] tabular-nums text-white/55"
376
+ className="font-mono text-[12px] tabular-nums text-muted-foreground"
377
377
  >
378
378
  {now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
379
379
  </time>
@@ -396,7 +396,10 @@ function ElapsedClock({ startedAt }: { startedAt: number }) {
396
396
  ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
397
397
  : `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
398
398
  return (
399
- <time title={t.presenter.elapsed} className="font-mono text-[18px] tabular-nums text-white">
399
+ <time
400
+ title={t.presenter.elapsed}
401
+ className="font-mono text-[18px] tabular-nums text-foreground"
402
+ >
400
403
  {text}
401
404
  </time>
402
405
  );