diff --git a/app/assets/javascripts/pose-picker.js b/app/assets/javascripts/pose-picker.js index f8cd92a4..55d5a057 100644 --- a/app/assets/javascripts/pose-picker.js +++ b/app/assets/javascripts/pose-picker.js @@ -6,7 +6,7 @@ * - Shows a submit button as fallback (if JS is disabled or slow to load) * - Uses Custom Element internals API to communicate state to CSS */ -class PosePicker extends HTMLElement { +class PosePickerPopover extends HTMLElement { #internals; constructor() { @@ -28,4 +28,4 @@ class PosePicker extends HTMLElement { } } -customElements.define("pose-picker", PosePicker); +customElements.define("pose-picker-popover", PosePickerPopover); diff --git a/app/assets/stylesheets/wardrobe/show.css b/app/assets/stylesheets/wardrobe/show.css index c06182e7..98ad195c 100644 --- a/app/assets/stylesheets/wardrobe/show.css +++ b/app/assets/stylesheets/wardrobe/show.css @@ -41,32 +41,91 @@ body.wardrobe-v2 { font-size: 1.2rem; } - /* Pose picker floats over the preview above the species/color picker */ - pose-picker { + /* Preview controls container - groups species/color picker and pose picker */ + .preview-controls { position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + bottom: 0; + left: 0; + right: 0; display: flex; align-items: center; justify-content: center; + gap: 0.5rem; padding: 1.5rem; pointer-events: none; - /* Allow clicks through when hidden */ /* Start hidden, reveal on hover or focus */ opacity: 0; transition: opacity 0.2s; - .pose-picker-form { + > * { pointer-events: auto; - /* Re-enable clicks on the form itself */ - background: rgba(0, 0, 0, 0.85); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border-radius: 12px; - padding: 1rem; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + } + } + + /* Pose picker button */ + .pose-picker-button { + padding: 0.5rem 0.75rem; + font-size: 0.95rem; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 0.375rem; + background: rgba(0, 0, 0, 0.7); + color: white; + cursor: pointer; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + display: flex; + align-items: center; + gap: 0.5rem; + transition: all 0.2s; + + .pose-emoji { + font-size: 1.1rem; + } + + .pose-label { + font-weight: normal; + min-width: 3.5rem; + } + + .chevron { + font-size: 0.8rem; + opacity: 0.7; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.8); + border-color: rgba(255, 255, 255, 0.5); + } + + &:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.8); + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); + } + + &[popovertargetopen] { + border-color: rgba(255, 255, 255, 0.8); + background-color: rgba(0, 0, 0, 0.8); + } + } + + /* Pose picker popover */ + pose-picker-popover { + background: rgba(0, 0, 0, 0.9); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-radius: 12px; + padding: 1.25rem; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + margin: 0; + position-area: bottom; + inset: auto; + + .pose-picker-form { + display: flex; + flex-direction: column; } .pose-picker-table { @@ -110,7 +169,7 @@ body.wardrobe-v2 { height: 60px; border-radius: 8px; overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); background: rgba(255, 255, 255, 0.1); border: 2px solid transparent; transition: all 0.2s; @@ -141,7 +200,7 @@ body.wardrobe-v2 { /* Selected state */ input[type="radio"]:checked + .pose-thumbnail { border-color: #48BB78; - box-shadow: 0 0 0 3px rgba(72, 187, 120, 0.3); + box-shadow: 0 0 0 3px rgba(72, 187, 120, 0.4); transform: scale(1.05); } @@ -154,7 +213,7 @@ body.wardrobe-v2 { /* Focus state */ input[type="radio"]:focus + .pose-thumbnail { border-color: rgba(255, 255, 255, 0.8); - box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2); + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3); } /* Unavailable state */ @@ -175,22 +234,20 @@ body.wardrobe-v2 { font-size: 0.95rem; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 6px; - background: rgba(0, 0, 0, 0.7); + background: rgba(255, 255, 255, 0.1); color: white; cursor: pointer; width: 100%; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); &:hover { - background-color: rgba(0, 0, 0, 0.8); + background-color: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.5); } &:focus { outline: none; border-color: rgba(255, 255, 255, 0.8); - box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2); } } @@ -203,35 +260,17 @@ body.wardrobe-v2 { } } - /* Once auto-loading is ready, hide the submit button completely */ - &:state(auto-loading) { - .pose-submit-button { - display: none; - } + /* Once auto-submit is enabled, hide the submit button completely */ + &:state(auto-loading) .pose-submit-button { + display: none; } } - /* Species/color picker floats over the preview at the bottom */ + /* Species/color picker */ species-color-picker { - position: absolute; - bottom: 0; - left: 0; - right: 0; display: flex; align-items: center; - justify-content: center; - padding: 1.5rem; - pointer-events: none; - /* Allow clicks through when hidden */ - - /* Start hidden, reveal on hover or focus */ - opacity: 0; - transition: opacity 0.2s; - - form { - pointer-events: auto; - /* Re-enable clicks on the form itself */ - } + gap: 0.5rem; select { padding: 0.5rem 2rem 0.5rem 0.75rem; @@ -312,23 +351,16 @@ body.wardrobe-v2 { } } - /* Show pickers on hover (real hover only, not simulated touch hover) */ + /* Show controls on hover (real hover only, not simulated touch hover) */ @media (hover: hover) { - &:hover pose-picker { - opacity: 1; - } - - &:hover species-color-picker { + &:hover .preview-controls { opacity: 1; } } - /* Show pickers when they have focus */ - &:has(pose-picker:focus-within) pose-picker { - opacity: 1; - } - - &:has(species-color-picker:focus-within) species-color-picker { + /* Show controls when they have focus or when popover is open */ + &:has(.preview-controls:focus-within) .preview-controls, + &:has(.pose-picker-button[popovertargetopen]) .preview-controls { opacity: 1; } } diff --git a/app/helpers/wardrobe_helper.rb b/app/helpers/wardrobe_helper.rb index d7f72b99..0615a63b 100644 --- a/app/helpers/wardrobe_helper.rb +++ b/app/helpers/wardrobe_helper.rb @@ -24,6 +24,20 @@ module WardrobeHelper safe_join fields end + # Get the emoji and label for a pose, for display in the pose picker button + def pose_emoji_and_label(pose) + case pose + when "HAPPY_MASC", "HAPPY_FEM" + { emoji: "😀", label: "Happy" } + when "SAD_MASC", "SAD_FEM" + { emoji: "😢", label: "Sad" } + when "SICK_MASC", "SICK_FEM" + { emoji: "🤢", label: "Sick" } + else + { emoji: "😀", label: "Default" } + end + end + # Group outfit items by zone, applying smart multi-zone simplification. # Returns an array of hashes: {zone:, items:} # This matches the logic from wardrobe-2020's getZonesAndItems function. diff --git a/app/views/wardrobe/show.html.haml b/app/views/wardrobe/show.html.haml index 6a32fd8b..d7fd50fb 100644 --- a/app/views/wardrobe/show.html.haml +++ b/app/views/wardrobe/show.html.haml @@ -29,53 +29,60 @@ - else = outfit_viewer @outfit - - if @pet_type - %pose-picker - = form_with url: wardrobe_v2_path, method: :get, class: "pose-picker-form" do |f| - = outfit_state_params except: [:pose] - %table.pose-picker-table - %thead - %tr - %th - %th - %span.emoji-icon{title: "Happy"} 😀 - %th - %span.emoji-icon{title: "Sad"} 😢 - %th - %span.emoji-icon{title: "Sick"} 🤢 - %tbody - %tr - %th - %span.emoji-icon{title: "Masculine"} 💁‍♂️ - %td - = render "pose_option", pose: "HAPPY_MASC", pet_state: @available_poses["HAPPY_MASC"], selected: @selected_pose == "HAPPY_MASC" - %td - = render "pose_option", pose: "SAD_MASC", pet_state: @available_poses["SAD_MASC"], selected: @selected_pose == "SAD_MASC" - %td - = render "pose_option", pose: "SICK_MASC", pet_state: @available_poses["SICK_MASC"], selected: @selected_pose == "SICK_MASC" - %tr - %th - %span.emoji-icon{title: "Feminine"} 💁‍♀️ - %td - = render "pose_option", pose: "HAPPY_FEM", pet_state: @available_poses["HAPPY_FEM"], selected: @selected_pose == "HAPPY_FEM" - %td - = render "pose_option", pose: "SAD_FEM", pet_state: @available_poses["SAD_FEM"], selected: @selected_pose == "SAD_FEM" - %td - = render "pose_option", pose: "SICK_FEM", pet_state: @available_poses["SICK_FEM"], selected: @selected_pose == "SICK_FEM" - = submit_tag "Change pose", name: nil, class: "pose-submit-button" + .preview-controls + %species-color-picker + = form_with url: wardrobe_v2_path, method: :get do |f| + = outfit_state_params except: [:color, :species] + = select_tag :color, + options_from_collection_for_select(@colors, "id", "human_name", + @selected_color&.id), + "aria-label": "Pet color" + = select_tag :species, + options_from_collection_for_select(@species, "id", "human_name", + @selected_species&.id), + "aria-label": "Pet species" + = submit_tag "Go", name: nil - %species-color-picker - = form_with url: wardrobe_v2_path, method: :get do |f| - = outfit_state_params except: [:color, :species] - = select_tag :color, - options_from_collection_for_select(@colors, "id", "human_name", - @selected_color&.id), - "aria-label": "Pet color" - = select_tag :species, - options_from_collection_for_select(@species, "id", "human_name", - @selected_species&.id), - "aria-label": "Pet species" - = submit_tag "Go", name: nil + - if @pet_type + - pose_info = pose_emoji_and_label(@selected_pose) + %button#pose-picker-button.pose-picker-button{type: "button", popovertarget: "pose-picker-popover"} + %span.pose-emoji= pose_info[:emoji] + %span.pose-label= pose_info[:label] + %span.chevron ▾ + + %pose-picker-popover#pose-picker-popover{popover: "auto"} + = form_with url: wardrobe_v2_path, method: :get, class: "pose-picker-form" do |f| + = outfit_state_params except: [:pose] + %table.pose-picker-table + %thead + %tr + %th + %th + %span.emoji-icon{title: "Happy"} 😀 + %th + %span.emoji-icon{title: "Sad"} 😢 + %th + %span.emoji-icon{title: "Sick"} 🤢 + %tbody + %tr + %th + %span.emoji-icon{title: "Masculine"} 💁‍♂️ + %td + = render "pose_option", pose: "HAPPY_MASC", pet_state: @available_poses["HAPPY_MASC"], selected: @selected_pose == "HAPPY_MASC" + %td + = render "pose_option", pose: "SAD_MASC", pet_state: @available_poses["SAD_MASC"], selected: @selected_pose == "SAD_MASC" + %td + = render "pose_option", pose: "SICK_MASC", pet_state: @available_poses["SICK_MASC"], selected: @selected_pose == "SICK_MASC" + %tr + %th + %span.emoji-icon{title: "Feminine"} 💁‍♀️ + %td + = render "pose_option", pose: "HAPPY_FEM", pet_state: @available_poses["HAPPY_FEM"], selected: @selected_pose == "HAPPY_FEM" + %td + = render "pose_option", pose: "SAD_FEM", pet_state: @available_poses["SAD_FEM"], selected: @selected_pose == "SAD_FEM" + %td + = render "pose_option", pose: "SICK_FEM", pet_state: @available_poses["SICK_FEM"], selected: @selected_pose == "SICK_FEM" + = submit_tag "Change pose", name: nil, class: "pose-submit-button" .outfit-controls-section %h1 Customize your pet