@open-slide/core 1.5.0 → 1.7.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 (48) hide show
  1. package/dist/{build-DZhbjQpQ.js → build-tLrkKUHr.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-BQdTMho4.d.ts → config-CfMThYN9.d.ts} +1 -1
  4. package/dist/{config-iKjqaX08.js → config-PwUHqZ_X.js} +246 -2
  5. package/dist/{dev-BjLGk5nN.js → dev-DpCIRbhT.js} +1 -1
  6. package/dist/{en-DDGqyNaW.js → en-BDnM5zKJ.js} +4 -0
  7. package/dist/index.d.ts +29 -4
  8. package/dist/index.js +20 -4
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +13 -1
  11. package/dist/{preview-jwLWHWkQ.js → preview-BSGlM6Se.js} +1 -1
  12. package/dist/{types-Dpr8nbih.d.ts → types-B-KrjgX8.d.ts} +5 -0
  13. package/dist/vite/index.d.ts +2 -2
  14. package/dist/vite/index.js +1 -1
  15. package/package.json +1 -1
  16. package/skills/create-theme/SKILL.md +30 -22
  17. package/skills/slide-authoring/SKILL.md +186 -0
  18. package/src/app/components/asset-view.tsx +8 -1
  19. package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
  20. package/src/app/components/inspector/inspect-overlay.tsx +132 -35
  21. package/src/app/components/inspector/inspector-panel.tsx +19 -256
  22. package/src/app/components/inspector/inspector-provider.tsx +102 -1
  23. package/src/app/components/panel/save-card.tsx +4 -4
  24. package/src/app/components/player.tsx +13 -3
  25. package/src/app/components/present/overview-grid.tsx +4 -1
  26. package/src/app/components/slide-transition-layer.tsx +154 -0
  27. package/src/app/components/style-panel/style-panel.tsx +3 -0
  28. package/src/app/components/themes/theme-detail.tsx +7 -2
  29. package/src/app/components/themes/themes-gallery.tsx +4 -1
  30. package/src/app/components/thumbnail-rail.tsx +10 -2
  31. package/src/app/lib/assets.ts +2 -0
  32. package/src/app/lib/export-html.ts +7 -2
  33. package/src/app/lib/export-pdf.ts +34 -2
  34. package/src/app/lib/folders.ts +35 -1
  35. package/src/app/lib/page-context.tsx +38 -0
  36. package/src/app/lib/sdk.ts +3 -1
  37. package/src/app/lib/transition.ts +23 -0
  38. package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
  39. package/src/app/lib/use-wheel-page-navigation.ts +7 -0
  40. package/src/app/routes/home-shell.tsx +13 -2
  41. package/src/app/routes/home.tsx +28 -2
  42. package/src/app/routes/presenter.tsx +7 -2
  43. package/src/app/routes/slide.tsx +19 -8
  44. package/src/locale/en.ts +4 -0
  45. package/src/locale/ja.ts +4 -0
  46. package/src/locale/types.ts +5 -0
  47. package/src/locale/zh-cn.ts +4 -0
  48. package/src/locale/zh-tw.ts +4 -0
@@ -33,6 +33,19 @@ async function patchSlideName(slideId: string, name: string): Promise<void> {
33
33
  if (!res.ok) throw new Error(`PATCH /__slides/${slideId} ${res.status}`);
34
34
  }
35
35
 
36
+ async function duplicateSlideReq(slideId: string, newId?: string): Promise<string> {
37
+ const init: RequestInit = { method: 'POST' };
38
+ if (newId !== undefined) {
39
+ init.headers = { 'content-type': 'application/json' };
40
+ init.body = JSON.stringify({ newId });
41
+ }
42
+ const res = await fetch(`/__slides/${slideId}/duplicate`, init);
43
+ if (!res.ok) throw new Error(`POST /__slides/${slideId}/duplicate ${res.status}`);
44
+ const body = (await res.json()) as { slideId?: unknown };
45
+ if (typeof body.slideId !== 'string') throw new Error('duplicate response missing slideId');
46
+ return body.slideId;
47
+ }
48
+
36
49
  async function deleteSlideReq(slideId: string): Promise<void> {
37
50
  const res = await fetch(`/__slides/${slideId}`, { method: 'DELETE' });
38
51
  if (!res.ok) throw new Error(`DELETE /__slides/${slideId} ${res.status}`);
@@ -83,6 +96,7 @@ export type UseFoldersResult = {
83
96
  remove: (id: string) => Promise<void>;
84
97
  assign: (slideId: string, folderId: string | null) => Promise<void>;
85
98
  renameSlide: (slideId: string, name: string) => Promise<void>;
99
+ duplicateSlide: (slideId: string, newId?: string) => Promise<string>;
86
100
  deleteSlide: (slideId: string) => Promise<void>;
87
101
  refresh: () => Promise<void>;
88
102
  };
@@ -165,6 +179,15 @@ export function useFolders(): UseFoldersResult {
165
179
  [refresh],
166
180
  );
167
181
 
182
+ const duplicateSlide = useCallback(
183
+ async (slideId: string, newId?: string) => {
184
+ const duplicatedId = await duplicateSlideReq(slideId, newId);
185
+ await refresh();
186
+ return duplicatedId;
187
+ },
188
+ [refresh],
189
+ );
190
+
168
191
  const deleteSlide = useCallback(
169
192
  async (slideId: string) => {
170
193
  await deleteSlideReq(slideId);
@@ -173,5 +196,16 @@ export function useFolders(): UseFoldersResult {
173
196
  [refresh],
174
197
  );
175
198
 
176
- return { manifest, loading, create, update, remove, assign, renameSlide, deleteSlide, refresh };
199
+ return {
200
+ manifest,
201
+ loading,
202
+ create,
203
+ update,
204
+ remove,
205
+ assign,
206
+ renameSlide,
207
+ duplicateSlide,
208
+ deleteSlide,
209
+ refresh,
210
+ };
177
211
  }
@@ -0,0 +1,38 @@
1
+ import { type Context, createContext, type PropsWithChildren, useContext, useMemo } from 'react';
2
+
3
+ type SlidePageContextValue = {
4
+ index: number;
5
+ total: number;
6
+ };
7
+
8
+ // Stored on globalThis so dev (src) and published (dist) copies of this module
9
+ // share one context instance — otherwise the provider writes to one context and
10
+ // the hook reads from another, and `useSlidePageNumber` always sees null.
11
+ const GLOBAL_KEY = '__open_slide_page_context__';
12
+ type GlobalWithCtx = typeof globalThis & {
13
+ [GLOBAL_KEY]?: Context<SlidePageContextValue | null>;
14
+ };
15
+ const g = globalThis as GlobalWithCtx;
16
+ if (!g[GLOBAL_KEY]) {
17
+ g[GLOBAL_KEY] = createContext<SlidePageContextValue | null>(null);
18
+ }
19
+ const SlidePageContext = g[GLOBAL_KEY];
20
+
21
+ export function SlidePageProvider({
22
+ index,
23
+ total,
24
+ children,
25
+ }: PropsWithChildren<{ index: number; total: number }>) {
26
+ const value = useMemo(() => ({ index, total }), [index, total]);
27
+ return <SlidePageContext.Provider value={value}>{children}</SlidePageContext.Provider>;
28
+ }
29
+
30
+ export function useSlidePageNumber(): { current: number; total: number } {
31
+ const ctx = useContext(SlidePageContext);
32
+ if (!ctx) {
33
+ throw new Error(
34
+ 'useSlidePageNumber must be called from a slide page rendered by @open-slide/core',
35
+ );
36
+ }
37
+ return { current: ctx.index + 1, total: ctx.total };
38
+ }
@@ -1,7 +1,8 @@
1
1
  import type { ComponentType } from 'react';
2
2
  import type { DesignSystem } from './design.ts';
3
+ import type { SlideTransition } from './transition.ts';
3
4
 
4
- export type Page = ComponentType;
5
+ export type Page = ComponentType & { transition?: SlideTransition };
5
6
 
6
7
  export type SlideMeta = {
7
8
  title?: string;
@@ -16,6 +17,7 @@ export type SlideModule = {
16
17
  design?: DesignSystem;
17
18
  // Index-aligned with `default`.
18
19
  notes?: (string | undefined)[];
20
+ transition?: SlideTransition;
19
21
  };
20
22
 
21
23
  export type FolderIcon = { type: 'emoji'; value: string } | { type: 'color'; value: string };
@@ -0,0 +1,23 @@
1
+ import type { Page } from './sdk';
2
+
3
+ export type TransitionPhase = {
4
+ keyframes: Keyframe[] | PropertyIndexedKeyframes;
5
+ easing?: string;
6
+ duration?: number;
7
+ delay?: number;
8
+ };
9
+
10
+ export type SlideTransition = {
11
+ duration: number;
12
+ easing?: string;
13
+ enter?: TransitionPhase;
14
+ exit?: TransitionPhase;
15
+ };
16
+
17
+ export function resolveTransition(
18
+ pages: Page[],
19
+ index: number,
20
+ moduleDefault?: SlideTransition,
21
+ ): SlideTransition | undefined {
22
+ return pages[index]?.transition ?? moduleDefault;
23
+ }
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const QUERY = '(prefers-reduced-motion: reduce)';
4
+
5
+ export function usePrefersReducedMotion(): boolean {
6
+ const [reduce, setReduce] = useState(() => {
7
+ if (typeof window === 'undefined') return false;
8
+ return window.matchMedia(QUERY).matches;
9
+ });
10
+
11
+ useEffect(() => {
12
+ const mql = window.matchMedia(QUERY);
13
+ const onChange = (e: MediaQueryListEvent) => setReduce(e.matches);
14
+ mql.addEventListener('change', onChange);
15
+ return () => mql.removeEventListener('change', onChange);
16
+ }, []);
17
+
18
+ return reduce;
19
+ }
@@ -31,6 +31,7 @@ export function useWheelPageNavigation<T extends HTMLElement>({
31
31
 
32
32
  const onWheel = (event: WheelEvent) => {
33
33
  if (event.defaultPrevented || event.ctrlKey || shouldIgnoreWheelTarget(event.target)) return;
34
+ if (isVisualViewportZoomed()) return;
34
35
 
35
36
  const deltaY = normalizeDeltaY(event);
36
37
  if (Math.abs(deltaY) <= Math.abs(normalizeDeltaX(event))) return;
@@ -84,6 +85,12 @@ function normalizeWheelDelta(delta: number, deltaMode: number) {
84
85
  return delta;
85
86
  }
86
87
 
88
+ function isVisualViewportZoomed() {
89
+ if (typeof window === 'undefined') return false;
90
+ const vv = window.visualViewport;
91
+ return vv != null && vv.scale > 1.01;
92
+ }
93
+
87
94
  function shouldIgnoreWheelTarget(target: EventTarget | null) {
88
95
  if (!(target instanceof HTMLElement)) return false;
89
96
  return Boolean(
@@ -22,6 +22,7 @@ export type HomeOutletContext = {
22
22
  titleMap: Record<string, string>;
23
23
  assign: (slideId: string, folderId: string | null) => Promise<void>;
24
24
  renameSlide: (slideId: string, name: string) => Promise<void>;
25
+ duplicateSlide: (slideId: string, newId?: string) => Promise<string>;
25
26
  deleteSlide: (slideId: string) => Promise<void>;
26
27
  };
27
28
 
@@ -32,8 +33,17 @@ function pathToSelectedId(pathname: string, search: URLSearchParams): string {
32
33
  }
33
34
 
34
35
  export function HomeShell() {
35
- const { manifest, loading, create, update, remove, assign, renameSlide, deleteSlide } =
36
- useFolders();
36
+ const {
37
+ manifest,
38
+ loading,
39
+ create,
40
+ update,
41
+ remove,
42
+ assign,
43
+ renameSlide,
44
+ duplicateSlide,
45
+ deleteSlide,
46
+ } = useFolders();
37
47
  const navigate = useNavigate();
38
48
  const location = useLocation();
39
49
  const [searchParams] = useSearchParams();
@@ -108,6 +118,7 @@ export function HomeShell() {
108
118
  titleMap,
109
119
  assign,
110
120
  renameSlide,
121
+ duplicateSlide,
111
122
  deleteSlide,
112
123
  };
113
124
 
@@ -2,6 +2,7 @@ import {
2
2
  ArrowDownAZ,
3
3
  ChevronDown,
4
4
  Clock,
5
+ Copy,
5
6
  FolderInput,
6
7
  FolderPlus,
7
8
  MoreHorizontal,
@@ -13,6 +14,7 @@ import {
13
14
  } from 'lucide-react';
14
15
  import { useEffect, useMemo, useRef, useState } from 'react';
15
16
  import { Link, useOutletContext } from 'react-router-dom';
17
+ import { toast } from 'sonner';
16
18
  import { Button } from '@/components/ui/button';
17
19
  import {
18
20
  Dialog,
@@ -28,11 +30,12 @@ import {
28
30
  DropdownMenuItem,
29
31
  DropdownMenuTrigger,
30
32
  } from '@/components/ui/dropdown-menu';
31
- import { useLocale } from '@/lib/use-locale';
33
+ import { format, useLocale } from '@/lib/use-locale';
32
34
  import { cn } from '@/lib/utils';
33
35
  import { FolderIconChip, SLIDE_DND_MIME } from '../components/sidebar/folder-item';
34
36
  import { DRAFT_ID } from '../components/sidebar/sidebar';
35
37
  import { SlideCanvas } from '../components/slide-canvas';
38
+ import { SlidePageProvider } from '../lib/page-context';
36
39
  import type { Folder, FolderIcon, SlideModule } from '../lib/sdk';
37
40
  import { loadSlide, slideCreatedAt } from '../lib/slides';
38
41
  import type { HomeOutletContext } from './home-shell';
@@ -77,6 +80,7 @@ export function Home() {
77
80
  titleMap,
78
81
  assign,
79
82
  renameSlide,
83
+ duplicateSlide,
80
84
  deleteSlide,
81
85
  } = useOutletContext<HomeOutletContext>();
82
86
  const t = useLocale();
@@ -163,6 +167,20 @@ export function Home() {
163
167
  folders={manifest.folders}
164
168
  currentFolderId={manifest.assignments[id] ?? null}
165
169
  onRename={(name) => renameSlide(id, name)}
170
+ onDuplicate={async () => {
171
+ const slideName = titleMap[id] ?? id;
172
+ try {
173
+ const newSlideId = await duplicateSlide(id);
174
+ toast.success(
175
+ format(t.home.toastSlideDuplicated, {
176
+ slide: slideName,
177
+ newSlide: newSlideId,
178
+ }),
179
+ );
180
+ } catch {
181
+ toast.error(t.home.toastSlideDuplicateFailed);
182
+ }
183
+ }}
166
184
  onMove={(folderId) => assign(id, folderId)}
167
185
  onDelete={() => deleteSlide(id)}
168
186
  onTitleResolved={reportTitle}
@@ -381,6 +399,7 @@ function SlideCard({
381
399
  folders,
382
400
  currentFolderId,
383
401
  onRename,
402
+ onDuplicate,
384
403
  onMove,
385
404
  onDelete,
386
405
  onTitleResolved,
@@ -389,6 +408,7 @@ function SlideCard({
389
408
  folders: Folder[];
390
409
  currentFolderId: string | null;
391
410
  onRename: (name: string) => Promise<void> | void;
411
+ onDuplicate: () => Promise<void> | void;
392
412
  onMove: (folderId: string | null) => Promise<void> | void;
393
413
  onDelete: () => Promise<void> | void;
394
414
  onTitleResolved?: (id: string, title: string) => void;
@@ -441,7 +461,9 @@ function SlideCard({
441
461
  {FirstPage ? (
442
462
  <div className="h-full w-full motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.03]">
443
463
  <SlideCanvas flat freezeMotion design={slide?.design}>
444
- <FirstPage />
464
+ <SlidePageProvider index={0} total={slide?.default.length ?? 1}>
465
+ <FirstPage />
466
+ </SlidePageProvider>
445
467
  </SlideCanvas>
446
468
  </div>
447
469
  ) : (
@@ -489,6 +511,10 @@ function SlideCard({
489
511
  <Pencil />
490
512
  {tCard.common.rename}
491
513
  </DropdownMenuItem>
514
+ <DropdownMenuItem onSelect={() => onDuplicate()}>
515
+ <Copy />
516
+ {tCard.home.duplicate}
517
+ </DropdownMenuItem>
492
518
  <DropdownMenuItem onSelect={() => setDialog('move')}>
493
519
  <FolderInput />
494
520
  {tCard.home.moveToFolder}
@@ -9,6 +9,7 @@ import {
9
9
  usePresenterChannel,
10
10
  } from '../components/present/use-presenter-channel';
11
11
  import { SlideCanvas } from '../components/slide-canvas';
12
+ import { SlidePageProvider } from '../lib/page-context';
12
13
  import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
13
14
  import { useSlideModule } from '../lib/use-slide-module';
14
15
 
@@ -138,7 +139,9 @@ export function Presenter() {
138
139
  <SectionLabel>{t.presenter.nowShowing}</SectionLabel>
139
140
  <div className="relative min-h-0 flex-1 overflow-hidden rounded-[8px] bg-black ring-1 ring-border">
140
141
  <SlideCanvas flat design={slide.design}>
141
- <CurrentPage />
142
+ <SlidePageProvider index={index} total={total}>
143
+ <CurrentPage />
144
+ </SlidePageProvider>
142
145
  </SlideCanvas>
143
146
  {blackout && (
144
147
  <div
@@ -164,7 +167,9 @@ export function Presenter() {
164
167
  >
165
168
  {NextPage ? (
166
169
  <SlideCanvas flat freezeMotion design={slide.design}>
167
- <NextPage />
170
+ <SlidePageProvider index={nextIndex} total={total}>
171
+ <NextPage />
172
+ </SlidePageProvider>
168
173
  </SlideCanvas>
169
174
  ) : (
170
175
  <div className="grid h-full place-items-center text-[11.5px] text-muted-foreground">
@@ -48,11 +48,13 @@ import { NotesDrawer } from '../components/notes-drawer';
48
48
  import { PdfProgressToast } from '../components/pdf-progress-toast';
49
49
  import { openPresenterWindow, Player } from '../components/player';
50
50
  import { SlideCanvas } from '../components/slide-canvas';
51
+ import { SlideTransitionLayer } from '../components/slide-transition-layer';
51
52
  import { type ThumbnailActions, ThumbnailRail } from '../components/thumbnail-rail';
52
53
  import { exportSlideAsHtml } from '../lib/export-html';
53
54
  import { exportSlideAsPdf, isSafari } from '../lib/export-pdf';
54
55
  import { remapNotesSessionCacheAfterReorder } from '../lib/inspector/use-notes';
55
56
  import type { SlideModule } from '../lib/sdk';
57
+ import { usePrefersReducedMotion } from '../lib/use-prefers-reduced-motion';
56
58
  import { useSlideModule } from '../lib/use-slide-module';
57
59
 
58
60
  const { showSlideUi, showSlideBrowser, allowHtmlDownload } = config.build;
@@ -75,6 +77,7 @@ export function Slide() {
75
77
  const { renameSlide } = useFolders();
76
78
  const slideViewportRef = useRef<HTMLElement>(null);
77
79
  const t = useLocale();
80
+ const prefersReducedMotion = usePrefersReducedMotion();
78
81
 
79
82
  const modulePages = useMemo(() => slide?.default ?? [], [slide]);
80
83
  const [pages, setPages] = useState<typeof modulePages>(modulePages);
@@ -236,6 +239,8 @@ export function Slide() {
236
239
  goTo(index - 1);
237
240
  } else if (e.key === 'f' || e.key === 'F') {
238
241
  setPlayMode('fullscreen');
242
+ } else if (import.meta.env.DEV && (e.key === 'd' || e.key === 'D')) {
243
+ setDesignOpen((v) => !v);
239
244
  }
240
245
  };
241
246
  window.addEventListener('keydown', onKey);
@@ -324,6 +329,7 @@ export function Slide() {
324
329
  <Player
325
330
  pages={pages}
326
331
  design={slide.design}
332
+ transition={slide.transition}
327
333
  index={index}
328
334
  onIndexChange={goTo}
329
335
  onExit={() => setPlayMode(null)}
@@ -334,17 +340,16 @@ export function Slide() {
334
340
  );
335
341
  }
336
342
 
337
- const CurrentPage = pages[index];
338
343
  const title = slide.meta?.title ?? slideId;
339
344
 
340
345
  return (
341
346
  <HistoryProvider>
342
- <InspectorProvider slideId={slideId}>
347
+ <InspectorProvider slideId={slideId} pageIndex={index}>
343
348
  <SelectionReporter />
344
349
  <div className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
345
350
  {/* Editorial toolbar — three zones, hairline separators, mono-folio center */}
346
- <header className="relative flex h-12 shrink-0 items-center justify-between border-b border-hairline bg-sidebar/85 px-2 backdrop-blur-md md:px-3">
347
- <div className="flex items-center gap-1.5 md:gap-2">
351
+ <header className="flex h-12 shrink-0 items-center gap-2 border-b border-hairline bg-sidebar/85 px-2 backdrop-blur-md md:px-3">
352
+ <div className="flex shrink-0 items-center gap-1.5 md:gap-2">
348
353
  {showSlideBrowser && (
349
354
  <Button asChild variant="ghost" size="icon-sm" title={t.slide.home}>
350
355
  <Link to="/" aria-label={t.slide.backToHome}>
@@ -378,13 +383,13 @@ export function Slide() {
378
383
  </div>
379
384
 
380
385
  {/* Centered title — the rail and mobile pill carry the page count. */}
381
- <div className="pointer-events-none absolute inset-x-0 top-1/2 flex -translate-y-1/2 justify-center px-2">
382
- <div className="pointer-events-auto min-w-0 max-w-[min(34rem,calc(100vw-22rem))]">
386
+ <div className="flex min-w-0 flex-1 justify-center px-2">
387
+ <div className="min-w-0 max-w-[34rem]">
383
388
  <InlineTitleEditor title={title} onSubmit={(next) => renameSlide(slideId, next)} />
384
389
  </div>
385
390
  </div>
386
391
 
387
- <div className="flex items-center gap-1">
392
+ <div className="flex shrink-0 items-center gap-1">
388
393
  {view === 'slides' && (
389
394
  <button
390
395
  type="button"
@@ -584,7 +589,13 @@ export function Slide() {
584
589
  canNext={index < pageCount - 1}
585
590
  />
586
591
  <SlideCanvas design={slide.design}>
587
- <CurrentPage />
592
+ <SlideTransitionLayer
593
+ pages={pages}
594
+ index={index}
595
+ total={pageCount}
596
+ moduleTransition={slide.transition}
597
+ disabled={prefersReducedMotion}
598
+ />
588
599
  </SlideCanvas>
589
600
  <ClickNavZones
590
601
  onPrev={() => goTo(index - 1)}
package/src/locale/en.ts CHANGED
@@ -38,6 +38,7 @@ export const en: Locale = {
38
38
  home: {
39
39
  appTitle: 'open-slide',
40
40
  draft: 'Draft',
41
+ duplicate: 'Duplicate',
41
42
  themes: 'Themes',
42
43
  assets: 'Assets',
43
44
  folders: 'Folders',
@@ -79,6 +80,8 @@ export const en: Locale = {
79
80
  deleteDialogDescriptionSuffix: 'This action cannot be undone.',
80
81
  toastFolderCreated: 'Created folder “{name}”',
81
82
  toastFolderCreateFailed: 'Failed to create folder',
83
+ toastSlideDuplicated: 'Duplicated “{slide}” as {newSlide}',
84
+ toastSlideDuplicateFailed: 'Could not duplicate slide',
82
85
  toastSlideMoved: 'Moved “{slide}” to {folder}',
83
86
  toastSlideMoveFailed: 'Failed to move slide',
84
87
  toastFolderDeleted: 'Deleted folder “{name}”',
@@ -270,6 +273,7 @@ export const en: Locale = {
270
273
  scopeSlide: 'This slide',
271
274
  scopeGlobal: 'Global',
272
275
  fileCount: { one: '{count} file', other: '{count} files' },
276
+ usageUnused: 'Unused',
273
277
  searchLogos: 'Search logos',
274
278
  upload: 'Upload',
275
279
  dropToUpload: 'Drop to upload',
package/src/locale/ja.ts CHANGED
@@ -38,6 +38,7 @@ export const ja: Locale = {
38
38
  home: {
39
39
  appTitle: 'open-slide',
40
40
  draft: '下書き',
41
+ duplicate: '複製',
41
42
  themes: 'テーマ',
42
43
  assets: 'アセット',
43
44
  folders: 'フォルダ',
@@ -79,6 +80,8 @@ export const ja: Locale = {
79
80
  deleteDialogDescriptionSuffix: 'この操作は元に戻せません。',
80
81
  toastFolderCreated: 'フォルダ「{name}」を作成しました',
81
82
  toastFolderCreateFailed: 'フォルダの作成に失敗しました',
83
+ toastSlideDuplicated: '「{slide}」を {newSlide} として複製しました',
84
+ toastSlideDuplicateFailed: 'スライドを複製できませんでした',
82
85
  toastSlideMoved: '「{slide}」を {folder} に移動しました',
83
86
  toastSlideMoveFailed: 'スライドの移動に失敗しました',
84
87
  toastFolderDeleted: 'フォルダ「{name}」を削除しました',
@@ -272,6 +275,7 @@ export const ja: Locale = {
272
275
  scopeSlide: 'このスライド',
273
276
  scopeGlobal: 'グローバル',
274
277
  fileCount: { one: 'ファイル {count} 件', other: 'ファイル {count} 件' },
278
+ usageUnused: '未使用',
275
279
  searchLogos: 'ロゴを検索',
276
280
  upload: 'アップロード',
277
281
  dropToUpload: 'ドロップでアップロード',
@@ -38,6 +38,7 @@ export type Locale = {
38
38
  home: {
39
39
  appTitle: string;
40
40
  draft: string;
41
+ duplicate: string;
41
42
  themes: string;
42
43
  assets: string;
43
44
  folders: string;
@@ -80,6 +81,9 @@ export type Locale = {
80
81
  /** template: "Created folder “{name}”" */
81
82
  toastFolderCreated: string;
82
83
  toastFolderCreateFailed: string;
84
+ /** template: "Duplicated “{slide}” as {newSlide}" */
85
+ toastSlideDuplicated: string;
86
+ toastSlideDuplicateFailed: string;
83
87
  /** template: "Moved “{slide}” to {folder}" */
84
88
  toastSlideMoved: string;
85
89
  toastSlideMoveFailed: string;
@@ -271,6 +275,7 @@ export type Locale = {
271
275
  scopeGlobal: string;
272
276
  /** templates: "{count} file" / "{count} files" */
273
277
  fileCount: Plural;
278
+ usageUnused: string;
274
279
  searchLogos: string;
275
280
  upload: string;
276
281
  dropToUpload: string;
@@ -38,6 +38,7 @@ export const zhCN: Locale = {
38
38
  home: {
39
39
  appTitle: 'open-slide',
40
40
  draft: '草稿',
41
+ duplicate: '复制',
41
42
  themes: '主题',
42
43
  assets: '素材',
43
44
  folders: '文件夹',
@@ -79,6 +80,8 @@ export const zhCN: Locale = {
79
80
  deleteDialogDescriptionSuffix: '此操作无法撤销。',
80
81
  toastFolderCreated: '已创建文件夹"{name}"',
81
82
  toastFolderCreateFailed: '创建文件夹失败',
83
+ toastSlideDuplicated: '已将"{slide}"复制为 {newSlide}',
84
+ toastSlideDuplicateFailed: '无法复制幻灯片',
82
85
  toastSlideMoved: '已将"{slide}"移至 {folder}',
83
86
  toastSlideMoveFailed: '移动幻灯片失败',
84
87
  toastFolderDeleted: '已删除文件夹"{name}"',
@@ -269,6 +272,7 @@ export const zhCN: Locale = {
269
272
  scopeSlide: '当前幻灯片',
270
273
  scopeGlobal: '全局',
271
274
  fileCount: { one: '{count} 个文件', other: '{count} 个文件' },
275
+ usageUnused: '未使用',
272
276
  searchLogos: '搜索 Logo',
273
277
  upload: '上传',
274
278
  dropToUpload: '拖入即可上传',
@@ -38,6 +38,7 @@ export const zhTW: Locale = {
38
38
  home: {
39
39
  appTitle: 'open-slide',
40
40
  draft: '草稿',
41
+ duplicate: '複製',
41
42
  themes: '主題',
42
43
  assets: '素材',
43
44
  folders: '資料夾',
@@ -79,6 +80,8 @@ export const zhTW: Locale = {
79
80
  deleteDialogDescriptionSuffix: '此操作無法復原。',
80
81
  toastFolderCreated: '已建立資料夾「{name}」',
81
82
  toastFolderCreateFailed: '建立資料夾失敗',
83
+ toastSlideDuplicated: '已將「{slide}」複製為 {newSlide}',
84
+ toastSlideDuplicateFailed: '無法複製投影片',
82
85
  toastSlideMoved: '已將「{slide}」移至 {folder}',
83
86
  toastSlideMoveFailed: '移動投影片失敗',
84
87
  toastFolderDeleted: '已刪除資料夾「{name}」',
@@ -269,6 +272,7 @@ export const zhTW: Locale = {
269
272
  scopeSlide: '此投影片',
270
273
  scopeGlobal: '全域',
271
274
  fileCount: { one: '{count} 個檔案', other: '{count} 個檔案' },
275
+ usageUnused: '未使用',
272
276
  searchLogos: '搜尋 Logo',
273
277
  upload: '上傳',
274
278
  dropToUpload: '拖入即可上傳',