@peaske7/readit 0.1.8 → 0.2.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 (118) hide show
  1. package/README.md +0 -3
  2. package/biome.json +1 -1
  3. package/bun.lock +43 -185
  4. package/docs/perf-baseline.md +75 -0
  5. package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
  6. package/e2e/perf/add-comment.spec.ts +118 -0
  7. package/e2e/perf/fixtures/generate.ts +331 -0
  8. package/e2e/perf/initial-load.spec.ts +49 -0
  9. package/e2e/perf/perf.setup.ts +23 -0
  10. package/e2e/perf/perf.teardown.ts +9 -0
  11. package/e2e/perf/scroll.spec.ts +39 -0
  12. package/e2e/perf/tab-switch.spec.ts +69 -0
  13. package/e2e/perf/text-selection.spec.ts +119 -0
  14. package/e2e/perf/utils/metrics.ts +286 -0
  15. package/e2e/perf/utils/perf-cli.ts +86 -0
  16. package/package.json +9 -18
  17. package/playwright.config.ts +12 -0
  18. package/src/App.tsx +124 -172
  19. package/src/{cli/index.ts → cli.ts} +37 -53
  20. package/src/components/ActionsMenu.tsx +6 -27
  21. package/src/components/DocumentViewer/DocumentViewer.tsx +77 -106
  22. package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
  23. package/src/components/Header.tsx +9 -20
  24. package/src/components/InlineEditor.tsx +5 -5
  25. package/src/components/MarginNote.tsx +71 -93
  26. package/src/components/MarginNotes.tsx +7 -34
  27. package/src/components/RawModal.tsx +9 -8
  28. package/src/components/ReanchorConfirm.tsx +2 -2
  29. package/src/components/SettingsModal.tsx +11 -89
  30. package/src/components/TabBar.tsx +4 -4
  31. package/src/components/TableOfContents.tsx +5 -5
  32. package/src/components/comments/CommentInput.tsx +7 -35
  33. package/src/components/comments/CommentListItem.tsx +9 -11
  34. package/src/components/comments/CommentManager.tsx +53 -37
  35. package/src/components/comments/CommentNav.tsx +14 -14
  36. package/src/components/ui/ActionLink.tsx +14 -18
  37. package/src/components/ui/Button.tsx +42 -43
  38. package/src/components/ui/Dialog.tsx +73 -113
  39. package/src/components/ui/DropdownMenu.tsx +113 -69
  40. package/src/components/ui/Text.tsx +30 -37
  41. package/src/contexts/CommentContext.tsx +75 -106
  42. package/src/contexts/LocaleContext.tsx +45 -4
  43. package/src/contexts/PositionsContext.tsx +16 -0
  44. package/src/contexts/SettingsContext.tsx +133 -0
  45. package/src/hooks/useClickOutside.ts +0 -4
  46. package/src/hooks/useCommentNavigation.ts +6 -29
  47. package/src/hooks/useComments.ts +6 -18
  48. package/src/hooks/useDocument.ts +35 -34
  49. package/src/hooks/useHeadings.test.ts +8 -50
  50. package/src/hooks/useHeadings.ts +5 -88
  51. package/src/hooks/useScrollSpy.ts +10 -14
  52. package/src/hooks/useTextSelection.ts +1 -38
  53. package/src/lib/__fixtures__/bench-data.ts +1 -41
  54. package/src/lib/anchor.bench.ts +57 -67
  55. package/src/lib/anchor.test.ts +5 -1
  56. package/src/lib/anchor.ts +13 -93
  57. package/src/lib/comment-storage.test.ts +4 -4
  58. package/src/lib/comment-storage.ts +2 -46
  59. package/src/lib/export.ts +7 -13
  60. package/src/lib/highlight/core.test.ts +1 -1
  61. package/src/lib/highlight/dom.ts +5 -68
  62. package/src/lib/highlight/highlighter.ts +102 -262
  63. package/src/lib/highlight/resolver.ts +112 -0
  64. package/src/lib/highlight/types.ts +0 -35
  65. package/src/lib/highlight/worker.ts +45 -0
  66. package/src/lib/i18n/en.ts +1 -50
  67. package/src/lib/i18n/ja.ts +1 -50
  68. package/src/lib/i18n/types.ts +1 -49
  69. package/src/lib/margin-layout.ts +5 -27
  70. package/src/lib/positions.ts +150 -0
  71. package/src/lib/utils.ts +2 -19
  72. package/src/schema.ts +81 -0
  73. package/src/{server/index.ts → server.ts} +74 -74
  74. package/src/{store/index.ts → store.ts} +14 -46
  75. package/vite.config.ts +8 -0
  76. package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
  77. package/src/components/DocumentViewer/InlineCode.tsx +0 -60
  78. package/src/components/DocumentViewer/index.ts +0 -1
  79. package/src/components/FloatingTOC.tsx +0 -61
  80. package/src/components/ShortcutCapture.tsx +0 -48
  81. package/src/components/ShortcutList.tsx +0 -198
  82. package/src/components/comments/CommentMinimap.tsx +0 -62
  83. package/src/components/ui/ActionBar.tsx +0 -16
  84. package/src/components/ui/SeparatorDot.tsx +0 -9
  85. package/src/contexts/LayoutContext.tsx +0 -88
  86. package/src/hooks/useClipboard.ts +0 -82
  87. package/src/hooks/useEditorScheme.ts +0 -51
  88. package/src/hooks/useFontPreference.ts +0 -59
  89. package/src/hooks/useKeybindings.ts +0 -108
  90. package/src/hooks/useKeyboardShortcuts.ts +0 -63
  91. package/src/hooks/useLayoutMode.ts +0 -44
  92. package/src/hooks/useLocalePreference.ts +0 -42
  93. package/src/hooks/useReanchorMode.ts +0 -33
  94. package/src/hooks/useScrollMetrics.ts +0 -56
  95. package/src/hooks/useThemePreference.ts +0 -66
  96. package/src/lib/comment-storage.bench.ts +0 -63
  97. package/src/lib/context.bench.ts +0 -41
  98. package/src/lib/context.test.ts +0 -224
  99. package/src/lib/context.ts +0 -193
  100. package/src/lib/editor-links.ts +0 -59
  101. package/src/lib/export.bench.ts +0 -35
  102. package/src/lib/highlight/colors.ts +0 -37
  103. package/src/lib/highlight/core.ts +0 -54
  104. package/src/lib/highlight/index.ts +0 -23
  105. package/src/lib/highlight/script-builder.ts +0 -485
  106. package/src/lib/html-processor.test.tsx +0 -170
  107. package/src/lib/html-processor.tsx +0 -95
  108. package/src/lib/i18n/completeness.test.ts +0 -51
  109. package/src/lib/i18n/translations.test.ts +0 -39
  110. package/src/lib/layout-constants.ts +0 -12
  111. package/src/lib/margin-layout.bench.ts +0 -28
  112. package/src/lib/scroll.test.ts +0 -118
  113. package/src/lib/scroll.ts +0 -47
  114. package/src/lib/shortcut-registry.test.ts +0 -173
  115. package/src/lib/shortcut-registry.ts +0 -209
  116. package/src/lib/utils.test.ts +0 -110
  117. package/src/store/index.test.ts +0 -242
  118. package/src/types/index.ts +0 -127
@@ -1,9 +1,8 @@
1
- import { BotMessageSquare, Copy } from "lucide-react";
2
1
  import { use, useEffect, useRef, useState } from "react";
3
- import { LayoutContext } from "../../contexts/LayoutContext";
4
2
  import { useLocale } from "../../contexts/LocaleContext";
3
+ import { SettingsContext } from "../../contexts/SettingsContext";
5
4
  import { cn } from "../../lib/utils";
6
- import { FontFamilies } from "../../types";
5
+ import { FontFamilies } from "../../schema";
7
6
  import { Button } from "../ui/Button";
8
7
  import { Text } from "../ui/Text";
9
8
 
@@ -11,21 +10,17 @@ interface CommentInputProps {
11
10
  selectedText: string | null;
12
11
  onSubmit: (commentText: string) => void;
13
12
  onCancel: () => void;
14
- onCopyRaw: () => void;
15
- onCopyForLLM: () => void;
16
13
  }
17
14
 
18
15
  export function CommentInput({
19
16
  selectedText,
20
17
  onSubmit,
21
18
  onCancel,
22
- onCopyRaw,
23
- onCopyForLLM,
24
19
  }: CommentInputProps) {
25
20
  const { t } = useLocale();
26
- const layout = use(LayoutContext);
27
- const fontClass = layout
28
- ? layout.fontFamily === FontFamilies.SANS_SERIF
21
+ const settings = use(SettingsContext);
22
+ const fontClass = settings
23
+ ? settings.fontFamily === FontFamilies.SANS_SERIF
29
24
  ? "font-sans"
30
25
  : "font-serif"
31
26
  : undefined;
@@ -34,7 +29,6 @@ export function CommentInput({
34
29
  const textareaRef = useRef<HTMLTextAreaElement>(null);
35
30
 
36
31
  useEffect(() => {
37
- // Only auto-focus on devices with precise pointing (desktop)
38
32
  if (textareaRef.current && window.matchMedia("(pointer: fine)").matches) {
39
33
  textareaRef.current.focus();
40
34
  }
@@ -64,8 +58,8 @@ export function CommentInput({
64
58
  data-comment-input
65
59
  className="border-t border-zinc-200 dark:border-zinc-700 pt-3 pb-2"
66
60
  >
67
- <Text variant="caption" asChild>
68
- <div className="italic mb-2 line-clamp-2">"{selectedText}"</div>
61
+ <Text variant="caption" as="div" className="italic mb-2 line-clamp-2">
62
+ "{selectedText}"
69
63
  </Text>
70
64
  <textarea
71
65
  ref={textareaRef}
@@ -80,28 +74,6 @@ export function CommentInput({
80
74
  onKeyDown={handleKeyDown}
81
75
  />
82
76
  <div className="flex justify-end items-center gap-3 mt-2 text-sm">
83
- <div className="flex gap-1">
84
- <Button
85
- variant="ghost"
86
- size="icon"
87
- className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
88
- onClick={onCopyRaw}
89
- title={t("comment.copyRawTitle")}
90
- aria-label={t("comment.copyRawLabel")}
91
- >
92
- <Copy size={14} />
93
- </Button>
94
- <Button
95
- variant="ghost"
96
- size="icon"
97
- className="size-7 text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
98
- onClick={onCopyForLLM}
99
- title={t("comment.copyLLMTitle")}
100
- aria-label={t("comment.copyLLMLabel")}
101
- >
102
- <BotMessageSquare size={14} />
103
- </Button>
104
- </div>
105
77
  <Button variant="ghost" size="sm" onClick={onCancel}>
106
78
  {t("comment.cancel")}
107
79
  </Button>
@@ -2,15 +2,13 @@ import { useState } from "react";
2
2
  import { useCommentContext } from "../../contexts/CommentContext";
3
3
  import { useLocale } from "../../contexts/LocaleContext";
4
4
  import { cn } from "../../lib/utils";
5
- import type { Comment } from "../../types";
5
+ import type { Comment } from "../../schema";
6
6
  import { InlineEditor } from "../InlineEditor";
7
- import { ActionBar } from "../ui/ActionBar";
8
7
  import { ActionLink } from "../ui/ActionLink";
9
8
  import { Text } from "../ui/Text";
10
9
 
11
10
  interface CommentListItemProps {
12
11
  comment: Comment;
13
- /** Called after navigation actions (Go to, Re-anchor) to close parent dropdown */
14
12
  onAction?: () => void;
15
13
  }
16
14
 
@@ -42,12 +40,12 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
42
40
  )}
43
41
  >
44
42
  <div className="flex items-center gap-1.5 mb-1">
45
- <Text variant="caption" asChild>
46
- <span className="italic line-clamp-1">"{comment.selectedText}"</span>
43
+ <Text variant="caption" as="span" className="italic line-clamp-1">
44
+ "{comment.selectedText}"
47
45
  </Text>
48
46
  {isUnresolved && (
49
- <Text variant="caption" asChild>
50
- <span className="shrink-0">· {t("commentList.unresolved")}</span>
47
+ <Text variant="caption" as="span" className="shrink-0">
48
+ · {t("commentList.unresolved")}
51
49
  </Text>
52
50
  )}
53
51
  </div>
@@ -63,11 +61,11 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
63
61
  />
64
62
  ) : (
65
63
  <>
66
- <Text variant="body" asChild>
67
- <p className="line-clamp-2">{comment.comment}</p>
64
+ <Text variant="body" className="line-clamp-2">
65
+ {comment.comment}
68
66
  </Text>
69
67
 
70
- <ActionBar className="gap-3 mt-1.5">
68
+ <div className="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-3 mt-1.5">
71
69
  <ActionLink onClick={() => setIsEditing(true)}>
72
70
  {t("commentList.edit")}
73
71
  </ActionLink>
@@ -84,7 +82,7 @@ export function CommentListItem({ comment, onAction }: CommentListItemProps) {
84
82
  {t("commentList.reanchor")}
85
83
  </ActionLink>
86
84
  )}
87
- </ActionBar>
85
+ </div>
88
86
  </>
89
87
  )}
90
88
  </div>
@@ -1,7 +1,13 @@
1
1
  import { Copy, Trash2 } from "lucide-react";
2
- import { useState } from "react";
3
- import { useCommentContext } from "../../contexts/CommentContext";
2
+ import { useCallback, useState } from "react";
3
+ import { toast } from "sonner";
4
+ import {
5
+ useCommentActions,
6
+ useCommentData,
7
+ } from "../../contexts/CommentContext";
4
8
  import { useLocale } from "../../contexts/LocaleContext";
9
+ import { generatePrompt } from "../../lib/export";
10
+ import { useAppStore } from "../../store";
5
11
  import { Button } from "../ui/Button";
6
12
  import { Text } from "../ui/Text";
7
13
  import { CommentListItem } from "./CommentListItem";
@@ -12,7 +18,17 @@ interface CommentManagerProps {
12
18
 
13
19
  export function CommentManager({ onClose }: CommentManagerProps) {
14
20
  const { t } = useLocale();
15
- const { comments, copyAllForLLM, deleteAll } = useCommentContext();
21
+ const { comments } = useCommentData();
22
+ const { deleteAll } = useCommentActions();
23
+ const fileName = useAppStore(
24
+ (s) => s.getActiveDocumentState()?.document.fileName ?? "",
25
+ );
26
+
27
+ const copyAll = useCallback(() => {
28
+ const text = generatePrompt(comments, fileName);
29
+ navigator.clipboard.writeText(text);
30
+ toast.success(t("toast.copiedAllComments"));
31
+ }, [comments, fileName, t]);
16
32
  const [confirmingDelete, setConfirmingDelete] = useState(false);
17
33
 
18
34
  const unresolvedCount = comments.filter(
@@ -58,45 +74,45 @@ export function CommentManager({ onClose }: CommentManagerProps) {
58
74
  </div>
59
75
  </div>
60
76
  ) : (
61
- <Text variant="caption" asChild>
62
- <div className="flex items-center justify-between px-3 py-2 border-b border-zinc-100">
63
- <span>
64
- {resolvedCount}
65
- {unresolvedCount > 0 && (
66
- <span>
67
- {" "}
68
- · {unresolvedCount} {t("commentManager.unresolved")}
69
- </span>
70
- )}
71
- </span>
72
- <span className="flex items-center gap-1">
73
- <button
74
- type="button"
75
- className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
76
- onClick={copyAllForLLM}
77
- title={t("commentManager.copyAllTitle")}
78
- >
79
- <Copy size={13} />
80
- </button>
81
- <button
82
- type="button"
83
- className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
84
- onClick={() => setConfirmingDelete(true)}
85
- title={t("commentManager.deleteAllTitle")}
86
- >
87
- <Trash2 size={13} />
88
- </button>
89
- </span>
90
- </div>
77
+ <Text
78
+ variant="caption"
79
+ as="div"
80
+ className="flex items-center justify-between px-3 py-2 border-b border-zinc-100"
81
+ >
82
+ <span>
83
+ {resolvedCount}
84
+ {unresolvedCount > 0 && (
85
+ <span>
86
+ {" "}
87
+ · {unresolvedCount} {t("commentManager.unresolved")}
88
+ </span>
89
+ )}
90
+ </span>
91
+ <span className="flex items-center gap-1">
92
+ <button
93
+ type="button"
94
+ className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-zinc-600 transition-colors"
95
+ onClick={copyAll}
96
+ title={t("commentManager.copyAllTitle")}
97
+ >
98
+ <Copy size={13} />
99
+ </button>
100
+ <button
101
+ type="button"
102
+ className="p-1 rounded hover:bg-zinc-100 text-zinc-400 hover:text-red-500 transition-colors"
103
+ onClick={() => setConfirmingDelete(true)}
104
+ title={t("commentManager.deleteAllTitle")}
105
+ >
106
+ <Trash2 size={13} />
107
+ </button>
108
+ </span>
91
109
  </Text>
92
110
  )}
93
111
 
94
112
  <div className="overflow-y-auto max-h-80">
95
113
  {sortedComments.length === 0 ? (
96
- <Text variant="caption" asChild>
97
- <div className="px-3 py-4 text-center">
98
- {t("commentManager.noComments")}
99
- </div>
114
+ <Text variant="caption" as="div" className="px-3 py-4 text-center">
115
+ {t("commentManager.noComments")}
100
116
  </Text>
101
117
  ) : (
102
118
  sortedComments.map((comment) => (
@@ -74,20 +74,20 @@ export function CommentNav() {
74
74
  <ChevronLeft className="w-4 h-4" />
75
75
  </Button>
76
76
 
77
- <Text variant="body" asChild>
78
- <span
79
- className={cn(
80
- "px-3 tabular-nums select-none min-w-[4rem] text-center",
81
- "transition-transform duration-200 ease-out",
82
- animating === "prev" && "-translate-x-0.5",
83
- animating === "next" && "translate-x-0.5",
84
- )}
85
- >
86
- {t("commentNav.of", {
87
- current: currentIndex + 1,
88
- total: totalComments,
89
- })}
90
- </span>
77
+ <Text
78
+ variant="body"
79
+ as="span"
80
+ className={cn(
81
+ "px-3 tabular-nums select-none min-w-[4rem] text-center",
82
+ "transition-transform duration-200 ease-out",
83
+ animating === "prev" && "-translate-x-0.5",
84
+ animating === "next" && "translate-x-0.5",
85
+ )}
86
+ >
87
+ {t("commentNav.of", {
88
+ current: currentIndex + 1,
89
+ total: totalComments,
90
+ })}
91
91
  </Text>
92
92
 
93
93
  <Button
@@ -1,32 +1,28 @@
1
- import { cva, type VariantProps } from "class-variance-authority";
2
1
  import { cn } from "../../lib/utils";
3
2
 
4
- const actionLinkVariants = cva(
5
- "cursor-pointer transition-colors duration-150",
6
- {
7
- variants: {
8
- variant: {
9
- default: "hover:text-zinc-600",
10
- destructive: "hover:text-red-500",
11
- },
12
- },
13
- defaultVariants: { variant: "default" },
14
- },
15
- );
3
+ const variantStyles = {
4
+ default: "hover:text-zinc-600",
5
+ destructive: "hover:text-red-500",
6
+ } as const;
7
+
8
+ type ActionLinkVariant = keyof typeof variantStyles;
16
9
 
17
10
  function ActionLink({
18
11
  className,
19
- variant,
12
+ variant = "default",
20
13
  ...props
21
- }: React.ComponentProps<"button"> & VariantProps<typeof actionLinkVariants>) {
14
+ }: React.ComponentProps<"button"> & { variant?: ActionLinkVariant }) {
22
15
  return (
23
16
  <button
24
17
  type="button"
25
- data-slot="action-link"
26
- className={cn(actionLinkVariants({ variant, className }))}
18
+ className={cn(
19
+ "cursor-pointer transition-colors duration-150",
20
+ variantStyles[variant],
21
+ className,
22
+ )}
27
23
  {...props}
28
24
  />
29
25
  );
30
26
  }
31
27
 
32
- export { ActionLink, actionLinkVariants };
28
+ export { ActionLink };
@@ -1,55 +1,54 @@
1
- import { Slot } from "@radix-ui/react-slot";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
1
  import { cn } from "../../lib/utils";
4
2
 
5
- const buttonVariants = cva(
6
- "inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors duration-150 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
7
- {
8
- variants: {
9
- variant: {
10
- default: "bg-blue-600 text-white hover:bg-blue-700",
11
- secondary:
12
- "bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700",
13
- outline:
14
- "border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800",
15
- ghost:
16
- "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-100",
17
- destructive: "bg-red-600 text-white hover:bg-red-700",
18
- link: "text-zinc-600 dark:text-zinc-400 underline-offset-4 hover:underline",
19
- },
20
- size: {
21
- default: "h-9 px-4",
22
- sm: "h-8 px-3 text-xs",
23
- lg: "h-10 px-6",
24
- icon: "size-9",
25
- },
26
- },
27
- defaultVariants: {
28
- variant: "default",
29
- size: "default",
30
- },
31
- },
32
- );
3
+ const baseStyles =
4
+ "inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors duration-150 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0";
5
+
6
+ const variantStyles = {
7
+ default: "bg-blue-600 text-white hover:bg-blue-700",
8
+ secondary:
9
+ "bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700",
10
+ outline:
11
+ "border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800",
12
+ ghost:
13
+ "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-100",
14
+ destructive: "bg-red-600 text-white hover:bg-red-700",
15
+ link: "text-zinc-600 dark:text-zinc-400 underline-offset-4 hover:underline",
16
+ } as const;
17
+
18
+ const sizeStyles = {
19
+ default: "h-9 px-4",
20
+ sm: "h-8 px-3 text-xs",
21
+ lg: "h-10 px-6",
22
+ icon: "size-9",
23
+ } as const;
24
+
25
+ type ButtonVariant = keyof typeof variantStyles;
26
+ type ButtonSize = keyof typeof sizeStyles;
27
+
28
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
29
+ variant?: ButtonVariant;
30
+ size?: ButtonSize;
31
+ }
33
32
 
34
33
  function Button({
35
34
  className,
36
- variant,
37
- size,
38
- asChild = false,
35
+ variant = "default",
36
+ size = "default",
39
37
  ...props
40
- }: React.ComponentProps<"button"> &
41
- VariantProps<typeof buttonVariants> & {
42
- asChild?: boolean;
43
- }) {
44
- const Comp = asChild ? Slot : "button";
45
-
38
+ }: ButtonProps) {
46
39
  return (
47
- <Comp
48
- data-slot="button"
49
- className={cn(buttonVariants({ variant, size, className }))}
40
+ <button
41
+ type="button"
42
+ className={cn(
43
+ baseStyles,
44
+ variantStyles[variant],
45
+ sizeStyles[size],
46
+ className,
47
+ )}
50
48
  {...props}
51
49
  />
52
50
  );
53
51
  }
54
52
 
55
- export { Button, buttonVariants };
53
+ export type { ButtonProps, ButtonSize, ButtonVariant };
54
+ export { Button };
@@ -1,95 +1,84 @@
1
- import * as DialogPrimitive from "@radix-ui/react-dialog";
2
1
  import { X } from "lucide-react";
3
- import { use } from "react";
4
- import { LayoutContext } from "../../contexts/LayoutContext";
2
+ import { useEffect, useRef } from "react";
5
3
  import { cn } from "../../lib/utils";
6
- import { FontFamilies } from "../../types";
7
- import { buttonVariants } from "./Button";
8
- import { textVariants } from "./Text";
9
4
 
10
- function Dialog({
11
- ...props
12
- }: React.ComponentProps<typeof DialogPrimitive.Root>) {
13
- return <DialogPrimitive.Root data-slot="dialog" {...props} />;
5
+ interface DialogProps {
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ children: React.ReactNode;
14
9
  }
15
10
 
16
- function DialogTrigger({
17
- ...props
18
- }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
19
- return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
20
- }
11
+ function Dialog({ open, onOpenChange, children }: DialogProps) {
12
+ const ref = useRef<HTMLDialogElement>(null);
21
13
 
22
- function DialogPortal({
23
- ...props
24
- }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
25
- return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
26
- }
14
+ useEffect(() => {
15
+ const dialog = ref.current;
16
+ if (!dialog) return;
27
17
 
28
- function DialogClose({
29
- ...props
30
- }: React.ComponentProps<typeof DialogPrimitive.Close>) {
31
- return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
32
- }
18
+ if (open && !dialog.open) {
19
+ dialog.showModal();
20
+ } else if (!open && dialog.open) {
21
+ dialog.close();
22
+ }
23
+ }, [open]);
24
+
25
+ useEffect(() => {
26
+ const dialog = ref.current;
27
+ if (!dialog) return;
28
+
29
+ const handleClose = () => onOpenChange(false);
30
+ dialog.addEventListener("close", handleClose);
31
+ return () => dialog.removeEventListener("close", handleClose);
32
+ }, [onOpenChange]);
33
+
34
+ const handleClick = (e: React.MouseEvent<HTMLDialogElement>) => {
35
+ if (e.target === ref.current) onOpenChange(false);
36
+ };
33
37
 
34
- function DialogOverlay({
35
- className,
36
- ...props
37
- }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
38
38
  return (
39
- <DialogPrimitive.Overlay
40
- data-slot="dialog-overlay"
41
- className={cn(
42
- "fixed inset-0 z-50 bg-black/20 dark:bg-black/40 backdrop-blur-sm",
43
- className,
44
- )}
45
- {...props}
46
- />
39
+ <dialog
40
+ ref={ref}
41
+ onClick={handleClick}
42
+ className="backdrop:bg-black/20 dark:backdrop:bg-black/40 backdrop:backdrop-blur-sm bg-transparent p-0 m-auto max-w-none"
43
+ >
44
+ {open ? children : null}
45
+ </dialog>
47
46
  );
48
47
  }
49
48
 
50
49
  function DialogContent({
51
50
  className,
52
51
  children,
53
- showCloseButton = true,
54
- ...props
55
- }: React.ComponentProps<typeof DialogPrimitive.Content> & {
56
- showCloseButton?: boolean;
52
+ onClose,
53
+ }: {
54
+ className?: string;
55
+ children: React.ReactNode;
56
+ onClose?: () => void;
57
57
  }) {
58
58
  return (
59
- <DialogPortal>
60
- <DialogOverlay />
61
- <DialogPrimitive.Content
62
- data-slot="dialog-content"
63
- className={cn(
64
- "fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
65
- "bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40 rounded-xl",
66
- "flex flex-col animate-in",
67
- className,
68
- )}
69
- {...props}
70
- >
71
- {children}
72
- {showCloseButton && (
73
- <DialogPrimitive.Close
74
- data-slot="dialog-close"
75
- className={cn(
76
- buttonVariants({ variant: "ghost", size: "icon" }),
77
- "absolute top-3 right-3 size-7",
78
- )}
79
- >
80
- <X className="w-4 h-4" />
81
- <span className="sr-only">Close</span>
82
- </DialogPrimitive.Close>
83
- )}
84
- </DialogPrimitive.Content>
85
- </DialogPortal>
59
+ <div
60
+ className={cn(
61
+ "w-full bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40 rounded-xl flex flex-col",
62
+ className,
63
+ )}
64
+ >
65
+ {children}
66
+ {onClose && (
67
+ <button
68
+ type="button"
69
+ onClick={onClose}
70
+ className="absolute top-3 right-3 size-7 inline-flex items-center justify-center rounded-lg text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
71
+ >
72
+ <X className="w-4 h-4" />
73
+ </button>
74
+ )}
75
+ </div>
86
76
  );
87
77
  }
88
78
 
89
79
  function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
90
80
  return (
91
81
  <div
92
- data-slot="dialog-header"
93
82
  className={cn(
94
83
  "flex items-center justify-between pl-4 pr-12 py-3 border-b border-zinc-100 dark:border-zinc-800",
95
84
  className,
@@ -101,56 +90,27 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
101
90
 
102
91
  function DialogTitle({
103
92
  className,
104
- ...props
105
- }: React.ComponentProps<typeof DialogPrimitive.Title>) {
106
- const layout = use(LayoutContext);
107
- const fontClass = layout
108
- ? layout.fontFamily === FontFamilies.SANS_SERIF
109
- ? "font-sans"
110
- : "font-serif"
111
- : undefined;
112
-
113
- return (
114
- <DialogPrimitive.Title
115
- data-slot="dialog-title"
116
- className={cn(textVariants({ variant: "section" }), fontClass, className)}
117
- {...props}
118
- />
119
- );
120
- }
121
-
122
- function DialogDescription({
123
- className,
124
- ...props
125
- }: React.ComponentProps<typeof DialogPrimitive.Description>) {
93
+ children,
94
+ }: {
95
+ className?: string;
96
+ children: React.ReactNode;
97
+ }) {
126
98
  return (
127
- <DialogPrimitive.Description
128
- data-slot="dialog-description"
129
- className={cn(textVariants({ variant: "caption" }), "text-sm", className)}
130
- {...props}
131
- />
99
+ <h2
100
+ className={cn(
101
+ "text-sm font-medium text-zinc-900 dark:text-zinc-100",
102
+ className,
103
+ )}
104
+ >
105
+ {children}
106
+ </h2>
132
107
  );
133
108
  }
134
109
 
135
110
  function DialogBody({ className, ...props }: React.ComponentProps<"div">) {
136
111
  return (
137
- <div
138
- data-slot="dialog-body"
139
- className={cn("flex-1 overflow-auto p-4", className)}
140
- {...props}
141
- />
112
+ <div className={cn("flex-1 overflow-auto p-4", className)} {...props} />
142
113
  );
143
114
  }
144
115
 
145
- export {
146
- Dialog,
147
- DialogBody,
148
- DialogClose,
149
- DialogContent,
150
- DialogDescription,
151
- DialogHeader,
152
- DialogOverlay,
153
- DialogPortal,
154
- DialogTitle,
155
- DialogTrigger,
156
- };
116
+ export { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle };