@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.
@@ -1,4 +1,4 @@
1
- import { createViteConfig } from "./config-g-uy_P5U.js";
1
+ import { createViteConfig } from "./config-DF58h0l4.js";
2
2
  import { build as build$1 } from "vite";
3
3
 
4
4
  //#region src/cli/build.ts
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-CFmlBbLh.js");
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-Cav2jYyI.js");
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-CotwHU_d.js");
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/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
  })
@@ -1,4 +1,4 @@
1
- import { createViteConfig } from "./config-g-uy_P5U.js";
1
+ import { createViteConfig } from "./config-DF58h0l4.js";
2
2
  import { createServer } from "vite";
3
3
 
4
4
  //#region src/cli/dev.ts
@@ -1,4 +1,4 @@
1
- import { createViteConfig } from "./config-g-uy_P5U.js";
1
+ import { createViteConfig } from "./config-DF58h0l4.js";
2
2
  import { preview as preview$1 } from "vite";
3
3
 
4
4
  //#region src/cli/preview.ts
@@ -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.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
- "check": "tsc --noEmit",
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 (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,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={() => 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);
@@ -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
+ };