Compare commits

..

5 commits

Author SHA1 Message Date
77ff55353c Copy selected/focus face picker styles from Impress 2020 2024-09-03 17:54:56 -07:00
a88fc14bd7 Use hi-res pet images in face picker for new item previews 2024-09-03 17:34:31 -07:00
9f44fd47e4 Add "No data yet" to species face tooltip when needed, in item previews 2024-09-03 17:27:43 -07:00
4c44f8d6a4 Fix species face picker going inert again after Turbo frame load
Here, I remember the trick I learned when building the outfit viewer:
web components are great for making sure stuff stays initialized well
in a Turbo environment!

The problem was, after submitting the form and getting a new preview
loaded via Turbo, the part where we remove `inert` would get undone.
Additionally, this script only loads *once* per session, so if you
Turbo-nav to a different item then that part of the page never ran.

Instead, we use web components to remove the attributes on mount, then
again if they're ever reapplied by Idiomorph.
2024-09-03 17:07:53 -07:00
2b2bffd9da Disable pet faces that the item doesn't fit, in new item previews 2024-09-03 16:42:04 -07:00
5 changed files with 133 additions and 24 deletions

View file

@ -1,6 +1,6 @@
// When the species *face* picker changes, update and submit the main picker form.
document.addEventListener("click", (e) => {
if (!e.target.matches(".species-face-picker input[type=radio]")) return;
// When the species face picker changes, update and submit the main picker form.
document.addEventListener("change", (e) => {
if (!e.target.matches("species-face-picker")) return;
try {
const mainPicker = document.querySelector("#item-preview .species-color-picker");
@ -14,8 +14,43 @@ document.addEventListener("click", (e) => {
}
});
// Now that the face picker is ready to go, mark it as usable.
for (const options of document.querySelectorAll(".species-face-picker-options")) {
options.removeAttribute("inert");
options.removeAttribute("aria-hidden");
class SpeciesFacePicker extends HTMLElement {
connectedCallback() {
this.addEventListener("click", this.#handleClick);
}
get value() {
return this.querySelector("input[type=radio]:checked")?.value;
}
#handleClick(e) {
if (e.target.matches("input[type=radio]")) {
this.dispatchEvent(new Event("change", {bubbles: true}));
}
}
}
class SpeciesFacePickerOptions extends HTMLElement {
static observedAttributes = ["inert", "aria-hidden"];
connectedCallback() {
// Once this component is loaded, we stop being inert and aria-hidden. We're ready!
this.#activate();
}
attributeChangedCallback() {
// If a Turbo Frame tries to morph us into being inert again, activate again!
// (It's important that the server's HTML always return `inert`, for progressive
// enhancement; and it's important to morph this element, so radio focus state
// is preserved. To thread that needle, we have to monitor and remove!)
this.#activate();
}
#activate() {
this.removeAttribute("inert");
this.removeAttribute("aria-hidden");
}
}
customElements.define("species-face-picker", SpeciesFacePicker);
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);

View file

@ -161,9 +161,46 @@ body.items-show
border-color: $error-border-color
color: $error-color
.species-face-picker
species-face-picker
display: block
position: relative
species-face-picker-options
display: flex
flex-wrap: wrap
img
width: 50px
height: 50px
transition: all 0.2s
// Calm down the default color, just a smidge! There's a lot of color
// on this page already, y'know?
opacity: .9
filter: saturate(90%)
label
display: flex
overflow: hidden
transition: all 0.2s
position: relative
line-height: 1
// NOTE: The box-shadows here were copy-pasted from Impress 2020, which uses
// Chakra UI's styling system to generate them! (The colors are from their
// color palette, too.)
&:has(input:checked)
border-radius: 6px
z-index: 1
background: #9AE6B4
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),0 10px 10px -5px rgba(0, 0, 0, 0.04), #2F855A 0 0 2px 2px
transform: scale(1.1)
&:has(input:focus)
background: #BEE3F8
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),0 10px 10px -5px rgba(0, 0, 0, 0.04), #4299e1 0 0 0 3px
transform: scale(1.2)
input[type=radio]
position: absolute
left: -10000px
@ -172,21 +209,30 @@ body.items-show
height: 1px
overflow: hidden
&:not(:checked) + img
opacity: .5
&:checked + img
opacity: 1
filter: saturate(110%)
&:disabled + img
opacity: .6
filter: saturate(0%)
label:has(input[type=radio]:disabled)
cursor: not-allowed
noscript
position: absolute
inset: 0
background: rgba(white, .75)
z-index: 1
cursor: auto
display: flex
align-items: center
justify-content: center
text-align: center
&:has(.species-face-picker-options[inert])
&:has(species-face-picker-options[inert])
cursor: wait
.item-zones-info

View file

@ -33,7 +33,9 @@ module ItemsHelper
def standard_species_search_links
all_species = Species.alphabetical.map(&:id)
PetType.random_basic_per_species(all_species).map do |pet_type|
image = pet_type_image(pet_type, :happy, :zoom)
human_name = pet_type.species.human_name
image = pet_type_image pet_type, :happy, :zoom,
alt: human_name, title: human_name
query = "species:#{pet_type.species.name}"
link_to(image, items_path(:q => query))
end.join.html_safe
@ -222,6 +224,18 @@ module ItemsHelper
cookies["DTIOutfitViewerIsPlaying"] == "true"
end
def item_fits?(item, pet_type)
item.appearances.any? { |a| a.fits? pet_type }
end
def species_face_tooltip(pet_type, item)
if item_fits?(item, pet_type)
"#{pet_type.species.human_name}"
else
"#{pet_type.species.human_name}: No data yet"
end
end
def item_zone_partial_fit?(appearances_in_zone, all_appearances)
appearances_in_zone.size < all_appearances.size
end
@ -230,12 +244,13 @@ module ItemsHelper
appearances_in_zone.map(&:species).uniq.map(&:human_name).sort.join(", ")
end
private
def pet_type_image(pet_type, emotion, size)
def pet_type_image(pet_type, emotion, size, **options)
src = pet_type_image_url(pet_type, emotion:, size:)
human_name = pet_type.species.name.humanize
image_tag(src, :alt => human_name, :title => human_name)
srcset = if size == :face
[[pet_type_image_url(pet_type, emotion:, size: :face_2x), "2x"]]
end
image_tag(src, srcset:, **options)
end
def item_header_user_lists_form_state

View file

@ -502,7 +502,7 @@ class Item < ApplicationRecord
Appearance = Struct.new(:item, :body, :swf_assets) do
include ActiveModel::Serializers::JSON
delegate :present?, :empty?, to: :swf_assets
delegate :species, to: :body
delegate :species, :fits?, :fits_all?, to: :body
def attributes
{item:, body:, swf_assets:}
@ -522,6 +522,14 @@ class Item < ApplicationRecord
def attributes
{id:, species:}
end
def fits_all?
id == 0
end
def fits?(target)
fits_all? || target.body_id == id
end
end
def appearances

View file

@ -33,19 +33,24 @@
"id", "human_name", @selected_preview_pet_type.species_id)
= submit_tag "Go", name: nil
%form.species-face-picker
%species-face-picker
%noscript
This fancy species picker requires Javascript, but you can still use the
dropdowns instead!
.species-face-picker-options{
"inert": true, # waits for JS to remove
%species-face-picker-options{
inert: true, # waits for JS to remove
"aria-hidden": true, # waits for JS to remove
}
- @preview_pet_type_options.each do |pet_type|
%label
%label{
title: species_face_tooltip(pet_type, @item),
}
= radio_button_tag "species_face_id", pet_type.species_id,
checked: pet_type == @preview_outfit.pet_type
= pet_type_image pet_type, :happy, :face
checked: pet_type == @preview_outfit.pet_type,
disabled: !item_fits?(@item, pet_type)
= pet_type_image pet_type,
item_fits?(@item, pet_type) ? :happy : :sad,
:face
.item-zones-info
%section