@peaske7/readit 0.1.8 → 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.
- package/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -5
- package/biome.json +18 -8
- package/bun.lock +426 -710
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +130 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +116 -0
- package/e2e/perf/fixtures/generate.ts +327 -0
- package/e2e/perf/initial-load.spec.ts +49 -0
- package/e2e/perf/perf.setup.ts +23 -0
- package/e2e/perf/perf.teardown.ts +9 -0
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/scroll.spec.ts +39 -0
- package/e2e/perf/tab-switch.spec.ts +69 -0
- package/e2e/perf/text-selection.spec.ts +119 -0
- package/e2e/perf/utils/metrics.ts +350 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +118 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +24 -41
- package/playwright.config.ts +12 -0
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/{cli/index.ts → cli.ts} +216 -70
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +218 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/Button.svelte +53 -0
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/Text.svelte +42 -0
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +1 -54
- package/src/lib/anchor.bench.ts +47 -68
- package/src/lib/anchor.test.ts +5 -9
- package/src/lib/anchor.ts +9 -93
- package/src/lib/comment-storage.bench.ts +6 -20
- package/src/lib/comment-storage.test.ts +45 -37
- package/src/lib/comment-storage.ts +23 -64
- package/src/lib/export.bench.ts +9 -23
- package/src/lib/export.ts +7 -14
- package/src/lib/headings.test.ts +103 -0
- package/src/lib/headings.ts +44 -0
- package/src/lib/highlight/core.test.ts +1 -6
- package/src/lib/highlight/dom.ts +53 -280
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +122 -302
- package/src/lib/highlight/{core.ts → resolver.ts} +3 -19
- package/src/lib/highlight/types.ts +0 -40
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +13 -36
- package/src/lib/i18n/ja.ts +14 -37
- package/src/lib/i18n/types.ts +13 -36
- package/src/lib/margin-layout.bench.ts +48 -15
- package/src/lib/margin-layout.ts +2 -31
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +177 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +157 -0
- package/src/lib/shortcut-registry.ts +138 -103
- package/src/lib/utils.ts +2 -48
- package/src/main.ts +16 -0
- package/src/schema.ts +92 -0
- package/src/{server/index.ts → server.ts} +427 -163
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +31 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -416
- package/src/components/ActionsMenu.tsx +0 -112
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -259
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -137
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/Header.tsx +0 -65
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -207
- package/src/components/MarginNotes.tsx +0 -50
- package/src/components/RawModal.tsx +0 -143
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -310
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -114
- package/src/components/comments/CommentListItem.tsx +0 -92
- package/src/components/comments/CommentManager.tsx +0 -113
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/ActionLink.tsx +0 -32
- package/src/components/ui/Button.tsx +0 -55
- package/src/components/ui/Dialog.tsx +0 -156
- package/src/components/ui/DropdownMenu.tsx +0 -114
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/components/ui/Text.tsx +0 -54
- package/src/contexts/CommentContext.tsx +0 -229
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/contexts/LocaleContext.tsx +0 -35
- package/src/hooks/useClickOutside.ts +0 -35
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useCommentNavigation.ts +0 -130
- package/src/hooks/useComments.ts +0 -323
- package/src/hooks/useDocument.ts +0 -156
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useHeadings.test.ts +0 -159
- package/src/hooks/useHeadings.ts +0 -129
- package/src/hooks/useKeybindings.ts +0 -108
- package/src/hooks/useKeyboardShortcuts.ts +0 -63
- package/src/hooks/useLayoutMode.ts +0 -44
- package/src/hooks/useLocalePreference.ts +0 -42
- package/src/hooks/useReanchorMode.ts +0 -33
- package/src/hooks/useScrollMetrics.ts +0 -56
- package/src/hooks/useScrollSpy.ts +0 -81
- package/src/hooks/useTextSelection.ts +0 -123
- package/src/hooks/useThemePreference.ts +0 -66
- package/src/lib/context.bench.ts +0 -41
- package/src/lib/context.test.ts +0 -224
- package/src/lib/context.ts +0 -193
- package/src/lib/editor-links.ts +0 -59
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/index.ts +0 -23
- package/src/lib/highlight/script-builder.ts +0 -485
- package/src/lib/html-processor.test.tsx +0 -170
- package/src/lib/html-processor.tsx +0 -95
- package/src/lib/i18n/completeness.test.ts +0 -51
- package/src/lib/i18n/translations.test.ts +0 -39
- package/src/lib/layout-constants.ts +0 -12
- package/src/lib/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/utils.test.ts +0 -110
- package/src/main.tsx +0 -13
- package/src/store/index.test.ts +0 -242
- package/src/store/index.ts +0 -254
- package/src/types/index.ts +0 -127
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
const baseStyles =
|
|
7
|
+
"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";
|
|
8
|
+
|
|
9
|
+
const variantStyles = {
|
|
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
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
const sizeStyles = {
|
|
22
|
+
default: "h-9 px-4",
|
|
23
|
+
sm: "h-8 px-3 text-xs",
|
|
24
|
+
lg: "h-10 px-6",
|
|
25
|
+
icon: "size-9",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
type ButtonVariant = keyof typeof variantStyles;
|
|
29
|
+
type ButtonSize = keyof typeof sizeStyles;
|
|
30
|
+
|
|
31
|
+
interface Props extends HTMLButtonAttributes {
|
|
32
|
+
variant?: ButtonVariant;
|
|
33
|
+
size?: ButtonSize;
|
|
34
|
+
children: Snippet;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
variant = "default",
|
|
39
|
+
size = "default",
|
|
40
|
+
class: className,
|
|
41
|
+
type = "button",
|
|
42
|
+
children,
|
|
43
|
+
...rest
|
|
44
|
+
}: Props = $props();
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<button
|
|
48
|
+
{...rest}
|
|
49
|
+
{type}
|
|
50
|
+
class={cn(baseStyles, variantStyles[variant], sizeStyles[size], className)}
|
|
51
|
+
>
|
|
52
|
+
{@render children()}
|
|
53
|
+
</button>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X } from "lucide-svelte";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
contentClass?: string;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
header?: Snippet;
|
|
12
|
+
headerActions?: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
open = $bindable(false),
|
|
17
|
+
onclose,
|
|
18
|
+
contentClass,
|
|
19
|
+
children,
|
|
20
|
+
header,
|
|
21
|
+
headerActions,
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
let dialogEl: HTMLDialogElement | undefined = $state();
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (!dialogEl) return;
|
|
28
|
+
|
|
29
|
+
if (open && !dialogEl.open) {
|
|
30
|
+
dialogEl.showModal();
|
|
31
|
+
} else if (!open && dialogEl.open) {
|
|
32
|
+
dialogEl.close();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
if (!dialogEl) return;
|
|
38
|
+
|
|
39
|
+
const handleClose = () => {
|
|
40
|
+
open = false;
|
|
41
|
+
onclose?.();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
dialogEl.addEventListener("close", handleClose);
|
|
45
|
+
return () => dialogEl!.removeEventListener("close", handleClose);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
49
|
+
if (e.target === dialogEl && dialogEl?.open) {
|
|
50
|
+
dialogEl.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleCloseClick() {
|
|
55
|
+
if (dialogEl?.open) dialogEl.close();
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<dialog
|
|
60
|
+
bind:this={dialogEl}
|
|
61
|
+
onclick={handleBackdropClick}
|
|
62
|
+
class="backdrop:bg-black/20 dark:backdrop:bg-black/40 backdrop:backdrop-blur-sm bg-transparent p-0 m-auto max-w-none"
|
|
63
|
+
>
|
|
64
|
+
{#if open}
|
|
65
|
+
<div
|
|
66
|
+
class={cn(
|
|
67
|
+
"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",
|
|
68
|
+
contentClass,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{#if header}
|
|
72
|
+
<div
|
|
73
|
+
class="flex items-center justify-between pl-4 pr-12 py-3 border-b border-zinc-100 dark:border-zinc-800"
|
|
74
|
+
>
|
|
75
|
+
<h2 class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
76
|
+
{@render header()}
|
|
77
|
+
</h2>
|
|
78
|
+
{#if headerActions}
|
|
79
|
+
{@render headerActions()}
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
<div class="flex-1 overflow-visible p-4">
|
|
85
|
+
{@render children()}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onclick={handleCloseClick}
|
|
91
|
+
class="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"
|
|
92
|
+
>
|
|
93
|
+
<X class="w-4 h-4" />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
{/if}
|
|
97
|
+
</dialog>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
open?: boolean;
|
|
7
|
+
trigger: Snippet;
|
|
8
|
+
align?: "start" | "end";
|
|
9
|
+
contentClass?: string;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
open = $bindable(false),
|
|
15
|
+
trigger,
|
|
16
|
+
align = "start",
|
|
17
|
+
contentClass,
|
|
18
|
+
children,
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
22
|
+
|
|
23
|
+
function toggle() {
|
|
24
|
+
open = !open;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function close() {
|
|
28
|
+
open = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
32
|
+
if (e.key === "Escape" && open) {
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
close();
|
|
35
|
+
}
|
|
36
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
toggle();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$effect(() => {
|
|
44
|
+
if (!open) return;
|
|
45
|
+
|
|
46
|
+
function handleClickOutside(e: MouseEvent) {
|
|
47
|
+
if (containerEl && !containerEl.contains(e.target as Node)) {
|
|
48
|
+
close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
53
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
58
|
+
<div
|
|
59
|
+
bind:this={containerEl}
|
|
60
|
+
class="relative inline-block"
|
|
61
|
+
onkeydown={handleKeydown}
|
|
62
|
+
>
|
|
63
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
64
|
+
<div onclick={toggle} role="button" tabindex="0" aria-expanded={open}>
|
|
65
|
+
{@render trigger()}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{#if open}
|
|
69
|
+
<div
|
|
70
|
+
class={cn(
|
|
71
|
+
"absolute top-full mt-1 z-50 min-w-[8rem] overflow-hidden rounded-xl py-1",
|
|
72
|
+
"bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-zinc-200/40 dark:border-zinc-700/40",
|
|
73
|
+
align === "end" ? "right-0" : "left-0",
|
|
74
|
+
contentClass,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{@render children()}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<script lang="ts" module>
|
|
83
|
+
export { default as DropdownMenuItem } from "./DropdownMenuItem.svelte";
|
|
84
|
+
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.svelte";
|
|
85
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
variant?: "default" | "destructive";
|
|
7
|
+
onselect?: () => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
variant = "default",
|
|
15
|
+
onselect,
|
|
16
|
+
class: className,
|
|
17
|
+
title,
|
|
18
|
+
children,
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
role="menuitem"
|
|
25
|
+
{title}
|
|
26
|
+
class={cn(
|
|
27
|
+
"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",
|
|
28
|
+
variant === "default" &&
|
|
29
|
+
"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 focus-visible:bg-zinc-50 dark:focus-visible:bg-zinc-800 focus-visible:text-zinc-900 dark:focus-visible:text-zinc-100",
|
|
30
|
+
variant === "destructive" &&
|
|
31
|
+
"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 focus-visible:bg-red-50 dark:focus-visible:bg-red-950 focus-visible:text-red-700 dark:focus-visible:text-red-300",
|
|
32
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
onclick={() => onselect?.()}
|
|
36
|
+
>
|
|
37
|
+
{@render children()}
|
|
38
|
+
</button>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { FontFamilies } from "../../schema";
|
|
5
|
+
import { settings } from "../../stores/settings.svelte";
|
|
6
|
+
|
|
7
|
+
const variantStyles = {
|
|
8
|
+
title:
|
|
9
|
+
"text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-100",
|
|
10
|
+
section: "text-sm font-medium text-zinc-900 dark:text-zinc-100",
|
|
11
|
+
subsection: "text-xs font-medium text-zinc-700 dark:text-zinc-300",
|
|
12
|
+
overline:
|
|
13
|
+
"text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider",
|
|
14
|
+
body: "text-sm text-zinc-600 dark:text-zinc-400",
|
|
15
|
+
caption: "text-xs text-zinc-500 dark:text-zinc-400",
|
|
16
|
+
micro: "text-[10px] text-zinc-400 dark:text-zinc-500",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
type TextVariant = keyof typeof variantStyles;
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
variant?: TextVariant;
|
|
23
|
+
as?: "p" | "span" | "div" | "h1" | "h2" | "h3" | "label" | "pre";
|
|
24
|
+
class?: string;
|
|
25
|
+
children: Snippet;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
variant = "body",
|
|
30
|
+
as = "p",
|
|
31
|
+
class: className,
|
|
32
|
+
children,
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
let fontClass = $derived(
|
|
36
|
+
settings.fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif",
|
|
37
|
+
);
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<svelte:element this={as} class={cn(fontClass, variantStyles[variant], className)}>
|
|
41
|
+
{@render children()}
|
|
42
|
+
</svelte:element>
|
package/src/env.d.ts
ADDED
package/src/index.css
CHANGED
|
@@ -101,8 +101,8 @@ html {
|
|
|
101
101
|
--comment-color-3-bg-focused: rgba(205, 145, 155, 0.65);
|
|
102
102
|
--comment-color-3-border: #b86b78;
|
|
103
103
|
|
|
104
|
-
/* Pending -
|
|
105
|
-
--pending-bg: rgba(
|
|
104
|
+
/* Pending - warm amber tint (matches comment highlight palette) */
|
|
105
|
+
--pending-bg: rgba(245, 222, 160, 0.45);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
.dark {
|
|
@@ -151,127 +151,38 @@ html {
|
|
|
151
151
|
--comment-color-3-bg-focused: rgba(184, 107, 120, 0.55);
|
|
152
152
|
--comment-color-3-border: #d08a98;
|
|
153
153
|
|
|
154
|
-
--pending-bg: rgba(
|
|
154
|
+
--pending-bg: rgba(202, 168, 74, 0.35);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/* ========================================
|
|
158
|
-
Comment Highlight Styles
|
|
158
|
+
Comment Highlight Styles (CSS Custom Highlight API)
|
|
159
159
|
======================================== */
|
|
160
160
|
|
|
161
|
-
/*
|
|
162
|
-
|
|
163
|
-
background-color: var(--
|
|
164
|
-
cursor: pointer;
|
|
165
|
-
transition:
|
|
166
|
-
background-color 150ms ease,
|
|
167
|
-
opacity 150ms ease;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/* Color variants based on data-color-index */
|
|
171
|
-
mark[data-comment-id][data-color-index="0"] {
|
|
172
|
-
--highlight-bg: var(--comment-color-0-bg);
|
|
173
|
-
--highlight-bg-focused: var(--comment-color-0-bg-focused);
|
|
174
|
-
--highlight-border: var(--comment-color-0-border);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
mark[data-comment-id][data-color-index="1"] {
|
|
178
|
-
--highlight-bg: var(--comment-color-1-bg);
|
|
179
|
-
--highlight-bg-focused: var(--comment-color-1-bg-focused);
|
|
180
|
-
--highlight-border: var(--comment-color-1-border);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
mark[data-comment-id][data-color-index="2"] {
|
|
184
|
-
--highlight-bg: var(--comment-color-2-bg);
|
|
185
|
-
--highlight-bg-focused: var(--comment-color-2-bg-focused);
|
|
186
|
-
--highlight-border: var(--comment-color-2-border);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
mark[data-comment-id][data-color-index="3"] {
|
|
190
|
-
--highlight-bg: var(--comment-color-3-bg);
|
|
191
|
-
--highlight-bg-focused: var(--comment-color-3-bg-focused);
|
|
192
|
-
--highlight-border: var(--comment-color-3-border);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/* Focused state - this highlight is hovered */
|
|
196
|
-
mark[data-comment-id][data-focused="true"] {
|
|
197
|
-
background-color: var(
|
|
198
|
-
--highlight-bg-focused,
|
|
199
|
-
var(--comment-color-0-bg-focused)
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/* Hover on highlight itself */
|
|
204
|
-
mark[data-comment-id]:hover {
|
|
205
|
-
background-color: var(
|
|
206
|
-
--highlight-bg-focused,
|
|
207
|
-
var(--comment-color-0-bg-focused)
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/* ========================================
|
|
212
|
-
Bracket Mode for Long Selections
|
|
213
|
-
======================================== */
|
|
214
|
-
|
|
215
|
-
/* Container for bracket-mode highlights */
|
|
216
|
-
mark[data-comment-id][data-bracket-mode="true"] {
|
|
217
|
-
background-color: transparent;
|
|
218
|
-
position: relative;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/* Only show background on first segment in bracket mode */
|
|
222
|
-
mark[data-comment-id][data-bracket-start="true"] {
|
|
223
|
-
background-color: var(--highlight-bg, var(--comment-color-0-bg));
|
|
224
|
-
border-radius: 2px 2px 0 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/* Focused bracket mode shows full highlight */
|
|
228
|
-
mark[data-comment-id][data-bracket-mode="true"][data-focused="true"],
|
|
229
|
-
mark[data-comment-id][data-bracket-mode="true"]:hover {
|
|
230
|
-
background-color: var(
|
|
231
|
-
--highlight-bg-focused,
|
|
232
|
-
var(--comment-color-0-bg-focused)
|
|
233
|
-
);
|
|
161
|
+
/* Color variants — paint-time only, zero DOM mutations */
|
|
162
|
+
::highlight(comment-color-0) {
|
|
163
|
+
background-color: var(--comment-color-0-bg);
|
|
234
164
|
}
|
|
235
165
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
position: absolute;
|
|
239
|
-
left: -20px;
|
|
240
|
-
width: 2px;
|
|
241
|
-
background-color: var(--bracket-color, var(--comment-color-0-border));
|
|
242
|
-
border-radius: 1px;
|
|
243
|
-
opacity: 0.6;
|
|
244
|
-
transition: opacity 150ms ease;
|
|
245
|
-
pointer-events: none;
|
|
166
|
+
::highlight(comment-color-1) {
|
|
167
|
+
background-color: var(--comment-color-1-bg);
|
|
246
168
|
}
|
|
247
169
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
opacity: 1;
|
|
170
|
+
::highlight(comment-color-2) {
|
|
171
|
+
background-color: var(--comment-color-2-bg);
|
|
251
172
|
}
|
|
252
173
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
position: absolute;
|
|
256
|
-
left: -24px;
|
|
257
|
-
font-family: monospace;
|
|
258
|
-
font-size: 10px;
|
|
259
|
-
color: var(--bracket-color, var(--comment-color-0-border));
|
|
260
|
-
opacity: 0.6;
|
|
261
|
-
pointer-events: none;
|
|
262
|
-
transition: opacity 150ms ease;
|
|
174
|
+
::highlight(comment-color-3) {
|
|
175
|
+
background-color: var(--comment-color-3-bg);
|
|
263
176
|
}
|
|
264
177
|
|
|
265
|
-
|
|
266
|
-
|
|
178
|
+
/* Focused state — higher priority, overrides color group */
|
|
179
|
+
::highlight(comment-focused) {
|
|
180
|
+
background-color: var(--comment-color-0-bg-focused);
|
|
267
181
|
}
|
|
268
182
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.bracket-marker[data-focused="true"] {
|
|
274
|
-
opacity: 1;
|
|
183
|
+
/* Pending selection highlight — neutral gray */
|
|
184
|
+
::highlight(pending-selection) {
|
|
185
|
+
background-color: var(--pending-bg);
|
|
275
186
|
}
|
|
276
187
|
|
|
277
188
|
/* ========================================
|
|
@@ -301,64 +212,6 @@ mark[data-comment-id][data-bracket-mode="true"]:hover {
|
|
|
301
212
|
opacity: 0.7;
|
|
302
213
|
}
|
|
303
214
|
|
|
304
|
-
/* Pending selection highlight - neutral gray */
|
|
305
|
-
mark[data-pending] {
|
|
306
|
-
background-color: var(--pending-bg);
|
|
307
|
-
cursor: text;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/* Ensure highlights are visible in all contexts including headings */
|
|
311
|
-
.prose mark[data-pending],
|
|
312
|
-
.prose h1 mark[data-pending],
|
|
313
|
-
.prose h2 mark[data-pending],
|
|
314
|
-
.prose h3 mark[data-pending],
|
|
315
|
-
.prose h4 mark[data-pending],
|
|
316
|
-
.prose h5 mark[data-pending],
|
|
317
|
-
.prose h6 mark[data-pending] {
|
|
318
|
-
background-color: var(--pending-bg) !important;
|
|
319
|
-
display: inline !important;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/* Ensure comment highlights are visible inside headings (prose overrides) */
|
|
323
|
-
.prose h1 mark[data-comment-id],
|
|
324
|
-
.prose h2 mark[data-comment-id],
|
|
325
|
-
.prose h3 mark[data-comment-id],
|
|
326
|
-
.prose h4 mark[data-comment-id],
|
|
327
|
-
.prose h5 mark[data-comment-id],
|
|
328
|
-
.prose h6 mark[data-comment-id] {
|
|
329
|
-
background-color: var(--highlight-bg, var(--comment-color-0-bg)) !important;
|
|
330
|
-
display: inline !important;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/* Bracket mode in headings - override transparent background */
|
|
334
|
-
.prose h1 mark[data-comment-id][data-bracket-mode="true"],
|
|
335
|
-
.prose h2 mark[data-comment-id][data-bracket-mode="true"],
|
|
336
|
-
.prose h3 mark[data-comment-id][data-bracket-mode="true"],
|
|
337
|
-
.prose h4 mark[data-comment-id][data-bracket-mode="true"],
|
|
338
|
-
.prose h5 mark[data-comment-id][data-bracket-mode="true"],
|
|
339
|
-
.prose h6 mark[data-comment-id][data-bracket-mode="true"] {
|
|
340
|
-
background-color: var(--highlight-bg, var(--comment-color-0-bg)) !important;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/* Hover state for comment highlights in headings */
|
|
344
|
-
.prose h1 mark[data-comment-id]:hover,
|
|
345
|
-
.prose h2 mark[data-comment-id]:hover,
|
|
346
|
-
.prose h3 mark[data-comment-id]:hover,
|
|
347
|
-
.prose h4 mark[data-comment-id]:hover,
|
|
348
|
-
.prose h5 mark[data-comment-id]:hover,
|
|
349
|
-
.prose h6 mark[data-comment-id]:hover,
|
|
350
|
-
.prose h1 mark[data-comment-id][data-focused="true"],
|
|
351
|
-
.prose h2 mark[data-comment-id][data-focused="true"],
|
|
352
|
-
.prose h3 mark[data-comment-id][data-focused="true"],
|
|
353
|
-
.prose h4 mark[data-comment-id][data-focused="true"],
|
|
354
|
-
.prose h5 mark[data-comment-id][data-focused="true"],
|
|
355
|
-
.prose h6 mark[data-comment-id][data-focused="true"] {
|
|
356
|
-
background-color: var(
|
|
357
|
-
--highlight-bg-focused,
|
|
358
|
-
var(--comment-color-0-bg-focused)
|
|
359
|
-
) !important;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
215
|
/* Toast animation */
|
|
363
216
|
[data-sonner-toast] {
|
|
364
217
|
animation: toast-in 0.2s ease-out;
|
|
@@ -579,6 +432,23 @@ mark[data-pending] {
|
|
|
579
432
|
margin: 1.75em 0;
|
|
580
433
|
}
|
|
581
434
|
|
|
435
|
+
/* Shiki syntax-highlighted code blocks (server-rendered) */
|
|
436
|
+
.prose pre.shiki {
|
|
437
|
+
margin: 1.5em 0;
|
|
438
|
+
border-radius: 0.5em;
|
|
439
|
+
font-size: 0.875em;
|
|
440
|
+
padding: 1em;
|
|
441
|
+
overflow-x: auto;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.prose pre.shiki code {
|
|
445
|
+
background: transparent;
|
|
446
|
+
padding: 0;
|
|
447
|
+
font-size: inherit;
|
|
448
|
+
font-weight: 400;
|
|
449
|
+
color: inherit;
|
|
450
|
+
}
|
|
451
|
+
|
|
582
452
|
.prose pre code {
|
|
583
453
|
background: transparent;
|
|
584
454
|
padding: 0;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import type { Comment, CommentFile } from "../../
|
|
1
|
+
import type { Comment, CommentFile } from "../../schema";
|
|
2
2
|
import { serializeComments } from "../comment-storage";
|
|
3
3
|
|
|
4
|
-
// --- Document fixtures ---
|
|
5
|
-
|
|
6
4
|
function generateMarkdownDoc(lineCount: number): string {
|
|
7
5
|
const lines: string[] = [];
|
|
8
6
|
lines.push("# Performance Test Document");
|
|
@@ -21,13 +19,11 @@ function generateMarkdownDoc(lineCount: number): string {
|
|
|
21
19
|
);
|
|
22
20
|
lines.push("");
|
|
23
21
|
|
|
24
|
-
// Add a list
|
|
25
22
|
for (let j = 1; j <= 3; j++) {
|
|
26
23
|
lines.push(`- Item ${j} in section ${sectionNum}`);
|
|
27
24
|
}
|
|
28
25
|
lines.push("");
|
|
29
26
|
|
|
30
|
-
// Add a code block
|
|
31
27
|
lines.push("```typescript");
|
|
32
28
|
lines.push(`function section${sectionNum}() {`);
|
|
33
29
|
lines.push(` const value = ${sectionNum} * 42;`);
|
|
@@ -36,13 +32,11 @@ function generateMarkdownDoc(lineCount: number): string {
|
|
|
36
32
|
lines.push("```");
|
|
37
33
|
lines.push("");
|
|
38
34
|
|
|
39
|
-
// Add a paragraph
|
|
40
35
|
lines.push(
|
|
41
36
|
`The conclusion of section ${sectionNum} summarizes the key findings and provides actionable recommendations for the reader to follow.`,
|
|
42
37
|
);
|
|
43
38
|
lines.push("");
|
|
44
39
|
|
|
45
|
-
// Add a table every few sections
|
|
46
40
|
if (sectionNum % 3 === 0) {
|
|
47
41
|
lines.push("| Column A | Column B | Column C |");
|
|
48
42
|
lines.push("|----------|----------|----------|");
|
|
@@ -65,11 +59,8 @@ export const SMALL_DOC = generateMarkdownDoc(30);
|
|
|
65
59
|
export const MEDIUM_DOC = generateMarkdownDoc(150);
|
|
66
60
|
export const LARGE_DOC = generateMarkdownDoc(300);
|
|
67
61
|
|
|
68
|
-
// --- Comment fixtures ---
|
|
69
|
-
|
|
70
62
|
function makeComment(index: number, doc: string): Comment {
|
|
71
63
|
const lines = doc.split("\n");
|
|
72
|
-
// Distribute comments across the document
|
|
73
64
|
const targetLine = Math.min(
|
|
74
65
|
Math.floor((index + 1) * (lines.length / 55)),
|
|
75
66
|
lines.length - 1,
|
|
@@ -81,7 +72,6 @@ function makeComment(index: number, doc: string): Comment {
|
|
|
81
72
|
? lineText.slice(0, Math.min(30, lineText.length))
|
|
82
73
|
: lineText || "default text";
|
|
83
74
|
|
|
84
|
-
// Calculate actual character offset
|
|
85
75
|
let startOffset = 0;
|
|
86
76
|
for (let i = 0; i < targetLine; i++) {
|
|
87
77
|
startOffset += lines[i].length + 1; // +1 for \n
|
|
@@ -92,7 +82,6 @@ function makeComment(index: number, doc: string): Comment {
|
|
|
92
82
|
id: `bench${String(index).padStart(3, "0")}`,
|
|
93
83
|
selectedText,
|
|
94
84
|
comment: `Benchmark comment ${index}: This text needs to be reviewed and updated.`,
|
|
95
|
-
createdAt: "2025-01-01T00:00:00.000Z",
|
|
96
85
|
startOffset,
|
|
97
86
|
endOffset,
|
|
98
87
|
lineHint: `L${targetLine + 1}`,
|
|
@@ -107,8 +96,6 @@ export const COMMENTS_1 = makeComments(1);
|
|
|
107
96
|
export const COMMENTS_10 = makeComments(10);
|
|
108
97
|
export const COMMENTS_50 = makeComments(50);
|
|
109
98
|
|
|
110
|
-
// --- Serialized comment file fixtures ---
|
|
111
|
-
|
|
112
99
|
function makeCommentFile(comments: Comment[]): CommentFile {
|
|
113
100
|
return {
|
|
114
101
|
source: "/bench/test-doc.md",
|
|
@@ -125,43 +112,3 @@ export const COMMENT_FILE_OBJ_LARGE = makeCommentFile(COMMENTS_50);
|
|
|
125
112
|
export const COMMENT_FILE_SMALL = serializeComments(COMMENT_FILE_OBJ_SMALL);
|
|
126
113
|
export const COMMENT_FILE_MEDIUM = serializeComments(COMMENT_FILE_OBJ_MEDIUM);
|
|
127
114
|
export const COMMENT_FILE_LARGE = serializeComments(COMMENT_FILE_OBJ_LARGE);
|
|
128
|
-
|
|
129
|
-
// --- Highlight position fixtures ---
|
|
130
|
-
|
|
131
|
-
export function makeHighlightPositions(count: number): Record<string, number> {
|
|
132
|
-
const positions: Record<string, number> = {};
|
|
133
|
-
for (let i = 0; i < count; i++) {
|
|
134
|
-
// ~200px spacing with clustering every 3rd note for overlap scenarios
|
|
135
|
-
positions[`c${i}`] = i * 200 + (i % 3 === 0 ? 50 : 0);
|
|
136
|
-
}
|
|
137
|
-
return positions;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// --- HTML fixture ---
|
|
141
|
-
|
|
142
|
-
function generateHtmlDoc(): string {
|
|
143
|
-
const parts: string[] = [
|
|
144
|
-
"<!DOCTYPE html>",
|
|
145
|
-
"<html><head>",
|
|
146
|
-
"<style>.main { color: red; font-size: 16px; }</style>",
|
|
147
|
-
"<script>console.log('test script');</script>",
|
|
148
|
-
"</head><body>",
|
|
149
|
-
];
|
|
150
|
-
|
|
151
|
-
for (let i = 0; i < 30; i++) {
|
|
152
|
-
parts.push(`<section id="s${i}">`);
|
|
153
|
-
parts.push(`<h2>Section ${i}</h2>`);
|
|
154
|
-
parts.push(
|
|
155
|
-
`<p>Paragraph with & entities <and> special "chars" in section ${i}.</p>`,
|
|
156
|
-
);
|
|
157
|
-
parts.push(
|
|
158
|
-
`<ul><li>Item ${i}.1</li><li>Item ${i}.2</li><li>Item ${i}.3</li></ul>`,
|
|
159
|
-
);
|
|
160
|
-
parts.push("</section>");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
parts.push("</body></html>");
|
|
164
|
-
return parts.join("\n");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export const HTML_DOC = generateHtmlDoc();
|