[WV2] Pose picker popover

This commit is contained in:
Emi Matchu 2025-11-11 18:07:06 -08:00
parent 76496f8a6d
commit 6eace54c34
4 changed files with 158 additions and 105 deletions

View file

@ -6,7 +6,7 @@
* - Shows a submit button as fallback (if JS is disabled or slow to load) * - Shows a submit button as fallback (if JS is disabled or slow to load)
* - Uses Custom Element internals API to communicate state to CSS * - Uses Custom Element internals API to communicate state to CSS
*/ */
class PosePicker extends HTMLElement { class PosePickerPopover extends HTMLElement {
#internals; #internals;
constructor() { constructor() {
@ -28,4 +28,4 @@ class PosePicker extends HTMLElement {
} }
} }
customElements.define("pose-picker", PosePicker); customElements.define("pose-picker-popover", PosePickerPopover);

View file

@ -41,32 +41,91 @@ body.wardrobe-v2 {
font-size: 1.2rem; font-size: 1.2rem;
} }
/* Pose picker floats over the preview above the species/color picker */ /* Preview controls container - groups species/color picker and pose picker */
pose-picker { .preview-controls {
position: absolute; position: absolute;
top: 50%; bottom: 0;
left: 50%; left: 0;
transform: translate(-50%, -50%); right: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5rem;
padding: 1.5rem; padding: 1.5rem;
pointer-events: none; pointer-events: none;
/* Allow clicks through when hidden */
/* Start hidden, reveal on hover or focus */ /* Start hidden, reveal on hover or focus */
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
.pose-picker-form { > * {
pointer-events: auto; pointer-events: auto;
/* Re-enable clicks on the form itself */ }
background: rgba(0, 0, 0, 0.85); }
/* 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); backdrop-filter: blur(8px);
-webkit-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; border-radius: 12px;
padding: 1rem; padding: 1.25rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); 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 { .pose-picker-table {
@ -110,7 +169,7 @@ body.wardrobe-v2 {
height: 60px; height: 60px;
border-radius: 8px; border-radius: 8px;
overflow: hidden; 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); background: rgba(255, 255, 255, 0.1);
border: 2px solid transparent; border: 2px solid transparent;
transition: all 0.2s; transition: all 0.2s;
@ -141,7 +200,7 @@ body.wardrobe-v2 {
/* Selected state */ /* Selected state */
input[type="radio"]:checked + .pose-thumbnail { input[type="radio"]:checked + .pose-thumbnail {
border-color: #48BB78; 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); transform: scale(1.05);
} }
@ -154,7 +213,7 @@ body.wardrobe-v2 {
/* Focus state */ /* Focus state */
input[type="radio"]:focus + .pose-thumbnail { input[type="radio"]:focus + .pose-thumbnail {
border-color: rgba(255, 255, 255, 0.8); 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 */ /* Unavailable state */
@ -175,22 +234,20 @@ body.wardrobe-v2 {
font-size: 0.95rem; font-size: 0.95rem;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px; border-radius: 6px;
background: rgba(0, 0, 0, 0.7); background: rgba(255, 255, 255, 0.1);
color: white; color: white;
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
&:hover { &: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); border-color: rgba(255, 255, 255, 0.5);
} }
&:focus { &:focus {
outline: none; outline: none;
border-color: rgba(255, 255, 255, 0.8); 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 */ /* Once auto-submit is enabled, hide the submit button completely */
&:state(auto-loading) { &:state(auto-loading) .pose-submit-button {
.pose-submit-button {
display: none; display: none;
} }
} }
}
/* Species/color picker floats over the preview at the bottom */ /* Species/color picker */
species-color-picker { species-color-picker {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex; display: flex;
align-items: center; 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;
form {
pointer-events: auto;
/* Re-enable clicks on the form itself */
}
select { select {
padding: 0.5rem 2rem 0.5rem 0.75rem; 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) { @media (hover: hover) {
&:hover pose-picker { &:hover .preview-controls {
opacity: 1;
}
&:hover species-color-picker {
opacity: 1; opacity: 1;
} }
} }
/* Show pickers when they have focus */ /* Show controls when they have focus or when popover is open */
&:has(pose-picker:focus-within) pose-picker { &:has(.preview-controls:focus-within) .preview-controls,
opacity: 1; &:has(.pose-picker-button[popovertargetopen]) .preview-controls {
}
&:has(species-color-picker:focus-within) species-color-picker {
opacity: 1; opacity: 1;
} }
} }

View file

@ -24,6 +24,20 @@ module WardrobeHelper
safe_join fields safe_join fields
end 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. # Group outfit items by zone, applying smart multi-zone simplification.
# Returns an array of hashes: {zone:, items:} # Returns an array of hashes: {zone:, items:}
# This matches the logic from wardrobe-2020's getZonesAndItems function. # This matches the logic from wardrobe-2020's getZonesAndItems function.

View file

@ -29,8 +29,28 @@
- else - else
= outfit_viewer @outfit = outfit_viewer @outfit
.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
- if @pet_type - if @pet_type
%pose-picker - 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| = form_with url: wardrobe_v2_path, method: :get, class: "pose-picker-form" do |f|
= outfit_state_params except: [:pose] = outfit_state_params except: [:pose]
%table.pose-picker-table %table.pose-picker-table
@ -64,19 +84,6 @@
= render "pose_option", pose: "SICK_FEM", pet_state: @available_poses["SICK_FEM"], selected: @selected_pose == "SICK_FEM" = 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" = submit_tag "Change pose", name: nil, class: "pose-submit-button"
%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
.outfit-controls-section .outfit-controls-section
%h1 Customize your pet %h1 Customize your pet