@open-slide/core 1.7.0 → 1.9.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 (43) hide show
  1. package/dist/{build-tLrkKUHr.js → build-ZM7IfDO-.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-PwUHqZ_X.js → config-BAZeaz2P.js} +289 -246
  4. package/dist/{config-CfMThYN9.d.ts → config-D_5nlXFU.d.ts} +6 -1
  5. package/dist/{dev-DpCIRbhT.js → dev-BQkNTG_t.js} +1 -1
  6. package/dist/format-CYOb2cAQ.js +1573 -0
  7. package/dist/index.d.ts +4 -4
  8. package/dist/index.js +38 -4
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +1 -1144
  11. package/dist/{preview-BSGlM6Se.js → preview-D8hUtbRA.js} +1 -1
  12. package/dist/{types-B-KrjgX8.d.ts → types-AalTbxMj.d.ts} +17 -3
  13. package/dist/vite/index.d.ts +2 -2
  14. package/dist/vite/index.js +1 -1
  15. package/package.json +2 -1
  16. package/skills/create-theme/SKILL.md +1 -1
  17. package/src/app/components/inspector/comment-widget.tsx +16 -2
  18. package/src/app/components/language-toggle.tsx +39 -0
  19. package/src/app/components/player.tsx +12 -17
  20. package/src/app/components/pptx-progress-toast.tsx +32 -0
  21. package/src/app/components/sidebar/folder-item.tsx +7 -2
  22. package/src/app/components/sidebar/sidebar-footer.tsx +51 -0
  23. package/src/app/components/sidebar/sidebar.tsx +95 -17
  24. package/src/app/lib/design-presets.ts +1 -1
  25. package/src/app/lib/export-pptx.ts +284 -0
  26. package/src/app/lib/folders.ts +28 -0
  27. package/src/app/lib/inspector/fiber.test.ts +154 -0
  28. package/src/app/lib/inspector/fiber.ts +12 -1
  29. package/src/app/lib/locale-store.ts +67 -0
  30. package/src/app/lib/use-click-page-navigation.ts +52 -0
  31. package/src/app/lib/use-is-mobile.ts +21 -0
  32. package/src/app/lib/use-locale.ts +4 -16
  33. package/src/app/routes/home-shell.tsx +8 -0
  34. package/src/app/routes/home.tsx +1 -1
  35. package/src/app/routes/slide.tsx +145 -53
  36. package/src/app/virtual.d.ts +1 -0
  37. package/src/locale/en.ts +18 -3
  38. package/src/locale/ja.ts +19 -3
  39. package/src/locale/types.ts +18 -3
  40. package/src/locale/zh-cn.ts +17 -3
  41. package/src/locale/zh-tw.ts +17 -3
  42. package/dist/en-BDnM5zKJ.js +0 -378
  43. package/src/app/components/click-nav-zones.tsx +0 -36
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { randomUUID } from "node:crypto";
6
- import { existsSync } from "node:fs";
6
+ import { existsSync, readFileSync } from "node:fs";
7
7
  import tailwindcss from "@tailwindcss/vite";
8
8
  import react from "@vitejs/plugin-react";
9
9
  import * as t$4 from "@babel/types";
@@ -1157,21 +1157,20 @@ function findReferencedAssets(source, assetPaths) {
1157
1157
  if (!ast) return referenced;
1158
1158
  const wanted = new Set(assetPaths);
1159
1159
  const identToPath = new Map();
1160
+ const importLocals = new Set();
1160
1161
  for (const imp of findImports$1(ast)) {
1161
1162
  if (!imp.defaultIdent) continue;
1162
- if (wanted.has(imp.source)) identToPath.set(imp.defaultIdent, imp.source);
1163
+ if (!wanted.has(imp.source)) continue;
1164
+ identToPath.set(imp.defaultIdent, imp.source);
1165
+ for (const spec of imp.node.specifiers) if (t$3.isImportDefaultSpecifier(spec) && spec.local.name === imp.defaultIdent) importLocals.add(spec.local);
1163
1166
  }
1164
1167
  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);
1168
+ walkAll(ast, (n) => {
1169
+ if (!t$3.isIdentifier(n)) return;
1170
+ const p = identToPath.get(n.name);
1171
+ if (!p) return;
1172
+ if (importLocals.has(n)) return;
1173
+ referenced.add(p);
1175
1174
  });
1176
1175
  return referenced;
1177
1176
  }
@@ -1245,7 +1244,7 @@ function applyRevertAsset(source, assetPath) {
1245
1244
 
1246
1245
  //#endregion
1247
1246
  //#region src/editing/slide-ops.ts
1248
- const SLIDE_ID_RE$3 = /^[a-z0-9_-]+$/i;
1247
+ const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
1249
1248
  function validateSlideName(v) {
1250
1249
  if (typeof v !== "string") return null;
1251
1250
  const trimmed = v.trim();
@@ -1309,7 +1308,7 @@ function readMetaTitleInSource(source) {
1309
1308
  return { kind: "missing" };
1310
1309
  }
1311
1310
  async function rmSlideDir(slidesRoot, slideId) {
1312
- if (!SLIDE_ID_RE$3.test(slideId)) return false;
1311
+ if (!SLIDE_ID_RE.test(slideId)) return false;
1313
1312
  const dir = path.resolve(slidesRoot, slideId);
1314
1313
  if (!dir.startsWith(slidesRoot + path.sep)) return false;
1315
1314
  try {
@@ -1323,7 +1322,7 @@ async function rmSlideDir(slidesRoot, slideId) {
1323
1322
  }
1324
1323
  }
1325
1324
  async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
1326
- if (!SLIDE_ID_RE$3.test(slideId)) return {
1325
+ if (!SLIDE_ID_RE.test(slideId)) return {
1327
1326
  ok: false,
1328
1327
  status: 400,
1329
1328
  error: "invalid slideId"
@@ -1346,7 +1345,7 @@ async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
1346
1345
  }
1347
1346
  let newId;
1348
1347
  if (desiredId !== void 0) {
1349
- if (!SLIDE_ID_RE$3.test(desiredId)) return {
1348
+ if (!SLIDE_ID_RE.test(desiredId)) return {
1350
1349
  ok: false,
1351
1350
  status: 400,
1352
1351
  error: "invalid newId"
@@ -1434,7 +1433,7 @@ async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
1434
1433
  }
1435
1434
  }
1436
1435
  function resolveSlideEntry(slidesRoot, slideId) {
1437
- if (!SLIDE_ID_RE$3.test(slideId)) return null;
1436
+ if (!SLIDE_ID_RE.test(slideId)) return null;
1438
1437
  const dir = path.resolve(slidesRoot, slideId);
1439
1438
  if (!dir.startsWith(slidesRoot + path.sep)) return null;
1440
1439
  return path.join(dir, "index.tsx");
@@ -1771,7 +1770,7 @@ function validateAssetName(v) {
1771
1770
  return trimmed;
1772
1771
  }
1773
1772
  function resolveAssetsDir(slidesRoot, slideId) {
1774
- if (!SLIDE_ID_RE$3.test(slideId)) return null;
1773
+ if (!SLIDE_ID_RE.test(slideId)) return null;
1775
1774
  const slideDir = path.resolve(slidesRoot, slideId);
1776
1775
  if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
1777
1776
  const assetsDir = path.resolve(slideDir, "assets");
@@ -1884,10 +1883,11 @@ function makeContext(opts) {
1884
1883
  slidesDir,
1885
1884
  slidesRoot,
1886
1885
  globalAssetsRoot,
1887
- manifestPath
1886
+ manifestPath,
1887
+ coreVersion: opts.coreVersion
1888
1888
  };
1889
1889
  }
1890
- async function readBody$2(req) {
1890
+ async function readBody(req) {
1891
1891
  return await new Promise((resolve, reject) => {
1892
1892
  const chunks = [];
1893
1893
  req.on("data", (c) => chunks.push(c));
@@ -1903,17 +1903,21 @@ async function readBody$2(req) {
1903
1903
  req.on("error", reject);
1904
1904
  });
1905
1905
  }
1906
- function json$2(res, status, body) {
1906
+ function json(res, status, body) {
1907
1907
  res.statusCode = status;
1908
1908
  res.setHeader("content-type", "application/json");
1909
1909
  res.end(JSON.stringify(body));
1910
1910
  }
1911
- function resolveSlideEntryPath(ctx, slideId) {
1912
- if (!SLIDE_ID_RE$3.test(slideId)) return null;
1913
- const full = path.resolve(ctx.slidesRoot, slideId, "index.tsx");
1914
- if (!full.startsWith(ctx.slidesRoot + path.sep)) return null;
1911
+ function resolveSlidePath(userCwd, slidesDir, slideId) {
1912
+ if (!SLIDE_ID_RE.test(slideId)) return null;
1913
+ const slidesRoot = path.resolve(userCwd, slidesDir);
1914
+ const full = path.resolve(slidesRoot, slideId, "index.tsx");
1915
+ if (!full.startsWith(slidesRoot + path.sep)) return null;
1915
1916
  return full;
1916
1917
  }
1918
+ function resolveSlideEntryPath(ctx, slideId) {
1919
+ return resolveSlidePath(ctx.userCwd, ctx.slidesDir, slideId);
1920
+ }
1917
1921
 
1918
1922
  //#endregion
1919
1923
  //#region src/vite/routes/assets.ts
@@ -1928,18 +1932,18 @@ function registerAssetRoutes(server, ctx) {
1928
1932
  if (usagesMatch && method === "GET") {
1929
1933
  const scope = usagesMatch[1];
1930
1934
  const filename = decodeURIComponent(usagesMatch[2]);
1931
- if (!validateAssetName(filename)) return json$2(res, 400, { error: "invalid path" });
1935
+ if (!validateAssetName(filename)) return json(res, 400, { error: "invalid path" });
1932
1936
  const isGlobal = scope === GLOBAL_SCOPE;
1933
1937
  const assetPath = isGlobal ? `@assets/${filename}` : `./assets/${filename}`;
1934
1938
  let slideIds;
1935
1939
  if (isGlobal) try {
1936
1940
  const entries = await fs.readdir(ctx.slidesRoot, { withFileTypes: true });
1937
- slideIds = entries.filter((e) => e.isDirectory() && SLIDE_ID_RE$3.test(e.name)).map((e) => e.name);
1941
+ slideIds = entries.filter((e) => e.isDirectory() && SLIDE_ID_RE.test(e.name)).map((e) => e.name);
1938
1942
  } catch {
1939
1943
  slideIds = [];
1940
1944
  }
1941
1945
  else {
1942
- if (!SLIDE_ID_RE$3.test(scope)) return json$2(res, 400, { error: "invalid slideId" });
1946
+ if (!SLIDE_ID_RE.test(scope)) return json(res, 400, { error: "invalid slideId" });
1943
1947
  slideIds = [scope];
1944
1948
  }
1945
1949
  const usages = [];
@@ -1962,7 +1966,7 @@ function registerAssetRoutes(server, ctx) {
1962
1966
  totalCount += count;
1963
1967
  }
1964
1968
  }
1965
- return json$2(res, 200, {
1969
+ return json(res, 200, {
1966
1970
  usages,
1967
1971
  totalCount
1968
1972
  });
@@ -1970,12 +1974,12 @@ function registerAssetRoutes(server, ctx) {
1970
1974
  if (listMatch && method === "GET") {
1971
1975
  const slideId = listMatch[1];
1972
1976
  const scopedDir = resolveScopedAssetsDir(ctx.slidesRoot, ctx.globalAssetsRoot, slideId);
1973
- if (!scopedDir) return json$2(res, 400, { error: "invalid slideId" });
1977
+ if (!scopedDir) return json(res, 400, { error: "invalid slideId" });
1974
1978
  let entries;
1975
1979
  try {
1976
1980
  entries = await fs.readdir(scopedDir);
1977
1981
  } catch (err) {
1978
- if (err.code === "ENOENT") return json$2(res, 200, { assets: [] });
1982
+ if (err.code === "ENOENT") return json(res, 200, { assets: [] });
1979
1983
  throw err;
1980
1984
  }
1981
1985
  const assets = [];
@@ -1998,11 +2002,11 @@ function registerAssetRoutes(server, ctx) {
1998
2002
  let scanIds;
1999
2003
  if (isGlobal) try {
2000
2004
  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);
2005
+ scanIds = dirs.filter((e) => e.isDirectory() && SLIDE_ID_RE.test(e.name)).map((e) => e.name);
2002
2006
  } catch {
2003
2007
  scanIds = [];
2004
2008
  }
2005
- else scanIds = SLIDE_ID_RE$3.test(slideId) ? [slideId] : [];
2009
+ else scanIds = SLIDE_ID_RE.test(slideId) ? [slideId] : [];
2006
2010
  const paths = assets.map((a) => isGlobal ? `@assets/${a.name}` : `./assets/${a.name}`);
2007
2011
  const pathToAsset = new Map(paths.map((p, i) => [p, assets[i]]));
2008
2012
  for (const sid of scanIds) {
@@ -2020,13 +2024,13 @@ function registerAssetRoutes(server, ctx) {
2020
2024
  }
2021
2025
  }
2022
2026
  }
2023
- return json$2(res, 200, { assets });
2027
+ return json(res, 200, { assets });
2024
2028
  }
2025
2029
  if (fileMatch) {
2026
2030
  const slideId = fileMatch[1];
2027
2031
  const filename = decodeURIComponent(fileMatch[2]);
2028
2032
  const file = resolveScopedAssetFile(ctx.slidesRoot, ctx.globalAssetsRoot, slideId, filename);
2029
- if (!file) return json$2(res, 400, { error: "invalid path" });
2033
+ if (!file) return json(res, 400, { error: "invalid path" });
2030
2034
  if (method === "GET") try {
2031
2035
  const buf = await fs.readFile(file);
2032
2036
  res.statusCode = 200;
@@ -2035,22 +2039,22 @@ function registerAssetRoutes(server, ctx) {
2035
2039
  res.end(buf);
2036
2040
  return;
2037
2041
  } catch (err) {
2038
- if (err.code === "ENOENT") return json$2(res, 404, { error: "asset not found" });
2042
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2039
2043
  throw err;
2040
2044
  }
2041
2045
  if (method === "POST") {
2042
2046
  const requestCheck = validateMutationRequest(req);
2043
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2047
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2044
2048
  const overwrite = url.searchParams.get("overwrite") === "1";
2045
2049
  const lenHeader = req.headers["content-length"];
2046
2050
  const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
2047
- if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json$2(res, 413, { error: "file too large" });
2051
+ if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json(res, 413, { error: "file too large" });
2048
2052
  if (!overwrite) try {
2049
2053
  await fs.access(file);
2050
- return json$2(res, 409, { error: "asset exists" });
2054
+ return json(res, 409, { error: "asset exists" });
2051
2055
  } catch {}
2052
2056
  const scopedDir = resolveScopedAssetsDir(ctx.slidesRoot, ctx.globalAssetsRoot, slideId);
2053
- if (!scopedDir) return json$2(res, 400, { error: "invalid slideId" });
2057
+ if (!scopedDir) return json(res, 400, { error: "invalid slideId" });
2054
2058
  await fs.mkdir(scopedDir, { recursive: true });
2055
2059
  const chunks = [];
2056
2060
  let total = 0;
@@ -2068,9 +2072,9 @@ function registerAssetRoutes(server, ctx) {
2068
2072
  req.on("end", () => resolve());
2069
2073
  req.on("error", reject);
2070
2074
  });
2071
- if (oversized) return json$2(res, 413, { error: "file too large" });
2075
+ if (oversized) return json(res, 413, { error: "file too large" });
2072
2076
  await fs.writeFile(file, Buffer.concat(chunks));
2073
- return json$2(res, 200, {
2077
+ return json(res, 200, {
2074
2078
  ok: true,
2075
2079
  name: filename,
2076
2080
  size: total,
@@ -2080,46 +2084,46 @@ function registerAssetRoutes(server, ctx) {
2080
2084
  }
2081
2085
  if (method === "PATCH") {
2082
2086
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2083
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2084
- const body = await readBody$2(req);
2087
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2088
+ const body = await readBody(req);
2085
2089
  const target = validateAssetName(body.name);
2086
- if (!target) return json$2(res, 400, { error: "invalid name" });
2087
- if (target === filename) return json$2(res, 200, {
2090
+ if (!target) return json(res, 400, { error: "invalid name" });
2091
+ if (target === filename) return json(res, 200, {
2088
2092
  ok: true,
2089
2093
  name: filename
2090
2094
  });
2091
2095
  const dest = resolveScopedAssetFile(ctx.slidesRoot, ctx.globalAssetsRoot, slideId, target);
2092
- if (!dest) return json$2(res, 400, { error: "invalid name" });
2096
+ if (!dest) return json(res, 400, { error: "invalid name" });
2093
2097
  try {
2094
2098
  await fs.access(dest);
2095
- return json$2(res, 409, { error: "target exists" });
2099
+ return json(res, 409, { error: "target exists" });
2096
2100
  } catch {}
2097
2101
  try {
2098
2102
  await fs.rename(file, dest);
2099
2103
  } catch (err) {
2100
- if (err.code === "ENOENT") return json$2(res, 404, { error: "asset not found" });
2104
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2101
2105
  throw err;
2102
2106
  }
2103
- return json$2(res, 200, {
2107
+ return json(res, 200, {
2104
2108
  ok: true,
2105
2109
  name: target
2106
2110
  });
2107
2111
  }
2108
2112
  if (method === "DELETE") {
2109
2113
  const requestCheck = validateMutationRequest(req);
2110
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2114
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2111
2115
  try {
2112
2116
  await fs.unlink(file);
2113
2117
  } catch (err) {
2114
- if (err.code === "ENOENT") return json$2(res, 404, { error: "asset not found" });
2118
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2115
2119
  throw err;
2116
2120
  }
2117
- return json$2(res, 200, { ok: true });
2121
+ return json(res, 200, { ok: true });
2118
2122
  }
2119
2123
  }
2120
2124
  return next();
2121
2125
  } catch (err) {
2122
- json$2(res, 500, { error: String(err.message ?? err) });
2126
+ json(res, 500, { error: String(err.message ?? err) });
2123
2127
  }
2124
2128
  });
2125
2129
  }
@@ -2239,32 +2243,32 @@ function registerCommentRoutes(server, ctx) {
2239
2243
  if (method === "GET" && url.pathname === "/") {
2240
2244
  const slideId = url.searchParams.get("slideId") ?? "";
2241
2245
  const file = resolveSlideEntryPath(ctx, slideId);
2242
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2246
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2243
2247
  let source;
2244
2248
  try {
2245
2249
  source = await fs.readFile(file, "utf8");
2246
2250
  } catch {
2247
- return json$2(res, 404, { error: "slide not found" });
2251
+ return json(res, 404, { error: "slide not found" });
2248
2252
  }
2249
- return json$2(res, 200, { comments: parseMarkers(source) });
2253
+ return json(res, 200, { comments: parseMarkers(source) });
2250
2254
  }
2251
2255
  if (method === "POST" && url.pathname === "/add") {
2252
2256
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2253
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2254
- const body = await readBody$2(req);
2257
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2258
+ const body = await readBody(req);
2255
2259
  const slideId = body.slideId ?? "";
2256
2260
  const file = resolveSlideEntryPath(ctx, slideId);
2257
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2258
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
2259
- if (!body.text || typeof body.text !== "string") return json$2(res, 400, { error: "missing text" });
2261
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2262
+ if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
2263
+ if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
2260
2264
  let source;
2261
2265
  try {
2262
2266
  source = await fs.readFile(file, "utf8");
2263
2267
  } catch {
2264
- return json$2(res, 404, { error: "slide not found" });
2268
+ return json(res, 404, { error: "slide not found" });
2265
2269
  }
2266
2270
  const plan = findInsertion(source, body.line, body.column);
2267
- if (!plan) return json$2(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
2271
+ if (!plan) return json(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
2268
2272
  const id = newCommentId();
2269
2273
  const ts = new Date().toISOString();
2270
2274
  const payload = b64urlEncode(JSON.stringify({
@@ -2275,36 +2279,36 @@ function registerCommentRoutes(server, ctx) {
2275
2279
  const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
2276
2280
  await fs.writeFile(file, next$1, "utf8");
2277
2281
  const markerLine = offsetToLine(next$1, plan.offset + 1);
2278
- return json$2(res, 200, {
2282
+ return json(res, 200, {
2279
2283
  id,
2280
2284
  line: markerLine
2281
2285
  });
2282
2286
  }
2283
2287
  if (method === "DELETE" && url.pathname.startsWith("/")) {
2284
2288
  const requestCheck = validateMutationRequest(req);
2285
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2289
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2286
2290
  const id = url.pathname.slice(1);
2287
- if (!/^c-[a-f0-9]+$/.test(id)) return json$2(res, 400, { error: "invalid id" });
2291
+ if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
2288
2292
  const slideId = url.searchParams.get("slideId") ?? "";
2289
2293
  const file = resolveSlideEntryPath(ctx, slideId);
2290
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2294
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2291
2295
  let source;
2292
2296
  try {
2293
2297
  source = await fs.readFile(file, "utf8");
2294
2298
  } catch {
2295
- return json$2(res, 404, { error: "slide not found" });
2299
+ return json(res, 404, { error: "slide not found" });
2296
2300
  }
2297
2301
  const lines = source.split("\n");
2298
2302
  const idRe = markerDeleteRegex(id);
2299
2303
  const hit = lines.findIndex((l) => idRe.test(l));
2300
- if (hit === -1) return json$2(res, 404, { error: "marker not found" });
2304
+ if (hit === -1) return json(res, 404, { error: "marker not found" });
2301
2305
  lines.splice(hit, 1);
2302
2306
  await fs.writeFile(file, lines.join("\n"), "utf8");
2303
- return json$2(res, 200, { ok: true });
2307
+ return json(res, 200, { ok: true });
2304
2308
  }
2305
2309
  next();
2306
2310
  } catch (err) {
2307
- json$2(res, 500, { error: String(err.message ?? err) });
2311
+ json(res, 500, { error: String(err.message ?? err) });
2308
2312
  }
2309
2313
  });
2310
2314
  }
@@ -2317,64 +2321,64 @@ function registerEditRoutes(server, ctx) {
2317
2321
  const method = req.method ?? "GET";
2318
2322
  if (method !== "POST") return next();
2319
2323
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2320
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2324
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2321
2325
  try {
2322
2326
  if (url.pathname === "/") {
2323
- const body = await readBody$2(req);
2327
+ const body = await readBody(req);
2324
2328
  const slideId = body.slideId ?? "";
2325
2329
  const file = resolveSlideEntryPath(ctx, slideId);
2326
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2327
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
2328
- if (!Array.isArray(body.ops)) return json$2(res, 400, { error: "missing ops" });
2330
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2331
+ if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
2332
+ if (!Array.isArray(body.ops)) return json(res, 400, { error: "missing ops" });
2329
2333
  let source;
2330
2334
  try {
2331
2335
  source = await fs.readFile(file, "utf8");
2332
2336
  } catch {
2333
- return json$2(res, 404, { error: "slide not found" });
2337
+ return json(res, 404, { error: "slide not found" });
2334
2338
  }
2335
2339
  const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
2336
- if (!result.ok) return json$2(res, result.status, { error: result.error });
2340
+ if (!result.ok) return json(res, result.status, { error: result.error });
2337
2341
  const changed = result.source !== source;
2338
2342
  if (changed) await fs.writeFile(file, result.source, "utf8");
2339
- return json$2(res, 200, {
2343
+ return json(res, 200, {
2340
2344
  ok: true,
2341
2345
  changed
2342
2346
  });
2343
2347
  }
2344
2348
  if (url.pathname === "/revert-asset") {
2345
- const body = await readBody$2(req);
2349
+ const body = await readBody(req);
2346
2350
  const slideId = body.slideId ?? "";
2347
2351
  const assetPath = body.assetPath;
2348
2352
  const file = resolveSlideEntryPath(ctx, slideId);
2349
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2350
- if (typeof assetPath !== "string" || !assetPath) return json$2(res, 400, { error: "missing assetPath" });
2351
- if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return json$2(res, 400, { error: "asset path must start with ./assets/ or @assets/" });
2353
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2354
+ if (typeof assetPath !== "string" || !assetPath) return json(res, 400, { error: "missing assetPath" });
2355
+ if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return json(res, 400, { error: "asset path must start with ./assets/ or @assets/" });
2352
2356
  let source;
2353
2357
  try {
2354
2358
  source = await fs.readFile(file, "utf8");
2355
2359
  } catch {
2356
- return json$2(res, 404, { error: "slide not found" });
2360
+ return json(res, 404, { error: "slide not found" });
2357
2361
  }
2358
2362
  const result = applyRevertAsset(source, assetPath);
2359
- if (!result.ok) return json$2(res, result.status, { error: result.error });
2363
+ if (!result.ok) return json(res, result.status, { error: result.error });
2360
2364
  const changed = result.source !== source;
2361
2365
  if (changed) await fs.writeFile(file, result.source, "utf8");
2362
- return json$2(res, 200, {
2366
+ return json(res, 200, {
2363
2367
  ok: true,
2364
2368
  changed
2365
2369
  });
2366
2370
  }
2367
2371
  if (url.pathname === "/batch") {
2368
- const body = await readBody$2(req);
2372
+ const body = await readBody(req);
2369
2373
  const slideId = body.slideId ?? "";
2370
2374
  const file = resolveSlideEntryPath(ctx, slideId);
2371
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2372
- if (!Array.isArray(body.edits)) return json$2(res, 400, { error: "missing edits" });
2375
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2376
+ if (!Array.isArray(body.edits)) return json(res, 400, { error: "missing edits" });
2373
2377
  let source;
2374
2378
  try {
2375
2379
  source = await fs.readFile(file, "utf8");
2376
2380
  } catch {
2377
- return json$2(res, 404, { error: "slide not found" });
2381
+ return json(res, 404, { error: "slide not found" });
2378
2382
  }
2379
2383
  const original = source;
2380
2384
  const results = [];
@@ -2397,7 +2401,7 @@ function registerEditRoutes(server, ctx) {
2397
2401
  }
2398
2402
  const changed = source !== original;
2399
2403
  if (changed) await fs.writeFile(file, source, "utf8");
2400
- return json$2(res, 200, {
2404
+ return json(res, 200, {
2401
2405
  ok: true,
2402
2406
  changed,
2403
2407
  results
@@ -2405,7 +2409,7 @@ function registerEditRoutes(server, ctx) {
2405
2409
  }
2406
2410
  return next();
2407
2411
  } catch (err) {
2408
- json$2(res, 500, { error: String(err.message ?? err) });
2412
+ json(res, 500, { error: String(err.message ?? err) });
2409
2413
  }
2410
2414
  });
2411
2415
  }
@@ -2446,6 +2450,19 @@ function validateName(v) {
2446
2450
  if (trimmed.length < 1 || trimmed.length > 40) return null;
2447
2451
  return trimmed;
2448
2452
  }
2453
+ function validateReorder(v, current) {
2454
+ if (!Array.isArray(v) || v.length !== current.length) return null;
2455
+ const known = new Set(current.map((f) => f.id));
2456
+ const seen = new Set();
2457
+ const out = [];
2458
+ for (const id of v) {
2459
+ if (typeof id !== "string" || !FOLDER_ID_RE.test(id)) return null;
2460
+ if (!known.has(id) || seen.has(id)) return null;
2461
+ seen.add(id);
2462
+ out.push(id);
2463
+ }
2464
+ return out;
2465
+ }
2449
2466
  function validateIcon(v) {
2450
2467
  if (!v || typeof v !== "object") return null;
2451
2468
  const icon = v;
@@ -2476,16 +2493,16 @@ function registerFolderRoutes(server, ctx) {
2476
2493
  try {
2477
2494
  if (method === "GET" && url.pathname === "/") {
2478
2495
  const manifest = await readManifest(ctx.manifestPath);
2479
- return json$2(res, 200, manifest);
2496
+ return json(res, 200, manifest);
2480
2497
  }
2481
2498
  if (method === "POST" && url.pathname === "/") {
2482
2499
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2483
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2484
- const body = await readBody$2(req);
2500
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2501
+ const body = await readBody(req);
2485
2502
  const name = validateName(body.name);
2486
- if (!name) return json$2(res, 400, { error: "invalid name" });
2503
+ if (!name) return json(res, 400, { error: "invalid name" });
2487
2504
  const icon = validateIcon(body.icon);
2488
- if (!icon) return json$2(res, 400, { error: "invalid icon" });
2505
+ if (!icon) return json(res, 400, { error: "invalid icon" });
2489
2506
  const manifest = await readManifest(ctx.manifestPath);
2490
2507
  const folder = {
2491
2508
  id: newFolderId(),
@@ -2494,64 +2511,76 @@ function registerFolderRoutes(server, ctx) {
2494
2511
  };
2495
2512
  manifest.folders.push(folder);
2496
2513
  await writeManifest(ctx.manifestPath, manifest);
2497
- return json$2(res, 200, folder);
2514
+ return json(res, 200, folder);
2498
2515
  }
2499
2516
  if (method === "PUT" && url.pathname === "/assign") {
2500
2517
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2501
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2502
- const body = await readBody$2(req);
2503
- if (typeof body.slideId !== "string" || !SLIDE_ID_RE$3.test(body.slideId)) return json$2(res, 400, { error: "invalid slideId" });
2518
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2519
+ const body = await readBody(req);
2520
+ if (typeof body.slideId !== "string" || !SLIDE_ID_RE.test(body.slideId)) return json(res, 400, { error: "invalid slideId" });
2504
2521
  const slideId = body.slideId;
2505
2522
  let folderId;
2506
2523
  if (body.folderId === null) folderId = null;
2507
2524
  else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
2508
- else return json$2(res, 400, { error: "invalid folderId" });
2525
+ else return json(res, 400, { error: "invalid folderId" });
2509
2526
  const manifest = await readManifest(ctx.manifestPath);
2510
- if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json$2(res, 404, { error: "folder not found" });
2527
+ if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
2511
2528
  if (folderId === null) delete manifest.assignments[slideId];
2512
2529
  else manifest.assignments[slideId] = folderId;
2513
2530
  await writeManifest(ctx.manifestPath, manifest);
2514
- return json$2(res, 200, { ok: true });
2531
+ return json(res, 200, { ok: true });
2532
+ }
2533
+ if (method === "PUT" && url.pathname === "/reorder") {
2534
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2535
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2536
+ const body = await readBody(req);
2537
+ const manifest = await readManifest(ctx.manifestPath);
2538
+ const ids = validateReorder(body.ids, manifest.folders);
2539
+ if (!ids) return json(res, 400, { error: "invalid ids" });
2540
+ const byId = new Map(manifest.folders.map((f) => [f.id, f]));
2541
+ manifest.folders = ids.map((id) => byId.get(id));
2542
+ await writeManifest(ctx.manifestPath, manifest);
2543
+ return json(res, 200, { ok: true });
2515
2544
  }
2516
2545
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
2517
2546
  if (idMatch) {
2518
2547
  const id = idMatch[1];
2519
- if (!FOLDER_ID_RE.test(id)) return json$2(res, 400, { error: "invalid id" });
2548
+ if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
2520
2549
  if (method === "PATCH") {
2521
2550
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2522
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2523
- const body = await readBody$2(req);
2551
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2552
+ const body = await readBody(req);
2524
2553
  const manifest = await readManifest(ctx.manifestPath);
2525
2554
  const folder = manifest.folders.find((f) => f.id === id);
2526
- if (!folder) return json$2(res, 404, { error: "folder not found" });
2555
+ if (!folder) return json(res, 404, { error: "folder not found" });
2527
2556
  if (body.name !== void 0) {
2528
2557
  const name = validateName(body.name);
2529
- if (!name) return json$2(res, 400, { error: "invalid name" });
2558
+ if (!name) return json(res, 400, { error: "invalid name" });
2530
2559
  folder.name = name;
2531
2560
  }
2532
2561
  if (body.icon !== void 0) {
2533
2562
  const icon = validateIcon(body.icon);
2534
- if (!icon) return json$2(res, 400, { error: "invalid icon" });
2563
+ if (!icon) return json(res, 400, { error: "invalid icon" });
2535
2564
  folder.icon = icon;
2536
2565
  }
2537
2566
  await writeManifest(ctx.manifestPath, manifest);
2538
- return json$2(res, 200, folder);
2567
+ return json(res, 200, folder);
2539
2568
  }
2540
2569
  if (method === "DELETE") {
2541
2570
  const requestCheck = validateMutationRequest(req);
2542
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2571
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2543
2572
  const manifest = await readManifest(ctx.manifestPath);
2544
2573
  const before = manifest.folders.length;
2545
2574
  manifest.folders = manifest.folders.filter((f) => f.id !== id);
2546
- if (manifest.folders.length === before) return json$2(res, 404, { error: "folder not found" });
2575
+ if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
2547
2576
  for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
2548
2577
  await writeManifest(ctx.manifestPath, manifest);
2549
- return json$2(res, 200, { ok: true });
2578
+ return json(res, 200, { ok: true });
2550
2579
  }
2551
2580
  }
2552
2581
  next();
2553
2582
  } catch (err) {
2554
- json$2(res, 500, { error: String(err.message ?? err) });
2583
+ json(res, 500, { error: String(err.message ?? err) });
2555
2584
  }
2556
2585
  });
2557
2586
  }
@@ -2566,30 +2595,30 @@ function registerSlideRoutes(server, ctx) {
2566
2595
  const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
2567
2596
  if (reorderMatch && method === "PUT") {
2568
2597
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2569
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2598
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2570
2599
  const slideId$1 = reorderMatch[1];
2571
- if (!SLIDE_ID_RE$3.test(slideId$1)) return json$2(res, 400, { error: "invalid slideId" });
2572
- const body = await readBody$2(req);
2573
- if (!Array.isArray(body.order)) return json$2(res, 400, { error: "invalid order" });
2600
+ if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
2601
+ const body = await readBody(req);
2602
+ if (!Array.isArray(body.order)) return json(res, 400, { error: "invalid order" });
2574
2603
  const order = [];
2575
2604
  for (const v of body.order) {
2576
- if (!Number.isInteger(v)) return json$2(res, 400, { error: "invalid order" });
2605
+ if (!Number.isInteger(v)) return json(res, 400, { error: "invalid order" });
2577
2606
  order.push(v);
2578
2607
  }
2579
2608
  const entry = resolveSlideEntry(ctx.slidesRoot, slideId$1);
2580
- if (!entry) return json$2(res, 400, { error: "invalid slideId" });
2609
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
2581
2610
  let source;
2582
2611
  try {
2583
2612
  source = await fs.readFile(entry, "utf8");
2584
2613
  } catch {
2585
- return json$2(res, 404, { error: "slide not found" });
2614
+ return json(res, 404, { error: "slide not found" });
2586
2615
  }
2587
2616
  const reordered = reorderDefaultExportPagesInSource(source, order);
2588
- if (reordered === null) return json$2(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
2617
+ if (reordered === null) return json(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
2589
2618
  const withNotes = reorderNotesArrayInSource(reordered, order);
2590
- if (withNotes === null) return json$2(res, 422, { error: "could not reorder pages — `notes` export has an unexpected shape" });
2619
+ if (withNotes === null) return json(res, 422, { error: "could not reorder pages — `notes` export has an unexpected shape" });
2591
2620
  if (withNotes !== source) await fs.writeFile(entry, withNotes, "utf8");
2592
- return json$2(res, 200, {
2621
+ return json(res, 200, {
2593
2622
  ok: true,
2594
2623
  slideId: slideId$1,
2595
2624
  order
@@ -2600,25 +2629,25 @@ function registerSlideRoutes(server, ctx) {
2600
2629
  const slideId$1 = pageOpMatch[1];
2601
2630
  const pageIndex = Number.parseInt(pageOpMatch[2], 10);
2602
2631
  const op = pageOpMatch[3];
2603
- if (!SLIDE_ID_RE$3.test(slideId$1)) return json$2(res, 400, { error: "invalid slideId" });
2604
- if (!Number.isInteger(pageIndex) || pageIndex < 0) return json$2(res, 400, { error: "invalid page index" });
2632
+ if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
2633
+ if (!Number.isInteger(pageIndex) || pageIndex < 0) return json(res, 400, { error: "invalid page index" });
2605
2634
  const isDelete = method === "DELETE" && !op;
2606
2635
  const isDuplicate = method === "POST" && op === "duplicate";
2607
2636
  if (!isDelete && !isDuplicate) return next();
2608
2637
  const requestCheck = validateMutationRequest(req);
2609
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2638
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2610
2639
  const entry = resolveSlideEntry(ctx.slidesRoot, slideId$1);
2611
- if (!entry) return json$2(res, 400, { error: "invalid slideId" });
2640
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
2612
2641
  let source;
2613
2642
  try {
2614
2643
  source = await fs.readFile(entry, "utf8");
2615
2644
  } catch {
2616
- return json$2(res, 404, { error: "slide not found" });
2645
+ return json(res, 404, { error: "slide not found" });
2617
2646
  }
2618
2647
  const updated = isDelete ? removePageFromDefaultExportInSource(source, pageIndex) : duplicatePageInDefaultExportInSource(source, pageIndex);
2619
- if (updated === null) return json$2(res, 422, { error: isDelete ? "could not delete page — index out of range or default export is not an array" : "could not duplicate page — index out of range or default export is not an array" });
2648
+ if (updated === null) return json(res, 422, { error: isDelete ? "could not delete page — index out of range or default export is not an array" : "could not duplicate page — index out of range or default export is not an array" });
2620
2649
  if (updated !== source) await fs.writeFile(entry, updated, "utf8");
2621
- return json$2(res, 200, {
2650
+ return json(res, 200, {
2622
2651
  ok: true,
2623
2652
  slideId: slideId$1,
2624
2653
  index: pageIndex
@@ -2627,20 +2656,20 @@ function registerSlideRoutes(server, ctx) {
2627
2656
  const duplicateMatch = url.pathname.match(/^\/([^/]+)\/duplicate$/);
2628
2657
  if (duplicateMatch && method === "POST") {
2629
2658
  const requestCheck = validateMutationRequest(req);
2630
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2659
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2631
2660
  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" });
2661
+ if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
2662
+ const body = await readBody(req);
2663
+ if (body.newId !== void 0 && typeof body.newId !== "string") return json(res, 400, { error: "invalid newId" });
2635
2664
  const duplicated = await duplicateSlideDir(ctx.slidesRoot, slideId$1, body.newId);
2636
- if (!duplicated.ok) return json$2(res, duplicated.status, { error: duplicated.error });
2665
+ if (!duplicated.ok) return json(res, duplicated.status, { error: duplicated.error });
2637
2666
  const manifest = await readManifest(ctx.manifestPath);
2638
2667
  const folderId = manifest.assignments[slideId$1];
2639
2668
  if (folderId) {
2640
2669
  manifest.assignments[duplicated.slideId] = folderId;
2641
2670
  await writeManifest(ctx.manifestPath, manifest);
2642
2671
  }
2643
- return json$2(res, 200, {
2672
+ return json(res, 200, {
2644
2673
  ok: true,
2645
2674
  slideId: duplicated.slideId
2646
2675
  });
@@ -2648,26 +2677,26 @@ function registerSlideRoutes(server, ctx) {
2648
2677
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
2649
2678
  if (!idMatch) return next();
2650
2679
  const slideId = idMatch[1];
2651
- if (!SLIDE_ID_RE$3.test(slideId)) return json$2(res, 400, { error: "invalid slideId" });
2680
+ if (!SLIDE_ID_RE.test(slideId)) return json(res, 400, { error: "invalid slideId" });
2652
2681
  if (method === "PATCH") {
2653
2682
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2654
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2655
- const body = await readBody$2(req);
2683
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2684
+ const body = await readBody(req);
2656
2685
  const name = validateSlideName(body.name);
2657
- if (!name) return json$2(res, 400, { error: "invalid name" });
2686
+ if (!name) return json(res, 400, { error: "invalid name" });
2658
2687
  const entry = resolveSlideEntry(ctx.slidesRoot, slideId);
2659
- if (!entry) return json$2(res, 400, { error: "invalid slideId" });
2688
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
2660
2689
  let source;
2661
2690
  try {
2662
2691
  source = await fs.readFile(entry, "utf8");
2663
2692
  } catch {
2664
- return json$2(res, 404, { error: "slide not found" });
2693
+ return json(res, 404, { error: "slide not found" });
2665
2694
  }
2666
2695
  const updated = updateMetaTitleInSource(source, name);
2667
- if (updated === null) return json$2(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
2696
+ if (updated === null) return json(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
2668
2697
  if (updated !== source) await fs.writeFile(entry, updated, "utf8");
2669
2698
  server.ws.send({ type: "full-reload" });
2670
- return json$2(res, 200, {
2699
+ return json(res, 200, {
2671
2700
  ok: true,
2672
2701
  slideId,
2673
2702
  name
@@ -2675,17 +2704,17 @@ function registerSlideRoutes(server, ctx) {
2675
2704
  }
2676
2705
  if (method === "DELETE") {
2677
2706
  const requestCheck = validateMutationRequest(req);
2678
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2707
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2679
2708
  const removed = await rmSlideDir(ctx.slidesRoot, slideId);
2680
- if (!removed) return json$2(res, 404, { error: "slide not found" });
2709
+ if (!removed) return json(res, 404, { error: "slide not found" });
2681
2710
  const manifest = await readManifest(ctx.manifestPath);
2682
2711
  delete manifest.assignments[slideId];
2683
2712
  await writeManifest(ctx.manifestPath, manifest);
2684
- return json$2(res, 200, { ok: true });
2713
+ return json(res, 200, { ok: true });
2685
2714
  }
2686
2715
  return next();
2687
2716
  } catch (err) {
2688
- json$2(res, 500, { error: String(err.message ?? err) });
2717
+ json(res, 500, { error: String(err.message ?? err) });
2689
2718
  }
2690
2719
  });
2691
2720
  }
@@ -2709,16 +2738,16 @@ function registerSvglRoutes(server) {
2709
2738
  target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
2710
2739
  } else if (reqUrl.pathname === "/svg") {
2711
2740
  const u = reqUrl.searchParams.get("u");
2712
- if (!u) return json$2(res, 400, { error: "missing u" });
2741
+ if (!u) return json(res, 400, { error: "missing u" });
2713
2742
  let parsed;
2714
2743
  try {
2715
2744
  parsed = new URL(u);
2716
2745
  } catch {
2717
- return json$2(res, 400, { error: "invalid u" });
2746
+ return json(res, 400, { error: "invalid u" });
2718
2747
  }
2719
- if (parsed.protocol !== "https:") return json$2(res, 400, { error: "https only" });
2748
+ if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
2720
2749
  const host = parsed.hostname.toLowerCase();
2721
- if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json$2(res, 400, { error: "host not allowed" });
2750
+ if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json(res, 400, { error: "host not allowed" });
2722
2751
  target = parsed.toString();
2723
2752
  } else return next();
2724
2753
  const upstream = await fetch(target);
@@ -2729,11 +2758,68 @@ function registerSvglRoutes(server) {
2729
2758
  const buf = Buffer.from(await upstream.arrayBuffer());
2730
2759
  res.end(buf);
2731
2760
  } catch (err) {
2732
- json$2(res, 502, { error: String(err.message ?? err) });
2761
+ json(res, 502, { error: String(err.message ?? err) });
2733
2762
  }
2734
2763
  });
2735
2764
  }
2736
2765
 
2766
+ //#endregion
2767
+ //#region src/vite/routes/update.ts
2768
+ const PKG = "@open-slide/core";
2769
+ const CACHE_TTL_MS = 10 * 60 * 1e3;
2770
+ let cache = null;
2771
+ function parseSemver(v) {
2772
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
2773
+ if (!m) return null;
2774
+ return [
2775
+ Number(m[1]),
2776
+ Number(m[2]),
2777
+ Number(m[3])
2778
+ ];
2779
+ }
2780
+ function isOutdated(current, latest) {
2781
+ const a = parseSemver(current);
2782
+ const b = parseSemver(latest);
2783
+ if (!a || !b) return false;
2784
+ for (let i = 0; i < 3; i++) {
2785
+ if (b[i] > a[i]) return true;
2786
+ if (b[i] < a[i]) return false;
2787
+ }
2788
+ return false;
2789
+ }
2790
+ async function fetchLatest(now) {
2791
+ if (cache && now - cache.at < CACHE_TTL_MS) return cache.latest;
2792
+ try {
2793
+ const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
2794
+ signal: AbortSignal.timeout(3e3),
2795
+ headers: { accept: "application/json" }
2796
+ });
2797
+ if (!res.ok) throw new Error(`registry ${res.status}`);
2798
+ const body = await res.json();
2799
+ const latest = typeof body.version === "string" ? body.version : null;
2800
+ cache = {
2801
+ at: now,
2802
+ latest
2803
+ };
2804
+ return latest;
2805
+ } catch {
2806
+ return cache?.latest ?? null;
2807
+ }
2808
+ }
2809
+ function registerUpdateRoutes(server, current) {
2810
+ server.middlewares.use("/__update-check", async (req, res, next) => {
2811
+ if ((req.method ?? "GET") !== "GET") return next();
2812
+ const latest = await fetchLatest(Date.now());
2813
+ const result = {
2814
+ current,
2815
+ latest,
2816
+ outdated: latest ? isOutdated(current, latest) : false
2817
+ };
2818
+ res.setHeader("cache-control", "no-store");
2819
+ json(res, 200, result);
2820
+ });
2821
+ }
2822
+
2737
2823
  //#endregion
2738
2824
  //#region src/vite/routes/watchers.ts
2739
2825
  function registerWatchers(server, ctx) {
@@ -2759,7 +2845,7 @@ function registerWatchers(server, ctx) {
2759
2845
  const parts = rel.split(path.sep);
2760
2846
  if (parts.length < 3 || parts[1] !== "assets") return;
2761
2847
  const slideId = parts[0];
2762
- if (!SLIDE_ID_RE$3.test(slideId)) return;
2848
+ if (!SLIDE_ID_RE.test(slideId)) return;
2763
2849
  server.ws.send({
2764
2850
  type: "custom",
2765
2851
  event: "open-slide:assets-changed",
@@ -2786,13 +2872,13 @@ function apiPlugin(opts) {
2786
2872
  registerAssetRoutes(server, ctx);
2787
2873
  registerSvglRoutes(server);
2788
2874
  registerFolderRoutes(server, ctx);
2875
+ registerUpdateRoutes(server, ctx.coreVersion);
2789
2876
  }
2790
2877
  };
2791
2878
  }
2792
2879
 
2793
2880
  //#endregion
2794
2881
  //#region src/vite/current-plugin.ts
2795
- const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
2796
2882
  const TEXT_SNIPPET_MAX = 120;
2797
2883
  function parseSelection(raw) {
2798
2884
  if (raw == null || typeof raw !== "object") return null;
@@ -2831,7 +2917,7 @@ function currentPlugin(opts) {
2831
2917
  selection: null
2832
2918
  };
2833
2919
  if (typeof raw?.slideId === "string") {
2834
- if (!SLIDE_ID_RE$2.test(raw.slideId)) return;
2920
+ if (!SLIDE_ID_RE.test(raw.slideId)) return;
2835
2921
  const totalPages = typeof raw.totalPages === "number" && Number.isFinite(raw.totalPages) && raw.totalPages > 0 ? Math.floor(raw.totalPages) : 1;
2836
2922
  const rawIndex = typeof raw.pageIndex === "number" && Number.isFinite(raw.pageIndex) ? Math.floor(raw.pageIndex) : 0;
2837
2923
  const pageIndex = Math.max(0, Math.min(totalPages - 1, rawIndex));
@@ -2866,35 +2952,6 @@ function currentPlugin(opts) {
2866
2952
 
2867
2953
  //#endregion
2868
2954
  //#region src/vite/design-plugin.ts
2869
- const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
2870
- async function readBody$1(req) {
2871
- return await new Promise((resolve, reject) => {
2872
- const chunks = [];
2873
- req.on("data", (c) => chunks.push(c));
2874
- req.on("end", () => {
2875
- const raw = Buffer.concat(chunks).toString("utf8");
2876
- if (!raw) return resolve({});
2877
- try {
2878
- resolve(JSON.parse(raw));
2879
- } catch (e) {
2880
- reject(e);
2881
- }
2882
- });
2883
- req.on("error", reject);
2884
- });
2885
- }
2886
- function json$1(res, status, body) {
2887
- res.statusCode = status;
2888
- res.setHeader("content-type", "application/json");
2889
- res.end(JSON.stringify(body));
2890
- }
2891
- function resolveSlidePath$1(userCwd, slidesDir, slideId) {
2892
- if (!SLIDE_ID_RE$1.test(slideId)) return null;
2893
- const slidesRoot = path.resolve(userCwd, slidesDir);
2894
- const full = path.resolve(slidesRoot, slideId, "index.tsx");
2895
- if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
2896
- return full;
2897
- }
2898
2955
  function parseSource$1(source) {
2899
2956
  try {
2900
2957
  return parse(source, {
@@ -3217,28 +3274,28 @@ function designPlugin(opts) {
3217
3274
  const url = new URL(req.url ?? "/", "http://local");
3218
3275
  const method = req.method ?? "GET";
3219
3276
  const slideId = url.searchParams.get("slideId") ?? "";
3220
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
3221
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
3277
+ const file = resolveSlidePath(userCwd, slidesDir, slideId);
3278
+ if (!file) return json(res, 400, { error: "invalid slideId" });
3222
3279
  try {
3223
3280
  if (method === "GET" && url.pathname === "/") {
3224
3281
  let source;
3225
3282
  try {
3226
3283
  source = await fs.readFile(file, "utf8");
3227
3284
  } catch {
3228
- return json$1(res, 404, { error: "slide not found" });
3285
+ return json(res, 404, { error: "slide not found" });
3229
3286
  }
3230
3287
  const parsed = parseSlideDesign(source);
3231
- if (parsed.ok) return json$1(res, 200, {
3288
+ if (parsed.ok) return json(res, 200, {
3232
3289
  design: parsed.design,
3233
3290
  exists: true,
3234
3291
  warning: null
3235
3292
  });
3236
- if (parsed.exists === false) return json$1(res, 200, {
3293
+ if (parsed.exists === false) return json(res, 200, {
3237
3294
  design: defaultDesign,
3238
3295
  exists: false,
3239
3296
  warning: null
3240
3297
  });
3241
- return json$1(res, 200, {
3298
+ return json(res, 200, {
3242
3299
  design: defaultDesign,
3243
3300
  exists: true,
3244
3301
  warning: parsed.error
@@ -3246,24 +3303,24 @@ function designPlugin(opts) {
3246
3303
  }
3247
3304
  if (method === "PUT" && url.pathname === "/") {
3248
3305
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
3249
- if (!requestCheck.ok) return json$1(res, requestCheck.status, { error: requestCheck.error });
3250
- const body = await readBody$1(req);
3306
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
3307
+ const body = await readBody(req);
3251
3308
  const patch = body.patch;
3252
- if (!patch || typeof patch !== "object") return json$1(res, 400, { error: "missing patch object" });
3309
+ if (!patch || typeof patch !== "object") return json(res, 400, { error: "missing patch object" });
3253
3310
  let source;
3254
3311
  try {
3255
3312
  source = await fs.readFile(file, "utf8");
3256
3313
  } catch {
3257
- return json$1(res, 404, { error: "slide not found" });
3314
+ return json(res, 404, { error: "slide not found" });
3258
3315
  }
3259
3316
  const parsed = parseSlideDesign(source);
3260
3317
  const baseDesign = parsed.ok ? parsed.design : defaultDesign;
3261
- if (!parsed.ok && parsed.exists) return json$1(res, 422, { error: parsed.error });
3318
+ if (!parsed.ok && parsed.exists) return json(res, 422, { error: parsed.error });
3262
3319
  const merged = mergeDesign(baseDesign, patch);
3263
3320
  const written = applyDesignWrite(source, merged);
3264
- if (!written.ok) return json$1(res, written.status, { error: written.error });
3321
+ if (!written.ok) return json(res, written.status, { error: written.error });
3265
3322
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
3266
- return json$1(res, 200, {
3323
+ return json(res, 200, {
3267
3324
  ok: true,
3268
3325
  design: merged,
3269
3326
  created: written.created
@@ -3271,17 +3328,17 @@ function designPlugin(opts) {
3271
3328
  }
3272
3329
  if (method === "POST" && url.pathname === "/reset") {
3273
3330
  const requestCheck = validateMutationRequest(req);
3274
- if (!requestCheck.ok) return json$1(res, requestCheck.status, { error: requestCheck.error });
3331
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
3275
3332
  let source;
3276
3333
  try {
3277
3334
  source = await fs.readFile(file, "utf8");
3278
3335
  } catch {
3279
- return json$1(res, 404, { error: "slide not found" });
3336
+ return json(res, 404, { error: "slide not found" });
3280
3337
  }
3281
3338
  const written = applyDesignWrite(source, defaultDesign);
3282
- if (!written.ok) return json$1(res, written.status, { error: written.error });
3339
+ if (!written.ok) return json(res, written.status, { error: written.error });
3283
3340
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
3284
- return json$1(res, 200, {
3341
+ return json(res, 200, {
3285
3342
  ok: true,
3286
3343
  design: defaultDesign,
3287
3344
  created: written.created
@@ -3289,7 +3346,7 @@ function designPlugin(opts) {
3289
3346
  }
3290
3347
  return next();
3291
3348
  } catch (err) {
3292
- json$1(res, 500, { error: String(err.message ?? err) });
3349
+ json(res, 500, { error: String(err.message ?? err) });
3293
3350
  }
3294
3351
  });
3295
3352
  }
@@ -3334,19 +3391,22 @@ function injectLocTags(code) {
3334
3391
  for (const ins of insertions) next = next.slice(0, ins.offset) + ins.text + next.slice(ins.offset);
3335
3392
  return next;
3336
3393
  }
3394
+ function isSlideSourceFile(id, slidesRootPosix) {
3395
+ const filePath = id.split(/[?#]/)[0].replace(/\\/g, "/");
3396
+ if (!filePath.startsWith(`${slidesRootPosix}/`)) return false;
3397
+ if (!filePath.endsWith(".tsx")) return false;
3398
+ if (filePath.endsWith(".d.ts") || filePath.endsWith(".test.tsx")) return false;
3399
+ const rel = filePath.slice(slidesRootPosix.length + 1);
3400
+ return rel.includes("/");
3401
+ }
3337
3402
  function locTagsPlugin(opts) {
3338
- const slidesRoot = path.resolve(opts.userCwd, opts.slidesDir ?? "slides");
3403
+ const slidesRoot = path.resolve(opts.userCwd, opts.slidesDir ?? "slides").replace(/\\/g, "/");
3339
3404
  return {
3340
3405
  name: "open-slide:loc-tags",
3341
3406
  apply: "serve",
3342
3407
  enforce: "pre",
3343
3408
  transform(code, id) {
3344
- const filePath = id.split("?")[0];
3345
- if (!filePath.startsWith(slidesRoot + path.sep)) 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;
3409
+ if (!isSlideSourceFile(id, slidesRoot)) return null;
3350
3410
  const next = injectLocTags(code);
3351
3411
  if (next === null) return null;
3352
3412
  return {
@@ -3359,35 +3419,6 @@ function locTagsPlugin(opts) {
3359
3419
 
3360
3420
  //#endregion
3361
3421
  //#region src/vite/notes-plugin.ts
3362
- const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
3363
- async function readBody(req) {
3364
- return await new Promise((resolve, reject) => {
3365
- const chunks = [];
3366
- req.on("data", (c) => chunks.push(c));
3367
- req.on("end", () => {
3368
- const raw = Buffer.concat(chunks).toString("utf8");
3369
- if (!raw) return resolve({});
3370
- try {
3371
- resolve(JSON.parse(raw));
3372
- } catch (e) {
3373
- reject(e);
3374
- }
3375
- });
3376
- req.on("error", reject);
3377
- });
3378
- }
3379
- function json(res, status, body) {
3380
- res.statusCode = status;
3381
- res.setHeader("content-type", "application/json");
3382
- res.end(JSON.stringify(body));
3383
- }
3384
- function resolveSlidePath(userCwd, slidesDir, slideId) {
3385
- if (!SLIDE_ID_RE.test(slideId)) return null;
3386
- const slidesRoot = path.resolve(userCwd, slidesDir);
3387
- const full = path.resolve(slidesRoot, slideId, "index.tsx");
3388
- if (!full.startsWith(slidesRoot + path.sep)) return null;
3389
- return full;
3390
- }
3391
3422
  function parseSource(source) {
3392
3423
  try {
3393
3424
  return parse(source, {
@@ -3700,7 +3731,7 @@ ${cases}
3700
3731
  `;
3701
3732
  }
3702
3733
  function openSlidePlugin(opts) {
3703
- const { userCwd, config } = opts;
3734
+ const { userCwd, config, coreVersion } = opts;
3704
3735
  const slidesDir = config.slidesDir ?? "slides";
3705
3736
  const slidesRoot = path.resolve(userCwd, slidesDir);
3706
3737
  const foldersManifestPath = path.join(slidesRoot, ".folders.json");
@@ -3761,7 +3792,8 @@ function openSlidePlugin(opts) {
3761
3792
  };
3762
3793
  const resolvedConfig = {
3763
3794
  ...config,
3764
- build: buildResolved
3795
+ build: buildResolved,
3796
+ version: coreVersion
3765
3797
  };
3766
3798
  return `export default ${JSON.stringify(resolvedConfig)};\n`;
3767
3799
  }
@@ -3978,6 +4010,15 @@ function findPackageRoot(fromFile) {
3978
4010
  }
3979
4011
  const PKG_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
3980
4012
  const APP_ROOT = path.join(PKG_ROOT, "src", "app");
4013
+ function readCoreVersion() {
4014
+ try {
4015
+ const raw = readFileSync(path.join(PKG_ROOT, "package.json"), "utf8");
4016
+ return JSON.parse(raw).version ?? "0.0.0";
4017
+ } catch {
4018
+ return "0.0.0";
4019
+ }
4020
+ }
4021
+ const CORE_VERSION = readCoreVersion();
3981
4022
  async function createViteConfig(opts) {
3982
4023
  const userCwd = path.resolve(opts.userCwd);
3983
4024
  const config = opts.config ?? await loadUserConfig(userCwd);
@@ -4000,7 +4041,8 @@ async function createViteConfig(opts) {
4000
4041
  tailwindcss(),
4001
4042
  openSlidePlugin({
4002
4043
  userCwd,
4003
- config
4044
+ config,
4045
+ coreVersion: CORE_VERSION
4004
4046
  }),
4005
4047
  themesPlugin({
4006
4048
  userCwd,
@@ -4010,7 +4052,8 @@ async function createViteConfig(opts) {
4010
4052
  apiPlugin({
4011
4053
  userCwd,
4012
4054
  slidesDir,
4013
- assetsDir
4055
+ assetsDir,
4056
+ coreVersion: CORE_VERSION
4014
4057
  }),
4015
4058
  notesPlugin({
4016
4059
  userCwd,