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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :ai_chat_slash_menu,
4
+ name: "Slash menu",
5
+ group: "AI chat",
6
+ helper: "ai_chat_slash_menu",
7
+ import: "unmagic/components/slash_menu",
8
+ description: "Commands offered on a slash. Every command is rendered and the typing filters them, so what can be " \
9
+ "offered is only ever what exists. Focus stays in the field; the menu takes the arrow keys, Enter and " \
10
+ "Tab while open.",
11
+ examples: [
12
+ { key: :composer, title: "In a composer", layout: :full,
13
+ description: "Type / in the box. Try /in, then Enter." }
14
+ ]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :ai_chat_tool_call,
4
+ name: "Tool call",
5
+ group: "AI chat",
6
+ helper: "ai_chat_tool_call",
7
+ description: "One reach for a tool, from the ask to the answer, as a row. A run of them is joined by a line into a " \
8
+ "timeline, because four calls in one breath are one stretch of work, not four events.",
9
+ examples: [
10
+ { key: :run, title: "A run of calls", layout: :full,
11
+ description: "Rows join up, counting the hidden gap markers between them. A done call's glyph says what it was " \
12
+ "about." },
13
+ { key: :states, title: "Every state", layout: :full,
14
+ description: "Queued, running (with a live clock and progress), waiting on a person, done, and failed." },
15
+ { key: :made, title: "What a call made", layout: :full,
16
+ description: "made keeps the output outside the fold, with the line running beside it." }
17
+ ]
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :ai_chat_welcome,
4
+ name: "Welcome",
5
+ group: "AI chat",
6
+ helper: "ai_chat_welcome",
7
+ import: "unmagic/components/ai_chat",
8
+ description: "What an empty conversation says, and suggestions that start one. A good suggestion is runnable, " \
9
+ "naming the reader's own data where there is some.",
10
+ examples: [
11
+ { key: :suggestions, title: "With suggestions", layout: :full,
12
+ description: "Pressing one fills the composer below and sends it; the last only fills it." }
13
+ ]
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :ai_chat_workspace,
4
+ name: "Workspace",
5
+ group: "AI chat",
6
+ helper: "ai_chat_workspace",
7
+ description: "The files an agent has put aside while it works, so you can see what it actually produced. A flat " \
8
+ "list of paths for now; a nested tree waits on the gem's tree view.",
9
+ examples: [
10
+ { key: :files, title: "Files, beside a plan",
11
+ description: "Two sections of one panel, with a divider between them. A file with a url is a link." }
12
+ ]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :avatar,
4
+ name: "Avatar",
5
+ helper: "avatar",
6
+ description: "A person's or organisation's picture, falling back to initials on one of six tints picked from " \
7
+ "the name — the same tint for the same name on every page and every server.",
8
+ examples: [
9
+ { key: :sizes, title: "Sizes and fallbacks",
10
+ description: "Three sizes, with and without an image. A broken image shows the initials through it." },
11
+ { key: :tints, title: "Tints",
12
+ description: "The tint comes from the name, so each person keeps their colour. tint: false is neutral." },
13
+ { key: :group, title: "A group",
14
+ description: "avatar_group stacks them, collapsing past max: into a counter that names the rest." }
15
+ ]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :badge,
4
+ name: "Badge",
5
+ helper: "badge",
6
+ import: nil,
7
+ description: "A small pill of text: a status, a count, a label.",
8
+ examples: [
9
+ { key: :tones, title: "Tones" },
10
+ { key: :in_context, title: "Beside the thing it describes" }
11
+ ]
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :board,
4
+ name: "Board",
5
+ helper: "board",
6
+ import: "unmagic/components",
7
+ description: "A Trello-style board: columns of cards, both reorderable by pointer or keyboard, with a way to add " \
8
+ "to each. Drops post to one endpoint (unmagic-sortable's), and the page morphs to the saved order.",
9
+ examples: [
10
+ { key: :live, title: "A working board", layout: :full,
11
+ description: "Drag cards within and between columns, drag a column by its header, or add a card or a list. " \
12
+ "It's saved in your session. On a phone, press and hold a card or a column's header to lift it. " \
13
+ "With the keyboard: Tab to a card or a column's grip, Space to pick it up, arrows to move it, " \
14
+ "Space to drop, Escape to cancel." },
15
+ { key: :handles, title: "Cards with handles", layout: :full,
16
+ description: "A sortable_handle in a card makes only the grip drag it, so the rest of the card stays a link, " \
17
+ "and on a touch screen the grip drags at once, without the long press." }
18
+ ]
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :button,
4
+ name: "Button",
5
+ helper: "button_classes",
6
+ import: nil,
7
+ description: "The class string for a button, so the look composes with link_to, button_to and form.submit " \
8
+ "alike. Pick a variant for its weight and a size to sit beside other controls.",
9
+ examples: [
10
+ { key: :variants, title: "Variants",
11
+ description: "Default, primary, ghost and danger. Danger keeps the default's shape; only the label and hover warn." },
12
+ { key: :sizes, title: "Sizes",
13
+ description: "Small, default and large, each matching an input of the same size." }
14
+ ]
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :callout,
4
+ name: "Callout",
5
+ helper: "callout",
6
+ import: nil,
7
+ description: "A tinted note stating the state of something in place: a health check, a warning above a form. " \
8
+ "Each tone brings its own icon, so meaning never rests on colour alone.",
9
+ examples: [
10
+ { key: :tones, title: "Tones", layout: :full },
11
+ { key: :neutral, title: "Neutral", layout: :full,
12
+ description: "A neutral callout has no icon; icon: false drops it from the other tones too." }
13
+ ]
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :card,
4
+ name: "Card",
5
+ helper: "card",
6
+ import: nil,
7
+ description: "A bordered surface for a section of a page, with an optional title, header actions and a " \
8
+ "tinted footer.",
9
+ examples: [
10
+ { key: :actions_and_footer, title: "Title, actions and footer" },
11
+ { key: :link, title: "A card that is one link",
12
+ description: "href: makes the whole card a link, for a row that opens a record. Nothing inside should be " \
13
+ "a link or button of its own." },
14
+ { key: :flush_table, title: "Flush, with a table", layout: :full,
15
+ description: "flush: true drops the body's padding, so a table runs edge to edge." }
16
+ ]
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :confirm,
4
+ name: "Confirm",
5
+ helper: nil,
6
+ import: "unmagic/components/confirm",
7
+ description: "A dialog in place of window.confirm for data-turbo-confirm. data-turbo-confirm-title, -accept " \
8
+ "and -variant=\"danger\" customise it, and confirm_dialog_template carries its words through I18n.",
9
+ examples: [
10
+ { key: :danger, title: "Danger",
11
+ description: "A custom accept label and the danger variant, for an action that destroys something." },
12
+ { key: :plain, title: "Plain",
13
+ description: "Only data-turbo-confirm, so the dialog uses its default title and buttons." }
14
+ ]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :copy_button,
4
+ name: "Copy button",
5
+ helper: "copy_button",
6
+ import: "unmagic/components/clipboard",
7
+ description: "A button that copies text to the clipboard, showing a check for a moment once it has, and " \
8
+ "announcing it to screen readers.",
9
+ examples: [
10
+ { key: :from_element, title: "Copy from an element",
11
+ description: "from: copies the text of the element (or the value of the input) with that id, at the " \
12
+ "moment of the click, so the text isn't duplicated into an attribute." },
13
+ { key: :with_label, title: "With a label",
14
+ description: "A block replaces the icon button with your own content." }
15
+ ]
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :detail_list,
4
+ name: "Detail list",
5
+ helper: "detail_list",
6
+ import: nil,
7
+ description: "A <dl> of a record's fields, laid out inline or stacked. A blank value renders as an em dash, " \
8
+ "so call sites don't each need .presence || \"—\".",
9
+ examples: [
10
+ { key: :inline, title: "Inline", layout: :full,
11
+ description: "Labels beside values. Notes is blank, so it shows an em dash." },
12
+ { key: :stacked, title: "Stacked", layout: :full,
13
+ description: "Small uppercase labels above values, two columns on wide screens." }
14
+ ]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :dialog,
4
+ name: "Dialog",
5
+ helper: "dialog",
6
+ import: "unmagic/components/modal",
7
+ description: "Native <dialog>s sharing one panel: a titled header with a close button, the body, and an " \
8
+ "optional footer. Load one from the server into the shared modal, or put one on the page.",
9
+ examples: [
10
+ { key: :modal, title: "Loaded into the shared modal", layout: :full,
11
+ description: "Clear the name and save to see a 422 keep the dialog open. A good save closes it in the " \
12
+ "same render as the morph refresh. Slow shows the skeleton; Forbidden shows the error panel." },
13
+ { key: :dialog_tag, title: "Already on the page",
14
+ description: "dialog_tag and dialog_button open a dialog with no request." }
15
+ ]
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :elapsed,
4
+ name: "Elapsed",
5
+ helper: "elapsed_tag",
6
+ import: "unmagic/components/elapsed",
7
+ description: "A clock counting up from a moment the server named, or down to one. Work that takes a minute and " \
8
+ "work that has hung look the same behind a spinner; a number that keeps moving is the difference.",
9
+ examples: [
10
+ { key: :running, title: "Counting up",
11
+ description: "The server renders the current reading and the element keeps it moving, a second at a time." },
12
+ { key: :countdown, title: "Counting down",
13
+ description: "direction: :down stops at zero and fires unmagic-elapsed:end." }
14
+ ]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :empty_state,
4
+ name: "Empty state",
5
+ helper: "empty_state",
6
+ import: nil,
7
+ description: "The dashed blank slate a list or table shows when there is nothing in it. It renders through " \
8
+ "the same empty_state seam as a table's, so an app that replaces one gets both.",
9
+ examples: [
10
+ { key: :default, title: "Default", layout: :full }
11
+ ]
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :forms,
4
+ name: "Form builder",
5
+ helper: "FormBuilder",
6
+ import: nil,
7
+ description: "The chrome around a form control — the wrapper, the label and its required marker, the hint " \
8
+ "and the error line — and the look of the controls it builds, through config.control_class.",
9
+ examples: [
10
+ { key: :fields, title: "Fields", layout: :full,
11
+ description: "field builds its control from as:, or takes one from a block; group lays fields out in a row." },
12
+ { key: :states, title: "Invalid, disabled and read-only", layout: :full,
13
+ description: "An attribute with errors marks its control aria-invalid and reads the errors underneath." },
14
+ { key: :control_classes, title: "control_classes, outside a builder", layout: :full,
15
+ description: "A select_tag or a hand-written input wears the same classes, sized to sit level with buttons." },
16
+ { key: :autogrow, title: "Autogrowing textarea", layout: :full,
17
+ description: "Grows from its rows up to its max-height, then scrolls. Needs import \"unmagic/components/autogrow\"." },
18
+ { key: :uuid_field, title: "UUID field", layout: :full,
19
+ description: "A hidden field holding a fresh UUIDv7, minted again when the element upgrades and on every " \
20
+ "form reset. Needs import \"unmagic/components/uuid_input\"." }
21
+ ]
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :local_time,
4
+ name: "Local time",
5
+ helper: "local_time_tag",
6
+ import: "unmagic/components/time",
7
+ description: "A timestamp shown in the viewer's own locale and time zone, formatted by the browser with Intl. " \
8
+ "Until the element upgrades, the server's rendering in Time.zone shows instead.",
9
+ examples: [
10
+ { key: :relative, title: "Relative, keeping itself current", layout: :full,
11
+ description: "format: :relative stays current on a shared timer; compact: true shortens it to \"5m\"." },
12
+ { key: :absolute, title: "Absolute, in the viewer's zone", layout: :full }
13
+ ]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :menu,
4
+ name: "Menu",
5
+ helper: "menu",
6
+ import: "unmagic/components/menu",
7
+ description: "A dropdown of actions built on <details>. It closes on an outside click, Escape, choosing an " \
8
+ "item, or a Turbo navigation, and arrow keys, Home and End move between items.",
9
+ examples: [
10
+ { key: :labelled, title: "With a label",
11
+ description: "A text trigger with a chevron, lined up with the trigger's left edge." },
12
+ { key: :actions, title: "Icon trigger with actions",
13
+ description: "With no label the trigger is a ⋮ button. Items can be button_to actions, and tone: :danger " \
14
+ "pairs with a confirm." }
15
+ ]
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :page_header,
4
+ name: "Page header",
5
+ helper: "page_header",
6
+ import: nil,
7
+ description: "The top of a page: a back link, the title with its badges, a description, and the page's " \
8
+ "actions on the right.",
9
+ examples: [
10
+ { key: :default, title: "Back link, badges and actions", layout: :full },
11
+ { key: :with_leading, title: "With something before the title", layout: :full,
12
+ description: "header.leading takes markup for an avatar or icon; title and description take blocks too." }
13
+ ]
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :skeleton,
4
+ name: "Skeleton",
5
+ helper: "skeleton",
6
+ import: nil,
7
+ description: "Blocks out an interface while it loads. Shapes take their size from what they stand in for, " \
8
+ "and screen readers hear one \"Loading…\" instead of every shape. Each example sits beside the " \
9
+ "real thing, so a difference in size shows at a glance.",
10
+ examples: [
11
+ { key: :blocking_out, title: "Blocking out your own layout", layout: :full,
12
+ description: "skeleton yields s.text, s.circle, s.block and s.button to arrange with your own markup." },
13
+ { key: :text_and_button, title: "skeleton_text and skeleton_button", layout: :full,
14
+ description: "A text line fills one line of its font; a button shape is a real button's height." },
15
+ { key: :page_header, title: "page_header skeleton: true", layout: :full },
16
+ { key: :detail_list, title: "detail_list skeleton: true", layout: :full },
17
+ { key: :card, title: "card skeleton: true", layout: :full }
18
+ ]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :sortable_list,
4
+ name: "Sortable list",
5
+ helper: "sortable_list",
6
+ import: "unmagic/components/sortable",
7
+ description: "A list whose items can be dragged, or moved with the keyboard, into a new place — here or in another " \
8
+ "list sharing its namespace. A drop fires unmagic-sortable:move and posts the move.",
9
+ examples: [
10
+ { key: :handles, title: "With handles",
11
+ description: "Only the grip drags, and the grip is the keyboard stop. Nothing is posted here, so the event is " \
12
+ "shown below the list." },
13
+ { key: :grid, title: "A wrapping grid", layout: :full,
14
+ description: "orientation: :grid. The drop slot follows reading order, and every arrow key moves." },
15
+ { key: :connected, title: "Two lists, one namespace", layout: :full,
16
+ description: "Items move between lists that share a namespace. Left and Right move a held item across." }
17
+ ]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :streaming_markdown,
4
+ name: "Streaming Markdown",
5
+ group: "AI chat",
6
+ helper: "streaming_markdown_tag",
7
+ import: "unmagic/components/streaming_markdown",
8
+ description: "Server-rendered HTML revealed at a steady pace as fuller renders of it arrive, so a reply reads as " \
9
+ "one smooth stream rather than the bursts it lands in. Parsing stays on the server; the element only " \
10
+ "reveals.",
11
+ examples: [
12
+ { key: :reveal, title: "A reply arriving in bursts", layout: :full,
13
+ description: "Press Play: the renders arrive unevenly, the reveal doesn't. Lists never flicker as they grow. " \
14
+ "Under reduced motion each render paints at once." },
15
+ { key: :stopped, title: "Stopped mid-reply", layout: :full,
16
+ description: "Stop swaps in a final element, and every flush still on its way is refused." }
17
+ ]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :table,
4
+ name: "Table",
5
+ helper: "table_for",
6
+ import: nil,
7
+ description: "Declarative index tables in the spirit of form_for: describe the columns, and the table " \
8
+ "draws the chrome — sortable headers, empty states, companion rows and pagination.",
9
+ examples: [
10
+ { key: :sortable, title: "Sortable, numeric and aligned", layout: :full,
11
+ description: "Click a sortable header to toggle ?sort and ?direction. The note under Ada is a companion row." },
12
+ { key: :empty, title: "Empty", layout: :full },
13
+ { key: :deferred, title: "Deferred", layout: :full,
14
+ description: "The first render never touches the collection. Reload to watch the skeleton swap for " \
15
+ "the rows without the columns shifting." },
16
+ { key: :table_tag, title: "A static table from plain data", layout: :full }
17
+ ]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :tabs,
4
+ name: "Tabs",
5
+ helper: "tabs",
6
+ import: "unmagic/components/tabs",
7
+ description: "A row of tabs: panels switched in the page with the ARIA tab pattern, or links to separate " \
8
+ "pages rendered on the server.",
9
+ examples: [
10
+ { key: :panels, title: "Panels in the page", layout: :full,
11
+ description: "Arrow keys, Home and End move between tabs. A disabled tab shows its reason, and with an " \
12
+ "id: the choice survives a reload." },
13
+ { key: :links, title: "Links to separate pages",
14
+ description: "Tabs with href: need no script; mark the current one active: true." }
15
+ ]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :toast,
4
+ name: "Toast",
5
+ helper: "flash_toasts",
6
+ import: "unmagic/components/toasts",
7
+ description: "Flashes and stream responses as toasts that dismiss themselves, pause while hovered or " \
8
+ "focused, and survive Drive visits and morph refreshes.",
9
+ examples: [
10
+ { key: :flash, title: "From a flash, after a redirect", layout: :full,
11
+ description: "Each button sets flash[type] and redirects back. Hover a toast to hold it open." },
12
+ { key: :stream, title: "From a stream response",
13
+ description: "turbo_stream.toast pops one without a redirect." },
14
+ { key: :above_dialog, title: "Above an open dialog",
15
+ description: "A toast streamed from a dialog shows above it, and times out even though the dialog " \
16
+ "makes it inert." }
17
+ ]
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Catalog.component :tooltip,
4
+ name: "Tooltip",
5
+ helper: "tooltip",
6
+ import: "unmagic/components/tooltip",
7
+ description: "A hint on hover or focus, drawn in the top layer so nothing clips it. It flips to the other " \
8
+ "side when there isn't room, stays on screen, and closes on Escape.",
9
+ examples: [
10
+ { key: :term, title: "A term in a sentence",
11
+ description: "Plain text content is styled as a term — a dashed underline — and made focusable." },
12
+ { key: :placement, title: "Placement and edges", layout: :full,
13
+ description: "placement: :bottom flips above when there's no room; a hint near the edge is clamped on screen." }
14
+ ]
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ Unmagic::Components::Browser::Engine.routes.draw do
4
+ root to: "pages#overview"
5
+ get "installation", to: "pages#installation"
6
+ get "theming", to: "pages#theming"
7
+ get "components/:slug", to: "pages#component", as: :component
8
+
9
+ # What the dialog and toast examples talk to.
10
+ get "dialogs/profile", to: "demos#edit_profile", as: :profile_dialog
11
+ patch "dialogs/profile", to: "demos#update_profile"
12
+ delete "dialogs/profile", to: "demos#destroy_profile"
13
+ get "dialogs/slow", to: "demos#slow_dialog", as: :slow_dialog
14
+ get "dialogs/forbidden", to: "demos#forbidden_dialog", as: :forbidden_dialog
15
+ post "toasts/flash", to: "demos#flash_toast", as: :flash_toast
16
+ post "toasts/stream", to: "demos#stream_toast", as: :stream_toast
17
+
18
+ # What the AI chat composer example talks to.
19
+ post "ai_chat/messages", to: "demos#ai_chat_message", as: :ai_chat_messages
20
+ post "ai_chat/stop", to: "demos#ai_chat_stop", as: :ai_chat_stop
21
+
22
+ # What the board example talks to.
23
+ patch "board/order", to: "demos#board_order", as: :board_order
24
+ post "board/cards", to: "demos#board_card", as: :board_cards
25
+ post "board/columns", to: "demos#board_column", as: :board_columns
26
+ delete "board", to: "demos#board_reset", as: :board
27
+
28
+ # The stylesheet, the components' JavaScript and Turbo, served from the gems
29
+ # themselves. Each URL carries its file's mtime, so it can be cached for good.
30
+ Unmagic::Components::Browser.asset_roots.each do |kind, root|
31
+ mount Rack::Files.new(root.to_s, "cache-control" => "public, max-age=31536000, immutable") => "assets/#{kind}", as: nil
32
+ end
33
+
34
+ # The old one-page-per-group URLs. A relative redirect keeps the mount prefix.
35
+ {
36
+ "deferred" => "table", "dialogs" => "dialog", "toasts" => "toast", "primitives" => "card",
37
+ "elements" => "tooltip", "skeletons" => "skeleton", "forms" => "forms"
38
+ }.each do |old, slug|
39
+ get old, to: redirect("components/#{slug}")
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ /* The browser's stylesheet, prebuilt by `rake browser:css` into
2
+ ../assets/browser.css and shipped with the gem, so a host mounting the browser
3
+ compiles nothing. It is what a host's app/assets/tailwind/application.css
4
+ holds, except that the gem's CSS is imported by path.
5
+
6
+ The dark variant follows the browser's data-theme toggle rather than the
7
+ system setting, the way a host with its own theme switch would set it up.
8
+
9
+ The browser's own pages and examples are styled with utilities, so its app/
10
+ and catalog/ are scanned. The components' CSS arrives through the import. */
11
+ @import "tailwindcss" source(none);
12
+ @source "../app";
13
+ @source "../catalog";
14
+
15
+ @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
16
+
17
+ @import "../../../../../app/assets/tailwind/unmagic_components/engine.css";
18
+
19
+ /* An example's Preview/Code switch is the gem's tabs, lifted beside the
20
+ example's heading rather than above its panels. */
21
+ .PreviewExample {
22
+ position: relative;
23
+ }
24
+
25
+ .PreviewExample__tabs > .UnmagicTabs__list {
26
+ position: absolute;
27
+ top: 0;
28
+ right: 0;
29
+ }
30
+
31
+ .PreviewExample__tabs > .UnmagicTabs__panel {
32
+ padding-top: 0;
33
+ }
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unmagic
4
+ module Components
5
+ # A browser for the components, which a host mounts to look through every
6
+ # component, its examples and their source inside its own app:
7
+ #
8
+ # mount Unmagic::Components::Browser::Engine => "/unmagic/components" if Rails.env.development?
9
+ #
10
+ # It is self-contained. Its stylesheet is prebuilt with the gem's default theme
11
+ # (rake browser:css), and it serves that, the components' JavaScript and Turbo
12
+ # itself, under its own importmap, so a host changes none of its own CSS or JS.
13
+ # turbo-rails has to be in the bundle.
14
+ module Browser
15
+ # Its root is this directory's browser/, not the gem root: the main engine's
16
+ # root is the gem root, so anything in the top-level app/ is autoloaded into
17
+ # every host.
18
+ class Engine < ::Rails::Engine
19
+ isolate_namespace Unmagic::Components::Browser
20
+
21
+ config.root = File.expand_path("browser", __dir__)
22
+ end
23
+
24
+ class << self
25
+ def root = Engine.root
26
+
27
+ # What the browser serves under <mount>/assets/<kind>/, by kind.
28
+ def asset_roots
29
+ roots = {
30
+ "stylesheets" => root.join("assets"),
31
+ "javascripts" => Components::Engine.root.join("app/assets/javascripts")
32
+ }
33
+ roots["turbo"] = ::Turbo::Engine.root.join("app/assets/javascripts") if defined?(::Turbo::Engine)
34
+ roots
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -7,7 +7,8 @@ module Unmagic
7
7
  # library. An app that already owns these concerns points them at its own
8
8
  # versions in an initializer.
9
9
  class Configuration
10
- attr_writer :empty_state, :pagination, :pagy_for, :submit_class, :control_class, :modal_frame_id, :flash_tones
10
+ attr_writer :empty_state, :pagination, :pagy_for, :submit_class, :control_class, :modal_frame_id, :flash_tones,
11
+ :code_block, :sortable_item, :sortable_url
11
12
 
12
13
  # The tone each flash type's toast wears, keyed by the flash type as a string.
13
14
  # A type that isn't listed is :info.
@@ -50,6 +51,35 @@ module Unmagic
50
51
  @submit_class ||= ->(_view, variant) { "UnmagicButton UnmagicButton--#{variant}" }
51
52
  end
52
53
 
54
+ # Renders a block of code: a tool call's payload, a failure's backtrace. Called
55
+ # with (view, source, language), where language is a symbol such as :json or
56
+ # :plaintext, and returns markup. The default is an unhighlighted
57
+ # <pre><code>; an app with a highlighter hands back its own.
58
+ #
59
+ # config.code_block = ->(view, source, language) { view.highlight_code(source, language: language) }
60
+ def code_block
61
+ @code_block ||= lambda do |view, source, language|
62
+ view.tag.pre(view.tag.code(source, class: ("language-#{language}" if language)))
63
+ end
64
+ end
65
+
66
+ # What a sortable item carries for a record: its key and its rank. Called with
67
+ # (view, record); returns { key:, rank: }. unmagic-sortable sets this to its
68
+ # signed, scoped keys; the default is the record's id and sortable_rank.
69
+ def sortable_item
70
+ @sortable_item ||= lambda do |_view, record|
71
+ rank = record.try(:sortable_rank)
72
+ { key: record.to_param, rank: rank.is_a?(BigDecimal) ? rank.to_s("F") : rank&.to_s }
73
+ end
74
+ end
75
+
76
+ # Where a sortable list posts a drop when it isn't given url:. Called with
77
+ # (view); nil means a drop only fires unmagic-sortable:move for your own script.
78
+ # unmagic-sortable points it at its endpoint.
79
+ def sortable_url
80
+ @sortable_url ||= ->(_view) { nil }
81
+ end
82
+
53
83
  # The classes on a form control. Called with (view, kind), where kind is one
54
84
  # of Control::CLASSES' keys (:input, :select, :check…); return an app's own
55
85
  # classes, or nil to leave the control unstyled.