@open-slide/core 1.0.5 → 1.1.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-CoON6kTb.js → build-DSqSio-T.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-D2y1AXaN.d.ts → config-C7vMYzFD.d.ts} +1 -1
- package/dist/{config-Bxtztw-H.js → config-KdiYeWtK.js} +114 -1
- package/dist/{dev-IezNC17X.js → dev-B_GVbr11.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +40 -4
- package/dist/{preview-BwYjtENY.js → preview-D_mxhj7w.js} +1 -1
- package/dist/{types-BVvl_xup.d.ts → types-DYgVpIGo.d.ts} +9 -0
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +5 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +168 -0
- package/src/app/components/inspector/inspect-overlay.tsx +96 -19
- package/src/app/components/inspector/inspector-panel.tsx +46 -13
- package/src/app/components/inspector/inspector-provider.tsx +83 -1
- package/src/app/components/inspector/save-bar.tsx +0 -3
- package/src/app/components/player.tsx +22 -26
- package/src/app/components/present/overview-grid.tsx +0 -5
- package/src/app/components/present/use-idle.ts +6 -4
- package/src/app/components/present/use-presenter-channel.ts +3 -10
- package/src/app/components/sidebar/folder-item.tsx +0 -2
- package/src/app/components/sidebar/icon-picker.tsx +0 -3
- package/src/app/components/slide-canvas.tsx +1 -10
- package/src/app/components/style-panel/design-provider.tsx +15 -6
- package/src/app/components/style-panel/style-panel.tsx +23 -11
- package/src/app/components/thumbnail-rail.tsx +220 -53
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/export-html.ts +1 -9
- package/src/app/lib/export-pdf.ts +0 -5
- package/src/app/lib/print-ready.ts +0 -4
- package/src/app/lib/sdk.ts +1 -2
- package/src/app/routes/presenter.tsx +27 -24
- package/src/app/routes/slide.tsx +53 -1
- package/src/locale/en.ts +9 -0
- package/src/locale/ja.ts +9 -0
- package/src/locale/types.ts +9 -0
- package/src/locale/zh-cn.ts +9 -0
- package/src/locale/zh-tw.ts +9 -0
|
@@ -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
|
);
|
package/src/app/routes/slide.tsx
CHANGED
|
@@ -68,12 +68,63 @@ export function Slide() {
|
|
|
68
68
|
};
|
|
69
69
|
}, [slideId]);
|
|
70
70
|
|
|
71
|
-
const
|
|
71
|
+
const modulePages = useMemo(() => slide?.default ?? [], [slide]);
|
|
72
|
+
const [pages, setPages] = useState<typeof modulePages>(modulePages);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setPages(modulePages);
|
|
75
|
+
}, [modulePages]);
|
|
72
76
|
const pageCount = pages.length;
|
|
73
77
|
const rawIndex = Number(searchParams.get('p') ?? '1') - 1;
|
|
74
78
|
const index = Number.isFinite(rawIndex) ? Math.max(0, Math.min(pageCount - 1, rawIndex)) : 0;
|
|
75
79
|
const view = searchParams.get('view') === 'assets' ? 'assets' : 'slides';
|
|
76
80
|
|
|
81
|
+
const reorderPage = useCallback(
|
|
82
|
+
async (from: number, to: number) => {
|
|
83
|
+
if (from === to) return;
|
|
84
|
+
const before = pages;
|
|
85
|
+
const nextPages = [...before];
|
|
86
|
+
const [moved] = nextPages.splice(from, 1);
|
|
87
|
+
nextPages.splice(to, 0, moved);
|
|
88
|
+
setPages(nextPages);
|
|
89
|
+
|
|
90
|
+
const order = before.map((_, i) => i);
|
|
91
|
+
const [movedIdx] = order.splice(from, 1);
|
|
92
|
+
order.splice(to, 0, movedIdx);
|
|
93
|
+
|
|
94
|
+
// Keep the user looking at the same page they were on before the drag.
|
|
95
|
+
let nextIndex = index;
|
|
96
|
+
if (index === from) nextIndex = to;
|
|
97
|
+
else if (from < index && to >= index) nextIndex = index - 1;
|
|
98
|
+
else if (from > index && to <= index) nextIndex = index + 1;
|
|
99
|
+
if (nextIndex !== index) {
|
|
100
|
+
setSearchParams(
|
|
101
|
+
(prev) => {
|
|
102
|
+
const params = new URLSearchParams(prev);
|
|
103
|
+
params.set('p', String(nextIndex + 1));
|
|
104
|
+
return params;
|
|
105
|
+
},
|
|
106
|
+
{ replace: true },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`/__slides/${encodeURIComponent(slideId)}/reorder`, {
|
|
112
|
+
method: 'PUT',
|
|
113
|
+
headers: { 'content-type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({ order }),
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
const detail = await res.json().catch(() => ({ error: res.statusText }));
|
|
118
|
+
throw new Error(detail.error ?? `HTTP ${res.status}`);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
setPages(before);
|
|
122
|
+
toast.error(`Reorder failed: ${String((err as Error).message ?? err)}`);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[pages, index, slideId, setSearchParams],
|
|
126
|
+
);
|
|
127
|
+
|
|
77
128
|
const goTo = useCallback(
|
|
78
129
|
(i: number) => {
|
|
79
130
|
const clamped = Math.max(0, Math.min(pageCount - 1, i));
|
|
@@ -356,6 +407,7 @@ export function Slide() {
|
|
|
356
407
|
design={slide.design}
|
|
357
408
|
current={index}
|
|
358
409
|
onSelect={goTo}
|
|
410
|
+
onReorder={import.meta.env.DEV ? reorderPage : undefined}
|
|
359
411
|
/>
|
|
360
412
|
</div>
|
|
361
413
|
<main
|
package/src/locale/en.ts
CHANGED
|
@@ -185,6 +185,13 @@ export const en: Locale = {
|
|
|
185
185
|
pickerLoading: 'Loading…',
|
|
186
186
|
pickerEmpty: "No images in this slide's assets folder yet. Add some from the Assets tab.",
|
|
187
187
|
placeholderHintLabel: 'Hint:',
|
|
188
|
+
crop: 'Crop…',
|
|
189
|
+
cropDialogTitle: 'Crop image',
|
|
190
|
+
cropDialogDescription: 'Drag the frame to choose what stays visible.',
|
|
191
|
+
cropFitCover: 'Fill',
|
|
192
|
+
cropFitContain: 'Fit',
|
|
193
|
+
cropApply: 'Apply',
|
|
194
|
+
cropResetAria: 'Reset crop',
|
|
188
195
|
noteForAgent: 'Note for the agent',
|
|
189
196
|
noteAgentPlaceholder: 'Describe a change for the agent…',
|
|
190
197
|
noteShortcutHint: '⌘↵ to send',
|
|
@@ -224,6 +231,8 @@ export const en: Locale = {
|
|
|
224
231
|
designToggle: 'Design',
|
|
225
232
|
designToggleTitle: 'Design tokens',
|
|
226
233
|
fontPresetCustom: 'Custom…',
|
|
234
|
+
shuffleAria: 'Shuffle design',
|
|
235
|
+
shuffleTitle: 'Shuffle for inspiration',
|
|
227
236
|
},
|
|
228
237
|
|
|
229
238
|
asset: {
|
package/src/locale/ja.ts
CHANGED
|
@@ -186,6 +186,13 @@ export const ja: Locale = {
|
|
|
186
186
|
pickerEmpty:
|
|
187
187
|
'このスライドのアセットフォルダにまだ画像がありません。「アセット」タブから追加してください。',
|
|
188
188
|
placeholderHintLabel: 'ヒント:',
|
|
189
|
+
crop: 'トリミング…',
|
|
190
|
+
cropDialogTitle: '画像をトリミング',
|
|
191
|
+
cropDialogDescription: '枠をドラッグして表示する範囲を選択します。',
|
|
192
|
+
cropFitCover: '塗りつぶす',
|
|
193
|
+
cropFitContain: '全体表示',
|
|
194
|
+
cropApply: '適用',
|
|
195
|
+
cropResetAria: 'トリミングをリセット',
|
|
189
196
|
noteForAgent: 'エージェントへのメモ',
|
|
190
197
|
noteAgentPlaceholder: 'エージェントに依頼する変更を記述…',
|
|
191
198
|
noteShortcutHint: '⌘↵ で送信',
|
|
@@ -226,6 +233,8 @@ export const ja: Locale = {
|
|
|
226
233
|
designToggle: 'デザイン',
|
|
227
234
|
designToggleTitle: 'デザイントークン',
|
|
228
235
|
fontPresetCustom: 'カスタム…',
|
|
236
|
+
shuffleAria: 'デザインをシャッフル',
|
|
237
|
+
shuffleTitle: 'シャッフルしてインスピレーションを得る',
|
|
229
238
|
},
|
|
230
239
|
|
|
231
240
|
asset: {
|
package/src/locale/types.ts
CHANGED
|
@@ -191,6 +191,13 @@ export type Locale = {
|
|
|
191
191
|
pickerLoading: string;
|
|
192
192
|
pickerEmpty: string;
|
|
193
193
|
placeholderHintLabel: string;
|
|
194
|
+
crop: string;
|
|
195
|
+
cropDialogTitle: string;
|
|
196
|
+
cropDialogDescription: string;
|
|
197
|
+
cropFitCover: string;
|
|
198
|
+
cropFitContain: string;
|
|
199
|
+
cropApply: string;
|
|
200
|
+
cropResetAria: string;
|
|
194
201
|
noteForAgent: string;
|
|
195
202
|
noteAgentPlaceholder: string;
|
|
196
203
|
noteShortcutHint: string;
|
|
@@ -228,6 +235,8 @@ export type Locale = {
|
|
|
228
235
|
designToggle: string;
|
|
229
236
|
designToggleTitle: string;
|
|
230
237
|
fontPresetCustom: string;
|
|
238
|
+
shuffleAria: string;
|
|
239
|
+
shuffleTitle: string;
|
|
231
240
|
};
|
|
232
241
|
|
|
233
242
|
asset: {
|
package/src/locale/zh-cn.ts
CHANGED
|
@@ -185,6 +185,13 @@ export const zhCN: Locale = {
|
|
|
185
185
|
pickerLoading: '加载中…',
|
|
186
186
|
pickerEmpty: '该幻灯片的素材文件夹中尚无图片。请从「素材」标签页添加。',
|
|
187
187
|
placeholderHintLabel: '提示:',
|
|
188
|
+
crop: '裁剪…',
|
|
189
|
+
cropDialogTitle: '裁剪图片',
|
|
190
|
+
cropDialogDescription: '拖动框线决定要保留的可见区域。',
|
|
191
|
+
cropFitCover: '填满',
|
|
192
|
+
cropFitContain: '完整显示',
|
|
193
|
+
cropApply: '应用',
|
|
194
|
+
cropResetAria: '重置裁剪',
|
|
188
195
|
noteForAgent: '给代理的备注',
|
|
189
196
|
noteAgentPlaceholder: '描述你希望代理执行的更改…',
|
|
190
197
|
noteShortcutHint: '⌘↵ 发送',
|
|
@@ -224,6 +231,8 @@ export const zhCN: Locale = {
|
|
|
224
231
|
designToggle: '设计',
|
|
225
232
|
designToggleTitle: '设计样式',
|
|
226
233
|
fontPresetCustom: '自定义…',
|
|
234
|
+
shuffleAria: '随机设计',
|
|
235
|
+
shuffleTitle: '随机配色获取灵感',
|
|
227
236
|
},
|
|
228
237
|
|
|
229
238
|
asset: {
|
package/src/locale/zh-tw.ts
CHANGED
|
@@ -185,6 +185,13 @@ export const zhTW: Locale = {
|
|
|
185
185
|
pickerLoading: '載入中…',
|
|
186
186
|
pickerEmpty: '此投影片的素材資料夾尚未有圖片。請從「素材」分頁加入。',
|
|
187
187
|
placeholderHintLabel: '提示:',
|
|
188
|
+
crop: '裁切…',
|
|
189
|
+
cropDialogTitle: '裁切圖片',
|
|
190
|
+
cropDialogDescription: '拖曳框線決定要保留的可見範圍。',
|
|
191
|
+
cropFitCover: '填滿',
|
|
192
|
+
cropFitContain: '完整顯示',
|
|
193
|
+
cropApply: '套用',
|
|
194
|
+
cropResetAria: '重設裁切',
|
|
188
195
|
noteForAgent: '給代理的備註',
|
|
189
196
|
noteAgentPlaceholder: '描述你希望代理進行的修改…',
|
|
190
197
|
noteShortcutHint: '⌘↵ 送出',
|
|
@@ -224,6 +231,8 @@ export const zhTW: Locale = {
|
|
|
224
231
|
designToggle: '設計',
|
|
225
232
|
designToggleTitle: '設計樣式',
|
|
226
233
|
fontPresetCustom: '自訂…',
|
|
234
|
+
shuffleAria: '隨機設計',
|
|
235
|
+
shuffleTitle: '隨機配色獲取靈感',
|
|
227
236
|
},
|
|
228
237
|
|
|
229
238
|
asset: {
|