@open-slide/core 0.0.3 → 0.0.4
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.
- package/dist/{build-Cav2jYyI.js → build-CuoESF2g.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-g-uy_P5U.js → config-DF58h0l4.js} +132 -5
- package/dist/{dev-CFmlBbLh.js → dev-rlOZacWo.js} +1 -1
- package/dist/{preview-CotwHU_d.js → preview-DCrD9X36.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +3 -2
- package/src/app/components/ClickNavZones.tsx +34 -0
- package/src/app/components/Player.tsx +22 -3
- package/src/app/components/inspector/CommentPopover.tsx +3 -11
- package/src/app/components/inspector/InspectOverlay.tsx +13 -2
- package/src/app/components/inspector/InspectorProvider.tsx +8 -1
- package/src/app/components/sidebar/FolderItem.tsx +1 -4
- package/src/app/components/ui/dialog.tsx +141 -0
- package/src/app/components/ui/dropdown-menu.tsx +41 -70
- package/src/app/components/ui/popover.tsx +22 -37
- package/src/app/components/ui/tabs.tsx +26 -36
- package/src/app/lib/export-html.ts +313 -0
- package/src/app/lib/folders.ts +40 -4
- package/src/app/lib/sdk.ts +1 -3
- package/src/app/routes/Home.tsx +453 -65
- package/src/app/routes/Slide.tsx +151 -38
package/dist/cli/bin.js
CHANGED
|
@@ -30,17 +30,17 @@ async function run(argv) {
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
if (cmd === "dev") {
|
|
33
|
-
const { dev } = await import("../dev-
|
|
33
|
+
const { dev } = await import("../dev-rlOZacWo.js");
|
|
34
34
|
await dev();
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
if (cmd === "build") {
|
|
38
|
-
const { build } = await import("../build-
|
|
38
|
+
const { build } = await import("../build-CuoESF2g.js");
|
|
39
39
|
await build();
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
if (cmd === "preview") {
|
|
43
|
-
const { preview } = await import("../preview-
|
|
43
|
+
const { preview } = await import("../preview-DCrD9X36.js");
|
|
44
44
|
await preview();
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
@@ -179,7 +179,7 @@ function commentsPlugin(opts) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
//#endregion
|
|
182
|
-
//#region src/vite/
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
607
|
+
filesPlugin({
|
|
481
608
|
userCwd,
|
|
482
609
|
slidesDir
|
|
483
610
|
})
|
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-slide/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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
|
-
"
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
28
|
"prepack": "pnpm build"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"clsx": "^2.1.1",
|
|
49
49
|
"emoji-picker-react": "^4.18.0",
|
|
50
50
|
"fast-glob": "^3.3.2",
|
|
51
|
+
"fflate": "^0.8.2",
|
|
51
52
|
"lucide-react": "^1.8.0",
|
|
52
53
|
"radix-ui": "^1.4.3",
|
|
53
54
|
"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 (
|
|
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,22 @@ 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 ref={rootRef} className="relative flex h-screen w-screen items-center justify-center bg-black">
|
|
58
63
|
<SlideCanvas flat>{PageComp ? <PageComp /> : null}</SlideCanvas>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
aria-label="Previous page"
|
|
67
|
+
onClick={() => index > 0 && onIndexChange(index - 1)}
|
|
68
|
+
disabled={index === 0}
|
|
69
|
+
className="absolute inset-y-0 left-0 z-10 w-[30%]"
|
|
70
|
+
/>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
aria-label="Next page"
|
|
74
|
+
onClick={() => index < pages.length - 1 && onIndexChange(index + 1)}
|
|
75
|
+
disabled={index === pages.length - 1}
|
|
76
|
+
className="absolute inset-y-0 right-0 z-10 w-[30%]"
|
|
77
|
+
/>
|
|
59
78
|
</div>
|
|
60
79
|
);
|
|
61
80
|
}
|
|
@@ -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={
|
|
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={
|
|
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);
|
|
@@ -46,11 +54,13 @@ export function InspectOverlay() {
|
|
|
46
54
|
|
|
47
55
|
window.addEventListener('pointermove', onMove, true);
|
|
48
56
|
window.addEventListener('click', onClick, true);
|
|
57
|
+
window.addEventListener('keydown', onKey, true);
|
|
49
58
|
return () => {
|
|
50
59
|
window.removeEventListener('pointermove', onMove, true);
|
|
51
60
|
window.removeEventListener('click', onClick, true);
|
|
61
|
+
window.removeEventListener('keydown', onKey, true);
|
|
52
62
|
};
|
|
53
|
-
}, [active, slideId, pending, setPending]);
|
|
63
|
+
}, [active, slideId, pending, setPending, cancel]);
|
|
54
64
|
|
|
55
65
|
if (!active) return null;
|
|
56
66
|
|
|
@@ -88,6 +98,7 @@ function pickElement(x: number, y: number): HTMLElement | null {
|
|
|
88
98
|
for (const el of stack) {
|
|
89
99
|
if (!(el instanceof HTMLElement)) continue;
|
|
90
100
|
if (el.closest('[data-inspector-ui]')) continue;
|
|
101
|
+
if (!el.closest('[data-inspector-root]')) continue;
|
|
91
102
|
return el;
|
|
92
103
|
}
|
|
93
104
|
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
|
) : (
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { XIcon } from 'lucide-react';
|
|
2
|
+
import { Dialog as DialogPrimitive } from 'radix-ui';
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
8
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
12
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
16
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
20
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function DialogOverlay({
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
27
|
+
return (
|
|
28
|
+
<DialogPrimitive.Overlay
|
|
29
|
+
data-slot="dialog-overlay"
|
|
30
|
+
className={cn(
|
|
31
|
+
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function DialogContent({
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
showCloseButton = true,
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
45
|
+
showCloseButton?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<DialogPortal data-slot="dialog-portal">
|
|
49
|
+
<DialogOverlay />
|
|
50
|
+
<DialogPrimitive.Content
|
|
51
|
+
data-slot="dialog-content"
|
|
52
|
+
className={cn(
|
|
53
|
+
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
{showCloseButton && (
|
|
60
|
+
<DialogPrimitive.Close
|
|
61
|
+
data-slot="dialog-close"
|
|
62
|
+
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
63
|
+
>
|
|
64
|
+
<XIcon />
|
|
65
|
+
<span className="sr-only">Close</span>
|
|
66
|
+
</DialogPrimitive.Close>
|
|
67
|
+
)}
|
|
68
|
+
</DialogPrimitive.Content>
|
|
69
|
+
</DialogPortal>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
data-slot="dialog-header"
|
|
77
|
+
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function DialogFooter({
|
|
84
|
+
className,
|
|
85
|
+
showCloseButton = false,
|
|
86
|
+
children,
|
|
87
|
+
...props
|
|
88
|
+
}: React.ComponentProps<'div'> & {
|
|
89
|
+
showCloseButton?: boolean;
|
|
90
|
+
}) {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
data-slot="dialog-footer"
|
|
94
|
+
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
|
95
|
+
{...props}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
{showCloseButton && (
|
|
99
|
+
<DialogPrimitive.Close asChild>
|
|
100
|
+
<Button variant="outline">Close</Button>
|
|
101
|
+
</DialogPrimitive.Close>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
108
|
+
return (
|
|
109
|
+
<DialogPrimitive.Title
|
|
110
|
+
data-slot="dialog-title"
|
|
111
|
+
className={cn('text-lg leading-none font-semibold', className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function DialogDescription({
|
|
118
|
+
className,
|
|
119
|
+
...props
|
|
120
|
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
121
|
+
return (
|
|
122
|
+
<DialogPrimitive.Description
|
|
123
|
+
data-slot="dialog-description"
|
|
124
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
Dialog,
|
|
132
|
+
DialogClose,
|
|
133
|
+
DialogContent,
|
|
134
|
+
DialogDescription,
|
|
135
|
+
DialogFooter,
|
|
136
|
+
DialogHeader,
|
|
137
|
+
DialogOverlay,
|
|
138
|
+
DialogPortal,
|
|
139
|
+
DialogTitle,
|
|
140
|
+
DialogTrigger,
|
|
141
|
+
};
|