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
data/lib/unmagic/components/browser/app/helpers/unmagic/components/browser/application_helper.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module Browser
|
|
6
|
+
module ApplicationHelper
|
|
7
|
+
EXAMPLES = "unmagic/components/browser/examples"
|
|
8
|
+
|
|
9
|
+
# The browser's own importmap: the names config/importmap.rb pins for a
|
|
10
|
+
# host, and Turbo's, resolved to the files the browser serves itself.
|
|
11
|
+
def browser_importmap_tags
|
|
12
|
+
javascripts = Browser.asset_roots.fetch("javascripts")
|
|
13
|
+
imports = {
|
|
14
|
+
"@hotwired/turbo-rails" => browser_asset_path("turbo", "turbo.min.js"),
|
|
15
|
+
"unmagic/components" => browser_asset_path("javascripts", "unmagic/components.js")
|
|
16
|
+
}
|
|
17
|
+
Dir[javascripts.join("unmagic/components/**/*.js")].sort.each do |file|
|
|
18
|
+
name = Pathname(file).relative_path_from(javascripts).to_s
|
|
19
|
+
imports[name.delete_suffix(".js")] = browser_asset_path("javascripts", name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
safe_join [
|
|
23
|
+
tag.script({ imports: imports }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce), # rubocop:disable Rails/OutputSafety -- JSON of our own paths
|
|
24
|
+
tag.script('import "unmagic/components"'.html_safe, type: "module", nonce: content_security_policy_nonce) # rubocop:disable Rails/OutputSafety -- a constant
|
|
25
|
+
], "\n"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def browser_stylesheet_tag
|
|
29
|
+
tag.link rel: "stylesheet", href: browser_asset_path("stylesheets", "browser.css")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A file the browser serves, stamped so a changed file is fetched afresh
|
|
33
|
+
# and an unchanged one comes from cache. The modification time covers
|
|
34
|
+
# editing in development; the version (turbo-rails' own, for Turbo)
|
|
35
|
+
# covers an upgrade, because gem packaging gives every file the same time.
|
|
36
|
+
def browser_asset_path(kind, name)
|
|
37
|
+
file = Browser.asset_roots.fetch(kind).join(name)
|
|
38
|
+
mtime = File.exist?(file) ? File.mtime(file).to_i : 0
|
|
39
|
+
version = (Gem.loaded_specs["turbo-rails"]&.version if kind == "turbo") || Components::VERSION
|
|
40
|
+
"#{root_path}assets/#{kind}/#{name}?v=#{version}-#{mtime}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def example_partial(component, key) = "#{EXAMPLES}/#{component.slug}/#{key}"
|
|
44
|
+
|
|
45
|
+
# What the Code tab shows: the example's partial, exactly as it's rendered.
|
|
46
|
+
def example_source(component, example)
|
|
47
|
+
Browser.root.join("app/views", EXAMPLES, component.slug, "_#{example.key}.html.erb").read.rstrip
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Unmagic
|
|
6
|
+
module Components
|
|
7
|
+
module Browser
|
|
8
|
+
# The board example's data, kept in the session. It takes drops the way
|
|
9
|
+
# unmagic-sortable's endpoint does — moved, prev, next and the column — with
|
|
10
|
+
# float ranks, which are plenty for one person's session.
|
|
11
|
+
class BoardStore
|
|
12
|
+
COLUMNS = [ %w[todo To\ do], %w[doing Doing], %w[review Review], %w[done Done] ].freeze
|
|
13
|
+
|
|
14
|
+
CARDS = [
|
|
15
|
+
[ "todo", "Write the job ad for the platform role", "Hiring", "Ada Lovelace" ],
|
|
16
|
+
[ "todo", "Book the room for Thursday's panel", "Ops", nil ],
|
|
17
|
+
[ "todo", "Ask Grace for interview feedback", "Hiring", "Grace Hopper" ],
|
|
18
|
+
[ "doing", "Shortlist the backend candidates", "Hiring", "Katherine Johnson" ],
|
|
19
|
+
[ "doing", "Draft the offer letter template", "Legal", "Alan Turing" ],
|
|
20
|
+
[ "review", "Check the salary bands against the market", "Finance", "Margaret Hamilton" ],
|
|
21
|
+
[ "done", "Post the role on the careers page", "Hiring", "Ada Lovelace" ],
|
|
22
|
+
[ "done", "Set up the take-home repository", "Eng", "Dennis Ritchie" ]
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(session)
|
|
26
|
+
@session = session
|
|
27
|
+
@session[:board] ||= seed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def columns = data["columns"].sort_by { |column| column["rank"] }
|
|
31
|
+
|
|
32
|
+
def cards_in(column)
|
|
33
|
+
data["cards"].select { |card| card["column"] == column["key"] }.sort_by { |card| card["rank"] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def move(key, prev:, following:, column: nil)
|
|
37
|
+
record = (data["columns"] + data["cards"]).find { |item| item["key"] == key }
|
|
38
|
+
return false unless record
|
|
39
|
+
|
|
40
|
+
record["rank"] = between(prev.presence&.to_f, following.presence&.to_f)
|
|
41
|
+
record["column"] = column if column.present? && record.key?("column")
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The session is a cookie, so the board stays small enough to fit in one.
|
|
46
|
+
LIMIT = 30
|
|
47
|
+
|
|
48
|
+
def add_card(title, column)
|
|
49
|
+
return if title.blank? || data["cards"].size >= LIMIT || data["columns"].none? { |c| c["key"] == column }
|
|
50
|
+
|
|
51
|
+
last = data["cards"].select { |card| card["column"] == column }.map { |card| card["rank"] }.max
|
|
52
|
+
data["cards"] << { "key" => "card-#{SecureRandom.hex(3)}", "title" => title.strip, "column" => column,
|
|
53
|
+
"tag" => nil, "owner" => nil, "rank" => between(last, nil) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_column(title)
|
|
57
|
+
return if title.blank? || data["columns"].size >= 8
|
|
58
|
+
|
|
59
|
+
last = data["columns"].map { |column| column["rank"] }.max
|
|
60
|
+
data["columns"] << { "key" => "column-#{SecureRandom.hex(3)}", "title" => title.strip, "rank" => between(last, nil) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reset! = @session[:board] = seed
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def data = @session[:board]
|
|
68
|
+
|
|
69
|
+
def between(lower, upper)
|
|
70
|
+
if lower && upper then (lower + upper) / 2
|
|
71
|
+
elsif upper then upper - 1
|
|
72
|
+
elsif lower then lower + 1
|
|
73
|
+
else 1.0
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def seed
|
|
78
|
+
{
|
|
79
|
+
"columns" => COLUMNS.each_with_index.map { |(key, title), index| { "key" => key, "title" => title, "rank" => index + 1.0 } },
|
|
80
|
+
"cards" => CARDS.each_with_index.map do |(column, title, tag, owner), index|
|
|
81
|
+
{ "key" => "card-#{index + 1}", "title" => title, "column" => column, "tag" => tag, "owner" => owner, "rank" => index + 1.0 }
|
|
82
|
+
end
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module Browser
|
|
6
|
+
# The components the browser shows, and the examples on each one's page.
|
|
7
|
+
#
|
|
8
|
+
# Each component describes itself in browser/catalog/<slug>.rb. Its examples
|
|
9
|
+
# are partials in app/views/unmagic/components/browser/examples/<slug>/,
|
|
10
|
+
# rendered live and shown as source, and _thumbnail is its card on the
|
|
11
|
+
# overview. In development the files are loaded again on every request, so a
|
|
12
|
+
# new example shows up without a restart.
|
|
13
|
+
module Catalog
|
|
14
|
+
Component = Data.define(:slug, :name, :description, :helper, :import, :group, :examples)
|
|
15
|
+
Example = Data.define(:key, :title, :description, :layout)
|
|
16
|
+
|
|
17
|
+
# :center sits natural-width content in the middle of the frame; :full lets a
|
|
18
|
+
# table, form or grid use the frame's whole width.
|
|
19
|
+
LAYOUTS = %i[center full].freeze
|
|
20
|
+
|
|
21
|
+
# The sidebar and the overview list ungrouped components first, under
|
|
22
|
+
# "Components", then each group under its own heading in this order.
|
|
23
|
+
GROUPS = [ nil, "AI chat" ].freeze
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def component(slug, name:, description:, examples:, helper: nil, import: nil, group: nil)
|
|
27
|
+
raise ArgumentError, "unknown group #{group.inspect} for #{slug}" unless GROUPS.include?(group)
|
|
28
|
+
|
|
29
|
+
registry[slug.to_s] = Component.new(
|
|
30
|
+
slug: slug.to_s, name: name, description: description, helper: helper, import: import, group: group,
|
|
31
|
+
examples: examples.map { |example| build_example(slug, **example) }
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def all = registry.values.sort_by { |component| [ GROUPS.index(component.group), component.name ] }
|
|
36
|
+
|
|
37
|
+
# [heading, components] pairs, in sidebar order.
|
|
38
|
+
def grouped = all.group_by(&:group).map { |group, components| [ group || "Components", components ] }
|
|
39
|
+
|
|
40
|
+
def find(slug) = registry[slug.to_s]
|
|
41
|
+
|
|
42
|
+
def reload!
|
|
43
|
+
@registry = {}
|
|
44
|
+
Dir[Browser.root.join("catalog/*.rb")].each { |file| load file }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def registry
|
|
50
|
+
reload! unless @registry
|
|
51
|
+
@registry
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_example(slug, key:, title:, description: nil, layout: :center)
|
|
55
|
+
unless LAYOUTS.include?(layout)
|
|
56
|
+
raise ArgumentError, "unknown layout #{layout.inspect} for #{slug}/#{key} (expected one of #{LAYOUTS.inspect})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Example.new(key: key.to_s, title: title, description: description, layout: layout)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module Unmagic
|
|
6
|
+
module Components
|
|
7
|
+
module Browser
|
|
8
|
+
# What the dialog preview edits: one validated field, kept in the session so a
|
|
9
|
+
# save visibly changes the page the modal was opened from.
|
|
10
|
+
class Profile
|
|
11
|
+
include ActiveModel::Model
|
|
12
|
+
include ActiveModel::Attributes
|
|
13
|
+
|
|
14
|
+
attribute :name, :string
|
|
15
|
+
attribute :role, :string
|
|
16
|
+
|
|
17
|
+
validates :name, presence: true
|
|
18
|
+
|
|
19
|
+
def self.model_name = ActiveModel::Name.new(self, nil, "Profile")
|
|
20
|
+
|
|
21
|
+
def persisted? = true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module Browser
|
|
6
|
+
# A canned assistant reply for the streaming examples, and the growing renders a
|
|
7
|
+
# server would broadcast while writing it. A host renders its own Markdown; the
|
|
8
|
+
# preview has none, so the reply is kept as blocks of HTML and a snapshot is the
|
|
9
|
+
# first n words of it, cut the way a half-written reply would be.
|
|
10
|
+
module Reply
|
|
11
|
+
BLOCKS = [
|
|
12
|
+
[ :p, "Three candidates haven't replied since the invitations went out on Monday." ],
|
|
13
|
+
[ :ul, [ "Ana Silva opened hers twice but hasn't answered.", "Grace Okafor hasn't opened hers.",
|
|
14
|
+
"Tom Reyes's bounced, so the address is probably wrong." ] ],
|
|
15
|
+
[ :p, "I'd send Ana a short nudge today, check Tom's address against his CV, and leave Grace until Thursday." ],
|
|
16
|
+
[ :pre, "invitations.unanswered.where(sent_at: ..3.days.ago)" ],
|
|
17
|
+
[ :p, "Want me to draft the nudge for Ana?" ]
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
include ERB::Util
|
|
22
|
+
|
|
23
|
+
def words = BLOCKS.sum { |_, content| Array(content).sum { |text| text.split.size } }
|
|
24
|
+
|
|
25
|
+
def html(limit = words)
|
|
26
|
+
remaining = limit
|
|
27
|
+
BLOCKS.filter_map do |tag, content|
|
|
28
|
+
next if remaining <= 0
|
|
29
|
+
|
|
30
|
+
case tag
|
|
31
|
+
when :ul
|
|
32
|
+
items = content.filter_map do |text|
|
|
33
|
+
next if remaining <= 0
|
|
34
|
+
|
|
35
|
+
"<li>#{take(text, remaining).tap { remaining -= text.split.size }}</li>"
|
|
36
|
+
end
|
|
37
|
+
"<ul>#{items.join}</ul>"
|
|
38
|
+
when :pre
|
|
39
|
+
text = take(content, remaining)
|
|
40
|
+
remaining -= content.split.size
|
|
41
|
+
"<pre><code>#{text}</code></pre>"
|
|
42
|
+
else
|
|
43
|
+
text = take(content, remaining)
|
|
44
|
+
remaining -= content.split.size
|
|
45
|
+
"<#{tag}>#{text}</#{tag}>"
|
|
46
|
+
end
|
|
47
|
+
end.join.html_safe # rubocop:disable Rails/OutputSafety -- the preview's own constant copy, escaped word by word
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Uneven bursts, as a model produces them.
|
|
51
|
+
def snapshots
|
|
52
|
+
sizes = [ 3, 1, 6, 2, 9, 4, 1, 7, 12, 3, 5, 2, 8 ]
|
|
53
|
+
limit = 0
|
|
54
|
+
result = []
|
|
55
|
+
sizes.cycle.each do |size|
|
|
56
|
+
limit = [ limit + size, words ].min
|
|
57
|
+
result << html(limit)
|
|
58
|
+
break if limit == words
|
|
59
|
+
end
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def take(text, count)
|
|
66
|
+
html_escape(text.split.first([ count, 0 ].max).join(" "))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module Unmagic
|
|
6
|
+
module Components
|
|
7
|
+
module Browser
|
|
8
|
+
# A row for the preview tables. Not an Active Record — the preview has no
|
|
9
|
+
# database — but enough of a model for dom_id.
|
|
10
|
+
class Thing
|
|
11
|
+
include ActiveModel::Model
|
|
12
|
+
include ActiveModel::Conversion
|
|
13
|
+
extend ActiveModel::Naming
|
|
14
|
+
|
|
15
|
+
attr_accessor :id, :name, :role, :score, :note
|
|
16
|
+
|
|
17
|
+
def to_key = [ id ]
|
|
18
|
+
def persisted? = id.present?
|
|
19
|
+
|
|
20
|
+
def self.all
|
|
21
|
+
[
|
|
22
|
+
new(id: 1, name: "Ada Lovelace", role: "Engineer", score: 120, note: "Replied: happy to chat next week."),
|
|
23
|
+
new(id: 2, name: "Grace Hopper", role: "Engineer", score: 98),
|
|
24
|
+
new(id: 3, name: "Katherine Johnson", role: "Analyst", score: 87),
|
|
25
|
+
new(id: 4, name: "Radia Perlman", role: "Architect", score: 64)
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="<%= dark_theme? ? "dark" : "light" %>" class="dark:[color-scheme:dark]">
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= [ content_for(:title), "unmagic-components" ].compact.join(" · ") %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= turbo_refresh_method_tag :morph %>
|
|
9
|
+
<%= turbo_refresh_scroll_tag :preserve %>
|
|
10
|
+
<%= browser_stylesheet_tag %>
|
|
11
|
+
<%= browser_importmap_tags %>
|
|
12
|
+
</head>
|
|
13
|
+
<body class="min-h-screen bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
|
|
14
|
+
<aside class="fixed inset-y-0 left-0 z-30 hidden w-64 flex-col border-r border-neutral-200 bg-neutral-50/70 lg:flex dark:border-neutral-800 dark:bg-neutral-900/40">
|
|
15
|
+
<div class="flex h-14 shrink-0 items-center px-5">
|
|
16
|
+
<%= link_to root_path, class: "flex items-center gap-2.5 text-sm font-semibold" do %>
|
|
17
|
+
<span class="grid size-6 place-items-center rounded-md bg-neutral-900 text-[0.625rem] font-bold text-white dark:bg-white dark:text-neutral-900">um</span>
|
|
18
|
+
unmagic-components
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<nav class="flex-1 overflow-y-auto px-3 pb-10" aria-label="Components">
|
|
22
|
+
<%= render "nav" %>
|
|
23
|
+
</nav>
|
|
24
|
+
</aside>
|
|
25
|
+
|
|
26
|
+
<div class="lg:pl-64">
|
|
27
|
+
<header class="sticky top-0 z-20 flex h-14 items-center gap-3 border-b border-neutral-200 bg-white/85 px-4 backdrop-blur sm:px-8 dark:border-neutral-800 dark:bg-neutral-950/85">
|
|
28
|
+
<details class="relative lg:hidden">
|
|
29
|
+
<summary class="<%= button_classes(:icon) %> list-none [&::-webkit-details-marker]:hidden" aria-label="Menu">
|
|
30
|
+
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
|
31
|
+
</summary>
|
|
32
|
+
<nav class="absolute left-0 top-11 max-h-[75vh] w-64 overflow-y-auto rounded-xl border border-neutral-200 bg-white p-2 shadow-lg dark:border-neutral-800 dark:bg-neutral-900" aria-label="Components">
|
|
33
|
+
<%= render "nav" %>
|
|
34
|
+
</nav>
|
|
35
|
+
</details>
|
|
36
|
+
|
|
37
|
+
<nav class="flex min-w-0 items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400" aria-label="Breadcrumb">
|
|
38
|
+
<%= yield :breadcrumbs %>
|
|
39
|
+
</nav>
|
|
40
|
+
|
|
41
|
+
<div class="ml-auto flex items-center gap-1">
|
|
42
|
+
<% switch_to = dark_theme? ? "light" : "dark" %>
|
|
43
|
+
<%= link_to "?theme=#{switch_to}", class: button_classes(:icon), "aria-label": "Switch to #{switch_to} theme", title: "Switch to #{switch_to} theme" do %>
|
|
44
|
+
<% if dark_theme? %>
|
|
45
|
+
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2m-7.07-2.93 1.41-1.41m11.32-11.32 1.41-1.41M2 12h2m16 0h2M4.93 4.93l1.41 1.41m11.32 11.32 1.41 1.41"/></svg>
|
|
46
|
+
<% else %>
|
|
47
|
+
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<%= link_to "https://github.com/unreasonable-magic/unmagic-components", class: button_classes(:icon), "aria-label": "GitHub", title: "GitHub" do %>
|
|
51
|
+
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-5-2"/></svg>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
55
|
+
|
|
56
|
+
<main class="mx-auto max-w-6xl px-4 py-10 sm:px-8 lg:py-14">
|
|
57
|
+
<%= yield %>
|
|
58
|
+
</main>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<%= modal_frame %>
|
|
62
|
+
<%= confirm_dialog_template %>
|
|
63
|
+
<%= flash_toasts %>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_code.html.erb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%# A block of source with a copy button. id: names the <pre> the button copies. %>
|
|
2
|
+
<div class="relative overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900">
|
|
3
|
+
<pre id="<%= id %>" class="m-0 overflow-x-auto p-4 pr-14 font-mono text-[0.8125rem] leading-6 text-neutral-800 dark:text-neutral-200"><code><%= source %></code></pre>
|
|
4
|
+
<div class="absolute right-2 top-2">
|
|
5
|
+
<%= copy_button from: id %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_crumb.html.erb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%# One breadcrumb: a link, or the current page when there's no path. %>
|
|
2
|
+
<% if local_assigns[:path] %>
|
|
3
|
+
<%= link_to label, path, class: "shrink-0 hover:text-neutral-900 dark:hover:text-neutral-100" %>
|
|
4
|
+
<svg class="size-3.5 shrink-0 text-neutral-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
|
|
5
|
+
<% else %>
|
|
6
|
+
<span class="truncate font-medium text-neutral-900 dark:text-neutral-100" aria-current="page"><%= label %></span>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%# One example: its heading, then a Preview/Code switch between the partial
|
|
2
|
+
rendered live and that same partial's source. The switch is the gem's own
|
|
3
|
+
tabs, placed beside the heading by browser/tailwind/browser.css.
|
|
4
|
+
|
|
5
|
+
The section is named by component and example together, so an example key
|
|
6
|
+
can't take an id the layout already uses: the dialog's "modal" example would
|
|
7
|
+
otherwise shadow the shared modal's #modal frame. %>
|
|
8
|
+
<% id = "#{component.slug}_#{example.key}" %>
|
|
9
|
+
<% center = example.layout == :center %>
|
|
10
|
+
|
|
11
|
+
<section id="<%= id %>" class="PreviewExample scroll-mt-20">
|
|
12
|
+
<div class="mb-3 min-h-9 pr-40">
|
|
13
|
+
<h2 class="text-base font-semibold">
|
|
14
|
+
<a href="#<%= id %>" class="hover:underline"><%= example.title %></a>
|
|
15
|
+
</h2>
|
|
16
|
+
<% if example.description %>
|
|
17
|
+
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400"><%= example.description %></p>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<%= tabs id: "#{id}_view", class: "PreviewExample__tabs" do |tabs| %>
|
|
22
|
+
<% tabs.tab "Preview" %>
|
|
23
|
+
<% tabs.tab "Code" %>
|
|
24
|
+
|
|
25
|
+
<% tabs.panel do %>
|
|
26
|
+
<div class="rounded-xl border border-neutral-200 bg-white bg-[radial-gradient(var(--color-neutral-200)_1px,transparent_1px)] [background-size:16px_16px] p-6 sm:p-10 dark:border-neutral-800 dark:bg-neutral-950 dark:bg-[radial-gradient(var(--color-neutral-800)_1px,transparent_1px)] <%= "flex min-h-52 items-center justify-center" if center %>">
|
|
27
|
+
<div class="<%= center ? "max-w-full" : "w-full" %>">
|
|
28
|
+
<%= render partial: example_partial(component, example.key) %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
33
|
+
<% tabs.panel do %>
|
|
34
|
+
<%= render "code", id: "#{id}_source", source: example_source(component, example) %>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
37
|
+
</section>
|
data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_nav.html.erb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# The sidebar's links, shared by the fixed sidebar and the small-screen menu. %>
|
|
2
|
+
<% item = "block rounded-md px-2.5 py-1.5 text-sm text-neutral-600 hover:bg-neutral-200/60 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100" %>
|
|
3
|
+
<% current = "block rounded-md px-2.5 py-1.5 text-sm font-medium bg-neutral-200/70 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100" %>
|
|
4
|
+
<% heading = "px-2.5 pb-1.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400" %>
|
|
5
|
+
|
|
6
|
+
<p class="<%= heading %> pt-4">Getting started</p>
|
|
7
|
+
<% [ [ "Overview", root_path ], [ "Installation", installation_path ], [ "Theming", theming_path ] ].each do |label, path| %>
|
|
8
|
+
<%= link_to label, path, class: current_page?(path) ? current : item, "aria-current": ("page" if current_page?(path)) %>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% @component_groups.each do |group, components| %>
|
|
12
|
+
<p class="<%= heading %> pt-6"><%= group %></p>
|
|
13
|
+
<% components.each do |component| %>
|
|
14
|
+
<% path = component_path(component.slug) %>
|
|
15
|
+
<%= link_to component.name, path, class: current_page?(path) ? current : item, "aria-current": ("page" if current_page?(path)) %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
data/lib/unmagic/components/browser/app/views/unmagic/components/browser/demos/edit_profile.html.erb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<%= dialog title: "Edit profile",
|
|
2
|
+
form: { model: @profile, url: profile_dialog_path, method: :patch, builder: Unmagic::Components::FormBuilder } do |dialog, form| %>
|
|
3
|
+
<%= form.field :name, "Name", required: true, autofocus: true %>
|
|
4
|
+
<%= form.field :role, "Role", hint: "Shown beside the name." %>
|
|
5
|
+
<% dialog.footer do %>
|
|
6
|
+
<%= tag.button "Cancel", type: "button", class: button_classes, data: { unmagic_dialog_close: "" } %>
|
|
7
|
+
<%= form.submit "Save" %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_16rem]">
|
|
2
|
+
<div class="flex min-w-0 flex-col gap-4">
|
|
3
|
+
<div id="showcase_scroll" class="h-[38rem] overflow-y-auto pr-2">
|
|
4
|
+
<%= ai_chat id: "showcase_entries", scroller: "#showcase_scroll" do %>
|
|
5
|
+
<%= ai_chat_message "Who hasn't replied to the Platform Engineer invitations yet?", role: :user, id: "showcase_q1" %>
|
|
6
|
+
|
|
7
|
+
<%= ai_chat_message role: :assistant, id: "showcase_a1" do |turn| %>
|
|
8
|
+
<% turn.reasoning duration: 4 do %>
|
|
9
|
+
<p>The invitations were sent on Monday. I need the job first, then the invitations without a reply, then
|
|
10
|
+
anything that bounced.</p>
|
|
11
|
+
<% end %>
|
|
12
|
+
<p>I'll check the invitations sent for that job this week.</p>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%= ai_chat_tool_call name: "find_job", state: :done, icon: :folder, id: "showcase_t1" do |tool| %>
|
|
16
|
+
<% tool.summary "Platform Engineer" %>
|
|
17
|
+
<% tool.timing duration: 0.21 %>
|
|
18
|
+
<% tool.asked({ query: "Platform Engineer", status: "open" }) %>
|
|
19
|
+
<% tool.answered({ id: "job_8f2", title: "Platform Engineer", company: "Acme", invitations: 12 }) %>
|
|
20
|
+
<% end %>
|
|
21
|
+
<div data-ai-chat-timeline="gap" hidden></div>
|
|
22
|
+
<%= ai_chat_tool_call name: "search_invitations", state: :done, icon: :messages_square, id: "showcase_t2", open: true do |tool| %>
|
|
23
|
+
<% tool.summary "unanswered · last 7 days" %>
|
|
24
|
+
<% tool.timing duration: 1.84 %>
|
|
25
|
+
<% tool.failures 1 %>
|
|
26
|
+
<% tool.asked({ job: "job_8f2", replied: false, since: "7 days ago" }) %>
|
|
27
|
+
<% tool.answered({ invitations: [ "Ana Silva", "Grace Okafor", "Tom Reyes" ], errors: [ { id: "inv_31", error: "timed out" } ] }) %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<div data-ai-chat-timeline="gap" hidden></div>
|
|
30
|
+
<%= ai_chat_tool_call name: "check_email", state: :running, id: "showcase_t3" do |tool| %>
|
|
31
|
+
<% tool.summary "tom.reyes@example.com" %>
|
|
32
|
+
<% tool.timing started_at: @now - 12 %>
|
|
33
|
+
<% tool.progress "Waiting on the mail server" %>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<%= ai_chat_message role: :assistant, id: "showcase_a2" do |turn| %>
|
|
37
|
+
<% turn.actions do %>
|
|
38
|
+
<%= ai_chat_action_bar for: "showcase_a2" do |bar| %>
|
|
39
|
+
<% bar.copy "Three candidates haven't replied." %>
|
|
40
|
+
<% bar.action "Try again", "#", icon: :rotate_cw, method: :post %>
|
|
41
|
+
<% bar.control { ai_chat_branch_picker index: 2, count: 2, previous: "#" } %>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% end %>
|
|
44
|
+
<p>Three candidates haven't replied since Monday:</p>
|
|
45
|
+
<ul>
|
|
46
|
+
<li><strong>Ana Silva</strong> opened hers twice but hasn't answered.</li>
|
|
47
|
+
<li><strong>Grace Okafor</strong> hasn't opened hers.</li>
|
|
48
|
+
<li><strong>Tom Reyes</strong>'s bounced — I'm checking the address now.</li>
|
|
49
|
+
</ul>
|
|
50
|
+
<p>Ana did say something relevant last week:</p>
|
|
51
|
+
<%= ai_chat_citation do |cite| %>
|
|
52
|
+
<% cite.avatar %>
|
|
53
|
+
<% cite.who "Ana Silva" %>
|
|
54
|
+
<% cite.when @now - 6.days, url: "#" %>
|
|
55
|
+
<% cite.quote "I'm keen, but I'm travelling until the 18th.\nCould we talk after that?" %>
|
|
56
|
+
<% end %>
|
|
57
|
+
<%= ai_chat_proposal id: "showcase_fact" do |offer| %>
|
|
58
|
+
<% offer.claim "Ana is travelling until the 18th" %>
|
|
59
|
+
<% offer.meta "About Ana Silva · from her message on the 10th" %>
|
|
60
|
+
<% offer.accept { button_to "Remember", "#", class: button_classes(:primary, size: :small) } %>
|
|
61
|
+
<% offer.reject { button_to "Dismiss", "#", class: button_classes(size: :small) } %>
|
|
62
|
+
<% end %>
|
|
63
|
+
<p>I'd nudge Grace today and leave Ana until after she's back.</p>
|
|
64
|
+
<% end %>
|
|
65
|
+
|
|
66
|
+
<%= ai_chat_permission tool: "send_email", state: :waiting, url: "#", id: "showcase_p1" do |ask| %>
|
|
67
|
+
<% ask.argument "to", "grace.okafor@example.com" %>
|
|
68
|
+
<% ask.summary { tag.p("Send Grace a short nudge about the Platform Engineer role.") } %>
|
|
69
|
+
<% ask.reason { tag.p("She hasn't opened the invitation, and the role closes on Friday.") } %>
|
|
70
|
+
<% ask.allow confirm: "Send this email to Grace?" %>
|
|
71
|
+
<% ask.refuse %>
|
|
72
|
+
<% end %>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<%= form_with url: "#", id: "showcase_composer" do |form| %>
|
|
77
|
+
<%= ai_chat_composer form: form, field: :content, state: :waiting, placeholder: "Reply, or / for a command…", stop_form: "showcase_stop" do |composer| %>
|
|
78
|
+
<% composer.attach do %>
|
|
79
|
+
<button type="button" class="<%= button_classes(:icon) %>" aria-label="Attach a file" title="Attach a file">
|
|
80
|
+
<%= Unmagic::Components::Icons.svg(self, :paperclip) %>
|
|
81
|
+
</button>
|
|
82
|
+
<% end %>
|
|
83
|
+
<% end %>
|
|
84
|
+
<% end %>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="h-fit rounded-xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900">
|
|
88
|
+
<%= ai_chat_plan id: "showcase_plan" do |plan| %>
|
|
89
|
+
<% plan.step "Find the job", state: :completed %>
|
|
90
|
+
<% plan.step "List who hasn't replied", state: :completed %>
|
|
91
|
+
<% plan.step "Check the bounced address", state: :in_progress %>
|
|
92
|
+
<% plan.step "Nudge Grace", state: :waiting %>
|
|
93
|
+
<% plan.step "Remind me about Ana on the 19th" %>
|
|
94
|
+
<% end %>
|
|
95
|
+
<%= ai_chat_workspace id: "showcase_workspace" do |workspace| %>
|
|
96
|
+
<% workspace.file "unanswered.csv", size: 1_240 %>
|
|
97
|
+
<% workspace.file "drafts/nudge-grace.md", size: 812, icon: :pencil %>
|
|
98
|
+
<% workspace.file "captures/tom-bounce.eml", size: 18_400 %>
|
|
99
|
+
<% end %>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|