@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.
- package/dist/{build-4wOJF1l4.js → build-6BeQ3cxb.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-evLWCV1-.js → config-AxZ5OE1u.js} +772 -201
- package/dist/{config-D2y1AXaN.d.ts → config-CtT8K4VF.d.ts} +1 -1
- package/dist/{dev-BUr0S-Ij.js → dev-C9eLmUEq.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +136 -24
- package/dist/{preview-DP_gIphz.js → preview-Cunm-f4i.js} +1 -1
- package/dist/{types-BVvl_xup.d.ts → types-CRHIeoNq.d.ts} +37 -4
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +5 -1
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +48 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +17 -2
- package/src/app/components/inspector/inspector-panel.tsx +90 -26
- package/src/app/components/inspector/inspector-provider.tsx +136 -1
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +26 -8
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/present/use-idle.ts +6 -4
- package/src/app/components/style-panel/design-provider.tsx +13 -0
- package/src/app/components/style-panel/style-panel.tsx +23 -11
- package/src/app/components/thumbnail-rail.tsx +317 -55
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/routes/home.tsx +34 -12
- package/src/app/routes/presenter.tsx +27 -24
- package/src/app/routes/slide.tsx +238 -51
- package/src/locale/en.ts +35 -4
- package/src/locale/ja.ts +35 -4
- package/src/locale/types.ts +38 -4
- package/src/locale/zh-cn.ts +35 -4
- 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
|
+
}
|
package/src/app/routes/home.tsx
CHANGED
|
@@ -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 } =
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
{
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-[
|
|
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-
|
|
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-
|
|
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-[
|
|
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-
|
|
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-
|
|
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-
|
|
201
|
+
<span className="text-muted-foreground">
|
|
202
202
|
{t.presenter.noNotesPrefix}
|
|
203
|
-
<code className="rounded-[3px] bg-
|
|
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-
|
|
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-
|
|
261
|
-
<span className="text-
|
|
262
|
-
<span className="text-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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
|
);
|