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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -1
- data/README.md +506 -1
- data/app/assets/icons/lucide/LICENSE +43 -0
- data/app/assets/icons/lucide/arrow-down.svg +1 -0
- data/app/assets/icons/lucide/arrow-left.svg +1 -0
- data/app/assets/icons/lucide/arrow-up.svg +1 -0
- data/app/assets/icons/lucide/ban.svg +1 -0
- data/app/assets/icons/lucide/brain.svg +1 -0
- data/app/assets/icons/lucide/check.svg +1 -0
- data/app/assets/icons/lucide/chevron-down.svg +1 -0
- data/app/assets/icons/lucide/chevron-left.svg +1 -0
- data/app/assets/icons/lucide/chevron-right.svg +1 -0
- data/app/assets/icons/lucide/circle-check.svg +1 -0
- data/app/assets/icons/lucide/circle-dashed.svg +1 -0
- data/app/assets/icons/lucide/circle-question-mark.svg +1 -0
- data/app/assets/icons/lucide/circle-stop.svg +1 -0
- data/app/assets/icons/lucide/circle-x.svg +1 -0
- data/app/assets/icons/lucide/clock.svg +1 -0
- data/app/assets/icons/lucide/copy.svg +1 -0
- data/app/assets/icons/lucide/ellipsis-vertical.svg +1 -0
- data/app/assets/icons/lucide/file.svg +1 -0
- data/app/assets/icons/lucide/folder.svg +1 -0
- data/app/assets/icons/lucide/grip-vertical.svg +1 -0
- data/app/assets/icons/lucide/info.svg +1 -0
- data/app/assets/icons/lucide/lightbulb.svg +1 -0
- data/app/assets/icons/lucide/list-checks.svg +1 -0
- data/app/assets/icons/lucide/loader-circle.svg +1 -0
- data/app/assets/icons/lucide/messages-square.svg +1 -0
- data/app/assets/icons/lucide/paperclip.svg +1 -0
- data/app/assets/icons/lucide/pencil.svg +1 -0
- data/app/assets/icons/lucide/plus.svg +1 -0
- data/app/assets/icons/lucide/rotate-cw.svg +1 -0
- data/app/assets/icons/lucide/shield-alert.svg +1 -0
- data/app/assets/icons/lucide/timer.svg +1 -0
- data/app/assets/icons/lucide/triangle-alert.svg +1 -0
- data/app/assets/icons/lucide/wrench.svg +1 -0
- data/app/assets/icons/lucide/x.svg +1 -0
- data/app/assets/javascripts/unmagic/components/ai_chat.js +142 -0
- data/app/assets/javascripts/unmagic/components/autoscroll.js +103 -0
- data/app/assets/javascripts/unmagic/components/board.js +65 -0
- data/app/assets/javascripts/unmagic/components/dropzone.js +206 -0
- data/app/assets/javascripts/unmagic/components/elapsed.js +64 -0
- data/app/assets/javascripts/unmagic/components/optimistic.js +71 -0
- data/app/assets/javascripts/unmagic/components/placement.js +30 -0
- data/app/assets/javascripts/unmagic/components/slash_menu.js +186 -0
- data/app/assets/javascripts/unmagic/components/sortable.js +6 -0
- data/app/assets/javascripts/unmagic/components/sortable_item.js +122 -0
- data/app/assets/javascripts/unmagic/components/sortable_list.js +135 -0
- data/app/assets/javascripts/unmagic/components/sortable_param.js +14 -0
- data/app/assets/javascripts/unmagic/components/sortable_session.js +502 -0
- data/app/assets/javascripts/unmagic/components/streaming_markdown.js +269 -0
- data/app/assets/javascripts/unmagic/components/toasts.js +14 -4
- data/app/assets/javascripts/unmagic/components/toolbar.js +76 -0
- data/app/assets/javascripts/unmagic/components/upsert.js +4 -20
- data/app/assets/javascripts/unmagic/components.js +10 -0
- data/app/assets/tailwind/unmagic_components/engine.css +1515 -0
- data/lib/unmagic/components/action_view_helpers.rb +558 -3
- data/lib/unmagic/components/ai_chat/action_bar.rb +70 -0
- data/lib/unmagic/components/ai_chat/attachments.rb +119 -0
- data/lib/unmagic/components/ai_chat/branch_picker.rb +65 -0
- data/lib/unmagic/components/ai_chat/citation.rb +89 -0
- data/lib/unmagic/components/ai_chat/composer.rb +160 -0
- data/lib/unmagic/components/ai_chat/failure.rb +80 -0
- data/lib/unmagic/components/ai_chat/message.rb +142 -0
- data/lib/unmagic/components/ai_chat/payload.rb +93 -0
- data/lib/unmagic/components/ai_chat/permission.rb +143 -0
- data/lib/unmagic/components/ai_chat/plan.rb +91 -0
- data/lib/unmagic/components/ai_chat/proposal.rb +102 -0
- data/lib/unmagic/components/ai_chat/reasoning.rb +75 -0
- data/lib/unmagic/components/ai_chat/request.rb +206 -0
- data/lib/unmagic/components/ai_chat/section.rb +53 -0
- data/lib/unmagic/components/ai_chat/slash_menu.rb +68 -0
- data/lib/unmagic/components/ai_chat/tool_call.rb +181 -0
- data/lib/unmagic/components/ai_chat/transcript.rb +68 -0
- data/lib/unmagic/components/ai_chat/welcome.rb +73 -0
- data/lib/unmagic/components/ai_chat/workspace.rb +74 -0
- data/lib/unmagic/components/ai_chat.rb +31 -0
- data/lib/unmagic/components/avatar.rb +134 -0
- data/lib/unmagic/components/board.rb +182 -0
- data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/application_controller.rb +80 -0
- data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/demos_controller.rb +137 -0
- data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/pages_controller.rb +40 -0
- data/lib/unmagic/components/browser/app/helpers/unmagic/components/browser/application_helper.rb +52 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/board_store.rb +88 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/catalog.rb +65 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/pager.rb +11 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/profile.rb +25 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/reply.rb +72 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/thing.rb +31 -0
- data/lib/unmagic/components/browser/app/views/layouts/unmagic/components/browser/application.html.erb +65 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_code.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_crumb.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_example.html.erb +37 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_nav.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/demos/edit_profile.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_conversation.html.erb +101 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_live.html.erb +53 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_welcome.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_always.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_hover.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_dropzone.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_sent.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_positions.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_compact.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_quote.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_parts.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_states.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_full.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_plain.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_parts.html.erb +23 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_states.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_turns.html.erb +16 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_json.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_text.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_answered.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_waiting.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_empty.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_states.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_decided.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_inline.html.erb +18 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_settled.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thinking.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_answered.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_choices.html.erb +13 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_form.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_thumbnail.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_composer.html.erb +15 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_thumbnail.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_made.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_run.html.erb +25 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_states.html.erb +22 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_suggestions.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_files.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_group.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_sizes.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_tints.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_in_context.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_tones.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_handles.html.erb +13 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_live.html.erb +34 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_thumbnail.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_sizes.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_variants.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_neutral.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_tones.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_actions_and_footer.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_flush_table.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_link.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_danger.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_plain.html.erb +2 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_from_element.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_with_label.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_inline.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_stacked.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_dialog_tag.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_modal.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_countdown.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_running.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_default.html.erb +1 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_autogrow.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_control_classes.html.erb +54 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_fields.html.erb +28 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_states.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_thumbnail.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_uuid_field.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_absolute.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_relative.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_thumbnail.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_actions.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_labelled.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_thumbnail.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_default.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_with_leading.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_blocking_out.html.erb +25 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_card.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_detail_list.html.erb +15 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_page_header.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_text_and_button.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_thumbnail.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_connected.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_grid.html.erb +20 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_handles.html.erb +22 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_reveal.html.erb +19 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_stopped.html.erb +31 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_deferred.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_empty.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_sortable.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_table_tag.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_links.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_panels.html.erb +18 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_above_dialog.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_flash.html.erb +13 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_stream.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_thumbnail.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_placement.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_term.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/component.html.erb +58 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/installation.html.erb +47 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/overview.html.erb +49 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/theming.html.erb +52 -0
- data/lib/unmagic/components/browser/assets/browser.css +2 -0
- data/lib/unmagic/components/browser/catalog/ai_chat.rb +21 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_action_bar.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_attachments.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_branch_picker.rb +12 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_citation.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_composer.rb +16 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_failure.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_message.rb +17 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_payload.rb +16 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_permission.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_plan.rb +14 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_proposal.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_reasoning.rb +14 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_request.rb +17 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_slash_menu.rb +14 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_tool_call.rb +17 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_welcome.rb +13 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_workspace.rb +12 -0
- data/lib/unmagic/components/browser/catalog/avatar.rb +15 -0
- data/lib/unmagic/components/browser/catalog/badge.rb +11 -0
- data/lib/unmagic/components/browser/catalog/board.rb +18 -0
- data/lib/unmagic/components/browser/catalog/button.rb +14 -0
- data/lib/unmagic/components/browser/catalog/callout.rb +13 -0
- data/lib/unmagic/components/browser/catalog/card.rb +16 -0
- data/lib/unmagic/components/browser/catalog/confirm.rb +14 -0
- data/lib/unmagic/components/browser/catalog/copy_button.rb +15 -0
- data/lib/unmagic/components/browser/catalog/detail_list.rb +14 -0
- data/lib/unmagic/components/browser/catalog/dialog.rb +15 -0
- data/lib/unmagic/components/browser/catalog/elapsed.rb +14 -0
- data/lib/unmagic/components/browser/catalog/empty_state.rb +11 -0
- data/lib/unmagic/components/browser/catalog/forms.rb +21 -0
- data/lib/unmagic/components/browser/catalog/local_time.rb +13 -0
- data/lib/unmagic/components/browser/catalog/menu.rb +15 -0
- data/lib/unmagic/components/browser/catalog/page_header.rb +13 -0
- data/lib/unmagic/components/browser/catalog/skeleton.rb +18 -0
- data/lib/unmagic/components/browser/catalog/sortable_list.rb +17 -0
- data/lib/unmagic/components/browser/catalog/streaming_markdown.rb +17 -0
- data/lib/unmagic/components/browser/catalog/table.rb +17 -0
- data/lib/unmagic/components/browser/catalog/tabs.rb +15 -0
- data/lib/unmagic/components/browser/catalog/toast.rb +17 -0
- data/lib/unmagic/components/browser/catalog/tooltip.rb +14 -0
- data/lib/unmagic/components/browser/config/routes.rb +41 -0
- data/lib/unmagic/components/browser/tailwind/browser.css +33 -0
- data/lib/unmagic/components/browser.rb +39 -0
- data/lib/unmagic/components/configuration.rb +31 -1
- data/lib/unmagic/components/duration.rb +35 -0
- data/lib/unmagic/components/elapsed.rb +46 -0
- data/lib/unmagic/components/engine.rb +9 -0
- data/lib/unmagic/components/icons.rb +34 -26
- data/lib/unmagic/components/modal.rb +1 -1
- data/lib/unmagic/components/page_header.rb +8 -0
- data/lib/unmagic/components/sortable.rb +104 -0
- data/lib/unmagic/components/streaming_markdown.rb +34 -0
- data/lib/unmagic/components/turbo_stream_actions.rb +16 -0
- data/lib/unmagic/components/version.rb +1 -1
- data/lib/unmagic/components.rb +30 -1
- metadata +308 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c4fbdc3dbc2a5dbba7758240b2c0fa271c81f46e00435e898ad179f23ce1937
|
|
4
|
+
data.tar.gz: 1ce6acc98aecc0153da190eca815d5ca469e6e46c0fe61bdd8b1fc75dd437e10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2cbcb4011c8504a0247e3a26d49042da521e22ccc658615049e7bdfc7ec9411fa86f984acb2abc9e59a99bc398c245dd03f99de30616f00c2a5ecc49ed688251
|
|
7
|
+
data.tar.gz: 6599c73d2d0c5846a8b5206e3df0071b31c8d958f88b3e21dcbdda761c48820c1f2a1854762c99ce09b6f360985be017a10c007be5887c26a5ae5c097ca4807a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Unmagic::Components::Browser::Engine`, a component browser a host mounts
|
|
15
|
+
(`mount Unmagic::Components::Browser::Engine => "/unmagic/components"`). It is
|
|
16
|
+
the preview app, moved into the gem: every component, its examples and their
|
|
17
|
+
source. It ships a prebuilt stylesheet and serves the components' JavaScript
|
|
18
|
+
and Turbo itself, so a host changes nothing else. It needs turbo-rails.
|
|
19
|
+
`bin/dev` now runs it from a one-file host.
|
|
20
|
+
- `trailing` on the page header builder puts markup beside the title as it is —
|
|
21
|
+
a status partial, a row of badges a helper already returned. `badge` wraps what
|
|
22
|
+
it is given, so an already-rendered one routed through it came out as a badge
|
|
23
|
+
inside a badge; this is the counterpart to `leading`, and the two interleave in
|
|
24
|
+
call order.
|
|
25
|
+
- `sortable_list` and `sortable_handle`: drag-and-drop ordering by pointer or
|
|
26
|
+
keyboard, extracted from hooops.
|
|
27
|
+
- Elements `<unmagic-sortable-list>`, `<unmagic-sortable-item>` and
|
|
28
|
+
`<unmagic-sortable-param>`.
|
|
29
|
+
- Keyboard reordering with announcements.
|
|
30
|
+
- Scrolling at the edges, Escape to cancel, and nested lists.
|
|
31
|
+
- A cancelable `unmagic-sortable:move` event.
|
|
32
|
+
- Touch: a long press lifts an item, so swipes still scroll and taps still
|
|
33
|
+
tap; a grip drags at once.
|
|
34
|
+
- `sortable-*` Tailwind variants.
|
|
35
|
+
- `config.sortable_item` and `config.sortable_url`, which the new
|
|
36
|
+
unmagic-sortable gem fills in.
|
|
37
|
+
- `board`, a Trello-style board of reorderable columns and cards, with add-card
|
|
38
|
+
and add-list forms (`board.js`).
|
|
39
|
+
|
|
40
|
+
- AI chat components, for rendering an agent's work. Designed from three
|
|
41
|
+
applications that each grew their own agent UI, and checked against
|
|
42
|
+
assistant-ui's primitives. Each has a design note in `docs/components/ai_chat`
|
|
43
|
+
and a page in the browser:
|
|
44
|
+
- `ai_chat`, the transcript: a polite log that follows new content while the
|
|
45
|
+
reader is at the bottom, with a jump-to-latest button and a welcome slot.
|
|
46
|
+
- `ai_chat_message`: a user's bubble or an assistant's prose, with reasoning,
|
|
47
|
+
actions, branches and attachments, and an optimistic template.
|
|
48
|
+
- `ai_chat_tool_call`: a row per tool call, joined into a timeline, with a
|
|
49
|
+
live clock, progress, partial-failure counts and folded payloads.
|
|
50
|
+
- `ai_chat_payload`, `ai_chat_reasoning`, `ai_chat_failure`, `ai_chat_plan`
|
|
51
|
+
and `ai_chat_workspace`.
|
|
52
|
+
- `ai_chat_request`, `ai_chat_permission` and `ai_chat_proposal`, for an agent
|
|
53
|
+
that stops to ask.
|
|
54
|
+
- `ai_chat_composer` (with `ai_chat_composer_action` and
|
|
55
|
+
`ai_chat_stop_form`), `ai_chat_slash_menu`, `ai_chat_welcome`,
|
|
56
|
+
`ai_chat_attachments` and `ai_chat_dropzone`.
|
|
57
|
+
- `ai_chat_citation`, `ai_chat_action_bar` and `ai_chat_branch_picker`.
|
|
58
|
+
- `streaming_markdown_tag` and `turbo_stream.stream_markdown`: server-rendered
|
|
59
|
+
HTML revealed at a steady pace as fuller renders arrive
|
|
60
|
+
(`<unmagic-streaming-markdown>`).
|
|
61
|
+
- `elapsed_tag`, a clock counting up from a time or down to one
|
|
62
|
+
(`<unmagic-elapsed>`).
|
|
63
|
+
- `avatar` and `avatar_group`, with initials on a tint picked from the name.
|
|
64
|
+
- `UnmagicProse`, styles for server-rendered Markdown.
|
|
65
|
+
- `config.code_block`, the seam payloads and prose code blocks render through,
|
|
66
|
+
for a host's highlighter.
|
|
67
|
+
- Custom elements `<unmagic-autoscroll>`, `<unmagic-optimistic>`,
|
|
68
|
+
`<unmagic-toolbar>`, `<unmagic-slash-menu>` and `<unmagic-dropzone>`.
|
|
69
|
+
- The browser groups components under headings, with the AI chat components in
|
|
70
|
+
their own group. Its composer example posts for real and streams a reply.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- The gem now depends on `unmagic-icon` and `unmagic-color`. Its glyphs are
|
|
75
|
+
Lucide SVGs shipped in `app/assets/icons/lucide` and rendered through
|
|
76
|
+
unmagic-icon, in place of the inline paths. The rendered `<svg>` gains
|
|
77
|
+
unmagic-icon's `unmagic-icon` class and `data-unmagic-icon` attribute, and the
|
|
78
|
+
modal's retry icon is Lucide's `rotate-cw`.
|
|
79
|
+
- The `upsert` stream action's placement rule moved to
|
|
80
|
+
`unmagic/components/placement`, shared with `<unmagic-optimistic>`. Its
|
|
81
|
+
behaviour is unchanged.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- The browser's dialog page opens its modal examples in the modal again. The
|
|
86
|
+
"modal" example's section had the id `modal`, which shadowed the shared
|
|
87
|
+
modal's frame, so Turbo loaded the link as a full page. Example sections are
|
|
88
|
+
now named `<component>_<example>`. The composer examples' fields no longer
|
|
89
|
+
share the id `content` either.
|
|
90
|
+
- `duration="0"` on `<unmagic-toasts>` now keeps a toast up until it is
|
|
91
|
+
dismissed, so "don't take this one away" is sayable — and a test asserting on a
|
|
92
|
+
toast can stop racing the timer. It used to fall back to the default, because
|
|
93
|
+
the guard was `duration > 0` and `Number(null)` is also `0`, so an absent
|
|
94
|
+
attribute and a deliberate zero were indistinguishable. Absent, empty, negative
|
|
95
|
+
and unparseable all still fall back rather than silently pinning every toast to
|
|
96
|
+
the screen.
|
|
97
|
+
|
|
98
|
+
- The helpers reach `ActionView::TestCase` too, so a host helper that composes
|
|
99
|
+
one of them (`badge`, `button_classes`) can be exercised by the host's own
|
|
100
|
+
helper spec. The engine only ran the `:action_view` load hook, which mixes
|
|
101
|
+
into `ActionView::Base` — what a template renders through, and not what a
|
|
102
|
+
helper test calls its subject on. The symptom was a helper that worked in the
|
|
103
|
+
app and raised `NoMethodError` in its spec, which is a confusing place to send
|
|
104
|
+
someone.
|
|
105
|
+
|
|
10
106
|
## [0.3.0] - 2026-09-16
|
|
11
107
|
|
|
12
108
|
### Added
|
|
@@ -161,7 +257,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
161
257
|
- Configurable empty-state, pagination and Pagy seams so the gem depends on neither
|
|
162
258
|
Pagy nor any host helper.
|
|
163
259
|
|
|
164
|
-
[Unreleased]: https://github.com/unreasonable-magic/unmagic-components/compare/v0.
|
|
260
|
+
[Unreleased]: https://github.com/unreasonable-magic/unmagic-components/compare/v0.4.0...HEAD
|
|
261
|
+
[0.4.0]: https://github.com/unreasonable-magic/unmagic-components/compare/v0.3.0...v0.4.0
|
|
165
262
|
[0.3.0]: https://github.com/unreasonable-magic/unmagic-components/compare/v0.2.0...v0.3.0
|
|
166
263
|
[0.2.0]: https://github.com/unreasonable-magic/unmagic-components/compare/v0.1.0...v0.2.0
|
|
167
264
|
[0.1.0]: https://github.com/unreasonable-magic/unmagic-components/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -36,6 +36,14 @@ the spirit of `form_for`: describe the columns, get the chrome.
|
|
|
36
36
|
the server with a skeleton, an error panel with retry, and a close that lands
|
|
37
37
|
in the same render as the page's refresh; same-page dialogs; and a confirm
|
|
38
38
|
dialog in place of `window.confirm`.
|
|
39
|
+
- **AI chat components** for rendering an agent's work: a transcript, streamed
|
|
40
|
+
replies revealed at a steady pace, tool calls strung into a timeline,
|
|
41
|
+
reasoning, plans, workspaces, permission gates, questions, inline proposals,
|
|
42
|
+
citations and a composer with Send and Stop. See [AI chat](#ai-chat).
|
|
43
|
+
- **Drag-and-drop ordering** by pointer or keyboard: sortable lists and a
|
|
44
|
+
Trello-style board, posting one-row moves that
|
|
45
|
+
[unmagic-sortable](https://github.com/unreasonable-magic/unmagic-sortable)
|
|
46
|
+
saves. See [Sortable lists and boards](#sortable-lists-and-boards).
|
|
39
47
|
- **No hard dependency** on a pagination library or on any helper of yours.
|
|
40
48
|
|
|
41
49
|
## Installation
|
|
@@ -69,11 +77,42 @@ show unmagic-components` prints where the gem is installed:
|
|
|
69
77
|
@import "/path/to/unmagic-components/app/assets/tailwind/unmagic_components/engine.css";
|
|
70
78
|
```
|
|
71
79
|
|
|
80
|
+
The gem brings two of its siblings with it: `unmagic-icon`, which renders the
|
|
81
|
+
glyphs, and `unmagic-color`, which picks avatar tints. The glyphs are a subset of
|
|
82
|
+
Lucide shipped inside this gem, so there is no icon set to download; under Rails
|
|
83
|
+
they are also available to your views as `unmagic_icon
|
|
84
|
+
"unmagic_components:lucide/<name>"`.
|
|
85
|
+
|
|
72
86
|
The engine mixes the helpers into ActionView automatically — no initializer
|
|
73
87
|
needed to get started. The interactive components need their JavaScript too; with
|
|
74
88
|
importmap-rails the engine pins it for you, so add `import "unmagic/components"`
|
|
75
89
|
to your application.js (see [Dialogs](#dialogs)).
|
|
76
90
|
|
|
91
|
+
### Browsing components
|
|
92
|
+
|
|
93
|
+
`Unmagic::Components::Browser` is an engine for looking through every component,
|
|
94
|
+
its examples and their source inside your own app. Mount it in your routes:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
mount Unmagic::Components::Browser::Engine => "/unmagic/components" if Rails.env.development?
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
It brings its own stylesheet, prebuilt with Tailwind's default theme, and loads
|
|
101
|
+
the components' JavaScript and Turbo itself, so nothing in your CSS or JS
|
|
102
|
+
changes. It needs `turbo-rails` in your bundle. It renders through your
|
|
103
|
+
configuration, so a `control_class` pointed at your own classes leaves its form
|
|
104
|
+
examples unstyled.
|
|
105
|
+
|
|
106
|
+
Its controllers inherit `ActionController::Base`, so your authentication doesn't
|
|
107
|
+
cover it. Its demo endpoints only write to the visitor's session, but mount it
|
|
108
|
+
outside development only behind a constraint of your own:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
authenticate :user, ->(user) { user.admin? } do
|
|
112
|
+
mount Unmagic::Components::Browser::Engine => "/unmagic/components"
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
77
116
|
## Theming
|
|
78
117
|
|
|
79
118
|
The components use Tailwind's own palette (neutral for surfaces, borders and
|
|
@@ -110,7 +149,7 @@ The gem's rules sit in `@layer components`, which Tailwind orders before
|
|
|
110
149
|
|
|
111
150
|
## Configuration
|
|
112
151
|
|
|
113
|
-
|
|
152
|
+
Seams, each with a working default. Point them at your own versions if you
|
|
114
153
|
already own these concerns:
|
|
115
154
|
|
|
116
155
|
```ruby
|
|
@@ -130,6 +169,16 @@ Unmagic::Components.configure do |config|
|
|
|
130
169
|
# renderer only needs something answering previous/next/page_url, so
|
|
131
170
|
# geared_pagination or your own object works just as well.
|
|
132
171
|
config.pagy_for = ->(view, collection) { view.table_pagy(collection) }
|
|
172
|
+
|
|
173
|
+
# A block of code: a tool call's payload, a failure's backtrace. Called with
|
|
174
|
+
# (view, source, language), where language is :json, :plaintext or whatever
|
|
175
|
+
# you passed; return markup. The default is an unhighlighted <pre><code>.
|
|
176
|
+
config.code_block = ->(view, source, language) { view.highlight_code(source, language: language) }
|
|
177
|
+
|
|
178
|
+
# What a sortable item carries for a record, and where drops post. The
|
|
179
|
+
# unmagic-sortable gem sets both to its signed keys and endpoint.
|
|
180
|
+
config.sortable_item = ->(view, record) { { key: record.to_param, rank: record.sortable_rank } }
|
|
181
|
+
config.sortable_url = ->(view) { view.reorder_path }
|
|
133
182
|
end
|
|
134
183
|
```
|
|
135
184
|
|
|
@@ -829,6 +878,462 @@ Needs `import "unmagic/components/toasts"`, which `import "unmagic/components"`
|
|
|
829
878
|
includes. The dismiss button's label is `unmagic.components.toast.dismiss`
|
|
830
879
|
("Dismiss").
|
|
831
880
|
|
|
881
|
+
## Sortable lists and boards
|
|
882
|
+
|
|
883
|
+
Drag-and-drop ordering, by pointer or keyboard. The server half — ranks, signed
|
|
884
|
+
keys and the endpoint — is the
|
|
885
|
+
[unmagic-sortable](https://github.com/unreasonable-magic/unmagic-sortable) gem,
|
|
886
|
+
which configures these components when it's installed:
|
|
887
|
+
|
|
888
|
+
```ruby
|
|
889
|
+
class Card < ApplicationRecord
|
|
890
|
+
include Unmagic::Sortable
|
|
891
|
+
sortable_within :board_id
|
|
892
|
+
sortable_column :column_id
|
|
893
|
+
end
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### `sortable_list(namespace: nil, params: {}, url: nil, orientation: :vertical, label: nil, **options, &block)`
|
|
897
|
+
|
|
898
|
+
```erb
|
|
899
|
+
<%= sortable_list label: "Interview steps", class: "flex flex-col gap-2" do |list| %>
|
|
900
|
+
<% @steps.ordered.each do |step| %>
|
|
901
|
+
<%= list.item(step) do %>
|
|
902
|
+
<%= sortable_handle label: "Move #{step.name}" %>
|
|
903
|
+
<%= step.name %>
|
|
904
|
+
<% end %>
|
|
905
|
+
<% end %>
|
|
906
|
+
<% end %>
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
- **Items:** `list.item(record)` takes its key and rank from
|
|
910
|
+
`config.sortable_item`; `key:` and `rank:` set them directly, and `label:` is
|
|
911
|
+
what a screen reader calls it.
|
|
912
|
+
- **Handles:** with a `sortable_handle` in an item, only the handle drags, so the
|
|
913
|
+
rest stays clickable, and the handle is the keyboard stop. Without one, the
|
|
914
|
+
whole item drags once the pointer moves, and the item takes focus.
|
|
915
|
+
- **Touch:** a finger lifts an item with a long press, so a swipe still scrolls
|
|
916
|
+
and a tap still taps. A `sortable_handle` grip drags at once.
|
|
917
|
+
- **Between lists:** lists sharing a `namespace:` exchange items. A drop posts
|
|
918
|
+
the destination list's `params:`.
|
|
919
|
+
- **Keyboard:** Space or Enter picks an item up. The arrows along the list move
|
|
920
|
+
it (`orientation:` decides which) and the arrows across move it to the next
|
|
921
|
+
list. Space drops it; Escape, or tabbing away, puts it back. Each step is
|
|
922
|
+
announced, and Escape also cancels a pointer drag.
|
|
923
|
+
- **Scrolling:** a drag near the edge of anything scrollable scrolls it.
|
|
924
|
+
- **The drop** fires a cancelable `unmagic-sortable:move` with
|
|
925
|
+
`{ key, original, prev, next, params }`, then PATCHes `url:`
|
|
926
|
+
(`config.sortable_url`) with `moved`, `original`, `prev`, `next` and the params.
|
|
927
|
+
Without a url, the event is all there is.
|
|
928
|
+
- **Styling:** `sortable-dragging:`, `sortable-placeholder:`, `sortable-lifted:`,
|
|
929
|
+
`sortable-active:` and `sortable-over:` variants decorate the drag states.
|
|
930
|
+
|
|
931
|
+
Needs `import "unmagic/components/sortable"`. I18n under
|
|
932
|
+
`unmagic.components.sortable`: `handle`, `instructions`, `picked`, `moved`,
|
|
933
|
+
`dropped` and `cancelled`, with `{item}`, `{list}`, `{position}` and `{count}`.
|
|
934
|
+
|
|
935
|
+
### `board(id:, url: nil, columns_url: nil, sortable_columns: true, label: nil, column_height: nil, **options, &block)`
|
|
936
|
+
|
|
937
|
+
```erb
|
|
938
|
+
<%= board id: "roadmap" do |board| %>
|
|
939
|
+
<% @columns.each do |column| %>
|
|
940
|
+
<% board.column column, title: column.name, params: { column_id: column.id } do |col| %>
|
|
941
|
+
<% col.actions { menu { |m| m.link "Rename", edit_column_path(column) } } %>
|
|
942
|
+
<% column.cards.ordered.each { |card| col.card(card) { render card } } %>
|
|
943
|
+
<% col.add url: cards_path, field: "card[title]", params: { "card[column_id]" => column.id } %>
|
|
944
|
+
<% end %>
|
|
945
|
+
<% end %>
|
|
946
|
+
<% board.add_column url: columns_path, field: "column[name]" %>
|
|
947
|
+
<% end %>
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
- **Moving:** cards move within and between the board's columns, posting the
|
|
951
|
+
column's `params:`. Columns move along the board by their header, and the grip
|
|
952
|
+
in it is their keyboard stop. On a phone, press and hold a card or a header. `sortable_columns: false` fixes them in place.
|
|
953
|
+
- **Forms:** `col.add` and `board.add_column` open into a one-field form. Enter
|
|
954
|
+
submits it, Escape closes it, and it stays open after adding, so several can be
|
|
955
|
+
added in a row.
|
|
956
|
+
- **Size:** `column_height:` (default `75dvh`) is how tall a column grows before
|
|
957
|
+
its cards scroll.
|
|
958
|
+
- **Where drops go:** `url:` is where they post; `columns_url:` sends column drops
|
|
959
|
+
elsewhere.
|
|
960
|
+
|
|
961
|
+
Needs `import "unmagic/components/sortable"` and `"unmagic/components/board"`.
|
|
962
|
+
I18n under `unmagic.components.board`: `label`, `add_card`, `add_card_submit`,
|
|
963
|
+
`add_column`, `add_column_submit`, `cancel`, `move_column` and `cards`.
|
|
964
|
+
|
|
965
|
+
## Avatars
|
|
966
|
+
|
|
967
|
+
### `avatar(name, src: nil, size: :medium, shape: :circle, tint: true, skeleton: false, **options)`
|
|
968
|
+
|
|
969
|
+
```erb
|
|
970
|
+
<%= avatar "Ada Lovelace" %>
|
|
971
|
+
<%= avatar @user.name, src: @user.avatar_url, size: :large %>
|
|
972
|
+
<%= avatar "Acme Ltd", shape: :square %>
|
|
973
|
+
|
|
974
|
+
<%= avatar_group max: 3, size: :small do |group| %>
|
|
975
|
+
<% @members.each { |member| group.avatar member.name, src: member.avatar_url } %>
|
|
976
|
+
<% end %>
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
- **Initials** come from the first and last words ("Ada Lovelace" is "AL") and
|
|
980
|
+
always render. The image sits on top of them, so a broken image shows the
|
|
981
|
+
initials without any script.
|
|
982
|
+
- **Tint:** the initials sit on one of six palette tints picked from the name
|
|
983
|
+
with `unmagic-color`'s stable string hash, so a person keeps their colour on
|
|
984
|
+
every page and every server. `tint: false` is neutral. Override
|
|
985
|
+
`UnmagicAvatar--tint-1` … `-6` to recolour them.
|
|
986
|
+
- **Sizes:** `:small`, `:medium` and `:large` (1.5, 2 and 2.5rem).
|
|
987
|
+
`skeleton: true` renders a skeleton circle of the same size.
|
|
988
|
+
- **Accessibility:** an avatar is `role="img"` named for the person. Pass
|
|
989
|
+
`"aria-hidden": true` when the name is written beside it.
|
|
990
|
+
- **Groups:** `avatar_group` sets the size and shape for every avatar in it, and
|
|
991
|
+
collapses past `max:` into a "+N" whose title names the rest.
|
|
992
|
+
|
|
993
|
+
I18n: `unmagic.components.avatar.group` ("%{count} people") and
|
|
994
|
+
`unmagic.components.avatar.more` ("+%{count}").
|
|
995
|
+
|
|
996
|
+
## Elapsed times
|
|
997
|
+
|
|
998
|
+
### `elapsed_tag(time, direction: :up, **options)`
|
|
999
|
+
|
|
1000
|
+
```erb
|
|
1001
|
+
<%= elapsed_tag tool_call.started_at %> <%# 3m 5s, and counting %>
|
|
1002
|
+
<%= elapsed_tag session.expires_at, direction: :down %> <%# 42s, and falling %>
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
A clock that counts up from a moment the server named, a second at a time, or
|
|
1006
|
+
down to one, stopping at zero and firing `unmagic-elapsed:end`. Work that takes a
|
|
1007
|
+
minute and work that has hung look the same behind a spinner; a number that keeps
|
|
1008
|
+
moving is the difference.
|
|
1009
|
+
|
|
1010
|
+
The server renders the current reading, so it is right before the script loads.
|
|
1011
|
+
Readings are whole seconds — `2s`, `3m 5s`, `1h 4m 2s` — and a settled duration
|
|
1012
|
+
under a second is milliseconds (`640ms`). The words are the
|
|
1013
|
+
`unmagic.components.elapsed.*` keys (`milliseconds`, `seconds`, `minutes`,
|
|
1014
|
+
`hours`); the element writes the English forms as it ticks. A blank time renders
|
|
1015
|
+
an em dash. Needs `import "unmagic/components/elapsed"`.
|
|
1016
|
+
|
|
1017
|
+
## AI chat
|
|
1018
|
+
|
|
1019
|
+
Components for rendering an agent's work, in the spirit of
|
|
1020
|
+
[assistant-ui](https://www.assistant-ui.com) but server-rendered: the host renders
|
|
1021
|
+
each turn, Turbo Streams keep it live, and small custom elements pace the reveal.
|
|
1022
|
+
|
|
1023
|
+
```erb
|
|
1024
|
+
<%= turbo_stream_from @chat %>
|
|
1025
|
+
|
|
1026
|
+
<div id="transcript" class="h-[70dvh] overflow-y-auto">
|
|
1027
|
+
<%= ai_chat id: "entries", scroller: "#transcript" do |chat| %>
|
|
1028
|
+
<% chat.welcome do %>
|
|
1029
|
+
<%= ai_chat_welcome heading: "What can I help with?" do |welcome| %>
|
|
1030
|
+
<% welcome.suggestion "Who hasn't replied yet?" %>
|
|
1031
|
+
<% end %>
|
|
1032
|
+
<% end %>
|
|
1033
|
+
<%= render @chat.entries %>
|
|
1034
|
+
<% end %>
|
|
1035
|
+
</div>
|
|
1036
|
+
|
|
1037
|
+
<%= form_with model: Message.new, url: chat_messages_path(@chat), id: "composer" do |form| %>
|
|
1038
|
+
<%= ai_chat_composer form: form, field: :content, state: @chat.run_state do |composer| %>
|
|
1039
|
+
<% composer.optimistic id: "message[client_id]", container: "#entries" %>
|
|
1040
|
+
<% end %>
|
|
1041
|
+
<% end %>
|
|
1042
|
+
<%= ai_chat_stop_form chat_stop_path(@chat) %>
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
```erb
|
|
1046
|
+
<%# app/views/messages/_message.html.erb %>
|
|
1047
|
+
<%= ai_chat_message role: message.role.to_sym, id: dom_id(message), streaming: message.pending? do %>
|
|
1048
|
+
<%= message.user? ? message.content : Markdown.render(message.content) %>
|
|
1049
|
+
<% end %>
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
They need Turbo and `import "unmagic/components"`, or the modules named below.
|
|
1053
|
+
|
|
1054
|
+
### How the pieces fit
|
|
1055
|
+
|
|
1056
|
+
- **Entries are upserted by id.** Broadcast each entry with the gem's `upsert`
|
|
1057
|
+
action into the transcript's id. A record already on the page is replaced in
|
|
1058
|
+
place; a new one is inserted in id order.
|
|
1059
|
+
|
|
1060
|
+
```ruby
|
|
1061
|
+
broadcast_action_to chat, action: :upsert, target: "entries", partial: "messages/message", locals: { message: self }
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
- **The question is drawn before the server has it.** `composer.optimistic`
|
|
1065
|
+
renders an `<unmagic-uuid-input>` and a template of the question. On submit,
|
|
1066
|
+
`<unmagic-optimistic>` draws the question, dimmed, under the minted id. Create
|
|
1067
|
+
the record under that id (`message[client_id]`) and its broadcast replaces the
|
|
1068
|
+
placeholder.
|
|
1069
|
+
- **Replies stream as whole renders.** While a reply is being written, send the
|
|
1070
|
+
whole of it so far, rendered, to the body's id. `<unmagic-streaming-markdown>`
|
|
1071
|
+
works out what's new and reveals it at the model's own average pace, so bursts
|
|
1072
|
+
read as one stream. When the turn settles, upsert the whole message. The new
|
|
1073
|
+
element picks up the reveal where the old one left off.
|
|
1074
|
+
|
|
1075
|
+
```ruby
|
|
1076
|
+
broadcast_action_to chat, action: :stream_markdown, target: "#{dom_id(self)}_content",
|
|
1077
|
+
html: Markdown.render(content)
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
- **The gem never parses Markdown.** Hand it your own rendered, sanitised HTML.
|
|
1081
|
+
`UnmagicProse` styles it, and code blocks, like tool payloads, go through
|
|
1082
|
+
`config.code_block`.
|
|
1083
|
+
- **Only the composer's button changes with the turn's state.** Redraw it with
|
|
1084
|
+
`ai_chat_composer_action` so a half-typed draft survives.
|
|
1085
|
+
- **Amber means waiting on a person.** A question, a permission request or a plan
|
|
1086
|
+
step parked on someone is the one state that won't move on its own, so it is
|
|
1087
|
+
the one in colour.
|
|
1088
|
+
|
|
1089
|
+
### `ai_chat(id:, scroller: nil, follow: true, scroll_to_latest: true, **options, &block)`
|
|
1090
|
+
|
|
1091
|
+
The scrolling region turns are rendered into: a polite `role="log"`. It renders
|
|
1092
|
+
no entries of its own; use a partial per entry type.
|
|
1093
|
+
|
|
1094
|
+
- **Following:** it keeps the scroller (the page, or `scroller:`) at the bottom
|
|
1095
|
+
while the reader is there, and stops when they scroll up. A jump-to-latest
|
|
1096
|
+
button shows meanwhile (`scroll_to_latest: false` for none). `follow: false`
|
|
1097
|
+
renders a bare log.
|
|
1098
|
+
- **Spacing:** turns are separated by a gap that hidden entries don't earn. Mark
|
|
1099
|
+
hidden entries between two tool calls (a tool result's anchor) with
|
|
1100
|
+
`data-ai-chat-timeline="gap"` so the run stays joined.
|
|
1101
|
+
- **Welcome:** `chat.welcome` shows while there are no entries, and hides as the
|
|
1102
|
+
first one arrives.
|
|
1103
|
+
|
|
1104
|
+
Needs `import "unmagic/components/autoscroll"`. I18n:
|
|
1105
|
+
`unmagic.components.ai_chat.transcript.label` ("Conversation") and `.latest`
|
|
1106
|
+
("Jump to latest"). The button's dock is sticky at `bottom-4`; lift it with
|
|
1107
|
+
`.UnmagicAIChat__latest { bottom: … }` if a sticky composer covers it.
|
|
1108
|
+
|
|
1109
|
+
### `ai_chat_message(content = nil, role:, id: nil, streaming: false, final: false, optimistic: nil, **options, &block)`
|
|
1110
|
+
|
|
1111
|
+
One turn. A user's turn is a bubble of plain text with its line breaks kept; an
|
|
1112
|
+
assistant's is unbubbled prose.
|
|
1113
|
+
|
|
1114
|
+
- **The body** of an assistant turn with an id is an
|
|
1115
|
+
`<unmagic-streaming-markdown id="#{id}_content">`. `streaming: true` shows a
|
|
1116
|
+
thinking spinner until the first flush and marks the body busy. `final: true`
|
|
1117
|
+
refuses any later flush, for a stopped or failed reply.
|
|
1118
|
+
- **Empty:** a settled assistant turn with nothing in it is `hidden`, keeping its
|
|
1119
|
+
id for broadcasts.
|
|
1120
|
+
- **Parts:** `turn.reasoning(**options) { }` (see `ai_chat_reasoning`),
|
|
1121
|
+
`turn.actions { }`, `turn.branches { }` and `turn.attachments { }` (above a
|
|
1122
|
+
user's bubble).
|
|
1123
|
+
- **Optimistic:** `optimistic: { id:, text: }` renders the template a composer
|
|
1124
|
+
fills from those fields, dimmed.
|
|
1125
|
+
|
|
1126
|
+
I18n: `unmagic.components.ai_chat.message.user` ("You said"), `.assistant`
|
|
1127
|
+
("Assistant said") and `.thinking` ("Thinking"), for screen readers.
|
|
1128
|
+
|
|
1129
|
+
### `streaming_markdown_tag(content = nil, id:, final: false, streaming: false, **options, &block)`
|
|
1130
|
+
|
|
1131
|
+
The streaming body on its own. Send flushes with
|
|
1132
|
+
`turbo_stream.stream_markdown(target, html)`, each carrying the whole render so
|
|
1133
|
+
far. Under reduced motion each flush paints at once. It fires
|
|
1134
|
+
`unmagic-streaming-markdown:settle` when it has caught up. Needs
|
|
1135
|
+
`import "unmagic/components/streaming_markdown"`.
|
|
1136
|
+
|
|
1137
|
+
### `ai_chat_reasoning(content = nil, title: nil, streaming: false, duration: nil, open: false, **options, &block)`
|
|
1138
|
+
|
|
1139
|
+
The model's thinking, collapsed above its reply. `duration:` titles it "Thought
|
|
1140
|
+
for 12s"; `streaming: true` shows "Thinking…". `reasoning.block { }` adds a
|
|
1141
|
+
block. Blank content renders nothing. I18n:
|
|
1142
|
+
`unmagic.components.ai_chat.reasoning.title`, `.thinking` and `.duration`.
|
|
1143
|
+
|
|
1144
|
+
### `ai_chat_tool_call(name:, state:, id: nil, icon: nil, open: false, timeline: true, **options, &block)`
|
|
1145
|
+
|
|
1146
|
+
```erb
|
|
1147
|
+
<%= ai_chat_tool_call name: call.name, state: call.state, id: dom_id(call), icon: call.category_icon do |tool| %>
|
|
1148
|
+
<% tool.summary call.summary %>
|
|
1149
|
+
<% tool.timing started_at: call.started_at, duration: call.duration %>
|
|
1150
|
+
<% tool.asked call.arguments %>
|
|
1151
|
+
<% tool.answered call.response %>
|
|
1152
|
+
<% end %>
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
- **State:** `:queued`, `:running`, `:waiting`, `:done` or `:failed`. A running
|
|
1156
|
+
call is busy, spins, and counts up with `elapsed_tag`. A done call shows
|
|
1157
|
+
`icon:`, what it was about, in place of a column of identical ticks.
|
|
1158
|
+
- **Timeline:** consecutive calls are joined by a line, which runs down an open
|
|
1159
|
+
call's body. `timeline: false` leaves a call out of the run.
|
|
1160
|
+
- **Parts:** `summary`, `timing`, `asked` and `answered` (payloads, in a
|
|
1161
|
+
`<details>`, so no script and no state to restore after a redraw), `failures`
|
|
1162
|
+
(a count for a batch that mostly worked), `progress` (a line with id
|
|
1163
|
+
`"#{id}_progress"` to replace as the tool reports), and `made { }` (output
|
|
1164
|
+
kept outside the fold).
|
|
1165
|
+
|
|
1166
|
+
I18n under `unmagic.components.ai_chat.tool_call`: `queued`, `running`,
|
|
1167
|
+
`waiting`, `done`, `failed`, `asked`, `answered`, `failures`, `partly_failed`,
|
|
1168
|
+
`running_for` and `took`.
|
|
1169
|
+
|
|
1170
|
+
### `ai_chat_payload(payload, label: nil, language: nil, duration: nil, copy: false, **options)`
|
|
1171
|
+
|
|
1172
|
+
What went into a tool call or came back. A Hash, an Array, or a string holding
|
|
1173
|
+
JSON is laid out a key to a line as `:json`; anything else is `:plaintext`
|
|
1174
|
+
unless `language:` says otherwise. It renders through `config.code_block`,
|
|
1175
|
+
scrolls past a height, wraps long lines, and is reachable by keyboard. `copy:
|
|
1176
|
+
true` adds a copy button. `nil` renders nothing. I18n:
|
|
1177
|
+
`unmagic.components.ai_chat.payload.took` and `.label` ("Payload").
|
|
1178
|
+
|
|
1179
|
+
### `ai_chat_composer(form:, field:, state: :idle, label: nil, stop_form: "stop_turn", placeholder: nil, rows: 2, **options, &block)`
|
|
1180
|
+
|
|
1181
|
+
- **The form needs an `id:`.** Send names it with `form=`, and Stop names
|
|
1182
|
+
`stop_form:`. Render `ai_chat_stop_form(url)` outside your form, since forms
|
|
1183
|
+
can't nest.
|
|
1184
|
+
- **State** — `:idle`, `:running`, `:stopping` or `:waiting` — changes only the
|
|
1185
|
+
button region (id `"#{form_id}_action"`, a polite live region). The field is
|
|
1186
|
+
never disabled. Redraw the region alone with
|
|
1187
|
+
`ai_chat_composer_action(form:, state:)`.
|
|
1188
|
+
- **Keys:** Enter sends and Shift+Enter makes a new line. Enter does nothing
|
|
1189
|
+
while there's no Send button. A successful submit resets the form and keeps
|
|
1190
|
+
focus in the field.
|
|
1191
|
+
- **Parts:** `composer.optimistic(id:, container:)`, `composer.attach { }`,
|
|
1192
|
+
`composer.actions { }` and `composer.menu { }` (an `ai_chat_slash_menu`).
|
|
1193
|
+
- **The field** is the composer's own chrome, not a `control_class` control: the
|
|
1194
|
+
box around it is the control and takes the focus ring.
|
|
1195
|
+
|
|
1196
|
+
Needs `import "unmagic/components/ai_chat"`. I18n under
|
|
1197
|
+
`unmagic.components.ai_chat.composer`: `send`, `stop`, `stopping`, `waiting`
|
|
1198
|
+
and `label`.
|
|
1199
|
+
|
|
1200
|
+
### `ai_chat_slash_menu(for: nil, id: nil, trigger: "/", above: false, insert: nil, **options, &block)`
|
|
1201
|
+
|
|
1202
|
+
Commands offered on a slash. `menu.item(name, description:, arguments:)` for
|
|
1203
|
+
each; every item is rendered and typing filters them by prefix. Up and Down move,
|
|
1204
|
+
Enter or Tab takes one, Escape closes, and focus stays in the field (the combobox
|
|
1205
|
+
pattern). Picking writes `"/name "` at the cursor. `for:` defaults to the
|
|
1206
|
+
composer's field. Needs `import "unmagic/components/slash_menu"`. I18n:
|
|
1207
|
+
`unmagic.components.ai_chat.slash_menu.label` ("Commands").
|
|
1208
|
+
|
|
1209
|
+
### `ai_chat_attachments(align: :start, **options, &block)` and `ai_chat_dropzone(input:, url: nil, field: nil, chips: nil, label: nil, **options, &block)`
|
|
1210
|
+
|
|
1211
|
+
`ai_chat_attachments` lists the files a sent turn carries
|
|
1212
|
+
(`files.file name, size:, url:, thumbnail:`; `align: :end` for a user's turn).
|
|
1213
|
+
|
|
1214
|
+
`ai_chat_dropzone` wraps a region that takes dropped and pasted files, with an
|
|
1215
|
+
overlay while dragging. `input:` is the file input the files join, which is also
|
|
1216
|
+
the way in for anyone who can't drag. Without `url:`, files wait on that input
|
|
1217
|
+
and post with the form. With `url:`, each file is POSTed on the spot as `file`,
|
|
1218
|
+
and the JSON response's `value` is posted with the message under `field:`.
|
|
1219
|
+
`chips:` is where attached files are listed: the composer's
|
|
1220
|
+
`[data-ai-chat-chips]`. It fires `unmagic-dropzone:attach` and `:error`. Needs
|
|
1221
|
+
`import "unmagic/components/dropzone"`. I18n under
|
|
1222
|
+
`unmagic.components.ai_chat.attachments`: `drop`, `attached`, `remove` and
|
|
1223
|
+
`failed`.
|
|
1224
|
+
|
|
1225
|
+
### `ai_chat_welcome(heading:, heading_tag: :h2, composer: "composer", field: nil, **options, &block)`
|
|
1226
|
+
|
|
1227
|
+
What an empty conversation says. `welcome.body(text)` and
|
|
1228
|
+
`welcome.suggestion(label, icon:, fill:, value:)`. Pressing a suggestion puts it
|
|
1229
|
+
in the composer form with id `composer:` and sends it, unless `fill: true` or a
|
|
1230
|
+
turn is running. Needs `import "unmagic/components/ai_chat"`.
|
|
1231
|
+
|
|
1232
|
+
### `ai_chat_request(state:, url: nil, method: :patch, prompt: nil, label: nil, scope: :response, live: false, **options, &block)`
|
|
1233
|
+
|
|
1234
|
+
```erb
|
|
1235
|
+
<%= ai_chat_request state: :waiting, url: answer_path(@chat), live: true do |request| %>
|
|
1236
|
+
<% request.question "How should it sound?", header: "Tone" do |q| %>
|
|
1237
|
+
<% q.option "Friendly", description: "A short, warm check-in" %>
|
|
1238
|
+
<% q.option "Direct" %>
|
|
1239
|
+
<% end %>
|
|
1240
|
+
<% end %>
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
- **Ask with questions** (`multiple: true` for checkboxes; answers post as
|
|
1244
|
+
`answers[i][]`) **or with a form** (`request.form { |form| … }`), not both.
|
|
1245
|
+
`request.decline` adds a Decline that skips validation.
|
|
1246
|
+
- **Answered states** — `:accepted`, `:declined`, `:cancelled`, `:timed_out` —
|
|
1247
|
+
keep the questions and show what was picked (`question picked:`) or answered
|
|
1248
|
+
(`request.answer label, value`). Declined and dropped cards dim.
|
|
1249
|
+
- **`live: true`** adds `role="alert"` and, if nothing else has focus, focuses
|
|
1250
|
+
the first option.
|
|
1251
|
+
|
|
1252
|
+
I18n under `unmagic.components.ai_chat.request`: `waiting`, `asked`, `answer`,
|
|
1253
|
+
`decline`, `accepted`, `declined`, `cancelled`, `timed_out` and `unanswered`.
|
|
1254
|
+
|
|
1255
|
+
### `ai_chat_permission(tool:, state:, url: nil, live: false, outcome: nil, **options, &block)`
|
|
1256
|
+
|
|
1257
|
+
```erb
|
|
1258
|
+
<%= ai_chat_permission tool: "delete_files", state: :waiting, url: permission_path(@call) do |ask| %>
|
|
1259
|
+
<% ask.argument "paths", "drafts/*" %>
|
|
1260
|
+
<% ask.summary { markdown(@call.summary) } %>
|
|
1261
|
+
<% ask.allow confirm: "Delete these files? This can't be undone." %>
|
|
1262
|
+
<% ask.allow "Allow for any chat", params: { widened: true } %>
|
|
1263
|
+
<% ask.refuse %>
|
|
1264
|
+
<% end %>
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
- **The ask** — the tool's own name and its arguments — comes before the
|
|
1268
|
+
reasoning, because it's what the decision is about.
|
|
1269
|
+
- **Actions:** the first `allow` is the prominent, red, consequential choice; a
|
|
1270
|
+
later one is the wider grant. Offer that only when there is something to
|
|
1271
|
+
widen. `allow` POSTs and `refuse` DELETEs to `url:`, each with `params:` and
|
|
1272
|
+
`confirm:`.
|
|
1273
|
+
- **Focus** goes to the card when it arrives, never to Allow.
|
|
1274
|
+
- **Answered:** `:granted`, `:refused`, or `:lapsed` (answered in the chat,
|
|
1275
|
+
nothing granted), with `outcome:` to reword the sentence.
|
|
1276
|
+
|
|
1277
|
+
I18n under `unmagic.components.ai_chat.permission`: `waiting`, `asked`, `allow`,
|
|
1278
|
+
`refuse`, `granted`, `refused` and `lapsed`.
|
|
1279
|
+
|
|
1280
|
+
### `ai_chat_proposal(state: :pending, icon: :lightbulb, id: nil, **options, &block)`
|
|
1281
|
+
|
|
1282
|
+
An offer the agent made in passing, which doesn't stop the work: `offer.claim`,
|
|
1283
|
+
`offer.meta`, `offer.accept { }` and `offer.reject { }` while `:pending`; the
|
|
1284
|
+
outcome (`"Saved"`, `"Dismissed"`, or `offer.outcome`) once `:accepted` or
|
|
1285
|
+
`:rejected`. It is an `<aside>` named by its claim, and `not-prose` so it can sit
|
|
1286
|
+
inside a reply. I18n: `unmagic.components.ai_chat.proposal.accepted` and
|
|
1287
|
+
`.rejected`.
|
|
1288
|
+
|
|
1289
|
+
### `ai_chat_citation(compact: false, **options, &block)`
|
|
1290
|
+
|
|
1291
|
+
A quotation rendered from the record, not the model's paraphrase:
|
|
1292
|
+
`cite.avatar` (with no block, the gem's avatar for `who`), `cite.who`,
|
|
1293
|
+
`cite.when(time, url:)` and `cite.quote(text)`. The quote is escaped text with
|
|
1294
|
+
its line breaks kept. `compact: true` is one line, for a list of sources.
|
|
1295
|
+
|
|
1296
|
+
### `ai_chat_failure(message = nil, live: false, **options, &block)`
|
|
1297
|
+
|
|
1298
|
+
A turn that fell over. The sentence is for whoever asked. `failure.cause` sits
|
|
1299
|
+
above the fold; `failure.detail(label, payload, language:)` rows fold away for
|
|
1300
|
+
whoever works on the assistant; `failure.retry { }` holds your control. Render
|
|
1301
|
+
it below a turn's body, never in its place. `live: true` adds `role="alert"`.
|
|
1302
|
+
I18n: `unmagic.components.ai_chat.failure.details`.
|
|
1303
|
+
|
|
1304
|
+
### `ai_chat_plan(**options, &block)` and `ai_chat_workspace(**options, &block)`
|
|
1305
|
+
|
|
1306
|
+
Two sections of one side panel. Both are collapsible, open by default, and
|
|
1307
|
+
always render, empty or not, so a broadcast has something to replace.
|
|
1308
|
+
|
|
1309
|
+
- **`plan.step(title, state:)`:** `:pending`, `:in_progress`, `:waiting` or
|
|
1310
|
+
`:completed`, with an optional block for detail. The count reads
|
|
1311
|
+
completed/total, or `completed:`/`total:` when a panel shows only some steps.
|
|
1312
|
+
- **`workspace.file(path, size:, url:, icon:)`:** a flat list of paths. A file
|
|
1313
|
+
with a url is a link.
|
|
1314
|
+
- **Both take** `title:`, `open:`, `empty:`, `collapsible:` and `title_tag:`
|
|
1315
|
+
(`:h2`). The workspace also takes `count:`.
|
|
1316
|
+
|
|
1317
|
+
I18n under `unmagic.components.ai_chat`: `plan.title`, `plan.empty`,
|
|
1318
|
+
`plan.pending`, `plan.in_progress`, `plan.waiting`, `plan.completed`,
|
|
1319
|
+
`workspace.title` and `workspace.empty`.
|
|
1320
|
+
|
|
1321
|
+
### `ai_chat_action_bar(for:, reveal: :hover, **options, &block)` and `ai_chat_branch_picker(index:, count:, previous: nil, next: nil, method: :get, **options)`
|
|
1322
|
+
|
|
1323
|
+
The controls under a turn. `bar.copy(text)`, `bar.action(label, url, icon:,
|
|
1324
|
+
method:, confirm:)` and `bar.control { }` make a toolbar with one Tab stop and
|
|
1325
|
+
arrow keys between controls (`<unmagic-toolbar>`). `reveal: :hover` shows it on
|
|
1326
|
+
hover or focus, and always on touch screens; it is never hidden from the
|
|
1327
|
+
keyboard.
|
|
1328
|
+
|
|
1329
|
+
`ai_chat_branch_picker` walks between versions of a turn: "Version 2 of 3",
|
|
1330
|
+
with a disabled end where there's nowhere to go. What a branch is stays your
|
|
1331
|
+
application's business, and one version renders nothing.
|
|
1332
|
+
|
|
1333
|
+
Needs `import "unmagic/components/toolbar"` and `"unmagic/components/ai_chat"`.
|
|
1334
|
+
I18n: `unmagic.components.ai_chat.action_bar.label` and
|
|
1335
|
+
`unmagic.components.ai_chat.branch_picker.label`, `.previous` and `.next`.
|
|
1336
|
+
|
|
832
1337
|
## Development
|
|
833
1338
|
|
|
834
1339
|
```sh
|