@open-slide/core 1.0.6 → 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.
@@ -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
  );
@@ -68,12 +68,63 @@ export function Slide() {
68
68
  };
69
69
  }, [slideId]);
70
70
 
71
- const pages = useMemo(() => slide?.default ?? [], [slide]);
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: {
@@ -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: {
@@ -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: {
@@ -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: {