@open-slide/core 0.0.3 → 0.0.5

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.
@@ -0,0 +1,16 @@
1
+ import { createViteConfig } from "./config-DF58h0l4.js";
2
+ import path from "node:path";
3
+ import { build as build$1, mergeConfig } from "vite";
4
+
5
+ //#region src/cli/build.ts
6
+ async function build(opts = {}) {
7
+ const base = await createViteConfig({
8
+ userCwd: process.cwd(),
9
+ mode: "build"
10
+ });
11
+ const config = mergeConfig(base, { build: { ...opts.outDir !== void 0 ? { outDir: path.resolve(process.cwd(), opts.outDir) } : {} } });
12
+ await build$1(config);
13
+ }
14
+
15
+ //#endregion
16
+ export { build };
package/dist/cli/bin.js CHANGED
@@ -1,57 +1,46 @@
1
1
  #!/usr/bin/env node
2
+ import chalk from "chalk";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
6
+ import { Command, Option } from "commander";
5
7
 
6
8
  //#region src/cli/run.ts
7
- const HELP = `open-slide — author slides, we handle the Vite/React stack
8
-
9
- Usage:
10
- open-slide dev Start dev server
11
- open-slide build Build a static site
12
- open-slide preview Preview the production build
13
- open-slide --help Show this message
14
- open-slide --version Print version
15
- `;
16
9
  async function readVersion() {
17
10
  const here = path.dirname(fileURLToPath(import.meta.url));
18
11
  const pkgPath = path.resolve(here, "..", "..", "package.json");
19
12
  const raw = await readFile(pkgPath, "utf8");
20
13
  return JSON.parse(raw).version;
21
14
  }
15
+ function parsePort(value) {
16
+ const n = Number(value);
17
+ if (!Number.isInteger(n) || n < 0 || n > 65535) throw new Error(`Invalid port: ${value}`);
18
+ return n;
19
+ }
22
20
  async function run(argv) {
23
- const [cmd] = argv;
24
- if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
25
- process.stdout.write(HELP);
26
- return;
27
- }
28
- if (cmd === "--version" || cmd === "-v") {
29
- process.stdout.write(`${await readVersion()}\n`);
30
- return;
31
- }
32
- if (cmd === "dev") {
33
- const { dev } = await import("../dev-CFmlBbLh.js");
34
- await dev();
35
- return;
36
- }
37
- if (cmd === "build") {
38
- const { build } = await import("../build-Cav2jYyI.js");
39
- await build();
40
- return;
41
- }
42
- if (cmd === "preview") {
43
- const { preview } = await import("../preview-CotwHU_d.js");
44
- await preview();
45
- return;
46
- }
47
- process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`);
48
- process.exit(1);
21
+ const version = await readVersion();
22
+ const program = new Command();
23
+ 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)"));
24
+ 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").action(async (flags) => {
25
+ const { dev } = await import("../dev-h-rxb3xY.js");
26
+ await dev(flags);
27
+ });
28
+ program.command("build").description("Build a static site").option("--out-dir <dir>", "output directory (defaults to `dist`)").action(async (flags) => {
29
+ const { build } = await import("../build-BCORlVF3.js");
30
+ await build(flags);
31
+ });
32
+ 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) => {
33
+ const { preview } = await import("../preview-lskE0s8A.js");
34
+ await preview(flags);
35
+ });
36
+ await program.parseAsync(argv, { from: "user" });
49
37
  }
50
38
 
51
39
  //#endregion
52
40
  //#region src/cli/bin.ts
53
41
  run(process.argv.slice(2)).catch((err) => {
54
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
42
+ const message = err instanceof Error ? err.message : String(err);
43
+ process.stderr.write(`${chalk.red("error:")} ${message}\n`);
55
44
  process.exit(1);
56
45
  });
57
46
 
@@ -179,7 +179,7 @@ function commentsPlugin(opts) {
179
179
  }
180
180
 
181
181
  //#endregion
182
- //#region src/vite/folders-plugin.ts
182
+ //#region src/vite/files-plugin.ts
183
183
  const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
184
184
  const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
185
185
  const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
@@ -236,6 +236,90 @@ function validateName(v) {
236
236
  if (trimmed.length < 1 || trimmed.length > 40) return null;
237
237
  return trimmed;
238
238
  }
239
+ function validateSlideName(v) {
240
+ if (typeof v !== "string") return null;
241
+ const trimmed = v.trim();
242
+ if (trimmed.length < 1 || trimmed.length > 80) return null;
243
+ return trimmed;
244
+ }
245
+ async function rmSlideDir(slidesRoot, slideId) {
246
+ if (!SLIDE_ID_RE.test(slideId)) return false;
247
+ const dir = path.resolve(slidesRoot, slideId);
248
+ if (!dir.startsWith(slidesRoot + path.sep)) return false;
249
+ try {
250
+ await fs.rm(dir, {
251
+ recursive: true,
252
+ force: true
253
+ });
254
+ return true;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ function resolveSlideEntry(slidesRoot, slideId) {
260
+ if (!SLIDE_ID_RE.test(slideId)) return null;
261
+ const dir = path.resolve(slidesRoot, slideId);
262
+ if (!dir.startsWith(slidesRoot + path.sep)) return null;
263
+ return path.join(dir, "index.tsx");
264
+ }
265
+ function escapeSingleQuoted(s) {
266
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
267
+ }
268
+ /**
269
+ * Rewrite (or insert) the `title` field in the slide module's `export const meta`.
270
+ *
271
+ * Strategy:
272
+ * 1. Find `export const meta` and brace-match its object literal.
273
+ * 2. If the object already has a `title: '...'` entry, replace the literal.
274
+ * 3. If the object exists but has no title, inject a new `title: '...'` line
275
+ * as the first property (preserving the author's surrounding indentation).
276
+ * 4. If there is no `meta` export at all, insert a fresh one right before
277
+ * `export default`.
278
+ *
279
+ * Returns the rewritten source, or `null` if the file shape was too surprising
280
+ * to touch safely (e.g. `export default` missing when we'd need to inject meta).
281
+ */
282
+ function updateMetaTitleInSource(source, title) {
283
+ const newLiteral = `'${escapeSingleQuoted(title)}'`;
284
+ const metaStart = source.search(/export\s+const\s+meta\b/);
285
+ if (metaStart !== -1) {
286
+ const eqIdx = source.indexOf("=", metaStart);
287
+ if (eqIdx === -1) return null;
288
+ const openBrace = source.indexOf("{", eqIdx);
289
+ if (openBrace === -1) return null;
290
+ let depth = 0;
291
+ let closeBrace = -1;
292
+ for (let i = openBrace; i < source.length; i++) {
293
+ const ch = source[i];
294
+ if (ch === "{") depth++;
295
+ else if (ch === "}") {
296
+ depth--;
297
+ if (depth === 0) {
298
+ closeBrace = i;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ if (closeBrace === -1) return null;
304
+ const body = source.slice(openBrace + 1, closeBrace);
305
+ const titleRe = /(^|[\s,{])(title\s*:\s*)(['"`])((?:\\.|(?!\3).)*)\3/;
306
+ const match = body.match(titleRe);
307
+ if (match) {
308
+ const newBody = body.replace(titleRe, `${match[1]}${match[2]}${newLiteral}`);
309
+ return source.slice(0, openBrace + 1) + newBody + source.slice(closeBrace);
310
+ }
311
+ const firstIndentMatch = body.match(/\n([ \t]+)\S/);
312
+ const indent = firstIndentMatch ? firstIndentMatch[1] : " ";
313
+ const trimmedBody = body.replace(/^\s*\n?/, "");
314
+ const needsSeparator = trimmedBody.trim().length > 0;
315
+ const insertion$1 = `\n${indent}title: ${newLiteral}${needsSeparator ? "," : ""}`;
316
+ return source.slice(0, openBrace + 1) + insertion$1 + body + source.slice(closeBrace);
317
+ }
318
+ const exportDefaultIdx = source.search(/export\s+default\b/);
319
+ if (exportDefaultIdx === -1) return null;
320
+ const insertion = `export const meta: SlideMeta = { title: ${newLiteral} };\n\n`;
321
+ return source.slice(0, exportDefaultIdx) + insertion + source.slice(exportDefaultIdx);
322
+ }
239
323
  function validateIcon(v) {
240
324
  if (!v || typeof v !== "object") return null;
241
325
  const icon = v;
@@ -256,22 +340,65 @@ function validateIcon(v) {
256
340
  }
257
341
  return null;
258
342
  }
259
- function foldersPlugin(opts) {
343
+ function filesPlugin(opts) {
260
344
  const userCwd = opts.userCwd;
261
345
  const slidesDir = opts.slidesDir ?? "slides";
262
346
  const slidesRoot = path.resolve(userCwd, slidesDir);
263
347
  const manifestPath = path.join(slidesRoot, ".folders.json");
264
348
  return {
265
- name: "open-slide:folders",
349
+ name: "open-slide:files",
266
350
  apply: "serve",
267
351
  configureServer(server) {
268
352
  server.watcher.add(manifestPath);
269
353
  server.watcher.on("change", (p) => {
270
354
  if (p === manifestPath) server.ws.send({
271
355
  type: "custom",
272
- event: "open-slide:folders-changed"
356
+ event: "open-slide:files-changed"
273
357
  });
274
358
  });
359
+ server.middlewares.use("/__slides", async (req, res, next) => {
360
+ const url = new URL(req.url ?? "/", "http://local");
361
+ const method = req.method ?? "GET";
362
+ try {
363
+ const idMatch = url.pathname.match(/^\/([^/]+)$/);
364
+ if (!idMatch) return next();
365
+ const slideId = idMatch[1];
366
+ if (!SLIDE_ID_RE.test(slideId)) return json(res, 400, { error: "invalid slideId" });
367
+ if (method === "PATCH") {
368
+ const body = await readBody(req);
369
+ const name = validateSlideName(body.name);
370
+ if (!name) return json(res, 400, { error: "invalid name" });
371
+ const entry = resolveSlideEntry(slidesRoot, slideId);
372
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
373
+ let source;
374
+ try {
375
+ source = await fs.readFile(entry, "utf8");
376
+ } catch {
377
+ return json(res, 404, { error: "slide not found" });
378
+ }
379
+ const updated = updateMetaTitleInSource(source, name);
380
+ if (updated === null) return json(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
381
+ if (updated !== source) await fs.writeFile(entry, updated, "utf8");
382
+ server.ws.send({ type: "full-reload" });
383
+ return json(res, 200, {
384
+ ok: true,
385
+ slideId,
386
+ name
387
+ });
388
+ }
389
+ if (method === "DELETE") {
390
+ const removed = await rmSlideDir(slidesRoot, slideId);
391
+ if (!removed) return json(res, 404, { error: "slide not found" });
392
+ const manifest = await readManifest(manifestPath);
393
+ delete manifest.assignments[slideId];
394
+ await writeManifest(manifestPath, manifest);
395
+ return json(res, 200, { ok: true });
396
+ }
397
+ return next();
398
+ } catch (err) {
399
+ json(res, 500, { error: String(err.message ?? err) });
400
+ }
401
+ });
275
402
  server.middlewares.use("/__folders", async (req, res, next) => {
276
403
  const url = new URL(req.url ?? "/", "http://local");
277
404
  const method = req.method ?? "GET";
@@ -477,7 +604,7 @@ async function createViteConfig(opts) {
477
604
  userCwd,
478
605
  slidesDir
479
606
  }),
480
- foldersPlugin({
607
+ filesPlugin({
481
608
  userCwd,
482
609
  slidesDir
483
610
  })
@@ -0,0 +1,19 @@
1
+ import { createViteConfig } from "./config-DF58h0l4.js";
2
+ import { createServer, mergeConfig } from "vite";
3
+
4
+ //#region src/cli/dev.ts
5
+ async function dev(opts = {}) {
6
+ const base = await createViteConfig({ userCwd: process.cwd() });
7
+ const config = mergeConfig(base, { server: {
8
+ ...opts.port !== void 0 ? { port: opts.port } : {},
9
+ ...opts.host !== void 0 ? { host: opts.host } : {},
10
+ ...opts.open !== void 0 ? { open: opts.open } : {}
11
+ } });
12
+ const server = await createServer(config);
13
+ await server.listen();
14
+ server.printUrls();
15
+ server.bindCLIShortcuts({ print: true });
16
+ }
17
+
18
+ //#endregion
19
+ export { dev };
@@ -0,0 +1,17 @@
1
+ import { createViteConfig } from "./config-DF58h0l4.js";
2
+ import { mergeConfig, preview as preview$1 } from "vite";
3
+
4
+ //#region src/cli/preview.ts
5
+ async function preview(opts = {}) {
6
+ const base = await createViteConfig({ userCwd: process.cwd() });
7
+ const config = mergeConfig(base, { preview: {
8
+ ...opts.port !== void 0 ? { port: opts.port } : {},
9
+ ...opts.host !== void 0 ? { host: opts.host } : {},
10
+ ...opts.open !== void 0 ? { open: opts.open } : {}
11
+ } });
12
+ const server = await preview$1(config);
13
+ server.printUrls();
14
+ }
15
+
16
+ //#endregion
17
+ export { preview };
@@ -1,3 +1,3 @@
1
- import { createViteConfig } from "../config-g-uy_P5U.js";
1
+ import { createViteConfig } from "../config-DF58h0l4.js";
2
2
 
3
3
  export { createViteConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-slide/core",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Runtime and CLI for open-slide — write slides in slides/, we handle the rest.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "build": "tsdown",
27
- "check": "tsc --noEmit",
27
+ "typecheck": "tsc --noEmit",
28
28
  "prepack": "pnpm build"
29
29
  },
30
30
  "engines": {
@@ -44,10 +44,13 @@
44
44
  "@fontsource-variable/geist": "^5.2.8",
45
45
  "@tailwindcss/vite": "^4.2.2",
46
46
  "@vitejs/plugin-react": "^4.3.3",
47
+ "chalk": "^5.3.0",
47
48
  "class-variance-authority": "^0.7.1",
48
49
  "clsx": "^2.1.1",
50
+ "commander": "^12.1.0",
49
51
  "emoji-picker-react": "^4.18.0",
50
52
  "fast-glob": "^3.3.2",
53
+ "fflate": "^0.8.2",
51
54
  "lucide-react": "^1.8.0",
52
55
  "radix-ui": "^1.4.3",
53
56
  "react": "^18.3.1",
@@ -0,0 +1,34 @@
1
+ import { useInspector } from './inspector/InspectorProvider';
2
+
3
+ type Props = {
4
+ onPrev: () => void;
5
+ onNext: () => void;
6
+ canPrev: boolean;
7
+ canNext: boolean;
8
+ };
9
+
10
+ export function ClickNavZones({ onPrev, onNext, canPrev, canNext }: Props) {
11
+ const { active } = useInspector();
12
+ if (active) return null;
13
+
14
+ return (
15
+ <>
16
+ <button
17
+ type="button"
18
+ aria-label="Previous page"
19
+ onClick={onPrev}
20
+ disabled={!canPrev}
21
+ data-inspector-ui
22
+ className="absolute inset-y-0 left-0 z-20 w-[18%] min-w-12"
23
+ />
24
+ <button
25
+ type="button"
26
+ aria-label="Next page"
27
+ onClick={onNext}
28
+ disabled={!canNext}
29
+ data-inspector-ui
30
+ className="absolute inset-y-0 right-0 z-20 w-[18%] min-w-12"
31
+ />
32
+ </>
33
+ );
34
+ }
@@ -33,10 +33,15 @@ export function Player({ pages, index, onIndexChange, onExit }: Props) {
33
33
 
34
34
  useEffect(() => {
35
35
  const onKey = (e: KeyboardEvent) => {
36
- if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') {
36
+ if (
37
+ e.key === 'ArrowRight' ||
38
+ e.key === 'ArrowDown' ||
39
+ e.key === ' ' ||
40
+ e.key === 'PageDown'
41
+ ) {
37
42
  e.preventDefault();
38
43
  if (index < pages.length - 1) onIndexChange(index + 1);
39
- } else if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
44
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
40
45
  e.preventDefault();
41
46
  if (index > 0) onIndexChange(index - 1);
42
47
  } else if (e.key === 'Escape') {
@@ -54,8 +59,25 @@ export function Player({ pages, index, onIndexChange, onExit }: Props) {
54
59
  const PageComp = pages[index];
55
60
 
56
61
  return (
57
- <div ref={rootRef} className="flex h-screen w-screen items-center justify-center bg-black">
62
+ <div
63
+ ref={rootRef}
64
+ className="relative flex h-screen w-screen items-center justify-center bg-black"
65
+ >
58
66
  <SlideCanvas flat>{PageComp ? <PageComp /> : null}</SlideCanvas>
67
+ <button
68
+ type="button"
69
+ aria-label="Previous page"
70
+ onClick={() => index > 0 && onIndexChange(index - 1)}
71
+ disabled={index === 0}
72
+ className="absolute inset-y-0 left-0 z-10 w-[30%]"
73
+ />
74
+ <button
75
+ type="button"
76
+ aria-label="Next page"
77
+ onClick={() => index < pages.length - 1 && onIndexChange(index + 1)}
78
+ disabled={index === pages.length - 1}
79
+ className="absolute inset-y-0 right-0 z-10 w-[30%]"
80
+ />
59
81
  </div>
60
82
  );
61
83
  }
@@ -1,3 +1,4 @@
1
+ import { useEffect, useRef } from 'react';
1
2
  import { cn } from '@/lib/utils';
2
3
  import { ScrollArea } from '@/components/ui/scroll-area';
3
4
  import type { Page } from '../lib/sdk';
@@ -15,6 +16,12 @@ const THUMB_SCALE = THUMB_WIDTH / CANVAS_WIDTH;
15
16
  const THUMB_HEIGHT = CANVAS_HEIGHT * THUMB_SCALE;
16
17
 
17
18
  export function ThumbnailRail({ pages, current, onSelect }: Props) {
19
+ const activeRef = useRef<HTMLButtonElement | null>(null);
20
+
21
+ useEffect(() => {
22
+ activeRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
23
+ }, [current]);
24
+
18
25
  return (
19
26
  <ScrollArea className="h-full border-r bg-card">
20
27
  <aside className="flex flex-col gap-2.5 p-3">
@@ -23,6 +30,7 @@ export function ThumbnailRail({ pages, current, onSelect }: Props) {
23
30
  return (
24
31
  <button
25
32
  key={i}
33
+ ref={active ? activeRef : undefined}
26
34
  onClick={() => onSelect(i)}
27
35
  aria-label={`Go to page ${i + 1}`}
28
36
  aria-current={active ? 'true' : undefined}
@@ -6,7 +6,7 @@ const POPOVER_W = 320;
6
6
  const POPOVER_H = 180;
7
7
 
8
8
  export function CommentPopover() {
9
- const { pending, setPending, add } = useInspector();
9
+ const { pending, setPending, add, cancel } = useInspector();
10
10
  const [text, setText] = useState('');
11
11
  const [submitting, setSubmitting] = useState(false);
12
12
  const [error, setError] = useState<string | null>(null);
@@ -16,14 +16,6 @@ export function CommentPopover() {
16
16
  taRef.current?.focus();
17
17
  }, []);
18
18
 
19
- useEffect(() => {
20
- const onKey = (e: KeyboardEvent) => {
21
- if (e.key === 'Escape') setPending(null);
22
- };
23
- window.addEventListener('keydown', onKey);
24
- return () => window.removeEventListener('keydown', onKey);
25
- }, [setPending]);
26
-
27
19
  if (!pending) return null;
28
20
 
29
21
  const left = clamp(pending.clickX + 12, 8, window.innerWidth - POPOVER_W - 8);
@@ -55,7 +47,7 @@ export function CommentPopover() {
55
47
  <button
56
48
  type="button"
57
49
  className="text-xs text-muted-foreground hover:text-foreground"
58
- onClick={() => setPending(null)}
50
+ onClick={cancel}
59
51
  >
60
52
 
61
53
  </button>
@@ -77,7 +69,7 @@ export function CommentPopover() {
77
69
  <div className="mt-2 flex items-center justify-end gap-2">
78
70
  <button
79
71
  type="button"
80
- onClick={() => setPending(null)}
72
+ onClick={cancel}
81
73
  className="rounded border px-2 py-1 text-xs hover:bg-muted"
82
74
  >
83
75
  Cancel
@@ -6,7 +6,7 @@ import { useInspector } from './InspectorProvider';
6
6
  type Highlight = { rect: DOMRect; hit: SlideSourceHit };
7
7
 
8
8
  export function InspectOverlay() {
9
- const { active, slideId, pending, setPending } = useInspector();
9
+ const { active, slideId, pending, setPending, cancel } = useInspector();
10
10
  const overlayRef = useRef<HTMLDivElement>(null);
11
11
  const [hover, setHover] = useState<Highlight | null>(null);
12
12
 
@@ -16,6 +16,14 @@ export function InspectOverlay() {
16
16
  return;
17
17
  }
18
18
 
19
+ const onKey = (e: KeyboardEvent) => {
20
+ if (e.key === 'Escape') {
21
+ e.preventDefault();
22
+ e.stopPropagation();
23
+ cancel();
24
+ }
25
+ };
26
+
19
27
  const onMove = (e: PointerEvent) => {
20
28
  if (pending) return;
21
29
  const el = pickElement(e.clientX, e.clientY);
@@ -34,23 +42,26 @@ export function InspectOverlay() {
34
42
  if (!hit) return;
35
43
  e.preventDefault();
36
44
  e.stopPropagation();
45
+ const anchorRect = hit.anchor.getBoundingClientRect();
37
46
  setPending({
38
47
  line: hit.line,
39
48
  column: hit.column,
40
- anchorRect: hit.anchor.getBoundingClientRect(),
49
+ anchorRect,
41
50
  clickX: e.clientX,
42
51
  clickY: e.clientY,
43
52
  });
44
- setHover(null);
53
+ setHover({ rect: anchorRect, hit });
45
54
  };
46
55
 
47
56
  window.addEventListener('pointermove', onMove, true);
48
57
  window.addEventListener('click', onClick, true);
58
+ window.addEventListener('keydown', onKey, true);
49
59
  return () => {
50
60
  window.removeEventListener('pointermove', onMove, true);
51
61
  window.removeEventListener('click', onClick, true);
62
+ window.removeEventListener('keydown', onKey, true);
52
63
  };
53
- }, [active, slideId, pending, setPending]);
64
+ }, [active, slideId, pending, setPending, cancel]);
54
65
 
55
66
  if (!active) return null;
56
67
 
@@ -88,6 +99,7 @@ function pickElement(x: number, y: number): HTMLElement | null {
88
99
  for (const el of stack) {
89
100
  if (!(el instanceof HTMLElement)) continue;
90
101
  if (el.closest('[data-inspector-ui]')) continue;
102
+ if (!el.closest('[data-inspector-root]')) continue;
91
103
  return el;
92
104
  }
93
105
  return null;
@@ -15,6 +15,7 @@ type InspectorCtx = {
15
15
  slideId: string;
16
16
  active: boolean;
17
17
  toggle: () => void;
18
+ cancel: () => void;
18
19
  comments: SlideComment[];
19
20
  error: string | null;
20
21
  refetch: () => Promise<void>;
@@ -44,11 +45,17 @@ export function InspectorProvider({ slideId, children }: { slideId: string; chil
44
45
  });
45
46
  }, []);
46
47
 
48
+ const cancel = useCallback(() => {
49
+ setActive(false);
50
+ setPending(null);
51
+ }, []);
52
+
47
53
  const value = useMemo<InspectorCtx>(
48
54
  () => ({
49
55
  slideId,
50
56
  active,
51
57
  toggle,
58
+ cancel,
52
59
  comments,
53
60
  error,
54
61
  refetch,
@@ -57,7 +64,7 @@ export function InspectorProvider({ slideId, children }: { slideId: string; chil
57
64
  pending,
58
65
  setPending,
59
66
  }),
60
- [slideId, active, toggle, comments, error, refetch, add, remove, pending],
67
+ [slideId, active, toggle, cancel, comments, error, refetch, add, remove, pending],
61
68
  );
62
69
 
63
70
  return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
@@ -114,10 +114,7 @@ export function FolderItem({
114
114
  </button>
115
115
  </PopoverTrigger>
116
116
  <PopoverContent side="right" align="start" className="w-auto p-2">
117
- <IconPicker
118
- value={row.folder.icon}
119
- onChange={(next) => row.onChangeIcon(next)}
120
- />
117
+ <IconPicker value={row.folder.icon} onChange={(next) => row.onChangeIcon(next)} />
121
118
  </PopoverContent>
122
119
  </Popover>
123
120
  ) : (