@ripla/godd-mcp 1.0.4-canary.9 → 1.0.4

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 (348) hide show
  1. package/README.md +89 -94
  2. package/dist/godd.cjs +349 -233
  3. package/dist/godd.js +2193 -838
  4. package/dist/index.js +152 -122
  5. package/{notes-api → notes/api}/.env.example +6 -1
  6. package/{notes-api → notes/api}/README.md +6 -5
  7. package/{notes-api → notes/api}/app/main.py +29 -2
  8. package/{notes-api → notes/api}/app/routers/auth.py +6 -1
  9. package/{notes-api → notes/api}/app/routers/issues.py +157 -1
  10. package/{notes-api → notes/api}/app/routers/pr.py +2 -2
  11. package/{notes-api → notes/api}/app/services/github_app_token.py +32 -1
  12. package/{notes-api → notes/api}/app/services/github_issues.py +101 -11
  13. package/{notes-api → notes/api}/app/services/github_pr.py +6 -1
  14. package/{notes-api → notes/api}/tests/test_github_app_token.py +49 -1
  15. package/{notes-api → notes/api}/tests/test_github_service.py +42 -0
  16. package/notes/api/tests/test_health.py +52 -0
  17. package/{notes-api → notes/api}/tests/test_issues.py +216 -0
  18. package/{notes-api → notes/api}/tests/test_pr.py +24 -0
  19. package/{notes-api → notes/api}/tests/test_pr_chain.py +19 -0
  20. package/{notes-api → notes/api}/uv.lock +178 -2
  21. package/notes/app/DESIGN.md +273 -0
  22. package/{notes-app → notes/app}/README.md +4 -1
  23. package/notes/app/docker/Dockerfile.e2e +19 -0
  24. package/notes/app/docker/Dockerfile.prod +15 -0
  25. package/notes/app/docker/Dockerfile.test +17 -0
  26. package/notes/app/e2e/file-tree.spec.ts +26 -0
  27. package/notes/app/e2e/file-view.spec.ts +25 -0
  28. package/notes/app/e2e/fixtures.ts +28 -0
  29. package/{notes-app → notes/app}/e2e/global-setup.ts +5 -7
  30. package/notes/app/e2e/login.spec.ts +25 -0
  31. package/notes/app/e2e/pages/login-page.ts +29 -0
  32. package/notes/app/e2e/pages/main-page.ts +29 -0
  33. package/notes/app/e2e/panel-resize-handle.spec.ts +213 -0
  34. package/{notes-app → notes/app}/eslint.config.js +9 -0
  35. package/{notes-app → notes/app}/package.json +0 -3
  36. package/{notes-app → notes/app}/pnpm-workspace.yaml +1 -0
  37. package/notes/app/src/components/CommentPanel.test.tsx +194 -0
  38. package/{notes-app → notes/app}/src/components/CommentPanel.tsx +119 -10
  39. package/notes/app/src/components/CommentSelectionPopover.tsx +26 -0
  40. package/notes/app/src/components/EditToolbar.test.tsx +102 -0
  41. package/{notes-app → notes/app}/src/components/EditToolbar.tsx +5 -54
  42. package/notes/app/src/components/FileInfoPanel.branch-tab.test.tsx +250 -0
  43. package/notes/app/src/components/FileInfoPanel.history-tab.test.tsx +146 -0
  44. package/notes/app/src/components/FileInfoPanel.test.tsx +101 -0
  45. package/{notes-app → notes/app}/src/components/FileInfoPanel.tsx +135 -20
  46. package/notes/app/src/components/IssueCommentThread.test.tsx +93 -0
  47. package/notes/app/src/components/IssueCommentThread.tsx +156 -0
  48. package/notes/app/src/components/IssueDetailView.test.tsx +1118 -0
  49. package/notes/app/src/components/IssueDetailView.tsx +476 -0
  50. package/notes/app/src/components/IssueList.test.tsx +545 -0
  51. package/notes/app/src/components/IssueList.tsx +374 -0
  52. package/{notes-app → notes/app}/src/components/LabelSelector.test.tsx +94 -8
  53. package/notes/app/src/components/LabelSelector.tsx +452 -0
  54. package/{notes-app → notes/app}/src/components/MarkdownEditor.tsx +140 -2
  55. package/notes/app/src/components/PanelResizeHandle.test.tsx +481 -0
  56. package/notes/app/src/components/PanelResizeHandle.tsx +96 -0
  57. package/{notes-app → notes/app}/src/hooks/useEditSession.test.ts +171 -23
  58. package/{notes-app → notes/app}/src/hooks/useEditSession.ts +49 -62
  59. package/notes/app/src/hooks/useMdUpLayout.test.ts +10 -0
  60. package/notes/app/src/hooks/useMdUpLayout.ts +27 -0
  61. package/notes/app/src/hooks/usePeriodicSync.test.ts +244 -0
  62. package/notes/app/src/hooks/usePeriodicSync.ts +83 -0
  63. package/notes/app/src/hooks/useResizablePanelWidth.test.ts +251 -0
  64. package/notes/app/src/hooks/useResizablePanelWidth.ts +167 -0
  65. package/{notes-app → notes/app}/src/lib/api.test.ts +81 -1
  66. package/{notes-app → notes/app}/src/lib/api.ts +98 -4
  67. package/notes/app/src/lib/commentAnchor.test.ts +22 -0
  68. package/notes/app/src/lib/commentAnchor.ts +28 -0
  69. package/notes/app/src/lib/commentHighlight.test.ts +110 -0
  70. package/notes/app/src/lib/commentHighlight.ts +44 -0
  71. package/notes/app/src/lib/github-sync.test.ts +126 -0
  72. package/notes/app/src/lib/github-sync.ts +89 -0
  73. package/notes/app/src/lib/issue-list-utils.test.ts +275 -0
  74. package/notes/app/src/lib/issue-list-utils.ts +135 -0
  75. package/notes/app/src/lib/issue-mobile-layout.test.ts +17 -0
  76. package/notes/app/src/lib/issue-mobile-layout.ts +20 -0
  77. package/notes/app/src/lib/issue-url-sync.test.ts +73 -0
  78. package/notes/app/src/lib/issue-url-sync.ts +43 -0
  79. package/notes/app/src/lib/main-page-default-view.test.ts +37 -0
  80. package/notes/app/src/lib/main-page-default-view.ts +15 -0
  81. package/notes/app/src/lib/resizable-panel.test.ts +199 -0
  82. package/notes/app/src/lib/resizable-panel.ts +136 -0
  83. package/notes/app/src/pages/LoginPage.test.tsx +17 -0
  84. package/notes/app/src/pages/MainPage.branch-modal.test.tsx +226 -0
  85. package/notes/app/src/pages/MainPage.issue-sync.test.tsx +145 -0
  86. package/notes/app/src/pages/MainPage.sidebar.test.tsx +361 -0
  87. package/{notes-app → notes/app}/src/pages/MainPage.tsx +406 -61
  88. package/{notes-app → notes/app}/src/pages/SettingsPage.test.tsx +32 -23
  89. package/{notes-app → notes/app}/src/pages/UserManagementPage.test.tsx +8 -18
  90. package/notes/app/src/test/mocks.ts +156 -0
  91. package/notes/app/src/test/render.tsx +27 -0
  92. package/package.json +3 -5
  93. package/notes-api/tests/test_health.py +0 -11
  94. package/notes-app/docker/Dockerfile.e2e +0 -14
  95. package/notes-app/docker/Dockerfile.prod +0 -12
  96. package/notes-app/docker/Dockerfile.test +0 -12
  97. package/notes-app/e2e/file-tree.spec.ts +0 -29
  98. package/notes-app/e2e/file-view.spec.ts +0 -24
  99. package/notes-app/e2e/login.spec.ts +0 -32
  100. package/notes-app/src/components/IssueDetailView.test.tsx +0 -201
  101. package/notes-app/src/components/IssueDetailView.tsx +0 -318
  102. package/notes-app/src/components/IssueList.test.tsx +0 -93
  103. package/notes-app/src/components/IssueList.tsx +0 -169
  104. package/notes-app/src/components/LabelSelector.tsx +0 -276
  105. package/notes-app/src/pages/LoginPage.test.tsx +0 -29
  106. package/stacks/django-vue.yaml +0 -33
  107. package/stacks/fastapi-react.yaml +0 -160
  108. package/stacks/flutter-firebase.yaml +0 -58
  109. package/stacks/nextjs-prisma.yaml +0 -33
  110. package/stacks/schema.json +0 -144
  111. package/templates/agents/architect.hbs +0 -183
  112. package/templates/agents/auto-documenter.hbs +0 -36
  113. package/templates/agents/backend-developer.hbs +0 -175
  114. package/templates/agents/code-reviewer.hbs +0 -29
  115. package/templates/agents/database-developer.hbs +0 -52
  116. package/templates/agents/debug-specialist.hbs +0 -39
  117. package/templates/agents/environment-setup.hbs +0 -41
  118. package/templates/agents/frontend-developer.hbs +0 -45
  119. package/templates/agents/integration-qa.hbs +0 -26
  120. package/templates/agents/performance-qa.hbs +0 -24
  121. package/templates/agents/pr-writer.hbs +0 -53
  122. package/templates/agents/quality-lead.hbs +0 -26
  123. package/templates/agents/requirements-analyst.hbs +0 -26
  124. package/templates/agents/security-analyst.hbs +0 -21
  125. package/templates/agents/security-reviewer.hbs +0 -20
  126. package/templates/agents/ssot-updater.hbs +0 -45
  127. package/templates/agents/technical-writer.hbs +0 -28
  128. package/templates/agents/unit-test-engineer.hbs +0 -19
  129. package/templates/agents/user-clone.hbs +0 -49
  130. package/templates/github-actions/aws/deploy-notes.yml.hbs +0 -117
  131. package/templates/github-actions/azure/deploy-notes.yml.hbs +0 -78
  132. package/templates/github-actions/gcp/deploy-notes.yml.hbs +0 -76
  133. package/templates/github-actions/vercel-railway/deploy-notes.yml.hbs +0 -49
  134. package/templates/mindsets/agent-stdio.hbs +0 -51
  135. package/templates/mindsets/dart.hbs +0 -39
  136. package/templates/mindsets/ddd-clean-architecture.hbs +0 -52
  137. package/templates/mindsets/electron.hbs +0 -41
  138. package/templates/mindsets/fastapi.hbs +0 -40
  139. package/templates/mindsets/flutter.hbs +0 -56
  140. package/templates/mindsets/kotlin.hbs +0 -46
  141. package/templates/mindsets/postgresql.hbs +0 -39
  142. package/templates/mindsets/python.hbs +0 -39
  143. package/templates/mindsets/react.hbs +0 -51
  144. package/templates/mindsets/swift.hbs +0 -46
  145. package/templates/mindsets/terraform.hbs +0 -53
  146. package/templates/mindsets/typescript.hbs +0 -34
  147. package/templates/mindsets/vite.hbs +0 -37
  148. package/templates/notes-compose.yml +0 -82
  149. package/templates/partials/cogito-geo.hbs +0 -84
  150. package/templates/partials/cogito-godd.hbs +0 -22
  151. package/templates/partials/cogito-grammar.hbs +0 -105
  152. package/templates/partials/cogito-particle.hbs +0 -76
  153. package/templates/partials/cogito-root-ai.hbs +0 -79
  154. package/templates/partials/cogito-root-arch.hbs +0 -75
  155. package/templates/partials/cogito-root-core.hbs +0 -75
  156. package/templates/partials/cogito-root-daily.hbs +0 -58
  157. package/templates/partials/cogito-root-dev.hbs +0 -71
  158. package/templates/partials/cogito-root-devops.hbs +0 -71
  159. package/templates/partials/cogito-root-framework.hbs +0 -80
  160. package/templates/partials/cogito-root-lang.hbs +0 -74
  161. package/templates/partials/godd-core.hbs +0 -50
  162. package/templates/partials/project-context.hbs +0 -52
  163. package/templates/partials/ssot-header.hbs +0 -40
  164. package/templates/partials/ssot-rules.hbs +0 -30
  165. package/templates/prompts/adr.hbs +0 -39
  166. package/templates/prompts/agent-dev-workflow.hbs +0 -34
  167. package/templates/prompts/analyze-report.hbs +0 -36
  168. package/templates/prompts/auto-documentation.hbs +0 -37
  169. package/templates/prompts/check.hbs +0 -52
  170. package/templates/prompts/commit.hbs +0 -96
  171. package/templates/prompts/dev.hbs +0 -127
  172. package/templates/prompts/docs-init.hbs +0 -101
  173. package/templates/prompts/docs-update.hbs +0 -51
  174. package/templates/prompts/git-workflow.hbs +0 -125
  175. package/templates/prompts/impact-analysis.hbs +0 -43
  176. package/templates/prompts/install.hbs +0 -40
  177. package/templates/prompts/new-branch.hbs +0 -61
  178. package/templates/prompts/notes-deploy.hbs +0 -138
  179. package/templates/prompts/pr-analyze.hbs +0 -44
  180. package/templates/prompts/pr-create.hbs +0 -125
  181. package/templates/prompts/push-execute.hbs +0 -83
  182. package/templates/prompts/push.hbs +0 -24
  183. package/templates/prompts/release-notes.hbs +0 -38
  184. package/templates/prompts/requirements-to-business-flow.hbs +0 -29
  185. package/templates/prompts/requirements-to-tickets.hbs +0 -26
  186. package/templates/prompts/review.hbs +0 -126
  187. package/templates/prompts/setup.hbs +0 -247
  188. package/templates/prompts/spec.hbs +0 -35
  189. package/templates/prompts/specification-to-tickets.hbs +0 -24
  190. package/templates/prompts/submit.hbs +0 -143
  191. package/templates/prompts/sync.hbs +0 -63
  192. package/templates/prompts/test-plan.hbs +0 -56
  193. package/templates/scripts/aws/terraform-backend-setup.sh.hbs +0 -149
  194. package/templates/terraform/aws/alb.tf.hbs +0 -58
  195. package/templates/terraform/aws/backend.tf.hbs +0 -10
  196. package/templates/terraform/aws/cloudfront.tf.hbs +0 -96
  197. package/templates/terraform/aws/ecr.tf.hbs +0 -30
  198. package/templates/terraform/aws/ecs.tf.hbs +0 -142
  199. package/templates/terraform/aws/iam.tf.hbs +0 -88
  200. package/templates/terraform/aws/local.tf.hbs +0 -34
  201. package/templates/terraform/aws/output.tf.hbs +0 -49
  202. package/templates/terraform/aws/provider.tf.hbs +0 -26
  203. package/templates/terraform/aws/rds.tf.hbs +0 -38
  204. package/templates/terraform/aws/s3.tf.hbs +0 -34
  205. package/templates/terraform/aws/secrets.tf.hbs +0 -53
  206. package/templates/terraform/aws/security_groups.tf.hbs +0 -81
  207. package/templates/terraform/aws/vpc.tf.hbs +0 -63
  208. package/templates/terraform/azure/backend.tf.hbs +0 -8
  209. package/templates/terraform/azure/iam.tf.hbs +0 -36
  210. package/templates/terraform/azure/main.tf.hbs +0 -203
  211. package/templates/terraform/azure/output.tf.hbs +0 -19
  212. package/templates/terraform/azure/provider.tf.hbs +0 -19
  213. package/templates/terraform/gcp/backend.tf.hbs +0 -6
  214. package/templates/terraform/gcp/iam.tf.hbs +0 -47
  215. package/templates/terraform/gcp/main.tf.hbs +0 -175
  216. package/templates/terraform/gcp/output.tf.hbs +0 -15
  217. package/templates/terraform/gcp/provider.tf.hbs +0 -19
  218. package/templates/terraform/vercel-railway/main.tf.hbs +0 -64
  219. package/templates/terraform/vercel-railway/output.tf.hbs +0 -11
  220. /package/{notes-api → notes/api}/.dockerignore +0 -0
  221. /package/{notes-api → notes/api}/alembic/README +0 -0
  222. /package/{notes-api → notes/api}/alembic/env.py +0 -0
  223. /package/{notes-api → notes/api}/alembic/script.py.mako +0 -0
  224. /package/{notes-api → notes/api}/alembic/versions/001_initial_notes_tables.py +0 -0
  225. /package/{notes-api → notes/api}/alembic/versions/002_add_performance_indexes.py +0 -0
  226. /package/{notes-api → notes/api}/alembic/versions/003_add_yjs_documents.py +0 -0
  227. /package/{notes-api → notes/api}/alembic/versions/004_add_comments.py +0 -0
  228. /package/{notes-api → notes/api}/alembic/versions/005_add_github_username.py +0 -0
  229. /package/{notes-api → notes/api}/alembic/versions/006_add_check_constraints.py +0 -0
  230. /package/{notes-api → notes/api}/alembic.ini +0 -0
  231. /package/{notes-api → notes/api}/app/__init__.py +0 -0
  232. /package/{notes-api → notes/api}/app/config.py +0 -0
  233. /package/{notes-api → notes/api}/app/database.py +0 -0
  234. /package/{notes-api → notes/api}/app/http_client.py +0 -0
  235. /package/{notes-api → notes/api}/app/models/__init__.py +0 -0
  236. /package/{notes-api → notes/api}/app/models/audit.py +0 -0
  237. /package/{notes-api → notes/api}/app/models/base.py +0 -0
  238. /package/{notes-api → notes/api}/app/models/collab.py +0 -0
  239. /package/{notes-api → notes/api}/app/models/comment.py +0 -0
  240. /package/{notes-api → notes/api}/app/models/session.py +0 -0
  241. /package/{notes-api → notes/api}/app/models/setting.py +0 -0
  242. /package/{notes-api → notes/api}/app/models/user.py +0 -0
  243. /package/{notes-api → notes/api}/app/path_validation.py +0 -0
  244. /package/{notes-api → notes/api}/app/routers/__init__.py +0 -0
  245. /package/{notes-api → notes/api}/app/routers/collab.py +0 -0
  246. /package/{notes-api → notes/api}/app/routers/comments.py +0 -0
  247. /package/{notes-api → notes/api}/app/routers/files.py +0 -0
  248. /package/{notes-api → notes/api}/app/routers/settings.py +0 -0
  249. /package/{notes-api → notes/api}/app/routers/tree.py +0 -0
  250. /package/{notes-api → notes/api}/app/routers/upload.py +0 -0
  251. /package/{notes-api → notes/api}/app/routers/users.py +0 -0
  252. /package/{notes-api → notes/api}/app/schemas/__init__.py +0 -0
  253. /package/{notes-api → notes/api}/app/schemas/auth.py +0 -0
  254. /package/{notes-api → notes/api}/app/schemas/user.py +0 -0
  255. /package/{notes-api → notes/api}/app/security.py +0 -0
  256. /package/{notes-api → notes/api}/app/services/__init__.py +0 -0
  257. /package/{notes-api → notes/api}/app/services/audit.py +0 -0
  258. /package/{notes-api → notes/api}/app/services/business_date.py +0 -0
  259. /package/{notes-api → notes/api}/app/services/collab.py +0 -0
  260. /package/{notes-api → notes/api}/app/services/github.py +0 -0
  261. /package/{notes-api → notes/api}/app/services/pr_chain.py +0 -0
  262. /package/{notes-api → notes/api}/app/startup.py +0 -0
  263. /package/{notes-api → notes/api}/docker/Dockerfile +0 -0
  264. /package/{notes-api → notes/api}/docker/Dockerfile.test +0 -0
  265. /package/{notes-api → notes/api}/pyproject.toml +0 -0
  266. /package/{notes-api → notes/api}/tests/conftest.py +0 -0
  267. /package/{notes-api → notes/api}/tests/test_audit.py +0 -0
  268. /package/{notes-api → notes/api}/tests/test_auth.py +0 -0
  269. /package/{notes-api → notes/api}/tests/test_business_date.py +0 -0
  270. /package/{notes-api → notes/api}/tests/test_collab.py +0 -0
  271. /package/{notes-api → notes/api}/tests/test_comments.py +0 -0
  272. /package/{notes-api → notes/api}/tests/test_config_ssl.py +0 -0
  273. /package/{notes-api → notes/api}/tests/test_config_warnings.py +0 -0
  274. /package/{notes-api → notes/api}/tests/test_files.py +0 -0
  275. /package/{notes-api → notes/api}/tests/test_path_validation.py +0 -0
  276. /package/{notes-api → notes/api}/tests/test_security.py +0 -0
  277. /package/{notes-api → notes/api}/tests/test_settings.py +0 -0
  278. /package/{notes-api → notes/api}/tests/test_tree.py +0 -0
  279. /package/{notes-api → notes/api}/tests/test_upload.py +0 -0
  280. /package/{notes-api → notes/api}/tests/test_users.py +0 -0
  281. /package/{notes-app → notes/app}/.dockerignore +0 -0
  282. /package/{notes-app → notes/app}/index.html +0 -0
  283. /package/{notes-app → notes/app}/nginx.conf +0 -0
  284. /package/{notes-app → notes/app}/package-lock.json +0 -0
  285. /package/{notes-app → notes/app}/playwright.config.ts +0 -0
  286. /package/{notes-app → notes/app}/pnpm-lock.yaml +0 -0
  287. /package/{notes-app → notes/app}/src/App.tsx +0 -0
  288. /package/{notes-app → notes/app}/src/components/BranchSelectorModal.tsx +0 -0
  289. /package/{notes-app → notes/app}/src/components/CodeBlockView.test.tsx +0 -0
  290. /package/{notes-app → notes/app}/src/components/CodeBlockView.tsx +0 -0
  291. /package/{notes-app → notes/app}/src/components/CollabStatus.tsx +0 -0
  292. /package/{notes-app → notes/app}/src/components/DiffPreview.tsx +0 -0
  293. /package/{notes-app → notes/app}/src/components/DrawioEditor.tsx +0 -0
  294. /package/{notes-app → notes/app}/src/components/DrawioViewer.tsx +0 -0
  295. /package/{notes-app → notes/app}/src/components/ErrorBoundary.test.tsx +0 -0
  296. /package/{notes-app → notes/app}/src/components/ErrorBoundary.tsx +0 -0
  297. /package/{notes-app → notes/app}/src/components/FileContentView.tsx +0 -0
  298. /package/{notes-app → notes/app}/src/components/FileContextMenu.test.tsx +0 -0
  299. /package/{notes-app → notes/app}/src/components/FileContextMenu.tsx +0 -0
  300. /package/{notes-app → notes/app}/src/components/FileDialogs.test.tsx +0 -0
  301. /package/{notes-app → notes/app}/src/components/FileDialogs.tsx +0 -0
  302. /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.test.tsx +0 -0
  303. /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.tsx +0 -0
  304. /package/{notes-app → notes/app}/src/components/Skeleton.tsx +0 -0
  305. /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.test.tsx +0 -0
  306. /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.tsx +0 -0
  307. /package/{notes-app → notes/app}/src/components/TextEditor.tsx +0 -0
  308. /package/{notes-app → notes/app}/src/components/ToastContainer.tsx +0 -0
  309. /package/{notes-app → notes/app}/src/components/TreeNode.tsx +0 -0
  310. /package/{notes-app → notes/app}/src/contexts/AuthContext.test.tsx +0 -0
  311. /package/{notes-app → notes/app}/src/contexts/AuthContext.tsx +0 -0
  312. /package/{notes-app → notes/app}/src/contexts/ToastContext.test.ts +0 -0
  313. /package/{notes-app → notes/app}/src/contexts/ToastContext.tsx +0 -0
  314. /package/{notes-app → notes/app}/src/hooks/useAutoSave.test.ts +0 -0
  315. /package/{notes-app → notes/app}/src/hooks/useAutoSave.ts +0 -0
  316. /package/{notes-app → notes/app}/src/hooks/useCollaboration.ts +0 -0
  317. /package/{notes-app → notes/app}/src/hooks/useComments.ts +0 -0
  318. /package/{notes-app → notes/app}/src/hooks/useYText.ts +0 -0
  319. /package/{notes-app → notes/app}/src/lib/branch-diff.test.ts +0 -0
  320. /package/{notes-app → notes/app}/src/lib/branch-diff.ts +0 -0
  321. /package/{notes-app → notes/app}/src/lib/created-file.test.ts +0 -0
  322. /package/{notes-app → notes/app}/src/lib/created-file.ts +0 -0
  323. /package/{notes-app → notes/app}/src/lib/csv-utils.test.ts +0 -0
  324. /package/{notes-app → notes/app}/src/lib/csv-utils.ts +0 -0
  325. /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.test.ts +0 -0
  326. /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.ts +0 -0
  327. /package/{notes-app → notes/app}/src/lib/draft-storage.test.ts +0 -0
  328. /package/{notes-app → notes/app}/src/lib/draft-storage.ts +0 -0
  329. /package/{notes-app → notes/app}/src/lib/mermaid-render.ts +0 -0
  330. /package/{notes-app → notes/app}/src/lib/postLoginRedirect.test.ts +0 -0
  331. /package/{notes-app → notes/app}/src/lib/postLoginRedirect.ts +0 -0
  332. /package/{notes-app → notes/app}/src/lib/tree-utils.ts +0 -0
  333. /package/{notes-app → notes/app}/src/lib/user-colors.ts +0 -0
  334. /package/{notes-app → notes/app}/src/lib/xlsx-utils.test.ts +0 -0
  335. /package/{notes-app → notes/app}/src/lib/xlsx-utils.ts +0 -0
  336. /package/{notes-app → notes/app}/src/main.tsx +0 -0
  337. /package/{notes-app → notes/app}/src/pages/LoginPage.tsx +0 -0
  338. /package/{notes-app → notes/app}/src/pages/MainPage.insertChildEntry.test.ts +0 -0
  339. /package/{notes-app → notes/app}/src/pages/SettingsPage.tsx +0 -0
  340. /package/{notes-app → notes/app}/src/pages/UserManagementPage.tsx +0 -0
  341. /package/{notes-app → notes/app}/src/styles/globals.css +0 -0
  342. /package/{notes-app → notes/app}/src/types.ts +0 -0
  343. /package/{notes-app → notes/app}/src/vite-env.d.ts +0 -0
  344. /package/{notes-app → notes/app}/tsconfig.json +0 -0
  345. /package/{notes-app → notes/app}/tsconfig.node.json +0 -0
  346. /package/{notes-app → notes/app}/vite.config.js +0 -0
  347. /package/{notes-app → notes/app}/vite.config.ts +0 -0
  348. /package/{notes-app → notes/app}/vitest.config.ts +0 -0
@@ -0,0 +1,452 @@
1
+ import { useState, useEffect, useRef, useCallback } from "react";
2
+ import { Tag, Plus, Check, X, Loader2, Pencil, Trash2 } from "lucide-react";
3
+ import {
4
+ listIssueLabelsApi,
5
+ createLabelApi,
6
+ updateLabelApi,
7
+ deleteLabelApi,
8
+ getErrorMessage,
9
+ } from "@/lib/api";
10
+ import type { IssueLabel } from "@/lib/api";
11
+ import { useToast } from "@/contexts/ToastContext";
12
+
13
+ type LabelSelectorProps = {
14
+ selected: string[];
15
+ onChange: (names: string[]) => void;
16
+ disabled?: boolean;
17
+ /** admin/editor: create, edit, delete repository labels */
18
+ canManageLabels?: boolean;
19
+ onLabelsMutated?: () => void;
20
+ };
21
+
22
+ const PRESET_COLORS = [
23
+ "e11d48", "ea580c", "ca8a04", "16a34a",
24
+ "0284c7", "7c3aed", "db2777", "64748b",
25
+ ];
26
+
27
+ export default function LabelSelector({
28
+ selected,
29
+ onChange,
30
+ disabled = false,
31
+ canManageLabels = false,
32
+ onLabelsMutated,
33
+ }: LabelSelectorProps) {
34
+ const { showToast } = useToast();
35
+ const [open, setOpen] = useState(false);
36
+ const [labels, setLabels] = useState<IssueLabel[]>([]);
37
+ const [loadingLabels, setLoadingLabels] = useState(false);
38
+ const [creatingMode, setCreatingMode] = useState(false);
39
+ const [newName, setNewName] = useState("");
40
+ const [newColor, setNewColor] = useState(PRESET_COLORS[0]);
41
+ const [creating, setCreating] = useState(false);
42
+ const [editingName, setEditingName] = useState<string | null>(null);
43
+ const [editName, setEditName] = useState("");
44
+ const [editColor, setEditColor] = useState(PRESET_COLORS[0]);
45
+ const [savingEdit, setSavingEdit] = useState(false);
46
+ const [confirmDeleteName, setConfirmDeleteName] = useState<string | null>(null);
47
+ const [deleting, setDeleting] = useState(false);
48
+ const containerRef = useRef<HTMLDivElement>(null);
49
+
50
+ const loadLabels = useCallback(async () => {
51
+ setLoadingLabels(true);
52
+ try {
53
+ const { labels: data } = await listIssueLabelsApi();
54
+ setLabels(data);
55
+ } catch (e) {
56
+ showToast(`ラベルの読み込みに失敗しました: ${getErrorMessage(e)}`, "error");
57
+ } finally {
58
+ setLoadingLabels(false);
59
+ }
60
+ }, [showToast]);
61
+
62
+ useEffect(() => {
63
+ if (open && labels.length === 0) {
64
+ void loadLabels();
65
+ }
66
+ }, [open, labels.length, loadLabels]);
67
+
68
+ useEffect(() => {
69
+ function handleClickOutside(e: MouseEvent) {
70
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
71
+ setOpen(false);
72
+ setCreatingMode(false);
73
+ setEditingName(null);
74
+ setConfirmDeleteName(null);
75
+ }
76
+ }
77
+ if (open) {
78
+ document.addEventListener("mousedown", handleClickOutside);
79
+ }
80
+ return () => document.removeEventListener("mousedown", handleClickOutside);
81
+ }, [open]);
82
+
83
+ function toggleLabel(name: string) {
84
+ if (selected.includes(name)) {
85
+ onChange(selected.filter((n) => n !== name));
86
+ } else {
87
+ onChange([...selected, name]);
88
+ }
89
+ }
90
+
91
+ function applyLabelRemoval(name: string) {
92
+ setLabels((prev) => prev.filter((l) => l.name !== name));
93
+ if (selected.includes(name)) {
94
+ onChange(selected.filter((n) => n !== name));
95
+ }
96
+ }
97
+
98
+ async function handleCreateLabel() {
99
+ if (!newName.trim()) {
100
+ showToast("ラベル名を入力してください", "error");
101
+ return;
102
+ }
103
+ setCreating(true);
104
+ try {
105
+ const created = await createLabelApi({ name: newName.trim(), color: newColor });
106
+ setLabels((prev) => [...prev, created]);
107
+ onChange([...selected, created.name]);
108
+ setNewName("");
109
+ setNewColor(PRESET_COLORS[0]);
110
+ setCreatingMode(false);
111
+ showToast(`ラベル「${created.name}」を作成しました`, "success");
112
+ onLabelsMutated?.();
113
+ } catch (e) {
114
+ showToast(`ラベルの作成に失敗しました: ${getErrorMessage(e)}`, "error");
115
+ } finally {
116
+ setCreating(false);
117
+ }
118
+ }
119
+
120
+ function startEditLabel(label: IssueLabel) {
121
+ setCreatingMode(false);
122
+ setConfirmDeleteName(null);
123
+ setEditingName(label.name);
124
+ setEditName(label.name);
125
+ setEditColor(label.color || PRESET_COLORS[0]);
126
+ }
127
+
128
+ async function handleUpdateLabel() {
129
+ if (!editingName || !editName.trim()) {
130
+ showToast("ラベル名を入力してください", "error");
131
+ return;
132
+ }
133
+ setSavingEdit(true);
134
+ try {
135
+ const payload: { new_name?: string; color: string } = { color: editColor };
136
+ if (editName.trim() !== editingName) {
137
+ payload.new_name = editName.trim();
138
+ }
139
+ const updated = await updateLabelApi(editingName, payload);
140
+ setLabels((prev) =>
141
+ prev.map((l) => (l.name === editingName ? updated : l)),
142
+ );
143
+ if (selected.includes(editingName) && updated.name !== editingName) {
144
+ onChange(selected.map((n) => (n === editingName ? updated.name : n)));
145
+ }
146
+ setEditingName(null);
147
+ showToast(`ラベル「${updated.name}」を更新しました`, "success");
148
+ onLabelsMutated?.();
149
+ } catch (e) {
150
+ showToast(`ラベルの更新に失敗しました: ${getErrorMessage(e)}`, "error");
151
+ } finally {
152
+ setSavingEdit(false);
153
+ }
154
+ }
155
+
156
+ async function handleDeleteLabel(name: string) {
157
+ setDeleting(true);
158
+ try {
159
+ await deleteLabelApi(name);
160
+ applyLabelRemoval(name);
161
+ setConfirmDeleteName(null);
162
+ showToast(`ラベル「${name}」を削除しました`, "success");
163
+ onLabelsMutated?.();
164
+ } catch (e) {
165
+ showToast(`ラベルの削除に失敗しました: ${getErrorMessage(e)}`, "error");
166
+ } finally {
167
+ setDeleting(false);
168
+ }
169
+ }
170
+
171
+ function renderColorPicker(
172
+ value: string,
173
+ onPick: (c: string) => void,
174
+ ) {
175
+ return (
176
+ <div className="flex gap-1 flex-wrap">
177
+ {PRESET_COLORS.map((c) => (
178
+ <button
179
+ key={c}
180
+ type="button"
181
+ aria-label={`色 #${c}`}
182
+ onClick={() => onPick(c)}
183
+ className={`w-5 h-5 rounded-full transition-transform ${
184
+ value === c ? "scale-125 ring-2 ring-white" : "hover:scale-110"
185
+ }`}
186
+ style={{ backgroundColor: `#${c}` }}
187
+ />
188
+ ))}
189
+ </div>
190
+ );
191
+ }
192
+
193
+ return (
194
+ <div ref={containerRef} className="relative">
195
+ <label className="block text-xs text-gray-400 mb-1">ラベル</label>
196
+
197
+ <div
198
+ role="button"
199
+ tabIndex={disabled ? -1 : 0}
200
+ aria-expanded={open}
201
+ aria-label="ラベルを選択"
202
+ onClick={() => !disabled && setOpen((v) => !v)}
203
+ onKeyDown={(e) => {
204
+ if (!disabled && (e.key === "Enter" || e.key === " ")) {
205
+ e.preventDefault();
206
+ setOpen((v) => !v);
207
+ }
208
+ }}
209
+ className={`min-h-[38px] w-full px-3 py-1.5 rounded bg-gray-900 border text-sm flex flex-wrap gap-1 items-center cursor-pointer transition-colors ${
210
+ disabled
211
+ ? "border-gray-700 cursor-not-allowed opacity-50"
212
+ : open
213
+ ? "border-blue-500"
214
+ : "border-gray-600 hover:border-gray-400"
215
+ }`}
216
+ >
217
+ {selected.length === 0 ? (
218
+ <span className="text-gray-500 flex items-center gap-1">
219
+ <Tag className="w-3 h-3" />
220
+ ラベルを選択…
221
+ </span>
222
+ ) : (
223
+ selected.map((name) => {
224
+ const label = labels.find((l) => l.name === name);
225
+ const color = label?.color ?? "64748b";
226
+ return (
227
+ <span
228
+ key={name}
229
+ className="flex items-center gap-1 px-2 py-0.5 rounded-full text-xs"
230
+ style={{
231
+ backgroundColor: `#${color}22`,
232
+ color: `#${color}`,
233
+ border: `1px solid #${color}44`,
234
+ }}
235
+ >
236
+ {name}
237
+ {!disabled && (
238
+ <button
239
+ type="button"
240
+ aria-label={`${name} を外す`}
241
+ onClick={(e) => {
242
+ e.stopPropagation();
243
+ toggleLabel(name);
244
+ }}
245
+ className="hover:opacity-70"
246
+ >
247
+ <X className="w-3 h-3" />
248
+ </button>
249
+ )}
250
+ </span>
251
+ );
252
+ })
253
+ )}
254
+ </div>
255
+
256
+ {open && (
257
+ <div className="absolute z-50 mt-1 w-full rounded border border-gray-600 bg-gray-800 shadow-xl">
258
+ {loadingLabels ? (
259
+ <div className="flex items-center gap-2 px-3 py-3 text-sm text-gray-400">
260
+ <Loader2 className="w-4 h-4 animate-spin" />
261
+ 読み込み中…
262
+ </div>
263
+ ) : (
264
+ <ul className="max-h-48 overflow-y-auto">
265
+ {labels.length === 0 && (
266
+ <li className="px-3 py-2 text-sm text-gray-500">
267
+ ラベルがありません
268
+ </li>
269
+ )}
270
+ {labels.map((label) => {
271
+ const isSelected = selected.includes(label.name);
272
+ const isConfirmingDelete = confirmDeleteName === label.name;
273
+ return (
274
+ <li key={label.name} className="border-b border-gray-700/50 last:border-0">
275
+ {isConfirmingDelete ? (
276
+ <div className="px-3 py-2 space-y-2">
277
+ <p className="text-xs text-gray-300">
278
+ 「{label.name}」を削除しますか?リポジトリ内の全 Issue から外れます。
279
+ </p>
280
+ <div className="flex gap-1">
281
+ <button
282
+ type="button"
283
+ disabled={deleting}
284
+ onClick={() => void handleDeleteLabel(label.name)}
285
+ className="flex-1 px-2 py-1 rounded text-xs bg-red-700 hover:bg-red-600 text-white disabled:opacity-50"
286
+ >
287
+ {deleting ? "削除中…" : "削除する"}
288
+ </button>
289
+ <button
290
+ type="button"
291
+ onClick={() => setConfirmDeleteName(null)}
292
+ className="px-2 py-1 rounded text-xs text-gray-400 hover:bg-gray-700"
293
+ >
294
+ キャンセル
295
+ </button>
296
+ </div>
297
+ </div>
298
+ ) : (
299
+ <div className="flex items-center gap-1 px-2 py-1.5 hover:bg-gray-700/60">
300
+ <button
301
+ type="button"
302
+ onClick={() => toggleLabel(label.name)}
303
+ className="flex-1 flex items-center gap-2 text-sm min-w-0"
304
+ >
305
+ <span
306
+ className="w-3 h-3 rounded-full shrink-0"
307
+ style={{ backgroundColor: `#${label.color}` }}
308
+ />
309
+ <span
310
+ className="flex-1 text-left truncate"
311
+ style={{ color: `#${label.color}` }}
312
+ >
313
+ {label.name}
314
+ </span>
315
+ {isSelected && (
316
+ <Check className="w-4 h-4 text-blue-400 shrink-0" />
317
+ )}
318
+ </button>
319
+ {canManageLabels && !disabled && (
320
+ <div className="flex shrink-0 gap-0.5">
321
+ <button
322
+ type="button"
323
+ aria-label={`${label.name} を編集`}
324
+ onClick={(e) => {
325
+ e.stopPropagation();
326
+ startEditLabel(label);
327
+ }}
328
+ className="p-1 rounded text-gray-400 hover:text-gray-200 hover:bg-gray-600"
329
+ >
330
+ <Pencil className="w-3.5 h-3.5" />
331
+ </button>
332
+ <button
333
+ type="button"
334
+ aria-label={`${label.name} を削除`}
335
+ onClick={(e) => {
336
+ e.stopPropagation();
337
+ setEditingName(null);
338
+ setConfirmDeleteName(label.name);
339
+ }}
340
+ className="p-1 rounded text-gray-400 hover:text-red-300 hover:bg-gray-600"
341
+ >
342
+ <Trash2 className="w-3.5 h-3.5" />
343
+ </button>
344
+ </div>
345
+ )}
346
+ </div>
347
+ )}
348
+ </li>
349
+ );
350
+ })}
351
+ </ul>
352
+ )}
353
+
354
+ {canManageLabels && !disabled && (
355
+ <div className="border-t border-gray-600">
356
+ {editingName ? (
357
+ <div className="p-2 space-y-2">
358
+ <p className="text-xs text-gray-400">ラベルを編集</p>
359
+ <input
360
+ autoFocus
361
+ type="text"
362
+ value={editName}
363
+ onChange={(e) => setEditName(e.target.value)}
364
+ placeholder="ラベル名"
365
+ maxLength={50}
366
+ className="w-full px-2 py-1 rounded bg-gray-900 border border-gray-600 text-gray-200 placeholder-gray-500 text-xs outline-none focus:border-blue-500"
367
+ onKeyDown={(e) => {
368
+ if (e.key === "Enter") void handleUpdateLabel();
369
+ if (e.key === "Escape") setEditingName(null);
370
+ }}
371
+ />
372
+ {renderColorPicker(editColor, setEditColor)}
373
+ <div className="flex gap-1">
374
+ <button
375
+ type="button"
376
+ onClick={() => void handleUpdateLabel()}
377
+ disabled={savingEdit || !editName.trim()}
378
+ className="flex-1 px-2 py-1 rounded text-xs bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50"
379
+ >
380
+ {savingEdit ? "保存中…" : "保存"}
381
+ </button>
382
+ <button
383
+ type="button"
384
+ onClick={() => setEditingName(null)}
385
+ className="px-2 py-1 rounded text-xs text-gray-400 hover:bg-gray-700"
386
+ >
387
+ キャンセル
388
+ </button>
389
+ </div>
390
+ </div>
391
+ ) : creatingMode ? (
392
+ <div className="p-2 space-y-2">
393
+ <input
394
+ autoFocus
395
+ type="text"
396
+ value={newName}
397
+ onChange={(e) => setNewName(e.target.value)}
398
+ placeholder="ラベル名"
399
+ maxLength={50}
400
+ className="w-full px-2 py-1 rounded bg-gray-900 border border-gray-600 text-gray-200 placeholder-gray-500 text-xs outline-none focus:border-blue-500"
401
+ onKeyDown={(e) => {
402
+ if (e.key === "Enter") void handleCreateLabel();
403
+ if (e.key === "Escape") setCreatingMode(false);
404
+ }}
405
+ />
406
+ {renderColorPicker(newColor, setNewColor)}
407
+ <div className="flex gap-1">
408
+ <button
409
+ type="button"
410
+ onClick={() => void handleCreateLabel()}
411
+ disabled={creating || !newName.trim()}
412
+ className="flex-1 px-2 py-1 rounded text-xs bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 flex items-center justify-center gap-1"
413
+ >
414
+ {creating ? (
415
+ <Loader2 className="w-3 h-3 animate-spin" />
416
+ ) : (
417
+ <Plus className="w-3 h-3" />
418
+ )}
419
+ 作成
420
+ </button>
421
+ <button
422
+ type="button"
423
+ onClick={() => {
424
+ setCreatingMode(false);
425
+ setNewName("");
426
+ }}
427
+ className="px-2 py-1 rounded text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-700"
428
+ >
429
+ キャンセル
430
+ </button>
431
+ </div>
432
+ </div>
433
+ ) : (
434
+ <button
435
+ type="button"
436
+ onClick={() => {
437
+ setConfirmDeleteName(null);
438
+ setCreatingMode(true);
439
+ }}
440
+ className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
441
+ >
442
+ <Plus className="w-4 h-4" />
443
+ 新規ラベルを作成…
444
+ </button>
445
+ )}
446
+ </div>
447
+ )}
448
+ </div>
449
+ )}
450
+ </div>
451
+ );
452
+ }
@@ -3,7 +3,7 @@ import {
3
3
  Undo2, Redo2, AlignLeft, AlignCenter, AlignRight,
4
4
  List, ListOrdered, TextQuote, Grid3X3, Minus, ImagePlus, X,
5
5
  } from "lucide-react";
6
- import { useEditor, EditorContent, ReactNodeViewRenderer } from "@tiptap/react";
6
+ import { useEditor, EditorContent, ReactNodeViewRenderer, Extension } from "@tiptap/react";
7
7
  import StarterKit from "@tiptap/starter-kit";
8
8
  import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
9
9
  import { common, createLowlight } from "lowlight";
@@ -19,11 +19,14 @@ import { Markdown } from "@tiptap/markdown";
19
19
  import Collaboration from "@tiptap/extension-collaboration";
20
20
  import CollaborationCaret from "@tiptap/extension-collaboration-caret";
21
21
  import { Plugin } from "@tiptap/pm/state";
22
+ import { buildHighlightDecorations } from "@/lib/commentHighlight";
22
23
  import type { AnyExtension, Editor } from "@tiptap/react";
23
24
  import type * as Y from "yjs";
24
25
  import type { WebsocketProvider } from "y-websocket";
25
26
  import { uploadImageApi } from "@/lib/api";
26
27
  import { useToast } from "@/contexts/ToastContext";
28
+ import type { CommentAnchor } from "@/lib/commentAnchor";
29
+ import CommentSelectionPopover from "./CommentSelectionPopover";
27
30
  import CodeBlockView from "./CodeBlockView";
28
31
 
29
32
  const lowlight = createLowlight(common);
@@ -45,8 +48,27 @@ type Props = {
45
48
  ydoc?: Y.Doc | null;
46
49
  provider?: WebsocketProvider | null;
47
50
  collabUser?: { name: string; color: string } | null;
51
+ onCommentAnchor?: (anchor: CommentAnchor) => void;
52
+ commentHighlights?: CommentAnchor[];
48
53
  };
49
54
 
55
+ function getEditorSelectionAnchor(editor: Editor): CommentAnchor | null {
56
+ const { from, to, empty } = editor.state.selection;
57
+ if (empty) return null;
58
+
59
+ const anchor_text = editor.state.doc.textBetween(from, to, "\n").trim();
60
+ if (!anchor_text) return null;
61
+
62
+ const textBefore = editor.state.doc.textBetween(0, from, "\n");
63
+ const selected = editor.state.doc.textBetween(from, to, "\n");
64
+
65
+ return {
66
+ anchor_text,
67
+ position_from: textBefore.length,
68
+ position_to: textBefore.length + selected.length,
69
+ };
70
+ }
71
+
50
72
  function ToolbarButton({
51
73
  onClick,
52
74
  active,
@@ -314,10 +336,23 @@ export default function MarkdownEditor({
314
336
  ydoc,
315
337
  provider,
316
338
  collabUser,
339
+ onCommentAnchor,
340
+ commentHighlights = [],
317
341
  }: Props) {
318
342
  const { showToast } = useToast();
319
343
  const onContentChangeRef = useRef(onContentChange);
320
344
  onContentChangeRef.current = onContentChange;
345
+ const onCommentAnchorRef = useRef(onCommentAnchor);
346
+ onCommentAnchorRef.current = onCommentAnchor;
347
+ const commentHighlightsRef = useRef(commentHighlights);
348
+ commentHighlightsRef.current = commentHighlights;
349
+
350
+ const [selectionPopover, setSelectionPopover] = useState<{
351
+ top: number;
352
+ left: number;
353
+ anchor: CommentAnchor;
354
+ } | null>(null);
355
+ const sourceTextareaRef = useRef<HTMLTextAreaElement>(null);
321
356
 
322
357
  const [mode, setMode] = useState<EditorMode>("wysiwyg");
323
358
  const [sourceText, setSourceText] = useState("");
@@ -388,6 +423,28 @@ export default function MarkdownEditor({
388
423
  },
389
424
  }), []);
390
425
 
426
+ const commentHighlightExtension = useMemo(
427
+ () =>
428
+ Extension.create({
429
+ name: "commentHighlight",
430
+ addProseMirrorPlugins() {
431
+ return [
432
+ new Plugin({
433
+ props: {
434
+ decorations(state) {
435
+ return buildHighlightDecorations(
436
+ state.doc,
437
+ commentHighlightsRef.current,
438
+ );
439
+ },
440
+ },
441
+ }),
442
+ ];
443
+ },
444
+ }),
445
+ [],
446
+ );
447
+
391
448
  const collabExtensions = useMemo((): AnyExtension[] => {
392
449
  if (!ydoc) return [];
393
450
  const exts: AnyExtension[] = [
@@ -428,6 +485,7 @@ export default function MarkdownEditor({
428
485
  Markdown.configure({
429
486
  markedOptions: { breaks: true, gfm: true },
430
487
  }),
488
+ commentHighlightExtension,
431
489
  ...collabExtensions,
432
490
  ],
433
491
  content: initialContent,
@@ -437,6 +495,72 @@ export default function MarkdownEditor({
437
495
  },
438
496
  });
439
497
 
498
+ useEffect(() => {
499
+ editor?.view.dispatch(editor.state.tr);
500
+ }, [editor, commentHighlights]);
501
+
502
+ useEffect(() => {
503
+ if (!editor || !onCommentAnchor) return;
504
+
505
+ const handleSelectionUpdate = () => {
506
+ const sel = window.getSelection();
507
+ if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
508
+ setSelectionPopover(null);
509
+ return;
510
+ }
511
+
512
+ const anchor = getEditorSelectionAnchor(editor);
513
+ if (!anchor) {
514
+ setSelectionPopover(null);
515
+ return;
516
+ }
517
+
518
+ const rect = sel.getRangeAt(0).getBoundingClientRect();
519
+ setSelectionPopover({
520
+ top: rect.top,
521
+ left: rect.left + rect.width / 2,
522
+ anchor,
523
+ });
524
+ };
525
+
526
+ editor.on("selectionUpdate", handleSelectionUpdate);
527
+ return () => {
528
+ editor.off("selectionUpdate", handleSelectionUpdate);
529
+ };
530
+ }, [editor, onCommentAnchor]);
531
+
532
+ const handleSourceSelection = useCallback(() => {
533
+ if (!onCommentAnchorRef.current) return;
534
+ const textarea = sourceTextareaRef.current;
535
+ if (!textarea) return;
536
+
537
+ const start = textarea.selectionStart;
538
+ const end = textarea.selectionEnd;
539
+ if (start === end) {
540
+ setSelectionPopover(null);
541
+ return;
542
+ }
543
+
544
+ const anchor_text = sourceText.slice(start, end).trim();
545
+ if (!anchor_text) {
546
+ setSelectionPopover(null);
547
+ return;
548
+ }
549
+
550
+ const anchor: CommentAnchor = {
551
+ anchor_text,
552
+ position_from: start,
553
+ position_to: end,
554
+ };
555
+
556
+ const rect = textarea.getBoundingClientRect();
557
+ setSelectionPopover({
558
+ top: rect.top + 24,
559
+ left: rect.left + rect.width / 2,
560
+ anchor,
561
+ });
562
+ }, [sourceText]);
563
+
440
564
  const [showSearch, setShowSearch] = useState(false);
441
565
  const [searchTerm, setSearchTerm] = useState("");
442
566
  const [replaceTerm, setReplaceTerm] = useState("");
@@ -671,17 +795,31 @@ export default function MarkdownEditor({
671
795
  prose-pre:bg-gray-800 prose-pre:border prose-pre:border-gray-700
672
796
  prose-table:border-collapse prose-th:border prose-th:border-gray-600 prose-th:px-3 prose-th:py-2
673
797
  prose-td:border prose-td:border-gray-700 prose-td:px-3 prose-td:py-1.5
674
- [&_.tiptap]:outline-none [&_.tiptap]:min-h-[300px]"
798
+ [&_.tiptap]:outline-none [&_.tiptap]:min-h-[300px]
799
+ [&_.comment-anchor-highlight]:bg-yellow-500/30 [&_.comment-anchor-highlight]:rounded-sm"
675
800
  />
676
801
  ) : (
677
802
  <textarea
803
+ ref={sourceTextareaRef}
678
804
  value={sourceText}
679
805
  onChange={(e) => handleSourceChange(e.target.value)}
806
+ onMouseUp={handleSourceSelection}
807
+ onKeyUp={handleSourceSelection}
680
808
  className="w-full h-full p-6 bg-gray-900 text-gray-100 font-mono text-sm resize-none outline-none border-none"
681
809
  spellCheck={false}
682
810
  />
683
811
  )}
684
812
  </div>
813
+ {selectionPopover && onCommentAnchor && (
814
+ <CommentSelectionPopover
815
+ top={selectionPopover.top}
816
+ left={selectionPopover.left}
817
+ onAddComment={() => {
818
+ onCommentAnchor(selectionPopover.anchor);
819
+ setSelectionPopover(null);
820
+ }}
821
+ />
822
+ )}
685
823
  </div>
686
824
  );
687
825
  }