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,135 @@
|
|
|
1
|
+
// <unmagic-sortable-list namespace="…" url="…"> — a list whose items can be
|
|
2
|
+
// dragged into a new place, rendered by `sortable_list` and `board`.
|
|
3
|
+
//
|
|
4
|
+
// namespace lists sharing one exchange items; a list without one takes only
|
|
5
|
+
// its own
|
|
6
|
+
// url where a drop is posted (PATCH); none, and the drop only fires the
|
|
7
|
+
// event below
|
|
8
|
+
// orientation vertical (default), horizontal or grid: which arrow keys move an
|
|
9
|
+
// item along the list
|
|
10
|
+
// label what the list is called in announcements
|
|
11
|
+
// data-sortable-instructions, -picked, -moved, -dropped, -cancelled
|
|
12
|
+
// the words announced, with {item}, {list}, {position}, {count}
|
|
13
|
+
//
|
|
14
|
+
// A drop into this list fires unmagic-sortable:move on the item, cancelable, with
|
|
15
|
+
// { key, original, prev, next, params }. Unless it's cancelled the list posts:
|
|
16
|
+
//
|
|
17
|
+
// moved the item's key
|
|
18
|
+
// original the rank it had when it was picked up, so the server can refuse a
|
|
19
|
+
// move made against a stale order
|
|
20
|
+
// prev the rank of the item now before it (blank at the start)
|
|
21
|
+
// next the rank of the item now after it (blank at the end)
|
|
22
|
+
// <name> one field per <unmagic-sortable-param> in this list: the column a
|
|
23
|
+
// card was dropped into
|
|
24
|
+
//
|
|
25
|
+
// With Turbo the post goes as a Turbo form submission, so a refresh or stream in
|
|
26
|
+
// the response reconciles the page. unmagic-sortable's endpoint does exactly this.
|
|
27
|
+
|
|
28
|
+
let counter = 0
|
|
29
|
+
|
|
30
|
+
class UnmagicSortableList extends HTMLElement {
|
|
31
|
+
#instructions = null
|
|
32
|
+
|
|
33
|
+
get namespace() {
|
|
34
|
+
return this.getAttribute("namespace") || ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get orientation() {
|
|
38
|
+
return this.getAttribute("orientation") || "vertical"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get label() {
|
|
42
|
+
return this.getAttribute("label") || ""
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Whether an item that came from `origin` may be dropped here.
|
|
46
|
+
accepts(origin) {
|
|
47
|
+
return origin === this || (this.namespace !== "" && this.namespace === origin?.namespace)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// This list's own items, in order, leaving out those of any list inside it.
|
|
51
|
+
items() {
|
|
52
|
+
return [...this.querySelectorAll("unmagic-sortable-item")].filter((item) => item.list === this)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
itemsExcept(item) {
|
|
56
|
+
return this.items().filter((other) => other !== item)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The first child that stays at the end whatever is dropped (an "add" tile).
|
|
60
|
+
tail() {
|
|
61
|
+
return [...this.children].find((child) => child.hasAttribute("data-sortable-tail")) ?? null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A hidden element holding the keyboard instructions, which every item's
|
|
65
|
+
// keyboard stop is described by. Made on demand, since a morph can remove it.
|
|
66
|
+
get instructionsId() {
|
|
67
|
+
if (!this.#instructions?.isConnected) {
|
|
68
|
+
this.#instructions = document.createElement("span")
|
|
69
|
+
this.#instructions.id = `unmagic-sortable-instructions-${++counter}`
|
|
70
|
+
this.#instructions.hidden = true
|
|
71
|
+
this.#instructions.textContent = this.dataset.sortableInstructions || ""
|
|
72
|
+
this.append(this.#instructions)
|
|
73
|
+
}
|
|
74
|
+
return this.#instructions.id
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
submit(item) {
|
|
78
|
+
const items = this.items()
|
|
79
|
+
const index = items.indexOf(item)
|
|
80
|
+
const params = this.#params()
|
|
81
|
+
const detail = {
|
|
82
|
+
key: item.key,
|
|
83
|
+
original: item.rank ?? "",
|
|
84
|
+
prev: items[index - 1]?.rank ?? "",
|
|
85
|
+
next: items[index + 1]?.rank ?? "",
|
|
86
|
+
params: Object.fromEntries(params.map((param) => [param.name, param.value])),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const proceed = item.dispatchEvent(new CustomEvent("unmagic-sortable:move", { bubbles: true, cancelable: true, detail }))
|
|
90
|
+
const url = this.getAttribute("url")
|
|
91
|
+
if (!proceed || !url) return
|
|
92
|
+
|
|
93
|
+
const form = document.createElement("form")
|
|
94
|
+
form.method = "post"
|
|
95
|
+
form.action = url
|
|
96
|
+
form.hidden = true
|
|
97
|
+
const token = document.querySelector("meta[name=csrf-token]")?.content
|
|
98
|
+
const tokenName = document.querySelector("meta[name=csrf-param]")?.content || "authenticity_token"
|
|
99
|
+
|
|
100
|
+
field(form, "_method", "patch")
|
|
101
|
+
if (token) field(form, tokenName, token)
|
|
102
|
+
field(form, "moved", detail.key)
|
|
103
|
+
field(form, "original", detail.original)
|
|
104
|
+
field(form, "prev", detail.prev)
|
|
105
|
+
field(form, "next", detail.next)
|
|
106
|
+
for (const param of params) field(form, param.name, param.value)
|
|
107
|
+
|
|
108
|
+
document.body.append(form)
|
|
109
|
+
form.requestSubmit()
|
|
110
|
+
form.remove()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// This list's own params: not an inner list's, and not inside one of this list's
|
|
114
|
+
// items. A board's card list sits inside its column's item, so an item around
|
|
115
|
+
// the list itself doesn't count.
|
|
116
|
+
#params() {
|
|
117
|
+
return [...this.querySelectorAll("unmagic-sortable-param")].filter((param) => {
|
|
118
|
+
if (param.closest("unmagic-sortable-list") !== this) return false
|
|
119
|
+
const item = param.closest("unmagic-sortable-item")
|
|
120
|
+
return !item || !this.contains(item)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function field(form, name, value) {
|
|
126
|
+
const input = document.createElement("input")
|
|
127
|
+
input.type = "hidden"
|
|
128
|
+
input.name = name
|
|
129
|
+
input.value = value ?? ""
|
|
130
|
+
form.append(input)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
customElements.get("unmagic-sortable-list") || customElements.define("unmagic-sortable-list", UnmagicSortableList)
|
|
134
|
+
|
|
135
|
+
export { UnmagicSortableList }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// <unmagic-sortable-param name="column_id" value="3"> — one field a list posts
|
|
2
|
+
// with a drop into it, rendered by `sortable_list`'s params:. It shows nothing.
|
|
3
|
+
|
|
4
|
+
class UnmagicSortableParam extends HTMLElement {
|
|
5
|
+
get name() {
|
|
6
|
+
return this.getAttribute("name")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get value() {
|
|
10
|
+
return this.getAttribute("value")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
customElements.get("unmagic-sortable-param") || customElements.define("unmagic-sortable-param", UnmagicSortableParam)
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
// The drag engine behind <unmagic-sortable-list> and <unmagic-sortable-item>, by
|
|
2
|
+
// pointer or by keyboard. One drag at a time.
|
|
3
|
+
//
|
|
4
|
+
// By pointer it floats a copy of the item under the cursor while the item itself
|
|
5
|
+
// moves through the lists as a live preview of where it will land: into whichever
|
|
6
|
+
// list under the pointer accepts it, at the slot the pointer is in (vertical lists
|
|
7
|
+
// and wrapping rows alike). Near the edge of anything scrollable, including the
|
|
8
|
+
// page, it scrolls. Escape puts the item back. On release, the list it landed in
|
|
9
|
+
// posts the move.
|
|
10
|
+
//
|
|
11
|
+
// On a touch screen, a finger is usually scrolling, so unless it's on a handle
|
|
12
|
+
// that stops scrolling (a grip) the item waits for a long press (LONG_PRESS,
|
|
13
|
+
// holding within TOUCH_SLOP) before it lifts. Moving sooner is a scroll and
|
|
14
|
+
// nothing is dragged; lifting sooner is a tap and reaches whatever was tapped.
|
|
15
|
+
// Once lifted, the page stops scrolling under the finger for the rest of the
|
|
16
|
+
// gesture.
|
|
17
|
+
//
|
|
18
|
+
// By keyboard, Space or Enter on an item's handle (or the item, when it has none)
|
|
19
|
+
// picks it up. The arrow keys along the list move it within the list; the arrows
|
|
20
|
+
// across it move it to the neighbouring list that accepts it. Space or Enter drops
|
|
21
|
+
// it, and Escape or leaving it puts it back. Every step is announced.
|
|
22
|
+
//
|
|
23
|
+
// The engine uses the elements only through their public methods and properties
|
|
24
|
+
// (item.list, list.accepts, list.items, list.submit), so nothing imports it back.
|
|
25
|
+
//
|
|
26
|
+
// State is data attributes, which the CSS and the sortable-* Tailwind variants key
|
|
27
|
+
// on: data-sortable-dragging on the floating copy, data-sortable-placeholder on the
|
|
28
|
+
// item while a pointer drags it, data-sortable-lifted while a keyboard holds it,
|
|
29
|
+
// data-sortable-active on every list that can take it, and data-sortable-over on
|
|
30
|
+
// the list under the pointer.
|
|
31
|
+
|
|
32
|
+
const THRESHOLD = 4 // pixels of movement before a press becomes a drag
|
|
33
|
+
const LONG_PRESS = 250 // milliseconds a finger holds still before an item lifts
|
|
34
|
+
const TOUCH_SLOP = 8 // pixels a finger may drift during that hold
|
|
35
|
+
const EDGE = 56 // pixels from a scroller's edge where dragging scrolls it
|
|
36
|
+
const MAX_SPEED = 20 // pixels a frame at the very edge
|
|
37
|
+
|
|
38
|
+
const LIST = "unmagic-sortable-list"
|
|
39
|
+
const DRAGGING = "data-sortable-dragging"
|
|
40
|
+
const PLACEHOLDER = "data-sortable-placeholder"
|
|
41
|
+
const LIFTED = "data-sortable-lifted"
|
|
42
|
+
const ACTIVE = "data-sortable-active"
|
|
43
|
+
const OVER = "data-sortable-over"
|
|
44
|
+
|
|
45
|
+
let active = null
|
|
46
|
+
|
|
47
|
+
export function dragging() {
|
|
48
|
+
return active !== null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function beginPointerDrag(item, event, { immediate = false } = {}) {
|
|
52
|
+
if (!active) active = new PointerDrag(item, event, immediate)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function beginKeyboardDrag(item, stop) {
|
|
56
|
+
if (!active) active = new KeyboardDrag(item, stop)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function keyboardDragOf(item) {
|
|
60
|
+
return active instanceof KeyboardDrag && active.item === item ? active : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Put a list's item where `before` is, or at the end of its items when null, which
|
|
64
|
+
// keeps it ahead of anything marked data-sortable-tail (an "add" tile).
|
|
65
|
+
export function place(list, item, before) {
|
|
66
|
+
if (before) {
|
|
67
|
+
before.before(item)
|
|
68
|
+
} else {
|
|
69
|
+
const tail = list.tail()
|
|
70
|
+
if (tail) tail.before(item)
|
|
71
|
+
else list.append(item)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class Drag {
|
|
76
|
+
constructor(item) {
|
|
77
|
+
this.item = item
|
|
78
|
+
this.origin = item.list
|
|
79
|
+
this.startParent = item.parentElement
|
|
80
|
+
this.startNext = item.nextElementSibling
|
|
81
|
+
this.targets = []
|
|
82
|
+
// A subclass's cancel isn't assigned until this constructor returns, so the
|
|
83
|
+
// listener calls through to whichever cancel the drag ends up with.
|
|
84
|
+
this.cacheListener = () => this.cancel()
|
|
85
|
+
document.addEventListener("turbo:before-cache", this.cacheListener)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Every list that can take the item: its own, and any sharing its namespace.
|
|
89
|
+
lightUp() {
|
|
90
|
+
this.targets = [...document.querySelectorAll(LIST)].filter((list) => list.accepts(this.origin))
|
|
91
|
+
for (const list of this.targets) list.setAttribute(ACTIVE, "")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
end() {
|
|
95
|
+
for (const list of this.targets) {
|
|
96
|
+
list.removeAttribute(ACTIVE)
|
|
97
|
+
list.removeAttribute(OVER)
|
|
98
|
+
}
|
|
99
|
+
document.removeEventListener("turbo:before-cache", this.cacheListener)
|
|
100
|
+
active = null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
moved() {
|
|
104
|
+
return this.item.parentElement !== this.startParent || this.item.nextElementSibling !== this.startNext
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
restore() {
|
|
108
|
+
this.startParent.insertBefore(this.item, this.startNext)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
commit() {
|
|
112
|
+
if (this.moved()) this.item.list?.submit(this.item)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class PointerDrag extends Drag {
|
|
117
|
+
#pointerId
|
|
118
|
+
#offsetX
|
|
119
|
+
#offsetY
|
|
120
|
+
#width
|
|
121
|
+
#startX
|
|
122
|
+
#startY
|
|
123
|
+
#x
|
|
124
|
+
#y
|
|
125
|
+
#started = false
|
|
126
|
+
#clone = null
|
|
127
|
+
#frame = null
|
|
128
|
+
#touch = false
|
|
129
|
+
#press = null
|
|
130
|
+
|
|
131
|
+
constructor(item, event, immediate) {
|
|
132
|
+
super(item)
|
|
133
|
+
this.#pointerId = event.pointerId
|
|
134
|
+
this.#touch = event.pointerType === "touch" && !immediate
|
|
135
|
+
|
|
136
|
+
const box = item.getBoundingClientRect()
|
|
137
|
+
this.#offsetX = event.clientX - box.left
|
|
138
|
+
this.#offsetY = event.clientY - box.top
|
|
139
|
+
this.#width = box.width
|
|
140
|
+
this.#startX = this.#x = event.clientX
|
|
141
|
+
this.#startY = this.#y = event.clientY
|
|
142
|
+
|
|
143
|
+
// A touch that may yet be a scroll isn't captured, so the browser can pan.
|
|
144
|
+
if (!this.#touch) item.setPointerCapture?.(event.pointerId)
|
|
145
|
+
window.addEventListener("pointermove", this.#move)
|
|
146
|
+
window.addEventListener("pointerup", this.#up)
|
|
147
|
+
window.addEventListener("pointercancel", this.cancel)
|
|
148
|
+
window.addEventListener("keydown", this.#keydown)
|
|
149
|
+
|
|
150
|
+
if (this.#touch) {
|
|
151
|
+
// Non-passive, so it can stop the page scrolling once the item has lifted. It
|
|
152
|
+
// does nothing before then, so a scroll is still a scroll.
|
|
153
|
+
window.addEventListener("touchmove", this.#holdPage, { passive: false })
|
|
154
|
+
window.addEventListener("contextmenu", this.#noMenu)
|
|
155
|
+
this.#press = setTimeout(this.#lift, LONG_PRESS)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (immediate) {
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
this.#start()
|
|
161
|
+
this.#follow()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#move = (event) => {
|
|
166
|
+
if (event.pointerId !== this.#pointerId) return
|
|
167
|
+
this.#x = event.clientX
|
|
168
|
+
this.#y = event.clientY
|
|
169
|
+
|
|
170
|
+
if (!this.#started) {
|
|
171
|
+
const distance = Math.hypot(this.#x - this.#startX, this.#y - this.#startY)
|
|
172
|
+
if (this.#touch) {
|
|
173
|
+
if (distance > TOUCH_SLOP) this.#stop() // moved before the hold: a scroll
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
if (distance < THRESHOLD) return
|
|
177
|
+
this.#start()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
this.#follow()
|
|
182
|
+
this.#reposition()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The copy is made before anything else is marked, so its own lists aren't
|
|
186
|
+
// mistaken for targets, and it is inert and stripped of ids so it can't be.
|
|
187
|
+
#start() {
|
|
188
|
+
this.#started = true
|
|
189
|
+
this.lightUp()
|
|
190
|
+
|
|
191
|
+
const clone = this.item.cloneNode(true)
|
|
192
|
+
clone.setAttribute(DRAGGING, "")
|
|
193
|
+
clone.setAttribute("aria-hidden", "true")
|
|
194
|
+
clone.inert = true
|
|
195
|
+
clone.removeAttribute("id")
|
|
196
|
+
for (const element of clone.querySelectorAll("[id]")) element.removeAttribute("id")
|
|
197
|
+
clone.style.width = `${this.#width}px`
|
|
198
|
+
document.body.append(clone)
|
|
199
|
+
this.#clone = clone
|
|
200
|
+
|
|
201
|
+
this.item.setAttribute(PLACEHOLDER, "")
|
|
202
|
+
this.#frame = requestAnimationFrame(this.#scroll)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The long press held: lift the item where the finger is, and take the pointer.
|
|
206
|
+
#lift = () => {
|
|
207
|
+
this.#press = null
|
|
208
|
+
if (active !== this || this.#started) return
|
|
209
|
+
|
|
210
|
+
this.item.setPointerCapture?.(this.#pointerId)
|
|
211
|
+
navigator.vibrate?.(10)
|
|
212
|
+
this.#start()
|
|
213
|
+
this.#follow()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#holdPage = (event) => {
|
|
217
|
+
if (this.#started && event.cancelable) event.preventDefault()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#noMenu = (event) => {
|
|
221
|
+
event.preventDefault()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#follow() {
|
|
225
|
+
this.#clone.style.transform = `translate(${this.#x - this.#offsetX}px, ${this.#y - this.#offsetY}px)`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#reposition() {
|
|
229
|
+
const list = this.#listAt(this.#x, this.#y)
|
|
230
|
+
for (const target of this.targets) target.toggleAttribute(OVER, target === list)
|
|
231
|
+
if (list) place(list, this.item, slotBefore(list, this.item, this.#x, this.#y))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// The innermost list under the pointer that takes this item. A column dragged
|
|
235
|
+
// over a column's cards passes the cards' list by for the board's.
|
|
236
|
+
#listAt(x, y) {
|
|
237
|
+
let list = document.elementFromPoint(x, y)?.closest(LIST)
|
|
238
|
+
while (list) {
|
|
239
|
+
if (list.accepts(this.origin) && !this.item.contains(list)) return list
|
|
240
|
+
list = list.parentElement?.closest(LIST)
|
|
241
|
+
}
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#scroll = () => {
|
|
246
|
+
this.#frame = null
|
|
247
|
+
if (!active || active !== this) return
|
|
248
|
+
|
|
249
|
+
let scrolled = false
|
|
250
|
+
for (const scroller of scrollersAt(this.#x, this.#y)) {
|
|
251
|
+
const page = scroller === document.scrollingElement
|
|
252
|
+
const box = page ? { left: 0, top: 0, right: innerWidth, bottom: innerHeight } : scroller.getBoundingClientRect()
|
|
253
|
+
const dx = scroller.scrollWidth > scroller.clientWidth ? speed(this.#x, box.left, box.right) : 0
|
|
254
|
+
const dy = scroller.scrollHeight > scroller.clientHeight ? speed(this.#y, box.top, box.bottom) : 0
|
|
255
|
+
if (!dx && !dy) continue
|
|
256
|
+
|
|
257
|
+
const beforeLeft = scroller.scrollLeft
|
|
258
|
+
const beforeTop = scroller.scrollTop
|
|
259
|
+
scroller.scrollLeft += dx
|
|
260
|
+
scroller.scrollTop += dy
|
|
261
|
+
scrolled ||= scroller.scrollLeft !== beforeLeft || scroller.scrollTop !== beforeTop
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (scrolled) this.#reposition()
|
|
265
|
+
this.#frame = requestAnimationFrame(this.#scroll)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#keydown = (event) => {
|
|
269
|
+
if (event.key !== "Escape" || !this.#started) return
|
|
270
|
+
event.preventDefault()
|
|
271
|
+
this.cancel()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#up = (event) => {
|
|
275
|
+
if (event.pointerId !== this.#pointerId) return
|
|
276
|
+
const started = this.#started
|
|
277
|
+
this.#stop()
|
|
278
|
+
if (!started) return
|
|
279
|
+
|
|
280
|
+
suppressNextClick()
|
|
281
|
+
this.commit()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
cancel = (event) => {
|
|
285
|
+
if (event?.pointerId !== undefined && event.pointerId !== this.#pointerId) return
|
|
286
|
+
const started = this.#started
|
|
287
|
+
this.#stop()
|
|
288
|
+
if (started) this.restore()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#stop() {
|
|
292
|
+
clearTimeout(this.#press)
|
|
293
|
+
this.#press = null
|
|
294
|
+
window.removeEventListener("touchmove", this.#holdPage)
|
|
295
|
+
window.removeEventListener("contextmenu", this.#noMenu)
|
|
296
|
+
window.removeEventListener("pointermove", this.#move)
|
|
297
|
+
window.removeEventListener("pointerup", this.#up)
|
|
298
|
+
window.removeEventListener("pointercancel", this.cancel)
|
|
299
|
+
window.removeEventListener("keydown", this.#keydown)
|
|
300
|
+
if (this.#frame) cancelAnimationFrame(this.#frame)
|
|
301
|
+
this.#frame = null
|
|
302
|
+
this.#clone?.remove()
|
|
303
|
+
this.item.removeAttribute(PLACEHOLDER)
|
|
304
|
+
this.end()
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
class KeyboardDrag extends Drag {
|
|
309
|
+
#stop
|
|
310
|
+
#moving = false
|
|
311
|
+
|
|
312
|
+
constructor(item, stop) {
|
|
313
|
+
super(item)
|
|
314
|
+
this.#stop = stop
|
|
315
|
+
this.lightUp()
|
|
316
|
+
item.setAttribute(LIFTED, "")
|
|
317
|
+
stop.addEventListener("blur", this.#blur)
|
|
318
|
+
announce(item, "picked")
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Keys while holding the item. Handled here and nowhere else: an outer item
|
|
322
|
+
// mustn't act on them too.
|
|
323
|
+
key(event) {
|
|
324
|
+
const list = this.item.list
|
|
325
|
+
const orientation = list?.orientation || "vertical"
|
|
326
|
+
const along = ALONG[orientation][event.key]
|
|
327
|
+
const across = ACROSS[orientation][event.key]
|
|
328
|
+
|
|
329
|
+
if (event.key === " " || event.key === "Enter") this.drop()
|
|
330
|
+
else if (event.key === "Escape") this.cancel()
|
|
331
|
+
else if (along) this.#step(along)
|
|
332
|
+
else if (across) this.#jump(across)
|
|
333
|
+
else return
|
|
334
|
+
|
|
335
|
+
event.preventDefault()
|
|
336
|
+
event.stopPropagation()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#step(delta) {
|
|
340
|
+
const items = this.item.list.items()
|
|
341
|
+
const neighbour = items[items.indexOf(this.item) + delta]
|
|
342
|
+
if (!neighbour) return
|
|
343
|
+
|
|
344
|
+
this.#moveTo(() => (delta < 0 ? neighbour.before(this.item) : neighbour.after(this.item)))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// To the same position in the next list along, or its end.
|
|
348
|
+
#jump(delta) {
|
|
349
|
+
const lists = this.targets.filter((list) => !this.item.contains(list))
|
|
350
|
+
const next = lists[lists.indexOf(this.item.list) + delta]
|
|
351
|
+
if (!next) return
|
|
352
|
+
|
|
353
|
+
const index = this.item.list.items().indexOf(this.item)
|
|
354
|
+
this.#moveTo(() => place(next, this.item, next.itemsExcept(this.item)[index] ?? null))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Moving a focused element can drop its focus, so it's handed straight back.
|
|
358
|
+
#moveTo(move) {
|
|
359
|
+
this.#moving = true
|
|
360
|
+
move()
|
|
361
|
+
this.#stop.focus()
|
|
362
|
+
this.#moving = false
|
|
363
|
+
this.item.scrollIntoView({ block: "nearest", inline: "nearest" })
|
|
364
|
+
announce(this.item, "moved")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
drop() {
|
|
368
|
+
this.#finish()
|
|
369
|
+
announce(this.item, "dropped")
|
|
370
|
+
this.commit()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
cancel = () => {
|
|
374
|
+
this.#moving = true
|
|
375
|
+
this.restore()
|
|
376
|
+
this.#stop.focus()
|
|
377
|
+
this.#moving = false
|
|
378
|
+
this.#finish()
|
|
379
|
+
announce(this.item, "cancelled")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#blur = () => {
|
|
383
|
+
if (this.#moving) return
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
if (active === this && document.activeElement !== this.#stop) this.cancel()
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#finish() {
|
|
390
|
+
this.#stop.removeEventListener("blur", this.#blur)
|
|
391
|
+
this.item.removeAttribute(LIFTED)
|
|
392
|
+
this.end()
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Which arrow moves an item along its list, and which moves it across to the next
|
|
397
|
+
// list, for each orientation.
|
|
398
|
+
const ALONG = {
|
|
399
|
+
vertical: { ArrowUp: -1, ArrowDown: 1 },
|
|
400
|
+
horizontal: { ArrowLeft: -1, ArrowRight: 1 },
|
|
401
|
+
grid: { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -1, ArrowDown: 1 },
|
|
402
|
+
}
|
|
403
|
+
const ACROSS = {
|
|
404
|
+
vertical: { ArrowLeft: -1, ArrowRight: 1 },
|
|
405
|
+
horizontal: { ArrowUp: -1, ArrowDown: 1 },
|
|
406
|
+
grid: {},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// The item the dragged one should sit before, or null for the end. It finds the
|
|
410
|
+
// item whose centre is nearest the pointer and picks a side: in a wrapping row, a
|
|
411
|
+
// row above means before and the same row goes by the centre line; in a list of
|
|
412
|
+
// full-width items, above the centre means before.
|
|
413
|
+
function slotBefore(list, dragged, x, y) {
|
|
414
|
+
const others = list.itemsExcept(dragged)
|
|
415
|
+
if (others.length === 0) return null
|
|
416
|
+
|
|
417
|
+
let nearest = null
|
|
418
|
+
let distance = Infinity
|
|
419
|
+
for (const item of others) {
|
|
420
|
+
const box = item.getBoundingClientRect()
|
|
421
|
+
const centreX = box.left + box.width / 2
|
|
422
|
+
const centreY = box.top + box.height / 2
|
|
423
|
+
const d = Math.hypot(x - centreX, y - centreY)
|
|
424
|
+
if (d < distance) {
|
|
425
|
+
distance = d
|
|
426
|
+
nearest = { item, box, centreX, centreY }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const across = nearest.box.width < list.getBoundingClientRect().width - 4
|
|
431
|
+
let after
|
|
432
|
+
if (across) {
|
|
433
|
+
if (y < nearest.box.top) after = false
|
|
434
|
+
else if (y > nearest.box.bottom) after = true
|
|
435
|
+
else after = x > nearest.centreX
|
|
436
|
+
} else {
|
|
437
|
+
after = y > nearest.centreY
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!after) return nearest.item
|
|
441
|
+
return others[others.indexOf(nearest.item) + 1] ?? null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Everything under a point that scrolls, innermost first, and the page.
|
|
445
|
+
function scrollersAt(x, y) {
|
|
446
|
+
const found = []
|
|
447
|
+
for (let element = document.elementFromPoint(x, y); element; element = element.parentElement) {
|
|
448
|
+
if (element === document.body || element === document.documentElement) break
|
|
449
|
+
const style = getComputedStyle(element)
|
|
450
|
+
const scrolls = /(auto|scroll)/.test(style.overflowX + style.overflowY)
|
|
451
|
+
if (scrolls && (element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight)) {
|
|
452
|
+
found.push(element)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
found.push(document.scrollingElement)
|
|
456
|
+
return found
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// How far to scroll this frame: faster the nearer the pointer is to an edge, and
|
|
460
|
+
// as fast as it goes past one.
|
|
461
|
+
function speed(position, start, end) {
|
|
462
|
+
if (position < start + EDGE) return -Math.round(MAX_SPEED * Math.min(1, (start + EDGE - position) / EDGE))
|
|
463
|
+
if (position > end - EDGE) return Math.round(MAX_SPEED * Math.min(1, (position - (end - EDGE)) / EDGE))
|
|
464
|
+
return 0
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// A drag ends with a click on whatever is under the pointer; swallow it so a drag
|
|
468
|
+
// off a linked card doesn't also follow the link.
|
|
469
|
+
function suppressNextClick() {
|
|
470
|
+
const swallow = (event) => {
|
|
471
|
+
event.stopPropagation()
|
|
472
|
+
event.preventDefault()
|
|
473
|
+
}
|
|
474
|
+
window.addEventListener("click", swallow, { capture: true, once: true })
|
|
475
|
+
setTimeout(() => window.removeEventListener("click", swallow, { capture: true }), 0)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function announce(item, key) {
|
|
479
|
+
const list = item.list
|
|
480
|
+
const template = list?.dataset[`sortable${key[0].toUpperCase()}${key.slice(1)}`]
|
|
481
|
+
if (!template) return
|
|
482
|
+
|
|
483
|
+
const items = list.items()
|
|
484
|
+
const text = template
|
|
485
|
+
.replaceAll("{item}", item.label)
|
|
486
|
+
.replaceAll("{list}", list.label)
|
|
487
|
+
.replaceAll("{position}", String(items.indexOf(item) + 1))
|
|
488
|
+
.replaceAll("{count}", String(items.length))
|
|
489
|
+
|
|
490
|
+
let region = document.querySelector("[data-unmagic-sortable-live]")
|
|
491
|
+
if (!region) {
|
|
492
|
+
region = document.createElement("div")
|
|
493
|
+
region.setAttribute("data-unmagic-sortable-live", "")
|
|
494
|
+
region.setAttribute("aria-live", "assertive")
|
|
495
|
+
region.className = "UnmagicVisuallyHidden"
|
|
496
|
+
document.body.append(region)
|
|
497
|
+
}
|
|
498
|
+
region.textContent = ""
|
|
499
|
+
requestAnimationFrame(() => {
|
|
500
|
+
region.textContent = text
|
|
501
|
+
})
|
|
502
|
+
}
|