@peaske7/readit 0.3.0-rc.0 → 0.3.0-rc.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/dist/.vite/manifest.json +1111 -0
- package/dist/assets/_basePickBy-BMMA4Tou.js +1 -0
- package/dist/assets/_baseUniq-D40qku1I.js +1 -0
- package/dist/assets/arc-Ckg65iy8.js +1 -0
- package/dist/assets/architecture-YZFGNWBL-Dv3EY0zV.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-ClRss4cm.js +36 -0
- package/dist/assets/array-Bjz-wYpJ.js +1 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CBcFvoK1.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-D4d3ZLam.js +10 -0
- package/dist/assets/channel-D9EJxDy_.js +1 -0
- package/dist/assets/chunk-2KRD3SAO-DaFfaCGO.js +1 -0
- package/dist/assets/chunk-336JU56O-yLEQoF0v.js +2 -0
- package/dist/assets/chunk-426QAEUC-Uyzd4wAA.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-DRuTD7x5.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-3xbpIi_o.js +206 -0
- package/dist/assets/chunk-55IACEB6-BExiaAoD.js +1 -0
- package/dist/assets/chunk-5FUZZQ4R-DatVvHnF.js +62 -0
- package/dist/assets/chunk-5PVQY5BW-BKgvrGh8.js +2 -0
- package/dist/assets/chunk-67CJDMHE-DMt8LNEX.js +1 -0
- package/dist/assets/chunk-7N4EOEYR-CzLGefVf.js +1 -0
- package/dist/assets/chunk-AA7GKIK3-B6GFAk4U.js +1 -0
- package/dist/assets/chunk-BSJP7CBP-BK29yehL.js +1 -0
- package/dist/assets/chunk-CIAEETIT-D7hBXImP.js +1 -0
- package/dist/assets/chunk-Dlc7tRH4.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-PYJdlmyH.js +1 -0
- package/dist/assets/chunk-ENJZ2VHE-DUHKBv6x.js +10 -0
- package/dist/assets/chunk-FMBD7UC4-2FWyCCAV.js +15 -0
- package/dist/assets/chunk-FOC6F5B3-DKFHrt4K.js +1 -0
- package/dist/assets/chunk-ICPOFSXX-Bh__D0ec.js +122 -0
- package/dist/assets/chunk-K5T4RW27-D51O7IkG.js +94 -0
- package/dist/assets/chunk-KGLVRYIC-DMHSCH4T.js +1 -0
- package/dist/assets/chunk-LIHQZDEY-C2aANxt9.js +1 -0
- package/dist/assets/chunk-ORNJ4GCN-Db_37NRX.js +1 -0
- package/dist/assets/chunk-OYMX7WX6-BltXOJLJ.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-8Lcg9gti.js +1 -0
- package/dist/assets/chunk-U2HBQHQK-ByS6tilY.js +70 -0
- package/dist/assets/chunk-X2U36JSP-Bm-4Gqg_.js +1 -0
- package/dist/assets/chunk-XPW4576I-Bqbompq4.js +32 -0
- package/dist/assets/chunk-YZCP3GAM-CsC0imPb.js +1 -0
- package/dist/assets/chunk-ZZ45TVLE-CG-CqfPC.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-Jy1uFUk4.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-ChiLl3rR.js +1 -0
- package/dist/assets/clone-BBjvuERA.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-q90QeGKv.js +1 -0
- package/dist/assets/cytoscape.esm-BfXff3fb.js +321 -0
- package/dist/assets/dagre-KV5264BT-BQWiLFJB.js +4 -0
- package/dist/assets/dagre-nn_aIZ2E.js +1 -0
- package/dist/assets/defaultLocale-BwmRmqJp.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-CJa7Y97H.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-tVQGBWfY.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-CpimFldm.js +43 -0
- package/dist/assets/diagram-TYMM5635-D11WQVgy.js +24 -0
- package/dist/assets/dist-BNz65Ibc.js +1 -0
- package/dist/assets/erDiagram-SMLLAGMA-C2bLd0jS.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-Kw3fOOLT.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-fyMhyE2X.js +292 -0
- package/dist/assets/gitGraph-7Q5UKJZL-BGFRt2qs.js +1 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-D4JoiOvg.js +106 -0
- package/dist/assets/graphlib-DGcD9J2L.js +1 -0
- package/dist/assets/index-Cow3qpoq.css +2 -0
- package/dist/assets/index-DUf7okYi.js +14 -0
- package/dist/assets/info-OMHHGYJF-DI6-Z9vh.js +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-D1ZkeMBy.js +2 -0
- package/dist/assets/init-TPm5RB77.js +1 -0
- package/dist/assets/isArrayLikeObject-69BLnVNM.js +1 -0
- package/dist/assets/isEmpty-DUS28g5f.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-Dv8hzjZB.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-COeB7F5r.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BbYmxCYU.js +89 -0
- package/dist/assets/katex-5SGEXwpi.js +261 -0
- package/dist/assets/line-_v2NGEdn.js +1 -0
- package/dist/assets/linear-CXMqTN8N.js +1 -0
- package/dist/assets/mermaid-config-C8a4L22x.js +1 -0
- package/dist/assets/mermaid-parser.core-CFmphzPP.js +4 -0
- package/dist/assets/mermaid.core-DnHAupTp.js +11 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-D7_lIep7.js +96 -0
- package/dist/assets/ordinal-D7l-8DAO.js +1 -0
- package/dist/assets/packet-4T2RLAQJ-DidW3JFc.js +1 -0
- package/dist/assets/path-BVpCanzE.js +1 -0
- package/dist/assets/pie-ZZUOXDRM-Bff2e5hg.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-DDvYHCT_.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DcLcIrdi.js +7 -0
- package/dist/assets/radar-PYXPWWZC-CsdZBH3M.js +1 -0
- package/dist/assets/requirementDiagram-MS252O5E-DLX6ld7D.js +84 -0
- package/dist/assets/rough.esm-BoTisKeL.js +1 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-D-1GtsHM.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-Bwxs0YQg.js +157 -0
- package/dist/assets/src-CrmkjRpa.js +1 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DW7rOcnQ.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Jm-24vQ2.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-DVdHyzxS.js +120 -0
- package/dist/assets/treeView-SZITEDCU-DPKseaET.js +1 -0
- package/dist/assets/treemap-W4RFUUIX-DH-7GZe_.js +1 -0
- package/dist/assets/vennDiagram-DHZGUBPP-DJaC6xmI.js +34 -0
- package/dist/assets/wardley-RL74JXVD-AgyXyBN5.js +1 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-CTKERPKv.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-BuExiLXc.js +7 -0
- package/{index.html → dist/index.html} +2 -1
- package/dist/index.js +2539 -0
- package/package.json +11 -1
- package/.agents/skills/remotion-best-practices/SKILL.md +0 -61
- package/.agents/skills/remotion-best-practices/rules/3d.md +0 -86
- package/.agents/skills/remotion-best-practices/rules/animations.md +0 -27
- package/.agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +0 -178
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +0 -100
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +0 -108
- package/.agents/skills/remotion-best-practices/rules/assets.md +0 -78
- package/.agents/skills/remotion-best-practices/rules/audio-visualization.md +0 -198
- package/.agents/skills/remotion-best-practices/rules/audio.md +0 -169
- package/.agents/skills/remotion-best-practices/rules/calculate-metadata.md +0 -134
- package/.agents/skills/remotion-best-practices/rules/can-decode.md +0 -75
- package/.agents/skills/remotion-best-practices/rules/charts.md +0 -120
- package/.agents/skills/remotion-best-practices/rules/compositions.md +0 -154
- package/.agents/skills/remotion-best-practices/rules/display-captions.md +0 -184
- package/.agents/skills/remotion-best-practices/rules/extract-frames.md +0 -229
- package/.agents/skills/remotion-best-practices/rules/ffmpeg.md +0 -38
- package/.agents/skills/remotion-best-practices/rules/fonts.md +0 -152
- package/.agents/skills/remotion-best-practices/rules/get-audio-duration.md +0 -58
- package/.agents/skills/remotion-best-practices/rules/get-video-dimensions.md +0 -68
- package/.agents/skills/remotion-best-practices/rules/get-video-duration.md +0 -60
- package/.agents/skills/remotion-best-practices/rules/gifs.md +0 -141
- package/.agents/skills/remotion-best-practices/rules/images.md +0 -134
- package/.agents/skills/remotion-best-practices/rules/import-srt-captions.md +0 -69
- package/.agents/skills/remotion-best-practices/rules/light-leaks.md +0 -73
- package/.agents/skills/remotion-best-practices/rules/lottie.md +0 -70
- package/.agents/skills/remotion-best-practices/rules/maps.md +0 -412
- package/.agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md +0 -34
- package/.agents/skills/remotion-best-practices/rules/measuring-text.md +0 -140
- package/.agents/skills/remotion-best-practices/rules/parameters.md +0 -109
- package/.agents/skills/remotion-best-practices/rules/sequencing.md +0 -118
- package/.agents/skills/remotion-best-practices/rules/sfx.md +0 -26
- package/.agents/skills/remotion-best-practices/rules/subtitles.md +0 -36
- package/.agents/skills/remotion-best-practices/rules/tailwind.md +0 -11
- package/.agents/skills/remotion-best-practices/rules/text-animations.md +0 -20
- package/.agents/skills/remotion-best-practices/rules/timing.md +0 -179
- package/.agents/skills/remotion-best-practices/rules/transcribe-captions.md +0 -70
- package/.agents/skills/remotion-best-practices/rules/transitions.md +0 -197
- package/.agents/skills/remotion-best-practices/rules/transparent-videos.md +0 -106
- package/.agents/skills/remotion-best-practices/rules/trimming.md +0 -51
- package/.agents/skills/remotion-best-practices/rules/videos.md +0 -171
- package/.agents/skills/remotion-best-practices/rules/voiceover.md +0 -99
- package/.agents/skills/simple/SKILL.md +0 -52
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +0 -3254
- package/.agents/skills/vercel-react-best-practices/README.md +0 -123
- package/.agents/skills/vercel-react-best-practices/SKILL.md +0 -141
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +0 -42
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -39
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -38
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -80
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -51
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +0 -28
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -59
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -50
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -107
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -50
- package/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +0 -60
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -37
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -49
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -26
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +0 -30
- package/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +0 -85
- package/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +0 -68
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +0 -75
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +0 -40
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +0 -38
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -44
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +0 -45
- package/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +0 -82
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +0 -35
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +0 -73
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +0 -96
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -76
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +0 -65
- package/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +0 -142
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +0 -38
- package/.claude/CLAUDE.md +0 -184
- package/.claude/commands/review.md +0 -120
- package/.claude/commands/sync-docs.md +0 -71
- package/.claude/roadmap.md +0 -121
- package/.claude/rules/style-guide.md +0 -830
- package/.claude/settings.json +0 -18
- package/.claude/user-stories.md +0 -333
- package/AGENTS.md +0 -68
- package/Makefile +0 -32
- package/biome.json +0 -79
- package/bun.lock +0 -854
- package/bunfig.toml +0 -2
- package/docs/design.md +0 -563
- package/docs/perf-baseline.md +0 -130
- package/docs/plans/2026-03-13-client-mode-design.md +0 -86
- package/docs/plans/2026-03-13-client-mode-plan.md +0 -605
- package/docs/plans/2026-03-13-keyboard-shortcuts-design.md +0 -129
- package/docs/plans/2026-03-13-keyboard-shortcuts-plan.md +0 -1471
- package/docs/plans/2026-03-13-multi-document-design.md +0 -183
- package/docs/plans/2026-03-13-performance-benchmarks-design.md +0 -121
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +0 -1176
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +0 -284
- package/e2e/comments.spec.ts +0 -81
- package/e2e/document-load.spec.ts +0 -32
- package/e2e/export.spec.ts +0 -58
- package/e2e/fixtures/sample.md +0 -7
- package/e2e/perf/add-comment.spec.ts +0 -116
- package/e2e/perf/fixtures/generate.ts +0 -327
- package/e2e/perf/initial-load.spec.ts +0 -49
- package/e2e/perf/perf.setup.ts +0 -23
- package/e2e/perf/perf.teardown.ts +0 -9
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/scroll.spec.ts +0 -39
- package/e2e/perf/tab-switch.spec.ts +0 -69
- package/e2e/perf/text-selection.spec.ts +0 -119
- package/e2e/perf/utils/metrics.ts +0 -350
- package/e2e/perf/utils/perf-cli.ts +0 -86
- package/e2e/persistence-file.spec.ts +0 -357
- package/e2e/utils/cli.ts +0 -84
- package/e2e/utils/selection.ts +0 -79
- package/go/cmd/readit/main.go +0 -416
- package/go/go.mod +0 -20
- package/go/go.sum +0 -41
- package/go/internal/server/anchor.go +0 -302
- package/go/internal/server/anchor_test.go +0 -111
- package/go/internal/server/comments.go +0 -390
- package/go/internal/server/documents.go +0 -113
- package/go/internal/server/embed.go +0 -17
- package/go/internal/server/headings.go +0 -33
- package/go/internal/server/headings_test.go +0 -75
- package/go/internal/server/htmltext.go +0 -123
- package/go/internal/server/markdown.go +0 -157
- package/go/internal/server/markdown_bench_test.go +0 -42
- package/go/internal/server/markdown_test.go +0 -79
- package/go/internal/server/server.go +0 -453
- package/go/internal/server/server_bench_test.go +0 -122
- package/go/internal/server/settings.go +0 -110
- package/go/internal/server/sse.go +0 -140
- package/go/internal/server/storage.go +0 -275
- package/go/internal/server/storage_test.go +0 -152
- package/go/internal/server/template.go +0 -66
- package/go/internal/server/types.go +0 -101
- package/go/internal/server/watcher.go +0 -74
- package/lefthook.yml +0 -8
- package/nvim-readit/lua/readit/health.lua +0 -64
- package/nvim-readit/lua/readit/init.lua +0 -463
- package/nvim-readit/plugin/readit.lua +0 -19
- package/playwright.config.ts +0 -34
- package/skills-lock.json +0 -20
- package/src/App.svelte +0 -890
- package/src/cli.ts +0 -881
- package/src/components/ActionsMenu.svelte +0 -95
- package/src/components/CommentBadge.svelte +0 -67
- package/src/components/CommentErrorBanner.svelte +0 -33
- package/src/components/CommentInput.svelte +0 -75
- package/src/components/CommentListItem.svelte +0 -95
- package/src/components/CommentManager.svelte +0 -129
- package/src/components/CommentNav.svelte +0 -109
- package/src/components/DocumentViewer.svelte +0 -233
- package/src/components/FloatingComment.svelte +0 -107
- package/src/components/Header.svelte +0 -76
- package/src/components/InlineEditor.svelte +0 -72
- package/src/components/MarginNote.svelte +0 -167
- package/src/components/MarginNotesContainer.svelte +0 -33
- package/src/components/MermaidEnhancer.svelte +0 -218
- package/src/components/MermaidModal.svelte +0 -67
- package/src/components/RawModal.svelte +0 -126
- package/src/components/ReanchorConfirm.svelte +0 -30
- package/src/components/SettingsModal.svelte +0 -220
- package/src/components/ShortcutCapture.svelte +0 -82
- package/src/components/ShortcutList.svelte +0 -145
- package/src/components/TabBar.svelte +0 -52
- package/src/components/TableOfContents.svelte +0 -125
- package/src/components/ui/ActionLink.svelte +0 -40
- package/src/components/ui/Button.svelte +0 -53
- package/src/components/ui/Dialog.svelte +0 -97
- package/src/components/ui/DropdownMenu.svelte +0 -85
- package/src/components/ui/DropdownMenuItem.svelte +0 -38
- package/src/components/ui/DropdownMenuSeparator.svelte +0 -11
- package/src/components/ui/Text.svelte +0 -42
- package/src/env.d.ts +0 -6
- package/src/index.css +0 -859
- package/src/lib/__fixtures__/bench-data.ts +0 -114
- package/src/lib/anchor.bench.ts +0 -91
- package/src/lib/anchor.test.ts +0 -527
- package/src/lib/anchor.ts +0 -381
- package/src/lib/comment-storage.bench.ts +0 -49
- package/src/lib/comment-storage.test.ts +0 -694
- package/src/lib/comment-storage.ts +0 -226
- package/src/lib/export.bench.ts +0 -21
- package/src/lib/export.ts +0 -36
- package/src/lib/fetch-or-throw.test.ts +0 -59
- package/src/lib/fetch-or-throw.ts +0 -12
- package/src/lib/headings.test.ts +0 -103
- package/src/lib/headings.ts +0 -44
- package/src/lib/highlight/core.test.ts +0 -93
- package/src/lib/highlight/dom.ts +0 -187
- package/src/lib/highlight/highlight-registry.ts +0 -221
- package/src/lib/highlight/highlight.bench.ts +0 -92
- package/src/lib/highlight/highlighter.ts +0 -247
- package/src/lib/highlight/resolver.ts +0 -38
- package/src/lib/highlight/types.ts +0 -17
- package/src/lib/html-text.test.ts +0 -162
- package/src/lib/html-text.ts +0 -161
- package/src/lib/i18n/en.ts +0 -124
- package/src/lib/i18n/index.ts +0 -3
- package/src/lib/i18n/ja.ts +0 -126
- package/src/lib/i18n/translations.ts +0 -27
- package/src/lib/i18n/types.ts +0 -130
- package/src/lib/key-lock.test.ts +0 -104
- package/src/lib/key-lock.ts +0 -23
- package/src/lib/margin-layout.bench.ts +0 -61
- package/src/lib/margin-layout.ts +0 -71
- package/src/lib/markdown-renderer.test.ts +0 -154
- package/src/lib/markdown-renderer.ts +0 -178
- package/src/lib/mermaid-config.ts +0 -38
- package/src/lib/mermaid-renderer.ts +0 -162
- package/src/lib/mermaid-worker.ts +0 -60
- package/src/lib/positions.ts +0 -157
- package/src/lib/shortcut-registry.ts +0 -244
- package/src/lib/utils.ts +0 -15
- package/src/main.ts +0 -16
- package/src/schema.ts +0 -92
- package/src/server.ts +0 -1216
- package/src/stores/app.svelte.ts +0 -231
- package/src/stores/locale.svelte.ts +0 -46
- package/src/stores/settings.svelte.ts +0 -90
- package/src/stores/shortcuts.svelte.ts +0 -104
- package/src/stores/ui.svelte.ts +0 -12
- package/src/template.ts +0 -104
- package/src/test-setup.ts +0 -48
- package/svelte.config.js +0 -5
- package/test.md +0 -74
- package/tsconfig.cli.json +0 -12
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -47
- package/vitest.config.ts +0 -15
- package/vscode-readit/.mcp.json +0 -7
- package/vscode-readit/.vscodeignore +0 -7
- package/vscode-readit/bun.lock +0 -78
- package/vscode-readit/icon.svg +0 -10
- package/vscode-readit/package.json +0 -110
- package/vscode-readit/src/extension.ts +0 -117
- package/vscode-readit/src/server-manager.ts +0 -272
- package/vscode-readit/src/webview-provider.ts +0 -204
- package/vscode-readit/tsconfig.json +0 -20
|
@@ -1,1471 +0,0 @@
|
|
|
1
|
-
# Keyboard Shortcuts — Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
-
|
|
5
|
-
**Goal:** Consolidate scattered keyboard shortcut handling into a centralized registry with rebinding UI and server-side persistence.
|
|
6
|
-
|
|
7
|
-
**Architecture:** A shortcut registry defines all shortcuts as data. A single `useKeyboardShortcuts` hook replaces all scattered `keydown` listeners. Custom bindings are persisted via the existing settings API (`~/.readit/settings/`). The settings modal gets a keyboard shortcuts editor with rebinding and enable/disable.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** React 19, TypeScript, Vitest, Bun.serve(), Tailwind CSS v4
|
|
10
|
-
|
|
11
|
-
**Design doc:** `docs/plans/2026-03-13-keyboard-shortcuts-design.md`
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
### Task 1: Types — Add ShortcutBinding and KeybindingOverride
|
|
16
|
-
|
|
17
|
-
**Files:**
|
|
18
|
-
- Modify: `src/types/index.ts`
|
|
19
|
-
|
|
20
|
-
**Step 1: Add types to `src/types/index.ts`**
|
|
21
|
-
|
|
22
|
-
Append after the `DocumentSettings` interface (line 90):
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
// Keyboard shortcut binding
|
|
26
|
-
export interface ShortcutBinding {
|
|
27
|
-
key: string; // KeyboardEvent.key value, e.g. "c", "ArrowUp"
|
|
28
|
-
alt?: boolean;
|
|
29
|
-
meta?: boolean; // ⌘ on Mac, Ctrl on Windows/Linux
|
|
30
|
-
shift?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// User override for a shortcut
|
|
34
|
-
export interface KeybindingOverride {
|
|
35
|
-
id: string;
|
|
36
|
-
binding?: ShortcutBinding; // undefined = use default
|
|
37
|
-
enabled: boolean;
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**Step 2: Extend `DocumentSettings` to include keybindings**
|
|
42
|
-
|
|
43
|
-
Change the `DocumentSettings` interface to:
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
export interface DocumentSettings {
|
|
47
|
-
version: number;
|
|
48
|
-
fontFamily: FontFamily;
|
|
49
|
-
keybindings?: KeybindingOverride[];
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
**Step 3: Run typecheck**
|
|
54
|
-
|
|
55
|
-
Run: `bun run typecheck`
|
|
56
|
-
Expected: PASS (no consumers of the new types yet)
|
|
57
|
-
|
|
58
|
-
**Step 4: Commit**
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
git add src/types/index.ts
|
|
62
|
-
git commit -m "feat: add ShortcutBinding and KeybindingOverride types"
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
### Task 2: Shortcut Registry — Definitions, Matcher, Display Formatter
|
|
68
|
-
|
|
69
|
-
**Files:**
|
|
70
|
-
- Create: `src/lib/shortcut-registry.ts`
|
|
71
|
-
- Create: `src/lib/shortcut-registry.test.ts`
|
|
72
|
-
|
|
73
|
-
**Step 1: Write the failing tests**
|
|
74
|
-
|
|
75
|
-
Create `src/lib/shortcut-registry.test.ts`:
|
|
76
|
-
|
|
77
|
-
```ts
|
|
78
|
-
import { describe, expect, it } from "vitest";
|
|
79
|
-
import {
|
|
80
|
-
DEFAULT_SHORTCUTS,
|
|
81
|
-
type ShortcutBinding,
|
|
82
|
-
formatBinding,
|
|
83
|
-
matchesBinding,
|
|
84
|
-
resolveShortcuts,
|
|
85
|
-
type KeybindingOverride,
|
|
86
|
-
RESERVED_BINDINGS,
|
|
87
|
-
isReservedBinding,
|
|
88
|
-
ShortcutActions,
|
|
89
|
-
} from "./shortcut-registry";
|
|
90
|
-
|
|
91
|
-
describe("DEFAULT_SHORTCUTS", () => {
|
|
92
|
-
it("defines 7 shortcuts", () => {
|
|
93
|
-
expect(DEFAULT_SHORTCUTS).toHaveLength(7);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("has unique IDs", () => {
|
|
97
|
-
const ids = DEFAULT_SHORTCUTS.map((s) => s.id);
|
|
98
|
-
expect(new Set(ids).size).toBe(ids.length);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("all enabled by default", () => {
|
|
102
|
-
for (const shortcut of DEFAULT_SHORTCUTS) {
|
|
103
|
-
expect(shortcut.enabled).toBe(true);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("matchesBinding", () => {
|
|
109
|
-
it("matches exact key + modifiers", () => {
|
|
110
|
-
const binding: ShortcutBinding = { key: "c", alt: true };
|
|
111
|
-
const event = new KeyboardEvent("keydown", { key: "c", altKey: true });
|
|
112
|
-
expect(matchesBinding(event, binding)).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("rejects wrong key", () => {
|
|
116
|
-
const binding: ShortcutBinding = { key: "c", alt: true };
|
|
117
|
-
const event = new KeyboardEvent("keydown", { key: "v", altKey: true });
|
|
118
|
-
expect(matchesBinding(event, binding)).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("rejects extra modifiers", () => {
|
|
122
|
-
const binding: ShortcutBinding = { key: "c", alt: true };
|
|
123
|
-
const event = new KeyboardEvent("keydown", {
|
|
124
|
-
key: "c",
|
|
125
|
-
altKey: true,
|
|
126
|
-
shiftKey: true,
|
|
127
|
-
});
|
|
128
|
-
expect(matchesBinding(event, binding)).toBe(false);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("matches binding with shift", () => {
|
|
132
|
-
const binding: ShortcutBinding = { key: "c", alt: true, shift: true };
|
|
133
|
-
const event = new KeyboardEvent("keydown", {
|
|
134
|
-
key: "c",
|
|
135
|
-
altKey: true,
|
|
136
|
-
shiftKey: true,
|
|
137
|
-
});
|
|
138
|
-
expect(matchesBinding(event, binding)).toBe(true);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("matches meta key", () => {
|
|
142
|
-
const binding: ShortcutBinding = { key: "c", meta: true };
|
|
143
|
-
const event = new KeyboardEvent("keydown", { key: "c", metaKey: true });
|
|
144
|
-
expect(matchesBinding(event, binding)).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("matches key-only binding (no modifiers)", () => {
|
|
148
|
-
const binding: ShortcutBinding = { key: "Escape" };
|
|
149
|
-
const event = new KeyboardEvent("keydown", { key: "Escape" });
|
|
150
|
-
expect(matchesBinding(event, binding)).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe("formatBinding", () => {
|
|
155
|
-
it("formats Alt+C", () => {
|
|
156
|
-
const binding: ShortcutBinding = { key: "c", alt: true };
|
|
157
|
-
// macOS
|
|
158
|
-
expect(formatBinding(binding, true)).toBe("Alt+C");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("formats meta key as ⌘ on Mac", () => {
|
|
162
|
-
const binding: ShortcutBinding = { key: "c", meta: true };
|
|
163
|
-
expect(formatBinding(binding, true)).toBe("⌘+C");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("formats meta key as Ctrl on non-Mac", () => {
|
|
167
|
-
const binding: ShortcutBinding = { key: "c", meta: true };
|
|
168
|
-
expect(formatBinding(binding, false)).toBe("Ctrl+C");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("formats shift modifier", () => {
|
|
172
|
-
const binding: ShortcutBinding = { key: "c", meta: true, shift: true };
|
|
173
|
-
expect(formatBinding(binding, true)).toBe("⌘+Shift+C");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("formats arrow keys with symbols", () => {
|
|
177
|
-
const binding: ShortcutBinding = { key: "ArrowUp", alt: true };
|
|
178
|
-
expect(formatBinding(binding, true)).toBe("Alt+↑");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("formats Escape", () => {
|
|
182
|
-
const binding: ShortcutBinding = { key: "Escape" };
|
|
183
|
-
expect(formatBinding(binding, true)).toBe("Esc");
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe("resolveShortcuts", () => {
|
|
188
|
-
it("returns defaults when no overrides", () => {
|
|
189
|
-
const resolved = resolveShortcuts([]);
|
|
190
|
-
expect(resolved).toEqual(DEFAULT_SHORTCUTS);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("applies enabled override", () => {
|
|
194
|
-
const overrides: KeybindingOverride[] = [
|
|
195
|
-
{ id: ShortcutActions.COPY_ALL, enabled: false },
|
|
196
|
-
];
|
|
197
|
-
const resolved = resolveShortcuts(overrides);
|
|
198
|
-
const copyAll = resolved.find((s) => s.id === ShortcutActions.COPY_ALL);
|
|
199
|
-
expect(copyAll?.enabled).toBe(false);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("applies binding override", () => {
|
|
203
|
-
const overrides: KeybindingOverride[] = [
|
|
204
|
-
{
|
|
205
|
-
id: ShortcutActions.COPY_ALL,
|
|
206
|
-
enabled: true,
|
|
207
|
-
binding: { key: "a", meta: true },
|
|
208
|
-
},
|
|
209
|
-
];
|
|
210
|
-
const resolved = resolveShortcuts(overrides);
|
|
211
|
-
const copyAll = resolved.find((s) => s.id === ShortcutActions.COPY_ALL);
|
|
212
|
-
expect(copyAll?.binding).toEqual({ key: "a", meta: true });
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("ignores unknown override IDs", () => {
|
|
216
|
-
const overrides: KeybindingOverride[] = [
|
|
217
|
-
{ id: "unknown_action", enabled: false },
|
|
218
|
-
];
|
|
219
|
-
const resolved = resolveShortcuts(overrides);
|
|
220
|
-
expect(resolved).toEqual(DEFAULT_SHORTCUTS);
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe("isReservedBinding", () => {
|
|
225
|
-
it("detects ⌘+W as reserved", () => {
|
|
226
|
-
expect(isReservedBinding({ key: "w", meta: true })).toBe(true);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("allows Alt+C", () => {
|
|
230
|
-
expect(isReservedBinding({ key: "c", alt: true })).toBe(false);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**Step 2: Run tests to verify they fail**
|
|
236
|
-
|
|
237
|
-
Run: `bun run test src/lib/shortcut-registry.test.ts`
|
|
238
|
-
Expected: FAIL — module not found
|
|
239
|
-
|
|
240
|
-
**Step 3: Write the implementation**
|
|
241
|
-
|
|
242
|
-
Create `src/lib/shortcut-registry.ts`:
|
|
243
|
-
|
|
244
|
-
```ts
|
|
245
|
-
import type { KeybindingOverride, ShortcutBinding } from "../types";
|
|
246
|
-
|
|
247
|
-
export type { ShortcutBinding, KeybindingOverride };
|
|
248
|
-
|
|
249
|
-
export const ShortcutActions = {
|
|
250
|
-
COPY_ALL: "copyAll",
|
|
251
|
-
COPY_ALL_RAW: "copyAllRaw",
|
|
252
|
-
NAVIGATE_NEXT: "navigateNext",
|
|
253
|
-
NAVIGATE_PREVIOUS: "navigatePrevious",
|
|
254
|
-
COPY_SELECTION_RAW: "copySelectionRaw",
|
|
255
|
-
COPY_SELECTION_LLM: "copySelectionLLM",
|
|
256
|
-
CLEAR_SELECTION: "clearSelection",
|
|
257
|
-
} as const;
|
|
258
|
-
|
|
259
|
-
export type ShortcutAction =
|
|
260
|
-
(typeof ShortcutActions)[keyof typeof ShortcutActions];
|
|
261
|
-
|
|
262
|
-
export interface ShortcutDefinition {
|
|
263
|
-
id: ShortcutAction;
|
|
264
|
-
label: string;
|
|
265
|
-
description: string;
|
|
266
|
-
defaultBinding: ShortcutBinding;
|
|
267
|
-
binding: ShortcutBinding; // resolved (default or user override)
|
|
268
|
-
enabled: boolean;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
|
|
272
|
-
{
|
|
273
|
-
id: ShortcutActions.COPY_ALL,
|
|
274
|
-
label: "Copy All (AI)",
|
|
275
|
-
description: "Copy all comments in AI prompt format",
|
|
276
|
-
defaultBinding: { key: "c", alt: true },
|
|
277
|
-
binding: { key: "c", alt: true },
|
|
278
|
-
enabled: true,
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
id: ShortcutActions.COPY_ALL_RAW,
|
|
282
|
-
label: "Copy All (Raw)",
|
|
283
|
-
description: "Copy all comments as raw text",
|
|
284
|
-
defaultBinding: { key: "c", alt: true, shift: true },
|
|
285
|
-
binding: { key: "c", alt: true, shift: true },
|
|
286
|
-
enabled: true,
|
|
287
|
-
},
|
|
288
|
-
{
|
|
289
|
-
id: ShortcutActions.NAVIGATE_NEXT,
|
|
290
|
-
label: "Next Comment",
|
|
291
|
-
description: "Navigate to next comment",
|
|
292
|
-
defaultBinding: { key: "ArrowDown", alt: true },
|
|
293
|
-
binding: { key: "ArrowDown", alt: true },
|
|
294
|
-
enabled: true,
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
id: ShortcutActions.NAVIGATE_PREVIOUS,
|
|
298
|
-
label: "Previous Comment",
|
|
299
|
-
description: "Navigate to previous comment",
|
|
300
|
-
defaultBinding: { key: "ArrowUp", alt: true },
|
|
301
|
-
binding: { key: "ArrowUp", alt: true },
|
|
302
|
-
enabled: true,
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
id: ShortcutActions.COPY_SELECTION_RAW,
|
|
306
|
-
label: "Copy Selection",
|
|
307
|
-
description: "Copy selected text",
|
|
308
|
-
defaultBinding: { key: "c", meta: true },
|
|
309
|
-
binding: { key: "c", meta: true },
|
|
310
|
-
enabled: true,
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
id: ShortcutActions.COPY_SELECTION_LLM,
|
|
314
|
-
label: "Copy Selection (LLM)",
|
|
315
|
-
description: "Copy selected text with context for LLM",
|
|
316
|
-
defaultBinding: { key: "c", meta: true, shift: true },
|
|
317
|
-
binding: { key: "c", meta: true, shift: true },
|
|
318
|
-
enabled: true,
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
id: ShortcutActions.CLEAR_SELECTION,
|
|
322
|
-
label: "Clear Selection",
|
|
323
|
-
description: "Clear text selection",
|
|
324
|
-
defaultBinding: { key: "Escape" },
|
|
325
|
-
binding: { key: "Escape" },
|
|
326
|
-
enabled: true,
|
|
327
|
-
},
|
|
328
|
-
];
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Check if a KeyboardEvent matches a ShortcutBinding.
|
|
332
|
-
* All modifier flags must match exactly (no extra modifiers allowed).
|
|
333
|
-
*/
|
|
334
|
-
export function matchesBinding(
|
|
335
|
-
event: KeyboardEvent,
|
|
336
|
-
binding: ShortcutBinding,
|
|
337
|
-
): boolean {
|
|
338
|
-
if (event.key !== binding.key) return false;
|
|
339
|
-
if (event.altKey !== (binding.alt ?? false)) return false;
|
|
340
|
-
if (event.metaKey !== (binding.meta ?? false)) return false;
|
|
341
|
-
if (event.shiftKey !== (binding.shift ?? false)) return false;
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const KEY_DISPLAY: Record<string, string> = {
|
|
346
|
-
ArrowUp: "↑",
|
|
347
|
-
ArrowDown: "↓",
|
|
348
|
-
ArrowLeft: "←",
|
|
349
|
-
ArrowRight: "→",
|
|
350
|
-
Escape: "Esc",
|
|
351
|
-
" ": "Space",
|
|
352
|
-
Enter: "Enter",
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Format a ShortcutBinding for display.
|
|
357
|
-
* Shows ⌘ on Mac, Ctrl on other platforms.
|
|
358
|
-
*/
|
|
359
|
-
export function formatBinding(binding: ShortcutBinding, isMac: boolean): string {
|
|
360
|
-
const parts: string[] = [];
|
|
361
|
-
|
|
362
|
-
if (binding.meta) parts.push(isMac ? "⌘" : "Ctrl");
|
|
363
|
-
if (binding.alt) parts.push("Alt");
|
|
364
|
-
if (binding.shift) parts.push("Shift");
|
|
365
|
-
|
|
366
|
-
const keyDisplay =
|
|
367
|
-
KEY_DISPLAY[binding.key] ?? binding.key.toUpperCase();
|
|
368
|
-
parts.push(keyDisplay);
|
|
369
|
-
|
|
370
|
-
return parts.join("+");
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Merge user overrides with default shortcuts.
|
|
375
|
-
* Unknown override IDs are ignored.
|
|
376
|
-
*/
|
|
377
|
-
export function resolveShortcuts(
|
|
378
|
-
overrides: KeybindingOverride[],
|
|
379
|
-
): ShortcutDefinition[] {
|
|
380
|
-
if (overrides.length === 0) return DEFAULT_SHORTCUTS;
|
|
381
|
-
|
|
382
|
-
const overrideMap = new Map(overrides.map((o) => [o.id, o]));
|
|
383
|
-
|
|
384
|
-
return DEFAULT_SHORTCUTS.map((shortcut) => {
|
|
385
|
-
const override = overrideMap.get(shortcut.id);
|
|
386
|
-
if (!override) return shortcut;
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
...shortcut,
|
|
390
|
-
binding: override.binding ?? shortcut.defaultBinding,
|
|
391
|
-
enabled: override.enabled,
|
|
392
|
-
};
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Browser-reserved key combos that cannot be rebound.
|
|
398
|
-
*/
|
|
399
|
-
export const RESERVED_BINDINGS: ShortcutBinding[] = [
|
|
400
|
-
{ key: "w", meta: true },
|
|
401
|
-
{ key: "t", meta: true },
|
|
402
|
-
{ key: "n", meta: true },
|
|
403
|
-
{ key: "q", meta: true },
|
|
404
|
-
{ key: "l", meta: true },
|
|
405
|
-
{ key: "r", meta: true },
|
|
406
|
-
{ key: "F5" },
|
|
407
|
-
{ key: "F11" },
|
|
408
|
-
{ key: "F12" },
|
|
409
|
-
];
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Check if a binding conflicts with browser-reserved shortcuts.
|
|
413
|
-
*/
|
|
414
|
-
export function isReservedBinding(binding: ShortcutBinding): boolean {
|
|
415
|
-
return RESERVED_BINDINGS.some(
|
|
416
|
-
(reserved) =>
|
|
417
|
-
reserved.key === binding.key &&
|
|
418
|
-
(reserved.meta ?? false) === (binding.meta ?? false) &&
|
|
419
|
-
(reserved.alt ?? false) === (binding.alt ?? false) &&
|
|
420
|
-
(reserved.shift ?? false) === (binding.shift ?? false),
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Convert a KeyboardEvent to a ShortcutBinding (for the capture UI).
|
|
426
|
-
*/
|
|
427
|
-
export function eventToBinding(event: KeyboardEvent): ShortcutBinding | undefined {
|
|
428
|
-
// Ignore standalone modifier keys
|
|
429
|
-
if (["Alt", "Shift", "Control", "Meta"].includes(event.key)) return undefined;
|
|
430
|
-
|
|
431
|
-
const binding: ShortcutBinding = { key: event.key };
|
|
432
|
-
if (event.altKey) binding.alt = true;
|
|
433
|
-
if (event.metaKey) binding.meta = true;
|
|
434
|
-
if (event.shiftKey) binding.shift = true;
|
|
435
|
-
|
|
436
|
-
return binding;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Check if two bindings are equal.
|
|
441
|
-
*/
|
|
442
|
-
export function bindingsEqual(
|
|
443
|
-
a: ShortcutBinding,
|
|
444
|
-
b: ShortcutBinding,
|
|
445
|
-
): boolean {
|
|
446
|
-
return (
|
|
447
|
-
a.key === b.key &&
|
|
448
|
-
(a.alt ?? false) === (b.alt ?? false) &&
|
|
449
|
-
(a.meta ?? false) === (b.meta ?? false) &&
|
|
450
|
-
(a.shift ?? false) === (b.shift ?? false)
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
**Step 4: Run tests to verify they pass**
|
|
456
|
-
|
|
457
|
-
Run: `bun run test src/lib/shortcut-registry.test.ts`
|
|
458
|
-
Expected: PASS — all tests green
|
|
459
|
-
|
|
460
|
-
**Step 5: Commit**
|
|
461
|
-
|
|
462
|
-
```bash
|
|
463
|
-
git add src/lib/shortcut-registry.ts src/lib/shortcut-registry.test.ts
|
|
464
|
-
git commit -m "feat: add shortcut registry with definitions, matcher, and formatter"
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
---
|
|
468
|
-
|
|
469
|
-
### Task 3: useKeybindings Hook — State Management & Persistence
|
|
470
|
-
|
|
471
|
-
**Files:**
|
|
472
|
-
- Create: `src/hooks/useKeybindings.ts`
|
|
473
|
-
|
|
474
|
-
**Step 1: Write the hook**
|
|
475
|
-
|
|
476
|
-
Create `src/hooks/useKeybindings.ts`:
|
|
477
|
-
|
|
478
|
-
```ts
|
|
479
|
-
import { useCallback, useEffect, useState } from "react";
|
|
480
|
-
import { toast } from "sonner";
|
|
481
|
-
import {
|
|
482
|
-
type ShortcutDefinition,
|
|
483
|
-
resolveShortcuts,
|
|
484
|
-
} from "../lib/shortcut-registry";
|
|
485
|
-
import type { KeybindingOverride, ShortcutBinding } from "../types";
|
|
486
|
-
|
|
487
|
-
interface UseKeybindingsResult {
|
|
488
|
-
shortcuts: ShortcutDefinition[];
|
|
489
|
-
updateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
|
|
490
|
-
toggleEnabled: (id: string) => Promise<void>;
|
|
491
|
-
resetToDefaults: () => Promise<void>;
|
|
492
|
-
isLoading: boolean;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
export function useKeybindings(filePath: string | null): UseKeybindingsResult {
|
|
496
|
-
const [overrides, setOverrides] = useState<KeybindingOverride[]>([]);
|
|
497
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
498
|
-
|
|
499
|
-
// Fetch keybindings from settings on mount
|
|
500
|
-
useEffect(() => {
|
|
501
|
-
if (!filePath) {
|
|
502
|
-
setIsLoading(false);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const fetchKeybindings = async () => {
|
|
507
|
-
try {
|
|
508
|
-
const response = await fetch("/api/settings");
|
|
509
|
-
if (response.ok) {
|
|
510
|
-
const settings = await response.json();
|
|
511
|
-
setOverrides(settings.keybindings ?? []);
|
|
512
|
-
}
|
|
513
|
-
} catch (err) {
|
|
514
|
-
console.error("Failed to fetch keybindings:", err);
|
|
515
|
-
} finally {
|
|
516
|
-
setIsLoading(false);
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
fetchKeybindings();
|
|
521
|
-
}, [filePath]);
|
|
522
|
-
|
|
523
|
-
const persistOverrides = useCallback(
|
|
524
|
-
async (newOverrides: KeybindingOverride[]) => {
|
|
525
|
-
try {
|
|
526
|
-
const response = await fetch("/api/settings");
|
|
527
|
-
if (!response.ok) return;
|
|
528
|
-
|
|
529
|
-
const currentSettings = await response.json();
|
|
530
|
-
const updated = { ...currentSettings, keybindings: newOverrides };
|
|
531
|
-
|
|
532
|
-
const putResponse = await fetch("/api/settings", {
|
|
533
|
-
method: "PUT",
|
|
534
|
-
headers: { "Content-Type": "application/json" },
|
|
535
|
-
body: JSON.stringify(updated),
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
if (!putResponse.ok) {
|
|
539
|
-
throw new Error("Failed to save keybindings");
|
|
540
|
-
}
|
|
541
|
-
} catch (err) {
|
|
542
|
-
console.error("Failed to save keybindings:", err);
|
|
543
|
-
toast.error("Failed to save keybindings");
|
|
544
|
-
}
|
|
545
|
-
},
|
|
546
|
-
[],
|
|
547
|
-
);
|
|
548
|
-
|
|
549
|
-
const updateBinding = useCallback(
|
|
550
|
-
async (id: string, binding: ShortcutBinding) => {
|
|
551
|
-
const newOverrides = overrides.filter((o) => o.id !== id);
|
|
552
|
-
newOverrides.push({ id, binding, enabled: true });
|
|
553
|
-
|
|
554
|
-
setOverrides(newOverrides);
|
|
555
|
-
await persistOverrides(newOverrides);
|
|
556
|
-
},
|
|
557
|
-
[overrides, persistOverrides],
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
const toggleEnabled = useCallback(
|
|
561
|
-
async (id: string) => {
|
|
562
|
-
const existing = overrides.find((o) => o.id === id);
|
|
563
|
-
const currentEnabled = existing?.enabled ?? true;
|
|
564
|
-
const newOverrides = overrides.filter((o) => o.id !== id);
|
|
565
|
-
newOverrides.push({
|
|
566
|
-
id,
|
|
567
|
-
binding: existing?.binding,
|
|
568
|
-
enabled: !currentEnabled,
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
setOverrides(newOverrides);
|
|
572
|
-
await persistOverrides(newOverrides);
|
|
573
|
-
},
|
|
574
|
-
[overrides, persistOverrides],
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
const resetToDefaults = useCallback(async () => {
|
|
578
|
-
setOverrides([]);
|
|
579
|
-
await persistOverrides([]);
|
|
580
|
-
toast.success("Keyboard shortcuts reset to defaults");
|
|
581
|
-
}, [persistOverrides]);
|
|
582
|
-
|
|
583
|
-
const shortcuts = resolveShortcuts(overrides);
|
|
584
|
-
|
|
585
|
-
return {
|
|
586
|
-
shortcuts,
|
|
587
|
-
updateBinding,
|
|
588
|
-
toggleEnabled,
|
|
589
|
-
resetToDefaults,
|
|
590
|
-
isLoading,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
**Step 2: Run typecheck**
|
|
596
|
-
|
|
597
|
-
Run: `bun run typecheck`
|
|
598
|
-
Expected: PASS
|
|
599
|
-
|
|
600
|
-
**Step 3: Commit**
|
|
601
|
-
|
|
602
|
-
```bash
|
|
603
|
-
git add src/hooks/useKeybindings.ts
|
|
604
|
-
git commit -m "feat: add useKeybindings hook for shortcut state and persistence"
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
---
|
|
608
|
-
|
|
609
|
-
### Task 4: Server — Accept keybindings in Settings API
|
|
610
|
-
|
|
611
|
-
**Files:**
|
|
612
|
-
- Modify: `src/server/index.ts`
|
|
613
|
-
|
|
614
|
-
**Step 1: Update the `updateSettings` function**
|
|
615
|
-
|
|
616
|
-
In `src/server/index.ts`, change `updateSettings` (lines 399-421) to accept and persist `keybindings`:
|
|
617
|
-
|
|
618
|
-
Replace the existing `updateSettings` function:
|
|
619
|
-
|
|
620
|
-
```ts
|
|
621
|
-
async function updateSettings(
|
|
622
|
-
ctx: RouteContext,
|
|
623
|
-
req: Request,
|
|
624
|
-
): Promise<Response> {
|
|
625
|
-
try {
|
|
626
|
-
const body = await req.json();
|
|
627
|
-
const { fontFamily, keybindings } = body;
|
|
628
|
-
|
|
629
|
-
if (fontFamily !== undefined && !isValidFontFamily(fontFamily)) {
|
|
630
|
-
return errorResponse("Invalid font family", 400);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Read current settings and merge
|
|
634
|
-
const current = await readSettingsFromFile(ctx.filePath);
|
|
635
|
-
const settings: DocumentSettings = {
|
|
636
|
-
...current,
|
|
637
|
-
...(fontFamily !== undefined && { fontFamily }),
|
|
638
|
-
...(keybindings !== undefined && { keybindings }),
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
await writeSettingsToFile(ctx.filePath, settings);
|
|
642
|
-
return json(settings);
|
|
643
|
-
} catch (err) {
|
|
644
|
-
console.error("Failed to save settings:", err);
|
|
645
|
-
return errorResponse("Failed to save settings", 500);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
Also import `DocumentSettings` at the top if not already imported. Check existing import at line 19 — it imports `type DocumentSettings` already, so no change needed.
|
|
651
|
-
|
|
652
|
-
**Step 2: Run typecheck**
|
|
653
|
-
|
|
654
|
-
Run: `bun run typecheck`
|
|
655
|
-
Expected: PASS
|
|
656
|
-
|
|
657
|
-
**Step 3: Commit**
|
|
658
|
-
|
|
659
|
-
```bash
|
|
660
|
-
git add src/server/index.ts
|
|
661
|
-
git commit -m "feat: accept keybindings in settings API"
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
---
|
|
665
|
-
|
|
666
|
-
### Task 5: useKeyboardShortcuts Hook — Centralized Key Listener
|
|
667
|
-
|
|
668
|
-
**Files:**
|
|
669
|
-
- Create: `src/hooks/useKeyboardShortcuts.ts`
|
|
670
|
-
|
|
671
|
-
**Step 1: Write the hook**
|
|
672
|
-
|
|
673
|
-
Create `src/hooks/useKeyboardShortcuts.ts`:
|
|
674
|
-
|
|
675
|
-
```ts
|
|
676
|
-
import { useEffect, useRef } from "react";
|
|
677
|
-
import {
|
|
678
|
-
type ShortcutAction,
|
|
679
|
-
type ShortcutDefinition,
|
|
680
|
-
matchesBinding,
|
|
681
|
-
} from "../lib/shortcut-registry";
|
|
682
|
-
|
|
683
|
-
type ActionMap = Partial<Record<ShortcutAction, () => void>>;
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Returns true if the event target is an input element where
|
|
687
|
-
* keyboard shortcuts should be suppressed.
|
|
688
|
-
*/
|
|
689
|
-
function isInputFocused(event: KeyboardEvent): boolean {
|
|
690
|
-
const target = event.target;
|
|
691
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
692
|
-
|
|
693
|
-
const tagName = target.tagName;
|
|
694
|
-
if (tagName === "INPUT" || tagName === "TEXTAREA") return true;
|
|
695
|
-
if (target.isContentEditable) return true;
|
|
696
|
-
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Single centralized keyboard shortcut listener.
|
|
702
|
-
* Replaces all scattered useEffect keydown handlers.
|
|
703
|
-
*
|
|
704
|
-
* @param shortcuts - Resolved shortcut definitions (from useKeybindings)
|
|
705
|
-
* @param actions - Map of shortcut ID to callback function
|
|
706
|
-
*/
|
|
707
|
-
export function useKeyboardShortcuts(
|
|
708
|
-
shortcuts: ShortcutDefinition[],
|
|
709
|
-
actions: ActionMap,
|
|
710
|
-
): void {
|
|
711
|
-
const actionsRef = useRef(actions);
|
|
712
|
-
actionsRef.current = actions;
|
|
713
|
-
|
|
714
|
-
const shortcutsRef = useRef(shortcuts);
|
|
715
|
-
shortcutsRef.current = shortcuts;
|
|
716
|
-
|
|
717
|
-
useEffect(() => {
|
|
718
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
719
|
-
if (isInputFocused(event)) return;
|
|
720
|
-
|
|
721
|
-
for (const shortcut of shortcutsRef.current) {
|
|
722
|
-
if (!shortcut.enabled) continue;
|
|
723
|
-
|
|
724
|
-
if (matchesBinding(event, shortcut.binding)) {
|
|
725
|
-
const action = actionsRef.current[shortcut.id];
|
|
726
|
-
if (action) {
|
|
727
|
-
event.preventDefault();
|
|
728
|
-
action();
|
|
729
|
-
}
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
736
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
737
|
-
}, []);
|
|
738
|
-
}
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
**Step 2: Run typecheck**
|
|
742
|
-
|
|
743
|
-
Run: `bun run typecheck`
|
|
744
|
-
Expected: PASS
|
|
745
|
-
|
|
746
|
-
**Step 3: Commit**
|
|
747
|
-
|
|
748
|
-
```bash
|
|
749
|
-
git add src/hooks/useKeyboardShortcuts.ts
|
|
750
|
-
git commit -m "feat: add centralized useKeyboardShortcuts hook"
|
|
751
|
-
```
|
|
752
|
-
|
|
753
|
-
---
|
|
754
|
-
|
|
755
|
-
### Task 6: Wire Up in App.tsx — Connect Hook to Actions
|
|
756
|
-
|
|
757
|
-
**Files:**
|
|
758
|
-
- Modify: `src/App.tsx`
|
|
759
|
-
- Modify: `src/hooks/useClipboard.ts`
|
|
760
|
-
- Modify: `src/hooks/useCommentNavigation.ts`
|
|
761
|
-
|
|
762
|
-
**Step 1: Remove old keyboard handlers from `useClipboard.ts`**
|
|
763
|
-
|
|
764
|
-
In `src/hooks/useClipboard.ts`, delete lines 72-103 (the refs and the useEffect keydown block). Also remove `useEffect` and `useRef` from the react import if no longer used. The hook should only export the action callbacks.
|
|
765
|
-
|
|
766
|
-
The updated hook becomes:
|
|
767
|
-
|
|
768
|
-
```ts
|
|
769
|
-
import { useCallback } from "react";
|
|
770
|
-
import { toast } from "sonner";
|
|
771
|
-
import { extractContext, formatForLLM } from "../lib/context";
|
|
772
|
-
import {
|
|
773
|
-
exportCommentsAsJson,
|
|
774
|
-
generatePrompt,
|
|
775
|
-
generateRawText,
|
|
776
|
-
} from "../lib/export";
|
|
777
|
-
import { truncate } from "../lib/utils";
|
|
778
|
-
import type { Comment, Document, Selection } from "../types";
|
|
779
|
-
|
|
780
|
-
interface UseClipboardParams {
|
|
781
|
-
comments: Comment[];
|
|
782
|
-
document: Document | undefined;
|
|
783
|
-
selection: Selection | undefined;
|
|
784
|
-
clearSelection: () => void;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
export function useClipboard({
|
|
788
|
-
comments,
|
|
789
|
-
document,
|
|
790
|
-
selection,
|
|
791
|
-
clearSelection,
|
|
792
|
-
}: UseClipboardParams) {
|
|
793
|
-
const copyAll = useCallback(() => {
|
|
794
|
-
if (!document) return;
|
|
795
|
-
const prompt = generatePrompt(comments, document.fileName);
|
|
796
|
-
navigator.clipboard.writeText(prompt);
|
|
797
|
-
toast.success("Copied all comments");
|
|
798
|
-
}, [comments, document]);
|
|
799
|
-
|
|
800
|
-
const copyAllRaw = useCallback(() => {
|
|
801
|
-
if (!document) return;
|
|
802
|
-
const raw = generateRawText(comments);
|
|
803
|
-
navigator.clipboard.writeText(raw);
|
|
804
|
-
toast.success("Copied all comments as raw text");
|
|
805
|
-
}, [comments, document]);
|
|
806
|
-
|
|
807
|
-
const exportJson = useCallback(() => {
|
|
808
|
-
if (!document) return;
|
|
809
|
-
exportCommentsAsJson(comments, document);
|
|
810
|
-
}, [comments, document]);
|
|
811
|
-
|
|
812
|
-
const copySelectionRaw = useCallback(() => {
|
|
813
|
-
if (!selection) return;
|
|
814
|
-
navigator.clipboard.writeText(selection.text);
|
|
815
|
-
toast.success(`Copied: "${truncate(selection.text)}"`);
|
|
816
|
-
clearSelection();
|
|
817
|
-
}, [selection, clearSelection]);
|
|
818
|
-
|
|
819
|
-
const copySelectionForLLM = useCallback(() => {
|
|
820
|
-
if (!selection || !document) return;
|
|
821
|
-
const context = extractContext({
|
|
822
|
-
content: document.content,
|
|
823
|
-
startOffset: selection.startOffset,
|
|
824
|
-
endOffset: selection.endOffset,
|
|
825
|
-
});
|
|
826
|
-
const formatted = formatForLLM({
|
|
827
|
-
context,
|
|
828
|
-
fileName: document.fileName,
|
|
829
|
-
});
|
|
830
|
-
navigator.clipboard.writeText(formatted);
|
|
831
|
-
toast.success(`Copied for LLM: "${truncate(selection.text)}"`);
|
|
832
|
-
clearSelection();
|
|
833
|
-
}, [selection, document, clearSelection]);
|
|
834
|
-
|
|
835
|
-
return {
|
|
836
|
-
copyAll,
|
|
837
|
-
copyAllRaw,
|
|
838
|
-
exportJson,
|
|
839
|
-
copySelectionRaw,
|
|
840
|
-
copySelectionForLLM,
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
**Step 2: Remove old keyboard handlers from `useCommentNavigation.ts`**
|
|
846
|
-
|
|
847
|
-
In `src/hooks/useCommentNavigation.ts`, delete lines 119-136 (the keyboard navigation useEffect). Keep the `useEffect` import since it's still used for cleanup on line 31. Remove the `sortedComments.length` dependency from nowhere since the removed effect was the only consumer.
|
|
848
|
-
|
|
849
|
-
The file should look the same but without the `// Keyboard navigation: Alt+↑/↓` useEffect block.
|
|
850
|
-
|
|
851
|
-
**Step 3: Wire up `useKeyboardShortcuts` in `App.tsx`**
|
|
852
|
-
|
|
853
|
-
Add imports at the top of `src/App.tsx`:
|
|
854
|
-
|
|
855
|
-
```ts
|
|
856
|
-
import { useKeybindings } from "./hooks/useKeybindings";
|
|
857
|
-
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
|
858
|
-
import { ShortcutActions } from "./lib/shortcut-registry";
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
In `AppContent`, after the existing `useClipboard` call (~line 71), add:
|
|
862
|
-
|
|
863
|
-
```ts
|
|
864
|
-
const { document: doc } = useDocument();
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
Wait — `document` is already destructured. We need access to `filePath` for `useKeybindings`. The `LayoutProvider` wraps `AppContent` and already has `filePath`. We can either:
|
|
868
|
-
- Pass it as a prop, or
|
|
869
|
-
- Access it from `CommentContext` (which already has it)
|
|
870
|
-
|
|
871
|
-
Looking at the code, `CommentProvider` receives `filePath`. Let's get it from the existing context. Check `CommentContext` — it has `filePath` available on the provider props but may not expose it. Let's use the `document.filePath` that's already available from `useDocument()`.
|
|
872
|
-
|
|
873
|
-
In `AppContent`, after `useClipboard` (line 71), add:
|
|
874
|
-
|
|
875
|
-
```ts
|
|
876
|
-
const { shortcuts } = useKeybindings(document?.filePath ?? null);
|
|
877
|
-
|
|
878
|
-
const { navigatePrevious, navigateNext } = use(CommentContext)!;
|
|
879
|
-
|
|
880
|
-
useKeyboardShortcuts(shortcuts, {
|
|
881
|
-
[ShortcutActions.COPY_ALL]: copyAll,
|
|
882
|
-
[ShortcutActions.COPY_ALL_RAW]: copyAllRaw,
|
|
883
|
-
[ShortcutActions.NAVIGATE_NEXT]: navigateNext,
|
|
884
|
-
[ShortcutActions.NAVIGATE_PREVIOUS]: navigatePrevious,
|
|
885
|
-
[ShortcutActions.COPY_SELECTION_RAW]: copySelectionRaw,
|
|
886
|
-
[ShortcutActions.COPY_SELECTION_LLM]: copySelectionForLLM,
|
|
887
|
-
[ShortcutActions.CLEAR_SELECTION]: clearSelection,
|
|
888
|
-
});
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
Note: `navigatePrevious` and `navigateNext` come from `CommentContext` — check if they're already destructured. Looking at line 37-46, the destructured values from `CommentContext` do NOT include `navigatePrevious` / `navigateNext`. But `CommentNav` uses them via its own `use(CommentContext)`. We need to add them to the destructure at line 37.
|
|
892
|
-
|
|
893
|
-
Update the destructure at line 37:
|
|
894
|
-
|
|
895
|
-
```ts
|
|
896
|
-
const {
|
|
897
|
-
comments,
|
|
898
|
-
sortedComments,
|
|
899
|
-
addComment,
|
|
900
|
-
reanchorComment,
|
|
901
|
-
reanchorTarget,
|
|
902
|
-
cancelReanchor,
|
|
903
|
-
hoveredCommentId,
|
|
904
|
-
setHoveredCommentId,
|
|
905
|
-
navigatePrevious,
|
|
906
|
-
navigateNext,
|
|
907
|
-
} = use(CommentContext)!;
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
**Step 4: Run typecheck**
|
|
911
|
-
|
|
912
|
-
Run: `bun run typecheck`
|
|
913
|
-
Expected: PASS
|
|
914
|
-
|
|
915
|
-
**Step 5: Run dev server and manually test**
|
|
916
|
-
|
|
917
|
-
Run: `bun dev`
|
|
918
|
-
|
|
919
|
-
Test:
|
|
920
|
-
- `Alt+↓` / `Alt+↑` still navigate comments
|
|
921
|
-
- `Alt+C` copies all comments (new!)
|
|
922
|
-
- `Alt+Shift+C` copies all raw (new!)
|
|
923
|
-
- `⌘+C` / `⌘+Shift+C` copy selection when text is selected
|
|
924
|
-
- `Escape` clears selection
|
|
925
|
-
|
|
926
|
-
**Step 6: Commit**
|
|
927
|
-
|
|
928
|
-
```bash
|
|
929
|
-
git add src/App.tsx src/hooks/useClipboard.ts src/hooks/useCommentNavigation.ts
|
|
930
|
-
git commit -m "feat: wire centralized keyboard shortcuts, remove scattered handlers"
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
---
|
|
934
|
-
|
|
935
|
-
### Task 7: ShortcutCapture Component
|
|
936
|
-
|
|
937
|
-
**Files:**
|
|
938
|
-
- Create: `src/components/ShortcutCapture.tsx`
|
|
939
|
-
|
|
940
|
-
**Step 1: Write the component**
|
|
941
|
-
|
|
942
|
-
Create `src/components/ShortcutCapture.tsx`:
|
|
943
|
-
|
|
944
|
-
```tsx
|
|
945
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
946
|
-
import {
|
|
947
|
-
type ShortcutBinding,
|
|
948
|
-
eventToBinding,
|
|
949
|
-
formatBinding,
|
|
950
|
-
isReservedBinding,
|
|
951
|
-
} from "../lib/shortcut-registry";
|
|
952
|
-
|
|
953
|
-
interface ShortcutCaptureProps {
|
|
954
|
-
currentBinding: ShortcutBinding;
|
|
955
|
-
onCapture: (binding: ShortcutBinding) => void;
|
|
956
|
-
onCancel: () => void;
|
|
957
|
-
isMac: boolean;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
export function ShortcutCapture({
|
|
961
|
-
currentBinding,
|
|
962
|
-
onCapture,
|
|
963
|
-
onCancel,
|
|
964
|
-
isMac,
|
|
965
|
-
}: ShortcutCaptureProps) {
|
|
966
|
-
const capturedRef = useRef<ShortcutBinding | undefined>();
|
|
967
|
-
|
|
968
|
-
const handleKeyDown = useCallback(
|
|
969
|
-
(e: KeyboardEvent) => {
|
|
970
|
-
e.preventDefault();
|
|
971
|
-
e.stopPropagation();
|
|
972
|
-
|
|
973
|
-
if (e.key === "Escape") {
|
|
974
|
-
onCancel();
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
const binding = eventToBinding(e);
|
|
979
|
-
if (!binding) return;
|
|
980
|
-
|
|
981
|
-
if (isReservedBinding(binding)) {
|
|
982
|
-
return; // Silently ignore reserved bindings
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
capturedRef.current = binding;
|
|
986
|
-
onCapture(binding);
|
|
987
|
-
},
|
|
988
|
-
[onCapture, onCancel],
|
|
989
|
-
);
|
|
990
|
-
|
|
991
|
-
useEffect(() => {
|
|
992
|
-
window.addEventListener("keydown", handleKeyDown, { capture: true });
|
|
993
|
-
return () =>
|
|
994
|
-
window.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
995
|
-
}, [handleKeyDown]);
|
|
996
|
-
|
|
997
|
-
return (
|
|
998
|
-
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-amber-50 border border-amber-200 text-amber-700 text-xs font-medium animate-pulse">
|
|
999
|
-
Press keys...
|
|
1000
|
-
</span>
|
|
1001
|
-
);
|
|
1002
|
-
}
|
|
1003
|
-
```
|
|
1004
|
-
|
|
1005
|
-
**Step 2: Run typecheck**
|
|
1006
|
-
|
|
1007
|
-
Run: `bun run typecheck`
|
|
1008
|
-
Expected: PASS
|
|
1009
|
-
|
|
1010
|
-
**Step 3: Commit**
|
|
1011
|
-
|
|
1012
|
-
```bash
|
|
1013
|
-
git add src/components/ShortcutCapture.tsx
|
|
1014
|
-
git commit -m "feat: add ShortcutCapture component for key rebinding"
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
---
|
|
1018
|
-
|
|
1019
|
-
### Task 8: ShortcutList Component
|
|
1020
|
-
|
|
1021
|
-
**Files:**
|
|
1022
|
-
- Create: `src/components/ShortcutList.tsx`
|
|
1023
|
-
|
|
1024
|
-
**Step 1: Write the component**
|
|
1025
|
-
|
|
1026
|
-
Create `src/components/ShortcutList.tsx`:
|
|
1027
|
-
|
|
1028
|
-
```tsx
|
|
1029
|
-
import { useCallback, useMemo, useState } from "react";
|
|
1030
|
-
import {
|
|
1031
|
-
type ShortcutBinding,
|
|
1032
|
-
type ShortcutDefinition,
|
|
1033
|
-
bindingsEqual,
|
|
1034
|
-
formatBinding,
|
|
1035
|
-
} from "../lib/shortcut-registry";
|
|
1036
|
-
import { ShortcutCapture } from "./ShortcutCapture";
|
|
1037
|
-
|
|
1038
|
-
interface ShortcutListProps {
|
|
1039
|
-
shortcuts: ShortcutDefinition[];
|
|
1040
|
-
onUpdateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
|
|
1041
|
-
onToggleEnabled: (id: string) => Promise<void>;
|
|
1042
|
-
onResetToDefaults: () => Promise<void>;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
const isMac =
|
|
1046
|
-
typeof navigator !== "undefined" &&
|
|
1047
|
-
/Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
1048
|
-
|
|
1049
|
-
export function ShortcutList({
|
|
1050
|
-
shortcuts,
|
|
1051
|
-
onUpdateBinding,
|
|
1052
|
-
onToggleEnabled,
|
|
1053
|
-
onResetToDefaults,
|
|
1054
|
-
}: ShortcutListProps) {
|
|
1055
|
-
const [capturingId, setCapturingId] = useState<string | undefined>();
|
|
1056
|
-
|
|
1057
|
-
const hasOverrides = useMemo(
|
|
1058
|
-
() =>
|
|
1059
|
-
shortcuts.some(
|
|
1060
|
-
(s) => !s.enabled || !bindingsEqual(s.binding, s.defaultBinding),
|
|
1061
|
-
),
|
|
1062
|
-
[shortcuts],
|
|
1063
|
-
);
|
|
1064
|
-
|
|
1065
|
-
const handleCapture = useCallback(
|
|
1066
|
-
async (id: string, binding: ShortcutBinding) => {
|
|
1067
|
-
// Check for conflicts
|
|
1068
|
-
const conflict = shortcuts.find(
|
|
1069
|
-
(s) => s.id !== id && s.enabled && bindingsEqual(s.binding, binding),
|
|
1070
|
-
);
|
|
1071
|
-
|
|
1072
|
-
if (conflict) {
|
|
1073
|
-
// Swap bindings
|
|
1074
|
-
const currentShortcut = shortcuts.find((s) => s.id === id);
|
|
1075
|
-
if (currentShortcut) {
|
|
1076
|
-
await onUpdateBinding(conflict.id, currentShortcut.binding);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
await onUpdateBinding(id, binding);
|
|
1081
|
-
setCapturingId(undefined);
|
|
1082
|
-
},
|
|
1083
|
-
[shortcuts, onUpdateBinding],
|
|
1084
|
-
);
|
|
1085
|
-
|
|
1086
|
-
return (
|
|
1087
|
-
<div className="space-y-3">
|
|
1088
|
-
<div className="space-y-1">
|
|
1089
|
-
{shortcuts.map((shortcut) => (
|
|
1090
|
-
<div
|
|
1091
|
-
key={shortcut.id}
|
|
1092
|
-
className="flex items-center gap-3 py-1.5 group"
|
|
1093
|
-
>
|
|
1094
|
-
<input
|
|
1095
|
-
type="checkbox"
|
|
1096
|
-
checked={shortcut.enabled}
|
|
1097
|
-
onChange={() => onToggleEnabled(shortcut.id)}
|
|
1098
|
-
className="w-3.5 h-3.5 rounded border-zinc-300 text-zinc-600 focus:ring-zinc-500 cursor-pointer"
|
|
1099
|
-
/>
|
|
1100
|
-
|
|
1101
|
-
<span className="flex-1 text-sm text-zinc-700 truncate">
|
|
1102
|
-
{shortcut.label}
|
|
1103
|
-
</span>
|
|
1104
|
-
|
|
1105
|
-
<div className="flex items-center gap-1.5 min-w-[100px] justify-end">
|
|
1106
|
-
{capturingId === shortcut.id ? (
|
|
1107
|
-
<ShortcutCapture
|
|
1108
|
-
currentBinding={shortcut.binding}
|
|
1109
|
-
onCapture={(binding) => handleCapture(shortcut.id, binding)}
|
|
1110
|
-
onCancel={() => setCapturingId(undefined)}
|
|
1111
|
-
isMac={isMac}
|
|
1112
|
-
/>
|
|
1113
|
-
) : (
|
|
1114
|
-
<>
|
|
1115
|
-
<kbd className="inline-flex items-center px-1.5 py-0.5 rounded bg-zinc-100 border border-zinc-200 text-zinc-600 text-xs font-mono">
|
|
1116
|
-
{formatBinding(shortcut.binding, isMac)}
|
|
1117
|
-
</kbd>
|
|
1118
|
-
<button
|
|
1119
|
-
type="button"
|
|
1120
|
-
onClick={() => setCapturingId(shortcut.id)}
|
|
1121
|
-
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 text-zinc-400 hover:text-zinc-600"
|
|
1122
|
-
title="Edit shortcut"
|
|
1123
|
-
>
|
|
1124
|
-
<svg
|
|
1125
|
-
className="w-3 h-3"
|
|
1126
|
-
fill="none"
|
|
1127
|
-
viewBox="0 0 24 24"
|
|
1128
|
-
strokeWidth={2}
|
|
1129
|
-
stroke="currentColor"
|
|
1130
|
-
>
|
|
1131
|
-
<path
|
|
1132
|
-
strokeLinecap="round"
|
|
1133
|
-
strokeLinejoin="round"
|
|
1134
|
-
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
|
|
1135
|
-
/>
|
|
1136
|
-
</svg>
|
|
1137
|
-
</button>
|
|
1138
|
-
</>
|
|
1139
|
-
)}
|
|
1140
|
-
</div>
|
|
1141
|
-
</div>
|
|
1142
|
-
))}
|
|
1143
|
-
</div>
|
|
1144
|
-
|
|
1145
|
-
{hasOverrides && (
|
|
1146
|
-
<button
|
|
1147
|
-
type="button"
|
|
1148
|
-
onClick={onResetToDefaults}
|
|
1149
|
-
className="text-xs text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
1150
|
-
>
|
|
1151
|
-
Reset to defaults
|
|
1152
|
-
</button>
|
|
1153
|
-
)}
|
|
1154
|
-
</div>
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
```
|
|
1158
|
-
|
|
1159
|
-
**Step 2: Run typecheck**
|
|
1160
|
-
|
|
1161
|
-
Run: `bun run typecheck`
|
|
1162
|
-
Expected: PASS
|
|
1163
|
-
|
|
1164
|
-
**Step 3: Commit**
|
|
1165
|
-
|
|
1166
|
-
```bash
|
|
1167
|
-
git add src/components/ShortcutList.tsx
|
|
1168
|
-
git commit -m "feat: add ShortcutList component for shortcut editor UI"
|
|
1169
|
-
```
|
|
1170
|
-
|
|
1171
|
-
---
|
|
1172
|
-
|
|
1173
|
-
### Task 9: Settings Modal — Add Keyboard Shortcuts Section
|
|
1174
|
-
|
|
1175
|
-
**Files:**
|
|
1176
|
-
- Modify: `src/components/SettingsModal.tsx`
|
|
1177
|
-
- Modify: `src/contexts/LayoutContext.tsx`
|
|
1178
|
-
|
|
1179
|
-
**Step 1: Expose keybindings from LayoutContext**
|
|
1180
|
-
|
|
1181
|
-
The `LayoutProvider` already uses `useFontPreference`. We need to also call `useKeybindings` and expose its values. Update `src/contexts/LayoutContext.tsx`:
|
|
1182
|
-
|
|
1183
|
-
```ts
|
|
1184
|
-
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
1185
|
-
import { useFontPreference } from "../hooks/useFontPreference";
|
|
1186
|
-
import { useKeybindings } from "../hooks/useKeybindings";
|
|
1187
|
-
import { useLayoutMode } from "../hooks/useLayoutMode";
|
|
1188
|
-
import type { ShortcutDefinition } from "../lib/shortcut-registry";
|
|
1189
|
-
import type { FontFamily, ShortcutBinding } from "../types";
|
|
1190
|
-
|
|
1191
|
-
interface LayoutContextValue {
|
|
1192
|
-
isFullscreen: boolean;
|
|
1193
|
-
toggleLayoutMode: () => void;
|
|
1194
|
-
fontFamily: FontFamily;
|
|
1195
|
-
setFontFamily: (font: FontFamily) => Promise<void>;
|
|
1196
|
-
shortcuts: ShortcutDefinition[];
|
|
1197
|
-
updateBinding: (id: string, binding: ShortcutBinding) => Promise<void>;
|
|
1198
|
-
toggleShortcutEnabled: (id: string) => Promise<void>;
|
|
1199
|
-
resetShortcutsToDefaults: () => Promise<void>;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
export const LayoutContext = createContext<LayoutContextValue | null>(null);
|
|
1203
|
-
|
|
1204
|
-
export function useLayoutContext(): LayoutContextValue {
|
|
1205
|
-
const value = use(LayoutContext);
|
|
1206
|
-
if (!value) {
|
|
1207
|
-
throw new Error("useLayoutContext must be used within a LayoutProvider");
|
|
1208
|
-
}
|
|
1209
|
-
return value;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
interface LayoutProviderProps {
|
|
1213
|
-
filePath: string;
|
|
1214
|
-
children: ReactNode;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
export function LayoutProvider({ filePath, children }: LayoutProviderProps) {
|
|
1218
|
-
const { isFullscreen, toggleLayoutMode } = useLayoutMode();
|
|
1219
|
-
const { fontFamily, setFontFamily } = useFontPreference(filePath);
|
|
1220
|
-
const {
|
|
1221
|
-
shortcuts,
|
|
1222
|
-
updateBinding,
|
|
1223
|
-
toggleEnabled: toggleShortcutEnabled,
|
|
1224
|
-
resetToDefaults: resetShortcutsToDefaults,
|
|
1225
|
-
} = useKeybindings(filePath);
|
|
1226
|
-
|
|
1227
|
-
const value = useMemo<LayoutContextValue>(
|
|
1228
|
-
() => ({
|
|
1229
|
-
isFullscreen,
|
|
1230
|
-
toggleLayoutMode,
|
|
1231
|
-
fontFamily,
|
|
1232
|
-
setFontFamily,
|
|
1233
|
-
shortcuts,
|
|
1234
|
-
updateBinding,
|
|
1235
|
-
toggleShortcutEnabled,
|
|
1236
|
-
resetShortcutsToDefaults,
|
|
1237
|
-
}),
|
|
1238
|
-
[
|
|
1239
|
-
isFullscreen,
|
|
1240
|
-
toggleLayoutMode,
|
|
1241
|
-
fontFamily,
|
|
1242
|
-
setFontFamily,
|
|
1243
|
-
shortcuts,
|
|
1244
|
-
updateBinding,
|
|
1245
|
-
toggleShortcutEnabled,
|
|
1246
|
-
resetShortcutsToDefaults,
|
|
1247
|
-
],
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
return <LayoutContext value={value}>{children}</LayoutContext>;
|
|
1251
|
-
}
|
|
1252
|
-
```
|
|
1253
|
-
|
|
1254
|
-
**Step 2: Update SettingsModal to include shortcuts**
|
|
1255
|
-
|
|
1256
|
-
Replace `src/components/SettingsModal.tsx`:
|
|
1257
|
-
|
|
1258
|
-
```tsx
|
|
1259
|
-
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
1260
|
-
import { cn } from "../lib/utils";
|
|
1261
|
-
import { FontFamilies } from "../types";
|
|
1262
|
-
import { ShortcutList } from "./ShortcutList";
|
|
1263
|
-
import {
|
|
1264
|
-
Dialog,
|
|
1265
|
-
DialogBody,
|
|
1266
|
-
DialogContent,
|
|
1267
|
-
DialogHeader,
|
|
1268
|
-
DialogTitle,
|
|
1269
|
-
} from "./ui/Dialog";
|
|
1270
|
-
import { Text, textVariants } from "./ui/Text";
|
|
1271
|
-
|
|
1272
|
-
interface SettingsModalProps {
|
|
1273
|
-
isOpen: boolean;
|
|
1274
|
-
onClose: () => void;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
1278
|
-
const {
|
|
1279
|
-
fontFamily,
|
|
1280
|
-
setFontFamily,
|
|
1281
|
-
shortcuts,
|
|
1282
|
-
updateBinding,
|
|
1283
|
-
toggleShortcutEnabled,
|
|
1284
|
-
resetShortcutsToDefaults,
|
|
1285
|
-
} = useLayoutContext();
|
|
1286
|
-
|
|
1287
|
-
return (
|
|
1288
|
-
<Dialog
|
|
1289
|
-
open={isOpen}
|
|
1290
|
-
onOpenChange={(open) => {
|
|
1291
|
-
if (!open) onClose();
|
|
1292
|
-
}}
|
|
1293
|
-
>
|
|
1294
|
-
<DialogContent className="max-w-md">
|
|
1295
|
-
<DialogHeader>
|
|
1296
|
-
<DialogTitle>Settings</DialogTitle>
|
|
1297
|
-
</DialogHeader>
|
|
1298
|
-
|
|
1299
|
-
<DialogBody className="space-y-6">
|
|
1300
|
-
<div>
|
|
1301
|
-
<Text variant="overline" asChild>
|
|
1302
|
-
<h3 className="mb-3">Font</h3>
|
|
1303
|
-
</Text>
|
|
1304
|
-
<div className="space-y-2">
|
|
1305
|
-
<label className="flex items-center gap-3 cursor-pointer">
|
|
1306
|
-
<input
|
|
1307
|
-
type="radio"
|
|
1308
|
-
name="fontFamily"
|
|
1309
|
-
value={FontFamilies.SERIF}
|
|
1310
|
-
checked={fontFamily === FontFamilies.SERIF}
|
|
1311
|
-
onChange={() => setFontFamily(FontFamilies.SERIF)}
|
|
1312
|
-
className="w-4 h-4 text-zinc-600 border-zinc-300 focus:ring-zinc-500"
|
|
1313
|
-
/>
|
|
1314
|
-
<Text variant="body" asChild>
|
|
1315
|
-
<span>Serif</span>
|
|
1316
|
-
</Text>
|
|
1317
|
-
</label>
|
|
1318
|
-
<label className="flex items-center gap-3 cursor-pointer">
|
|
1319
|
-
<input
|
|
1320
|
-
type="radio"
|
|
1321
|
-
name="fontFamily"
|
|
1322
|
-
value={FontFamilies.SANS_SERIF}
|
|
1323
|
-
checked={fontFamily === FontFamilies.SANS_SERIF}
|
|
1324
|
-
onChange={() => setFontFamily(FontFamilies.SANS_SERIF)}
|
|
1325
|
-
className="w-4 h-4 text-zinc-600 border-zinc-300 focus:ring-zinc-500"
|
|
1326
|
-
/>
|
|
1327
|
-
<Text variant="body" asChild>
|
|
1328
|
-
<span>Sans-serif</span>
|
|
1329
|
-
</Text>
|
|
1330
|
-
</label>
|
|
1331
|
-
</div>
|
|
1332
|
-
</div>
|
|
1333
|
-
|
|
1334
|
-
<div>
|
|
1335
|
-
<Text variant="overline" asChild>
|
|
1336
|
-
<h3 className="mb-3">Preview</h3>
|
|
1337
|
-
</Text>
|
|
1338
|
-
<div
|
|
1339
|
-
className={cn(
|
|
1340
|
-
textVariants({ variant: "body" }),
|
|
1341
|
-
"p-3 rounded-md border border-zinc-100 leading-relaxed",
|
|
1342
|
-
fontFamily === FontFamilies.SERIF ? "font-serif" : "font-sans",
|
|
1343
|
-
)}
|
|
1344
|
-
>
|
|
1345
|
-
The quick brown fox jumps over the lazy dog. 1234567890
|
|
1346
|
-
</div>
|
|
1347
|
-
</div>
|
|
1348
|
-
|
|
1349
|
-
<div>
|
|
1350
|
-
<Text variant="overline" asChild>
|
|
1351
|
-
<h3 className="mb-3">Keyboard Shortcuts</h3>
|
|
1352
|
-
</Text>
|
|
1353
|
-
<ShortcutList
|
|
1354
|
-
shortcuts={shortcuts}
|
|
1355
|
-
onUpdateBinding={updateBinding}
|
|
1356
|
-
onToggleEnabled={toggleShortcutEnabled}
|
|
1357
|
-
onResetToDefaults={resetShortcutsToDefaults}
|
|
1358
|
-
/>
|
|
1359
|
-
</div>
|
|
1360
|
-
</DialogBody>
|
|
1361
|
-
</DialogContent>
|
|
1362
|
-
</Dialog>
|
|
1363
|
-
);
|
|
1364
|
-
}
|
|
1365
|
-
```
|
|
1366
|
-
|
|
1367
|
-
**Step 3: Update App.tsx to use shortcuts from LayoutContext**
|
|
1368
|
-
|
|
1369
|
-
In `src/App.tsx`, update the `useKeyboardShortcuts` wiring to get shortcuts from `LayoutContext` instead of calling `useKeybindings` directly (since it's now in the provider):
|
|
1370
|
-
|
|
1371
|
-
```ts
|
|
1372
|
-
const { shortcuts } = use(LayoutContext)!;
|
|
1373
|
-
```
|
|
1374
|
-
|
|
1375
|
-
Remove the `useKeybindings` import and direct call from `AppContent`. Keep everything else the same.
|
|
1376
|
-
|
|
1377
|
-
**Step 4: Run typecheck**
|
|
1378
|
-
|
|
1379
|
-
Run: `bun run typecheck`
|
|
1380
|
-
Expected: PASS
|
|
1381
|
-
|
|
1382
|
-
**Step 5: Run dev server and test the full flow**
|
|
1383
|
-
|
|
1384
|
-
Run: `bun dev`
|
|
1385
|
-
|
|
1386
|
-
Test:
|
|
1387
|
-
- Open Settings modal → Keyboard Shortcuts section visible
|
|
1388
|
-
- Toggle a shortcut off → verify it no longer fires
|
|
1389
|
-
- Click edit pencil → "Press keys..." appears → press new combo → binding updates
|
|
1390
|
-
- Verify conflict swap works (rebind to an existing binding)
|
|
1391
|
-
- Click "Reset to defaults" → all bindings reset
|
|
1392
|
-
- Reload page → customizations persist
|
|
1393
|
-
|
|
1394
|
-
**Step 6: Commit**
|
|
1395
|
-
|
|
1396
|
-
```bash
|
|
1397
|
-
git add src/contexts/LayoutContext.tsx src/components/SettingsModal.tsx src/App.tsx
|
|
1398
|
-
git commit -m "feat: add keyboard shortcuts editor to settings modal"
|
|
1399
|
-
```
|
|
1400
|
-
|
|
1401
|
-
---
|
|
1402
|
-
|
|
1403
|
-
### Task 10: Update Tooltip Hints Across Components
|
|
1404
|
-
|
|
1405
|
-
**Files:**
|
|
1406
|
-
- Modify: `src/components/comments/CommentNav.tsx`
|
|
1407
|
-
- Modify: `src/components/MarginNote.tsx`
|
|
1408
|
-
|
|
1409
|
-
**Step 1: Update CommentNav button titles**
|
|
1410
|
-
|
|
1411
|
-
The `CommentNav.tsx` component shows hardcoded shortcut hints in button titles (e.g., `title="Next comment (Alt+↓)"`). These should stay as-is for now since they show defaults. If bindings are customized, the titles will be slightly inaccurate, but this is acceptable for v1. A future improvement could make these dynamic.
|
|
1412
|
-
|
|
1413
|
-
No code changes needed for this step — just a note for future work.
|
|
1414
|
-
|
|
1415
|
-
**Step 2: Update MarginNote copy hints**
|
|
1416
|
-
|
|
1417
|
-
In `src/components/MarginNote.tsx`, the action links show hardcoded shortcut hints:
|
|
1418
|
-
```tsx
|
|
1419
|
-
<ActionLink onClick={handleCopy} title="Copy raw text (⌘C)">
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
|
-
Same as above — acceptable for v1 with hardcoded defaults.
|
|
1423
|
-
|
|
1424
|
-
No code changes needed.
|
|
1425
|
-
|
|
1426
|
-
**Step 3: Commit**
|
|
1427
|
-
|
|
1428
|
-
Skip — no changes.
|
|
1429
|
-
|
|
1430
|
-
---
|
|
1431
|
-
|
|
1432
|
-
### Task 11: Final Verification
|
|
1433
|
-
|
|
1434
|
-
**Step 1: Run full test suite**
|
|
1435
|
-
|
|
1436
|
-
Run: `bun run test`
|
|
1437
|
-
Expected: All tests pass
|
|
1438
|
-
|
|
1439
|
-
**Step 2: Run typecheck**
|
|
1440
|
-
|
|
1441
|
-
Run: `bun run typecheck`
|
|
1442
|
-
Expected: PASS
|
|
1443
|
-
|
|
1444
|
-
**Step 3: Run lint**
|
|
1445
|
-
|
|
1446
|
-
Run: `bun run check`
|
|
1447
|
-
Expected: PASS (fix any issues with `bun run check:fix`)
|
|
1448
|
-
|
|
1449
|
-
**Step 4: Manual smoke test**
|
|
1450
|
-
|
|
1451
|
-
Run: `bun dev` and open a markdown file.
|
|
1452
|
-
|
|
1453
|
-
Verify all shortcuts:
|
|
1454
|
-
- `Alt+C` → copies all comments (AI format)
|
|
1455
|
-
- `Alt+Shift+C` → copies all comments (raw)
|
|
1456
|
-
- `Alt+↓` → next comment
|
|
1457
|
-
- `Alt+↑` → previous comment
|
|
1458
|
-
- Select text → `⌘+C` copies selection
|
|
1459
|
-
- Select text → `⌘+Shift+C` copies for LLM
|
|
1460
|
-
- `Escape` → clears selection
|
|
1461
|
-
- Settings → toggle/rebind/reset all work
|
|
1462
|
-
- Reload → customizations persist
|
|
1463
|
-
|
|
1464
|
-
**Step 5: Commit**
|
|
1465
|
-
|
|
1466
|
-
```bash
|
|
1467
|
-
git add -A
|
|
1468
|
-
git commit -m "chore: lint and format fixes for keyboard shortcuts feature"
|
|
1469
|
-
```
|
|
1470
|
-
|
|
1471
|
-
(Only if there are lint/format changes to commit)
|