@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,167 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import {
3
+ clampPanelWidth,
4
+ computeResizedWidth,
5
+ isRtlContext,
6
+ loadStoredPanelWidth,
7
+ PANEL_WIDTH_SAVE_DEBOUNCE_MS,
8
+ type ResizeEdge,
9
+ saveStoredPanelWidth,
10
+ } from "@/lib/resizable-panel";
11
+
12
+ type Options = {
13
+ storageKey: string;
14
+ defaultWidth: number;
15
+ minWidth: number;
16
+ maxWidth: number;
17
+ edge: ResizeEdge;
18
+ };
19
+
20
+ type ActiveDrag = {
21
+ handle: HTMLElement;
22
+ pointerId: number;
23
+ handleMove: (event: PointerEvent) => void;
24
+ handleUp: (event: PointerEvent) => void;
25
+ };
26
+
27
+ export function useResizablePanelWidth(options: Options) {
28
+ const { storageKey, defaultWidth, minWidth, maxWidth, edge } = options;
29
+ const [width, setWidth] = useState(() =>
30
+ loadStoredPanelWidth(storageKey, defaultWidth, minWidth, maxWidth),
31
+ );
32
+ const widthRef = useRef(width);
33
+ const storageKeyRef = useRef(storageKey);
34
+ const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
35
+ const skipInitialSaveRef = useRef(true);
36
+ const lastSavedWidthRef = useRef<number | null>(null);
37
+ const activeDragRef = useRef<ActiveDrag | null>(null);
38
+
39
+ useEffect(() => {
40
+ storageKeyRef.current = storageKey;
41
+ }, [storageKey]);
42
+
43
+ useEffect(() => {
44
+ widthRef.current = width;
45
+ }, [width]);
46
+
47
+ const cancelSaveTimer = useCallback(() => {
48
+ if (saveTimerRef.current != null) {
49
+ clearTimeout(saveTimerRef.current);
50
+ saveTimerRef.current = null;
51
+ }
52
+ }, []);
53
+
54
+ const persistWidth = useCallback((nextWidth: number) => {
55
+ if (lastSavedWidthRef.current === nextWidth) return;
56
+ lastSavedWidthRef.current = nextWidth;
57
+ saveStoredPanelWidth(storageKeyRef.current, nextWidth);
58
+ }, []);
59
+
60
+ const flushSave = useCallback(() => {
61
+ cancelSaveTimer();
62
+ persistWidth(widthRef.current);
63
+ }, [cancelSaveTimer, persistWidth]);
64
+
65
+ useEffect(() => {
66
+ if (skipInitialSaveRef.current) {
67
+ skipInitialSaveRef.current = false;
68
+ lastSavedWidthRef.current = width;
69
+ return;
70
+ }
71
+ cancelSaveTimer();
72
+ saveTimerRef.current = setTimeout(() => {
73
+ persistWidth(width);
74
+ saveTimerRef.current = null;
75
+ }, PANEL_WIDTH_SAVE_DEBOUNCE_MS);
76
+ return cancelSaveTimer;
77
+ }, [storageKey, width, cancelSaveTimer, persistWidth]);
78
+
79
+ useEffect(() => {
80
+ const handleBeforeUnload = () => {
81
+ flushSave();
82
+ };
83
+ window.addEventListener("beforeunload", handleBeforeUnload);
84
+ return () => {
85
+ window.removeEventListener("beforeunload", handleBeforeUnload);
86
+ flushSave();
87
+
88
+ const drag = activeDragRef.current;
89
+ if (drag == null) return;
90
+
91
+ try {
92
+ drag.handle.releasePointerCapture(drag.pointerId);
93
+ } catch {
94
+ // pointer already released
95
+ }
96
+ document.body.style.cursor = "";
97
+ document.body.style.userSelect = "";
98
+ window.removeEventListener("pointermove", drag.handleMove);
99
+ window.removeEventListener("pointerup", drag.handleUp);
100
+ window.removeEventListener("pointercancel", drag.handleUp);
101
+ activeDragRef.current = null;
102
+ };
103
+ }, [flushSave]);
104
+
105
+ const onResizePointerDown = useCallback(
106
+ (event: React.PointerEvent<HTMLElement>) => {
107
+ event.preventDefault();
108
+ const handle = event.currentTarget;
109
+ handle.setPointerCapture(event.pointerId);
110
+
111
+ const startX = event.clientX;
112
+ const startWidth = widthRef.current;
113
+ // 書字方向はドラッグ中に変わらないため pointerdown 時に一度だけ判定する。
114
+ const rtl = isRtlContext(handle);
115
+
116
+ const handleMove = (moveEvent: PointerEvent) => {
117
+ const next = computeResizedWidth(
118
+ startWidth,
119
+ startX,
120
+ moveEvent.clientX,
121
+ edge,
122
+ minWidth,
123
+ maxWidth,
124
+ rtl,
125
+ );
126
+ widthRef.current = next;
127
+ setWidth(next);
128
+ };
129
+
130
+ const handleUp = (upEvent: PointerEvent) => {
131
+ handle.releasePointerCapture(upEvent.pointerId);
132
+ document.body.style.cursor = "";
133
+ document.body.style.userSelect = "";
134
+ window.removeEventListener("pointermove", handleMove);
135
+ window.removeEventListener("pointerup", handleUp);
136
+ window.removeEventListener("pointercancel", handleUp);
137
+ activeDragRef.current = null;
138
+ flushSave();
139
+ };
140
+
141
+ activeDragRef.current = {
142
+ handle,
143
+ pointerId: event.pointerId,
144
+ handleMove,
145
+ handleUp,
146
+ };
147
+
148
+ document.body.style.cursor = "col-resize";
149
+ document.body.style.userSelect = "none";
150
+ window.addEventListener("pointermove", handleMove);
151
+ window.addEventListener("pointerup", handleUp);
152
+ window.addEventListener("pointercancel", handleUp);
153
+ },
154
+ [edge, maxWidth, minWidth, flushSave],
155
+ );
156
+
157
+ const applyWidth = useCallback(
158
+ (next: number) => {
159
+ const clamped = clampPanelWidth(next, minWidth, maxWidth);
160
+ widthRef.current = clamped;
161
+ setWidth(clamped);
162
+ },
163
+ [maxWidth, minWidth],
164
+ );
165
+
166
+ return { width, onResizePointerDown, applyWidth };
167
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { getErrorMessage, isAbortError, getToken, setToken, clearToken, encodePath } from "./api";
2
+ import { getErrorMessage, isAbortError, getToken, setToken, clearToken, encodePath, buildRequestSignal } from "./api";
3
3
 
4
4
  describe("getErrorMessage", () => {
5
5
  it("Error インスタンスからメッセージを取得する", () => {
@@ -229,6 +229,86 @@ describe("fetchSessionsApi", () => {
229
229
  });
230
230
  });
231
231
 
232
+ describe("apiFetch signal 合成(デフォルトタイムアウト保持)", () => {
233
+ beforeEach(() => {
234
+ vi.restoreAllMocks();
235
+ localStorage.clear();
236
+ });
237
+
238
+ it("caller signal を渡してもキャンセルが forward 先に伝播する", async () => {
239
+ const { fetchHistoryApi, setToken } = await import("./api");
240
+ setToken("test-token");
241
+ let captured: AbortSignal | null = null;
242
+ vi.spyOn(globalThis, "fetch").mockImplementation(
243
+ (_input: RequestInfo | URL, init?: RequestInit) => {
244
+ captured = init?.signal ?? null;
245
+ return Promise.resolve(
246
+ new Response(JSON.stringify([]), {
247
+ status: 200,
248
+ headers: { "Content-Type": "application/json" },
249
+ }),
250
+ );
251
+ },
252
+ );
253
+
254
+ const controller = new AbortController();
255
+ await fetchHistoryApi("docs/a.md", { signal: controller.signal });
256
+
257
+ expect(captured).not.toBeNull();
258
+ // タイムアウトと合成済みのため caller signal とは別インスタンス
259
+ expect(captured).not.toBe(controller.signal);
260
+ expect(captured!.aborted).toBe(false);
261
+ controller.abort();
262
+ expect(captured!.aborted).toBe(true);
263
+ });
264
+
265
+ it("caller signal なしでもタイムアウト signal が forward される", async () => {
266
+ const { fetchHistoryApi, setToken } = await import("./api");
267
+ setToken("test-token");
268
+ let captured: AbortSignal | null = null;
269
+ vi.spyOn(globalThis, "fetch").mockImplementation(
270
+ (_input: RequestInfo | URL, init?: RequestInit) => {
271
+ captured = init?.signal ?? null;
272
+ return Promise.resolve(
273
+ new Response(JSON.stringify([]), {
274
+ status: 200,
275
+ headers: { "Content-Type": "application/json" },
276
+ }),
277
+ );
278
+ },
279
+ );
280
+
281
+ await fetchHistoryApi("docs/a.md");
282
+ expect(captured).toBeInstanceOf(AbortSignal);
283
+ });
284
+ });
285
+
286
+ describe("buildRequestSignal", () => {
287
+ it("signal なしのときタイムアウト signal を返す", () => {
288
+ const signal = buildRequestSignal(undefined, 10_000);
289
+ expect(signal).toBeInstanceOf(AbortSignal);
290
+ expect(signal.aborted).toBe(false);
291
+ });
292
+
293
+ it("caller signal を合成し、caller の abort が伝播する", () => {
294
+ const controller = new AbortController();
295
+ const signal = buildRequestSignal(controller.signal, 10_000);
296
+ expect(signal).not.toBe(controller.signal);
297
+ expect(signal.aborted).toBe(false);
298
+ controller.abort();
299
+ expect(signal.aborted).toBe(true);
300
+ });
301
+
302
+ it("タイムアウトが経過すると合成 signal が abort する", async () => {
303
+ const controller = new AbortController();
304
+ const signal = buildRequestSignal(controller.signal, 10);
305
+ expect(signal.aborted).toBe(false);
306
+ await new Promise((resolve) => setTimeout(resolve, 60));
307
+ expect(signal.aborted).toBe(true);
308
+ expect(controller.signal.aborted).toBe(false); // caller 側は abort されない
309
+ });
310
+ });
311
+
232
312
  describe("encodePath", () => {
233
313
  it("encodes special characters in path segments", () => {
234
314
  expect(encodePath("docs/my file.md")).toBe("docs/my%20file.md");
@@ -34,6 +34,18 @@ async function refreshToken(): Promise<string | null> {
34
34
  return newToken;
35
35
  }
36
36
 
37
+ /**
38
+ * caller が signal を渡しても、デフォルトタイムアウトを失わないよう合成する。
39
+ * caller のキャンセル / タイムアウトのどちらか一方が abort すれば合成 signal も abort される。
40
+ */
41
+ export function buildRequestSignal(
42
+ callerSignal: AbortSignal | null | undefined,
43
+ timeoutMs: number,
44
+ ): AbortSignal {
45
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
46
+ return callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
47
+ }
48
+
37
49
  export async function apiFetch(
38
50
  path: string,
39
51
  init: RequestInit = {},
@@ -44,7 +56,7 @@ export async function apiFetch(
44
56
  headers.set("Authorization", `Bearer ${token}`);
45
57
  }
46
58
 
47
- const signal = init.signal ?? AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
59
+ const signal = buildRequestSignal(init.signal, DEFAULT_TIMEOUT_MS);
48
60
 
49
61
  let res = await fetch(`${API_BASE}${path}`, {
50
62
  ...init,
@@ -232,8 +244,11 @@ export type CommitEntry = {
232
244
  date: string;
233
245
  };
234
246
 
235
- export async function fetchHistoryApi(path: string): Promise<CommitEntry[]> {
236
- const res = await apiFetch(`/history/${encodePath(path)}`);
247
+ export async function fetchHistoryApi(
248
+ path: string,
249
+ options: { signal?: AbortSignal } = {},
250
+ ): Promise<CommitEntry[]> {
251
+ const res = await apiFetch(`/history/${encodePath(path)}`, { signal: options.signal });
237
252
  if (!res.ok) {
238
253
  const body = await res.json().catch(() => null);
239
254
  throw new Error(formatApiDetail(body, `Failed to fetch history: ${res.status}`));
@@ -437,6 +452,7 @@ export async function fetchProposalsApi(): Promise<Proposal[]> {
437
452
  export type IssueLabel = {
438
453
  name: string;
439
454
  color: string;
455
+ description?: string;
440
456
  };
441
457
 
442
458
  export type IssueSummary = {
@@ -470,11 +486,19 @@ export type IssueUpdateInput = {
470
486
  };
471
487
 
472
488
  export async function listIssuesApi(
473
- params: { state?: string; labels?: string; page?: number; per_page?: number; signal?: AbortSignal } = {},
489
+ params: {
490
+ state?: string;
491
+ labels?: string;
492
+ assignee?: string;
493
+ page?: number;
494
+ per_page?: number;
495
+ signal?: AbortSignal;
496
+ } = {},
474
497
  ): Promise<{ issues: IssueSummary[] }> {
475
498
  const q = new URLSearchParams();
476
499
  if (params.state) q.set("state", params.state);
477
500
  if (params.labels) q.set("labels", params.labels);
501
+ if (params.assignee) q.set("assignee", params.assignee);
478
502
  if (params.page) q.set("page", String(params.page));
479
503
  if (params.per_page) q.set("per_page", String(params.per_page));
480
504
  const qs = q.toString();
@@ -524,6 +548,42 @@ export async function updateIssueApi(
524
548
  return res.json();
525
549
  }
526
550
 
551
+ export type IssueComment = {
552
+ id: number;
553
+ body: string;
554
+ author: string;
555
+ createdAt: string;
556
+ updatedAt: string;
557
+ url: string;
558
+ };
559
+
560
+ export async function listIssueCommentsApi(
561
+ number: number,
562
+ ): Promise<{ comments: IssueComment[] }> {
563
+ const res = await apiFetch(`/issues/${number}/comments`);
564
+ if (!res.ok) {
565
+ const body = await res.json().catch(() => null);
566
+ throw new Error(formatApiDetail(body, `Failed to list issue comments: ${res.status}`));
567
+ }
568
+ return res.json();
569
+ }
570
+
571
+ export async function createIssueCommentApi(
572
+ number: number,
573
+ body: string,
574
+ ): Promise<IssueComment> {
575
+ const res = await apiFetch(`/issues/${number}/comments`, {
576
+ method: "POST",
577
+ headers: { "Content-Type": "application/json" },
578
+ body: JSON.stringify({ body }),
579
+ });
580
+ if (!res.ok) {
581
+ const resBody = await res.json().catch(() => null);
582
+ throw new Error(formatApiDetail(resBody, `Failed to post issue comment: ${res.status}`));
583
+ }
584
+ return res.json();
585
+ }
586
+
527
587
  export async function listIssueLabelsApi(): Promise<{ labels: IssueLabel[] }> {
528
588
  const res = await apiFetch("/issues/labels");
529
589
  if (!res.ok) {
@@ -552,6 +612,40 @@ export async function createLabelApi(input: LabelCreateInput): Promise<IssueLabe
552
612
  return res.json();
553
613
  }
554
614
 
615
+ export type LabelUpdateInput = {
616
+ new_name?: string;
617
+ color?: string;
618
+ description?: string;
619
+ };
620
+
621
+ function labelPath(name: string): string {
622
+ return `/issues/labels/${encodeURIComponent(name)}`;
623
+ }
624
+
625
+ export async function updateLabelApi(
626
+ name: string,
627
+ input: LabelUpdateInput,
628
+ ): Promise<IssueLabel> {
629
+ const res = await apiFetch(labelPath(name), {
630
+ method: "PATCH",
631
+ headers: { "Content-Type": "application/json" },
632
+ body: JSON.stringify(input),
633
+ });
634
+ if (!res.ok) {
635
+ const body = await res.json().catch(() => null);
636
+ throw new Error(formatApiDetail(body, `Failed to update label: ${res.status}`));
637
+ }
638
+ return res.json();
639
+ }
640
+
641
+ export async function deleteLabelApi(name: string): Promise<void> {
642
+ const res = await apiFetch(labelPath(name), { method: "DELETE" });
643
+ if (!res.ok) {
644
+ const body = await res.json().catch(() => null);
645
+ throw new Error(formatApiDetail(body, `Failed to delete label: ${res.status}`));
646
+ }
647
+ }
648
+
555
649
  // ---------------------------------------------------------------------------
556
650
  // Comments
557
651
  // ---------------------------------------------------------------------------
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { anchorFromPlainTextSelection } from "./commentAnchor";
3
+
4
+ describe("anchorFromPlainTextSelection", () => {
5
+ it("returns anchor with positions when selection exists in full text", () => {
6
+ const full = "Hello world";
7
+ const result = anchorFromPlainTextSelection(full, "world");
8
+ expect(result).toEqual({
9
+ anchor_text: "world",
10
+ position_from: 6,
11
+ position_to: 11,
12
+ });
13
+ });
14
+
15
+ it("returns null for empty selection", () => {
16
+ expect(anchorFromPlainTextSelection("Hello", " ")).toBeNull();
17
+ });
18
+
19
+ it("returns null when selection is not found in full text", () => {
20
+ expect(anchorFromPlainTextSelection("Hello", "missing")).toBeNull();
21
+ });
22
+ });
@@ -0,0 +1,28 @@
1
+ export type CommentAnchor = {
2
+ anchor_text: string;
3
+ position_from: number;
4
+ position_to: number;
5
+ };
6
+
7
+ export function anchorFromPlainTextSelection(
8
+ fullText: string,
9
+ selectedText: string,
10
+ ): CommentAnchor | null {
11
+ const anchor_text = selectedText.trim();
12
+ if (!anchor_text) return null;
13
+
14
+ const position_from = fullText.indexOf(anchor_text);
15
+ if (position_from === -1) return null;
16
+
17
+ return {
18
+ anchor_text,
19
+ position_from,
20
+ position_to: position_from + anchor_text.length,
21
+ };
22
+ }
23
+
24
+ export function getWindowSelectionAnchor(fullText: string): CommentAnchor | null {
25
+ const selection = window.getSelection();
26
+ if (!selection || selection.isCollapsed) return null;
27
+ return anchorFromPlainTextSelection(fullText, selection.toString());
28
+ }
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Schema } from "@tiptap/pm/model";
3
+ import { buildHighlightDecorations, textOffsetToPos } from "@/lib/commentHighlight";
4
+ import type { CommentAnchor } from "@/lib/commentAnchor";
5
+
6
+ const schema = new Schema({
7
+ nodes: {
8
+ doc: { content: "block+" },
9
+ paragraph: { content: "inline*", group: "block" },
10
+ text: { group: "inline" },
11
+ },
12
+ });
13
+
14
+ function paragraph(text: string) {
15
+ return schema.node("paragraph", null, text ? [schema.text(text)] : undefined);
16
+ }
17
+
18
+ function docFromParagraphs(...lines: string[]) {
19
+ return schema.node("doc", null, lines.map(paragraph));
20
+ }
21
+
22
+ function plainText(doc: ReturnType<typeof docFromParagraphs>): string {
23
+ return doc.textBetween(0, doc.content.size, "\n");
24
+ }
25
+
26
+ describe("textOffsetToPos", () => {
27
+ it("単一段落内の offset を ProseMirror 位置に変換する", () => {
28
+ const doc = docFromParagraphs("Hello world");
29
+ const text = plainText(doc);
30
+
31
+ expect(textOffsetToPos(doc, 0)).toBe(0);
32
+ expect(doc.textBetween(0, textOffsetToPos(doc, 6)!, "\n")).toBe("Hello ");
33
+ const endPos = textOffsetToPos(doc, text.length);
34
+ expect(endPos).not.toBeNull();
35
+ expect(doc.textBetween(0, endPos!, "\n")).toBe(text);
36
+ });
37
+
38
+ it("段落境界の \\n を plain-text offset に含める", () => {
39
+ const doc = docFromParagraphs("Line one", "Line two");
40
+ const text = plainText(doc);
41
+ const secondLineStart = text.indexOf("Line two");
42
+
43
+ expect(secondLineStart).toBeGreaterThan(0);
44
+ expect(text[secondLineStart - 1]).toBe("\n");
45
+ expect(textOffsetToPos(doc, secondLineStart)).not.toBeNull();
46
+ expect(
47
+ doc.textBetween(0, textOffsetToPos(doc, secondLineStart)!, "\n"),
48
+ ).toBe(text.slice(0, secondLineStart));
49
+ });
50
+
51
+ it("範囲外 offset は null を返す", () => {
52
+ const doc = docFromParagraphs("Hello");
53
+ expect(textOffsetToPos(doc, -1)).toBeNull();
54
+ expect(textOffsetToPos(doc, plainText(doc).length + 1)).toBeNull();
55
+ });
56
+ });
57
+
58
+ describe("buildHighlightDecorations", () => {
59
+ it("有効なアンカー範囲に decoration を付与する", () => {
60
+ const doc = docFromParagraphs("Hello world");
61
+ const highlights: CommentAnchor[] = [
62
+ {
63
+ anchor_text: "world",
64
+ position_from: 6,
65
+ position_to: 11,
66
+ },
67
+ ];
68
+
69
+ const from = textOffsetToPos(doc, highlights[0].position_from);
70
+ const to = textOffsetToPos(doc, highlights[0].position_to);
71
+ expect(doc.textBetween(from!, to!, "\n")).toBe("world");
72
+
73
+ const set = buildHighlightDecorations(doc, highlights);
74
+ expect(set.find(0, doc.content.size).length).toBe(1);
75
+ });
76
+
77
+ it("2 段落目のアンカーを正しい位置にハイライトする", () => {
78
+ const doc = docFromParagraphs("First paragraph", "Second target");
79
+ const text = plainText(doc);
80
+ const from = text.indexOf("target");
81
+ const to = from + "target".length;
82
+
83
+ const startPos = textOffsetToPos(doc, from);
84
+ const endPos = textOffsetToPos(doc, to);
85
+ expect(doc.textBetween(startPos!, endPos!, "\n")).toBe("target");
86
+
87
+ const set = buildHighlightDecorations(doc, [
88
+ {
89
+ anchor_text: "target",
90
+ position_from: from,
91
+ position_to: to,
92
+ },
93
+ ]);
94
+
95
+ expect(set.find(0, doc.content.size).length).toBe(1);
96
+ });
97
+
98
+ it("無効な offset は decoration を作らない", () => {
99
+ const doc = docFromParagraphs("Hello");
100
+ const set = buildHighlightDecorations(doc, [
101
+ {
102
+ anchor_text: "missing",
103
+ position_from: 100,
104
+ position_to: 107,
105
+ },
106
+ ]);
107
+
108
+ expect(set.find(0, doc.content.size).length).toBe(0);
109
+ });
110
+ });
@@ -0,0 +1,44 @@
1
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
2
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
3
+ import type { CommentAnchor } from "@/lib/commentAnchor";
4
+
5
+ const PLAIN_TEXT_BLOCK_SEPARATOR = "\n";
6
+
7
+ /**
8
+ * Maps a plain-text offset (same convention as `doc.textBetween(0, pos, "\\n")`)
9
+ * to a ProseMirror document position.
10
+ */
11
+ export function textOffsetToPos(doc: ProseMirrorNode, targetOffset: number): number | null {
12
+ if (targetOffset < 0) return null;
13
+
14
+ const maxPos = doc.content.size;
15
+ for (let pos = 0; pos <= maxPos; pos++) {
16
+ if (doc.textBetween(0, pos, PLAIN_TEXT_BLOCK_SEPARATOR).length === targetOffset) {
17
+ return pos;
18
+ }
19
+ }
20
+
21
+ return null;
22
+ }
23
+
24
+ export function buildHighlightDecorations(
25
+ doc: ProseMirrorNode,
26
+ highlights: CommentAnchor[],
27
+ ): DecorationSet {
28
+ const decorations: Decoration[] = [];
29
+
30
+ for (const highlight of highlights) {
31
+ const from = textOffsetToPos(doc, highlight.position_from);
32
+ const to = textOffsetToPos(doc, highlight.position_to);
33
+ if (from != null && to != null && from < to) {
34
+ decorations.push(
35
+ Decoration.inline(from, to, {
36
+ class: "comment-anchor-highlight",
37
+ title: highlight.anchor_text,
38
+ }),
39
+ );
40
+ }
41
+ }
42
+
43
+ return DecorationSet.create(doc, decorations);
44
+ }