@peaske7/readit 0.1.4 → 0.1.5
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/.agents/skills/remotion-best-practices/SKILL.md +61 -0
- package/.agents/skills/remotion-best-practices/rules/3d.md +86 -0
- package/.agents/skills/remotion-best-practices/rules/animations.md +27 -0
- package/.agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +178 -0
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/.agents/skills/remotion-best-practices/rules/assets.md +78 -0
- package/.agents/skills/remotion-best-practices/rules/audio-visualization.md +198 -0
- package/.agents/skills/remotion-best-practices/rules/audio.md +169 -0
- package/.agents/skills/remotion-best-practices/rules/calculate-metadata.md +134 -0
- package/.agents/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/.agents/skills/remotion-best-practices/rules/charts.md +120 -0
- package/.agents/skills/remotion-best-practices/rules/compositions.md +154 -0
- package/.agents/skills/remotion-best-practices/rules/display-captions.md +184 -0
- package/.agents/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/.agents/skills/remotion-best-practices/rules/ffmpeg.md +38 -0
- package/.agents/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/.agents/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/.agents/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/.agents/skills/remotion-best-practices/rules/get-video-duration.md +60 -0
- package/.agents/skills/remotion-best-practices/rules/gifs.md +141 -0
- package/.agents/skills/remotion-best-practices/rules/images.md +134 -0
- package/.agents/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
- package/.agents/skills/remotion-best-practices/rules/light-leaks.md +73 -0
- package/.agents/skills/remotion-best-practices/rules/lottie.md +70 -0
- package/.agents/skills/remotion-best-practices/rules/maps.md +412 -0
- package/.agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
- package/.agents/skills/remotion-best-practices/rules/measuring-text.md +140 -0
- package/.agents/skills/remotion-best-practices/rules/parameters.md +109 -0
- package/.agents/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/.agents/skills/remotion-best-practices/rules/sfx.md +26 -0
- package/.agents/skills/remotion-best-practices/rules/subtitles.md +36 -0
- package/.agents/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/.agents/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/.agents/skills/remotion-best-practices/rules/timing.md +179 -0
- package/.agents/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
- package/.agents/skills/remotion-best-practices/rules/transitions.md +197 -0
- package/.agents/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
- package/.agents/skills/remotion-best-practices/rules/trimming.md +51 -0
- package/.agents/skills/remotion-best-practices/rules/videos.md +171 -0
- package/.agents/skills/remotion-best-practices/rules/voiceover.md +99 -0
- package/.agents/skills/simple/SKILL.md +52 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +3254 -0
- package/.agents/skills/vercel-react-best-practices/README.md +123 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +141 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/CLAUDE.md +142 -0
- package/.claude/commands/review.md +120 -0
- package/.claude/commands/sync-docs.md +71 -0
- package/.claude/roadmap.md +98 -0
- package/.claude/rules/style-guide.md +830 -0
- package/.claude/settings.json +18 -0
- package/.claude/user-stories.md +248 -0
- package/AGENTS.md +64 -0
- package/README.md +7 -7
- package/biome.json +69 -0
- package/bun.lock +1124 -0
- package/docs/design.md +563 -0
- package/docs/plans/2026-03-13-keyboard-shortcuts-design.md +129 -0
- package/docs/plans/2026-03-13-keyboard-shortcuts-plan.md +1471 -0
- package/docs/plans/2026-03-13-multi-document-design.md +183 -0
- package/docs/plans/2026-03-13-performance-benchmarks-design.md +121 -0
- package/e2e/comments.spec.ts +125 -0
- package/e2e/document-load.spec.ts +54 -0
- package/e2e/export.spec.ts +58 -0
- package/e2e/fixtures/sample.html +13 -0
- package/e2e/fixtures/sample.md +7 -0
- package/e2e/persistence-file.spec.ts +342 -0
- package/e2e/utils/cli.ts +84 -0
- package/e2e/utils/selection.ts +135 -0
- package/{dist/index.html → index.html} +8 -2
- package/lefthook.yml +8 -0
- package/package.json +17 -39
- package/playwright.config.ts +22 -0
- package/skills-lock.json +20 -0
- package/src/App.tsx +396 -0
- package/src/cli/index.ts +328 -0
- package/src/components/ActionsMenu.tsx +110 -0
- package/src/components/DocumentViewer/CodeBlock.tsx +83 -0
- package/src/components/DocumentViewer/DocumentViewer.tsx +257 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +251 -0
- package/src/components/DocumentViewer/MermaidDiagram.tsx +137 -0
- package/src/components/DocumentViewer/index.ts +1 -0
- package/src/components/FloatingTOC.tsx +59 -0
- package/src/components/Header.tsx +63 -0
- package/src/components/InlineEditor.tsx +72 -0
- package/src/components/MarginNote.tsx +198 -0
- package/src/components/MarginNotes.tsx +50 -0
- package/src/components/RawModal.tsx +141 -0
- package/src/components/ReanchorConfirm.tsx +33 -0
- package/src/components/SettingsModal.tsx +221 -0
- package/src/components/ShortcutCapture.tsx +45 -0
- package/src/components/ShortcutList.tsx +157 -0
- package/src/components/TabBar.tsx +60 -0
- package/src/components/TableOfContents.tsx +108 -0
- package/src/components/comments/CommentBadge.tsx +43 -0
- package/src/components/comments/CommentInput.tsx +119 -0
- package/src/components/comments/CommentListItem.tsx +82 -0
- package/src/components/comments/CommentManager.tsx +106 -0
- package/src/components/comments/CommentMinimap.tsx +62 -0
- package/src/components/comments/CommentNav.tsx +104 -0
- package/src/components/ui/ActionBar.tsx +16 -0
- package/src/components/ui/ActionLink.tsx +32 -0
- package/src/components/ui/Button.tsx +55 -0
- package/src/components/ui/Dialog.tsx +156 -0
- package/src/components/ui/DropdownMenu.tsx +114 -0
- package/src/components/ui/SeparatorDot.tsx +9 -0
- package/src/components/ui/Text.tsx +54 -0
- package/src/contexts/CommentContext.tsx +222 -0
- package/src/contexts/LayoutContext.tsx +76 -0
- package/src/hooks/useClickOutside.ts +35 -0
- package/src/hooks/useClipboard.ts +79 -0
- package/src/hooks/useCommentNavigation.ts +130 -0
- package/src/hooks/useComments.ts +323 -0
- package/src/hooks/useDocument.ts +131 -0
- package/src/hooks/useFontPreference.ts +76 -0
- package/src/hooks/useHeadings.test.ts +159 -0
- package/src/hooks/useHeadings.ts +129 -0
- package/src/hooks/useKeybindings.ts +120 -0
- package/src/hooks/useKeyboardShortcuts.ts +63 -0
- package/src/hooks/useLayoutMode.ts +44 -0
- package/src/hooks/useReanchorMode.ts +33 -0
- package/src/hooks/useScrollMetrics.ts +56 -0
- package/src/hooks/useScrollSpy.ts +81 -0
- package/src/hooks/useTextSelection.ts +123 -0
- package/src/hooks/useThemePreference.ts +66 -0
- package/src/index.css +823 -0
- package/src/lib/__fixtures__/bench-data.ts +167 -0
- package/src/lib/anchor.bench.ts +112 -0
- package/src/lib/anchor.test.ts +531 -0
- package/src/lib/anchor.ts +465 -0
- package/src/lib/comment-storage.bench.ts +63 -0
- package/src/lib/comment-storage.test.ts +624 -0
- package/src/lib/comment-storage.ts +263 -0
- package/src/lib/context.bench.ts +41 -0
- package/src/lib/context.test.ts +224 -0
- package/src/lib/context.ts +193 -0
- package/src/lib/export.bench.ts +35 -0
- package/src/lib/export.ts +43 -0
- package/src/lib/highlight/colors.ts +37 -0
- package/src/lib/highlight/core.test.ts +98 -0
- package/src/lib/highlight/core.ts +54 -0
- package/src/lib/highlight/dom.ts +342 -0
- package/src/lib/highlight/highlighter.ts +427 -0
- package/src/lib/highlight/index.ts +23 -0
- package/src/lib/highlight/script-builder.ts +485 -0
- package/src/lib/highlight/types.ts +57 -0
- package/src/lib/html-processor.test.tsx +170 -0
- package/src/lib/html-processor.tsx +95 -0
- package/src/lib/layout-constants.ts +12 -0
- package/src/lib/margin-layout.bench.ts +28 -0
- package/src/lib/margin-layout.ts +100 -0
- package/src/lib/scroll.test.ts +118 -0
- package/src/lib/scroll.ts +47 -0
- package/src/lib/shortcut-registry.test.ts +173 -0
- package/src/lib/shortcut-registry.ts +209 -0
- package/src/lib/utils.test.ts +110 -0
- package/src/lib/utils.ts +50 -0
- package/src/main.tsx +10 -0
- package/src/server/index.ts +766 -0
- package/src/store/index.test.ts +220 -0
- package/src/store/index.ts +234 -0
- package/src/test-setup.ts +1 -0
- package/src/types/index.ts +115 -0
- package/test.md +74 -0
- package/tsconfig.cli.json +12 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +19 -0
- package/vitest.config.ts +15 -0
- package/dist/assets/_basePickBy-hOr-yGsE.js +0 -1
- package/dist/assets/_baseUniq-b7bzdUSn.js +0 -1
- package/dist/assets/arc-D65wG9gm.js +0 -1
- package/dist/assets/architecture-PBZL5I3N-DBa6CAv_.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-Djwpsh98.js +0 -36
- package/dist/assets/array-DOVTz2Mq.js +0 -1
- package/dist/assets/blockDiagram-WCTKOSBZ-BdW5TTxj.js +0 -132
- package/dist/assets/c4Diagram-IC4MRINW-DTmkHEXu.js +0 -10
- package/dist/assets/channel-B3MUFipN.js +0 -1
- package/dist/assets/chunk-4BX2VUAB-DEqzsvDc.js +0 -1
- package/dist/assets/chunk-55IACEB6-BzVuSUV8.js +0 -1
- package/dist/assets/chunk-7E7YKBS2-CZ8IcA4c.js +0 -1
- package/dist/assets/chunk-7R4GIKGN-CWVVC8HX.js +0 -79
- package/dist/assets/chunk-C72U2L5F-B1Tso5TH.js +0 -1
- package/dist/assets/chunk-EGIJ26TM-Cx_7CFik.js +0 -1
- package/dist/assets/chunk-FMBD7UC4-Cfk_iGhv.js +0 -15
- package/dist/assets/chunk-GEFDOKGD-C_5hRbJt.js +0 -2
- package/dist/assets/chunk-GLR3WWYH-CkY7IyBj.js +0 -2
- package/dist/assets/chunk-HHEYEP7N-B0I4X5cr.js +0 -1
- package/dist/assets/chunk-JSJVCQXG-CAjwlVLg.js +0 -1
- package/dist/assets/chunk-KX2RTZJC-DWqnZZ02.js +0 -1
- package/dist/assets/chunk-KYZI473N-gjRVhJgJ.js +0 -53
- package/dist/assets/chunk-L3YUKLVL-D7C9GuxL.js +0 -1
- package/dist/assets/chunk-MX3YWQON-i-77iuVj.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-B22Pvemm.js +0 -220
- package/dist/assets/chunk-O4XLMI2P-ZQd5L6ZD.js +0 -7
- package/dist/assets/chunk-OZEHJAEY-BaPKTELw.js +0 -1
- package/dist/assets/chunk-PQ6SQG4A-DqE1eupT.js +0 -1
- package/dist/assets/chunk-PU5JKC2W-BTqWqedh.js +0 -70
- package/dist/assets/chunk-QZHKN3VN-Nm9TvMss.js +0 -1
- package/dist/assets/chunk-R5LLSJPH-DkiNs1dN.js +0 -1
- package/dist/assets/chunk-WL4C6EOR-CioD2fv2.js +0 -189
- package/dist/assets/chunk-XIRO2GV7-B4GGQONY.js +0 -1
- package/dist/assets/chunk-XPW4576I-C0IbbQos.js +0 -32
- package/dist/assets/chunk-XZSTWKYB-DMOqFWmT.js +0 -94
- package/dist/assets/chunk-YBOYWFTD-CoeQgeVY.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-DV9ltQ7h.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-C6nD9wmM.js +0 -1
- package/dist/assets/clone-DuY6BQEm.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-B6FexK6p.js +0 -1
- package/dist/assets/cytoscape.esm-DoTFyJaN.js +0 -321
- package/dist/assets/dagre-CCcocoCU.js +0 -1
- package/dist/assets/dagre-KLK3FWXG-DIELowj9.js +0 -4
- package/dist/assets/defaultLocale-Ck2Xxk-C.js +0 -1
- package/dist/assets/diagram-E7M64L7V-D1mm0PoO.js +0 -24
- package/dist/assets/diagram-IFDJBPK2-7DVjly8y.js +0 -43
- package/dist/assets/diagram-P4PSJMXO-jO7pfyMb.js +0 -24
- package/dist/assets/dist-BywRdrPx.js +0 -1
- package/dist/assets/erDiagram-INFDFZHY-DSRxlRFy.js +0 -70
- package/dist/assets/flowDiagram-PKNHOUZH-CgKzzNdR.js +0 -162
- package/dist/assets/ganttDiagram-A5KZAMGK-CtsE7Y4E.js +0 -292
- package/dist/assets/gitGraph-HDMCJU4V-BU9uhwtz.js +0 -1
- package/dist/assets/gitGraphDiagram-K3NZZRJ6-DOU8RGdw.js +0 -65
- package/dist/assets/graphlib-WkJoBgka.js +0 -1
- package/dist/assets/index-CKVArt9D.js +0 -562
- package/dist/assets/index-DzRKJazf.css +0 -2
- package/dist/assets/info-3K5VOQVL-CPpvM-SG.js +0 -1
- package/dist/assets/infoDiagram-LFFYTUFH-VKLs5DsF.js +0 -2
- package/dist/assets/init-Bft5Ffpj.js +0 -1
- package/dist/assets/isArrayLikeObject-icl0H0jo.js +0 -1
- package/dist/assets/isEmpty-Du8sNmkE.js +0 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-CsWvEjux.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-BzJGTdIT.js +0 -139
- package/dist/assets/kanban-definition-K7BYSVSG-B_9ClJ1A.js +0 -89
- package/dist/assets/katex-BJrMXEjr.js +0 -261
- package/dist/assets/line-CC_tDGId.js +0 -1
- package/dist/assets/linear-Cts_d04Y.js +0 -1
- package/dist/assets/math-CNhlSIO3.js +0 -1
- package/dist/assets/mermaid-parser.core-Vb9KKv1R.js +0 -4
- package/dist/assets/mermaid.core-C_7xsp3d.js +0 -11
- package/dist/assets/mindmap-definition-YRQLILUH-BWmfy5wB.js +0 -68
- package/dist/assets/ordinal-DIg8h6NI.js +0 -1
- package/dist/assets/packet-RMMSAZCW-Q-WG6o3b.js +0 -1
- package/dist/assets/path-DfRbCp9y.js +0 -1
- package/dist/assets/pie-UPGHQEXC-Cwi2tLlt.js +0 -1
- package/dist/assets/pieDiagram-SKSYHLDU-Dyf3X_in.js +0 -30
- package/dist/assets/quadrantDiagram-337W2JSQ-B5_5m61Q.js +0 -7
- package/dist/assets/radar-KQ55EAFF-Dtw2VzxY.js +0 -1
- package/dist/assets/requirementDiagram-Z7DCOOCP-BSERBnlW.js +0 -73
- package/dist/assets/rough.esm-KjoEK0it.js +0 -1
- package/dist/assets/sankeyDiagram-WA2Y5GQK-CMcEY8Cz.js +0 -10
- package/dist/assets/sequenceDiagram-2WXFIKYE-D28qcXwC.js +0 -145
- package/dist/assets/src-C8kkzlHX.js +0 -1
- package/dist/assets/stateDiagram-RAJIS63D-7oVrCmRl.js +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DtFptQAd.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-rbCfBEvG.js +0 -61
- package/dist/assets/treemap-KZPCXAKY-BlRvF0um.js +0 -1
- package/dist/assets/vennDiagram-LZ73GAT5-DBit3zWa.js +0 -34
- package/dist/assets/xychartDiagram-JWTSCODW-BVYXv51y.js +0 -7
- package/dist/index.js +0 -1040
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { expect, test } from "@playwright/test";
|
|
11
|
+
import { spawnCli } from "./utils/cli";
|
|
12
|
+
import { addComment, selectTextInArticle } from "./utils/selection";
|
|
13
|
+
|
|
14
|
+
const FIXTURES_DIR = resolve(import.meta.dirname, "fixtures");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the expected comment file path for a source file.
|
|
18
|
+
*/
|
|
19
|
+
function getCommentPath(sourcePath: string): string {
|
|
20
|
+
const absolute = resolve(sourcePath);
|
|
21
|
+
const normalized = absolute.replace(/^\//, "").replace(/^[A-Z]:[\\/]/, "");
|
|
22
|
+
const ext = normalized.lastIndexOf(".");
|
|
23
|
+
const withoutExt = ext > 0 ? normalized.slice(0, ext) : normalized;
|
|
24
|
+
return join(os.homedir(), ".readit", "comments", `${withoutExt}.comments.md`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Clean up comment file for a source file.
|
|
29
|
+
*/
|
|
30
|
+
function cleanupCommentFile(sourcePath: string): void {
|
|
31
|
+
const commentPath = getCommentPath(sourcePath);
|
|
32
|
+
if (existsSync(commentPath)) {
|
|
33
|
+
rmSync(commentPath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test.describe("File-Based Comment Persistence", () => {
|
|
38
|
+
const sampleMdPath = resolve(FIXTURES_DIR, "sample.md");
|
|
39
|
+
|
|
40
|
+
test.beforeEach(() => {
|
|
41
|
+
// Clean up any existing comment file before each test
|
|
42
|
+
cleanupCommentFile(sampleMdPath);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test.afterEach(() => {
|
|
46
|
+
// Clean up after each test
|
|
47
|
+
cleanupCommentFile(sampleMdPath);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("comments are saved to markdown file", async ({ page }) => {
|
|
51
|
+
const { url, cleanup } = await spawnCli(sampleMdPath, {
|
|
52
|
+
port: 4590,
|
|
53
|
+
clean: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await page.goto(url);
|
|
58
|
+
|
|
59
|
+
// Wait for document to load
|
|
60
|
+
const article = page.locator("article");
|
|
61
|
+
await expect(article).toBeVisible();
|
|
62
|
+
|
|
63
|
+
// Add a comment
|
|
64
|
+
const textToSelect = "testing text selection";
|
|
65
|
+
await selectTextInArticle(page, textToSelect);
|
|
66
|
+
|
|
67
|
+
const commentText = "This is a test comment for file persistence";
|
|
68
|
+
await addComment(page, commentText);
|
|
69
|
+
|
|
70
|
+
// Wait for comment to be visible in UI
|
|
71
|
+
await expect(page.locator("body")).toContainText(commentText);
|
|
72
|
+
|
|
73
|
+
// Give time for the file to be written
|
|
74
|
+
await page.waitForTimeout(500);
|
|
75
|
+
|
|
76
|
+
// Verify the comment file was created
|
|
77
|
+
const commentPath = getCommentPath(sampleMdPath);
|
|
78
|
+
expect(existsSync(commentPath)).toBe(true);
|
|
79
|
+
|
|
80
|
+
// Verify the file contains the comment
|
|
81
|
+
const fileContent = readFileSync(commentPath, "utf-8");
|
|
82
|
+
expect(fileContent).toContain(textToSelect);
|
|
83
|
+
expect(fileContent).toContain(commentText);
|
|
84
|
+
expect(fileContent).toContain("version: 1");
|
|
85
|
+
} finally {
|
|
86
|
+
await cleanup();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("comments persist across page reload", async ({ page }) => {
|
|
91
|
+
const { url, cleanup } = await spawnCli(sampleMdPath, {
|
|
92
|
+
port: 4591,
|
|
93
|
+
clean: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await page.goto(url);
|
|
98
|
+
|
|
99
|
+
const article = page.locator("article");
|
|
100
|
+
await expect(article).toBeVisible();
|
|
101
|
+
|
|
102
|
+
// Add a comment
|
|
103
|
+
const textToSelect = "testing text selection";
|
|
104
|
+
await selectTextInArticle(page, textToSelect);
|
|
105
|
+
|
|
106
|
+
const commentText = "Persistent comment test";
|
|
107
|
+
await addComment(page, commentText);
|
|
108
|
+
|
|
109
|
+
// Wait for comment to appear
|
|
110
|
+
await expect(page.locator("body")).toContainText(commentText);
|
|
111
|
+
|
|
112
|
+
// Reload the page
|
|
113
|
+
await page.reload();
|
|
114
|
+
|
|
115
|
+
// Wait for document to reload
|
|
116
|
+
await expect(article).toBeVisible();
|
|
117
|
+
|
|
118
|
+
// Verify comment still exists after reload
|
|
119
|
+
await expect(page.locator("body")).toContainText(commentText);
|
|
120
|
+
|
|
121
|
+
// Verify highlight still exists
|
|
122
|
+
const highlight = article.locator("mark[data-comment-id]").first();
|
|
123
|
+
await expect(highlight).toBeVisible();
|
|
124
|
+
} finally {
|
|
125
|
+
await cleanup();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("comments persist across server restart", async ({ page }) => {
|
|
130
|
+
const PORT = 4592;
|
|
131
|
+
|
|
132
|
+
// First session: add a comment
|
|
133
|
+
const { url: url1, cleanup: cleanup1 } = await spawnCli(sampleMdPath, {
|
|
134
|
+
port: PORT,
|
|
135
|
+
clean: false,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const commentText = "Comment that survives restart";
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await page.goto(url1);
|
|
142
|
+
|
|
143
|
+
const article = page.locator("article");
|
|
144
|
+
await expect(article).toBeVisible();
|
|
145
|
+
|
|
146
|
+
await selectTextInArticle(page, "testing text selection");
|
|
147
|
+
await addComment(page, commentText);
|
|
148
|
+
|
|
149
|
+
// Wait for comment to appear
|
|
150
|
+
await expect(page.locator("body")).toContainText(commentText);
|
|
151
|
+
} finally {
|
|
152
|
+
await cleanup1();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Wait for server to fully shut down
|
|
156
|
+
await page.waitForTimeout(1000);
|
|
157
|
+
|
|
158
|
+
// Second session: verify comment persists
|
|
159
|
+
const { url: url2, cleanup: cleanup2 } = await spawnCli(sampleMdPath, {
|
|
160
|
+
port: PORT,
|
|
161
|
+
clean: false,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await page.goto(url2);
|
|
166
|
+
|
|
167
|
+
const article = page.locator("article");
|
|
168
|
+
await expect(article).toBeVisible();
|
|
169
|
+
|
|
170
|
+
// Comment should still exist from previous session
|
|
171
|
+
await expect(page.locator("body")).toContainText(commentText);
|
|
172
|
+
|
|
173
|
+
// Highlight should still exist
|
|
174
|
+
const highlight = article.locator("mark[data-comment-id]").first();
|
|
175
|
+
await expect(highlight).toBeVisible();
|
|
176
|
+
} finally {
|
|
177
|
+
await cleanup2();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("--clean flag deletes comment file", async ({ page }) => {
|
|
182
|
+
const PORT = 4593;
|
|
183
|
+
|
|
184
|
+
// First, create a comment file manually
|
|
185
|
+
const commentPath = getCommentPath(sampleMdPath);
|
|
186
|
+
const commentDir = dirname(commentPath);
|
|
187
|
+
mkdirSync(commentDir, { recursive: true });
|
|
188
|
+
writeFileSync(
|
|
189
|
+
commentPath,
|
|
190
|
+
`---
|
|
191
|
+
source: ${sampleMdPath}
|
|
192
|
+
hash: abc123
|
|
193
|
+
version: 1
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
<!-- c:12345678|L5|2024-12-24T10:00:00Z -->
|
|
197
|
+
> testing text selection
|
|
198
|
+
|
|
199
|
+
Pre-existing comment to be cleared.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
`,
|
|
203
|
+
"utf-8",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(existsSync(commentPath)).toBe(true);
|
|
207
|
+
|
|
208
|
+
// Start server with --clean flag
|
|
209
|
+
const { url, cleanup } = await spawnCli(sampleMdPath, {
|
|
210
|
+
port: PORT,
|
|
211
|
+
clean: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await page.goto(url);
|
|
216
|
+
|
|
217
|
+
const article = page.locator("article");
|
|
218
|
+
await expect(article).toBeVisible();
|
|
219
|
+
|
|
220
|
+
// Wait for clean operation to complete
|
|
221
|
+
await page.waitForTimeout(500);
|
|
222
|
+
|
|
223
|
+
// Verify no comments in UI
|
|
224
|
+
const highlight = article.locator("mark[data-comment-id]");
|
|
225
|
+
await expect(highlight).toHaveCount(0);
|
|
226
|
+
} finally {
|
|
227
|
+
await cleanup();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("edit updates the comment file", async ({ page }) => {
|
|
232
|
+
const { url, cleanup } = await spawnCli(sampleMdPath, {
|
|
233
|
+
port: 4594,
|
|
234
|
+
clean: false,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await page.goto(url);
|
|
239
|
+
|
|
240
|
+
const article = page.locator("article");
|
|
241
|
+
await expect(article).toBeVisible();
|
|
242
|
+
|
|
243
|
+
// Add initial comment
|
|
244
|
+
const textToSelect = "testing text selection";
|
|
245
|
+
await selectTextInArticle(page, textToSelect);
|
|
246
|
+
const initialComment = "Initial comment text";
|
|
247
|
+
await addComment(page, initialComment);
|
|
248
|
+
|
|
249
|
+
await expect(page.locator("body")).toContainText(initialComment);
|
|
250
|
+
|
|
251
|
+
// Wait for file to be written
|
|
252
|
+
await page.waitForTimeout(500);
|
|
253
|
+
|
|
254
|
+
// Verify initial comment in file
|
|
255
|
+
const commentPath = getCommentPath(sampleMdPath);
|
|
256
|
+
let fileContent = readFileSync(commentPath, "utf-8");
|
|
257
|
+
expect(fileContent).toContain(initialComment);
|
|
258
|
+
|
|
259
|
+
// Find the margin note containing the selected text (stable identifier)
|
|
260
|
+
const marginNote = page
|
|
261
|
+
.locator(".group")
|
|
262
|
+
.filter({ hasText: textToSelect })
|
|
263
|
+
.first();
|
|
264
|
+
await marginNote.hover();
|
|
265
|
+
|
|
266
|
+
// Find and click edit button
|
|
267
|
+
const editButton = marginNote.locator('button:has-text("Edit")');
|
|
268
|
+
await editButton.click();
|
|
269
|
+
|
|
270
|
+
// Wait for edit mode to activate
|
|
271
|
+
const textarea = marginNote.locator("textarea");
|
|
272
|
+
await expect(textarea).toBeVisible();
|
|
273
|
+
|
|
274
|
+
// Clear and type new text
|
|
275
|
+
await textarea.fill("Updated comment text");
|
|
276
|
+
|
|
277
|
+
// Save edit
|
|
278
|
+
const saveButton = marginNote.locator('button:has-text("Save")');
|
|
279
|
+
await saveButton.click();
|
|
280
|
+
|
|
281
|
+
// Wait for file to be updated
|
|
282
|
+
await page.waitForTimeout(500);
|
|
283
|
+
|
|
284
|
+
// Verify updated comment in file
|
|
285
|
+
fileContent = readFileSync(commentPath, "utf-8");
|
|
286
|
+
expect(fileContent).toContain("Updated comment text");
|
|
287
|
+
} finally {
|
|
288
|
+
await cleanup();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("delete removes comment from file", async ({ page }) => {
|
|
293
|
+
const { url, cleanup } = await spawnCli(sampleMdPath, {
|
|
294
|
+
port: 4595,
|
|
295
|
+
clean: false,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await page.goto(url);
|
|
300
|
+
|
|
301
|
+
const article = page.locator("article");
|
|
302
|
+
await expect(article).toBeVisible();
|
|
303
|
+
|
|
304
|
+
// Add a comment
|
|
305
|
+
await selectTextInArticle(page, "testing text selection");
|
|
306
|
+
const commentText = "Comment to be deleted";
|
|
307
|
+
await addComment(page, commentText);
|
|
308
|
+
|
|
309
|
+
await expect(page.locator("body")).toContainText(commentText);
|
|
310
|
+
|
|
311
|
+
// Wait for file to be written
|
|
312
|
+
await page.waitForTimeout(500);
|
|
313
|
+
|
|
314
|
+
// Verify comment in file
|
|
315
|
+
const commentPath = getCommentPath(sampleMdPath);
|
|
316
|
+
let fileContent = readFileSync(commentPath, "utf-8");
|
|
317
|
+
expect(fileContent).toContain(commentText);
|
|
318
|
+
|
|
319
|
+
// Find the margin note containing the comment
|
|
320
|
+
const marginNote = page
|
|
321
|
+
.locator(".group")
|
|
322
|
+
.filter({ hasText: commentText })
|
|
323
|
+
.first();
|
|
324
|
+
await marginNote.hover();
|
|
325
|
+
|
|
326
|
+
// Click delete button
|
|
327
|
+
const deleteButton = marginNote.locator('button:has-text("Delete")');
|
|
328
|
+
await deleteButton.click();
|
|
329
|
+
|
|
330
|
+
// Wait for file to be updated
|
|
331
|
+
await page.waitForTimeout(500);
|
|
332
|
+
|
|
333
|
+
// Verify comment removed from file (file may be deleted if no comments left)
|
|
334
|
+
if (existsSync(commentPath)) {
|
|
335
|
+
fileContent = readFileSync(commentPath, "utf-8");
|
|
336
|
+
expect(fileContent).not.toContain(commentText);
|
|
337
|
+
}
|
|
338
|
+
} finally {
|
|
339
|
+
await cleanup();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
package/e2e/utils/cli.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
interface SpawnCliResult {
|
|
6
|
+
url: string;
|
|
7
|
+
process: ChildProcess;
|
|
8
|
+
cleanup: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SpawnCliOptions {
|
|
12
|
+
port?: number;
|
|
13
|
+
clean?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CLI_PATH = resolve(import.meta.dirname, "../../dist/index.js");
|
|
17
|
+
|
|
18
|
+
export async function spawnCli(
|
|
19
|
+
fixturePath: string,
|
|
20
|
+
options: SpawnCliOptions = {},
|
|
21
|
+
): Promise<SpawnCliResult> {
|
|
22
|
+
const { port = 4567, clean = true } = options;
|
|
23
|
+
|
|
24
|
+
const args = [CLI_PATH, fixturePath, "--no-open", "--port", String(port)];
|
|
25
|
+
if (clean) {
|
|
26
|
+
args.push("--clean");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cliProcess = spawn("bun", args, {
|
|
30
|
+
cwd: resolve(import.meta.dirname, "../.."),
|
|
31
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const url = await waitForServerReady(cliProcess);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
url,
|
|
38
|
+
process: cliProcess,
|
|
39
|
+
cleanup: async () => {
|
|
40
|
+
cliProcess.kill("SIGTERM");
|
|
41
|
+
await new Promise<void>((resolve) => {
|
|
42
|
+
cliProcess.once("exit", () => resolve());
|
|
43
|
+
setTimeout(resolve, 1000); // Fallback timeout
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function waitForServerReady(cliProcess: ChildProcess): Promise<string> {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
let output = "";
|
|
52
|
+
|
|
53
|
+
const timeout = setTimeout(() => {
|
|
54
|
+
reject(new Error("Server did not start within timeout"));
|
|
55
|
+
}, 10_000);
|
|
56
|
+
|
|
57
|
+
cliProcess.stdout?.on("data", (data: Buffer) => {
|
|
58
|
+
output += data.toString();
|
|
59
|
+
|
|
60
|
+
// Look for "URL: http://..." in output
|
|
61
|
+
const urlMatch = output.match(/URL:\s+(http:\/\/[^\s]+)/);
|
|
62
|
+
if (urlMatch) {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
resolve(urlMatch[1]);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
cliProcess.stderr?.on("data", (data: Buffer) => {
|
|
69
|
+
console.error("[CLI stderr]", data.toString());
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
cliProcess.on("error", (err) => {
|
|
73
|
+
clearTimeout(timeout);
|
|
74
|
+
reject(err);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
cliProcess.on("exit", (code) => {
|
|
78
|
+
if (code !== 0 && code !== null) {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
reject(new Error(`CLI exited with code ${code}: ${output}`));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { FrameLocator, Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Select text within an article element (for markdown documents)
|
|
5
|
+
* Uses custom event to trigger selection handler (workaround for Playwright mouse issues)
|
|
6
|
+
*/
|
|
7
|
+
export async function selectTextInArticle(
|
|
8
|
+
page: Page,
|
|
9
|
+
textToSelect: string,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
// Find text and calculate offsets, then dispatch custom event
|
|
12
|
+
await page.evaluate((text) => {
|
|
13
|
+
const article = document.querySelector("article");
|
|
14
|
+
if (!article) throw new Error("Article element not found");
|
|
15
|
+
|
|
16
|
+
const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
|
|
17
|
+
let currentOffset = 0;
|
|
18
|
+
|
|
19
|
+
while (walker.nextNode()) {
|
|
20
|
+
const textNode = walker.currentNode as Text;
|
|
21
|
+
const content = textNode.textContent || "";
|
|
22
|
+
const index = content.indexOf(text);
|
|
23
|
+
|
|
24
|
+
if (index !== -1) {
|
|
25
|
+
const startOffset = currentOffset + index;
|
|
26
|
+
const endOffset = startOffset + text.length;
|
|
27
|
+
|
|
28
|
+
// Dispatch custom event that DocumentViewer listens for
|
|
29
|
+
const event = new CustomEvent("test:select-text", {
|
|
30
|
+
detail: { text, startOffset, endOffset },
|
|
31
|
+
});
|
|
32
|
+
window.dispatchEvent(event);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
currentOffset += content.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(`Text "${text}" not found in article`);
|
|
40
|
+
}, textToSelect);
|
|
41
|
+
|
|
42
|
+
// Wait for React to process the selection
|
|
43
|
+
await page.waitForTimeout(100);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Select text within an iframe (for HTML documents)
|
|
48
|
+
* Uses custom event to trigger selection handler (same as markdown)
|
|
49
|
+
*/
|
|
50
|
+
export async function selectTextInIframe(
|
|
51
|
+
page: Page,
|
|
52
|
+
_iframe: FrameLocator,
|
|
53
|
+
textToSelect: string,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
// Get the actual frame object for evaluation
|
|
56
|
+
const frame = page.frame({ url: /^about:srcdoc/ }) || page.frames()[1];
|
|
57
|
+
if (!frame) throw new Error("Could not find iframe frame");
|
|
58
|
+
|
|
59
|
+
// Calculate offsets in iframe, then dispatch custom event to parent
|
|
60
|
+
const offsets = await frame.evaluate((text) => {
|
|
61
|
+
const body = document.body;
|
|
62
|
+
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
63
|
+
let currentOffset = 0;
|
|
64
|
+
|
|
65
|
+
while (walker.nextNode()) {
|
|
66
|
+
const textNode = walker.currentNode as Text;
|
|
67
|
+
const content = textNode.textContent || "";
|
|
68
|
+
const index = content.indexOf(text);
|
|
69
|
+
|
|
70
|
+
if (index !== -1) {
|
|
71
|
+
const startOffset = currentOffset + index;
|
|
72
|
+
const endOffset = startOffset + text.length;
|
|
73
|
+
return { text, startOffset, endOffset };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
currentOffset += content.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(`Text "${text}" not found in iframe`);
|
|
80
|
+
}, textToSelect);
|
|
81
|
+
|
|
82
|
+
// Dispatch custom event to parent window (IframeContainer listens for this)
|
|
83
|
+
await page.evaluate((detail) => {
|
|
84
|
+
const event = new CustomEvent("test:select-text", { detail });
|
|
85
|
+
window.dispatchEvent(event);
|
|
86
|
+
}, offsets);
|
|
87
|
+
|
|
88
|
+
// Wait for React to process state update
|
|
89
|
+
await page.waitForTimeout(200);
|
|
90
|
+
|
|
91
|
+
// Manually send applyHighlights to iframe (workaround for isReadyRef timing)
|
|
92
|
+
// This ensures the iframe gets the highlight even if React's useEffect hasn't fired
|
|
93
|
+
await page.evaluate(
|
|
94
|
+
(selection) => {
|
|
95
|
+
const iframe = document.querySelector("iframe");
|
|
96
|
+
if (iframe?.contentWindow) {
|
|
97
|
+
iframe.contentWindow.postMessage(
|
|
98
|
+
{
|
|
99
|
+
type: "applyHighlights",
|
|
100
|
+
comments: [],
|
|
101
|
+
pendingSelection: selection,
|
|
102
|
+
},
|
|
103
|
+
"*",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{ startOffset: offsets.startOffset, endOffset: offsets.endOffset },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Wait for iframe to apply highlights
|
|
111
|
+
await page.waitForTimeout(200);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add a comment to the current selection
|
|
116
|
+
* Assumes CommentInputArea is visible
|
|
117
|
+
*/
|
|
118
|
+
export async function addComment(
|
|
119
|
+
page: Page,
|
|
120
|
+
commentText: string,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
// Wait for the comment input textarea to appear
|
|
123
|
+
// Give more time for iframe postMessage round-trip
|
|
124
|
+
const textarea = page.locator('textarea[placeholder="Add your comment..."]');
|
|
125
|
+
await textarea.waitFor({ state: "visible", timeout: 10000 });
|
|
126
|
+
|
|
127
|
+
// Fill in the comment
|
|
128
|
+
await textarea.fill(commentText);
|
|
129
|
+
|
|
130
|
+
// Click the Add button
|
|
131
|
+
await page.getByRole("button", { name: "Add" }).click();
|
|
132
|
+
|
|
133
|
+
// Wait for the textarea to disappear (comment submitted)
|
|
134
|
+
await textarea.waitFor({ state: "hidden", timeout: 5000 });
|
|
135
|
+
}
|
|
@@ -7,10 +7,16 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📖</text></svg>">
|
|
10
|
-
<script
|
|
11
|
-
|
|
10
|
+
<script>
|
|
11
|
+
(() => {
|
|
12
|
+
var t = localStorage.getItem("readit:theme");
|
|
13
|
+
var d = t === "dark" || (t !== "light" && matchMedia("(prefers-color-scheme: dark)").matches);
|
|
14
|
+
if (d) document.documentElement.classList.add("dark");
|
|
15
|
+
})();
|
|
16
|
+
</script>
|
|
12
17
|
</head>
|
|
13
18
|
<body class="min-h-screen">
|
|
14
19
|
<div id="root"></div>
|
|
20
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
15
21
|
</body>
|
|
16
22
|
</html>
|
package/lefthook.yml
ADDED
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaske7/readit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A CLI tool to review Markdown documents with inline comments",
|
|
5
|
-
"
|
|
5
|
+
"author": "Jay Shimada <peaske@pm.me>",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"keywords": [
|
|
7
8
|
"markdown",
|
|
8
9
|
"review",
|
|
@@ -10,32 +11,15 @@
|
|
|
10
11
|
"comments",
|
|
11
12
|
"documentation"
|
|
12
13
|
],
|
|
13
|
-
"
|
|
14
|
-
"license": "MIT",
|
|
15
|
-
"bin": {
|
|
16
|
-
"readit": "dist/index.js"
|
|
17
|
-
},
|
|
14
|
+
"type": "module",
|
|
18
15
|
"main": "./dist/index.js",
|
|
19
|
-
"directories": {
|
|
20
|
-
"doc": "docs"
|
|
21
|
-
},
|
|
22
|
-
"repository": {
|
|
23
|
-
"type": "git",
|
|
24
|
-
"url": "git+https://github.com/peaske7/readit.git"
|
|
25
|
-
},
|
|
26
|
-
"bugs": {
|
|
27
|
-
"url": "https://github.com/peaske7/readit/issues"
|
|
28
|
-
},
|
|
29
|
-
"homepage": "https://github.com/peaske7/readit#readme",
|
|
30
|
-
"files": [
|
|
31
|
-
"dist"
|
|
32
|
-
],
|
|
33
16
|
"scripts": {
|
|
34
|
-
"dev": "
|
|
35
|
-
"build": "vite build &&
|
|
36
|
-
"build:cli": "
|
|
37
|
-
"start": "
|
|
17
|
+
"dev": "NODE_ENV=development bun --watch src/cli/index.ts -- test.md & bunx vite",
|
|
18
|
+
"build": "bunx vite build && bun run build:cli",
|
|
19
|
+
"build:cli": "bun build src/cli/index.ts --outdir dist --target bun --format esm --packages external",
|
|
20
|
+
"start": "bun run build && bun dist/index.js",
|
|
38
21
|
"test": "vitest run",
|
|
22
|
+
"bench": "vitest bench",
|
|
39
23
|
"test:watch": "vitest",
|
|
40
24
|
"test:e2e": "playwright test",
|
|
41
25
|
"test:e2e:ui": "playwright test --ui",
|
|
@@ -44,15 +28,17 @@
|
|
|
44
28
|
"format": "biome format --write .",
|
|
45
29
|
"lint": "biome lint .",
|
|
46
30
|
"typecheck": "tsc --noEmit",
|
|
47
|
-
"prepack": "
|
|
31
|
+
"prepack": "bun run build",
|
|
48
32
|
"prepare": "lefthook install || true"
|
|
49
33
|
},
|
|
50
34
|
"dependencies": {
|
|
35
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
36
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
37
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
51
38
|
"class-variance-authority": "^0.7.1",
|
|
52
39
|
"clsx": "^2.1.1",
|
|
53
40
|
"commander": "^14.0.3",
|
|
54
41
|
"dompurify": "^3.3.3",
|
|
55
|
-
"express": "^5.2.1",
|
|
56
42
|
"lucide-react": "^0.577.0",
|
|
57
43
|
"mermaid": "^11.13.0",
|
|
58
44
|
"open": "^11.0.0",
|
|
@@ -66,7 +52,8 @@
|
|
|
66
52
|
"tailwind-merge": "^3.5.0",
|
|
67
53
|
"unified": "^11.0.5",
|
|
68
54
|
"unist-util-visit": "^5.1.0",
|
|
69
|
-
"zod": "^4.3.6"
|
|
55
|
+
"zod": "^4.3.6",
|
|
56
|
+
"zustand": "^5.0.11"
|
|
70
57
|
},
|
|
71
58
|
"devDependencies": {
|
|
72
59
|
"@biomejs/biome": "^2.4.6",
|
|
@@ -74,29 +61,20 @@
|
|
|
74
61
|
"@tailwindcss/vite": "^4.2.1",
|
|
75
62
|
"@testing-library/jest-dom": "^6.9.1",
|
|
76
63
|
"@testing-library/react": "^16.3.2",
|
|
64
|
+
"@types/bun": "^1.3.10",
|
|
77
65
|
"@types/dompurify": "^3.2.0",
|
|
78
|
-
"@types/express": "^5.0.6",
|
|
79
66
|
"@types/jsdom": "^28.0.0",
|
|
80
|
-
"@types/node": "^25.4.0",
|
|
81
67
|
"@types/react": "^19.2.14",
|
|
82
68
|
"@types/react-dom": "^19.2.3",
|
|
83
69
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
84
70
|
"@vitejs/plugin-react": "^6.0.0",
|
|
85
|
-
"concurrently": "^9.2.1",
|
|
86
71
|
"jsdom": "^28.1.0",
|
|
87
72
|
"lefthook": "^2.1.4",
|
|
88
73
|
"react": "^19.2.4",
|
|
89
74
|
"react-dom": "^19.2.4",
|
|
90
75
|
"tailwindcss": "^4.2.1",
|
|
91
|
-
"tsup": "^8.5.1",
|
|
92
|
-
"tsx": "^4.21.0",
|
|
93
76
|
"typescript": "^5.9.3",
|
|
94
77
|
"vite": "^8.0.0",
|
|
95
|
-
"vitest": "^4.1.0"
|
|
96
|
-
"wait-on": "^9.0.4"
|
|
97
|
-
},
|
|
98
|
-
"packageManager": "pnpm@10.26.0",
|
|
99
|
-
"engines": {
|
|
100
|
-
"node": ">=22.0.0"
|
|
78
|
+
"vitest": "^4.1.0"
|
|
101
79
|
}
|
|
102
80
|
}
|