collavre 0.1.0

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 (410) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +221 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/stylesheets/collavre/actiontext.css +577 -0
  5. data/app/assets/stylesheets/collavre/activity_logs.css +99 -0
  6. data/app/assets/stylesheets/collavre/comments_popup.css +692 -0
  7. data/app/assets/stylesheets/collavre/creatives.css +559 -0
  8. data/app/assets/stylesheets/collavre/dark_mode.css +118 -0
  9. data/app/assets/stylesheets/collavre/mention_menu.css +43 -0
  10. data/app/assets/stylesheets/collavre/popup.css +160 -0
  11. data/app/assets/stylesheets/collavre/print.css +37 -0
  12. data/app/assets/stylesheets/collavre/slide_view.css +79 -0
  13. data/app/assets/stylesheets/collavre/user_menu.css +34 -0
  14. data/app/channels/collavre/comments_presence_channel.rb +54 -0
  15. data/app/channels/collavre/slide_view_channel.rb +11 -0
  16. data/app/channels/collavre/topics_channel.rb +12 -0
  17. data/app/components/collavre/avatar_component.html.erb +15 -0
  18. data/app/components/collavre/avatar_component.rb +59 -0
  19. data/app/components/collavre/inbox/badge_component.html.erb +6 -0
  20. data/app/components/collavre/inbox/badge_component.rb +18 -0
  21. data/app/components/collavre/plans_timeline_component.html.erb +14 -0
  22. data/app/components/collavre/plans_timeline_component.rb +56 -0
  23. data/app/components/collavre/popup_menu_component.html.erb +6 -0
  24. data/app/components/collavre/popup_menu_component.rb +30 -0
  25. data/app/components/collavre/progress_filter_component.html.erb +5 -0
  26. data/app/components/collavre/progress_filter_component.rb +10 -0
  27. data/app/components/collavre/user_mention_menu_component.html.erb +3 -0
  28. data/app/components/collavre/user_mention_menu_component.rb +8 -0
  29. data/app/controllers/collavre/application_controller.rb +15 -0
  30. data/app/controllers/collavre/attachments_controller.rb +44 -0
  31. data/app/controllers/collavre/calendar_events_controller.rb +15 -0
  32. data/app/controllers/collavre/comment_read_pointers_controller.rb +80 -0
  33. data/app/controllers/collavre/comments/activity_logs_controller.rb +26 -0
  34. data/app/controllers/collavre/comments/reactions_controller.rb +82 -0
  35. data/app/controllers/collavre/comments_controller.rb +464 -0
  36. data/app/controllers/collavre/contacts_controller.rb +10 -0
  37. data/app/controllers/collavre/creative_expanded_states_controller.rb +27 -0
  38. data/app/controllers/collavre/creative_imports_controller.rb +24 -0
  39. data/app/controllers/collavre/creative_plans_controller.rb +69 -0
  40. data/app/controllers/collavre/creative_shares_controller.rb +79 -0
  41. data/app/controllers/collavre/creatives_controller.rb +535 -0
  42. data/app/controllers/collavre/devices_controller.rb +19 -0
  43. data/app/controllers/collavre/email_verifications_controller.rb +16 -0
  44. data/app/controllers/collavre/emails_controller.rb +11 -0
  45. data/app/controllers/collavre/github_auth_controller.rb +25 -0
  46. data/app/controllers/collavre/google_auth_controller.rb +43 -0
  47. data/app/controllers/collavre/inbox_items_controller.rb +64 -0
  48. data/app/controllers/collavre/invites_controller.rb +27 -0
  49. data/app/controllers/collavre/notion_auth_controller.rb +25 -0
  50. data/app/controllers/collavre/passwords_controller.rb +37 -0
  51. data/app/controllers/collavre/plans_controller.rb +110 -0
  52. data/app/controllers/collavre/sessions_controller.rb +57 -0
  53. data/app/controllers/collavre/topics_controller.rb +58 -0
  54. data/app/controllers/collavre/user_themes_controller.rb +58 -0
  55. data/app/controllers/collavre/users_controller.rb +390 -0
  56. data/app/helpers/collavre/application_helper.rb +4 -0
  57. data/app/helpers/collavre/comments_helper.rb +9 -0
  58. data/app/helpers/collavre/creatives_helper.rb +343 -0
  59. data/app/helpers/collavre/navigation_helper.rb +163 -0
  60. data/app/helpers/collavre/user_themes_helper.rb +4 -0
  61. data/app/javascript/collavre.js +26 -0
  62. data/app/javascript/components/InlineLexicalEditor.jsx +889 -0
  63. data/app/javascript/components/LinkPopup.jsx +112 -0
  64. data/app/javascript/components/creative_tree_row.js +503 -0
  65. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +95 -0
  66. data/app/javascript/components/plugins/image_upload_plugin.jsx +162 -0
  67. data/app/javascript/controllers/click_target_controller.js +13 -0
  68. data/app/javascript/controllers/comment_controller.js +162 -0
  69. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +68 -0
  70. data/app/javascript/controllers/comments/form_controller.js +530 -0
  71. data/app/javascript/controllers/comments/list_controller.js +715 -0
  72. data/app/javascript/controllers/comments/mention_menu_controller.js +41 -0
  73. data/app/javascript/controllers/comments/popup_controller.js +385 -0
  74. data/app/javascript/controllers/comments/presence_controller.js +311 -0
  75. data/app/javascript/controllers/comments/topics_controller.js +338 -0
  76. data/app/javascript/controllers/common_popup_controller.js +55 -0
  77. data/app/javascript/controllers/creatives/drag_drop_controller.js +45 -0
  78. data/app/javascript/controllers/creatives/expansion_controller.js +222 -0
  79. data/app/javascript/controllers/creatives/import_controller.js +116 -0
  80. data/app/javascript/controllers/creatives/row_editor_controller.js +8 -0
  81. data/app/javascript/controllers/creatives/select_mode_controller.js +231 -0
  82. data/app/javascript/controllers/creatives/set_plan_modal_controller.js +107 -0
  83. data/app/javascript/controllers/creatives/tree_controller.js +218 -0
  84. data/app/javascript/controllers/index.js +79 -0
  85. data/app/javascript/controllers/link_creative_controller.js +91 -0
  86. data/app/javascript/controllers/popup_menu_controller.js +82 -0
  87. data/app/javascript/controllers/progress_filter_controller.js +35 -0
  88. data/app/javascript/controllers/reaction_picker_controller.js +107 -0
  89. data/app/javascript/controllers/share_invite_controller.js +15 -0
  90. data/app/javascript/controllers/share_user_search_controller.js +121 -0
  91. data/app/javascript/controllers/tabs_controller.js +43 -0
  92. data/app/javascript/creatives/drag_drop/dom.js +170 -0
  93. data/app/javascript/creatives/drag_drop/event_handlers.js +846 -0
  94. data/app/javascript/creatives/drag_drop/indicator.js +35 -0
  95. data/app/javascript/creatives/drag_drop/operations.js +116 -0
  96. data/app/javascript/creatives/drag_drop/state.js +31 -0
  97. data/app/javascript/creatives/tree_renderer.js +248 -0
  98. data/app/javascript/lib/api/__tests__/queue_manager.test.js +153 -0
  99. data/app/javascript/lib/api/creatives.js +79 -0
  100. data/app/javascript/lib/api/csrf_fetch.js +22 -0
  101. data/app/javascript/lib/api/drag_drop.js +31 -0
  102. data/app/javascript/lib/api/queue_manager.js +423 -0
  103. data/app/javascript/lib/apply_lexical_styles.js +15 -0
  104. data/app/javascript/lib/common_popup.js +195 -0
  105. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +91 -0
  106. data/app/javascript/lib/lexical/__tests__/attachment_payload.test.js +194 -0
  107. data/app/javascript/lib/lexical/action_text_attachment_node.js +459 -0
  108. data/app/javascript/lib/lexical/attachment_node.jsx +170 -0
  109. data/app/javascript/lib/lexical/attachment_payload.js +293 -0
  110. data/app/javascript/lib/lexical/dom_attachment_utils.js +66 -0
  111. data/app/javascript/lib/lexical/image_node.jsx +159 -0
  112. data/app/javascript/lib/lexical/style_attributes.js +40 -0
  113. data/app/javascript/lib/responsive_images.js +54 -0
  114. data/app/javascript/lib/turbo_stream_actions.js +33 -0
  115. data/app/javascript/lib/utils/markdown.js +23 -0
  116. data/app/javascript/modules/creative_guide.js +53 -0
  117. data/app/javascript/modules/creative_row_editor.js +1841 -0
  118. data/app/javascript/modules/creative_row_swipe.js +43 -0
  119. data/app/javascript/modules/creatives.js +15 -0
  120. data/app/javascript/modules/export_to_markdown.js +34 -0
  121. data/app/javascript/modules/inbox_panel.js +226 -0
  122. data/app/javascript/modules/lexical_inline_editor.jsx +133 -0
  123. data/app/javascript/modules/mention_menu.js +77 -0
  124. data/app/javascript/modules/plans_menu.js +39 -0
  125. data/app/javascript/modules/plans_timeline.js +397 -0
  126. data/app/javascript/modules/share_modal.js +73 -0
  127. data/app/javascript/modules/share_user_popup.js +77 -0
  128. data/app/javascript/modules/slide_view.js +163 -0
  129. data/app/javascript/services/cable.js +32 -0
  130. data/app/javascript/slide_view.js +2 -0
  131. data/app/javascript/utils/caret_position.js +42 -0
  132. data/app/javascript/utils/clipboard.js +40 -0
  133. data/app/jobs/collavre/ai_agent_job.rb +27 -0
  134. data/app/jobs/collavre/inbox_summary_job.rb +24 -0
  135. data/app/jobs/collavre/notion_export_job.rb +30 -0
  136. data/app/jobs/collavre/notion_sync_job.rb +48 -0
  137. data/app/jobs/collavre/permission_cache_cleanup_job.rb +36 -0
  138. data/app/jobs/collavre/permission_cache_job.rb +71 -0
  139. data/app/jobs/collavre/push_notification_job.rb +86 -0
  140. data/app/mailers/collavre/application_mailer.rb +17 -0
  141. data/app/mailers/collavre/creative_mailer.rb +9 -0
  142. data/app/mailers/collavre/email_verification_mailer.rb +20 -0
  143. data/app/mailers/collavre/inbox_mailer.rb +19 -0
  144. data/app/mailers/collavre/invitation_mailer.rb +16 -0
  145. data/app/mailers/collavre/passwords_mailer.rb +10 -0
  146. data/app/models/collavre/activity_log.rb +13 -0
  147. data/app/models/collavre/application_record.rb +5 -0
  148. data/app/models/collavre/calendar_event.rb +20 -0
  149. data/app/models/collavre/comment.rb +307 -0
  150. data/app/models/collavre/comment_presence_store.rb +30 -0
  151. data/app/models/collavre/comment_reaction.rb +11 -0
  152. data/app/models/collavre/comment_read_pointer.rb +26 -0
  153. data/app/models/collavre/contact.rb +23 -0
  154. data/app/models/collavre/creative.rb +413 -0
  155. data/app/models/collavre/creative_expanded_state.rb +11 -0
  156. data/app/models/collavre/creative_share.rb +122 -0
  157. data/app/models/collavre/creative_shares_cache.rb +18 -0
  158. data/app/models/collavre/current.rb +14 -0
  159. data/app/models/collavre/device.rb +13 -0
  160. data/app/models/collavre/email.rb +14 -0
  161. data/app/models/collavre/github_account.rb +10 -0
  162. data/app/models/collavre/github_repository_link.rb +19 -0
  163. data/app/models/collavre/inbox_item.rb +95 -0
  164. data/app/models/collavre/invitation.rb +22 -0
  165. data/app/models/collavre/label.rb +47 -0
  166. data/app/models/collavre/mcp_tool.rb +30 -0
  167. data/app/models/collavre/notion_account.rb +17 -0
  168. data/app/models/collavre/notion_block_link.rb +10 -0
  169. data/app/models/collavre/notion_page_link.rb +19 -0
  170. data/app/models/collavre/plan.rb +20 -0
  171. data/app/models/collavre/session.rb +24 -0
  172. data/app/models/collavre/system_setting.rb +144 -0
  173. data/app/models/collavre/tag.rb +10 -0
  174. data/app/models/collavre/task.rb +10 -0
  175. data/app/models/collavre/task_action.rb +10 -0
  176. data/app/models/collavre/topic.rb +12 -0
  177. data/app/models/collavre/user.rb +174 -0
  178. data/app/models/collavre/user_theme.rb +10 -0
  179. data/app/models/collavre/variation.rb +5 -0
  180. data/app/models/collavre/webauthn_credential.rb +11 -0
  181. data/app/services/collavre/ai_agent_service.rb +193 -0
  182. data/app/services/collavre/ai_client.rb +183 -0
  183. data/app/services/collavre/ai_system_prompt_renderer.rb +38 -0
  184. data/app/services/collavre/auto_theme_generator.rb +198 -0
  185. data/app/services/collavre/comment_link_formatter.rb +60 -0
  186. data/app/services/collavre/comments/action_executor.rb +262 -0
  187. data/app/services/collavre/comments/action_validator.rb +58 -0
  188. data/app/services/collavre/comments/calendar_command.rb +97 -0
  189. data/app/services/collavre/comments/command_processor.rb +37 -0
  190. data/app/services/collavre/comments/mcp_command.rb +109 -0
  191. data/app/services/collavre/comments/mcp_command_builder.rb +32 -0
  192. data/app/services/collavre/creatives/filter_pipeline.rb +196 -0
  193. data/app/services/collavre/creatives/filters/assignee_filter.rb +30 -0
  194. data/app/services/collavre/creatives/filters/base_filter.rb +24 -0
  195. data/app/services/collavre/creatives/filters/comment_filter.rb +21 -0
  196. data/app/services/collavre/creatives/filters/date_filter.rb +58 -0
  197. data/app/services/collavre/creatives/filters/progress_filter.rb +25 -0
  198. data/app/services/collavre/creatives/filters/search_filter.rb +28 -0
  199. data/app/services/collavre/creatives/filters/tag_filter.rb +16 -0
  200. data/app/services/collavre/creatives/importer.rb +47 -0
  201. data/app/services/collavre/creatives/index_query.rb +191 -0
  202. data/app/services/collavre/creatives/path_exporter.rb +131 -0
  203. data/app/services/collavre/creatives/permission_cache_builder.rb +194 -0
  204. data/app/services/collavre/creatives/permission_checker.rb +42 -0
  205. data/app/services/collavre/creatives/plan_tagger.rb +53 -0
  206. data/app/services/collavre/creatives/progress_service.rb +89 -0
  207. data/app/services/collavre/creatives/reorderer.rb +187 -0
  208. data/app/services/collavre/creatives/tree_builder.rb +231 -0
  209. data/app/services/collavre/creatives/tree_formatter.rb +36 -0
  210. data/app/services/collavre/gemini_parent_recommender.rb +77 -0
  211. data/app/services/collavre/github/client.rb +112 -0
  212. data/app/services/collavre/github/pull_request_analyzer.rb +280 -0
  213. data/app/services/collavre/github/pull_request_processor.rb +181 -0
  214. data/app/services/collavre/github/webhook_provisioner.rb +130 -0
  215. data/app/services/collavre/google_calendar_service.rb +149 -0
  216. data/app/services/collavre/link_preview_fetcher.rb +230 -0
  217. data/app/services/collavre/markdown_importer.rb +202 -0
  218. data/app/services/collavre/mcp_service.rb +217 -0
  219. data/app/services/collavre/notion_client.rb +231 -0
  220. data/app/services/collavre/notion_creative_exporter.rb +296 -0
  221. data/app/services/collavre/notion_service.rb +249 -0
  222. data/app/services/collavre/ppt_importer.rb +76 -0
  223. data/app/services/collavre/ruby_llm_interaction_logger.rb +34 -0
  224. data/app/services/collavre/system_events/context_builder.rb +41 -0
  225. data/app/services/collavre/system_events/dispatcher.rb +19 -0
  226. data/app/services/collavre/system_events/router.rb +72 -0
  227. data/app/services/collavre/tools/creative_retrieval_service.rb +138 -0
  228. data/app/views/admin/shared/_tabs.html.erb +4 -0
  229. data/app/views/collavre/comments/_activity_log_details.html.erb +23 -0
  230. data/app/views/collavre/comments/_comment.html.erb +147 -0
  231. data/app/views/collavre/comments/_comments_popup.html.erb +46 -0
  232. data/app/views/collavre/comments/_list.html.erb +10 -0
  233. data/app/views/collavre/comments/_presence_avatars.html.erb +8 -0
  234. data/app/views/collavre/comments/_reaction_picker.html.erb +15 -0
  235. data/app/views/collavre/comments/_read_receipts.html.erb +19 -0
  236. data/app/views/collavre/creatives/_add_button.html.erb +20 -0
  237. data/app/views/collavre/creatives/_delete_button.html.erb +12 -0
  238. data/app/views/collavre/creatives/_github_integration_modal.html.erb +77 -0
  239. data/app/views/collavre/creatives/_import_upload_zone.html.erb +10 -0
  240. data/app/views/collavre/creatives/_inline_edit_form.html.erb +89 -0
  241. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +24 -0
  242. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +90 -0
  243. data/app/views/collavre/creatives/_set_plan_modal.html.erb +25 -0
  244. data/app/views/collavre/creatives/_share_button.html.erb +55 -0
  245. data/app/views/collavre/creatives/edit.html.erb +4 -0
  246. data/app/views/collavre/creatives/index.html.erb +192 -0
  247. data/app/views/collavre/creatives/new.html.erb +4 -0
  248. data/app/views/collavre/creatives/show.html.erb +29 -0
  249. data/app/views/collavre/creatives/slide_view.html.erb +20 -0
  250. data/app/views/collavre/email_verification_mailer/verify.html.erb +4 -0
  251. data/app/views/collavre/email_verification_mailer/verify.text.erb +2 -0
  252. data/app/views/collavre/emails/index.html.erb +19 -0
  253. data/app/views/collavre/emails/show.html.erb +5 -0
  254. data/app/views/collavre/inbox_items/_item.html.erb +18 -0
  255. data/app/views/collavre/inbox_items/_items.html.erb +3 -0
  256. data/app/views/collavre/inbox_items/_list.html.erb +7 -0
  257. data/app/views/collavre/inbox_items/index.html.erb +1 -0
  258. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +11 -0
  259. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +8 -0
  260. data/app/views/collavre/invitation_mailer/invite.html.erb +5 -0
  261. data/app/views/collavre/invitation_mailer/invite.text.erb +3 -0
  262. data/app/views/collavre/invites/show.html.erb +8 -0
  263. data/app/views/collavre/passwords/edit.html.erb +12 -0
  264. data/app/views/collavre/passwords/new.html.erb +17 -0
  265. data/app/views/collavre/passwords_mailer/reset.html.erb +4 -0
  266. data/app/views/collavre/passwords_mailer/reset.text.erb +2 -0
  267. data/app/views/collavre/sessions/new.html.erb +27 -0
  268. data/app/views/collavre/sessions/providers/_google.html.erb +7 -0
  269. data/app/views/collavre/sessions/providers/_passkey.html.erb +3 -0
  270. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -0
  271. data/app/views/collavre/shared/_navigation.html.erb +37 -0
  272. data/app/views/collavre/shared/navigation/_help_button.html.erb +1 -0
  273. data/app/views/collavre/shared/navigation/_inbox_button.html.erb +5 -0
  274. data/app/views/collavre/shared/navigation/_mobile_inbox_button.html.erb +3 -0
  275. data/app/views/collavre/shared/navigation/_mobile_plans_button.html.erb +1 -0
  276. data/app/views/collavre/shared/navigation/_panels.html.erb +18 -0
  277. data/app/views/collavre/shared/navigation/_plans_button.html.erb +3 -0
  278. data/app/views/collavre/shared/navigation/_search_form.html.erb +6 -0
  279. data/app/views/collavre/user_themes/index.html.erb +75 -0
  280. data/app/views/collavre/users/_app_header.html.erb +5 -0
  281. data/app/views/collavre/users/_contact_management.html.erb +77 -0
  282. data/app/views/collavre/users/_id_pwd_fields.html.erb +27 -0
  283. data/app/views/collavre/users/edit_ai.html.erb +79 -0
  284. data/app/views/collavre/users/edit_password.html.erb +27 -0
  285. data/app/views/collavre/users/index.html.erb +80 -0
  286. data/app/views/collavre/users/new.html.erb +33 -0
  287. data/app/views/collavre/users/new_ai.html.erb +87 -0
  288. data/app/views/collavre/users/passkeys.html.erb +43 -0
  289. data/app/views/collavre/users/show.html.erb +143 -0
  290. data/app/views/inbox/badge_component/_count.html.erb +2 -0
  291. data/app/views/layouts/collavre/slide.html.erb +29 -0
  292. data/config/locales/comments.en.yml +114 -0
  293. data/config/locales/comments.ko.yml +110 -0
  294. data/config/locales/contacts.en.yml +16 -0
  295. data/config/locales/contacts.ko.yml +16 -0
  296. data/config/locales/creatives.en.yml +183 -0
  297. data/config/locales/creatives.ko.yml +164 -0
  298. data/config/locales/invites.en.yml +19 -0
  299. data/config/locales/invites.ko.yml +17 -0
  300. data/config/locales/notifications.en.yml +8 -0
  301. data/config/locales/notifications.ko.yml +8 -0
  302. data/config/locales/plans.en.yml +12 -0
  303. data/config/locales/plans.ko.yml +19 -0
  304. data/config/locales/themes.en.yml +29 -0
  305. data/config/locales/themes.ko.yml +27 -0
  306. data/config/locales/users.en.yml +151 -0
  307. data/config/locales/users.ko.yml +146 -0
  308. data/config/routes.rb +92 -0
  309. data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
  310. data/db/migrate/20250128110017_create_creatives.rb +11 -0
  311. data/db/migrate/20250128120122_create_users.rb +11 -0
  312. data/db/migrate/20250128120123_create_sessions.rb +11 -0
  313. data/db/migrate/20250128123633_create_subscribers.rb +10 -0
  314. data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
  315. data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
  316. data/db/migrate/20250522115048_remove_name_from_creatives.rb +5 -0
  317. data/db/migrate/20250522190651_add_parent_id_to_creatives.rb +5 -0
  318. data/db/migrate/20250523133100_rename_inventory_count_to_progress.rb +13 -0
  319. data/db/migrate/20250523133101_add_sequence_to_creatives.rb +5 -0
  320. data/db/migrate/20250525205100_add_user_id_to_creatives.rb +10 -0
  321. data/db/migrate/20250527014217_create_creative_shares.rb +12 -0
  322. data/db/migrate/20250528142349_add_origin_id_to_creatives.rb +5 -0
  323. data/db/migrate/20250530060200_create_tags.rb +9 -0
  324. data/db/migrate/20250531105150_add_value_to_tags.rb +5 -0
  325. data/db/migrate/20250531140142_create_labels.rb +12 -0
  326. data/db/migrate/20250531140145_change_tag_to_label_reference.rb +6 -0
  327. data/db/migrate/20250601000000_create_comments.rb +10 -0
  328. data/db/migrate/20250601061830_drop_plans_and_variations.rb +6 -0
  329. data/db/migrate/20250604122600_add_owner_to_labels.rb +5 -0
  330. data/db/migrate/20250606000000_rename_email_address_to_email_in_users.rb +7 -0
  331. data/db/migrate/20250606150329_create_creative_hierarchies.rb +16 -0
  332. data/db/migrate/20250610142000_create_creative_expanded_states.rb +11 -0
  333. data/db/migrate/20250611030138_create_invitations.rb +15 -0
  334. data/db/migrate/20250611105524_create_inbox_items.rb +14 -0
  335. data/db/migrate/20250612150000_update_permissions.rb +43 -0
  336. data/db/migrate/20250612232913_add_email_verified_at_to_users.rb +5 -0
  337. data/db/migrate/20250616065905_add_avatar_to_users.rb +5 -0
  338. data/db/migrate/20250617092111_add_display_level_to_users.rb +5 -0
  339. data/db/migrate/20250620004558_create_emails.rb +13 -0
  340. data/db/migrate/20250621000000_create_comment_read_pointers.rb +11 -0
  341. data/db/migrate/20250622000000_add_completion_mark_to_users.rb +5 -0
  342. data/db/migrate/20250623000000_add_theme_to_users.rb +5 -0
  343. data/db/migrate/20250624000000_add_name_to_users.rb +18 -0
  344. data/db/migrate/20250714190000_create_devices.rb +17 -0
  345. data/db/migrate/20250715120000_add_notifications_enabled_to_users.rb +5 -0
  346. data/db/migrate/20250823000000_add_no_access_permission.rb +25 -0
  347. data/db/migrate/20250826000000_add_calendar_id_to_users.rb +5 -0
  348. data/db/migrate/20250827000000_add_google_oauth_tokens_to_users.rb +8 -0
  349. data/db/migrate/20250827061238_add_timezone_to_users.rb +5 -0
  350. data/db/migrate/20250828000000_create_calendar_events.rb +16 -0
  351. data/db/migrate/20250828060000_add_creative_to_calendar_events.rb +5 -0
  352. data/db/migrate/20250830141052_add_locale_to_users.rb +5 -0
  353. data/db/migrate/20250830141101_add_message_key_to_inbox_items.rb +6 -0
  354. data/db/migrate/20250902025423_remove_description_and_featured_image_from_creatives.rb +6 -0
  355. data/db/migrate/20250910000000_add_private_to_comments.rb +5 -0
  356. data/db/migrate/20250910105640_migrate_write_permissions_to_admin.rb +17 -0
  357. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +27 -0
  358. data/db/migrate/20250923002959_deduplicate_device_fcm_tokens.rb +25 -0
  359. data/db/migrate/20250924000000_add_system_admin_to_users.rb +6 -0
  360. data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
  361. data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
  362. data/db/migrate/20250928000000_add_action_and_approver_to_comments.rb +6 -0
  363. data/db/migrate/20250928010000_add_action_execution_tracking_to_comments.rb +6 -0
  364. data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
  365. data/db/migrate/20250929000000_add_comment_and_creative_refs_to_inbox_items.rb +6 -0
  366. data/db/migrate/20251001000001_create_contacts.rb +71 -0
  367. data/db/migrate/20251002000000_add_shared_by_to_creative_shares.rb +14 -0
  368. data/db/migrate/20251124120902_add_ai_fields_to_users.rb +7 -0
  369. data/db/migrate/20251124122218_add_created_by_id_to_users.rb +5 -0
  370. data/db/migrate/20251124124521_add_llm_api_key_to_users.rb +5 -0
  371. data/db/migrate/20251124130000_add_searchable_to_users.rb +6 -0
  372. data/db/migrate/20251125072705_migrate_linked_creative_children_to_origin.rb +28 -0
  373. data/db/migrate/20251126040752_add_description_to_creatives.rb +75 -0
  374. data/db/migrate/20251127000000_move_comments_from_linked_creatives_to_origins.rb +18 -0
  375. data/db/migrate/20251201073823_add_tools_to_users.rb +5 -0
  376. data/db/migrate/20251202062715_create_mcp_tools.rb +16 -0
  377. data/db/migrate/20251204125754_create_tasks_and_task_actions.rb +23 -0
  378. data/db/migrate/20251204125756_add_routing_expression_to_users.rb +5 -0
  379. data/db/migrate/20251204161133_set_default_routing_expression_for_ai_agents.rb +13 -0
  380. data/db/migrate/20251211033025_nullify_self_referencing_origins.rb +9 -0
  381. data/db/migrate/20251211080040_create_topics_and_add_topic_to_comments.rb +15 -0
  382. data/db/migrate/20251215143500_create_activity_logs.rb +16 -0
  383. data/db/migrate/20251222125727_create_webauthn_credentials.rb +14 -0
  384. data/db/migrate/20251222125839_add_webauthn_id_to_users.rb +5 -0
  385. data/db/migrate/20251223022315_create_comment_reactions.rb +13 -0
  386. data/db/migrate/20251223072625_create_user_themes.rb +11 -0
  387. data/db/migrate/20251230074456_add_creative_id_to_labels.rb +5 -0
  388. data/db/migrate/20251230113607_refactor_labels.rb +38 -0
  389. data/db/migrate/20251231010012_backfill_tags_for_labels.rb +15 -0
  390. data/db/migrate/20251231013234_drop_subscribers.rb +9 -0
  391. data/db/migrate/20260106090544_allow_null_user_id_in_creative_shares.rb +6 -0
  392. data/db/migrate/20260106160643_create_system_settings.rb +11 -0
  393. data/db/migrate/20260116000000_create_creative_shares_cache.rb +15 -0
  394. data/db/migrate/20260116000001_populate_creative_shares_cache.rb +23 -0
  395. data/db/migrate/20260119022933_make_source_share_id_nullable_in_creative_shares_caches.rb +7 -0
  396. data/db/migrate/20260119023446_populate_owner_cache_entries.rb +25 -0
  397. data/db/migrate/20260119100000_add_account_lockout_to_users.rb +6 -0
  398. data/db/migrate/20260119110000_add_last_active_at_to_sessions.rb +6 -0
  399. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +60 -0
  400. data/db/migrate/20260120162259_remove_fk_from_creative_shares_caches.rb +7 -0
  401. data/db/migrate/20260120163856_remove_timestamps_from_creative_shares_caches.rb +6 -0
  402. data/lib/collavre/configuration.rb +14 -0
  403. data/lib/collavre/engine.rb +77 -0
  404. data/lib/collavre/user_extensions.rb +29 -0
  405. data/lib/collavre/version.rb +3 -0
  406. data/lib/collavre.rb +26 -0
  407. data/lib/generators/collavre/install/install_generator.rb +105 -0
  408. data/lib/generators/collavre/install/templates/build.cjs.tt +100 -0
  409. data/lib/tasks/collavre_assets.rake +15 -0
  410. metadata +591 -0
@@ -0,0 +1,293 @@
1
+ const DEFAULT_STATUS = "ready"
2
+
3
+ function coerceNumber(value) {
4
+ if (value === null || value === undefined) return null
5
+ const parsed = Number(value)
6
+ if (!Number.isFinite(parsed)) return null
7
+ return parsed
8
+ }
9
+
10
+ function isImageContentType(contentType) {
11
+ return typeof contentType === "string" && /^image\//i.test(contentType)
12
+ }
13
+
14
+ export function parseDimension(value) {
15
+ if (value === null || value === undefined) return null
16
+ if (typeof value === "number" && Number.isFinite(value)) return value
17
+ const match = String(value).trim().match(/([0-9]+(?:\.[0-9]+)?)/)
18
+ if (!match) return null
19
+ const parsed = parseFloat(match[1])
20
+ return Number.isFinite(parsed) ? parsed : null
21
+ }
22
+
23
+ export function formatFileSize(bytes) {
24
+ const value = coerceNumber(bytes)
25
+ if (!Number.isFinite(value) || value <= 0) return ""
26
+ const units = ["B", "KB", "MB", "GB", "TB"]
27
+ let size = value
28
+ let unitIndex = 0
29
+ while (size >= 1024 && unitIndex < units.length - 1) {
30
+ size /= 1024
31
+ unitIndex += 1
32
+ }
33
+ return `${size % 1 === 0 ? size : size.toFixed(1)} ${units[unitIndex]}`
34
+ }
35
+
36
+ export function normalizeAttachmentCaption(rawCaption, {filename, filesize}) {
37
+ if (!rawCaption) return ""
38
+ const normalized = rawCaption.replace(/•/g, " ").replace(/\s+/g, " ").trim()
39
+ if (!normalized) return ""
40
+ const defaults = new Set()
41
+ if (filename) {
42
+ defaults.add(filename.toLowerCase())
43
+ }
44
+ if (filename && Number.isFinite(filesize)) {
45
+ defaults.add(`${filename} ${formatFileSize(filesize)}`.toLowerCase())
46
+ }
47
+ return defaults.has(normalized.toLowerCase()) ? "" : normalized
48
+ }
49
+
50
+ export function sanitizeAttachmentPayload(raw = {}) {
51
+ const sanitized = {
52
+ sgid: raw.sgid || null,
53
+ url: raw.url || null,
54
+ filename: (raw.filename || "").trim(),
55
+ contentType: raw.contentType || raw["content-type"] || null,
56
+ filesize: coerceNumber(raw.filesize),
57
+ caption: raw.caption || "",
58
+ previewable:
59
+ raw.previewable !== undefined && raw.previewable !== null
60
+ ? Boolean(raw.previewable)
61
+ : false,
62
+ width: parseDimension(raw.width ?? raw.dataWidth ?? raw["data-width"]),
63
+ height: parseDimension(raw.height ?? raw.dataHeight ?? raw["data-height"]),
64
+ status: raw.status || DEFAULT_STATUS,
65
+ progress: Number.isFinite(raw.progress) ? raw.progress : raw.status === "ready" ? 100 : 0,
66
+ localUrl: raw.localUrl || null,
67
+ error: raw.error || null
68
+ }
69
+
70
+ if (!sanitized.previewable) {
71
+ if (isImageContentType(sanitized.contentType)) {
72
+ sanitized.previewable = true
73
+ } else if (typeof sanitized.url === "string") {
74
+ const urlPath = sanitized.url.split("?")[0].toLowerCase()
75
+ const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]
76
+ sanitized.previewable = imageExtensions.some((ext) => urlPath.endsWith(ext))
77
+ }
78
+ }
79
+
80
+ sanitized.caption = normalizeAttachmentCaption((raw.caption || "").trim(), sanitized)
81
+
82
+ return sanitized
83
+ }
84
+
85
+ export function attachmentPayloadFromAttachmentElement(element) {
86
+ if (!(element instanceof Element)) return null
87
+ const payload = sanitizeAttachmentPayload({
88
+ sgid: element.getAttribute("sgid"),
89
+ url: element.getAttribute("url"),
90
+ filename: element.getAttribute("filename"),
91
+ contentType: element.getAttribute("content-type"),
92
+ filesize: element.getAttribute("filesize"),
93
+ caption: element.getAttribute("caption"),
94
+ previewable: element.getAttribute("previewable") === "true",
95
+ width: element.getAttribute("data-width"),
96
+ height: element.getAttribute("data-height")
97
+ })
98
+ return payload
99
+ }
100
+
101
+ export function attachmentPayloadFromFigure(figure) {
102
+ if (!(figure instanceof Element)) return null
103
+ if (!figure.classList.contains("attachment")) return null
104
+ if (figure.closest("action-text-attachment")) return null
105
+ let data = null
106
+ const dataset = figure.getAttribute("data-trix-attachment")
107
+ if (dataset) {
108
+ try {
109
+ data = JSON.parse(dataset)
110
+ } catch (_error) {
111
+ data = null
112
+ }
113
+ }
114
+
115
+ const payload = sanitizeAttachmentPayload({
116
+ sgid: data?.sgid || data?.attachable_sgid || null,
117
+ url: data?.url || data?.href || null,
118
+ filename: data?.filename || data?.name || figure.querySelector(".attachment__name")?.textContent || "",
119
+ contentType: data?.contentType || data?.content_type || figure.getAttribute("data-trix-content-type"),
120
+ filesize: data?.filesize ?? data?.file_size ?? data?.size ?? null,
121
+ previewable: data?.previewable ?? figure.classList.contains("attachment--preview"),
122
+ width: data?.width ?? data?.presentation?.width ?? figure.getAttribute("data-width"),
123
+ height: data?.height ?? data?.presentation?.height ?? figure.getAttribute("data-height"),
124
+ caption: data?.caption || figure.querySelector("figcaption")?.textContent || ""
125
+ })
126
+
127
+ if (!payload.url) {
128
+ const img = figure.querySelector("img")
129
+ if (img) {
130
+ payload.url = img.getAttribute("src") || null
131
+ payload.previewable = true
132
+ payload.width = payload.width || parseDimension(img.getAttribute("data-width") || img.getAttribute("width") || img.style.width)
133
+ payload.height = payload.height || parseDimension(img.getAttribute("data-height") || img.getAttribute("height") || img.style.height)
134
+ if (!payload.caption) {
135
+ payload.caption = normalizeAttachmentCaption(img.getAttribute("alt"), payload)
136
+ }
137
+ }
138
+ }
139
+
140
+ return payload
141
+ }
142
+
143
+ function buildDataTrixAttachment(payload) {
144
+ return JSON.stringify({
145
+ sgid: payload.sgid,
146
+ contentType: payload.contentType,
147
+ filename: payload.filename,
148
+ name: payload.filename,
149
+ filesize: payload.filesize,
150
+ size: payload.filesize,
151
+ url: payload.url,
152
+ href: payload.url,
153
+ previewable: payload.previewable,
154
+ caption: payload.caption,
155
+ presentation: {
156
+ width: payload.width || undefined,
157
+ height: payload.height || undefined
158
+ }
159
+ })
160
+ }
161
+
162
+ export function attachmentPayloadToHTMLElement(payload, options = {}) {
163
+ const {includeFigure = true} = options
164
+ const sanitized = sanitizeAttachmentPayload(payload)
165
+ const element = document.createElement("action-text-attachment")
166
+ if (sanitized.sgid) element.setAttribute("sgid", sanitized.sgid)
167
+ if (sanitized.contentType) element.setAttribute("content-type", sanitized.contentType)
168
+ if (sanitized.url) element.setAttribute("url", sanitized.url)
169
+ if (sanitized.filename) element.setAttribute("filename", sanitized.filename)
170
+ if (Number.isFinite(sanitized.filesize)) element.setAttribute("filesize", String(sanitized.filesize))
171
+ if (sanitized.caption) element.setAttribute("caption", sanitized.caption)
172
+ if (sanitized.previewable) element.setAttribute("previewable", "true")
173
+ if (Number.isFinite(sanitized.width)) element.setAttribute("data-width", String(Math.round(sanitized.width)))
174
+ if (Number.isFinite(sanitized.height)) element.setAttribute("data-height", String(Math.round(sanitized.height)))
175
+
176
+ if (includeFigure) {
177
+ const figure = document.createElement("figure")
178
+ figure.className = `attachment ${sanitized.previewable ? "attachment--preview" : "attachment--file"}`
179
+ if (sanitized.contentType) {
180
+ figure.classList.add(`attachment--${sanitized.contentType.split("/")[1] || sanitized.contentType}`)
181
+ figure.setAttribute("data-trix-content-type", sanitized.contentType)
182
+ }
183
+ figure.setAttribute("data-trix-attachment", buildDataTrixAttachment(sanitized))
184
+
185
+ if (sanitized.previewable && sanitized.url) {
186
+ const img = document.createElement("img")
187
+ img.src = sanitized.url
188
+ img.alt = sanitized.caption || sanitized.filename || ""
189
+ img.style.maxWidth = "100%"
190
+ img.style.height = "auto"
191
+ if (Number.isFinite(sanitized.width)) {
192
+ img.setAttribute("data-width", String(Math.round(sanitized.width)))
193
+ img.style.width = `${Math.round(sanitized.width)}px`
194
+ }
195
+ if (Number.isFinite(sanitized.height)) {
196
+ img.setAttribute("data-height", String(Math.round(sanitized.height)))
197
+ }
198
+ figure.appendChild(img)
199
+ } else {
200
+ const wrapper = document.createElement("div")
201
+ wrapper.className = "attachment__file"
202
+
203
+ const link = document.createElement("a")
204
+ link.href = sanitized.url || "#"
205
+ link.className = "attachment__download"
206
+ if (sanitized.filename) link.setAttribute("download", sanitized.filename)
207
+ link.textContent = sanitized.filename || "Attachment"
208
+ wrapper.appendChild(link)
209
+
210
+ const info = document.createElement("div")
211
+ info.className = "attachment__file-info"
212
+ const nameSpan = document.createElement("span")
213
+ nameSpan.className = "attachment__name"
214
+ nameSpan.textContent = sanitized.filename || "Attachment"
215
+ info.appendChild(nameSpan)
216
+ if (Number.isFinite(sanitized.filesize)) {
217
+ const sizeSpan = document.createElement("span")
218
+ sizeSpan.className = "attachment__size"
219
+ sizeSpan.textContent = formatFileSize(sanitized.filesize)
220
+ info.appendChild(sizeSpan)
221
+ }
222
+ wrapper.appendChild(info)
223
+ figure.appendChild(wrapper)
224
+ }
225
+
226
+ const figcaption = document.createElement("figcaption")
227
+ figcaption.className = "attachment__caption"
228
+ const captionName = document.createElement("span")
229
+ captionName.className = "attachment__name"
230
+ captionName.textContent = sanitized.caption || sanitized.filename || "Attachment"
231
+ figcaption.appendChild(captionName)
232
+ if (Number.isFinite(sanitized.filesize)) {
233
+ const captionSize = document.createElement("span")
234
+ captionSize.className = "attachment__size"
235
+ captionSize.textContent = formatFileSize(sanitized.filesize)
236
+ figcaption.appendChild(captionSize)
237
+ }
238
+ figure.appendChild(figcaption)
239
+
240
+ element.appendChild(figure)
241
+ }
242
+ return {element, payload: sanitized}
243
+ }
244
+
245
+ export function ensureSgid(payload) {
246
+ if (payload.sgid) return payload
247
+ const id = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2)
248
+ return {...payload, sgid: `temp-${id}`}
249
+ }
250
+
251
+ function attachmentIdentity(payload) {
252
+ if (payload.sgid) return `sgid:${payload.sgid}`
253
+ const parts = [payload.url || "", payload.filename || ""]
254
+ const size = Number.isFinite(payload.filesize) ? String(payload.filesize) : ""
255
+ parts.push(size)
256
+ return `meta:${parts.join("|")}`
257
+ }
258
+
259
+ export function canonicalizeAttachmentElements(root, options = {}) {
260
+ const {includeFigure = false} = options
261
+ if (!root || typeof root.querySelectorAll !== "function") return
262
+ const nodes = Array.from(
263
+ root.querySelectorAll("action-text-attachment, figure.attachment")
264
+ )
265
+ let lastKeptKey = null
266
+
267
+ nodes.forEach((node) => {
268
+ if (node.tagName === "FIGURE" && node.closest("action-text-attachment")) {
269
+ return
270
+ }
271
+
272
+ const payload =
273
+ node.tagName === "ACTION-TEXT-ATTACHMENT"
274
+ ? attachmentPayloadFromAttachmentElement(node)
275
+ : attachmentPayloadFromFigure(node)
276
+
277
+ if (!payload) {
278
+ node.remove()
279
+ lastKeptKey = null
280
+ return
281
+ }
282
+
283
+ const key = attachmentIdentity(payload)
284
+ if (lastKeptKey && key === lastKeptKey) {
285
+ node.remove()
286
+ return
287
+ }
288
+
289
+ const {element} = attachmentPayloadToHTMLElement(payload, {includeFigure})
290
+ node.replaceWith(element)
291
+ lastKeptKey = key
292
+ })
293
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ sanitizeAttachmentPayload,
3
+ attachmentPayloadFromAttachmentElement,
4
+ attachmentPayloadFromFigure,
5
+ attachmentPayloadToHTMLElement
6
+ } from "./attachment_payload"
7
+
8
+ export function extractAttachmentPayloadFromDOM(element) {
9
+ if (!(element instanceof Element)) return null
10
+ if (element.tagName === "ACTION-TEXT-ATTACHMENT") {
11
+ return attachmentPayloadFromAttachmentElement(element)
12
+ }
13
+ if (element.tagName === "FIGURE") {
14
+ return attachmentPayloadFromFigure(element)
15
+ }
16
+ return null
17
+ }
18
+
19
+ export function ensureAttachmentWrapper(element) {
20
+ if (!(element instanceof Element)) return null
21
+ const payload = attachmentPayloadFromAttachmentElement(element)
22
+ if (payload) return {element, payload}
23
+
24
+ const figure = element.tagName === "FIGURE" ? element : element.querySelector("figure.attachment")
25
+ if (!figure) return null
26
+ const figurePayload = attachmentPayloadFromFigure(figure)
27
+ if (!figurePayload) return null
28
+ const {element: wrapper, payload: sanitized} = attachmentPayloadToHTMLElement(figurePayload)
29
+ figure.replaceWith(wrapper)
30
+ return {element: wrapper, payload: sanitized}
31
+ }
32
+
33
+ export function serializeAttachmentPayloadToHTML(payload) {
34
+ const {element} = attachmentPayloadToHTMLElement(payload)
35
+ return element.outerHTML
36
+ }
37
+
38
+ export function normalizeAttachmentPair(container) {
39
+ if (!(container instanceof Element)) return
40
+ const attachments = Array.from(container.querySelectorAll("action-text-attachment"))
41
+ const seen = new Set()
42
+
43
+ attachments.forEach((attachment) => {
44
+ const payload = attachmentPayloadFromAttachmentElement(attachment)
45
+ if (!payload) return
46
+ const key = `${payload.sgid || payload.filename}-${payload.url || ""}`
47
+ if (seen.has(key)) {
48
+ attachment.remove()
49
+ return
50
+ }
51
+ seen.add(key)
52
+
53
+ const figure = attachment.querySelector("figure.attachment")
54
+ if (figure) {
55
+ attachmentPayloadToHTMLElement(payload) // ensures figure JSON is in-sync
56
+ }
57
+ })
58
+
59
+ container.querySelectorAll("figure.attachment").forEach((figure) => {
60
+ if (figure.closest("action-text-attachment")) return
61
+ const payload = attachmentPayloadFromFigure(figure)
62
+ if (!payload) return
63
+ const {element} = attachmentPayloadToHTMLElement(payload)
64
+ figure.replaceWith(element)
65
+ })
66
+ }
@@ -0,0 +1,159 @@
1
+ import {
2
+ $applyNodeReplacement,
3
+ createEditor,
4
+ DecoratorNode,
5
+ } from "lexical"
6
+
7
+ export class ImageNode extends DecoratorNode {
8
+ __src
9
+ __altText
10
+ __width
11
+ __height
12
+ __maxWidth
13
+
14
+ static getType() {
15
+ return "image"
16
+ }
17
+
18
+ static clone(node) {
19
+ return new ImageNode(
20
+ node.__src,
21
+ node.__altText,
22
+ node.__maxWidth,
23
+ node.__width,
24
+ node.__height,
25
+ node.__key
26
+ )
27
+ }
28
+
29
+ static importJSON(serializedNode) {
30
+ const { altText, height, width, maxWidth, src } = serializedNode
31
+ const node = $createImageNode({
32
+ altText,
33
+ height,
34
+ maxWidth,
35
+ src,
36
+ width,
37
+ })
38
+ return node
39
+ }
40
+
41
+ exportDOM() {
42
+ const element = document.createElement("img")
43
+ element.setAttribute("src", this.__src)
44
+ element.setAttribute("alt", this.__altText)
45
+ if (this.__width) {
46
+ element.setAttribute("width", this.__width.toString())
47
+ }
48
+ if (this.__height) {
49
+ element.setAttribute("height", this.__height.toString())
50
+ }
51
+ return { element }
52
+ }
53
+
54
+ static importDOM() {
55
+ return {
56
+ img: (node) => ({
57
+ conversion: convertImageElement,
58
+ priority: 0,
59
+ }),
60
+ }
61
+ }
62
+
63
+ constructor(src, altText, maxWidth, width, height, key) {
64
+ super(key)
65
+ this.__src = src
66
+ this.__altText = altText
67
+ this.__maxWidth = maxWidth
68
+ this.__width = width || "inherit"
69
+ this.__height = height || "inherit"
70
+ }
71
+
72
+ exportJSON() {
73
+ return {
74
+ altText: this.getAltText(),
75
+ height: this.__height === "inherit" ? 0 : this.__height,
76
+ maxWidth: this.__maxWidth,
77
+ src: this.getSrc(),
78
+ type: "image",
79
+ version: 1,
80
+ width: this.__width === "inherit" ? 0 : this.__width,
81
+ }
82
+ }
83
+
84
+ setWidthAndHeight(width, height) {
85
+ const writable = this.getWritable()
86
+ writable.__width = width
87
+ writable.__height = height
88
+ }
89
+
90
+ createDOM(config) {
91
+ const span = document.createElement("span")
92
+ const theme = config.theme
93
+ const className = theme.image
94
+ if (className !== undefined) {
95
+ span.className = className
96
+ }
97
+ return span
98
+ }
99
+
100
+ updateDOM() {
101
+ return false
102
+ }
103
+
104
+ getSrc() {
105
+ return this.__src
106
+ }
107
+
108
+ getAltText() {
109
+ return this.__altText
110
+ }
111
+
112
+ decorate() {
113
+ const handleClick = (e) => {
114
+ // Stop propagation to prevent creative-row navigation
115
+ // and prevent default to stop parent link navigation
116
+ e.stopPropagation()
117
+ e.preventDefault()
118
+ }
119
+
120
+ return (
121
+ <img
122
+ src={this.__src}
123
+ alt={this.__altText}
124
+ onClick={handleClick}
125
+ style={{
126
+ width: this.__width === "inherit" ? "auto" : this.__width,
127
+ height: this.__height === "inherit" ? "auto" : this.__height,
128
+ maxWidth: "100%",
129
+ cursor: "default",
130
+ }}
131
+ />
132
+ )
133
+ }
134
+ }
135
+
136
+ function convertImageElement(domNode) {
137
+ if (domNode instanceof HTMLImageElement) {
138
+ const { alt: altText, src, width, height } = domNode
139
+ const node = $createImageNode({ altText, height, src, width })
140
+ return { node }
141
+ }
142
+ return null
143
+ }
144
+
145
+ export function $createImageNode({
146
+ altText,
147
+ height,
148
+ maxWidth = 500,
149
+ src,
150
+ width,
151
+ }) {
152
+ return $applyNodeReplacement(
153
+ new ImageNode(src, altText, maxWidth, width, height)
154
+ )
155
+ }
156
+
157
+ export function $isImageNode(node) {
158
+ return node instanceof ImageNode
159
+ }
@@ -0,0 +1,40 @@
1
+ export function syncLexicalStyleAttributes(root) {
2
+ if (!root || typeof root.querySelectorAll !== "function") return
3
+
4
+ const elements = root.querySelectorAll(
5
+ "[style], [data-lexical-color], [data-lexical-background-color]"
6
+ )
7
+
8
+ elements.forEach((element) => {
9
+ const {style, dataset} = element
10
+
11
+ if (style) {
12
+ const color = style.color && style.color.trim()
13
+ const backgroundColor = style.backgroundColor && style.backgroundColor.trim()
14
+
15
+ if (color) {
16
+ dataset.lexicalColor = color
17
+ } else if (dataset.lexicalColor) {
18
+ style.color = dataset.lexicalColor
19
+ }
20
+
21
+ if (backgroundColor) {
22
+ dataset.lexicalBackgroundColor = backgroundColor
23
+ } else if (dataset.lexicalBackgroundColor) {
24
+ style.backgroundColor = dataset.lexicalBackgroundColor
25
+ }
26
+ } else {
27
+ if (dataset.lexicalColor) {
28
+ element.setAttribute(
29
+ "style",
30
+ `color: ${dataset.lexicalColor};${dataset.lexicalBackgroundColor ? ` background-color: ${dataset.lexicalBackgroundColor};` : ""}`
31
+ )
32
+ return
33
+ }
34
+
35
+ if (dataset.lexicalBackgroundColor) {
36
+ element.setAttribute("style", `background-color: ${dataset.lexicalBackgroundColor};`)
37
+ }
38
+ }
39
+ })
40
+ }
@@ -0,0 +1,54 @@
1
+ export function updateResponsiveImages(container, targetWidth) {
2
+ if (!container) return
3
+
4
+ const images = container.querySelectorAll("img")
5
+ const containerWidth = typeof targetWidth === "number" ? targetWidth : container.clientWidth
6
+
7
+ if (!containerWidth || Number.isNaN(containerWidth) || containerWidth <= 0) return
8
+
9
+ images.forEach((img) => {
10
+ const src = img.getAttribute("src")
11
+ if (!src) return
12
+
13
+ // Skip if not an internal image or already processed (optional check)
14
+ // For now, we assume we want to update all images that look like they might support resizing
15
+ // or just append params if the server supports it.
16
+ // Assuming ActiveStorage variants or similar can be requested via params,
17
+ // but standard ActiveStorage URLs are signed and immutable.
18
+ // If we are using a proxy or a service that supports resizing via query params (like Cloudinary or a custom proxy), this works.
19
+ // If using standard ActiveStorage, we might need to request a variant URL from the server.
20
+ // However, the user request said: "dynamically append width/height parameters based on the currently requested size"
21
+ // This implies the image server supports it.
22
+
23
+ try {
24
+ const url = new URL(src, window.location.origin)
25
+ // Only update if width changed significantly to avoid thrashing
26
+ const currentW = url.searchParams.get("w")
27
+ const targetW = Math.round(containerWidth)
28
+
29
+ if (currentW && Math.abs(Number(currentW) - targetW) < 50) return
30
+
31
+ url.searchParams.set("w", targetW)
32
+ // url.searchParams.set("h", ...) // Height is usually auto based on aspect ratio
33
+
34
+ // Update src only if changed
35
+ if (url.toString() !== src) {
36
+ img.src = url.toString()
37
+ }
38
+ } catch (e) {
39
+ // Ignore invalid URLs
40
+ }
41
+ })
42
+ }
43
+
44
+ // Observer to watch for resizing
45
+ export function observeResponsiveImages(container) {
46
+ if (!container) return null
47
+
48
+ const observer = new ResizeObserver(() => {
49
+ updateResponsiveImages(container)
50
+ })
51
+
52
+ observer.observe(container)
53
+ return observer
54
+ }
@@ -0,0 +1,33 @@
1
+ import { Turbo } from "@hotwired/turbo-rails"
2
+
3
+ Turbo.StreamActions.update_reactions = function () {
4
+ const targetId = this.getAttribute("target")
5
+ const dataJSON = this.getAttribute("data")
6
+
7
+ console.log("[Turbo] update_reactions action received", { targetId, dataJSON })
8
+
9
+ if (!targetId || !dataJSON) {
10
+ console.warn("[Turbo] update_reactions missing target or data")
11
+ return
12
+ }
13
+
14
+ try {
15
+ const data = JSON.parse(dataJSON)
16
+ const element = document.getElementById(targetId)
17
+
18
+ if (element) {
19
+ // Find the stimulus controller instance using the global Stimulus application
20
+ const controller = window.Stimulus?.getControllerForElementAndIdentifier(element, "comment")
21
+ if (controller && typeof controller.updateReactionsUI === 'function') {
22
+ console.log("[Turbo] calling updateReactionsUI", data)
23
+ controller.updateReactionsUI(data)
24
+ } else {
25
+ console.warn("[Turbo] comment controller not found", { element, controller })
26
+ }
27
+ } else {
28
+ console.warn("[Turbo] target element not found", targetId)
29
+ }
30
+ } catch (e) {
31
+ console.error("Failed to process update_reactions stream action", e)
32
+ }
33
+ }
@@ -0,0 +1,23 @@
1
+ import { marked } from 'marked'
2
+
3
+ export function renderMarkdown(html) {
4
+ return marked.parse(html)
5
+ }
6
+
7
+ export function renderMarkdownInline(html) {
8
+ return marked.parseInline(html)
9
+ }
10
+
11
+ export function renderCommentMarkdown(text) {
12
+ const content = text || ''
13
+ const html = content.includes('\n') ? marked.parse(content) : marked.parseInline(content)
14
+ return html.trim()
15
+ }
16
+
17
+ export function renderMarkdownInContainer(container) {
18
+ container.querySelectorAll('.comment-content').forEach((element) => {
19
+ if (element.dataset.rendered === 'true') return
20
+ element.innerHTML = renderCommentMarkdown(element.textContent)
21
+ element.dataset.rendered = 'true'
22
+ })
23
+ }