collavre 0.22.0 → 0.24.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 (221) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/images/collavre/landing/interface-en-dark.png +0 -0
  4. data/app/assets/images/collavre/landing/interface-en.png +0 -0
  5. data/app/assets/images/collavre/landing/interface-ko-dark.png +0 -0
  6. data/app/assets/images/collavre/landing/interface-ko.png +0 -0
  7. data/app/assets/stylesheets/collavre/actiontext.css +251 -90
  8. data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
  9. data/app/assets/stylesheets/collavre/comments_popup.css +158 -61
  10. data/app/assets/stylesheets/collavre/creatives.css +11 -2
  11. data/app/assets/stylesheets/collavre/landing.css +92 -0
  12. data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
  13. data/app/assets/stylesheets/collavre/popup.css +4 -2
  14. data/app/assets/stylesheets/collavre/tables.css +91 -0
  15. data/app/channels/collavre/inbox_badge_channel.rb +30 -0
  16. data/app/controllers/collavre/admin/settings_controller.rb +52 -67
  17. data/app/controllers/collavre/api/v1/agents_controller.rb +17 -188
  18. data/app/controllers/collavre/api/v1/base_controller.rb +9 -27
  19. data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +224 -0
  20. data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
  21. data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
  22. data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
  23. data/app/controllers/collavre/attachments_controller.rb +13 -3
  24. data/app/controllers/collavre/concerns/slide_viewable.rb +8 -32
  25. data/app/controllers/collavre/creatives_controller.rb +47 -103
  26. data/app/controllers/collavre/inbox_items_controller.rb +10 -4
  27. data/app/controllers/collavre/landing_controller.rb +5 -2
  28. data/app/controllers/collavre/tasks_controller.rb +13 -4
  29. data/app/controllers/collavre/topics_controller.rb +54 -1
  30. data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
  31. data/app/controllers/concerns/collavre/api/authenticatable.rb +40 -0
  32. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
  33. data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
  34. data/app/errors/collavre/api_error.rb +20 -0
  35. data/app/helpers/collavre/application_helper.rb +1 -0
  36. data/app/helpers/collavre/creatives_helper.rb +30 -8
  37. data/app/javascript/collavre.js +2 -0
  38. data/app/javascript/components/ImageResizer.jsx +9 -3
  39. data/app/javascript/components/InlineLexicalEditor.jsx +157 -72
  40. data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +87 -0
  41. data/app/javascript/components/creative_tree_row.js +28 -5
  42. data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
  43. data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +15 -4
  44. data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
  45. data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
  46. data/app/javascript/controllers/__tests__/search_popup_controller.test.js +104 -0
  47. data/app/javascript/controllers/__tests__/topic_list_controller.test.js +82 -0
  48. data/app/javascript/controllers/comment_controller.js +5 -4
  49. data/app/javascript/controllers/comment_version_controller.js +2 -1
  50. data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
  51. data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +3 -2
  52. data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
  53. data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +61 -0
  54. data/app/javascript/controllers/comments/form_controller.js +21 -5
  55. data/app/javascript/controllers/comments/list_controller.js +18 -17
  56. data/app/javascript/controllers/comments/presence_controller.js +2 -1
  57. data/app/javascript/controllers/comments/topics_controller.js +61 -8
  58. data/app/javascript/controllers/common_popup_controller.js +2 -2
  59. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
  60. data/app/javascript/controllers/creatives/import_controller.js +2 -1
  61. data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
  62. data/app/javascript/controllers/creatives/tree_controller.js +142 -1
  63. data/app/javascript/controllers/image_lightbox_controller.js +2 -1
  64. data/app/javascript/controllers/inbox_badge_controller.js +33 -0
  65. data/app/javascript/controllers/index.js +7 -1
  66. data/app/javascript/controllers/search_popup_controller.js +17 -1
  67. data/app/javascript/controllers/share_modal_controller.js +4 -3
  68. data/app/javascript/controllers/topic_list_controller.js +78 -0
  69. data/app/javascript/controllers/topic_search_controller.js +2 -1
  70. data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
  71. data/app/javascript/creatives/topic_move_members_popup.js +156 -0
  72. data/app/javascript/creatives/tree_renderer.js +11 -0
  73. data/app/javascript/lib/__tests__/common_popup_bounds.test.js +58 -0
  74. data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
  75. data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
  76. data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
  77. data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
  78. data/app/javascript/lib/api/api_error.js +108 -0
  79. data/app/javascript/lib/api/queue_manager.js +38 -4
  80. data/app/javascript/lib/common_popup.js +42 -10
  81. data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
  82. data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
  83. data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
  84. data/app/javascript/lib/editor/code_languages.js +173 -0
  85. data/app/javascript/lib/editor/code_token_theme.js +41 -0
  86. data/app/javascript/lib/lexical/__tests__/code_block_toggle.test.js +326 -0
  87. data/app/javascript/lib/lexical/__tests__/code_fence_shortcut.test.js +179 -0
  88. data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
  89. data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
  90. data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
  91. data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +20 -1
  92. data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
  93. data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
  94. data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
  95. data/app/javascript/lib/lexical/code_block_toggle.js +130 -0
  96. data/app/javascript/lib/lexical/code_fence_shortcut.js +60 -0
  97. data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
  98. data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
  99. data/app/javascript/lib/lexical/selection_boundary.js +58 -0
  100. data/app/javascript/lib/lexical/table_transformer.js +182 -0
  101. data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
  102. data/app/javascript/lib/turbo_confirm.js +46 -0
  103. data/app/javascript/lib/typo_correction.js +146 -0
  104. data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
  105. data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
  106. data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
  107. data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
  108. data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
  109. data/app/javascript/lib/utils/confirm_dialog.js +10 -0
  110. data/app/javascript/lib/utils/dialog.js +300 -0
  111. data/app/javascript/lib/utils/markdown.js +154 -67
  112. data/app/javascript/lib/utils/sanitize_description.js +31 -0
  113. data/app/javascript/lib/utils/table_download.js +15 -0
  114. data/app/javascript/modules/__tests__/creative_inline_payload.test.js +231 -0
  115. data/app/javascript/modules/__tests__/creative_save_queue.test.js +228 -0
  116. data/app/javascript/modules/__tests__/creative_tree_dom.test.js +425 -0
  117. data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
  118. data/app/javascript/modules/creative_inline_payload.js +86 -0
  119. data/app/javascript/modules/creative_row_editor.js +231 -471
  120. data/app/javascript/modules/creative_row_editor_helpers.js +76 -0
  121. data/app/javascript/modules/creative_save_queue.js +95 -0
  122. data/app/javascript/modules/creative_tree_dom.js +260 -0
  123. data/app/javascript/modules/export_to_markdown.js +2 -1
  124. data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
  125. data/app/javascript/modules/slide_view.js +11 -2
  126. data/app/javascript/modules/typo_corrector.js +534 -0
  127. data/app/jobs/collavre/ai_agent_job.rb +7 -4
  128. data/app/jobs/collavre/compress_job.rb +6 -2
  129. data/app/jobs/collavre/permission_cache_job.rb +20 -2
  130. data/app/jobs/collavre/trigger_loop_check_job.rb +20 -3
  131. data/app/jobs/collavre/update_mcp_tools_job.rb +17 -0
  132. data/app/models/collavre/channel.rb +16 -0
  133. data/app/models/collavre/comment/approvable.rb +16 -0
  134. data/app/models/collavre/comment/broadcastable.rb +46 -7
  135. data/app/models/collavre/comment/notifiable.rb +15 -5
  136. data/app/models/collavre/comment.rb +87 -32
  137. data/app/models/collavre/comment_version.rb +1 -1
  138. data/app/models/collavre/creative/describable.rb +104 -19
  139. data/app/models/collavre/creative/linkable.rb +9 -0
  140. data/app/models/collavre/creative/permissible.rb +97 -49
  141. data/app/models/collavre/creative.rb +73 -5
  142. data/app/models/collavre/creative_share.rb +78 -26
  143. data/app/models/collavre/inbox_item.rb +2 -2
  144. data/app/models/collavre/task.rb +27 -0
  145. data/app/models/collavre/user.rb +90 -10
  146. data/app/models/concerns/collavre/indexed_json_columns.rb +58 -0
  147. data/app/services/collavre/agent_type_classifier.rb +23 -0
  148. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +4 -0
  149. data/app/services/collavre/ai_agent/message_builder.rb +1 -1
  150. data/app/services/collavre/ai_agent/session_provisioner.rb +126 -0
  151. data/app/services/collavre/ai_agent/task_claim_service.rb +96 -0
  152. data/app/services/collavre/ai_client.rb +81 -12
  153. data/app/services/collavre/auto_theme_generator.rb +15 -11
  154. data/app/services/collavre/creatives/children_index.rb +86 -0
  155. data/app/services/collavre/creatives/comment_badge_index.rb +73 -0
  156. data/app/services/collavre/creatives/creative_tree_serializer.rb +122 -0
  157. data/app/services/collavre/creatives/effective_creative_resolution.rb +37 -0
  158. data/app/services/collavre/creatives/index_query.rb +85 -16
  159. data/app/services/collavre/creatives/permission_checker.rb +1 -1
  160. data/app/services/collavre/creatives/permission_filter.rb +144 -17
  161. data/app/services/collavre/creatives/tree_builder.rb +107 -78
  162. data/app/services/collavre/gemini_parent_recommender.rb +1 -1
  163. data/app/services/collavre/http_client.rb +119 -0
  164. data/app/services/collavre/inbox_reply_service.rb +5 -0
  165. data/app/services/collavre/link_preview_fetcher.rb +129 -38
  166. data/app/services/collavre/markdown_converter.rb +13 -3
  167. data/app/services/collavre/markdown_importer.rb +30 -6
  168. data/app/services/collavre/mobile/event_summarizer.rb +40 -0
  169. data/app/services/collavre/orchestration/agent_context_builder.rb +1 -15
  170. data/app/services/collavre/orchestration/agent_orchestrator.rb +34 -8
  171. data/app/services/collavre/orchestration/arbiter.rb +16 -0
  172. data/app/services/collavre/orchestration/matcher.rb +79 -4
  173. data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
  174. data/app/services/collavre/orchestration/stuck_detector.rb +141 -34
  175. data/app/services/collavre/system_events/context_builder.rb +1 -14
  176. data/app/services/collavre/tools/creative_batch_service.rb +3 -2
  177. data/app/services/collavre/tools/creative_create_service.rb +8 -8
  178. data/app/services/collavre/tools/creative_update_service.rb +23 -8
  179. data/app/services/collavre/tools/preview_attach_service.rb +5 -7
  180. data/app/services/collavre/typo_corrector.rb +188 -0
  181. data/app/views/collavre/comments/_comment.html.erb +9 -4
  182. data/app/views/collavre/comments/_comments_popup.html.erb +25 -4
  183. data/app/views/collavre/creatives/_inline_edit_form.html.erb +10 -0
  184. data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
  185. data/app/views/collavre/creatives/index.html.erb +14 -1
  186. data/app/views/collavre/creatives/slide_view.html.erb +1 -1
  187. data/app/views/collavre/landing/show.html.erb +52 -10
  188. data/app/views/collavre/users/show.html.erb +3 -0
  189. data/app/views/collavre/users/typo_correction.html.erb +50 -0
  190. data/app/views/layouts/collavre/slide.html.erb +1 -0
  191. data/config/locales/comments.en.yml +16 -0
  192. data/config/locales/comments.ko.yml +16 -0
  193. data/config/locales/creatives.en.yml +6 -0
  194. data/config/locales/creatives.ko.yml +5 -0
  195. data/config/locales/integrations.en.yml +1 -1
  196. data/config/locales/integrations.ko.yml +1 -1
  197. data/config/locales/landing.en.yml +22 -10
  198. data/config/locales/landing.ko.yml +22 -10
  199. data/config/locales/mobile.en.yml +16 -0
  200. data/config/locales/mobile.ko.yml +16 -0
  201. data/config/locales/orchestration.en.yml +1 -0
  202. data/config/locales/orchestration.ko.yml +1 -0
  203. data/config/locales/users.en.yml +15 -0
  204. data/config/locales/users.ko.yml +15 -0
  205. data/config/routes.rb +13 -0
  206. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +13 -4
  207. data/db/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
  208. data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
  209. data/db/migrate/20260702000001_enforce_system_settings_key_not_null.rb +9 -0
  210. data/db/migrate/20260702000002_add_creative_index_and_fk_to_tags.rb +11 -0
  211. data/db/migrate/20260702000003_add_creative_type_index_to_labels.rb +10 -0
  212. data/db/migrate/20260702000004_add_creative_created_at_index_to_comments.rb +10 -0
  213. data/db/migrate/20260702000005_promote_channel_config_index_columns.rb +76 -0
  214. data/db/migrate/20260713000001_add_comments_count_to_creatives.rb +20 -0
  215. data/db/migrate/20260713000002_add_comment_versions_count_to_comments.rb +20 -0
  216. data/db/migrate/20260715000000_change_creatives_description_to_plain_text.rb +16 -0
  217. data/db/seeds.rb +51 -0
  218. data/lib/collavre/version.rb +1 -1
  219. data/lib/generators/collavre/install/install_generator.rb +1 -0
  220. metadata +96 -2
  221. data/app/services/collavre/tools/description_normalizable.rb +0 -16
@@ -328,6 +328,13 @@ body.chat-fullscreen {
328
328
  margin-top: 0.2em;
329
329
  }
330
330
 
331
+ .comment-content img,
332
+ .comment-content video {
333
+ max-width: 100%;
334
+ height: auto;
335
+ border-radius: var(--radius-2);
336
+ }
337
+
331
338
  .comment-content pre {
332
339
  white-space: pre-wrap;
333
340
  word-break: break-word;
@@ -360,67 +367,8 @@ body.chat-fullscreen {
360
367
  margin: 0;
361
368
  }
362
369
 
363
- /* Table styling */
364
- .comment-content table {
365
- border-collapse: collapse;
366
- width: 100%;
367
- font-size: 0.85em;
368
- margin: 0;
369
- }
370
-
371
- .comment-content table th,
372
- .comment-content table td {
373
- border: 1px solid var(--color-border);
374
- padding: 0.3em 0.6em;
375
- text-align: left;
376
- white-space: nowrap;
377
- }
378
-
379
- .comment-content table th {
380
- background: var(--color-section-bg);
381
- font-weight: 600;
382
- }
383
-
384
- .comment-content table tr:hover td {
385
- background: color-mix(in srgb, var(--color-section-bg) 40%, transparent);
386
- }
387
-
388
- /* Table download wrapper */
389
- .table-download-wrapper {
390
- position: relative;
391
- overflow-x: auto;
392
- margin: 0.4em 0;
393
- }
394
-
395
- .table-download-toolbar {
396
- display: flex;
397
- justify-content: flex-end;
398
- gap: 0.3em;
399
- padding: 0.15em 0;
400
- opacity: 0;
401
- transition: opacity 0.15s ease;
402
- }
403
-
404
- .table-download-wrapper:hover .table-download-toolbar {
405
- opacity: 1;
406
- }
407
-
408
- .table-download-btn {
409
- background: none;
410
- border: 1px solid var(--color-border);
411
- border-radius: var(--radius-2);
412
- color: var(--color-muted);
413
- font-size: 0.75em;
414
- padding: 0.15em 0.5em;
415
- cursor: pointer;
416
- line-height: 1.4;
417
- transition: color 0.15s ease, border-color 0.15s ease;
418
- }
419
-
420
- .table-download-btn:hover {
421
- color: var(--color-active);
422
- border-color: var(--color-active);
423
- }
370
+ /* Table + download-toolbar styling moved to collavre/tables.css (shared with
371
+ creative description tables). Load 'collavre/tables' alongside this file. */
424
372
 
425
373
  /* Quote indicator in form */
426
374
  .comment-quote-indicator {
@@ -1768,6 +1716,46 @@ body.chat-fullscreen {
1768
1716
  opacity: 0.5;
1769
1717
  font-style: italic;
1770
1718
  }
1719
+
1720
+ /* Topic list popup button (pinned right of the scrolling tab bar) */
1721
+ .comment-topics-bar {
1722
+ display: flex;
1723
+ align-items: center;
1724
+ gap: 0.5em;
1725
+ }
1726
+ .comment-topics-bar .comment-topics-list {
1727
+ flex: 1;
1728
+ min-width: 0;
1729
+ }
1730
+ .topic-list-btn {
1731
+ flex-shrink: 0;
1732
+ display: inline-flex;
1733
+ align-items: center;
1734
+ justify-content: center;
1735
+ width: 24px;
1736
+ height: 24px;
1737
+ border-radius: var(--radius-3);
1738
+ border: 1px solid var(--color-border);
1739
+ background: transparent;
1740
+ cursor: pointer;
1741
+ color: var(--color-muted);
1742
+ padding: 0;
1743
+ }
1744
+ .topic-list-btn:hover {
1745
+ background: var(--color-section-bg);
1746
+ color: var(--color-text);
1747
+ }
1748
+
1749
+ /* Archived entries inside the topic list popup */
1750
+ .topic-list-item {
1751
+ display: inline-flex;
1752
+ align-items: center;
1753
+ gap: 0.3em;
1754
+ }
1755
+ .topic-list-item--archived {
1756
+ opacity: 0.5;
1757
+ font-style: italic;
1758
+ }
1771
1759
  .topic-branch-icon {
1772
1760
  font-size: 0.75em;
1773
1761
  opacity: 0.6;
@@ -1989,3 +1977,112 @@ body.chat-fullscreen {
1989
1977
  li:hover .topic-create-option {
1990
1978
  text-decoration: underline;
1991
1979
  }
1980
+
1981
+ /* ── Inline typo correction (volatile overlay; never serialized) ───────────── */
1982
+ /* The backdrop mirrors the textarea text exactly and paints <mark> spans over
1983
+ it. The backdrop sits ON TOP of the textarea (higher z-index) but is
1984
+ pointer-events:none, so clicks pass through to the textarea everywhere EXCEPT
1985
+ over a <mark> (pointer-events:auto) — that's what makes the marks clickable.
1986
+ If the textarea were on top, the browser would hit-test it first and the mark
1987
+ click handlers would never fire. The backdrop's text is transparent, so the
1988
+ textarea's real (opaque) glyphs show through and only the underlines paint. */
1989
+ .typo-input-wrap {
1990
+ position: relative;
1991
+ display: block;
1992
+ }
1993
+
1994
+ .typo-input-wrap > textarea {
1995
+ position: relative;
1996
+ background: transparent;
1997
+ z-index: 0;
1998
+ }
1999
+
2000
+ .typo-backdrop {
2001
+ position: absolute;
2002
+ inset: 0;
2003
+ overflow: hidden;
2004
+ pointer-events: none;
2005
+ z-index: 1;
2006
+ }
2007
+
2008
+ .typo-highlights {
2009
+ margin: 0;
2010
+ color: transparent;
2011
+ white-space: pre-wrap;
2012
+ overflow-wrap: break-word;
2013
+ word-break: break-word;
2014
+ }
2015
+
2016
+ /* Two states, distinguished by BOTH shape and colour (colour-blind safe). */
2017
+ .typo-mark {
2018
+ background: transparent;
2019
+ color: transparent;
2020
+ pointer-events: auto;
2021
+ cursor: pointer;
2022
+ border-radius: 2px;
2023
+ }
2024
+
2025
+ /* Auto-applied (>= threshold): faint straight underline — resolved, dim. */
2026
+ .typo-mark-applied {
2027
+ text-decoration: underline solid var(--color-link);
2028
+ text-decoration-thickness: 1px;
2029
+ text-underline-offset: 2px;
2030
+ opacity: 0.55;
2031
+ }
2032
+
2033
+ /* Candidate (< threshold): wavy dotted amber underline — needs a decision. */
2034
+ .typo-mark-candidate {
2035
+ text-decoration: underline wavy var(--color-warning);
2036
+ text-decoration-thickness: 1px;
2037
+ text-underline-offset: 2px;
2038
+ }
2039
+
2040
+ /* Creatable combobox reusing CommonPopup positioning. */
2041
+ .typo-popup {
2042
+ position: absolute;
2043
+ z-index: 100000;
2044
+ min-width: 180px;
2045
+ max-width: 320px;
2046
+ background: var(--surface-section);
2047
+ border: 1px solid var(--border-color);
2048
+ border-radius: 8px;
2049
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
2050
+ padding: 6px;
2051
+ }
2052
+
2053
+ .typo-popup-input {
2054
+ width: 100%;
2055
+ box-sizing: border-box;
2056
+ padding: 6px 8px;
2057
+ border: 1px solid var(--border-color);
2058
+ border-radius: 6px;
2059
+ font: inherit;
2060
+ margin-bottom: 6px;
2061
+ }
2062
+
2063
+ .typo-popup-list {
2064
+ list-style: none;
2065
+ margin: 0;
2066
+ padding: 0;
2067
+ max-height: 200px;
2068
+ overflow-y: auto;
2069
+ }
2070
+
2071
+ .typo-popup-list .common-popup-item {
2072
+ padding: 6px 8px;
2073
+ border-radius: 6px;
2074
+ cursor: pointer;
2075
+ display: flex;
2076
+ justify-content: space-between;
2077
+ gap: 8px;
2078
+ }
2079
+
2080
+ .typo-popup-list .common-popup-item.active,
2081
+ .typo-popup-list .common-popup-item:hover {
2082
+ background: var(--surface-hover);
2083
+ }
2084
+
2085
+ .typo-popup-role {
2086
+ color: var(--text-muted);
2087
+ font-size: 0.85em;
2088
+ }
@@ -257,14 +257,23 @@ creative-tree-row.show-edit .creative-row {
257
257
  word-break: break-word;
258
258
  }
259
259
 
260
- /* Allow attachments and images to be clickable independently of the parent link */
260
+ /* Allow attachments, images, and videos to be interactive independently of the
261
+ parent link. Without pointer-events on the video, the clickable creative row
262
+ would swallow clicks on the native playback controls (play/pause/fullscreen). */
261
263
  .creative-content a[download],
262
- .creative-content img {
264
+ .creative-content img,
265
+ .creative-content video {
263
266
  pointer-events: auto;
264
267
  position: relative;
265
268
  z-index: var(--layer-1);
266
269
  }
267
270
 
271
+ .creative-content video {
272
+ max-width: 100%;
273
+ height: auto;
274
+ border-radius: var(--radius-2);
275
+ }
276
+
268
277
  .creative-row:hover .creative-content {
269
278
  cursor: pointer;
270
279
  }
@@ -94,6 +94,18 @@
94
94
  z-index: 0;
95
95
  }
96
96
 
97
+ .landing-hero-eyebrow { /* L4 — category line, above the claim */
98
+ font-size: var(--text-2);
99
+ font-weight: var(--weight-6);
100
+ letter-spacing: 0.08em;
101
+ text-transform: uppercase;
102
+ color: var(--color-active);
103
+ margin: 0 auto var(--space-4);
104
+ text-align: center;
105
+ position: relative;
106
+ z-index: 1;
107
+ }
108
+
97
109
  .landing-hero-title { /* L1 — main headline */
98
110
  font-size: var(--text-7);
99
111
  font-weight: var(--weight-8);
@@ -117,6 +129,17 @@
117
129
  z-index: 1;
118
130
  }
119
131
 
132
+ .landing-hero-tagline { /* L4 — the mechanism, one line */
133
+ font-size: var(--text-2);
134
+ color: var(--text-muted);
135
+ margin: 0 auto var(--space-6);
136
+ max-width: 720px;
137
+ text-align: center;
138
+ line-height: var(--leading-2);
139
+ position: relative;
140
+ z-index: 1;
141
+ }
142
+
120
143
  .landing-hero-brand { /* L1 — "콜라브" in brand color */
121
144
  font-size: var(--text-7);
122
145
  font-weight: var(--weight-8);
@@ -238,6 +261,7 @@
238
261
  background: var(--surface-section);
239
262
  }
240
263
 
264
+ /* Three tools, three cards — one row on desktop. */
241
265
  .landing-problem-grid {
242
266
  display: grid;
243
267
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
@@ -258,6 +282,12 @@
258
282
  border-color: var(--text-muted);
259
283
  }
260
284
 
285
+ /* The fourth card is the punchline — the copy the AI era added. */
286
+ .landing-problem-card-accent {
287
+ border-color: var(--color-active);
288
+ background: color-mix(in srgb, var(--color-active) 6%, var(--surface-bg));
289
+ }
290
+
261
291
  .landing-problem-icon {
262
292
  font-size: var(--text-5); /* L2 — emoji decorative */
263
293
  display: block;
@@ -432,6 +462,68 @@ body.dark-mode .landing-demo-dark { display: block; }
432
462
  body:not(.light-mode) .landing-demo-dark { display: block; }
433
463
  }
434
464
 
465
+ /* ─── Interface: the product still ─────────────────────────────────────────── */
466
+ .landing-shot-frame {
467
+ max-width: 980px;
468
+ margin: 0 auto;
469
+ border-radius: var(--radius-4);
470
+ border: var(--border-1) solid var(--border-color);
471
+ background: var(--surface-section);
472
+ overflow: hidden;
473
+ box-shadow: var(--shadow-3);
474
+ }
475
+
476
+ .landing-shot-img {
477
+ display: block;
478
+ width: 100%;
479
+ height: auto;
480
+ }
481
+
482
+ /* Same light/dark pairing the demo video uses: the screenshot is app chrome, so a
483
+ light shot on a dark page reads as a bug, not as a screenshot. */
484
+ .landing-shot-dark { display: none; }
485
+
486
+ body.dark-mode .landing-shot-light { display: none; }
487
+ body.dark-mode .landing-shot-dark { display: block; }
488
+
489
+ @media (prefers-color-scheme: dark) {
490
+ body:not(.light-mode) .landing-shot-light { display: none; }
491
+ body:not(.light-mode) .landing-shot-dark { display: block; }
492
+ }
493
+
494
+ .landing-interface-points {
495
+ list-style: none;
496
+ padding: 0;
497
+ margin: var(--space-5) auto 0;
498
+ max-width: 980px;
499
+ display: grid;
500
+ grid-template-columns: repeat(2, minmax(0, 1fr));
501
+ gap: var(--space-3) var(--space-5);
502
+ }
503
+
504
+ .landing-interface-points li {
505
+ position: relative;
506
+ padding-left: var(--space-4);
507
+ font-size: var(--text-2);
508
+ color: var(--text-muted);
509
+ line-height: var(--leading-2);
510
+ }
511
+
512
+ .landing-interface-points li::before {
513
+ content: "";
514
+ position: absolute;
515
+ left: 0;
516
+ top: 0.6em;
517
+ width: 6px;
518
+ height: 6px;
519
+ border-radius: 50%;
520
+ background: var(--color-accent, var(--text-muted));
521
+ }
522
+
523
+ @media (max-width: 780px) {
524
+ .landing-interface-points { grid-template-columns: 1fr; }
525
+ }
526
+
435
527
  /* ─── Responsive ────────────────────────────────────────────────────────────── */
436
528
  @media (max-width: 640px) {
437
529
  .landing-container {
@@ -32,6 +32,17 @@
32
32
  to { opacity: 1; }
33
33
  }
34
34
 
35
+ /* Top-layer dialogs (alert/confirm/prompt rendered as <dialog>.showModal())
36
+ * use the native ::backdrop instead of the .modal-dialog-overlay div, so the
37
+ * backdrop covers everything below — including other top-layer surfaces such
38
+ * as the image lightbox. Only matches real <dialog> elements; the <div>
39
+ * consumers of .modal-dialog never get a ::backdrop. */
40
+ dialog.modal-dialog::backdrop {
41
+ background: rgba(0, 0, 0, 0.5);
42
+ backdrop-filter: blur(4px);
43
+ -webkit-backdrop-filter: blur(4px);
44
+ }
45
+
35
46
  /* ── Panel ── */
36
47
  .modal-dialog {
37
48
  display: none;
@@ -180,6 +191,27 @@ textarea.modal-dialog-input {
180
191
  opacity: 0.9;
181
192
  }
182
193
 
194
+ /* Destructive confirm action (delete/unlink). Matches .btn-danger convention. */
195
+ .modal-dialog-btn-danger {
196
+ background: var(--color-danger);
197
+ color: var(--text-on-badge);
198
+ border-color: var(--color-danger);
199
+ }
200
+
201
+ .modal-dialog-btn-danger:hover {
202
+ opacity: 0.9;
203
+ }
204
+
205
+ /* ── Confirm dialog message ──
206
+ * pre-wrap preserves the \n line breaks that native confirm() honored. */
207
+ .confirm-dialog-message {
208
+ white-space: pre-wrap;
209
+ overflow-wrap: break-word;
210
+ color: var(--text-primary);
211
+ font-size: var(--text-1);
212
+ line-height: var(--leading-3, 1.5);
213
+ }
214
+
183
215
  /* ── Footer hints (keyboard shortcuts) ── */
184
216
  .modal-dialog-hints {
185
217
  display: flex;
@@ -81,14 +81,16 @@
81
81
  }
82
82
 
83
83
  #link-creative-modal,
84
- #topic-search-modal {
84
+ #topic-search-modal,
85
+ #topic-list-modal {
85
86
  min-width: 320px;
86
87
  z-index: calc(var(--layer-modal) + 10);
87
88
  }
88
89
 
89
90
  /* When chat is fullscreen, these modals must sit above it */
90
91
  body.chat-fullscreen #link-creative-modal,
91
- body.chat-fullscreen #topic-search-modal {
92
+ body.chat-fullscreen #topic-search-modal,
93
+ body.chat-fullscreen #topic-list-modal {
92
94
  z-index: calc(var(--layer-fullscreen) + 1);
93
95
  }
94
96
 
@@ -0,0 +1,91 @@
1
+ /*
2
+ * Shared markdown table styling + CSV/Excel download toolbar.
3
+ *
4
+ * Single source of truth so chat-message tables (.comment-content) and creative
5
+ * description tables (.creative-content / .creative-title-content) render
6
+ * identically and both expose the same top-right download buttons. The download
7
+ * toolbar markup is produced by lib/utils/table_download.js.
8
+ *
9
+ * Load this wherever either context renders: the creatives index, the slide
10
+ * layout, and any page that shows the comments popup.
11
+ */
12
+
13
+ /* Table styling */
14
+ .comment-content table,
15
+ .creative-content table,
16
+ .creative-title-content table {
17
+ border-collapse: collapse;
18
+ width: 100%;
19
+ font-size: 0.85em;
20
+ margin: 0;
21
+ /* Override legacy actiontext.css fixed layout so creative tables expand to
22
+ content and horizontally scroll like chat tables (tables.css loads after
23
+ actiontext.css; same specificity, so it must restate this property). */
24
+ table-layout: auto;
25
+ }
26
+
27
+ .comment-content table th,
28
+ .comment-content table td,
29
+ .creative-content table th,
30
+ .creative-content table td,
31
+ .creative-title-content table th,
32
+ .creative-title-content table td {
33
+ border: 1px solid var(--color-border);
34
+ padding: 0.3em 0.6em;
35
+ text-align: left;
36
+ white-space: nowrap;
37
+ /* Clear actiontext.css cell floors so creative cells size to content like
38
+ chat cells instead of forcing a min width / breaking long words. */
39
+ min-width: 0;
40
+ word-break: normal;
41
+ }
42
+
43
+ .comment-content table th,
44
+ .creative-content table th,
45
+ .creative-title-content table th {
46
+ background: var(--color-section-bg);
47
+ font-weight: 600;
48
+ }
49
+
50
+ .comment-content table tr:hover td,
51
+ .creative-content table tr:hover td,
52
+ .creative-title-content table tr:hover td {
53
+ background: color-mix(in srgb, var(--color-section-bg) 40%, transparent);
54
+ }
55
+
56
+ /* Table download wrapper */
57
+ .table-download-wrapper {
58
+ position: relative;
59
+ overflow-x: auto;
60
+ margin: 0.4em 0;
61
+ }
62
+
63
+ .table-download-toolbar {
64
+ display: flex;
65
+ justify-content: flex-end;
66
+ gap: 0.3em;
67
+ padding: 0.15em 0;
68
+ opacity: 0;
69
+ transition: opacity 0.15s ease;
70
+ }
71
+
72
+ .table-download-wrapper:hover .table-download-toolbar {
73
+ opacity: 1;
74
+ }
75
+
76
+ .table-download-btn {
77
+ background: none;
78
+ border: 1px solid var(--color-border);
79
+ border-radius: var(--radius-2);
80
+ color: var(--color-muted);
81
+ font-size: 0.75em;
82
+ padding: 0.15em 0.5em;
83
+ cursor: pointer;
84
+ line-height: 1.4;
85
+ transition: color 0.15s ease, border-color 0.15s ease;
86
+ }
87
+
88
+ .table-download-btn:hover {
89
+ color: var(--color-active);
90
+ border-color: var(--color-active);
91
+ }
@@ -0,0 +1,30 @@
1
+ module Collavre
2
+ # Keeps the global inbox badge in sync without a full page reload.
3
+ #
4
+ # The badge is updated in real time by fire-and-forget Turbo Stream
5
+ # broadcasts on the ["inbox", user] stream (see Comment::Broadcastable).
6
+ # ActionCable does not replay messages missed while a socket is down, so any
7
+ # badge update broadcast during a WebSocket gap (Turbo navigation, sleep/wake,
8
+ # network blip, server restart, the window before the cable connects) was lost
9
+ # until the next full page render — the "badge only shows up after refresh" bug.
10
+ #
11
+ # This channel closes that gap with the established pull-on-subscribe pattern:
12
+ # ActionCable re-runs #subscribed on every (re)connect, so we re-push the
13
+ # authoritative count each time. No client polling, no time window — the server
14
+ # self-heals the count on reconnect.
15
+ class InboxBadgeChannel < ApplicationCable::Channel
16
+ def subscribed
17
+ return reject unless current_user
18
+
19
+ # Deliver the snapshot through THIS subscription (transmit), not by
20
+ # re-broadcasting to the sibling ["inbox", user] Turbo stream. On reconnect
21
+ # the two subscriptions re-attach independently, so a broadcast from here
22
+ # could fire before that stream re-attaches and be dropped — leaving the
23
+ # badge stale. transmit only reaches this just-confirmed subscriber, so the
24
+ # snapshot can never be sent while no client is listening.
25
+ inbox = Creative.inbox_for(current_user)
26
+ snapshot = Comment.inbox_badge_turbo_stream(inbox, current_user)
27
+ transmit(snapshot) if snapshot
28
+ end
29
+ end
30
+ end