unmagic-components 0.3.0 → 0.4.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 (292) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -1
  3. data/README.md +506 -1
  4. data/app/assets/icons/lucide/LICENSE +43 -0
  5. data/app/assets/icons/lucide/arrow-down.svg +1 -0
  6. data/app/assets/icons/lucide/arrow-left.svg +1 -0
  7. data/app/assets/icons/lucide/arrow-up.svg +1 -0
  8. data/app/assets/icons/lucide/ban.svg +1 -0
  9. data/app/assets/icons/lucide/brain.svg +1 -0
  10. data/app/assets/icons/lucide/check.svg +1 -0
  11. data/app/assets/icons/lucide/chevron-down.svg +1 -0
  12. data/app/assets/icons/lucide/chevron-left.svg +1 -0
  13. data/app/assets/icons/lucide/chevron-right.svg +1 -0
  14. data/app/assets/icons/lucide/circle-check.svg +1 -0
  15. data/app/assets/icons/lucide/circle-dashed.svg +1 -0
  16. data/app/assets/icons/lucide/circle-question-mark.svg +1 -0
  17. data/app/assets/icons/lucide/circle-stop.svg +1 -0
  18. data/app/assets/icons/lucide/circle-x.svg +1 -0
  19. data/app/assets/icons/lucide/clock.svg +1 -0
  20. data/app/assets/icons/lucide/copy.svg +1 -0
  21. data/app/assets/icons/lucide/ellipsis-vertical.svg +1 -0
  22. data/app/assets/icons/lucide/file.svg +1 -0
  23. data/app/assets/icons/lucide/folder.svg +1 -0
  24. data/app/assets/icons/lucide/grip-vertical.svg +1 -0
  25. data/app/assets/icons/lucide/info.svg +1 -0
  26. data/app/assets/icons/lucide/lightbulb.svg +1 -0
  27. data/app/assets/icons/lucide/list-checks.svg +1 -0
  28. data/app/assets/icons/lucide/loader-circle.svg +1 -0
  29. data/app/assets/icons/lucide/messages-square.svg +1 -0
  30. data/app/assets/icons/lucide/paperclip.svg +1 -0
  31. data/app/assets/icons/lucide/pencil.svg +1 -0
  32. data/app/assets/icons/lucide/plus.svg +1 -0
  33. data/app/assets/icons/lucide/rotate-cw.svg +1 -0
  34. data/app/assets/icons/lucide/shield-alert.svg +1 -0
  35. data/app/assets/icons/lucide/timer.svg +1 -0
  36. data/app/assets/icons/lucide/triangle-alert.svg +1 -0
  37. data/app/assets/icons/lucide/wrench.svg +1 -0
  38. data/app/assets/icons/lucide/x.svg +1 -0
  39. data/app/assets/javascripts/unmagic/components/ai_chat.js +142 -0
  40. data/app/assets/javascripts/unmagic/components/autoscroll.js +103 -0
  41. data/app/assets/javascripts/unmagic/components/board.js +65 -0
  42. data/app/assets/javascripts/unmagic/components/dropzone.js +206 -0
  43. data/app/assets/javascripts/unmagic/components/elapsed.js +64 -0
  44. data/app/assets/javascripts/unmagic/components/optimistic.js +71 -0
  45. data/app/assets/javascripts/unmagic/components/placement.js +30 -0
  46. data/app/assets/javascripts/unmagic/components/slash_menu.js +186 -0
  47. data/app/assets/javascripts/unmagic/components/sortable.js +6 -0
  48. data/app/assets/javascripts/unmagic/components/sortable_item.js +122 -0
  49. data/app/assets/javascripts/unmagic/components/sortable_list.js +135 -0
  50. data/app/assets/javascripts/unmagic/components/sortable_param.js +14 -0
  51. data/app/assets/javascripts/unmagic/components/sortable_session.js +502 -0
  52. data/app/assets/javascripts/unmagic/components/streaming_markdown.js +269 -0
  53. data/app/assets/javascripts/unmagic/components/toasts.js +14 -4
  54. data/app/assets/javascripts/unmagic/components/toolbar.js +76 -0
  55. data/app/assets/javascripts/unmagic/components/upsert.js +4 -20
  56. data/app/assets/javascripts/unmagic/components.js +10 -0
  57. data/app/assets/tailwind/unmagic_components/engine.css +1515 -0
  58. data/lib/unmagic/components/action_view_helpers.rb +558 -3
  59. data/lib/unmagic/components/ai_chat/action_bar.rb +70 -0
  60. data/lib/unmagic/components/ai_chat/attachments.rb +119 -0
  61. data/lib/unmagic/components/ai_chat/branch_picker.rb +65 -0
  62. data/lib/unmagic/components/ai_chat/citation.rb +89 -0
  63. data/lib/unmagic/components/ai_chat/composer.rb +160 -0
  64. data/lib/unmagic/components/ai_chat/failure.rb +80 -0
  65. data/lib/unmagic/components/ai_chat/message.rb +142 -0
  66. data/lib/unmagic/components/ai_chat/payload.rb +93 -0
  67. data/lib/unmagic/components/ai_chat/permission.rb +143 -0
  68. data/lib/unmagic/components/ai_chat/plan.rb +91 -0
  69. data/lib/unmagic/components/ai_chat/proposal.rb +102 -0
  70. data/lib/unmagic/components/ai_chat/reasoning.rb +75 -0
  71. data/lib/unmagic/components/ai_chat/request.rb +206 -0
  72. data/lib/unmagic/components/ai_chat/section.rb +53 -0
  73. data/lib/unmagic/components/ai_chat/slash_menu.rb +68 -0
  74. data/lib/unmagic/components/ai_chat/tool_call.rb +181 -0
  75. data/lib/unmagic/components/ai_chat/transcript.rb +68 -0
  76. data/lib/unmagic/components/ai_chat/welcome.rb +73 -0
  77. data/lib/unmagic/components/ai_chat/workspace.rb +74 -0
  78. data/lib/unmagic/components/ai_chat.rb +31 -0
  79. data/lib/unmagic/components/avatar.rb +134 -0
  80. data/lib/unmagic/components/board.rb +182 -0
  81. data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/application_controller.rb +80 -0
  82. data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/demos_controller.rb +137 -0
  83. data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/pages_controller.rb +40 -0
  84. data/lib/unmagic/components/browser/app/helpers/unmagic/components/browser/application_helper.rb +52 -0
  85. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/board_store.rb +88 -0
  86. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/catalog.rb +65 -0
  87. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/pager.rb +11 -0
  88. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/profile.rb +25 -0
  89. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/reply.rb +72 -0
  90. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/thing.rb +31 -0
  91. data/lib/unmagic/components/browser/app/views/layouts/unmagic/components/browser/application.html.erb +65 -0
  92. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_code.html.erb +7 -0
  93. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_crumb.html.erb +7 -0
  94. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_example.html.erb +37 -0
  95. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_nav.html.erb +17 -0
  96. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/demos/edit_profile.html.erb +9 -0
  97. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_conversation.html.erb +101 -0
  98. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_live.html.erb +53 -0
  99. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_thumbnail.html.erb +7 -0
  100. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_welcome.html.erb +12 -0
  101. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_always.html.erb +6 -0
  102. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_hover.html.erb +12 -0
  103. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_thumbnail.html.erb +5 -0
  104. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_dropzone.html.erb +17 -0
  105. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_sent.html.erb +11 -0
  106. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_thumbnail.html.erb +6 -0
  107. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_positions.html.erb +5 -0
  108. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_thumbnail.html.erb +3 -0
  109. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_compact.html.erb +10 -0
  110. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_quote.html.erb +11 -0
  111. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_thumbnail.html.erb +7 -0
  112. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_parts.html.erb +14 -0
  113. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_states.html.erb +8 -0
  114. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_thumbnail.html.erb +5 -0
  115. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_full.html.erb +11 -0
  116. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_plain.html.erb +3 -0
  117. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_thumbnail.html.erb +5 -0
  118. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_parts.html.erb +23 -0
  119. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_states.html.erb +9 -0
  120. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_thumbnail.html.erb +4 -0
  121. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_turns.html.erb +16 -0
  122. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_json.html.erb +4 -0
  123. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_text.html.erb +4 -0
  124. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_thumbnail.html.erb +3 -0
  125. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_answered.html.erb +12 -0
  126. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_thumbnail.html.erb +7 -0
  127. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_waiting.html.erb +12 -0
  128. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_empty.html.erb +3 -0
  129. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_states.html.erb +9 -0
  130. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_thumbnail.html.erb +7 -0
  131. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_decided.html.erb +11 -0
  132. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_inline.html.erb +18 -0
  133. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_thumbnail.html.erb +6 -0
  134. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_settled.html.erb +6 -0
  135. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thinking.html.erb +5 -0
  136. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thumbnail.html.erb +3 -0
  137. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_answered.html.erb +10 -0
  138. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_choices.html.erb +13 -0
  139. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_form.html.erb +9 -0
  140. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_thumbnail.html.erb +8 -0
  141. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_composer.html.erb +15 -0
  142. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_thumbnail.html.erb +9 -0
  143. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_made.html.erb +12 -0
  144. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_run.html.erb +25 -0
  145. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_states.html.erb +22 -0
  146. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_thumbnail.html.erb +7 -0
  147. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_suggestions.html.erb +11 -0
  148. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_thumbnail.html.erb +6 -0
  149. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_files.html.erb +12 -0
  150. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_thumbnail.html.erb +7 -0
  151. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_group.html.erb +5 -0
  152. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_sizes.html.erb +8 -0
  153. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_thumbnail.html.erb +5 -0
  154. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_tints.html.erb +7 -0
  155. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_in_context.html.erb +14 -0
  156. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_thumbnail.html.erb +5 -0
  157. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_tones.html.erb +8 -0
  158. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_handles.html.erb +13 -0
  159. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_live.html.erb +34 -0
  160. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_thumbnail.html.erb +10 -0
  161. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_sizes.html.erb +12 -0
  162. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_thumbnail.html.erb +4 -0
  163. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_variants.html.erb +6 -0
  164. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_neutral.html.erb +3 -0
  165. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_thumbnail.html.erb +5 -0
  166. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_tones.html.erb +14 -0
  167. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_actions_and_footer.html.erb +7 -0
  168. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_flush_table.html.erb +6 -0
  169. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_link.html.erb +6 -0
  170. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_thumbnail.html.erb +6 -0
  171. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_danger.html.erb +3 -0
  172. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_plain.html.erb +2 -0
  173. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_thumbnail.html.erb +7 -0
  174. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_from_element.html.erb +4 -0
  175. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_thumbnail.html.erb +4 -0
  176. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_with_label.html.erb +3 -0
  177. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_inline.html.erb +5 -0
  178. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_stacked.html.erb +7 -0
  179. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_thumbnail.html.erb +5 -0
  180. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_dialog_tag.html.erb +8 -0
  181. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_modal.html.erb +10 -0
  182. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_thumbnail.html.erb +4 -0
  183. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_countdown.html.erb +3 -0
  184. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_running.html.erb +8 -0
  185. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_thumbnail.html.erb +4 -0
  186. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_default.html.erb +1 -0
  187. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_thumbnail.html.erb +3 -0
  188. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_autogrow.html.erb +4 -0
  189. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_control_classes.html.erb +54 -0
  190. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_fields.html.erb +28 -0
  191. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_states.html.erb +11 -0
  192. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_thumbnail.html.erb +8 -0
  193. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_uuid_field.html.erb +9 -0
  194. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_absolute.html.erb +5 -0
  195. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_relative.html.erb +14 -0
  196. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_thumbnail.html.erb +14 -0
  197. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_actions.html.erb +10 -0
  198. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_labelled.html.erb +6 -0
  199. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_thumbnail.html.erb +14 -0
  200. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_default.html.erb +7 -0
  201. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_thumbnail.html.erb +5 -0
  202. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_with_leading.html.erb +14 -0
  203. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_blocking_out.html.erb +25 -0
  204. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_card.html.erb +7 -0
  205. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_detail_list.html.erb +15 -0
  206. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_page_header.html.erb +7 -0
  207. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_text_and_button.html.erb +17 -0
  208. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_thumbnail.html.erb +10 -0
  209. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_connected.html.erb +17 -0
  210. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_grid.html.erb +20 -0
  211. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_handles.html.erb +22 -0
  212. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_thumbnail.html.erb +7 -0
  213. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_reveal.html.erb +19 -0
  214. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_stopped.html.erb +31 -0
  215. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_thumbnail.html.erb +5 -0
  216. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_deferred.html.erb +6 -0
  217. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_empty.html.erb +5 -0
  218. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_sortable.html.erb +8 -0
  219. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_table_tag.html.erb +4 -0
  220. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_thumbnail.html.erb +4 -0
  221. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_links.html.erb +5 -0
  222. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_panels.html.erb +18 -0
  223. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_thumbnail.html.erb +5 -0
  224. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_above_dialog.html.erb +10 -0
  225. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_flash.html.erb +13 -0
  226. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_stream.html.erb +6 -0
  227. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_thumbnail.html.erb +10 -0
  228. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_placement.html.erb +8 -0
  229. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_term.html.erb +5 -0
  230. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_thumbnail.html.erb +5 -0
  231. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/component.html.erb +58 -0
  232. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/installation.html.erb +47 -0
  233. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/overview.html.erb +49 -0
  234. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/theming.html.erb +52 -0
  235. data/lib/unmagic/components/browser/assets/browser.css +2 -0
  236. data/lib/unmagic/components/browser/catalog/ai_chat.rb +21 -0
  237. data/lib/unmagic/components/browser/catalog/ai_chat_action_bar.rb +15 -0
  238. data/lib/unmagic/components/browser/catalog/ai_chat_attachments.rb +15 -0
  239. data/lib/unmagic/components/browser/catalog/ai_chat_branch_picker.rb +12 -0
  240. data/lib/unmagic/components/browser/catalog/ai_chat_citation.rb +15 -0
  241. data/lib/unmagic/components/browser/catalog/ai_chat_composer.rb +16 -0
  242. data/lib/unmagic/components/browser/catalog/ai_chat_failure.rb +15 -0
  243. data/lib/unmagic/components/browser/catalog/ai_chat_message.rb +17 -0
  244. data/lib/unmagic/components/browser/catalog/ai_chat_payload.rb +16 -0
  245. data/lib/unmagic/components/browser/catalog/ai_chat_permission.rb +15 -0
  246. data/lib/unmagic/components/browser/catalog/ai_chat_plan.rb +14 -0
  247. data/lib/unmagic/components/browser/catalog/ai_chat_proposal.rb +15 -0
  248. data/lib/unmagic/components/browser/catalog/ai_chat_reasoning.rb +14 -0
  249. data/lib/unmagic/components/browser/catalog/ai_chat_request.rb +17 -0
  250. data/lib/unmagic/components/browser/catalog/ai_chat_slash_menu.rb +14 -0
  251. data/lib/unmagic/components/browser/catalog/ai_chat_tool_call.rb +17 -0
  252. data/lib/unmagic/components/browser/catalog/ai_chat_welcome.rb +13 -0
  253. data/lib/unmagic/components/browser/catalog/ai_chat_workspace.rb +12 -0
  254. data/lib/unmagic/components/browser/catalog/avatar.rb +15 -0
  255. data/lib/unmagic/components/browser/catalog/badge.rb +11 -0
  256. data/lib/unmagic/components/browser/catalog/board.rb +18 -0
  257. data/lib/unmagic/components/browser/catalog/button.rb +14 -0
  258. data/lib/unmagic/components/browser/catalog/callout.rb +13 -0
  259. data/lib/unmagic/components/browser/catalog/card.rb +16 -0
  260. data/lib/unmagic/components/browser/catalog/confirm.rb +14 -0
  261. data/lib/unmagic/components/browser/catalog/copy_button.rb +15 -0
  262. data/lib/unmagic/components/browser/catalog/detail_list.rb +14 -0
  263. data/lib/unmagic/components/browser/catalog/dialog.rb +15 -0
  264. data/lib/unmagic/components/browser/catalog/elapsed.rb +14 -0
  265. data/lib/unmagic/components/browser/catalog/empty_state.rb +11 -0
  266. data/lib/unmagic/components/browser/catalog/forms.rb +21 -0
  267. data/lib/unmagic/components/browser/catalog/local_time.rb +13 -0
  268. data/lib/unmagic/components/browser/catalog/menu.rb +15 -0
  269. data/lib/unmagic/components/browser/catalog/page_header.rb +13 -0
  270. data/lib/unmagic/components/browser/catalog/skeleton.rb +18 -0
  271. data/lib/unmagic/components/browser/catalog/sortable_list.rb +17 -0
  272. data/lib/unmagic/components/browser/catalog/streaming_markdown.rb +17 -0
  273. data/lib/unmagic/components/browser/catalog/table.rb +17 -0
  274. data/lib/unmagic/components/browser/catalog/tabs.rb +15 -0
  275. data/lib/unmagic/components/browser/catalog/toast.rb +17 -0
  276. data/lib/unmagic/components/browser/catalog/tooltip.rb +14 -0
  277. data/lib/unmagic/components/browser/config/routes.rb +41 -0
  278. data/lib/unmagic/components/browser/tailwind/browser.css +33 -0
  279. data/lib/unmagic/components/browser.rb +39 -0
  280. data/lib/unmagic/components/configuration.rb +31 -1
  281. data/lib/unmagic/components/duration.rb +35 -0
  282. data/lib/unmagic/components/elapsed.rb +46 -0
  283. data/lib/unmagic/components/engine.rb +9 -0
  284. data/lib/unmagic/components/icons.rb +34 -26
  285. data/lib/unmagic/components/modal.rb +1 -1
  286. data/lib/unmagic/components/page_header.rb +8 -0
  287. data/lib/unmagic/components/sortable.rb +104 -0
  288. data/lib/unmagic/components/streaming_markdown.rb +34 -0
  289. data/lib/unmagic/components/turbo_stream_actions.rb +16 -0
  290. data/lib/unmagic/components/version.rb +1 -1
  291. data/lib/unmagic/components.rb +30 -1
  292. metadata +308 -4
@@ -0,0 +1,43 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucide Icons and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ ---
18
+
19
+ The following Lucide icons are derived from the Feather project:
20
+
21
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
22
+
23
+ The MIT License (MIT) (for the icons listed above)
24
+
25
+ Copyright (c) 2013-present Cole Bemis
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M4.929 4.929 19.07 19.071"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 18V5"/><path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"/><path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"/><path d="M17.997 5.125a4 4 0 0 1 2.526 5.77"/><path d="M18 18a4 4 0 0 0 2-7.464"/><path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"/><path d="M6 18a4 4 0 0 1-2-7.464"/><path d="M6.003 5.125a4 4 0 0 0-2.526 5.77"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.1 2.182a10 10 0 0 1 3.8 0"/><path d="M13.9 21.818a10 10 0 0 1-3.8 0"/><path d="M17.609 3.721a10 10 0 0 1 2.69 2.7"/><path d="M2.182 13.9a10 10 0 0 1 0-3.8"/><path d="M20.279 17.609a10 10 0 0 1-2.7 2.69"/><path d="M21.818 10.1a10 10 0 0 1 0 3.8"/><path d="M3.721 6.391a10 10 0 0 1 2.7-2.69"/><path d="M6.391 20.279a10 10 0 0 1-2.69-2.7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 5h8"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/><path d="M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
@@ -0,0 +1,142 @@
1
+ // The AI chat components' behaviour on plain elements, wired once by delegation
2
+ // from the document so a composer, a suggestion or a question streamed in later
3
+ // needs no setup.
4
+ //
5
+ // The composer (`ai_chat_composer`):
6
+ // - Enter sends and Shift+Enter makes a new line, but only while the form's Send
7
+ // button is on the page. While a turn runs it is Stop, so Enter does nothing
8
+ // rather than failing quietly. Composing text (an IME) is left alone.
9
+ // - After a successful submit the form is reset rather than the field blanked,
10
+ // so <unmagic-uuid-input> mints the next id and autogrow re-measures. A failed
11
+ // request leaves what was typed where it was.
12
+ //
13
+ // Suggestions (`ai_chat_welcome`): pressing one fills the composer and sends it,
14
+ // unless it is marked to fill only, or a turn is running.
15
+ //
16
+ // Questions and permission requests (`ai_chat_request`, `ai_chat_permission`)
17
+ // streamed in with role="alert" take focus when nothing else has it — the first
18
+ // option of a question, the card itself for a permission, never its Allow button.
19
+ // Someone typing in the composer keeps their focus.
20
+ //
21
+ // Branch pickers (`ai_chat_branch_picker`): moving to another version replaces the
22
+ // turn, so focus goes back to the same step in the replacement, and a reader can
23
+ // keep stepping without finding the control again.
24
+
25
+ const INSTALLED = Symbol.for("unmagic.components.ai_chat")
26
+
27
+ if (!document[INSTALLED]) {
28
+ document[INSTALLED] = true
29
+
30
+ document.addEventListener("keydown", (event) => {
31
+ const field = event.target
32
+ if (!(field instanceof HTMLTextAreaElement) || !field.hasAttribute("data-ai-chat-composer-field")) return
33
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing || event.defaultPrevented) return
34
+
35
+ event.preventDefault()
36
+ const form = field.form
37
+ const send = form && sendButton(form)
38
+ if (send) form.requestSubmit(send)
39
+ })
40
+
41
+ document.addEventListener("turbo:submit-end", (event) => {
42
+ const form = event.target
43
+ if (!(form instanceof HTMLFormElement) || !event.detail?.success) return
44
+
45
+ const field = form.querySelector("[data-ai-chat-composer-field]")
46
+ if (!field) return
47
+
48
+ form.reset()
49
+ field.focus()
50
+ })
51
+
52
+ document.addEventListener("click", (event) => {
53
+ const button = event.target instanceof Element && event.target.closest("[data-ai-chat-suggestion]")
54
+ if (button) suggest(button)
55
+ })
56
+
57
+ new MutationObserver((records) => {
58
+ for (const record of records) {
59
+ for (const node of record.addedNodes) if (node instanceof Element) arrive(node)
60
+ }
61
+ }).observe(document.documentElement, { childList: true, subtree: true })
62
+
63
+ let pendingStep = null
64
+
65
+ document.addEventListener("click", (event) => {
66
+ const step = event.target instanceof Element && event.target.closest(".UnmagicAIChatBranchPicker__step")
67
+ if (!step) return
68
+
69
+ const picker = step.closest(".UnmagicAIChatBranchPicker")
70
+ const turn = picker?.parentElement?.closest("[id]")
71
+ if (!turn) return
72
+
73
+ pendingStep = { turn: turn.id, index: Array.from(picker.querySelectorAll(".UnmagicAIChatBranchPicker__step")).indexOf(step) }
74
+ })
75
+
76
+ const restoreStep = () => {
77
+ if (!pendingStep) return
78
+ const steps = document.getElementById(pendingStep.turn)?.querySelectorAll(".UnmagicAIChatBranchPicker__step")
79
+ if (!steps) return
80
+
81
+ const step = steps[pendingStep.index]
82
+ pendingStep = null
83
+ const target = step?.getAttribute("aria-disabled") === "true" ? Array.from(steps).find((other) => other !== step) : step
84
+ target?.focus()
85
+ }
86
+
87
+ document.addEventListener("turbo:before-stream-render", (event) => {
88
+ if (!pendingStep) return
89
+ const render = event.detail.render
90
+ event.detail.render = async (stream) => {
91
+ await render(stream)
92
+ restoreStep()
93
+ }
94
+ })
95
+ document.addEventListener("turbo:render", restoreStep)
96
+ document.addEventListener("turbo:frame-render", restoreStep)
97
+ }
98
+
99
+ // The form's own Send button: the one submit that names the form. Stop names a
100
+ // different form, so its presence doesn't count.
101
+ function sendButton(form) {
102
+ if (!form.id) return form.querySelector("button[type=submit]:not([form])")
103
+ return document.querySelector(`button[type=submit][form="${CSS.escape(form.id)}"]`)
104
+ }
105
+
106
+ function suggest(button) {
107
+ const form = document.getElementById(button.dataset.aiChatSuggestionForm)
108
+ if (!(form instanceof HTMLFormElement)) return
109
+
110
+ const name = button.dataset.aiChatSuggestionField
111
+ const field =
112
+ (name && form.elements.namedItem(name)) ||
113
+ form.querySelector("[data-ai-chat-composer-field]") ||
114
+ form.querySelector("textarea")
115
+ if (!field) return
116
+
117
+ field.value = button.dataset.aiChatSuggestion
118
+ field.dispatchEvent(new Event("input", { bubbles: true }))
119
+ field.focus()
120
+ field.selectionStart = field.selectionEnd = field.value.length
121
+
122
+ if ("aiChatSuggestionFill" in button.dataset) return
123
+ const send = sendButton(form)
124
+ if (send) form.requestSubmit(send)
125
+ }
126
+
127
+ const ASKING = ".UnmagicAIChatRequest--waiting[role=alert], .UnmagicAIChatPermission--waiting[role=alert]"
128
+
129
+ function arrive(node) {
130
+ const card = node.matches(ASKING) ? node : node.querySelector(ASKING)
131
+ if (!card) return
132
+
133
+ const focused = document.activeElement
134
+ if (focused && focused !== document.body && focused !== document.documentElement) return
135
+
136
+ if (card.classList.contains("UnmagicAIChatPermission")) {
137
+ card.focus()
138
+ } else {
139
+ const first = card.querySelector("input:not([type=hidden]), select, textarea")
140
+ ;(first ?? card).focus()
141
+ }
142
+ }
@@ -0,0 +1,103 @@
1
+ // <unmagic-autoscroll> — keeps a growing region pinned to the bottom while the
2
+ // reader is there, rendered by `ai_chat`.
3
+ //
4
+ // New content scrolls the page (or the scroller) to the bottom, but only while the
5
+ // reader is already within `threshold` pixels of it. Scroll up to re-read and
6
+ // nothing moves; scroll back down and following resumes. The reader's own
7
+ // scrolling is the only thing that decides.
8
+ //
9
+ // threshold="32" how near the bottom still counts as at it
10
+ // scroller="#panel" the scrolling box, found from the element outwards, then
11
+ // in the document; the page when absent
12
+ //
13
+ // A descendant [data-autoscroll-latest] button is shown while the reader is away
14
+ // from the bottom and takes them back when pressed. The element fires
15
+ // unmagic-autoscroll:pin and unmagic-autoscroll:unpin as that changes. It opens at
16
+ // the bottom, including when Turbo restores it from the cache. No Turbo needed.
17
+
18
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)")
19
+
20
+ class UnmagicAutoscroll extends HTMLElement {
21
+ #pinned = true
22
+ #frame = null
23
+ #scroller = null
24
+ #target = null
25
+ #observer = new MutationObserver(() => this.#follow())
26
+
27
+ constructor() {
28
+ super()
29
+ this.addEventListener("click", (event) => {
30
+ if (event.target instanceof Element && event.target.closest("[data-autoscroll-latest]")) {
31
+ this.scrollToBottom({ smooth: !reducedMotion.matches })
32
+ }
33
+ })
34
+ }
35
+
36
+ connectedCallback() {
37
+ this.#scroller = this.#resolveScroller()
38
+ this.#target = this.#scroller ?? window
39
+ this.#target.addEventListener("scroll", this.#track, { passive: true })
40
+ this.#observer.observe(this, { childList: true, subtree: true, characterData: true })
41
+ this.#pinned = true
42
+ this.#latest(false)
43
+ this.scrollToBottom()
44
+ }
45
+
46
+ disconnectedCallback() {
47
+ this.#observer.disconnect()
48
+ this.#target?.removeEventListener("scroll", this.#track)
49
+ this.#target = null
50
+ cancelAnimationFrame(this.#frame)
51
+ this.#frame = null
52
+ }
53
+
54
+ get pinned() {
55
+ return this.#pinned
56
+ }
57
+
58
+ scrollToBottom({ smooth = false } = {}) {
59
+ const scrolling = this.#scrolling
60
+ scrolling.scrollTo({ top: scrolling.scrollHeight, behavior: smooth ? "smooth" : "instant" })
61
+ }
62
+
63
+ get #scrolling() {
64
+ return this.#scroller ?? document.scrollingElement
65
+ }
66
+
67
+ #resolveScroller() {
68
+ const selector = this.getAttribute("scroller")
69
+ if (!selector) return null
70
+ return this.closest(selector) ?? document.querySelector(selector)
71
+ }
72
+
73
+ // A streamed reply changes the DOM many times a frame; scroll once per frame.
74
+ #follow() {
75
+ if (!this.#pinned || this.#frame) return
76
+
77
+ this.#frame = requestAnimationFrame(() => {
78
+ this.#frame = null
79
+ if (this.#pinned) this.scrollToBottom()
80
+ })
81
+ }
82
+
83
+ #track = () => {
84
+ const { scrollTop, scrollHeight, clientHeight } = this.#scrolling
85
+ const pinned = scrollHeight - clientHeight - scrollTop <= this.#threshold
86
+ if (pinned === this.#pinned) return
87
+
88
+ this.#pinned = pinned
89
+ this.#latest(!pinned)
90
+ this.dispatchEvent(new CustomEvent(`unmagic-autoscroll:${pinned ? "pin" : "unpin"}`, { bubbles: true }))
91
+ }
92
+
93
+ get #threshold() {
94
+ const value = Number.parseInt(this.getAttribute("threshold"), 10)
95
+ return Number.isNaN(value) ? 32 : value
96
+ }
97
+
98
+ #latest(show) {
99
+ for (const button of this.querySelectorAll("[data-autoscroll-latest]")) button.hidden = !show
100
+ }
101
+ }
102
+
103
+ customElements.get("unmagic-autoscroll") || customElements.define("unmagic-autoscroll", UnmagicAutoscroll)
@@ -0,0 +1,65 @@
1
+ // A board's "Add a card" and "Add a list" forms (`board`), wired once by
2
+ // delegation so a column streamed in later works with no setup.
3
+ //
4
+ // Opening one focuses its field. Enter submits and Shift+Enter makes a new line.
5
+ // Escape, or the × button, closes it. After a successful submit the form is reset
6
+ // and stays open with the field focused, so several can be added in a row: the
7
+ // refresh that follows would otherwise morph the <details> shut, since the
8
+ // server's markup has it closed.
9
+
10
+ const INSTALLED = Symbol.for("unmagic.components.board")
11
+
12
+ if (!document[INSTALLED]) {
13
+ document[INSTALLED] = true
14
+
15
+ // toggle doesn't bubble, but it does pass through the capture phase.
16
+ document.addEventListener(
17
+ "toggle",
18
+ (event) => {
19
+ const details = event.target
20
+ if (details instanceof HTMLDetailsElement && details.matches(".UnmagicBoard__add") && details.open) {
21
+ details.querySelector("[data-board-add-field]")?.focus()
22
+ }
23
+ },
24
+ true,
25
+ )
26
+
27
+ document.addEventListener("turbo:before-morph-attribute", (event) => {
28
+ if (event.detail?.attributeName === "open" && event.target.matches?.(".UnmagicBoard__add")) {
29
+ event.preventDefault()
30
+ }
31
+ })
32
+
33
+ document.addEventListener("keydown", (event) => {
34
+ const field = event.target
35
+ if (!(field instanceof HTMLTextAreaElement) || !field.hasAttribute("data-board-add-field")) return
36
+
37
+ if (event.key === "Escape") {
38
+ event.preventDefault()
39
+ close(field)
40
+ } else if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
41
+ event.preventDefault()
42
+ field.form?.requestSubmit()
43
+ }
44
+ })
45
+
46
+ document.addEventListener("click", (event) => {
47
+ const button = event.target instanceof Element && event.target.closest("[data-board-add-cancel]")
48
+ if (button) close(button)
49
+ })
50
+
51
+ document.addEventListener("turbo:submit-end", (event) => {
52
+ const form = event.target
53
+ if (!(form instanceof HTMLFormElement) || !form.hasAttribute("data-board-add") || !event.detail?.success) return
54
+
55
+ form.reset()
56
+ form.querySelector("[data-board-add-field]")?.focus()
57
+ })
58
+ }
59
+
60
+ function close(within) {
61
+ const details = within.closest(".UnmagicBoard__add")
62
+ if (!details) return
63
+ details.open = false
64
+ details.querySelector("summary")?.focus()
65
+ }