@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,87 +1,77 @@
1
- import { cva } from "class-variance-authority";
2
- import { useState } from "react";
3
- import { useCommentContext } from "../contexts/CommentContext";
4
- import { useLayoutContext } from "../contexts/LayoutContext";
1
+ import { memo, useCallback, useState } from "react";
2
+ import { useCommentActions } from "../contexts/CommentContext";
5
3
  import { useLocale } from "../contexts/LocaleContext";
4
+ import { usePositions } from "../contexts/PositionsContext";
5
+ import { useSettings } from "../contexts/SettingsContext";
6
6
  import { cn } from "../lib/utils";
7
- import { type Comment, FontFamilies } from "../types";
7
+ import { type Comment, FontFamilies } from "../schema";
8
+ import { useUI } from "../store";
8
9
  import { InlineEditor } from "./InlineEditor";
9
- import { ActionBar } from "./ui/ActionBar";
10
10
  import { ActionLink } from "./ui/ActionLink";
11
- import { SeparatorDot } from "./ui/SeparatorDot";
12
11
 
13
12
  interface MarginNoteProps {
14
13
  comment: Comment;
15
- top: number;
16
14
  commentIndex?: number;
17
15
  }
18
16
 
19
- const selectedTextVariants = cva(
20
- "text-sm italic mb-1 line-clamp-1 flex items-center gap-1 transition-colors duration-150",
21
- {
22
- variants: {
23
- hovered: {
24
- true: "text-zinc-600 dark:text-zinc-400",
25
- false: "text-zinc-400 dark:text-zinc-500",
26
- },
27
- },
28
- defaultVariants: { hovered: false },
29
- },
30
- );
17
+ function selectedTextClass(hovered: boolean) {
18
+ return cn(
19
+ "text-sm italic mb-1 line-clamp-1 flex items-center gap-1 transition-colors duration-150",
20
+ hovered
21
+ ? "text-zinc-600 dark:text-zinc-400"
22
+ : "text-zinc-400 dark:text-zinc-500",
23
+ );
24
+ }
31
25
 
32
- const commentTextVariants = cva(
33
- "text-sm whitespace-pre-wrap transition-colors duration-150",
34
- {
35
- variants: {
36
- hovered: {
37
- true: "text-zinc-800 dark:text-zinc-200",
38
- false: "text-zinc-500 dark:text-zinc-400",
39
- },
40
- },
41
- defaultVariants: { hovered: false },
42
- },
43
- );
26
+ function commentTextClass(hovered: boolean) {
27
+ return cn(
28
+ "text-sm whitespace-pre-wrap transition-colors duration-150",
29
+ hovered
30
+ ? "text-zinc-800 dark:text-zinc-200"
31
+ : "text-zinc-500 dark:text-zinc-400",
32
+ );
33
+ }
44
34
 
45
- const badgeVariants = cva(
46
- "absolute -left-4 top-2 text-xs tabular-nums transition-colors duration-150",
47
- {
48
- variants: {
49
- hovered: {
50
- true: "text-zinc-600 dark:text-zinc-400",
51
- false: "text-zinc-400 dark:text-zinc-500",
52
- },
53
- },
54
- defaultVariants: { hovered: false },
55
- },
56
- );
35
+ function badgeClass(hovered: boolean) {
36
+ return cn(
37
+ "absolute -left-4 top-2 text-xs tabular-nums transition-colors duration-150",
38
+ hovered
39
+ ? "text-zinc-600 dark:text-zinc-400"
40
+ : "text-zinc-400 dark:text-zinc-500",
41
+ );
42
+ }
57
43
 
58
- export function MarginNote({
44
+ export const MarginNote = memo(function MarginNote({
59
45
  comment,
60
- top,
61
46
  commentIndex = 0,
62
47
  }: MarginNoteProps) {
63
- const { fontFamily } = useLayoutContext();
48
+ const { fontFamily } = useSettings();
64
49
  const { t } = useLocale();
65
50
  const {
66
51
  editComment,
67
52
  deleteComment,
68
- copyCommentRaw,
69
- copyCommentForLLM,
70
- hoveredCommentId,
53
+ copyComment,
71
54
  setHoveredCommentId,
72
55
  scrollToHighlight,
73
- } = useCommentContext();
56
+ } = useCommentActions();
74
57
 
75
- const isHovered = hoveredCommentId === comment.id;
58
+ const pos = usePositions();
59
+ const refCallback = useCallback(
60
+ (el: HTMLElement | null) => {
61
+ if (el) pos.register(comment.id, el);
62
+ else pos.unregister(comment.id);
63
+ },
64
+ [pos, comment.id],
65
+ );
66
+
67
+ const isHovered = useUI((s) => s.hoveredCommentId === comment.id);
76
68
  const fontClass =
77
69
  fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif";
78
70
  const [isEditing, setIsEditing] = useState(false);
79
71
 
80
72
  const hasNote = comment.comment.trim().length > 0;
81
73
 
82
- const handleCopy = () => {
83
- copyCommentRaw(comment);
84
- };
74
+ const handleCopy = () => copyComment(comment);
85
75
 
86
76
  const createdAtFormatted = new Date(comment.createdAt).toLocaleString();
87
77
 
@@ -89,30 +79,39 @@ export function MarginNote({
89
79
  if (!hasNote && !isEditing) {
90
80
  return (
91
81
  <article
82
+ ref={refCallback}
92
83
  className="absolute left-0 right-0 group"
93
- style={{ top }}
84
+ style={{
85
+ visibility: "hidden",
86
+ contentVisibility: "auto",
87
+ containIntrinsicSize: "auto 80px",
88
+ }}
94
89
  title={`Added: ${createdAtFormatted}`}
95
90
  data-comment-id={comment.id}
96
91
  onMouseEnter={() => setHoveredCommentId(comment.id)}
97
92
  onMouseLeave={() => setHoveredCommentId(undefined)}
98
93
  >
99
- <span className={badgeVariants({ hovered: isHovered })}>—</span>
94
+ <span className={badgeClass(isHovered)}>—</span>
100
95
 
101
96
  <div className="pt-2 pb-2 pl-3">
102
- <ActionBar
103
- className={cn("gap-1.5 duration-150", isHovered && "opacity-100")}
97
+ <div
98
+ className={cn(
99
+ "flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity",
100
+ "gap-1.5 duration-150",
101
+ isHovered && "opacity-100",
102
+ )}
104
103
  >
105
104
  <ActionLink onClick={() => setIsEditing(true)}>
106
105
  {t("marginNote.addNote")}
107
106
  </ActionLink>
108
- <SeparatorDot />
107
+ <span aria-hidden="true">·</span>
109
108
  <ActionLink
110
109
  variant="destructive"
111
110
  onClick={() => deleteComment(comment.id)}
112
111
  >
113
112
  {t("marginNote.delete")}
114
113
  </ActionLink>
115
- </ActionBar>
114
+ </div>
116
115
  </div>
117
116
  </article>
118
117
  );
@@ -120,16 +119,15 @@ export function MarginNote({
120
119
 
121
120
  return (
122
121
  <article
122
+ ref={refCallback}
123
123
  className="absolute left-0 right-0 group"
124
- style={{ top }}
124
+ style={{ visibility: "hidden" }}
125
125
  title={`Added: ${createdAtFormatted}`}
126
126
  data-comment-id={comment.id}
127
127
  onMouseEnter={() => setHoveredCommentId(comment.id)}
128
128
  onMouseLeave={() => setHoveredCommentId(undefined)}
129
129
  >
130
- <span className={badgeVariants({ hovered: isHovered })}>
131
- {commentIndex + 1}
132
- </span>
130
+ <span className={badgeClass(isHovered)}>{commentIndex + 1}</span>
133
131
 
134
132
  <div
135
133
  className={cn(
@@ -138,12 +136,7 @@ export function MarginNote({
138
136
  )}
139
137
  >
140
138
  {!isEditing && (
141
- <div
142
- className={cn(
143
- fontClass,
144
- selectedTextVariants({ hovered: isHovered }),
145
- )}
146
- >
139
+ <div className={cn(fontClass, selectedTextClass(isHovered))}>
147
140
  <button
148
141
  type="button"
149
142
  onClick={() => scrollToHighlight(comment.id)}
@@ -165,43 +158,28 @@ export function MarginNote({
165
158
  />
166
159
  ) : (
167
160
  <>
168
- <p
169
- className={cn(
170
- fontClass,
171
- commentTextVariants({ hovered: isHovered }),
172
- )}
173
- >
161
+ <p className={cn(fontClass, commentTextClass(isHovered))}>
174
162
  {comment.comment}
175
163
  </p>
176
- <ActionBar className="gap-1.5 mt-2">
164
+ <div className="flex items-center text-xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity gap-1.5 mt-2">
177
165
  <ActionLink onClick={() => setIsEditing(true)}>
178
166
  {t("marginNote.edit")}
179
167
  </ActionLink>
180
- <SeparatorDot />
168
+ <span aria-hidden="true">·</span>
181
169
  <ActionLink
182
170
  variant="destructive"
183
171
  onClick={() => deleteComment(comment.id)}
184
172
  >
185
173
  {t("marginNote.delete")}
186
174
  </ActionLink>
187
- <SeparatorDot />
188
- <ActionLink
189
- onClick={handleCopy}
190
- title={t("marginNote.copyTitle")}
191
- >
175
+ <span aria-hidden="true">·</span>
176
+ <ActionLink onClick={handleCopy}>
192
177
  {t("marginNote.copy")}
193
178
  </ActionLink>
194
- <SeparatorDot />
195
- <ActionLink
196
- onClick={() => copyCommentForLLM(comment)}
197
- title={t("marginNote.llmTitle")}
198
- >
199
- {t("marginNote.llm")}
200
- </ActionLink>
201
- </ActionBar>
179
+ </div>
202
180
  </>
203
181
  )}
204
182
  </div>
205
183
  </article>
206
184
  );
207
- }
185
+ });
@@ -1,50 +1,23 @@
1
- import { useMemo } from "react";
2
- import { resolveMarginNotePositions } from "../lib/margin-layout";
3
- import type { Comment } from "../types";
1
+ import { memo } from "react";
2
+ import type { Comment } from "../schema";
4
3
  import { MarginNote } from "./MarginNote";
5
4
 
6
5
  interface MarginNotesProps {
7
- /** Comments pre-sorted by startOffset */
8
6
  sortedComments: Comment[];
9
- highlightPositions: Record<string, number>;
10
- pendingSelectionTop?: number;
11
7
  }
12
8
 
13
- export function MarginNotes({
9
+ export const MarginNotes = memo(function MarginNotes({
14
10
  sortedComments,
15
- highlightPositions,
16
- pendingSelectionTop,
17
11
  }: MarginNotesProps) {
18
- // Calculate resolved positions (avoiding overlaps with input and other notes)
19
- const resolvedPositions = useMemo(
20
- () =>
21
- resolveMarginNotePositions(
22
- sortedComments.map((c) => c.id),
23
- highlightPositions,
24
- pendingSelectionTop,
25
- ),
26
- [sortedComments, highlightPositions, pendingSelectionTop],
27
- );
28
-
29
12
  if (sortedComments.length === 0) {
30
13
  return null;
31
14
  }
32
15
 
33
16
  return (
34
17
  <div className="relative w-64">
35
- {sortedComments.map((comment, index) => {
36
- const top = resolvedPositions.get(comment.id);
37
- if (top === undefined) return null;
38
-
39
- return (
40
- <MarginNote
41
- key={comment.id}
42
- comment={comment}
43
- top={top}
44
- commentIndex={index}
45
- />
46
- );
47
- })}
18
+ {sortedComments.map((comment, index) => (
19
+ <MarginNote key={comment.id} comment={comment} commentIndex={index} />
20
+ ))}
48
21
  </div>
49
22
  );
50
- }
23
+ });
@@ -8,7 +8,6 @@ import {
8
8
  Dialog,
9
9
  DialogBody,
10
10
  DialogContent,
11
- DialogDescription,
12
11
  DialogHeader,
13
12
  DialogTitle,
14
13
  } from "./ui/Dialog";
@@ -88,7 +87,7 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
88
87
  if (!open) onClose();
89
88
  }}
90
89
  >
91
- <DialogContent className="max-w-2xl max-h-[80vh]">
90
+ <DialogContent className="max-w-2xl max-h-[80vh]" onClose={onClose}>
92
91
  <DialogHeader>
93
92
  <DialogTitle>{t("rawModal.title")}</DialogTitle>
94
93
  {state.status === "success" && (
@@ -105,9 +104,9 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
105
104
  </DialogHeader>
106
105
 
107
106
  {(state.status === "success" || state.status === "empty") && (
108
- <DialogDescription className="px-4 py-2 border-b border-zinc-50 dark:border-zinc-800 text-xs text-zinc-400 dark:text-zinc-500 font-mono truncate">
107
+ <div className="px-4 py-2 border-b border-zinc-50 dark:border-zinc-800 text-xs text-zinc-400 dark:text-zinc-500 font-mono truncate">
109
108
  {state.path}
110
- </DialogDescription>
109
+ </div>
111
110
  )}
112
111
 
113
112
  <DialogBody>
@@ -130,10 +129,12 @@ export function RawModal({ isOpen, onClose }: RawModalProps) {
130
129
  )}
131
130
 
132
131
  {state.status === "success" && (
133
- <Text variant="body" asChild>
134
- <pre className="text-xs font-mono whitespace-pre-wrap break-words leading-relaxed">
135
- {state.content}
136
- </pre>
132
+ <Text
133
+ variant="body"
134
+ as="pre"
135
+ className="text-xs font-mono whitespace-pre-wrap break-words leading-relaxed"
136
+ >
137
+ {state.content}
137
138
  </Text>
138
139
  )}
139
140
  </DialogBody>
@@ -20,8 +20,8 @@ export function ReanchorConfirm({
20
20
  <Text variant="body" className="mb-2">
21
21
  {t("reanchor.question")}
22
22
  </Text>
23
- <Text variant="caption" asChild>
24
- <p className="italic line-clamp-2 mb-2">"{selectionText}"</p>
23
+ <Text variant="caption" className="italic line-clamp-2 mb-2">
24
+ "{selectionText}"
25
25
  </Text>
26
26
  <div className="flex gap-3 text-sm">
27
27
  <Button variant="link" size="sm" onClick={onConfirm}>
@@ -1,17 +1,14 @@
1
- import { Check, ChevronDown, ExternalLink } from "lucide-react";
2
- import { useLayoutContext } from "../contexts/LayoutContext";
1
+ import { Check, ChevronDown } from "lucide-react";
3
2
  import { useLocale } from "../contexts/LocaleContext";
3
+ import { useSettings } from "../contexts/SettingsContext";
4
4
  import { type Locale, Locales } from "../lib/i18n";
5
5
  import { cn } from "../lib/utils";
6
6
  import {
7
- type EditorScheme,
8
- EditorSchemes,
9
7
  FontFamilies,
10
8
  type FontFamily,
11
9
  type ThemeMode,
12
10
  ThemeModes,
13
- } from "../types";
14
- import { ShortcutList } from "./ShortcutList";
11
+ } from "../schema";
15
12
  import {
16
13
  Dialog,
17
14
  DialogBody,
@@ -74,8 +71,6 @@ function ThemePreviewBadge() {
74
71
  );
75
72
  }
76
73
 
77
- /* ─── Font selector ──────────────────────────────────────────── */
78
-
79
74
  function FontPreviewBadge({ fontClass }: { fontClass: string }) {
80
75
  return (
81
76
  <span
@@ -89,8 +84,6 @@ function FontPreviewBadge({ fontClass }: { fontClass: string }) {
89
84
  );
90
85
  }
91
86
 
92
- /* ─── Shared trigger style ───────────────────────────────────── */
93
-
94
87
  const triggerClassName = cn(
95
88
  "inline-flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-sm",
96
89
  "border border-zinc-200 dark:border-zinc-700",
@@ -100,21 +93,8 @@ const triggerClassName = cn(
100
93
  "transition-colors cursor-pointer",
101
94
  );
102
95
 
103
- /* ─── Settings Modal ─────────────────────────────────────────── */
104
-
105
96
  export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
106
- const {
107
- fontFamily,
108
- setFontFamily,
109
- editorScheme,
110
- setEditorScheme,
111
- themeMode,
112
- setThemeMode,
113
- shortcuts,
114
- updateBinding,
115
- toggleShortcutEnabled,
116
- resetShortcutsToDefaults,
117
- } = useLayoutContext();
97
+ const { fontFamily, setFontFamily, themeMode, setThemeMode } = useSettings();
118
98
  const { locale, setLocale, t } = useLocale();
119
99
 
120
100
  const themeOptions = [
@@ -136,22 +116,10 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
136
116
  },
137
117
  ];
138
118
 
139
- const editorOptions = [
140
- { value: EditorSchemes.NONE, label: t("settings.editor.none") },
141
- { value: EditorSchemes.VSCODE, label: t("settings.editor.vscode") },
142
- {
143
- value: EditorSchemes.VSCODE_INSIDERS,
144
- label: t("settings.editor.vscodeInsiders"),
145
- },
146
- { value: EditorSchemes.CURSOR, label: t("settings.editor.cursor") },
147
- ];
148
-
149
119
  const activeTheme =
150
120
  themeOptions.find((o) => o.value === themeMode) ?? themeOptions[0];
151
121
  const activeFont =
152
122
  fontOptions.find((o) => o.value === fontFamily) ?? fontOptions[0];
153
- const activeEditor =
154
- editorOptions.find((o) => o.value === editorScheme) ?? editorOptions[0];
155
123
  const activeLocale =
156
124
  LOCALE_OPTIONS.find((o) => o.value === locale) ?? LOCALE_OPTIONS[0];
157
125
 
@@ -162,15 +130,15 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
162
130
  if (!open) onClose();
163
131
  }}
164
132
  >
165
- <DialogContent className="max-w-md">
133
+ <DialogContent className="max-w-md" onClose={onClose}>
166
134
  <DialogHeader>
167
135
  <DialogTitle>{t("settings.title")}</DialogTitle>
168
136
  </DialogHeader>
169
137
 
170
138
  <DialogBody className="space-y-4">
171
139
  <div>
172
- <Text variant="overline" asChild>
173
- <h3 className="mb-3">{t("settings.theme")}</h3>
140
+ <Text variant="overline" as="h3" className="mb-3">
141
+ {t("settings.theme")}
174
142
  </Text>
175
143
  <DropdownMenu>
176
144
  <DropdownMenuTrigger asChild>
@@ -201,8 +169,8 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
201
169
  </div>
202
170
 
203
171
  <div>
204
- <Text variant="overline" asChild>
205
- <h3 className="mb-3">{t("settings.font")}</h3>
172
+ <Text variant="overline" as="h3" className="mb-3">
173
+ {t("settings.font")}
206
174
  </Text>
207
175
  <DropdownMenu>
208
176
  <DropdownMenuTrigger asChild>
@@ -231,8 +199,8 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
231
199
  </div>
232
200
 
233
201
  <div>
234
- <Text variant="overline" asChild>
235
- <h3 className="mb-3">{t("settings.language")}</h3>
202
+ <Text variant="overline" as="h3" className="mb-3">
203
+ {t("settings.language")}
236
204
  </Text>
237
205
  <DropdownMenu>
238
206
  <DropdownMenuTrigger asChild>
@@ -257,52 +225,6 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
257
225
  </DropdownMenuContent>
258
226
  </DropdownMenu>
259
227
  </div>
260
-
261
- <div>
262
- <Text variant="overline" asChild>
263
- <h3 className="mb-3">{t("settings.editor")}</h3>
264
- </Text>
265
- <DropdownMenu>
266
- <DropdownMenuTrigger asChild>
267
- <button type="button" className={triggerClassName}>
268
- <ExternalLink className="size-3 text-zinc-400 dark:text-zinc-500" />
269
- <span>{activeEditor.label}</span>
270
- <ChevronDown className="size-3 text-zinc-400 dark:text-zinc-500" />
271
- </button>
272
- </DropdownMenuTrigger>
273
- <DropdownMenuContent align="start" className="min-w-[160px]">
274
- {editorOptions.map((option) => (
275
- <DropdownMenuItem
276
- key={option.value}
277
- onSelect={() =>
278
- setEditorScheme(option.value as EditorScheme)
279
- }
280
- className="flex items-center gap-2"
281
- >
282
- <span className="flex-1">{option.label}</span>
283
- {editorScheme === option.value && (
284
- <Check className="size-3.5 text-zinc-500 dark:text-zinc-400" />
285
- )}
286
- </DropdownMenuItem>
287
- ))}
288
- </DropdownMenuContent>
289
- </DropdownMenu>
290
- </div>
291
-
292
- <div>
293
- <Text variant="overline" asChild>
294
- <h3 className="mb-1">{t("settings.keyboardShortcuts")}</h3>
295
- </Text>
296
- <p className="text-xs text-zinc-400 dark:text-zinc-500 mb-3">
297
- {t("settings.clickToRebind")}
298
- </p>
299
- <ShortcutList
300
- shortcuts={shortcuts}
301
- onUpdateBinding={updateBinding}
302
- onToggleEnabled={toggleShortcutEnabled}
303
- onResetToDefaults={resetShortcutsToDefaults}
304
- />
305
- </div>
306
228
  </DialogBody>
307
229
  </DialogContent>
308
230
  </Dialog>
@@ -13,7 +13,7 @@ export function TabBar() {
13
13
 
14
14
  return (
15
15
  <div
16
- className="flex border-b border-zinc-200 bg-zinc-50 px-2 overflow-x-auto"
16
+ className="flex border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900 px-2 overflow-x-auto"
17
17
  role="tablist"
18
18
  >
19
19
  {documentOrder.map((filePath) => {
@@ -30,8 +30,8 @@ export function TabBar() {
30
30
  className={cn(
31
31
  "flex items-center gap-1.5 px-3 py-1.5 text-sm border-b-2 whitespace-nowrap cursor-pointer select-none",
32
32
  isActive
33
- ? "border-zinc-900 text-zinc-900"
34
- : "border-transparent text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100",
33
+ ? "border-zinc-900 dark:border-zinc-100 text-zinc-900 dark:text-zinc-100"
34
+ : "border-transparent text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800",
35
35
  )}
36
36
  onClick={() => setActiveDocument(filePath)}
37
37
  onKeyDown={(e) => {
@@ -44,7 +44,7 @@ export function TabBar() {
44
44
  <span>{docState.document.fileName}</span>
45
45
  <button
46
46
  type="button"
47
- className="ml-1 rounded p-0.5 hover:bg-zinc-200"
47
+ className="ml-1 rounded p-0.5 hover:bg-zinc-200 dark:hover:bg-zinc-700"
48
48
  onClick={(e) => {
49
49
  e.stopPropagation();
50
50
  closeDocument(filePath);
@@ -1,8 +1,8 @@
1
1
  import { use, useMemo, useState } from "react";
2
- import { LayoutContext } from "../contexts/LayoutContext";
2
+ import { SettingsContext } from "../contexts/SettingsContext";
3
3
  import type { Heading } from "../hooks/useHeadings";
4
4
  import { cn } from "../lib/utils";
5
- import { FontFamilies } from "../types";
5
+ import { FontFamilies } from "../schema";
6
6
 
7
7
  interface TableOfContentsProps {
8
8
  headings: Heading[];
@@ -15,9 +15,9 @@ export function TableOfContents({
15
15
  activeId,
16
16
  onHeadingClick,
17
17
  }: TableOfContentsProps) {
18
- const layout = use(LayoutContext);
19
- const fontClass = layout
20
- ? layout.fontFamily === FontFamilies.SANS_SERIF
18
+ const settings = use(SettingsContext);
19
+ const fontClass = settings
20
+ ? settings.fontFamily === FontFamilies.SANS_SERIF
21
21
  ? "font-sans"
22
22
  : "font-serif"
23
23
  : undefined;