[WV2] Pose picker popover
This commit is contained in:
parent
76496f8a6d
commit
6eace54c34
4 changed files with 158 additions and 105 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
}
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
/* Pose picker button */
|
||||||
border-radius: 12px;
|
.pose-picker-button {
|
||||||
padding: 1rem;
|
padding: 0.5rem 0.75rem;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
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 {
|
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -29,53 +29,60 @@
|
||||||
- else
|
- else
|
||||||
= outfit_viewer @outfit
|
= outfit_viewer @outfit
|
||||||
|
|
||||||
- if @pet_type
|
.preview-controls
|
||||||
%pose-picker
|
%species-color-picker
|
||||||
= form_with url: wardrobe_v2_path, method: :get, class: "pose-picker-form" do |f|
|
= form_with url: wardrobe_v2_path, method: :get do |f|
|
||||||
= outfit_state_params except: [:pose]
|
= outfit_state_params except: [:color, :species]
|
||||||
%table.pose-picker-table
|
= select_tag :color,
|
||||||
%thead
|
options_from_collection_for_select(@colors, "id", "human_name",
|
||||||
%tr
|
@selected_color&.id),
|
||||||
%th
|
"aria-label": "Pet color"
|
||||||
%th
|
= select_tag :species,
|
||||||
%span.emoji-icon{title: "Happy"} 😀
|
options_from_collection_for_select(@species, "id", "human_name",
|
||||||
%th
|
@selected_species&.id),
|
||||||
%span.emoji-icon{title: "Sad"} 😢
|
"aria-label": "Pet species"
|
||||||
%th
|
= submit_tag "Go", name: nil
|
||||||
%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"
|
|
||||||
|
|
||||||
%species-color-picker
|
- if @pet_type
|
||||||
= form_with url: wardrobe_v2_path, method: :get do |f|
|
- pose_info = pose_emoji_and_label(@selected_pose)
|
||||||
= outfit_state_params except: [:color, :species]
|
%button#pose-picker-button.pose-picker-button{type: "button", popovertarget: "pose-picker-popover"}
|
||||||
= select_tag :color,
|
%span.pose-emoji= pose_info[:emoji]
|
||||||
options_from_collection_for_select(@colors, "id", "human_name",
|
%span.pose-label= pose_info[:label]
|
||||||
@selected_color&.id),
|
%span.chevron ▾
|
||||||
"aria-label": "Pet color"
|
|
||||||
= select_tag :species,
|
%pose-picker-popover#pose-picker-popover{popover: "auto"}
|
||||||
options_from_collection_for_select(@species, "id", "human_name",
|
= form_with url: wardrobe_v2_path, method: :get, class: "pose-picker-form" do |f|
|
||||||
@selected_species&.id),
|
= outfit_state_params except: [:pose]
|
||||||
"aria-label": "Pet species"
|
%table.pose-picker-table
|
||||||
= submit_tag "Go", name: nil
|
%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
|
.outfit-controls-section
|
||||||
%h1 Customize your pet
|
%h1 Customize your pet
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue