@peaske7/readit 0.2.0 → 0.2.1

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 (173) hide show
  1. package/.claude/CLAUDE.md +118 -76
  2. package/.claude/commands/review.md +1 -1
  3. package/.claude/roadmap.md +32 -9
  4. package/.claude/user-stories.md +100 -15
  5. package/AGENTS.md +30 -26
  6. package/Makefile +32 -0
  7. package/README.md +90 -2
  8. package/biome.json +18 -8
  9. package/bun.lock +426 -568
  10. package/bunfig.toml +2 -0
  11. package/docs/perf-baseline.md +56 -1
  12. package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
  13. package/e2e/comments.spec.ts +14 -58
  14. package/e2e/document-load.spec.ts +1 -23
  15. package/e2e/export.spec.ts +4 -4
  16. package/e2e/perf/add-comment.spec.ts +9 -11
  17. package/e2e/perf/fixtures/generate.ts +1 -5
  18. package/e2e/perf/screenshot-final.png +0 -0
  19. package/e2e/perf/utils/metrics.ts +73 -9
  20. package/e2e/persistence-file.spec.ts +41 -26
  21. package/e2e/utils/selection.ts +17 -73
  22. package/go/cmd/readit/main.go +416 -0
  23. package/go/go.mod +20 -0
  24. package/go/go.sum +41 -0
  25. package/go/internal/server/anchor.go +302 -0
  26. package/go/internal/server/anchor_test.go +111 -0
  27. package/go/internal/server/comments.go +390 -0
  28. package/go/internal/server/documents.go +113 -0
  29. package/go/internal/server/embed.go +17 -0
  30. package/go/internal/server/headings.go +33 -0
  31. package/go/internal/server/headings_test.go +75 -0
  32. package/go/internal/server/htmltext.go +123 -0
  33. package/go/internal/server/markdown.go +157 -0
  34. package/go/internal/server/markdown_bench_test.go +42 -0
  35. package/go/internal/server/markdown_test.go +79 -0
  36. package/go/internal/server/server.go +453 -0
  37. package/go/internal/server/server_bench_test.go +122 -0
  38. package/go/internal/server/settings.go +110 -0
  39. package/go/internal/server/sse.go +140 -0
  40. package/go/internal/server/storage.go +275 -0
  41. package/go/internal/server/storage_test.go +118 -0
  42. package/go/internal/server/template.go +66 -0
  43. package/go/internal/server/types.go +101 -0
  44. package/go/internal/server/watcher.go +74 -0
  45. package/index.html +4 -14
  46. package/nvim-readit/lua/readit/health.lua +64 -0
  47. package/nvim-readit/lua/readit/init.lua +463 -0
  48. package/nvim-readit/plugin/readit.lua +19 -0
  49. package/package.json +20 -28
  50. package/shell/_readit +158 -0
  51. package/shell/readit.zsh +87 -0
  52. package/src/App.svelte +881 -0
  53. package/src/cli.ts +183 -21
  54. package/src/components/ActionsMenu.svelte +95 -0
  55. package/src/components/CommentBadge.svelte +67 -0
  56. package/src/components/CommentErrorBanner.svelte +33 -0
  57. package/src/components/CommentInput.svelte +75 -0
  58. package/src/components/CommentListItem.svelte +95 -0
  59. package/src/components/CommentManager.svelte +129 -0
  60. package/src/components/CommentNav.svelte +109 -0
  61. package/src/components/DocumentViewer.svelte +218 -0
  62. package/src/components/FloatingComment.svelte +107 -0
  63. package/src/components/Header.svelte +76 -0
  64. package/src/components/InlineEditor.svelte +72 -0
  65. package/src/components/MarginNote.svelte +167 -0
  66. package/src/components/MarginNotesContainer.svelte +33 -0
  67. package/src/components/RawModal.svelte +126 -0
  68. package/src/components/ReanchorConfirm.svelte +30 -0
  69. package/src/components/SettingsModal.svelte +220 -0
  70. package/src/components/ShortcutCapture.svelte +82 -0
  71. package/src/components/ShortcutList.svelte +145 -0
  72. package/src/components/TabBar.svelte +52 -0
  73. package/src/components/TableOfContents.svelte +125 -0
  74. package/src/components/ui/ActionLink.svelte +40 -0
  75. package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
  76. package/src/components/ui/Dialog.svelte +97 -0
  77. package/src/components/ui/DropdownMenu.svelte +85 -0
  78. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  79. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  80. package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
  81. package/src/env.d.ts +6 -0
  82. package/src/index.css +36 -166
  83. package/src/lib/__fixtures__/bench-data.ts +0 -13
  84. package/src/lib/anchor.bench.ts +1 -12
  85. package/src/lib/anchor.test.ts +0 -8
  86. package/src/lib/anchor.ts +0 -4
  87. package/src/lib/comment-storage.bench.ts +49 -0
  88. package/src/lib/comment-storage.test.ts +41 -33
  89. package/src/lib/comment-storage.ts +21 -18
  90. package/src/lib/export.bench.ts +21 -0
  91. package/src/lib/export.ts +0 -1
  92. package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
  93. package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
  94. package/src/lib/highlight/core.test.ts +0 -5
  95. package/src/lib/highlight/dom.ts +52 -216
  96. package/src/lib/highlight/highlight-registry.ts +221 -0
  97. package/src/lib/highlight/highlight.bench.ts +92 -0
  98. package/src/lib/highlight/highlighter.ts +112 -132
  99. package/src/lib/highlight/resolver.ts +5 -79
  100. package/src/lib/highlight/types.ts +0 -5
  101. package/src/lib/html-text.test.ts +162 -0
  102. package/src/lib/html-text.ts +161 -0
  103. package/src/lib/i18n/en.ts +26 -0
  104. package/src/lib/i18n/ja.ts +26 -0
  105. package/src/lib/i18n/types.ts +25 -0
  106. package/src/lib/margin-layout.bench.ts +61 -0
  107. package/src/lib/margin-layout.ts +0 -7
  108. package/src/lib/markdown-renderer.test.ts +154 -0
  109. package/src/lib/markdown-renderer.ts +177 -0
  110. package/src/lib/mermaid-config.ts +38 -0
  111. package/src/lib/mermaid-renderer.ts +162 -0
  112. package/src/lib/mermaid-worker.ts +60 -0
  113. package/src/lib/positions.ts +31 -24
  114. package/src/lib/shortcut-registry.ts +244 -0
  115. package/src/lib/utils.ts +0 -29
  116. package/src/main.ts +16 -0
  117. package/src/schema.ts +16 -5
  118. package/src/server.ts +355 -91
  119. package/src/stores/app.svelte.ts +231 -0
  120. package/src/stores/locale.svelte.ts +46 -0
  121. package/src/stores/settings.svelte.ts +90 -0
  122. package/src/stores/shortcuts.svelte.ts +104 -0
  123. package/src/stores/ui.svelte.ts +12 -0
  124. package/src/template.ts +104 -0
  125. package/src/test-setup.ts +47 -0
  126. package/svelte.config.js +5 -0
  127. package/tsconfig.json +2 -2
  128. package/vite.config.ts +23 -3
  129. package/vscode-readit/.mcp.json +7 -0
  130. package/vscode-readit/.vscodeignore +7 -0
  131. package/vscode-readit/bun.lock +78 -0
  132. package/vscode-readit/icon.svg +10 -0
  133. package/vscode-readit/package.json +110 -0
  134. package/vscode-readit/src/extension.ts +117 -0
  135. package/vscode-readit/src/server-manager.ts +272 -0
  136. package/vscode-readit/src/webview-provider.ts +204 -0
  137. package/vscode-readit/tsconfig.json +20 -0
  138. package/e2e/fixtures/sample.html +0 -13
  139. package/src/App.tsx +0 -368
  140. package/src/components/ActionsMenu.tsx +0 -91
  141. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  142. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
  143. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
  144. package/src/components/Header.tsx +0 -54
  145. package/src/components/InlineEditor.tsx +0 -74
  146. package/src/components/MarginNote.tsx +0 -185
  147. package/src/components/MarginNotes.tsx +0 -23
  148. package/src/components/RawModal.tsx +0 -144
  149. package/src/components/ReanchorConfirm.tsx +0 -36
  150. package/src/components/SettingsModal.tsx +0 -232
  151. package/src/components/TabBar.tsx +0 -60
  152. package/src/components/TableOfContents.tsx +0 -108
  153. package/src/components/comments/CommentBadge.tsx +0 -49
  154. package/src/components/comments/CommentInput.tsx +0 -86
  155. package/src/components/comments/CommentListItem.tsx +0 -90
  156. package/src/components/comments/CommentManager.tsx +0 -129
  157. package/src/components/comments/CommentNav.tsx +0 -109
  158. package/src/components/ui/ActionLink.tsx +0 -28
  159. package/src/components/ui/Dialog.tsx +0 -116
  160. package/src/components/ui/DropdownMenu.tsx +0 -158
  161. package/src/contexts/CommentContext.tsx +0 -198
  162. package/src/contexts/LocaleContext.tsx +0 -76
  163. package/src/contexts/PositionsContext.tsx +0 -16
  164. package/src/contexts/SettingsContext.tsx +0 -133
  165. package/src/hooks/useClickOutside.ts +0 -31
  166. package/src/hooks/useCommentNavigation.ts +0 -107
  167. package/src/hooks/useComments.ts +0 -311
  168. package/src/hooks/useDocument.ts +0 -157
  169. package/src/hooks/useScrollSpy.ts +0 -77
  170. package/src/hooks/useTextSelection.ts +0 -86
  171. package/src/lib/highlight/worker.ts +0 -45
  172. package/src/main.tsx +0 -13
  173. package/src/store.ts +0 -222
@@ -1,116 +0,0 @@
1
- import { X } from "lucide-react";
2
- import { useEffect, useRef } from "react";
3
- import { cn } from "../../lib/utils";
4
-
5
- interface DialogProps {
6
- open: boolean;
7
- onOpenChange: (open: boolean) => void;
8
- children: React.ReactNode;
9
- }
10
-
11
- function Dialog({ open, onOpenChange, children }: DialogProps) {
12
- const ref = useRef<HTMLDialogElement>(null);
13
-
14
- useEffect(() => {
15
- const dialog = ref.current;
16
- if (!dialog) return;
17
-
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
- };
37
-
38
- return (
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>
46
- );
47
- }
48
-
49
- function DialogContent({
50
- className,
51
- children,
52
- onClose,
53
- }: {
54
- className?: string;
55
- children: React.ReactNode;
56
- onClose?: () => void;
57
- }) {
58
- return (
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>
76
- );
77
- }
78
-
79
- function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
80
- return (
81
- <div
82
- className={cn(
83
- "flex items-center justify-between pl-4 pr-12 py-3 border-b border-zinc-100 dark:border-zinc-800",
84
- className,
85
- )}
86
- {...props}
87
- />
88
- );
89
- }
90
-
91
- function DialogTitle({
92
- className,
93
- children,
94
- }: {
95
- className?: string;
96
- children: React.ReactNode;
97
- }) {
98
- return (
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>
107
- );
108
- }
109
-
110
- function DialogBody({ className, ...props }: React.ComponentProps<"div">) {
111
- return (
112
- <div className={cn("flex-1 overflow-auto p-4", className)} {...props} />
113
- );
114
- }
115
-
116
- export { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle };
@@ -1,158 +0,0 @@
1
- import { createContext, use, useCallback, useRef, useState } from "react";
2
- import { useClickOutside } from "../../hooks/useClickOutside";
3
- import { cn } from "../../lib/utils";
4
-
5
- interface DropdownState {
6
- open: boolean;
7
- setOpen: (open: boolean) => void;
8
- }
9
-
10
- const DropdownContext = createContext<DropdownState>({
11
- open: false,
12
- setOpen: () => {},
13
- });
14
-
15
- function DropdownMenu({
16
- open: controlledOpen,
17
- onOpenChange,
18
- children,
19
- }: {
20
- open?: boolean;
21
- onOpenChange?: (open: boolean) => void;
22
- children: React.ReactNode;
23
- }) {
24
- const [internalOpen, setInternalOpen] = useState(false);
25
- const isControlled = controlledOpen !== undefined;
26
- const open = isControlled ? controlledOpen : internalOpen;
27
- const setOpen = useCallback(
28
- (v: boolean) => {
29
- if (!isControlled) setInternalOpen(v);
30
- onOpenChange?.(v);
31
- },
32
- [isControlled, onOpenChange],
33
- );
34
-
35
- const ref = useRef<HTMLDivElement>(null);
36
- useClickOutside(ref, () => setOpen(false), open);
37
-
38
- return (
39
- <DropdownContext value={{ open, setOpen }}>
40
- <div ref={ref} className="relative inline-block">
41
- {children}
42
- </div>
43
- </DropdownContext>
44
- );
45
- }
46
-
47
- function DropdownMenuTrigger({
48
- asChild,
49
- children,
50
- ...props
51
- }: {
52
- asChild?: boolean;
53
- children: React.ReactNode;
54
- } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
55
- const { open, setOpen } = use(DropdownContext);
56
-
57
- if (
58
- asChild &&
59
- children &&
60
- typeof children === "object" &&
61
- "props" in children
62
- ) {
63
- const child = children as React.ReactElement<Record<string, unknown>>;
64
- return (
65
- <child.type
66
- {...child.props}
67
- onClick={(e: React.MouseEvent) => {
68
- setOpen(!open);
69
- if (typeof child.props.onClick === "function") child.props.onClick(e);
70
- }}
71
- />
72
- );
73
- }
74
-
75
- return (
76
- <button type="button" onClick={() => setOpen(!open)} {...props}>
77
- {children}
78
- </button>
79
- );
80
- }
81
-
82
- function DropdownMenuContent({
83
- className,
84
- align = "start",
85
- children,
86
- }: {
87
- className?: string;
88
- align?: "start" | "end";
89
- children: React.ReactNode;
90
- }) {
91
- const { open } = use(DropdownContext);
92
- if (!open) return null;
93
-
94
- return (
95
- <div
96
- className={cn(
97
- "absolute top-full mt-1 z-50 min-w-[8rem] overflow-hidden rounded-xl py-1",
98
- "bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40",
99
- align === "end" ? "right-0" : "left-0",
100
- className,
101
- )}
102
- >
103
- {children}
104
- </div>
105
- );
106
- }
107
-
108
- function DropdownMenuItem({
109
- className,
110
- variant = "default",
111
- onSelect,
112
- children,
113
- ...props
114
- }: {
115
- className?: string;
116
- variant?: "default" | "destructive";
117
- onSelect?: () => void;
118
- children: React.ReactNode;
119
- title?: string;
120
- }) {
121
- const { setOpen } = use(DropdownContext);
122
-
123
- return (
124
- <button
125
- type="button"
126
- className={cn(
127
- "w-full px-3 py-1.5 text-left text-sm outline-none select-none transition-colors duration-150 flex items-center gap-2 cursor-default",
128
- variant === "default" &&
129
- "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-100",
130
- variant === "destructive" &&
131
- "text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300",
132
- "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
133
- className,
134
- )}
135
- onClick={() => {
136
- onSelect?.();
137
- setOpen(false);
138
- }}
139
- {...props}
140
- >
141
- {children}
142
- </button>
143
- );
144
- }
145
-
146
- function DropdownMenuSeparator({ className }: { className?: string }) {
147
- return (
148
- <div className={cn("my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)} />
149
- );
150
- }
151
-
152
- export {
153
- DropdownMenu,
154
- DropdownMenuContent,
155
- DropdownMenuItem,
156
- DropdownMenuSeparator,
157
- DropdownMenuTrigger,
158
- };
@@ -1,198 +0,0 @@
1
- import {
2
- createContext,
3
- type ReactNode,
4
- use,
5
- useCallback,
6
- useEffect,
7
- useMemo,
8
- } from "react";
9
- import { toast } from "sonner";
10
- import { useCommentNavigation } from "../hooks/useCommentNavigation";
11
- import { useComments } from "../hooks/useComments";
12
- import { formatComment } from "../lib/export";
13
- import { truncate } from "../lib/utils";
14
- import type { Comment } from "../schema";
15
- import { appStore, useAppStore } from "../store";
16
- import { useLocale } from "./LocaleContext";
17
-
18
- // Stable callbacks — never causes re-renders
19
- interface CommentActionsValue {
20
- addComment: (
21
- selectedText: string,
22
- comment: string,
23
- startOffset: number,
24
- endOffset: number,
25
- ) => void;
26
- editComment: (id: string, newText: string) => void;
27
- deleteComment: (id: string) => void;
28
- deleteAll: () => void;
29
- reanchorComment: (
30
- id: string,
31
- selectedText: string,
32
- startOffset: number,
33
- endOffset: number,
34
- ) => void;
35
- setHoveredCommentId: (id: string | undefined) => void;
36
- navigateToComment: (commentId: string) => void;
37
- navigatePrevious: () => void;
38
- navigateNext: () => void;
39
- startReanchor: (commentId: string) => void;
40
- cancelReanchor: () => void;
41
- copyComment: (comment: Comment) => void;
42
- scrollToHighlight: (commentId: string) => void;
43
- }
44
-
45
- const CommentActionsContext = createContext<CommentActionsValue | null>(null);
46
-
47
- export function useCommentActions(): CommentActionsValue {
48
- const value = use(CommentActionsContext);
49
- if (!value) {
50
- throw new Error("useCommentActions must be used within a CommentProvider");
51
- }
52
- return value;
53
- }
54
-
55
- // Volatile — re-renders consumers on change
56
- interface CommentDataValue {
57
- comments: Comment[];
58
- commentCount: number;
59
- sortedComments: Comment[];
60
- currentIndex: number;
61
- reanchorTarget: { commentId: string } | null;
62
- }
63
-
64
- const CommentDataContext = createContext<CommentDataValue | null>(null);
65
-
66
- export function useCommentData(): CommentDataValue {
67
- const value = use(CommentDataContext);
68
- if (!value) {
69
- throw new Error("useCommentData must be used within a CommentProvider");
70
- }
71
- return value;
72
- }
73
-
74
- export type CommentContextValue = CommentActionsValue & CommentDataValue;
75
-
76
- export function useCommentContext(): CommentContextValue {
77
- return { ...useCommentActions(), ...useCommentData() };
78
- }
79
-
80
- export const CommentContext = CommentDataContext;
81
-
82
- interface CommentProviderProps {
83
- filePath: string;
84
- clean: boolean;
85
- children: ReactNode;
86
- }
87
-
88
- export function CommentProvider({
89
- filePath,
90
- clean,
91
- children,
92
- }: CommentProviderProps) {
93
- const {
94
- comments,
95
- error: commentsError,
96
- addComment,
97
- deleteComment,
98
- deleteAll,
99
- editComment,
100
- reanchorComment,
101
- } = useComments(filePath, { clean });
102
-
103
- const sortedComments = useAppStore(
104
- (s) => s.documents.get(filePath)?.sortedComments ?? [],
105
- );
106
-
107
- const {
108
- currentIndex,
109
- setHoveredCommentId,
110
- navigateToComment,
111
- navigatePrevious,
112
- navigateNext,
113
- } = useCommentNavigation(sortedComments);
114
-
115
- const reanchorTarget = useAppStore(
116
- (s) => s.getActiveDocumentState()?.reanchorTarget ?? null,
117
- );
118
- const startReanchor = useCallback((commentId: string) => {
119
- appStore.getState().setReanchorTarget({ commentId });
120
- }, []);
121
- const cancelReanchor = useCallback(() => {
122
- appStore.getState().setReanchorTarget(null);
123
- }, []);
124
- const { t } = useLocale();
125
-
126
- useEffect(() => {
127
- if (commentsError) {
128
- toast.error(commentsError);
129
- }
130
- }, [commentsError]);
131
-
132
- const copyComment = useCallback(
133
- (comment: Comment) => {
134
- navigator.clipboard.writeText(formatComment(comment));
135
- toast.success(t("toast.copied", { text: truncate(comment.comment) }));
136
- },
137
- [t],
138
- );
139
-
140
- const scrollToHighlight = useCallback((commentId: string) => {
141
- const mark = window.document.querySelector(
142
- `mark[data-comment-id="${commentId}"]`,
143
- );
144
- if (mark) {
145
- mark.scrollIntoView({ behavior: "smooth", block: "center" });
146
- }
147
- }, []);
148
-
149
- const actions = useMemo<CommentActionsValue>(
150
- () => ({
151
- addComment,
152
- editComment,
153
- deleteComment,
154
- deleteAll,
155
- reanchorComment,
156
- setHoveredCommentId,
157
- navigateToComment,
158
- navigatePrevious,
159
- navigateNext,
160
- startReanchor,
161
- cancelReanchor,
162
- copyComment,
163
- scrollToHighlight,
164
- }),
165
- [
166
- addComment,
167
- editComment,
168
- deleteComment,
169
- deleteAll,
170
- reanchorComment,
171
- setHoveredCommentId,
172
- navigateToComment,
173
- navigatePrevious,
174
- navigateNext,
175
- startReanchor,
176
- cancelReanchor,
177
- copyComment,
178
- scrollToHighlight,
179
- ],
180
- );
181
-
182
- const data = useMemo<CommentDataValue>(
183
- () => ({
184
- comments,
185
- commentCount: comments.length,
186
- sortedComments,
187
- currentIndex,
188
- reanchorTarget,
189
- }),
190
- [comments, sortedComments, currentIndex, reanchorTarget],
191
- );
192
-
193
- return (
194
- <CommentActionsContext value={actions}>
195
- <CommentDataContext value={data}>{children}</CommentDataContext>
196
- </CommentActionsContext>
197
- );
198
- }
@@ -1,76 +0,0 @@
1
- import {
2
- createContext,
3
- type ReactNode,
4
- use,
5
- useCallback,
6
- useMemo,
7
- useState,
8
- } from "react";
9
- import {
10
- createT,
11
- type Locale,
12
- Locales,
13
- type TranslationKey,
14
- } from "../lib/i18n";
15
-
16
- const STORAGE_KEY = "readit:locale";
17
-
18
- function detectLocale(): Locale {
19
- const browserLang = navigator.language.slice(0, 2).toLowerCase();
20
- if (browserLang === "ja") return Locales.JA;
21
- return Locales.EN;
22
- }
23
-
24
- function getStoredLocale(): Locale {
25
- try {
26
- const stored = localStorage.getItem(STORAGE_KEY);
27
- if (stored === Locales.JA || stored === Locales.EN) {
28
- return stored;
29
- }
30
- } catch {
31
- // localStorage may be unavailable
32
- }
33
- return detectLocale();
34
- }
35
-
36
- interface LocaleContextValue {
37
- locale: Locale;
38
- setLocale: (locale: Locale) => void;
39
- t: (key: TranslationKey, params?: Record<string, string | number>) => string;
40
- }
41
-
42
- const LocaleContext = createContext<LocaleContextValue | null>(null);
43
-
44
- export function useLocale(): LocaleContextValue {
45
- const value = use(LocaleContext);
46
- if (!value) {
47
- throw new Error("useLocale must be used within a LocaleProvider");
48
- }
49
- return value;
50
- }
51
-
52
- interface LocaleProviderProps {
53
- children: ReactNode;
54
- }
55
-
56
- export function LocaleProvider({ children }: LocaleProviderProps) {
57
- const [locale, setLocaleState] = useState<Locale>(getStoredLocale);
58
-
59
- const setLocale = useCallback((newLocale: Locale) => {
60
- setLocaleState(newLocale);
61
- try {
62
- localStorage.setItem(STORAGE_KEY, newLocale);
63
- } catch {
64
- // localStorage may be unavailable
65
- }
66
- }, []);
67
-
68
- const t = useMemo(() => createT(locale), [locale]);
69
-
70
- const value = useMemo<LocaleContextValue>(
71
- () => ({ locale, setLocale, t }),
72
- [locale, setLocale, t],
73
- );
74
-
75
- return <LocaleContext value={value}>{children}</LocaleContext>;
76
- }
@@ -1,16 +0,0 @@
1
- import { createContext, type ReactNode, use, useRef } from "react";
2
- import { Positions } from "../lib/positions";
3
-
4
- const Ctx = createContext<Positions | null>(null);
5
-
6
- export function usePositions(): Positions {
7
- const value = use(Ctx);
8
- if (!value) throw new Error("usePositions requires PositionsProvider");
9
- return value;
10
- }
11
-
12
- export function PositionsProvider({ children }: { children: ReactNode }) {
13
- const ref = useRef<Positions | null>(null);
14
- if (!ref.current) ref.current = new Positions();
15
- return <Ctx value={ref.current}>{children}</Ctx>;
16
- }
@@ -1,133 +0,0 @@
1
- import {
2
- createContext,
3
- type ReactNode,
4
- use,
5
- useCallback,
6
- useEffect,
7
- useMemo,
8
- useState,
9
- } from "react";
10
- import { toast } from "sonner";
11
- import {
12
- FontFamilies,
13
- type FontFamily,
14
- type ThemeMode,
15
- ThemeModes,
16
- } from "../schema";
17
-
18
- const THEME_STORAGE_KEY = "readit:theme";
19
- const DARK_MQ = "(prefers-color-scheme: dark)";
20
-
21
- function getStoredTheme(): ThemeMode {
22
- try {
23
- const stored = localStorage.getItem(THEME_STORAGE_KEY);
24
- if (
25
- stored === ThemeModes.LIGHT ||
26
- stored === ThemeModes.DARK ||
27
- stored === ThemeModes.SYSTEM
28
- ) {
29
- return stored;
30
- }
31
- } catch {
32
- // localStorage may be unavailable
33
- }
34
- return ThemeModes.SYSTEM;
35
- }
36
-
37
- function applyTheme(mode: ThemeMode): void {
38
- const isDark =
39
- mode === ThemeModes.DARK ||
40
- (mode === ThemeModes.SYSTEM && window.matchMedia(DARK_MQ).matches);
41
-
42
- document.documentElement.classList.toggle("dark", isDark);
43
- }
44
-
45
- interface SettingsContextValue {
46
- fontFamily: FontFamily;
47
- setFontFamily: (font: FontFamily) => Promise<void>;
48
- themeMode: ThemeMode;
49
- setThemeMode: (mode: ThemeMode) => void;
50
- }
51
-
52
- export const SettingsContext = createContext<SettingsContextValue | null>(null);
53
-
54
- export function useSettings(): SettingsContextValue {
55
- const value = use(SettingsContext);
56
- if (!value) {
57
- throw new Error("useSettings must be used within a SettingsProvider");
58
- }
59
- return value;
60
- }
61
-
62
- export function SettingsProvider({ children }: { children: ReactNode }) {
63
- const [fontFamily, setFontFamilyState] = useState<FontFamily>(
64
- FontFamilies.SERIF,
65
- );
66
-
67
- useEffect(() => {
68
- const fetchSettings = async () => {
69
- try {
70
- const response = await fetch("/api/settings");
71
- if (response.ok) {
72
- const settings = await response.json();
73
- setFontFamilyState(settings.fontFamily || FontFamilies.SERIF);
74
- }
75
- } catch (err) {
76
- console.error("Failed to fetch settings:", err);
77
- }
78
- };
79
-
80
- fetchSettings();
81
- }, []);
82
-
83
- const setFontFamily = useCallback(async (font: FontFamily) => {
84
- setFontFamilyState(font);
85
-
86
- try {
87
- const response = await fetch("/api/settings", {
88
- method: "PUT",
89
- headers: { "Content-Type": "application/json" },
90
- body: JSON.stringify({ fontFamily: font }),
91
- });
92
-
93
- if (!response.ok) {
94
- throw new Error("Failed to save settings");
95
- }
96
- } catch (err) {
97
- console.error("Failed to save font preference:", err);
98
- toast.error("Failed to save font preference");
99
- }
100
- }, []);
101
-
102
- const [themeMode, setThemeModeState] = useState<ThemeMode>(getStoredTheme);
103
-
104
- useEffect(() => {
105
- applyTheme(themeMode);
106
- }, [themeMode]);
107
-
108
- useEffect(() => {
109
- if (themeMode !== ThemeModes.SYSTEM) return;
110
-
111
- const mq = window.matchMedia(DARK_MQ);
112
- const handler = () => applyTheme(ThemeModes.SYSTEM);
113
-
114
- mq.addEventListener("change", handler);
115
- return () => mq.removeEventListener("change", handler);
116
- }, [themeMode]);
117
-
118
- const setThemeMode = useCallback((mode: ThemeMode) => {
119
- setThemeModeState(mode);
120
- try {
121
- localStorage.setItem(THEME_STORAGE_KEY, mode);
122
- } catch {
123
- // localStorage may be unavailable
124
- }
125
- }, []);
126
-
127
- const value = useMemo<SettingsContextValue>(
128
- () => ({ fontFamily, setFontFamily, themeMode, setThemeMode }),
129
- [fontFamily, setFontFamily, themeMode, setThemeMode],
130
- );
131
-
132
- return <SettingsContext value={value}>{children}</SettingsContext>;
133
- }