@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
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-iKjqaX08.js";
2
+ import { createViteConfig } from "./config-PwUHqZ_X.js";
3
3
  import path from "node:path";
4
4
  import { build as build$1, mergeConfig } from "vite";
5
5
 
package/dist/cli/bin.js CHANGED
@@ -57,15 +57,15 @@ async function run(argv) {
57
57
  program.name("open-slide").description("Author slides — we handle the Vite/React stack.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `open-slide --help` for usage)"));
58
58
  program.command("dev").description("Start the dev server").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").option("--no-skills-check", "skip the built-in skills drift check").action(async (flags) => {
59
59
  if (flags.skillsCheck !== false) await runSkillsDriftCheck(resolveBuiltinSkillsDir());
60
- const { dev } = await import("../dev-BjLGk5nN.js");
60
+ const { dev } = await import("../dev-DpCIRbhT.js");
61
61
  await dev(flags);
62
62
  });
63
63
  program.command("build").description("Build a static site").option("--out-dir <dir>", "output directory (defaults to `dist`)").action(async (flags) => {
64
- const { build } = await import("../build-DZhbjQpQ.js");
64
+ const { build } = await import("../build-tLrkKUHr.js");
65
65
  await build(flags);
66
66
  });
67
67
  program.command("preview").description("Preview the production build").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").action(async (flags) => {
68
- const { preview } = await import("../preview-jwLWHWkQ.js");
68
+ const { preview } = await import("../preview-BSGlM6Se.js");
69
69
  await preview(flags);
70
70
  });
71
71
  program.command("sync:skills").description("Sync built-in skills from @open-slide/core into this workspace").option("--dry-run", "show what would change without writing").action(async (flags) => {
@@ -1,4 +1,4 @@
1
- import { Locale } from "./types-Dpr8nbih.js";
1
+ import { Locale } from "./types-B-KrjgX8.js";
2
2
 
3
3
  //#region src/config.d.ts
4
4
  type OpenSlideBuildConfig = {
@@ -1150,6 +1150,31 @@ function findAssetUsages(source, assetPath) {
1150
1150
  if (!target?.defaultIdent) return 0;
1151
1151
  return collectImgSrcUses(ast, target.defaultIdent).length;
1152
1152
  }
1153
+ function findReferencedAssets(source, assetPaths) {
1154
+ const referenced = new Set();
1155
+ if (assetPaths.length === 0) return referenced;
1156
+ const ast = parseSource$2(source);
1157
+ if (!ast) return referenced;
1158
+ const wanted = new Set(assetPaths);
1159
+ const identToPath = new Map();
1160
+ for (const imp of findImports$1(ast)) {
1161
+ if (!imp.defaultIdent) continue;
1162
+ if (wanted.has(imp.source)) identToPath.set(imp.defaultIdent, imp.source);
1163
+ }
1164
+ if (identToPath.size === 0) return referenced;
1165
+ walkJsx(ast, (n) => {
1166
+ if (!t$3.isJSXElement(n)) return;
1167
+ const opening = n.openingElement;
1168
+ if (!t$3.isJSXIdentifier(opening.name) || opening.name.name !== "img") return;
1169
+ const src = findJsxAttr(opening, "src");
1170
+ if (!src?.value || !t$3.isJSXExpressionContainer(src.value)) return;
1171
+ const expr = src.value.expression;
1172
+ if (!t$3.isIdentifier(expr)) return;
1173
+ const p = identToPath.get(expr.name);
1174
+ if (p) referenced.add(p);
1175
+ });
1176
+ return referenced;
1177
+ }
1153
1178
  function applyRevertAsset(source, assetPath) {
1154
1179
  const ast = parseSource$2(source);
1155
1180
  if (!ast) return {
@@ -1227,6 +1252,62 @@ function validateSlideName(v) {
1227
1252
  if (trimmed.length < 1 || trimmed.length > 80) return null;
1228
1253
  return trimmed;
1229
1254
  }
1255
+ function unwrapExpression(node) {
1256
+ let current = node;
1257
+ while (current && (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression")) current = current.expression;
1258
+ return current;
1259
+ }
1260
+ function readMetaTitleInSource(source) {
1261
+ let ast;
1262
+ try {
1263
+ ast = parse(source, {
1264
+ sourceType: "module",
1265
+ plugins: ["typescript", "jsx"],
1266
+ errorRecovery: true
1267
+ });
1268
+ } catch {
1269
+ return { kind: "unsupported" };
1270
+ }
1271
+ const body = ast.program?.body ?? [];
1272
+ for (const stmt of body) {
1273
+ if (stmt.type !== "ExportNamedDeclaration") continue;
1274
+ const decl = stmt.declaration;
1275
+ if (!decl || decl.type !== "VariableDeclaration") continue;
1276
+ const declarations = decl.declarations ?? [];
1277
+ for (const d of declarations) {
1278
+ const id = d.id;
1279
+ if (!id || id.type !== "Identifier" || id.name !== "meta") continue;
1280
+ const init = unwrapExpression(d.init);
1281
+ if (!init || init.type !== "ObjectExpression") return { kind: "unsupported" };
1282
+ const properties = init.properties ?? [];
1283
+ for (const property of properties) {
1284
+ if (property.type !== "ObjectProperty" || property.computed) continue;
1285
+ const key = property.key;
1286
+ const keyName = key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? key.value : void 0;
1287
+ if (keyName !== "title") continue;
1288
+ const value = property.value;
1289
+ if (value?.type === "StringLiteral" && typeof value.value === "string") return {
1290
+ kind: "found",
1291
+ title: value.value
1292
+ };
1293
+ if (value?.type === "TemplateLiteral") {
1294
+ const expressions = value.expressions ?? [];
1295
+ const quasis = value.quasis ?? [];
1296
+ const firstValue = quasis[0]?.value;
1297
+ const cooked = firstValue?.cooked;
1298
+ const raw = firstValue?.raw;
1299
+ if (expressions.length === 0 && typeof (cooked ?? raw) === "string") return {
1300
+ kind: "found",
1301
+ title: cooked ?? raw
1302
+ };
1303
+ }
1304
+ return { kind: "unsupported" };
1305
+ }
1306
+ return { kind: "missing" };
1307
+ }
1308
+ }
1309
+ return { kind: "missing" };
1310
+ }
1230
1311
  async function rmSlideDir(slidesRoot, slideId) {
1231
1312
  if (!SLIDE_ID_RE$3.test(slideId)) return false;
1232
1313
  const dir = path.resolve(slidesRoot, slideId);
@@ -1241,6 +1322,117 @@ async function rmSlideDir(slidesRoot, slideId) {
1241
1322
  return false;
1242
1323
  }
1243
1324
  }
1325
+ async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
1326
+ if (!SLIDE_ID_RE$3.test(slideId)) return {
1327
+ ok: false,
1328
+ status: 400,
1329
+ error: "invalid slideId"
1330
+ };
1331
+ const root = path.resolve(slidesRoot);
1332
+ const srcDir = path.resolve(root, slideId);
1333
+ if (!srcDir.startsWith(root + path.sep)) return {
1334
+ ok: false,
1335
+ status: 400,
1336
+ error: "invalid slideId"
1337
+ };
1338
+ try {
1339
+ await fs.access(path.join(srcDir, "index.tsx"));
1340
+ } catch {
1341
+ return {
1342
+ ok: false,
1343
+ status: 404,
1344
+ error: "slide not found"
1345
+ };
1346
+ }
1347
+ let newId;
1348
+ if (desiredId !== void 0) {
1349
+ if (!SLIDE_ID_RE$3.test(desiredId)) return {
1350
+ ok: false,
1351
+ status: 400,
1352
+ error: "invalid newId"
1353
+ };
1354
+ newId = desiredId;
1355
+ const dstDir$1 = path.resolve(root, newId);
1356
+ if (!dstDir$1.startsWith(root + path.sep)) return {
1357
+ ok: false,
1358
+ status: 400,
1359
+ error: "invalid newId"
1360
+ };
1361
+ try {
1362
+ await fs.access(dstDir$1);
1363
+ return {
1364
+ ok: false,
1365
+ status: 409,
1366
+ error: "slide already exists"
1367
+ };
1368
+ } catch {}
1369
+ } else {
1370
+ let suffix = 1;
1371
+ while (true) {
1372
+ newId = suffix === 1 ? `${slideId}-copy` : `${slideId}-copy-${suffix}`;
1373
+ try {
1374
+ await fs.access(path.resolve(root, newId));
1375
+ suffix++;
1376
+ } catch {
1377
+ break;
1378
+ }
1379
+ }
1380
+ }
1381
+ const dstDir = path.resolve(root, newId);
1382
+ if (!dstDir.startsWith(root + path.sep)) return {
1383
+ ok: false,
1384
+ status: 400,
1385
+ error: "invalid newId"
1386
+ };
1387
+ const srcEntry = path.join(srcDir, "index.tsx");
1388
+ let copiedEntrySource;
1389
+ try {
1390
+ const source = await fs.readFile(srcEntry, "utf8");
1391
+ const metaTitle = readMetaTitleInSource(source);
1392
+ if (metaTitle.kind === "unsupported") return {
1393
+ ok: false,
1394
+ status: 422,
1395
+ error: "could not update copied slide title"
1396
+ };
1397
+ const title = metaTitle.kind === "found" ? metaTitle.title : slideId;
1398
+ const updated = updateMetaTitleInSource(source, `${title} (copy)`);
1399
+ if (updated === null) return {
1400
+ ok: false,
1401
+ status: 422,
1402
+ error: "could not update copied slide title"
1403
+ };
1404
+ copiedEntrySource = updated;
1405
+ } catch {
1406
+ return {
1407
+ ok: false,
1408
+ status: 404,
1409
+ error: "slide not found"
1410
+ };
1411
+ }
1412
+ try {
1413
+ await fs.cp(srcDir, dstDir, {
1414
+ recursive: true,
1415
+ errorOnExist: true,
1416
+ force: false
1417
+ });
1418
+ await fs.writeFile(path.join(dstDir, "index.tsx"), copiedEntrySource, "utf8");
1419
+ return {
1420
+ ok: true,
1421
+ slideId: newId
1422
+ };
1423
+ } catch (err) {
1424
+ if (err.code === "EEXIST") return {
1425
+ ok: false,
1426
+ status: 409,
1427
+ error: "slide already exists"
1428
+ };
1429
+ return {
1430
+ ok: false,
1431
+ status: 500,
1432
+ error: String(err.message ?? err)
1433
+ };
1434
+ }
1435
+ }
1244
1436
  function resolveSlideEntry(slidesRoot, slideId) {
1245
1437
  if (!SLIDE_ID_RE$3.test(slideId)) return null;
1246
1438
  const dir = path.resolve(slidesRoot, slideId);
@@ -1796,10 +1988,38 @@ function registerAssetRoutes(server, ctx) {
1796
1988
  size: stat.size,
1797
1989
  mtime: stat.mtimeMs,
1798
1990
  mime: mimeForFilename(name),
1799
- url: `/__assets/${slideId}/${encodeURIComponent(name)}`
1991
+ url: `/__assets/${slideId}/${encodeURIComponent(name)}`,
1992
+ unused: true
1800
1993
  });
1801
1994
  }
1802
1995
  assets.sort((a, b) => a.name.localeCompare(b.name));
1996
+ if (assets.length > 0) {
1997
+ const isGlobal = slideId === GLOBAL_SCOPE;
1998
+ let scanIds;
1999
+ if (isGlobal) try {
2000
+ const dirs = await fs.readdir(ctx.slidesRoot, { withFileTypes: true });
2001
+ scanIds = dirs.filter((e) => e.isDirectory() && SLIDE_ID_RE$3.test(e.name)).map((e) => e.name);
2002
+ } catch {
2003
+ scanIds = [];
2004
+ }
2005
+ else scanIds = SLIDE_ID_RE$3.test(slideId) ? [slideId] : [];
2006
+ const paths = assets.map((a) => isGlobal ? `@assets/${a.name}` : `./assets/${a.name}`);
2007
+ const pathToAsset = new Map(paths.map((p, i) => [p, assets[i]]));
2008
+ for (const sid of scanIds) {
2009
+ const entry = resolveSlideEntry(ctx.slidesRoot, sid);
2010
+ if (!entry) continue;
2011
+ let source;
2012
+ try {
2013
+ source = await fs.readFile(entry, "utf8");
2014
+ } catch {
2015
+ continue;
2016
+ }
2017
+ for (const p of findReferencedAssets(source, paths)) {
2018
+ const a = pathToAsset.get(p);
2019
+ if (a) a.unused = false;
2020
+ }
2021
+ }
2022
+ }
1803
2023
  return json$2(res, 200, { assets });
1804
2024
  }
1805
2025
  if (fileMatch) {
@@ -2404,6 +2624,27 @@ function registerSlideRoutes(server, ctx) {
2404
2624
  index: pageIndex
2405
2625
  });
2406
2626
  }
2627
+ const duplicateMatch = url.pathname.match(/^\/([^/]+)\/duplicate$/);
2628
+ if (duplicateMatch && method === "POST") {
2629
+ const requestCheck = validateMutationRequest(req);
2630
+ if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2631
+ const slideId$1 = duplicateMatch[1];
2632
+ if (!SLIDE_ID_RE$3.test(slideId$1)) return json$2(res, 400, { error: "invalid slideId" });
2633
+ const body = await readBody$2(req);
2634
+ if (body.newId !== void 0 && typeof body.newId !== "string") return json$2(res, 400, { error: "invalid newId" });
2635
+ const duplicated = await duplicateSlideDir(ctx.slidesRoot, slideId$1, body.newId);
2636
+ if (!duplicated.ok) return json$2(res, duplicated.status, { error: duplicated.error });
2637
+ const manifest = await readManifest(ctx.manifestPath);
2638
+ const folderId = manifest.assignments[slideId$1];
2639
+ if (folderId) {
2640
+ manifest.assignments[duplicated.slideId] = folderId;
2641
+ await writeManifest(ctx.manifestPath, manifest);
2642
+ }
2643
+ return json$2(res, 200, {
2644
+ ok: true,
2645
+ slideId: duplicated.slideId
2646
+ });
2647
+ }
2407
2648
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
2408
2649
  if (!idMatch) return next();
2409
2650
  const slideId = idMatch[1];
@@ -3102,7 +3343,10 @@ function locTagsPlugin(opts) {
3102
3343
  transform(code, id) {
3103
3344
  const filePath = id.split("?")[0];
3104
3345
  if (!filePath.startsWith(slidesRoot + path.sep)) return null;
3105
- if (!filePath.endsWith(`${path.sep}index.tsx`)) return null;
3346
+ if (!filePath.endsWith(".tsx")) return null;
3347
+ if (filePath.endsWith(".d.ts") || filePath.endsWith(".test.tsx")) return null;
3348
+ const rel = filePath.slice(slidesRoot.length + path.sep.length);
3349
+ if (!rel.includes(path.sep)) return null;
3106
3350
  const next = injectLocTags(code);
3107
3351
  if (next === null) return null;
3108
3352
  return {
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-iKjqaX08.js";
2
+ import { createViteConfig } from "./config-PwUHqZ_X.js";
3
3
  import { createServer, mergeConfig } from "vite";
4
4
 
5
5
  //#region src/cli/dev.ts
@@ -34,6 +34,7 @@ const en = {
34
34
  home: {
35
35
  appTitle: "open-slide",
36
36
  draft: "Draft",
37
+ duplicate: "Duplicate",
37
38
  themes: "Themes",
38
39
  assets: "Assets",
39
40
  folders: "Folders",
@@ -75,6 +76,8 @@ const en = {
75
76
  deleteDialogDescriptionSuffix: "This action cannot be undone.",
76
77
  toastFolderCreated: "Created folder “{name}”",
77
78
  toastFolderCreateFailed: "Failed to create folder",
79
+ toastSlideDuplicated: "Duplicated “{slide}” as {newSlide}",
80
+ toastSlideDuplicateFailed: "Could not duplicate slide",
78
81
  toastSlideMoved: "Moved “{slide}” to {folder}",
79
82
  toastSlideMoveFailed: "Failed to move slide",
80
83
  toastFolderDeleted: "Deleted folder “{name}”",
@@ -259,6 +262,7 @@ const en = {
259
262
  one: "{count} file",
260
263
  other: "{count} files"
261
264
  },
265
+ usageUnused: "Unused",
262
266
  searchLogos: "Search logos",
263
267
  upload: "Upload",
264
268
  dropToUpload: "Drop to upload",
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Locale, Plural } from "./types-Dpr8nbih.js";
2
- import { OpenSlideConfig } from "./config-BQdTMho4.js";
1
+ import { Locale, Plural } from "./types-B-KrjgX8.js";
2
+ import { OpenSlideConfig } from "./config-CfMThYN9.js";
3
3
  import { CSSProperties, ComponentType, HTMLAttributes } from "react";
4
4
  import * as react_jsx_runtime0 from "react/jsx-runtime";
5
5
 
@@ -45,9 +45,33 @@ declare function designToCssVars(d: DesignSystem): Record<string, string>;
45
45
  declare function cssVarsToString(vars: Record<string, string>): string;
46
46
  declare const defaultDesign: DesignSystem;
47
47
 
48
+ //#endregion
49
+ //#region src/app/lib/page-context.d.ts
50
+ declare function useSlidePageNumber(): {
51
+ current: number;
52
+ total: number;
53
+ };
54
+
55
+ //#endregion
56
+ //#region src/app/lib/transition.d.ts
57
+ type TransitionPhase = {
58
+ keyframes: Keyframe[] | PropertyIndexedKeyframes;
59
+ easing?: string;
60
+ duration?: number;
61
+ delay?: number;
62
+ };
63
+ type SlideTransition = {
64
+ duration: number;
65
+ easing?: string;
66
+ enter?: TransitionPhase;
67
+ exit?: TransitionPhase;
68
+ };
69
+
48
70
  //#endregion
49
71
  //#region src/app/lib/sdk.d.ts
50
- type Page = ComponentType;
72
+ type Page = ComponentType & {
73
+ transition?: SlideTransition;
74
+ };
51
75
  type SlideMeta = {
52
76
  title?: string;
53
77
  theme?: string;
@@ -59,9 +83,10 @@ type SlideModule = {
59
83
  meta?: SlideMeta;
60
84
  design?: DesignSystem;
61
85
  notes?: (string | undefined)[];
86
+ transition?: SlideTransition;
62
87
  };
63
88
  declare const CANVAS_WIDTH = 1920;
64
89
  declare const CANVAS_HEIGHT = 1080;
65
90
 
66
91
  //#endregion
67
- export { CANVAS_HEIGHT, CANVAS_WIDTH, DesignFonts, DesignPalette, DesignSystem, DesignTypeScale, ImagePlaceholder, ImagePlaceholderProps, Locale, OpenSlideConfig, Page, Plural, SlideMeta, SlideModule, cssVarsToString, defaultDesign, designToCssVars };
92
+ export { CANVAS_HEIGHT, CANVAS_WIDTH, DesignFonts, DesignPalette, DesignSystem, DesignTypeScale, ImagePlaceholder, ImagePlaceholderProps, Locale, OpenSlideConfig, Page, Plural, SlideMeta, SlideModule, SlideTransition, TransitionPhase, cssVarsToString, defaultDesign, designToCssVars, useSlidePageNumber };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { en } from "./en-DDGqyNaW.js";
1
+ import { en } from "./en-BDnM5zKJ.js";
2
2
  import { cssVarsToString, defaultDesign, designToCssVars } from "./design-cpzS8aud.js";
3
- import { useRef, useState } from "react";
3
+ import { createContext, useContext, useRef, useState } from "react";
4
4
  import { toast } from "sonner";
5
5
  import config from "virtual:open-slide/config";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -43,7 +43,8 @@ async function uploadWithAutoRename(slideId, file) {
43
43
  size: body?.size ?? uploaded.size,
44
44
  mtime: body?.mtime ?? Date.now(),
45
45
  mime: body?.mime ?? uploaded.type ?? "application/octet-stream",
46
- url: body?.url ?? `/__assets/${slideId}/${encodeURIComponent(uploaded.name)}`
46
+ url: body?.url ?? `/__assets/${slideId}/${encodeURIComponent(uploaded.name)}`,
47
+ unused: body?.unused ?? false
47
48
  };
48
49
  return {
49
50
  ok: true,
@@ -296,10 +297,25 @@ function PlaceholderIcon() {
296
297
  });
297
298
  }
298
299
 
300
+ //#endregion
301
+ //#region src/app/lib/page-context.tsx
302
+ const GLOBAL_KEY = "__open_slide_page_context__";
303
+ const g = globalThis;
304
+ if (!g[GLOBAL_KEY]) g[GLOBAL_KEY] = createContext(null);
305
+ const SlidePageContext = g[GLOBAL_KEY];
306
+ function useSlidePageNumber() {
307
+ const ctx = useContext(SlidePageContext);
308
+ if (!ctx) throw new Error("useSlidePageNumber must be called from a slide page rendered by @open-slide/core");
309
+ return {
310
+ current: ctx.index + 1,
311
+ total: ctx.total
312
+ };
313
+ }
314
+
299
315
  //#endregion
300
316
  //#region src/app/lib/sdk.ts
301
317
  const CANVAS_WIDTH = 1920;
302
318
  const CANVAS_HEIGHT = 1080;
303
319
 
304
320
  //#endregion
305
- export { CANVAS_HEIGHT, CANVAS_WIDTH, ImagePlaceholder, cssVarsToString, defaultDesign, designToCssVars };
321
+ export { CANVAS_HEIGHT, CANVAS_WIDTH, ImagePlaceholder, cssVarsToString, defaultDesign, designToCssVars, useSlidePageNumber };
@@ -1,4 +1,4 @@
1
- import { Locale, Plural } from "../types-Dpr8nbih.js";
1
+ import { Locale, Plural } from "../types-B-KrjgX8.js";
2
2
 
3
3
  //#region src/locale/en.d.ts
4
4
  declare const en: Locale;
@@ -1,4 +1,4 @@
1
- import { en } from "../en-DDGqyNaW.js";
1
+ import { en } from "../en-BDnM5zKJ.js";
2
2
 
3
3
  //#region src/locale/format.ts
4
4
  function format(template, vars) {
@@ -48,6 +48,7 @@ const ja = {
48
48
  home: {
49
49
  appTitle: "open-slide",
50
50
  draft: "下書き",
51
+ duplicate: "複製",
51
52
  themes: "テーマ",
52
53
  assets: "アセット",
53
54
  folders: "フォルダ",
@@ -89,6 +90,8 @@ const ja = {
89
90
  deleteDialogDescriptionSuffix: "この操作は元に戻せません。",
90
91
  toastFolderCreated: "フォルダ「{name}」を作成しました",
91
92
  toastFolderCreateFailed: "フォルダの作成に失敗しました",
93
+ toastSlideDuplicated: "「{slide}」を {newSlide} として複製しました",
94
+ toastSlideDuplicateFailed: "スライドを複製できませんでした",
92
95
  toastSlideMoved: "「{slide}」を {folder} に移動しました",
93
96
  toastSlideMoveFailed: "スライドの移動に失敗しました",
94
97
  toastFolderDeleted: "フォルダ「{name}」を削除しました",
@@ -273,6 +276,7 @@ const ja = {
273
276
  one: "ファイル {count} 件",
274
277
  other: "ファイル {count} 件"
275
278
  },
279
+ usageUnused: "未使用",
276
280
  searchLogos: "ロゴを検索",
277
281
  upload: "アップロード",
278
282
  dropToUpload: "ドロップでアップロード",
@@ -421,6 +425,7 @@ const zhCN = {
421
425
  home: {
422
426
  appTitle: "open-slide",
423
427
  draft: "草稿",
428
+ duplicate: "复制",
424
429
  themes: "主题",
425
430
  assets: "素材",
426
431
  folders: "文件夹",
@@ -462,6 +467,8 @@ const zhCN = {
462
467
  deleteDialogDescriptionSuffix: "此操作无法撤销。",
463
468
  toastFolderCreated: "已创建文件夹\"{name}\"",
464
469
  toastFolderCreateFailed: "创建文件夹失败",
470
+ toastSlideDuplicated: "已将\"{slide}\"复制为 {newSlide}",
471
+ toastSlideDuplicateFailed: "无法复制幻灯片",
465
472
  toastSlideMoved: "已将\"{slide}\"移至 {folder}",
466
473
  toastSlideMoveFailed: "移动幻灯片失败",
467
474
  toastFolderDeleted: "已删除文件夹\"{name}\"",
@@ -646,6 +653,7 @@ const zhCN = {
646
653
  one: "{count} 个文件",
647
654
  other: "{count} 个文件"
648
655
  },
656
+ usageUnused: "未使用",
649
657
  searchLogos: "搜索 Logo",
650
658
  upload: "上传",
651
659
  dropToUpload: "拖入即可上传",
@@ -794,6 +802,7 @@ const zhTW = {
794
802
  home: {
795
803
  appTitle: "open-slide",
796
804
  draft: "草稿",
805
+ duplicate: "複製",
797
806
  themes: "主題",
798
807
  assets: "素材",
799
808
  folders: "資料夾",
@@ -835,6 +844,8 @@ const zhTW = {
835
844
  deleteDialogDescriptionSuffix: "此操作無法復原。",
836
845
  toastFolderCreated: "已建立資料夾「{name}」",
837
846
  toastFolderCreateFailed: "建立資料夾失敗",
847
+ toastSlideDuplicated: "已將「{slide}」複製為 {newSlide}",
848
+ toastSlideDuplicateFailed: "無法複製投影片",
838
849
  toastSlideMoved: "已將「{slide}」移至 {folder}",
839
850
  toastSlideMoveFailed: "移動投影片失敗",
840
851
  toastFolderDeleted: "已刪除資料夾「{name}」",
@@ -1019,6 +1030,7 @@ const zhTW = {
1019
1030
  one: "{count} 個檔案",
1020
1031
  other: "{count} 個檔案"
1021
1032
  },
1033
+ usageUnused: "未使用",
1022
1034
  searchLogos: "搜尋 Logo",
1023
1035
  upload: "上傳",
1024
1036
  dropToUpload: "拖入即可上傳",
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-iKjqaX08.js";
2
+ import { createViteConfig } from "./config-PwUHqZ_X.js";
3
3
  import { mergeConfig, preview as preview$1 } from "vite";
4
4
 
5
5
  //#region src/cli/preview.ts
@@ -38,6 +38,7 @@ 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 @@ 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;
@@ -265,6 +269,7 @@ type Locale = {
265
269
  scopeGlobal: string;
266
270
  /** templates: "{count} file" / "{count} files" */
267
271
  fileCount: Plural;
272
+ usageUnused: string;
268
273
  searchLogos: string;
269
274
  upload: string;
270
275
  dropToUpload: string;
@@ -1,5 +1,5 @@
1
- import "../types-Dpr8nbih.js";
2
- import { OpenSlideConfig } from "../config-BQdTMho4.js";
1
+ import "../types-B-KrjgX8.js";
2
+ import { OpenSlideConfig } from "../config-CfMThYN9.js";
3
3
  import { InlineConfig } from "vite";
4
4
 
5
5
  //#region src/vite/config.d.ts
@@ -1,4 +1,4 @@
1
1
  import "../design-cpzS8aud.js";
2
- import { createViteConfig } from "../config-iKjqaX08.js";
2
+ import { createViteConfig } from "../config-PwUHqZ_X.js";
3
3
 
4
4
  export { createViteConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-slide/core",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Runtime and CLI for open-slide — write slides in slides/, we handle the rest.",
5
5
  "type": "module",
6
6
  "exports": {