@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.
Files changed (320) hide show
  1. package/.agents/skills/remotion-best-practices/SKILL.md +61 -0
  2. package/.agents/skills/remotion-best-practices/rules/3d.md +86 -0
  3. package/.agents/skills/remotion-best-practices/rules/animations.md +27 -0
  4. package/.agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +178 -0
  5. package/.agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  6. package/.agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  7. package/.agents/skills/remotion-best-practices/rules/assets.md +78 -0
  8. package/.agents/skills/remotion-best-practices/rules/audio-visualization.md +198 -0
  9. package/.agents/skills/remotion-best-practices/rules/audio.md +169 -0
  10. package/.agents/skills/remotion-best-practices/rules/calculate-metadata.md +134 -0
  11. package/.agents/skills/remotion-best-practices/rules/can-decode.md +75 -0
  12. package/.agents/skills/remotion-best-practices/rules/charts.md +120 -0
  13. package/.agents/skills/remotion-best-practices/rules/compositions.md +154 -0
  14. package/.agents/skills/remotion-best-practices/rules/display-captions.md +184 -0
  15. package/.agents/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  16. package/.agents/skills/remotion-best-practices/rules/ffmpeg.md +38 -0
  17. package/.agents/skills/remotion-best-practices/rules/fonts.md +152 -0
  18. package/.agents/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  19. package/.agents/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  20. package/.agents/skills/remotion-best-practices/rules/get-video-duration.md +60 -0
  21. package/.agents/skills/remotion-best-practices/rules/gifs.md +141 -0
  22. package/.agents/skills/remotion-best-practices/rules/images.md +134 -0
  23. package/.agents/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
  24. package/.agents/skills/remotion-best-practices/rules/light-leaks.md +73 -0
  25. package/.agents/skills/remotion-best-practices/rules/lottie.md +70 -0
  26. package/.agents/skills/remotion-best-practices/rules/maps.md +412 -0
  27. package/.agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
  28. package/.agents/skills/remotion-best-practices/rules/measuring-text.md +140 -0
  29. package/.agents/skills/remotion-best-practices/rules/parameters.md +109 -0
  30. package/.agents/skills/remotion-best-practices/rules/sequencing.md +118 -0
  31. package/.agents/skills/remotion-best-practices/rules/sfx.md +26 -0
  32. package/.agents/skills/remotion-best-practices/rules/subtitles.md +36 -0
  33. package/.agents/skills/remotion-best-practices/rules/tailwind.md +11 -0
  34. package/.agents/skills/remotion-best-practices/rules/text-animations.md +20 -0
  35. package/.agents/skills/remotion-best-practices/rules/timing.md +179 -0
  36. package/.agents/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
  37. package/.agents/skills/remotion-best-practices/rules/transitions.md +197 -0
  38. package/.agents/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
  39. package/.agents/skills/remotion-best-practices/rules/trimming.md +51 -0
  40. package/.agents/skills/remotion-best-practices/rules/videos.md +171 -0
  41. package/.agents/skills/remotion-best-practices/rules/voiceover.md +99 -0
  42. package/.agents/skills/simple/SKILL.md +52 -0
  43. package/.agents/skills/vercel-react-best-practices/AGENTS.md +3254 -0
  44. package/.agents/skills/vercel-react-best-practices/README.md +123 -0
  45. package/.agents/skills/vercel-react-best-practices/SKILL.md +141 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  61. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  62. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  63. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  64. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  65. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  66. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  67. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  68. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  69. package/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
  70. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  71. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  72. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  73. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  74. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  75. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  76. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  77. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  78. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  79. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  80. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  81. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  82. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  83. package/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
  84. package/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
  85. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  86. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  87. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  88. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  89. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  90. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  91. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  92. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  93. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  94. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  95. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  96. package/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
  97. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  98. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  99. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  100. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  101. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  102. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  103. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  104. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  105. package/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
  106. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  107. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  108. package/.claude/CLAUDE.md +142 -0
  109. package/.claude/commands/review.md +120 -0
  110. package/.claude/commands/sync-docs.md +71 -0
  111. package/.claude/roadmap.md +98 -0
  112. package/.claude/rules/style-guide.md +830 -0
  113. package/.claude/settings.json +18 -0
  114. package/.claude/user-stories.md +248 -0
  115. package/AGENTS.md +64 -0
  116. package/README.md +7 -7
  117. package/biome.json +69 -0
  118. package/bun.lock +1124 -0
  119. package/docs/design.md +563 -0
  120. package/docs/plans/2026-03-13-keyboard-shortcuts-design.md +129 -0
  121. package/docs/plans/2026-03-13-keyboard-shortcuts-plan.md +1471 -0
  122. package/docs/plans/2026-03-13-multi-document-design.md +183 -0
  123. package/docs/plans/2026-03-13-performance-benchmarks-design.md +121 -0
  124. package/e2e/comments.spec.ts +125 -0
  125. package/e2e/document-load.spec.ts +54 -0
  126. package/e2e/export.spec.ts +58 -0
  127. package/e2e/fixtures/sample.html +13 -0
  128. package/e2e/fixtures/sample.md +7 -0
  129. package/e2e/persistence-file.spec.ts +342 -0
  130. package/e2e/utils/cli.ts +84 -0
  131. package/e2e/utils/selection.ts +135 -0
  132. package/{dist/index.html → index.html} +8 -2
  133. package/lefthook.yml +8 -0
  134. package/package.json +17 -39
  135. package/playwright.config.ts +22 -0
  136. package/skills-lock.json +20 -0
  137. package/src/App.tsx +396 -0
  138. package/src/cli/index.ts +328 -0
  139. package/src/components/ActionsMenu.tsx +110 -0
  140. package/src/components/DocumentViewer/CodeBlock.tsx +83 -0
  141. package/src/components/DocumentViewer/DocumentViewer.tsx +257 -0
  142. package/src/components/DocumentViewer/IframeContainer.tsx +251 -0
  143. package/src/components/DocumentViewer/MermaidDiagram.tsx +137 -0
  144. package/src/components/DocumentViewer/index.ts +1 -0
  145. package/src/components/FloatingTOC.tsx +59 -0
  146. package/src/components/Header.tsx +63 -0
  147. package/src/components/InlineEditor.tsx +72 -0
  148. package/src/components/MarginNote.tsx +198 -0
  149. package/src/components/MarginNotes.tsx +50 -0
  150. package/src/components/RawModal.tsx +141 -0
  151. package/src/components/ReanchorConfirm.tsx +33 -0
  152. package/src/components/SettingsModal.tsx +221 -0
  153. package/src/components/ShortcutCapture.tsx +45 -0
  154. package/src/components/ShortcutList.tsx +157 -0
  155. package/src/components/TabBar.tsx +60 -0
  156. package/src/components/TableOfContents.tsx +108 -0
  157. package/src/components/comments/CommentBadge.tsx +43 -0
  158. package/src/components/comments/CommentInput.tsx +119 -0
  159. package/src/components/comments/CommentListItem.tsx +82 -0
  160. package/src/components/comments/CommentManager.tsx +106 -0
  161. package/src/components/comments/CommentMinimap.tsx +62 -0
  162. package/src/components/comments/CommentNav.tsx +104 -0
  163. package/src/components/ui/ActionBar.tsx +16 -0
  164. package/src/components/ui/ActionLink.tsx +32 -0
  165. package/src/components/ui/Button.tsx +55 -0
  166. package/src/components/ui/Dialog.tsx +156 -0
  167. package/src/components/ui/DropdownMenu.tsx +114 -0
  168. package/src/components/ui/SeparatorDot.tsx +9 -0
  169. package/src/components/ui/Text.tsx +54 -0
  170. package/src/contexts/CommentContext.tsx +222 -0
  171. package/src/contexts/LayoutContext.tsx +76 -0
  172. package/src/hooks/useClickOutside.ts +35 -0
  173. package/src/hooks/useClipboard.ts +79 -0
  174. package/src/hooks/useCommentNavigation.ts +130 -0
  175. package/src/hooks/useComments.ts +323 -0
  176. package/src/hooks/useDocument.ts +131 -0
  177. package/src/hooks/useFontPreference.ts +76 -0
  178. package/src/hooks/useHeadings.test.ts +159 -0
  179. package/src/hooks/useHeadings.ts +129 -0
  180. package/src/hooks/useKeybindings.ts +120 -0
  181. package/src/hooks/useKeyboardShortcuts.ts +63 -0
  182. package/src/hooks/useLayoutMode.ts +44 -0
  183. package/src/hooks/useReanchorMode.ts +33 -0
  184. package/src/hooks/useScrollMetrics.ts +56 -0
  185. package/src/hooks/useScrollSpy.ts +81 -0
  186. package/src/hooks/useTextSelection.ts +123 -0
  187. package/src/hooks/useThemePreference.ts +66 -0
  188. package/src/index.css +823 -0
  189. package/src/lib/__fixtures__/bench-data.ts +167 -0
  190. package/src/lib/anchor.bench.ts +112 -0
  191. package/src/lib/anchor.test.ts +531 -0
  192. package/src/lib/anchor.ts +465 -0
  193. package/src/lib/comment-storage.bench.ts +63 -0
  194. package/src/lib/comment-storage.test.ts +624 -0
  195. package/src/lib/comment-storage.ts +263 -0
  196. package/src/lib/context.bench.ts +41 -0
  197. package/src/lib/context.test.ts +224 -0
  198. package/src/lib/context.ts +193 -0
  199. package/src/lib/export.bench.ts +35 -0
  200. package/src/lib/export.ts +43 -0
  201. package/src/lib/highlight/colors.ts +37 -0
  202. package/src/lib/highlight/core.test.ts +98 -0
  203. package/src/lib/highlight/core.ts +54 -0
  204. package/src/lib/highlight/dom.ts +342 -0
  205. package/src/lib/highlight/highlighter.ts +427 -0
  206. package/src/lib/highlight/index.ts +23 -0
  207. package/src/lib/highlight/script-builder.ts +485 -0
  208. package/src/lib/highlight/types.ts +57 -0
  209. package/src/lib/html-processor.test.tsx +170 -0
  210. package/src/lib/html-processor.tsx +95 -0
  211. package/src/lib/layout-constants.ts +12 -0
  212. package/src/lib/margin-layout.bench.ts +28 -0
  213. package/src/lib/margin-layout.ts +100 -0
  214. package/src/lib/scroll.test.ts +118 -0
  215. package/src/lib/scroll.ts +47 -0
  216. package/src/lib/shortcut-registry.test.ts +173 -0
  217. package/src/lib/shortcut-registry.ts +209 -0
  218. package/src/lib/utils.test.ts +110 -0
  219. package/src/lib/utils.ts +50 -0
  220. package/src/main.tsx +10 -0
  221. package/src/server/index.ts +766 -0
  222. package/src/store/index.test.ts +220 -0
  223. package/src/store/index.ts +234 -0
  224. package/src/test-setup.ts +1 -0
  225. package/src/types/index.ts +115 -0
  226. package/test.md +74 -0
  227. package/tsconfig.cli.json +12 -0
  228. package/tsconfig.json +20 -0
  229. package/vite.config.ts +19 -0
  230. package/vitest.config.ts +15 -0
  231. package/dist/assets/_basePickBy-hOr-yGsE.js +0 -1
  232. package/dist/assets/_baseUniq-b7bzdUSn.js +0 -1
  233. package/dist/assets/arc-D65wG9gm.js +0 -1
  234. package/dist/assets/architecture-PBZL5I3N-DBa6CAv_.js +0 -1
  235. package/dist/assets/architectureDiagram-2XIMDMQ5-Djwpsh98.js +0 -36
  236. package/dist/assets/array-DOVTz2Mq.js +0 -1
  237. package/dist/assets/blockDiagram-WCTKOSBZ-BdW5TTxj.js +0 -132
  238. package/dist/assets/c4Diagram-IC4MRINW-DTmkHEXu.js +0 -10
  239. package/dist/assets/channel-B3MUFipN.js +0 -1
  240. package/dist/assets/chunk-4BX2VUAB-DEqzsvDc.js +0 -1
  241. package/dist/assets/chunk-55IACEB6-BzVuSUV8.js +0 -1
  242. package/dist/assets/chunk-7E7YKBS2-CZ8IcA4c.js +0 -1
  243. package/dist/assets/chunk-7R4GIKGN-CWVVC8HX.js +0 -79
  244. package/dist/assets/chunk-C72U2L5F-B1Tso5TH.js +0 -1
  245. package/dist/assets/chunk-EGIJ26TM-Cx_7CFik.js +0 -1
  246. package/dist/assets/chunk-FMBD7UC4-Cfk_iGhv.js +0 -15
  247. package/dist/assets/chunk-GEFDOKGD-C_5hRbJt.js +0 -2
  248. package/dist/assets/chunk-GLR3WWYH-CkY7IyBj.js +0 -2
  249. package/dist/assets/chunk-HHEYEP7N-B0I4X5cr.js +0 -1
  250. package/dist/assets/chunk-JSJVCQXG-CAjwlVLg.js +0 -1
  251. package/dist/assets/chunk-KX2RTZJC-DWqnZZ02.js +0 -1
  252. package/dist/assets/chunk-KYZI473N-gjRVhJgJ.js +0 -53
  253. package/dist/assets/chunk-L3YUKLVL-D7C9GuxL.js +0 -1
  254. package/dist/assets/chunk-MX3YWQON-i-77iuVj.js +0 -1
  255. package/dist/assets/chunk-NQ4KR5QH-B22Pvemm.js +0 -220
  256. package/dist/assets/chunk-O4XLMI2P-ZQd5L6ZD.js +0 -7
  257. package/dist/assets/chunk-OZEHJAEY-BaPKTELw.js +0 -1
  258. package/dist/assets/chunk-PQ6SQG4A-DqE1eupT.js +0 -1
  259. package/dist/assets/chunk-PU5JKC2W-BTqWqedh.js +0 -70
  260. package/dist/assets/chunk-QZHKN3VN-Nm9TvMss.js +0 -1
  261. package/dist/assets/chunk-R5LLSJPH-DkiNs1dN.js +0 -1
  262. package/dist/assets/chunk-WL4C6EOR-CioD2fv2.js +0 -189
  263. package/dist/assets/chunk-XIRO2GV7-B4GGQONY.js +0 -1
  264. package/dist/assets/chunk-XPW4576I-C0IbbQos.js +0 -32
  265. package/dist/assets/chunk-XZSTWKYB-DMOqFWmT.js +0 -94
  266. package/dist/assets/chunk-YBOYWFTD-CoeQgeVY.js +0 -1
  267. package/dist/assets/classDiagram-VBA2DB6C-DV9ltQ7h.js +0 -1
  268. package/dist/assets/classDiagram-v2-RAHNMMFH-C6nD9wmM.js +0 -1
  269. package/dist/assets/clone-DuY6BQEm.js +0 -1
  270. package/dist/assets/cose-bilkent-S5V4N54A-B6FexK6p.js +0 -1
  271. package/dist/assets/cytoscape.esm-DoTFyJaN.js +0 -321
  272. package/dist/assets/dagre-CCcocoCU.js +0 -1
  273. package/dist/assets/dagre-KLK3FWXG-DIELowj9.js +0 -4
  274. package/dist/assets/defaultLocale-Ck2Xxk-C.js +0 -1
  275. package/dist/assets/diagram-E7M64L7V-D1mm0PoO.js +0 -24
  276. package/dist/assets/diagram-IFDJBPK2-7DVjly8y.js +0 -43
  277. package/dist/assets/diagram-P4PSJMXO-jO7pfyMb.js +0 -24
  278. package/dist/assets/dist-BywRdrPx.js +0 -1
  279. package/dist/assets/erDiagram-INFDFZHY-DSRxlRFy.js +0 -70
  280. package/dist/assets/flowDiagram-PKNHOUZH-CgKzzNdR.js +0 -162
  281. package/dist/assets/ganttDiagram-A5KZAMGK-CtsE7Y4E.js +0 -292
  282. package/dist/assets/gitGraph-HDMCJU4V-BU9uhwtz.js +0 -1
  283. package/dist/assets/gitGraphDiagram-K3NZZRJ6-DOU8RGdw.js +0 -65
  284. package/dist/assets/graphlib-WkJoBgka.js +0 -1
  285. package/dist/assets/index-CKVArt9D.js +0 -562
  286. package/dist/assets/index-DzRKJazf.css +0 -2
  287. package/dist/assets/info-3K5VOQVL-CPpvM-SG.js +0 -1
  288. package/dist/assets/infoDiagram-LFFYTUFH-VKLs5DsF.js +0 -2
  289. package/dist/assets/init-Bft5Ffpj.js +0 -1
  290. package/dist/assets/isArrayLikeObject-icl0H0jo.js +0 -1
  291. package/dist/assets/isEmpty-Du8sNmkE.js +0 -1
  292. package/dist/assets/ishikawaDiagram-PHBUUO56-CsWvEjux.js +0 -70
  293. package/dist/assets/journeyDiagram-4ABVD52K-BzJGTdIT.js +0 -139
  294. package/dist/assets/kanban-definition-K7BYSVSG-B_9ClJ1A.js +0 -89
  295. package/dist/assets/katex-BJrMXEjr.js +0 -261
  296. package/dist/assets/line-CC_tDGId.js +0 -1
  297. package/dist/assets/linear-Cts_d04Y.js +0 -1
  298. package/dist/assets/math-CNhlSIO3.js +0 -1
  299. package/dist/assets/mermaid-parser.core-Vb9KKv1R.js +0 -4
  300. package/dist/assets/mermaid.core-C_7xsp3d.js +0 -11
  301. package/dist/assets/mindmap-definition-YRQLILUH-BWmfy5wB.js +0 -68
  302. package/dist/assets/ordinal-DIg8h6NI.js +0 -1
  303. package/dist/assets/packet-RMMSAZCW-Q-WG6o3b.js +0 -1
  304. package/dist/assets/path-DfRbCp9y.js +0 -1
  305. package/dist/assets/pie-UPGHQEXC-Cwi2tLlt.js +0 -1
  306. package/dist/assets/pieDiagram-SKSYHLDU-Dyf3X_in.js +0 -30
  307. package/dist/assets/quadrantDiagram-337W2JSQ-B5_5m61Q.js +0 -7
  308. package/dist/assets/radar-KQ55EAFF-Dtw2VzxY.js +0 -1
  309. package/dist/assets/requirementDiagram-Z7DCOOCP-BSERBnlW.js +0 -73
  310. package/dist/assets/rough.esm-KjoEK0it.js +0 -1
  311. package/dist/assets/sankeyDiagram-WA2Y5GQK-CMcEY8Cz.js +0 -10
  312. package/dist/assets/sequenceDiagram-2WXFIKYE-D28qcXwC.js +0 -145
  313. package/dist/assets/src-C8kkzlHX.js +0 -1
  314. package/dist/assets/stateDiagram-RAJIS63D-7oVrCmRl.js +0 -1
  315. package/dist/assets/stateDiagram-v2-FVOUBMTO-DtFptQAd.js +0 -1
  316. package/dist/assets/timeline-definition-YZTLITO2-rbCfBEvG.js +0 -61
  317. package/dist/assets/treemap-KZPCXAKY-BlRvF0um.js +0 -1
  318. package/dist/assets/vennDiagram-LZ73GAT5-DBit3zWa.js +0 -34
  319. package/dist/assets/xychartDiagram-JWTSCODW-BVYXv51y.js +0 -7
  320. package/dist/index.js +0 -1040
@@ -0,0 +1,76 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { toast } from "sonner";
3
+ import { FontFamilies, type FontFamily } from "../types";
4
+
5
+ interface UseFontPreferenceResult {
6
+ fontFamily: FontFamily;
7
+ setFontFamily: (font: FontFamily) => Promise<void>;
8
+ isLoading: boolean;
9
+ }
10
+
11
+ export function useFontPreference(
12
+ filePath: string | null,
13
+ ): UseFontPreferenceResult {
14
+ const [fontFamily, setFontFamilyState] = useState<FontFamily>(
15
+ FontFamilies.SERIF,
16
+ );
17
+ const [isLoading, setIsLoading] = useState(true);
18
+
19
+ const filePathRef = useRef(filePath);
20
+ filePathRef.current = filePath;
21
+
22
+ // Fetch settings when filePath changes
23
+ useEffect(() => {
24
+ if (!filePath) {
25
+ setIsLoading(false);
26
+ return;
27
+ }
28
+
29
+ setIsLoading(true);
30
+
31
+ const fetchSettings = async () => {
32
+ try {
33
+ const query = `?path=${encodeURIComponent(filePath)}`;
34
+ const response = await fetch(`/api/settings${query}`);
35
+ if (response.ok) {
36
+ const settings = await response.json();
37
+ setFontFamilyState(settings.fontFamily || FontFamilies.SERIF);
38
+ }
39
+ } catch (err) {
40
+ console.error("Failed to fetch settings:", err);
41
+ } finally {
42
+ setIsLoading(false);
43
+ }
44
+ };
45
+
46
+ fetchSettings();
47
+ }, [filePath]);
48
+
49
+ const setFontFamily = useCallback(async (font: FontFamily) => {
50
+ // Optimistic update
51
+ setFontFamilyState(font);
52
+
53
+ try {
54
+ const fp = filePathRef.current;
55
+ const query = fp ? `?path=${encodeURIComponent(fp)}` : "";
56
+ const response = await fetch(`/api/settings${query}`, {
57
+ method: "PUT",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify({ fontFamily: font }),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ throw new Error("Failed to save settings");
64
+ }
65
+ } catch (err) {
66
+ console.error("Failed to save font preference:", err);
67
+ toast.error("Failed to save font preference");
68
+ }
69
+ }, []);
70
+
71
+ return {
72
+ fontFamily,
73
+ setFontFamily,
74
+ isLoading,
75
+ };
76
+ }
@@ -0,0 +1,159 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { useHeadings } from "./useHeadings";
4
+
5
+ describe("useHeadings - markdown", () => {
6
+ it("extracts basic headings", () => {
7
+ const content = `# Heading 1
8
+ ## Heading 2
9
+ ### Heading 3`;
10
+
11
+ const { result } = renderHook(() => useHeadings(content, "markdown"));
12
+
13
+ expect(result.current).toEqual([
14
+ { id: "heading-1", text: "Heading 1", level: 1 },
15
+ { id: "heading-2", text: "Heading 2", level: 2 },
16
+ { id: "heading-3", text: "Heading 3", level: 3 },
17
+ ]);
18
+ });
19
+
20
+ it("handles duplicate headings", () => {
21
+ const content = `## Section
22
+ ## Section
23
+ ## Section`;
24
+
25
+ const { result } = renderHook(() => useHeadings(content, "markdown"));
26
+
27
+ expect(result.current).toEqual([
28
+ { id: "section", text: "Section", level: 2 },
29
+ { id: "section-1", text: "Section", level: 2 },
30
+ { id: "section-2", text: "Section", level: 2 },
31
+ ]);
32
+ });
33
+
34
+ it("ignores headings inside fenced code blocks", () => {
35
+ const content = `# Real Heading
36
+
37
+ \`\`\`bash
38
+ # This is a comment, not a heading
39
+ echo "hello"
40
+ \`\`\`
41
+
42
+ ## Another Real Heading`;
43
+
44
+ const { result } = renderHook(() => useHeadings(content, "markdown"));
45
+
46
+ expect(result.current).toEqual([
47
+ { id: "real-heading", text: "Real Heading", level: 1 },
48
+ { id: "another-real-heading", text: "Another Real Heading", level: 2 },
49
+ ]);
50
+ });
51
+
52
+ it("ignores headings inside triple-tilde code blocks", () => {
53
+ const content = `# Real Heading
54
+
55
+ ~~~python
56
+ # Python comment
57
+ def foo():
58
+ pass
59
+ ~~~
60
+
61
+ ## Another Real Heading`;
62
+
63
+ const { result } = renderHook(() => useHeadings(content, "markdown"));
64
+
65
+ expect(result.current).toEqual([
66
+ { id: "real-heading", text: "Real Heading", level: 1 },
67
+ { id: "another-real-heading", text: "Another Real Heading", level: 2 },
68
+ ]);
69
+ });
70
+
71
+ it("handles multiple code blocks", () => {
72
+ const content = `# Introduction
73
+
74
+ \`\`\`bash
75
+ # Comment 1
76
+ \`\`\`
77
+
78
+ ## Methods
79
+
80
+ \`\`\`python
81
+ # Comment 2
82
+ \`\`\`
83
+
84
+ ## Results`;
85
+
86
+ const { result } = renderHook(() => useHeadings(content, "markdown"));
87
+
88
+ expect(result.current).toEqual([
89
+ { id: "introduction", text: "Introduction", level: 1 },
90
+ { id: "methods", text: "Methods", level: 2 },
91
+ { id: "results", text: "Results", level: 2 },
92
+ ]);
93
+ });
94
+
95
+ it("handles code block with language specifier", () => {
96
+ const content = `# Setup
97
+
98
+ \`\`\`bash
99
+ # Use a custom port
100
+ npx readit document.md --port 3000
101
+ \`\`\`
102
+
103
+ ## Usage`;
104
+
105
+ const { result } = renderHook(() => useHeadings(content, "markdown"));
106
+
107
+ expect(result.current).toEqual([
108
+ { id: "setup", text: "Setup", level: 1 },
109
+ { id: "usage", text: "Usage", level: 2 },
110
+ ]);
111
+ });
112
+
113
+ it("returns empty array for null content", () => {
114
+ const { result } = renderHook(() => useHeadings(null, "markdown"));
115
+ expect(result.current).toEqual([]);
116
+ });
117
+
118
+ it("returns empty array for null type", () => {
119
+ const { result } = renderHook(() => useHeadings("# Heading", null));
120
+ expect(result.current).toEqual([]);
121
+ });
122
+ });
123
+
124
+ describe("useHeadings - html", () => {
125
+ it("extracts basic headings", () => {
126
+ const content = `<h1>Heading 1</h1>
127
+ <h2>Heading 2</h2>
128
+ <h3>Heading 3</h3>`;
129
+
130
+ const { result } = renderHook(() => useHeadings(content, "html"));
131
+
132
+ expect(result.current).toEqual([
133
+ { id: "heading-1", text: "Heading 1", level: 1 },
134
+ { id: "heading-2", text: "Heading 2", level: 2 },
135
+ { id: "heading-3", text: "Heading 3", level: 3 },
136
+ ]);
137
+ });
138
+
139
+ it("uses existing id attribute", () => {
140
+ const content = `<h1 id="custom-id">Heading 1</h1>`;
141
+
142
+ const { result } = renderHook(() => useHeadings(content, "html"));
143
+
144
+ expect(result.current).toEqual([
145
+ { id: "custom-id", text: "Heading 1", level: 1 },
146
+ ]);
147
+ });
148
+
149
+ it("decodes HTML entities", () => {
150
+ const content = `<h1>Hello &amp; World</h1>`;
151
+
152
+ const { result } = renderHook(() => useHeadings(content, "html"));
153
+
154
+ // Note: & is stripped, leaving "Hello World" → "hello-world" (hyphens collapsed)
155
+ expect(result.current).toEqual([
156
+ { id: "hello-world", text: "Hello & World", level: 1 },
157
+ ]);
158
+ });
159
+ });
@@ -0,0 +1,129 @@
1
+ import { useMemo } from "react";
2
+ import { slugify } from "../lib/utils";
3
+ import type { DocumentType } from "../types";
4
+
5
+ export interface Heading {
6
+ id: string;
7
+ text: string;
8
+ level: 1 | 2 | 3 | 4 | 5 | 6;
9
+ }
10
+
11
+ /**
12
+ * Remove code blocks from markdown content.
13
+ * Handles both fenced (```) and indented (4 spaces) code blocks.
14
+ */
15
+ function stripCodeBlocks(content: string): string {
16
+ // Remove fenced code blocks (``` or ~~~)
17
+ let result = content.replace(/^(`{3,}|~{3,}).*$[\s\S]*?^\1\s*$/gm, "");
18
+
19
+ // Remove indented code blocks (4 spaces or 1 tab at start of line)
20
+ // Only remove if preceded by a blank line (to avoid removing list items)
21
+ result = result.replace(/(?:^|\n\n)((?:(?:[ ]{4}|\t).+\n?)+)/g, "\n\n");
22
+
23
+ return result;
24
+ }
25
+
26
+ /**
27
+ * Extract headings from markdown content
28
+ */
29
+ function parseMarkdownHeadings(content: string): Heading[] {
30
+ const headings: Heading[] = [];
31
+ const seenIds = new Map<string, number>();
32
+
33
+ // Strip code blocks to avoid matching # comments in code
34
+ const contentWithoutCode = stripCodeBlocks(content);
35
+
36
+ const regex = /^(#{1,6})\s+(.+)$/gm;
37
+ let match = regex.exec(contentWithoutCode);
38
+
39
+ while (match !== null) {
40
+ const level = match[1].length as 1 | 2 | 3 | 4 | 5 | 6;
41
+ const text = match[2].trim();
42
+ const baseId = slugify(text);
43
+ const count = seenIds.get(baseId) ?? 0;
44
+ const id = count > 0 ? `${baseId}-${count}` : baseId;
45
+ seenIds.set(baseId, count + 1);
46
+
47
+ headings.push({ id, text, level });
48
+ match = regex.exec(contentWithoutCode);
49
+ }
50
+
51
+ return headings;
52
+ }
53
+
54
+ /**
55
+ * Generate ID matching the iframe's ensureHeadingIds algorithm.
56
+ * Note: This differs from utils/slugify - it strips underscores to match
57
+ * the iframe script's ID generation exactly.
58
+ */
59
+ function generateHeadingId(text: string): string {
60
+ return text
61
+ .toLowerCase()
62
+ .trim()
63
+ .replace(/[^a-z0-9 -]/g, "")
64
+ .replace(/ +/g, "-")
65
+ .replace(/-+/g, "-");
66
+ }
67
+
68
+ /**
69
+ * Extract headings from HTML content
70
+ */
71
+ function parseHtmlHeadings(content: string): Heading[] {
72
+ const headings: Heading[] = [];
73
+ const seenIds = new Map<string, number>();
74
+
75
+ // Match h1-h6 tags, capturing attributes and text content
76
+ const regex = /<h([1-6])([^>]*)>([^<]+)<\/h\1>/gi;
77
+ let match = regex.exec(content);
78
+
79
+ while (match !== null) {
80
+ const level = Number.parseInt(match[1], 10) as 1 | 2 | 3 | 4 | 5 | 6;
81
+ const attributes = match[2];
82
+ // Strip any remaining HTML tags and decode entities
83
+ const text = match[3]
84
+ .replace(/<[^>]+>/g, "")
85
+ .replace(/&amp;/g, "&")
86
+ .replace(/&lt;/g, "<")
87
+ .replace(/&gt;/g, ">")
88
+ .replace(/&quot;/g, '"')
89
+ .trim();
90
+
91
+ if (text) {
92
+ // Extract existing id attribute if present
93
+ const idMatch = /\sid=["']([^"']+)["']/i.exec(attributes);
94
+
95
+ // Use existing ID or generate one with duplicate handling
96
+ const id = idMatch
97
+ ? idMatch[1]
98
+ : (() => {
99
+ const baseId = generateHeadingId(text);
100
+ const count = seenIds.get(baseId) ?? 0;
101
+ seenIds.set(baseId, count + 1);
102
+ return count > 0 ? `${baseId}-${count}` : baseId;
103
+ })();
104
+
105
+ headings.push({ id, text, level });
106
+ }
107
+ match = regex.exec(content);
108
+ }
109
+
110
+ return headings;
111
+ }
112
+
113
+ /**
114
+ * Hook to extract headings from document content
115
+ */
116
+ export function useHeadings(
117
+ content: string | null,
118
+ type: DocumentType | null,
119
+ ): Heading[] {
120
+ return useMemo(() => {
121
+ if (!content || !type) return [];
122
+
123
+ if (type === "markdown") {
124
+ return parseMarkdownHeadings(content);
125
+ }
126
+
127
+ return parseHtmlHeadings(content);
128
+ }, [content, type]);
129
+ }
@@ -0,0 +1,120 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { toast } from "sonner";
3
+ import {
4
+ resolveShortcuts,
5
+ type ShortcutDefinition,
6
+ } from "../lib/shortcut-registry";
7
+ import type { KeybindingOverride, ShortcutBinding } from "../types";
8
+
9
+ interface UseKeybindingsResult {
10
+ shortcuts: ShortcutDefinition[];
11
+ updateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
12
+ toggleEnabled: (id: string) => Promise<void>;
13
+ resetToDefaults: () => Promise<void>;
14
+ isLoading: boolean;
15
+ }
16
+
17
+ export function useKeybindings(filePath: string | null): UseKeybindingsResult {
18
+ const [overrides, setOverrides] = useState<KeybindingOverride[]>([]);
19
+ const [isLoading, setIsLoading] = useState(true);
20
+
21
+ const filePathRef = useRef(filePath);
22
+ filePathRef.current = filePath;
23
+
24
+ // Fetch keybindings from settings on mount
25
+ useEffect(() => {
26
+ if (!filePath) {
27
+ setIsLoading(false);
28
+ return;
29
+ }
30
+
31
+ const fetchKeybindings = async () => {
32
+ try {
33
+ const query = `?path=${encodeURIComponent(filePath)}`;
34
+ const response = await fetch(`/api/settings${query}`);
35
+ if (response.ok) {
36
+ const settings = await response.json();
37
+ setOverrides(settings.keybindings ?? []);
38
+ }
39
+ } catch (err) {
40
+ console.error("Failed to fetch keybindings:", err);
41
+ } finally {
42
+ setIsLoading(false);
43
+ }
44
+ };
45
+
46
+ fetchKeybindings();
47
+ }, [filePath]);
48
+
49
+ const persistOverrides = useCallback(
50
+ async (newOverrides: KeybindingOverride[]) => {
51
+ try {
52
+ const fp = filePathRef.current;
53
+ const query = fp ? `?path=${encodeURIComponent(fp)}` : "";
54
+ const response = await fetch(`/api/settings${query}`);
55
+ if (!response.ok) return;
56
+
57
+ const currentSettings = await response.json();
58
+ const updated = { ...currentSettings, keybindings: newOverrides };
59
+
60
+ const putResponse = await fetch(`/api/settings${query}`, {
61
+ method: "PUT",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify(updated),
64
+ });
65
+
66
+ if (!putResponse.ok) {
67
+ throw new Error("Failed to save keybindings");
68
+ }
69
+ } catch (err) {
70
+ console.error("Failed to save keybindings:", err);
71
+ toast.error("Failed to save keybindings");
72
+ }
73
+ },
74
+ [],
75
+ );
76
+
77
+ const updateBinding = useCallback(
78
+ async (id: string, binding: ShortcutBinding) => {
79
+ const newOverrides = overrides.filter((o) => o.id !== id);
80
+ newOverrides.push({ id, binding, enabled: true });
81
+
82
+ setOverrides(newOverrides);
83
+ await persistOverrides(newOverrides);
84
+ },
85
+ [overrides, persistOverrides],
86
+ );
87
+
88
+ const toggleEnabled = useCallback(
89
+ async (id: string) => {
90
+ const existing = overrides.find((o) => o.id === id);
91
+ const currentEnabled = existing?.enabled ?? true;
92
+ const newOverrides = overrides.filter((o) => o.id !== id);
93
+ newOverrides.push({
94
+ id,
95
+ binding: existing?.binding,
96
+ enabled: !currentEnabled,
97
+ });
98
+
99
+ setOverrides(newOverrides);
100
+ await persistOverrides(newOverrides);
101
+ },
102
+ [overrides, persistOverrides],
103
+ );
104
+
105
+ const resetToDefaults = useCallback(async () => {
106
+ setOverrides([]);
107
+ await persistOverrides([]);
108
+ toast.success("Keyboard shortcuts reset to defaults");
109
+ }, [persistOverrides]);
110
+
111
+ const shortcuts = resolveShortcuts(overrides);
112
+
113
+ return {
114
+ shortcuts,
115
+ updateBinding,
116
+ toggleEnabled,
117
+ resetToDefaults,
118
+ isLoading,
119
+ };
120
+ }
@@ -0,0 +1,63 @@
1
+ import { useEffect, useRef } from "react";
2
+ import {
3
+ matchesBinding,
4
+ type ShortcutAction,
5
+ type ShortcutDefinition,
6
+ } from "../lib/shortcut-registry";
7
+
8
+ type ActionMap = Partial<Record<ShortcutAction, () => void>>;
9
+
10
+ /**
11
+ * Returns true if the event target is an input element where
12
+ * keyboard shortcuts should be suppressed.
13
+ */
14
+ function isInputFocused(event: KeyboardEvent): boolean {
15
+ const target = event.target;
16
+ if (!(target instanceof HTMLElement)) return false;
17
+
18
+ const tagName = target.tagName;
19
+ if (tagName === "INPUT" || tagName === "TEXTAREA") return true;
20
+ if (target.isContentEditable) return true;
21
+
22
+ return false;
23
+ }
24
+
25
+ /**
26
+ * Single centralized keyboard shortcut listener.
27
+ * Replaces all scattered useEffect keydown handlers.
28
+ *
29
+ * @param shortcuts - Resolved shortcut definitions (from useKeybindings)
30
+ * @param actions - Map of shortcut ID to callback function
31
+ */
32
+ export function useKeyboardShortcuts(
33
+ shortcuts: ShortcutDefinition[],
34
+ actions: ActionMap,
35
+ ): void {
36
+ const actionsRef = useRef(actions);
37
+ actionsRef.current = actions;
38
+
39
+ const shortcutsRef = useRef(shortcuts);
40
+ shortcutsRef.current = shortcuts;
41
+
42
+ useEffect(() => {
43
+ const handleKeyDown = (event: KeyboardEvent) => {
44
+ if (isInputFocused(event)) return;
45
+
46
+ for (const shortcut of shortcutsRef.current) {
47
+ if (!shortcut.enabled) continue;
48
+
49
+ if (matchesBinding(event, shortcut.binding)) {
50
+ const action = actionsRef.current[shortcut.id];
51
+ if (action) {
52
+ event.preventDefault();
53
+ action();
54
+ }
55
+ return;
56
+ }
57
+ }
58
+ };
59
+
60
+ window.addEventListener("keydown", handleKeyDown);
61
+ return () => window.removeEventListener("keydown", handleKeyDown);
62
+ }, []);
63
+ }
@@ -0,0 +1,44 @@
1
+ import { useCallback, useState } from "react";
2
+ import { type LayoutMode, LayoutModes } from "../types";
3
+
4
+ const STORAGE_KEY = "readit:layout-mode";
5
+
6
+ interface UseLayoutModeResult {
7
+ layoutMode: LayoutMode;
8
+ toggleLayoutMode: () => void;
9
+ isFullscreen: boolean;
10
+ }
11
+
12
+ export function useLayoutMode(): UseLayoutModeResult {
13
+ const [layoutMode, setLayoutMode] = useState<LayoutMode>(() => {
14
+ try {
15
+ const stored = localStorage.getItem(STORAGE_KEY);
16
+ return stored === LayoutModes.FULLSCREEN
17
+ ? LayoutModes.FULLSCREEN
18
+ : LayoutModes.CENTERED;
19
+ } catch {
20
+ return LayoutModes.CENTERED;
21
+ }
22
+ });
23
+
24
+ const toggleLayoutMode = useCallback(() => {
25
+ setLayoutMode((prev) => {
26
+ const next =
27
+ prev === LayoutModes.CENTERED
28
+ ? LayoutModes.FULLSCREEN
29
+ : LayoutModes.CENTERED;
30
+ try {
31
+ localStorage.setItem(STORAGE_KEY, next);
32
+ } catch {
33
+ // localStorage may be unavailable
34
+ }
35
+ return next;
36
+ });
37
+ }, []);
38
+
39
+ return {
40
+ layoutMode,
41
+ toggleLayoutMode,
42
+ isFullscreen: layoutMode === LayoutModes.FULLSCREEN,
43
+ };
44
+ }
@@ -0,0 +1,33 @@
1
+ import { useCallback } from "react";
2
+ import { appStore, useAppStore } from "../store";
3
+
4
+ interface UseReanchorModeResult {
5
+ reanchorTarget: { commentId: string } | null;
6
+ startReanchor: (commentId: string) => void;
7
+ cancelReanchor: () => void;
8
+ }
9
+
10
+ /**
11
+ * Hook for managing re-anchor mode state.
12
+ * When active, the user can select new text to re-anchor an unresolved comment.
13
+ * State lives in the Zustand store for tab-switch preservation.
14
+ */
15
+ export function useReanchorMode(): UseReanchorModeResult {
16
+ const reanchorTarget = useAppStore(
17
+ (s) => s.getActiveDocumentState()?.reanchorTarget ?? null,
18
+ );
19
+
20
+ const startReanchor = useCallback((commentId: string) => {
21
+ appStore.getState().setReanchorTarget({ commentId });
22
+ }, []);
23
+
24
+ const cancelReanchor = useCallback(() => {
25
+ appStore.getState().setReanchorTarget(null);
26
+ }, []);
27
+
28
+ return {
29
+ reanchorTarget,
30
+ startReanchor,
31
+ cancelReanchor,
32
+ };
33
+ }
@@ -0,0 +1,56 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ interface ScrollMetrics {
4
+ documentHeight: number;
5
+ viewportHeight: number;
6
+ scrollTop: number;
7
+ }
8
+
9
+ /**
10
+ * Track document scroll and viewport dimensions for minimap calculations.
11
+ * Updates are throttled to once per animation frame to prevent scroll jank.
12
+ */
13
+ export function useScrollMetrics(): ScrollMetrics {
14
+ const [metrics, setMetrics] = useState<ScrollMetrics>({
15
+ documentHeight: 0,
16
+ viewportHeight: 0,
17
+ scrollTop: 0,
18
+ });
19
+
20
+ const rafIdRef = useRef<number | null>(null);
21
+
22
+ useEffect(() => {
23
+ const updateMetrics = () => {
24
+ setMetrics({
25
+ documentHeight: document.body.scrollHeight,
26
+ viewportHeight: window.innerHeight,
27
+ scrollTop: window.scrollY,
28
+ });
29
+ };
30
+
31
+ // Throttle scroll updates to once per animation frame
32
+ const handleScroll = () => {
33
+ if (rafIdRef.current !== null) return;
34
+ rafIdRef.current = requestAnimationFrame(() => {
35
+ updateMetrics();
36
+ rafIdRef.current = null;
37
+ });
38
+ };
39
+
40
+ // Initial measurement
41
+ updateMetrics();
42
+
43
+ window.addEventListener("scroll", handleScroll, { passive: true });
44
+ window.addEventListener("resize", updateMetrics);
45
+
46
+ return () => {
47
+ window.removeEventListener("scroll", handleScroll);
48
+ window.removeEventListener("resize", updateMetrics);
49
+ if (rafIdRef.current !== null) {
50
+ cancelAnimationFrame(rafIdRef.current);
51
+ }
52
+ };
53
+ }, []);
54
+
55
+ return metrics;
56
+ }