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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// <unmagic-streaming-markdown id="…_content"> — server-rendered HTML revealed at a
|
|
2
|
+
// steady pace as fuller renders arrive, rendered by `streaming_markdown_tag` and
|
|
3
|
+
// `ai_chat_message`.
|
|
4
|
+
//
|
|
5
|
+
// The server renders a reply to HTML and sends the whole render on every flush,
|
|
6
|
+
// through the `stream_markdown` Turbo Stream action registered below. Flushes
|
|
7
|
+
// arrive in bursts, which paints chunkily; this element decouples reveal cadence
|
|
8
|
+
// from arrival. It holds the newest render as a target and eases a character
|
|
9
|
+
// cursor toward it every animation frame. Parsing stays on the server: this only
|
|
10
|
+
// reveals a growing prefix of the HTML.
|
|
11
|
+
//
|
|
12
|
+
// It reveals at a constant velocity — the model's own average production rate —
|
|
13
|
+
// and lets the buffer (how far the cursor trails the live edge) absorb the bursts.
|
|
14
|
+
// A proportional "close a fraction of the gap" scheme would speed up and slow down
|
|
15
|
+
// on every burst. The rate is an average over RATE_SMOOTHING; a soft pull toward a
|
|
16
|
+
// small buffer (BUFFER_SECONDS) stops the trailing distance drifting; a lag cap
|
|
17
|
+
// catches up on a big jump (a reconnect) and a trickle floor finishes the tail.
|
|
18
|
+
//
|
|
19
|
+
// The visible prefix is reconciled block by block: settled leading blocks
|
|
20
|
+
// (identical by tag and text) are left alone, and everything from the first
|
|
21
|
+
// changed block is rebuilt, which keeps lists and tables structurally intact.
|
|
22
|
+
//
|
|
23
|
+
// When the settled turn replaces the streaming one mid-reveal, the replacement
|
|
24
|
+
// picks up where the old element left off (HANDOFF) and paces to the end, but only
|
|
25
|
+
// if its content extends what was on screen; a divergent settle (a "stopped" note)
|
|
26
|
+
// shows at once.
|
|
27
|
+
//
|
|
28
|
+
// final settled: show the content as it is and refuse any later flush.
|
|
29
|
+
// This is what lets a Stop button's optimistic swap stick before
|
|
30
|
+
// the server has settled the turn.
|
|
31
|
+
// aria-busy rendered while a reply is live; removed once the reveal has
|
|
32
|
+
// caught up and gone quiet, so a live region announces the reply
|
|
33
|
+
// once rather than on every flush.
|
|
34
|
+
//
|
|
35
|
+
// It fires unmagic-streaming-markdown:settle when it catches up. Under reduced
|
|
36
|
+
// motion every flush paints in full. Needs Turbo.
|
|
37
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
|
38
|
+
|
|
39
|
+
const RATE_SMOOTHING = 0.6 // seconds: the window for the production-rate average, which is the reveal speed
|
|
40
|
+
const BUFFER_SECONDS = 0.3 // seconds of text the cursor aims to keep between itself and the live edge
|
|
41
|
+
const SOFTNESS = 0.4 // per second: how firmly that buffer is held; smaller is steadier and drifts more
|
|
42
|
+
const MAX_LAG_SECONDS = 1.5 // always drain the whole backlog within this long
|
|
43
|
+
const MIN_TRICKLE = 24 // characters a second, so the tail still finishes
|
|
44
|
+
const IDLE_MS = 700 // keep the loop alive this long after the last flush
|
|
45
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
46
|
+
|
|
47
|
+
// Reveal progress carried across an element swap, keyed by element id.
|
|
48
|
+
const HANDOFF = new Map()
|
|
49
|
+
|
|
50
|
+
class UnmagicStreamingMarkdown extends HTMLElement {
|
|
51
|
+
#target = null // DocumentFragment: the newest full render
|
|
52
|
+
#targetLength = 0 // its revealable length (the sum of top-level block text)
|
|
53
|
+
#revealed = 0 // characters on screen, fractional between frames
|
|
54
|
+
#rate = 0 // estimated production rate in characters a second
|
|
55
|
+
#lastArrival = 0
|
|
56
|
+
#lastFrame = 0
|
|
57
|
+
#frame = null
|
|
58
|
+
#frozen = false
|
|
59
|
+
|
|
60
|
+
connectedCallback() {
|
|
61
|
+
if (this.hasAttribute("final")) {
|
|
62
|
+
HANDOFF.delete(this.id)
|
|
63
|
+
this.#frozen = true
|
|
64
|
+
this.#revealed = blockLength(this)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handoff = HANDOFF.get(this.id)
|
|
69
|
+
HANDOFF.delete(this.id)
|
|
70
|
+
const full = blockLength(this)
|
|
71
|
+
const continues =
|
|
72
|
+
handoff && handoff.revealed > 0 && handoff.revealed < full && this.textContent.startsWith(handoff.text)
|
|
73
|
+
|
|
74
|
+
if (continues && !reducedMotion.matches) {
|
|
75
|
+
this.#target = adoptChildren(this)
|
|
76
|
+
this.#targetLength = full
|
|
77
|
+
this.#revealed = handoff.revealed
|
|
78
|
+
this.#rate = handoff.rate
|
|
79
|
+
this.#lastArrival = performance.now()
|
|
80
|
+
this.#paint()
|
|
81
|
+
this.#schedule()
|
|
82
|
+
} else {
|
|
83
|
+
this.#revealed = full
|
|
84
|
+
this.#targetLength = full
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
disconnectedCallback() {
|
|
89
|
+
if (this.#frame) cancelAnimationFrame(this.#frame)
|
|
90
|
+
this.#frame = null
|
|
91
|
+
|
|
92
|
+
if (this.#revealed < this.#targetLength) {
|
|
93
|
+
HANDOFF.set(this.id, { revealed: this.#revealed, rate: this.#rate, text: this.textContent })
|
|
94
|
+
} else {
|
|
95
|
+
HANDOFF.delete(this.id)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Called by the stream_markdown action with a fragment of the whole render so far.
|
|
100
|
+
streamUpdate(fragment) {
|
|
101
|
+
if (this.#frozen) return
|
|
102
|
+
|
|
103
|
+
const now = performance.now()
|
|
104
|
+
const length = blockLength(fragment)
|
|
105
|
+
const grew = Math.max(0, length - this.#targetLength)
|
|
106
|
+
this.#target = fragment
|
|
107
|
+
this.#targetLength = length
|
|
108
|
+
if (this.#revealed > length) this.#revealed = length
|
|
109
|
+
|
|
110
|
+
this.#observeRate(now, grew)
|
|
111
|
+
this.#lastArrival = now
|
|
112
|
+
this.setAttribute("aria-busy", "true")
|
|
113
|
+
|
|
114
|
+
if (reducedMotion.matches) {
|
|
115
|
+
this.#revealed = length
|
|
116
|
+
this.#paint()
|
|
117
|
+
this.#settle()
|
|
118
|
+
} else {
|
|
119
|
+
this.#paint()
|
|
120
|
+
this.#schedule()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Lock onto the first reading, so the opening characters don't crawl while the
|
|
125
|
+
// average warms up.
|
|
126
|
+
#observeRate(now, grew) {
|
|
127
|
+
if (!this.#lastArrival) return
|
|
128
|
+
|
|
129
|
+
const dt = (now - this.#lastArrival) / 1000
|
|
130
|
+
if (dt <= 0) return
|
|
131
|
+
|
|
132
|
+
const instant = grew / dt
|
|
133
|
+
if (this.#rate === 0) this.#rate = instant
|
|
134
|
+
else this.#rate += (1 - Math.exp(-dt / RATE_SMOOTHING)) * (instant - this.#rate)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#schedule() {
|
|
138
|
+
if (this.#frame) return
|
|
139
|
+
this.#lastFrame = performance.now()
|
|
140
|
+
this.#frame = requestAnimationFrame((now) => this.#tick(now))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#tick(now) {
|
|
144
|
+
this.#frame = null
|
|
145
|
+
const dt = Math.min(0.05, (now - this.#lastFrame) / 1000) // a backgrounded tab resumes gently
|
|
146
|
+
this.#lastFrame = now
|
|
147
|
+
|
|
148
|
+
const backlog = this.#targetLength - this.#revealed
|
|
149
|
+
if (backlog > 0) {
|
|
150
|
+
let velocity = this.#rate + (backlog - this.#rate * BUFFER_SECONDS) * SOFTNESS
|
|
151
|
+
velocity = Math.max(velocity, backlog / MAX_LAG_SECONDS)
|
|
152
|
+
velocity = Math.min(2000, Math.max(MIN_TRICKLE, velocity))
|
|
153
|
+
this.#revealed = Math.min(this.#targetLength, this.#revealed + velocity * dt)
|
|
154
|
+
if (this.#targetLength - this.#revealed < 0.5) this.#revealed = this.#targetLength
|
|
155
|
+
this.#paint()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const streaming = now - this.#lastArrival < IDLE_MS
|
|
159
|
+
if (this.#revealed < this.#targetLength || streaming) this.#schedule()
|
|
160
|
+
else this.#settle()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#settle() {
|
|
164
|
+
if (!this.hasAttribute("aria-busy")) return
|
|
165
|
+
this.removeAttribute("aria-busy")
|
|
166
|
+
this.dispatchEvent(new CustomEvent("unmagic-streaming-markdown:settle", { bubbles: true }))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#paint() {
|
|
170
|
+
const visible = truncatedClone(this.#target, Math.floor(this.#revealed))
|
|
171
|
+
try {
|
|
172
|
+
this.#reconcile(visible)
|
|
173
|
+
} catch {
|
|
174
|
+
this.replaceChildren(visible)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#reconcile(fragment) {
|
|
179
|
+
const incoming = Array.from(fragment.children)
|
|
180
|
+
if (incoming.length === 0) return this.replaceChildren(fragment)
|
|
181
|
+
|
|
182
|
+
const current = Array.from(this.children)
|
|
183
|
+
let i = 0
|
|
184
|
+
while (
|
|
185
|
+
i < current.length &&
|
|
186
|
+
i < incoming.length &&
|
|
187
|
+
current[i].nodeName === incoming[i].nodeName &&
|
|
188
|
+
current[i].textContent === incoming[i].textContent
|
|
189
|
+
)
|
|
190
|
+
i++
|
|
191
|
+
|
|
192
|
+
if (i === 0) {
|
|
193
|
+
this.replaceChildren()
|
|
194
|
+
} else {
|
|
195
|
+
const anchor = current[i - 1]
|
|
196
|
+
while (anchor.nextSibling) anchor.nextSibling.remove()
|
|
197
|
+
}
|
|
198
|
+
for (let k = i; k < incoming.length; k++) this.append(incoming[k])
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
customElements.get("unmagic-streaming-markdown") ||
|
|
203
|
+
customElements.define("unmagic-streaming-markdown", UnmagicStreamingMarkdown)
|
|
204
|
+
|
|
205
|
+
// Move an element's children into a fragment, leaving it empty to reveal into.
|
|
206
|
+
function adoptChildren(root) {
|
|
207
|
+
const fragment = document.createDocumentFragment()
|
|
208
|
+
while (root.firstChild) fragment.append(root.firstChild)
|
|
209
|
+
return fragment
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// A clone of the target's top-level blocks, cut to `limit` characters of block
|
|
213
|
+
// text; the last block is split where the cursor lands inside it.
|
|
214
|
+
function truncatedClone(target, limit) {
|
|
215
|
+
const fragment = document.createDocumentFragment()
|
|
216
|
+
if (!target) return fragment
|
|
217
|
+
|
|
218
|
+
let count = 0
|
|
219
|
+
for (const child of target.children) {
|
|
220
|
+
if (count >= limit) break
|
|
221
|
+
const length = child.textContent.length
|
|
222
|
+
const clone = child.cloneNode(true)
|
|
223
|
+
if (count + length <= limit) {
|
|
224
|
+
fragment.append(clone)
|
|
225
|
+
count += length
|
|
226
|
+
} else {
|
|
227
|
+
truncateTo(clone, limit - count)
|
|
228
|
+
fragment.append(clone)
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return fragment
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The text length of an element's top-level blocks. Whitespace between blocks
|
|
236
|
+
// doesn't count.
|
|
237
|
+
function blockLength(root) {
|
|
238
|
+
let length = 0
|
|
239
|
+
for (const child of root.children) length += child.textContent.length
|
|
240
|
+
return length
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function truncateTo(root, limit) {
|
|
244
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
245
|
+
let count = 0
|
|
246
|
+
let lastKept = null
|
|
247
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
248
|
+
const length = node.data.length
|
|
249
|
+
if (count + length <= limit) {
|
|
250
|
+
count += length
|
|
251
|
+
lastKept = node
|
|
252
|
+
if (count === limit) break
|
|
253
|
+
} else {
|
|
254
|
+
node.data = node.data.slice(0, limit - count)
|
|
255
|
+
lastKept = node
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!lastKept) return root.replaceChildren()
|
|
260
|
+
for (let node = lastKept; node && node !== root; node = node.parentNode) {
|
|
261
|
+
while (node.nextSibling) node.nextSibling.remove()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// `update`, for streamed Markdown: hand the whole render to the element, which
|
|
266
|
+
// paces its way to it.
|
|
267
|
+
Turbo.StreamActions.stream_markdown = function () {
|
|
268
|
+
for (const element of this.targetElements) element.streamUpdate?.(this.templateContent)
|
|
269
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Each toast arrives as an inert <template data-unmagic-toast-template> inside the
|
|
4
4
|
// element: rendered with the page, morphed in by a refresh, or appended by
|
|
5
5
|
// turbo_stream.toast. The element clones each one into its stack, animates it in,
|
|
6
|
-
// and dismisses it after the element's `duration` (milliseconds)
|
|
7
|
-
// focusing a toast holds it open.
|
|
6
|
+
// and dismisses it after the element's `duration` (milliseconds), or keeps it up
|
|
7
|
+
// until it is dismissed when that is 0. Hovering or focusing a toast holds it open.
|
|
8
8
|
//
|
|
9
9
|
// The stack is data-turbo-permanent, so a toast on screen survives a Drive visit
|
|
10
10
|
// or a morph. Its timer lives here, in the module, keyed by the toast rather than
|
|
@@ -54,9 +54,16 @@ class UnmagicToasts extends HTMLElement {
|
|
|
54
54
|
return this.querySelector(":scope > .UnmagicToasts__stack")
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// `duration="0"` keeps a toast up until it is dismissed. Absent, empty, negative or unparseable
|
|
58
|
+
// all fall back to the default rather than silently pinning every toast to the screen — which is
|
|
59
|
+
// why this isn't `Number(attr) || DURATION`: Number(null) and Number("") are both 0, so a missing
|
|
60
|
+
// attribute would have read as "never" too.
|
|
57
61
|
get duration() {
|
|
58
|
-
const
|
|
59
|
-
|
|
62
|
+
const raw = this.getAttribute("duration")?.trim()
|
|
63
|
+
if (!raw) return DURATION
|
|
64
|
+
|
|
65
|
+
const duration = Number(raw)
|
|
66
|
+
return Number.isFinite(duration) && duration >= 0 ? duration : DURATION
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
#consume() {
|
|
@@ -118,6 +125,9 @@ class UnmagicToasts extends HTMLElement {
|
|
|
118
125
|
|
|
119
126
|
function schedule(toast, duration) {
|
|
120
127
|
if (timers.has(toast)) return
|
|
128
|
+
// A duration of 0 is a toast that waits to be dismissed: no timer, and so nothing for hold and
|
|
129
|
+
// release to pause — both look the toast up in `timers` and leave when it isn't there.
|
|
130
|
+
if (duration === 0) return
|
|
121
131
|
|
|
122
132
|
const entry = { remaining: duration, started: 0, timer: null }
|
|
123
133
|
timers.set(toast, entry)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// <unmagic-toolbar role="toolbar"> — a row of controls with one Tab stop,
|
|
2
|
+
// rendered by `ai_chat_action_bar`.
|
|
3
|
+
//
|
|
4
|
+
// The WAI-ARIA toolbar pattern: Tab enters and leaves the row as one stop, and
|
|
5
|
+
// Left, Right, Home and End move between its controls (a roving tabindex). The
|
|
6
|
+
// control that last had focus is the one Tab returns to. Controls are its
|
|
7
|
+
// buttons, links and anything already carrying a tabindex, skipping disabled and
|
|
8
|
+
// hidden ones. It rescans when its contents change, so a streamed or
|
|
9
|
+
// re-rendered bar needs no setup.
|
|
10
|
+
|
|
11
|
+
class UnmagicToolbar extends HTMLElement {
|
|
12
|
+
#observer = new MutationObserver(() => this.#reset())
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super()
|
|
16
|
+
this.addEventListener("keydown", this.#keydown)
|
|
17
|
+
this.addEventListener("focusin", (event) => {
|
|
18
|
+
if (this.#controls.includes(event.target)) this.#activate(event.target)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connectedCallback() {
|
|
23
|
+
this.#reset()
|
|
24
|
+
this.#observer.observe(this, { childList: true, subtree: true })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
disconnectedCallback() {
|
|
28
|
+
this.#observer.disconnect()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get #controls() {
|
|
32
|
+
return Array.from(this.querySelectorAll("button, a[href], [tabindex]")).filter(
|
|
33
|
+
(control) => !control.disabled && control.getAttribute("aria-disabled") !== "true" && !control.closest("[hidden]"),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#reset() {
|
|
38
|
+
const controls = this.#controls
|
|
39
|
+
const current = controls.find((control) => control.getAttribute("tabindex") === "0") ?? controls[0]
|
|
40
|
+
if (current) this.#activate(current, controls)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#activate(target, controls = this.#controls) {
|
|
44
|
+
for (const control of controls) control.setAttribute("tabindex", control === target ? "0" : "-1")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#keydown = (event) => {
|
|
48
|
+
const controls = this.#controls
|
|
49
|
+
const index = controls.indexOf(document.activeElement)
|
|
50
|
+
if (index === -1) return
|
|
51
|
+
|
|
52
|
+
let next
|
|
53
|
+
switch (event.key) {
|
|
54
|
+
case "ArrowRight":
|
|
55
|
+
next = controls[(index + 1) % controls.length]
|
|
56
|
+
break
|
|
57
|
+
case "ArrowLeft":
|
|
58
|
+
next = controls[(index - 1 + controls.length) % controls.length]
|
|
59
|
+
break
|
|
60
|
+
case "Home":
|
|
61
|
+
next = controls[0]
|
|
62
|
+
break
|
|
63
|
+
case "End":
|
|
64
|
+
next = controls[controls.length - 1]
|
|
65
|
+
break
|
|
66
|
+
default:
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
event.preventDefault()
|
|
71
|
+
this.#activate(next, controls)
|
|
72
|
+
next.focus()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
customElements.get("unmagic-toolbar") || customElements.define("unmagic-toolbar", UnmagicToolbar)
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// server is now confirming — it is replaced in place, so a record that is both on the page and
|
|
10
10
|
// broadcast never duplicates. Otherwise it is inserted into the target container at the position
|
|
11
11
|
// its id sorts to, so out-of-order delivery still lands in id order. That ordering assumes
|
|
12
|
-
// time-ordered ids (UUIDv7, ULID); with random ids the position is stable but arbitrary.
|
|
12
|
+
// time-ordered ids (UUIDv7, ULID); with random ids the position is stable but arbitrary. The rule
|
|
13
|
+
// itself lives in placement.js, shared with <unmagic-optimistic>.
|
|
13
14
|
//
|
|
14
15
|
// A list showing newest first says so, and the comparison flips:
|
|
15
16
|
//
|
|
@@ -18,28 +19,11 @@
|
|
|
18
19
|
// broadcast_action_to "tasks", action: :upsert, target: "task_rows",
|
|
19
20
|
// attributes: { order: "desc" }, partial: "tasks/row", locals: { task: self }
|
|
20
21
|
import { Turbo } from "@hotwired/turbo-rails"
|
|
22
|
+
import { place } from "unmagic/components/placement"
|
|
21
23
|
|
|
22
24
|
Turbo.StreamActions.upsert = function () {
|
|
23
25
|
const incoming = this.templateContent.firstElementChild
|
|
24
26
|
if (!incoming) return
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
if (existing) {
|
|
28
|
-
existing.replaceWith(incoming)
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const container = this.targetElements[0]
|
|
33
|
-
if (!container) return
|
|
34
|
-
|
|
35
|
-
const descending = this.getAttribute("order") === "desc"
|
|
36
|
-
|
|
37
|
-
// Without an id there is nothing to sort on, so it goes last.
|
|
38
|
-
const successor = incoming.id
|
|
39
|
-
? Array.from(container.children).find(
|
|
40
|
-
(child) => child.id && (descending ? child.id < incoming.id : child.id > incoming.id),
|
|
41
|
-
)
|
|
42
|
-
: null
|
|
43
|
-
|
|
44
|
-
container.insertBefore(incoming, successor ?? null)
|
|
28
|
+
place(incoming, this.targetElements[0], { descending: this.getAttribute("order") === "desc" })
|
|
45
29
|
}
|
|
@@ -17,3 +17,13 @@ import "unmagic/components/tabs"
|
|
|
17
17
|
import "unmagic/components/clipboard"
|
|
18
18
|
import "unmagic/components/autogrow"
|
|
19
19
|
import "unmagic/components/uuid_input"
|
|
20
|
+
import "unmagic/components/elapsed"
|
|
21
|
+
import "unmagic/components/autoscroll"
|
|
22
|
+
import "unmagic/components/optimistic"
|
|
23
|
+
import "unmagic/components/streaming_markdown"
|
|
24
|
+
import "unmagic/components/toolbar"
|
|
25
|
+
import "unmagic/components/slash_menu"
|
|
26
|
+
import "unmagic/components/dropzone"
|
|
27
|
+
import "unmagic/components/ai_chat"
|
|
28
|
+
import "unmagic/components/sortable"
|
|
29
|
+
import "unmagic/components/board"
|