Compare commits

..

No commits in common. "812e8226bbf19ee851db389605914e7ae825fccb" and "ab46d90d6acf2e1f90d8ddb4205017bfa12784b5" have entirely different histories.

236 changed files with 1122 additions and 1841 deletions

View file

@ -20,14 +20,11 @@ services:
depends_on:
- mysql
environment:
DB_USER: root
mysql:
image: mariadb:10.6
restart: unless-stopped
environment:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'true'
MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
volumes:
- mysql-data:/var/lib/mysql
networks:

View file

@ -18,6 +18,7 @@ gem 'sprockets', '~> 4.2'
gem 'haml', '~> 6.1', '>= 6.1.1'
gem 'sass-rails', '~> 6.0'
gem 'terser', '~> 1.1', '>= 1.1.17'
gem 'react-rails', '~> 2.7', '>= 2.7.1'
gem 'jsbundling-rails', '~> 1.3'
gem 'turbo-rails', '~> 2.0'

View file

@ -6,31 +6,29 @@ PATH
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.15)
railties
actioncable (8.1.1)
actionpack (= 8.1.1)
activesupport (= 8.1.1)
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.1.1)
actionpack (= 8.1.1)
activejob (= 8.1.1)
activerecord (= 8.1.1)
activestorage (= 8.1.1)
activesupport (= 8.1.1)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
actionmailer (8.1.1)
actionpack (= 8.1.1)
actionview (= 8.1.1)
activejob (= 8.1.1)
activesupport (= 8.1.1)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.1.1)
actionview (= 8.1.1)
activesupport (= 8.1.1)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@ -38,59 +36,58 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.1.1)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.1)
activerecord (= 8.1.1)
activestorage (= 8.1.1)
activesupport (= 8.1.1)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.1.1)
activesupport (= 8.1.1)
actionview (8.0.2)
activesupport (= 8.0.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.1.1)
activesupport (= 8.1.1)
activejob (8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.3.6)
activemodel (8.1.1)
activesupport (= 8.1.1)
activerecord (8.1.1)
activemodel (= 8.1.1)
activesupport (= 8.1.1)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
timeout (>= 0.4.0)
activestorage (8.1.1)
actionpack (= 8.1.1)
activejob (= 8.1.1)
activerecord (= 8.1.1)
activesupport (= 8.1.1)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
marcel (~> 1.0)
activesupport (8.1.1)
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
ast (2.4.3)
async (2.35.0)
async (2.27.0)
console (~> 1.29)
fiber-annotation
io-event (~> 1.11)
metrics (~> 0.12)
traces (~> 0.18)
async-container (0.27.7)
traces (~> 0.15)
async-container (0.24.0)
async (~> 2.22)
async-http (0.89.0)
async (>= 2.10.2)
@ -102,38 +99,41 @@ GEM
protocol-http1 (~> 0.30)
protocol-http2 (~> 0.22)
traces (~> 0.10)
async-http-cache (0.4.6)
async-http-cache (0.4.5)
async-http (~> 0.56)
async-pool (0.11.1)
async-pool (0.11.0)
async (>= 2.0)
async-service (0.16.0)
async-service (0.13.0)
async
async-container (~> 0.16)
string-format (~> 0.2)
attr_required (1.0.2)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
backport (1.2.0)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.5.0)
bigdecimal (4.0.1)
benchmark (0.4.1)
bigdecimal (3.2.2)
bindata (2.5.1)
bindex (0.8.1)
bootsnap (1.20.1)
bootsnap (1.18.6)
msgpack (~> 1.2)
builder (3.3.0)
childprocess (5.1.0)
logger (~> 1.5)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
console (1.34.2)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
console (1.32.0)
fiber-annotation
fiber-local (~> 1.1)
json
crack (1.0.1)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
date (3.5.1)
date (3.4.1)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
@ -154,7 +154,7 @@ GEM
e2mmap (0.1.0)
email_validator (2.2.4)
activemodel
erb (6.0.1)
erb (5.0.2)
erubi (1.13.1)
execjs (2.10.0)
falcon (0.48.6)
@ -170,47 +170,43 @@ GEM
protocol-http (~> 0.31)
protocol-rack (~> 0.7)
samovar (~> 2.3)
faraday (2.14.0)
faraday (2.13.4)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.4.0)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
net-http (~> 0.5)
faraday-net_http (3.4.1)
net-http (>= 0.5.0)
ffi (1.17.2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
fiber-storage
fiber-storage (1.0.1)
globalid (1.3.0)
globalid (1.2.1)
activesupport (>= 6.1)
haml (6.4.0)
haml (6.3.0)
temple (>= 0.8.2)
thor
tilt
hashdiff (1.2.1)
hashie (5.1.0)
logger
hashdiff (1.2.0)
hashie (5.0.0)
http_accept_language (2.1.1)
i18n (1.14.8)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.2)
io-endpoint (0.16.0)
io-event (1.14.2)
io-stream (0.11.1)
irb (1.16.0)
io-console (0.8.1)
io-endpoint (0.15.2)
io-event (1.12.1)
io-stream (0.10.0)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jaro_winkler (1.6.1)
jsbundling-rails (1.3.1)
railties (>= 6.0.0)
json (2.18.0)
json-jwt (1.17.0)
json (2.13.1)
json-jwt (1.16.7)
activesupport (>= 4.2)
aes_key_wrap
base64
@ -229,31 +225,28 @@ GEM
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lint_roller (1.1.0)
localhost (1.6.0)
localhost (1.5.0)
logger (1.7.0)
loofah (2.25.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
logger
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
mapping (1.1.3)
marcel (1.1.0)
marcel (1.0.4)
memory_profiler (1.1.0)
metrics (0.15.0)
metrics (0.12.2)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (6.0.1)
prism (~> 1.5)
minitest (5.25.5)
msgpack (1.8.0)
mysql2 (0.5.7)
bigdecimal
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.2)
mysql2 (0.5.6)
net-http (0.6.0)
uri
net-imap (0.5.9)
date
net-protocol
net-pop (0.1.2)
@ -262,19 +255,12 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.18.10)
nio4r (2.7.4)
nokogiri (1.18.9)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
omniauth (2.1.4)
omniauth (2.1.3)
hashie (>= 3.4.6)
logger
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.2)
@ -296,49 +282,49 @@ GEM
tzinfo
validate_url
webfinger (~> 2.0)
openssl (3.3.2)
openssl (3.3.0)
orm_adapter (0.5.0)
parallel (1.27.0)
parser (3.3.10.0)
parser (3.3.9.0)
ast (~> 2.4.1)
racc
pp (0.6.3)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
prism (1.7.0)
process-metrics (0.8.0)
prism (1.4.0)
process-metrics (0.5.1)
console (~> 1.8)
json (~> 2)
samovar (~> 2.1)
protocol-hpack (1.5.1)
protocol-http (0.56.1)
protocol-http1 (0.35.2)
protocol-http (0.51.0)
protocol-http1 (0.34.1)
protocol-http (~> 0.22)
protocol-http2 (0.23.0)
protocol-http2 (0.22.1)
protocol-hpack (~> 1.4)
protocol-http (~> 0.47)
protocol-rack (0.19.0)
protocol-rack (0.15.0)
io-stream (>= 0.10)
protocol-http (~> 0.43)
rack (>= 1.0)
psych (5.3.1)
psych (5.2.6)
date
stringio
public_suffix (7.0.0)
public_suffix (6.0.2)
racc (1.8.1)
rack (3.2.4)
rack-attack (6.8.0)
rack (3.1.16)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-mini-profiler (3.3.1)
rack (>= 1.2.0)
rack-oauth2 (2.3.0)
rack-oauth2 (2.2.1)
activesupport
attr_required
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (4.2.1)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
@ -347,22 +333,22 @@ GEM
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.3.1)
rackup (2.2.1)
rack (>= 3)
rails (8.1.1)
actioncable (= 8.1.1)
actionmailbox (= 8.1.1)
actionmailer (= 8.1.1)
actionpack (= 8.1.1)
actiontext (= 8.1.1)
actionview (= 8.1.1)
activejob (= 8.1.1)
activemodel (= 8.1.1)
activerecord (= 8.1.1)
activestorage (= 8.1.1)
activesupport (= 8.1.1)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 8.1.1)
railties (= 8.0.2)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
@ -370,41 +356,45 @@ GEM
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.1.0)
rails-i18n (8.0.1)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.1.1)
actionpack (= 8.1.1)
activesupport (= 8.1.1)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rake (13.3.0)
rbs (2.8.4)
rdiscount (2.2.7.3)
rdoc (7.0.3)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.11.3)
reline (0.6.3)
react-rails (2.7.1)
babel-transpiler (>= 0.7.0)
connection_pool
execjs
railties (>= 3.2)
tilt
regexp_parser (2.10.0)
reline (0.6.2)
io-console (~> 0.5)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.4.4)
rspec-core (3.13.6)
rexml (3.4.1)
rspec-core (3.13.5)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.7)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.1.1)
@ -415,8 +405,8 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.6)
rubocop (1.82.1)
rspec-support (3.13.4)
rubocop (1.79.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@ -424,14 +414,15 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.48.0, < 2.0)
rubocop-ast (>= 1.46.0, < 2.0)
ruby-progressbar (~> 1.7)
tsort (>= 0.2.0)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0)
rubocop-ast (1.46.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
ruby-progressbar (1.13.0)
samovar (2.4.1)
samovar (2.3.0)
console (~> 1.0)
mapping (~> 1.0)
sanitize (6.1.3)
@ -448,10 +439,10 @@ GEM
sprockets-rails
tilt
securerandom (0.4.1)
sentry-rails (5.28.1)
sentry-rails (5.26.0)
railties (>= 5.0)
sentry-ruby (~> 5.28.1)
sentry-ruby (5.28.1)
sentry-ruby (~> 5.26.0)
sentry-ruby (5.26.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
shell (0.8.1)
@ -473,9 +464,9 @@ GEM
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
solargraph-rails (1.2.4)
solargraph-rails (1.1.2)
activesupport
solargraph (>= 0.48.0, <= 0.57)
solargraph (>= 0.48.0, < 0.53.0)
sprockets (4.2.2)
concurrent-ruby (~> 1.0)
logger
@ -485,8 +476,7 @@ GEM
activesupport (>= 6.1)
sprockets (>= 3.0.0)
stackprof (0.2.27)
string-format (0.2.0)
stringio (3.2.0)
stringio (3.1.7)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
@ -499,18 +489,18 @@ GEM
thor (1.4.0)
thread-local (1.1.0)
tilt (2.6.1)
timeout (0.6.0)
traces (0.18.2)
timeout (0.4.3)
traces (0.15.2)
tsort (0.2.0)
turbo-rails (2.0.20)
turbo-rails (2.0.16)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
useragent (0.16.11)
validate_url (1.0.15)
activemodel (>= 3.0.0)
@ -526,7 +516,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.26.1)
webmock (3.25.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -535,14 +525,11 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
will_paginate (4.0.1)
yard (0.9.38)
zeitwerk (2.7.4)
yard (0.9.37)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
arm64-darwin
ruby
x86_64-linux
DEPENDENCIES
RocketAMF!
@ -570,6 +557,7 @@ DEPENDENCIES
rails (~> 8.0, >= 8.0.1)
rails-i18n (~> 8.0, >= 8.0.1)
rdiscount (~> 2.2, >= 2.2.7.1)
react-rails (~> 2.7, >= 2.7.1)
rspec-rails (~> 7.0)
sanitize (~> 6.0, >= 6.0.2)
sass-rails (~> 6.0)

View file

@ -1,31 +0,0 @@
/**
* PosePicker web component
*
* Progressive enhancement for pose picker forms:
* - Auto-submits the form when a pose is selected (if JS is enabled)
* - 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 PosePickerPopover extends HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
}
connectedCallback() {
// Listen for changes to auto-submit the form, then tell CSS about it!
this.addEventListener("change", this.#handleChange);
this.#internals.states.add("auto-loading");
}
#handleChange(e) {
// Only auto-submit if a radio button was changed
if (e.target.type === "radio") {
this.querySelector("form").requestSubmit();
}
}
}
customElements.define("pose-picker-popover", PosePickerPopover);

View file

@ -73,7 +73,6 @@ outfit-viewer
border-radius: 100%
border: 2px solid transparent
transition: all .25s
cursor: pointer
.playing-label, .paused-label
display: none

View file

@ -0,0 +1,545 @@
@import "../application/item-badges.css";
body.wardrobe-v2 {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
font-family: sans-serif;
}
.wardrobe-container {
display: flex;
height: 100vh;
background: #000;
@media (max-width: 800px) {
flex-direction: column;
}
}
.outfit-preview-section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #000;
position: relative;
min-height: 400px;
container-type: size;
/* The outfit viewer is a square filling the space, to at most 600px. */
outfit-viewer {
width: min(100cqw, 100cqh, 600px);
height: min(100cqw, 100cqh, 600px);
}
.no-preview-message {
color: white;
text-align: center;
padding: 2rem;
font-size: 1.2rem;
}
/* Species/color picker floats over the preview at the bottom */
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 */
}
select {
padding: 0.5rem 2rem 0.5rem 0.75rem;
font-size: 1rem;
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);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
&: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);
}
option {
background: #2D3748;
color: white;
}
}
/* Submit button: progressive enhancement pattern */
/* If JS is disabled, the button is always visible */
/* If JS is enabled but slow to load, fade in after 0.75s */
/* Once the web component loads, hide the button completely */
input[type="submit"] {
padding: 0.5rem 1rem;
font-size: 1rem;
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);
&: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);
}
}
/* If JS is enabled, hide the submit button initially with a delay */
@media (scripting: enabled) {
input[type="submit"] {
position: absolute;
margin-left: 0.5em;
opacity: 0;
animation: fade-in 0.25s forwards;
animation-delay: 0.75s;
}
}
/* Once auto-loading is ready, hide the submit button completely */
&:state(auto-loading) {
input[type="submit"] {
display: none;
}
}
}
/* Show picker on hover (real hover only, not simulated touch hover) */
@media (hover: hover) {
&:hover species-color-picker {
opacity: 1;
}
}
/* Show picker when it has focus */
&:has(species-color-picker:focus-within) species-color-picker {
opacity: 1;
}
}
.outfit-controls-section {
width: 400px;
background: #fff;
padding: 2rem;
overflow-y: auto;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
@media (max-width: 800px) {
width: 100%;
max-height: 40vh;
}
h1 {
margin-top: 0;
font-size: 1.75rem;
color: #448844;
}
h2 {
font-size: 1.25rem;
color: #448844;
margin-top: 2rem;
}
}
.current-selection {
padding: 1rem;
background: #f0f0f0;
border-radius: 4px;
margin: 1rem 0;
p {
margin: 0;
color: #666;
}
strong {
color: #000;
}
}
.worn-items {
margin-top: 2rem;
.items-list {
list-style: none;
padding: 0;
margin: 0.5rem 0;
}
.item-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: #f9f9f9;
margin-bottom: 0.5rem;
border-radius: 8px;
color: #333;
transition: background 0.2s, box-shadow 0.2s;
position: relative;
&:hover {
background: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:hover .item-remove-button {
opacity: 1;
}
}
.item-thumbnail {
flex-shrink: 0;
width: 50px;
height: 50px;
border-radius: 6px;
overflow: hidden;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
.item-info {
flex: 1;
min-width: 0;
/* Allow text to truncate */
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item-name {
font-weight: 500;
color: #2D3748;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-badges {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
}
.item-remove-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
margin: 0;
border: none;
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
line-height: 1;
width: 1.75rem;
height: 1.75rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s, background 0.2s, transform 0.1s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
&:hover {
background: rgba(255, 255, 255, 1);
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
&:focus {
opacity: 1;
outline: 2px solid #448844;
outline-offset: 2px;
}
}
}
.item-search-form {
margin-bottom: 1.5rem;
display: flex;
gap: 0.5rem;
input[type="text"] {
flex: 1;
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
&:focus {
outline: none;
border-color: #448844;
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
}
}
input[type="submit"] {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
border: none;
border-radius: 6px;
background: #448844;
color: white;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #357535;
}
&:active {
transform: scale(0.98);
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.3);
}
}
}
.search-results {
.search-results-header {
margin-bottom: 1.5rem;
.back-button {
padding: 0.5rem 1rem;
margin: 0 0 1rem 0;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
color: #448844;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: background 0.2s, border-color 0.2s;
&:hover {
background: #f9f9f9;
border-color: #448844;
}
&:focus {
outline: none;
border-color: #448844;
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
}
}
}
.search-results-list {
list-style: none;
padding: 0;
margin: 1rem 0;
.item-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: #f9f9f9;
margin-bottom: 0.5rem;
border-radius: 8px;
color: #333;
transition: background 0.2s, box-shadow 0.2s;
position: relative;
&:hover {
background: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:hover .item-add-button {
opacity: 1;
}
}
.item-thumbnail {
flex-shrink: 0;
width: 50px;
height: 50px;
border-radius: 6px;
overflow: hidden;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item-name {
font-weight: 500;
color: #2D3748;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-badges {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
}
.item-add-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
margin: 0;
border: none;
background: rgba(68, 136, 68, 0.9);
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
line-height: 1;
width: 1.75rem;
height: 1.75rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s, background 0.2s, transform 0.1s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
&:hover {
background: rgba(68, 136, 68, 1);
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
&:focus {
opacity: 1;
outline: 2px solid #448844;
outline-offset: 2px;
}
}
}
.empty-state {
padding: 2rem;
text-align: center;
color: #666;
}
.pagination {
margin: 1.5rem 0;
display: flex;
justify-content: center;
gap: 0.5rem;
font-size: 0.9rem;
a,
span,
em {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #448844;
background: white;
&:hover {
background: #f9f9f9;
border-color: #448844;
}
}
.current,
em {
background: #448844;
color: white;
border-color: #448844;
font-style: normal;
}
.disabled {
color: #ccc;
cursor: not-allowed;
border-color: #eee;
&:hover {
background: white;
border-color: #eee;
}
}
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View file

@ -1,713 +0,0 @@
@import "../application/item-badges.css";
/* Base button defaults - applied to all interactive controls */
button,
input[type="submit"],
select {
padding: 0.5rem 0.75rem;
font-size: 0.95rem;
border-radius: 0.375rem;
border: 1px solid #ddd;
background: white;
color: #448844;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f9f9f9;
border-color: #448844;
}
&:focus {
outline: none;
border-color: #448844;
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
}
}
/* Overlay variant - dark translucent buttons on preview background */
.outfit-preview-section {
button,
select,
input[type="submit"] {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
&:hover {
background: rgba(0, 0, 0, 0.8);
border-color: rgba(255, 255, 255, 0.5);
}
&:focus {
border-color: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
}
}
/* Primary action variant - solid green for main actions */
.search-form input[type="submit"] {
padding: 0.75rem 1.5rem;
font-size: 1rem;
border: none;
background: #448844;
color: white;
font-weight: 500;
&:hover {
background: #357535;
}
&:focus {
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.3);
}
&:active {
transform: scale(0.98);
}
}
/* Icon button pattern - small action buttons with hover reveals */
.item-remove-button,
.item-add-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
margin: 0;
border: none;
border-radius: 4px;
font-size: 1rem;
line-height: 1;
width: 1.75rem;
height: 1.75rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s, background 0.2s, transform 0.1s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
&:hover {
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
&:focus {
opacity: 1;
outline: 2px solid #448844;
outline-offset: 2px;
}
}
.item-remove-button {
background: rgba(255, 255, 255, 0.9);
&:hover {
background: rgba(255, 255, 255, 1);
}
}
.item-add-button {
background: rgba(68, 136, 68, 0.9);
&:hover {
background: rgba(68, 136, 68, 1);
}
}
/* Pagination links - treated as buttons for consistency */
.pagination {
a,
span,
em {
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
border-radius: 4px;
border: 1px solid #ddd;
background: white;
color: #448844;
text-decoration: none;
transition: all 0.2s;
&:hover {
background: #f9f9f9;
border-color: #448844;
}
}
.current,
em {
background: #448844;
color: white;
border-color: #448844;
font-style: normal;
&:hover {
background: #448844;
border-color: #448844;
}
}
.disabled {
color: #ccc;
border-color: #eee;
cursor: not-allowed;
&:hover {
background: white;
border-color: #eee;
}
}
}
body.wardrobe-v2 {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
font-family: sans-serif;
}
.wardrobe-container {
display: grid;
height: 100vh;
background: #000;
/* Mobile: vertical stack with preview on top, controls below */
grid-template-areas:
"preview"
"controls";
grid-template-rows: minmax(100px, 45%) minmax(300px, 55%);
grid-template-columns: 100%;
/* Desktop: side-by-side layout */
@media (min-width: 801px) {
grid-template-areas: "preview controls";
grid-template-rows: 100%;
grid-template-columns: 50% 50%;
}
}
.outfit-preview-section {
grid-area: preview;
display: flex;
align-items: center;
justify-content: center;
background: #000;
position: relative;
container-type: size;
/* The outfit viewer is a square filling the space, to at most 600px. */
outfit-viewer {
width: min(100cqw, 100cqh, 600px);
height: min(100cqw, 100cqh, 600px);
}
.no-preview-message {
color: white;
text-align: center;
padding: 2rem;
font-size: 1.2rem;
}
/* Preview controls container - groups species/color picker and pose picker */
.preview-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1.5rem;
pointer-events: none;
/* Start hidden, reveal on hover or focus */
opacity: 0;
transition: opacity 0.2s;
> * {
pointer-events: auto;
}
}
/* Pose picker button */
.pose-picker-button {
anchor-name: --pose-picker-anchor;
display: flex;
align-items: center;
gap: 0.5rem;
.pose-emoji {
font-size: 1.1rem;
}
.pose-label {
font-weight: normal;
min-width: 3.5rem;
}
.chevron {
font-size: 0.8rem;
opacity: 0.7;
}
&:focus,
&[popovertargetopen] {
border-color: rgba(255, 255, 255, 0.8);
}
}
/* Pose picker popover */
pose-picker-popover {
position: absolute;
position-anchor: --pose-picker-anchor;
margin: 0;
inset: auto;
position-area: top;
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);
.pose-picker-form {
display: flex;
flex-direction: column;
}
.pose-picker-table {
border-collapse: separate;
border-spacing: 0.5rem;
th {
text-align: center;
color: white;
padding: 0.25rem;
font-weight: normal;
}
td {
padding: 0;
}
}
.emoji-icon {
font-size: 1.25rem;
display: inline-block;
user-select: none;
}
.pose-option {
display: block;
cursor: pointer;
position: relative;
width: 60px;
height: 60px;
input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.pose-thumbnail {
width: 60px;
height: 60px;
border-radius: 8px;
overflow: hidden;
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;
display: flex;
align-items: center;
justify-content: center;
}
.pose-thumbnail-viewer {
width: 60px !important;
height: 60px !important;
transform: scale(0.8);
}
.pose-unavailable {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
.question-mark {
font-size: 2rem;
}
}
/* Selected state */
input[type="radio"]:checked + .pose-thumbnail {
border-color: #48BB78;
box-shadow: 0 0 0 3px rgba(72, 187, 120, 0.4);
transform: scale(1.05);
}
/* Hover state (only for available poses) */
&.available:hover .pose-thumbnail {
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.05);
}
/* 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.3);
}
/* Unavailable state */
&.unavailable {
cursor: not-allowed;
.pose-thumbnail {
opacity: 0.5;
background: rgba(128, 128, 128, 0.2);
}
}
}
/* Submit button: progressive enhancement pattern */
.pose-submit-button {
margin-top: 1rem;
width: 100%;
}
/* If JS is enabled, hide the submit button initially with a delay */
@media (scripting: enabled) {
.pose-submit-button {
opacity: 0;
animation: fade-in 0.25s forwards;
animation-delay: 0.75s;
}
}
/* Once auto-submit is enabled, hide the submit button completely */
&:state(auto-loading) .pose-submit-button {
display: none;
}
}
/* Species/color picker */
species-color-picker {
display: flex;
align-items: center;
gap: 0.5rem;
select {
padding: 0.5rem 2rem 0.5rem 0.75rem;
font-size: 1rem;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
option {
background: #2D3748;
color: white;
}
}
/* Submit button: progressive enhancement pattern */
/* If JS is disabled, the button is always visible */
/* If JS is enabled but slow to load, fade in after 0.75s */
/* Once the web component loads, hide the button completely */
input[type="submit"] {
padding: 0.5rem 1rem;
font-size: 1rem;
}
/* If JS is enabled, hide the submit button initially with a delay */
@media (scripting: enabled) {
input[type="submit"] {
position: absolute;
margin-left: 0.5em;
opacity: 0;
animation: fade-in 0.25s forwards;
animation-delay: 0.75s;
}
}
/* Once auto-loading is ready, hide the submit button completely */
&:state(auto-loading) {
input[type="submit"] {
display: none;
}
}
}
/* Show controls on hover (real hover only, not simulated touch hover) */
@media (hover: hover) {
&:hover .preview-controls {
opacity: 1;
}
}
/* 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;
}
}
.outfit-controls-section {
grid-area: controls;
background: #fff;
padding: 2rem;
box-sizing: border-box;
overflow-y: auto;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
h1 {
margin-top: 0;
font-size: 1.75rem;
color: #448844;
}
h2 {
font-size: 1.25rem;
color: #448844;
margin-top: 2rem;
}
.back-button {
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 500;
}
}
.current-selection {
padding: 1rem;
background: #f0f0f0;
border-radius: 4px;
margin: 1rem 0;
p {
margin: 0;
color: #666;
}
strong {
color: #000;
}
}
.worn-items {
margin-top: 2rem;
.items-list {
list-style: none;
padding: 0;
margin: 0.5rem 0;
}
.item-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: #f9f9f9;
margin-bottom: 0.5rem;
border-radius: 8px;
color: #333;
transition: background 0.2s, box-shadow 0.2s;
position: relative;
&:hover {
background: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:hover .item-remove-button {
opacity: 1;
}
}
.item-thumbnail {
flex-shrink: 0;
width: 50px;
height: 50px;
border-radius: 6px;
overflow: hidden;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
.item-info {
flex: 1;
min-width: 0;
/* Allow text to truncate */
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item-name {
font-weight: 500;
color: #2D3748;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-badges {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
}
/* .item-remove-button styles are defined in button system above */
}
.item-search-form {
margin-bottom: 1.5rem;
display: flex;
gap: 0.5rem;
align-items: stretch;
> form {
display: contents;
}
.search-form {
input[type="text"] {
flex: 1;
padding: 0.75rem;
font-size: 1rem;
border-radius: 6px;
border: 1px solid #ddd;
background: white;
&:focus {
outline: none;
border-color: #448844;
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
}
}
/* input[type="submit"] styles are defined in button system above */
}
}
.search-results {
.search-results-list {
list-style: none;
padding: 0;
margin: 1rem 0;
.item-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: #f9f9f9;
margin-bottom: 0.5rem;
border-radius: 8px;
color: #333;
transition: background 0.2s, box-shadow 0.2s;
position: relative;
&:hover {
background: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:hover .item-add-button {
opacity: 1;
}
}
.item-thumbnail {
flex-shrink: 0;
width: 50px;
height: 50px;
border-radius: 6px;
overflow: hidden;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item-name {
font-weight: 500;
color: #2D3748;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-badges {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
}
/* .item-add-button styles are defined in button system above */
}
.empty-state {
padding: 2rem;
text-align: center;
color: #666;
}
.pagination {
margin: 1.5rem 0;
display: flex;
justify-content: center;
gap: 0.5rem;
/* Pagination link styles are defined in button system above */
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View file

@ -77,6 +77,53 @@ class OutfitsController < ApplicationController
@campaign = Fundraising::Campaign.current rescue nil
end
def new_v2
# Get selected species and color from params, or default to Blue Acara
@selected_species = params[:species] ? Species.find_by_id(params[:species]) : Species.find_by_name("Acara")
@selected_color = params[:color] ? Color.find_by_id(params[:color]) : Color.find_by_name("Blue")
# Load valid colors for the selected species (colors that have existing pet types)
@species = Species.alphabetical
@colors = @selected_species.compatible_colors
# Find the best pet type for this species+color combo
# If the exact combo doesn't exist, this will fall back to a simple color
@pet_type = PetType.for_species_and_color(
species_id: @selected_species.id,
color_id: @selected_color.id
)
# Use the pet type's actual color as the selected color
# (might differ from requested color if we fell back to a simple color)
@selected_color = @pet_type&.color
# Load items from the objects[] parameter
item_ids = params[:objects] || []
items = Item.where(id: item_ids)
# Build the outfit
@outfit = Outfit.new(
pet_state: @pet_type&.canonical_pet_state,
worn_items: items,
)
# Preload the manifests for all visible layers, so they load efficiently
# in parallel rather than sequentially when rendering
SwfAsset.preload_manifests(@outfit.visible_layers)
# Handle search mode
@search_mode = params[:q].present?
if @search_mode
search_filters = build_search_filters(params[:q], @outfit)
query_params = ActionController::Parameters.new(
search_filters.each_with_index.map { |filter, i| [i.to_s, filter] }.to_h
)
@query = Item::Search::Query.from_params(query_params, current_user)
@search_results = @query.results.paginate(page: params.dig(:q, :page), per_page: 30)
end
render layout: false
end
def show
@outfit = Outfit.find(params[:id])
@ -129,5 +176,51 @@ class OutfitsController < ApplicationController
:status => :bad_request
end
def build_search_filters(query_params, outfit)
filters = []
# Add name filter if present
if query_params[:name].present?
filters << { key: "name", value: query_params[:name] }
end
# Add item kind filter if present
if query_params[:item_kind].present?
case query_params[:item_kind]
when "nc"
filters << { key: "is_nc", value: "true" }
when "np"
filters << { key: "is_np", value: "true" }
when "pb"
filters << { key: "is_pb", value: "true" }
end
end
# Add zone filter if present
if query_params[:zone].present?
filters << { key: "occupied_zone_set_name", value: query_params[:zone] }
end
# Always add auto-filter for items that fit the current pet
pet_type = outfit.pet_type
if pet_type
fit_filter = {
key: "fits",
value: {
species_id: pet_type.species_id.to_s,
color_id: pet_type.color_id.to_s
}
}
# Include alt_style_id if present
if outfit.alt_style_id.present?
fit_filter[:value][:alt_style_id] = outfit.alt_style_id.to_s
end
filters << fit_filter
end
filters
end
end

View file

@ -1,144 +0,0 @@
class WardrobeController < ApplicationController
def show
# Get selected species and color from params, or default to Blue Acara
@selected_species = params[:species] ? Species.find_by_id(params[:species]) : Species.find_by_name("Acara")
@selected_color = params[:color] ? Color.find_by_id(params[:color]) : Color.find_by_name("Blue")
# Load valid colors for the selected species (colors that have existing pet types)
@species = Species.alphabetical
@colors = @selected_species.compatible_colors
# Find the best pet type for this species+color combo
# If the exact combo doesn't exist, this will fall back to a simple color
@pet_type = PetType.for_species_and_color(
species_id: @selected_species.id,
color_id: @selected_color.id
)
# Use the pet type's actual color as the selected color
# (might differ from requested color if we fell back to a simple color)
@selected_color = @pet_type&.color
# Get the selected pose from params, or default to nil (will use canonical)
@selected_pose = params[:pose]
# Find the pet state for the selected pose, or use canonical
@pet_state = if @pet_type && @selected_pose.present?
@pet_type.pet_states.with_pose(@selected_pose).first || @pet_type.canonical_pet_state
else
@pet_type&.canonical_pet_state
end
# If we found a pet_state, use its actual pose as the selected pose
@selected_pose = @pet_state&.pose
# Load all available poses for this pet type (for the pose picker)
@available_poses = @pet_type ? available_poses_for(@pet_type) : {}
# Preload the layers for all available poses so the thumbnails render efficiently
if @pet_type
pose_pet_states = @available_poses.values.compact
SwfAsset.preload_manifests(pose_pet_states.flat_map(&:swf_assets))
end
# Load items from the objects[] parameter
item_ids = params[:objects] || []
items = Item.where(id: item_ids)
# Build the outfit
@outfit = Outfit.new(
pet_state: @pet_state,
worn_items: items,
)
# Preload the manifests for all visible layers, so they load efficiently
# in parallel rather than sequentially when rendering
SwfAsset.preload_manifests(@outfit.visible_layers)
# Handle search mode
@search_mode = params[:q].present?
if @search_mode
search_filters = build_search_filters(params[:q], @outfit)
query_params = ActionController::Parameters.new(
search_filters.each_with_index.map { |filter, i| [i.to_s, filter] }.to_h
)
@query = Item::Search::Query.from_params(query_params, current_user)
@search_results = @query.results.paginate(page: params.dig(:q, :page), per_page: 30)
end
render layout: false
end
private
# Returns a hash of pose => pet_state for all the main poses,
# indicating which poses are available for this pet type.
# Uses the same logic as the Rainbow Pool to pick the "canonical" pet state
# for each pose when multiple states exist.
def available_poses_for(pet_type)
poses_hash = {}
# Group all pet states by pose, then pick the best one for each pose
# using emotion_order (same logic as Rainbow Pool)
pet_type.pet_states.emotion_order.group_by(&:pose).each do |pose, states|
# Only include the main poses (skip UNKNOWN, UNCONVERTED, etc.)
if PetState::MAIN_POSES.include?(pose)
poses_hash[pose] = states.first
end
end
# Ensure all main poses are in the hash, even if nil
PetState::MAIN_POSES.each do |pose|
poses_hash[pose] ||= nil
end
poses_hash
end
def build_search_filters(query_params, outfit)
filters = []
# Add name filter if present
if query_params[:name].present?
filters << { key: "name", value: query_params[:name] }
end
# Add item kind filter if present
if query_params[:item_kind].present?
case query_params[:item_kind]
when "nc"
filters << { key: "is_nc", value: "true" }
when "np"
filters << { key: "is_np", value: "true" }
when "pb"
filters << { key: "is_pb", value: "true" }
end
end
# Add zone filter if present
if query_params[:zone].present?
filters << { key: "occupied_zone_set_name", value: query_params[:zone] }
end
# Always add auto-filter for items that fit the current pet
pet_type = outfit.pet_type
if pet_type
fit_filter = {
key: "fits",
value: {
species_id: pet_type.species_id.to_s,
color_id: pet_type.color_id.to_s
}
}
# Include alt_style_id if present
if outfit.alt_style_id.present?
fit_filter[:value][:alt_style_id] = outfit.alt_style_id.to_s
end
filters << fit_filter
end
filters
end
end

View file

@ -65,6 +65,30 @@ module OutfitsHelper
text_field_tag 'name', nil, options
end
# Generate hidden fields to preserve outfit state in URL params.
# Use the `except` parameter to skip certain fields, e.g. to override
# them with specific values, like in the species/color picker.
def outfit_state_params(outfit = @outfit, except: [])
fields = []
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
unless except.include?(:worn_items)
outfit.worn_items.each do |item|
fields << hidden_field_tag('objects[]', item.id)
end
end
unless except.include?(:q)
(params[:q] || {}).each do |key, value|
fields << hidden_field_tag("q[#{key}]", value) if value.present?
end
end
safe_join fields
end
def outfit_viewer(...)
render partial: "outfit_viewer",
locals: parse_outfit_viewer_options(...)
@ -75,8 +99,133 @@ module OutfitsHelper
locals: parse_outfit_viewer_options(...)
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.
def outfit_items_by_zone(outfit)
return [] if outfit.pet_type.nil?
# Get item appearances for this outfit
item_appearances = Item.appearances_for(
outfit.worn_items,
outfit.pet_type,
swf_asset_includes: [:zone]
)
# Separate incompatible items (no layers for this pet)
compatible_items = []
incompatible_items = []
outfit.worn_items.each do |item|
appearance = item_appearances[item.id]
if appearance&.present?
compatible_items << {item: item, appearance: appearance}
else
incompatible_items << item
end
end
# Group items by zone - multi-zone items appear in each zone
items_by_zone = Hash.new { |h, k| h[k] = [] }
zones_by_id = {}
compatible_items.each do |item_with_appearance|
item = item_with_appearance[:item]
appearance = item_with_appearance[:appearance]
# Get unique zones for this item (an item may have multiple assets per zone)
appearance.swf_assets.map(&:zone).uniq.each do |zone|
zones_by_id[zone.id] = zone
items_by_zone[zone.id] << item
end
end
# Create zone groups with sorted items
zones_and_items = items_by_zone.map do |zone_id, items|
{
zone_id: zone_id,
zone_label: zones_by_id[zone_id].label,
items: items.sort_by { |item| item.name.downcase }
}
end
# Sort zone groups alphabetically by label, then by ID for tiebreaking
zones_and_items.sort_by! do |group|
[group[:zone_label].downcase, group[:zone_id]]
end
# Apply multi-zone simplification: remove redundant single-item groups
zones_and_items = simplify_multi_zone_groups(zones_and_items)
# Add zone ID disambiguation for duplicate labels
zones_and_items = disambiguate_zone_labels(zones_and_items)
# Add incompatible items section if any
if incompatible_items.any?
zones_and_items << {
zone_id: nil,
zone_label: "Incompatible",
items: incompatible_items.sort_by { |item| item.name.downcase }
}
end
zones_and_items
end
private
# Simplify zone groups by removing redundant single-item groups.
# Keep groups with multiple items (conflicts). For single-item groups,
# only keep them if the item doesn't appear in a multi-item group.
def simplify_multi_zone_groups(zones_and_items)
# Find groups with conflicts (multiple items)
groups_with_conflicts = zones_and_items.select { |g| g[:items].length > 1 }
# Track which items appear in conflict groups
items_with_conflicts = Set.new(
groups_with_conflicts.flat_map { |g| g[:items].map(&:id) }
)
# Track which items we've already shown
items_we_have_seen = Set.new
# Filter groups
zones_and_items.select do |group|
# Always keep groups with multiple items
if group[:items].length > 1
group[:items].each { |item| items_we_have_seen.add(item.id) }
true
else
# For single-item groups, only keep if:
# - Item hasn't been seen yet AND
# - Item won't appear in a conflict group
item = group[:items].first
item_id = item.id
if items_we_have_seen.include?(item_id) || items_with_conflicts.include?(item_id)
false
else
items_we_have_seen.add(item_id)
true
end
end
end
end
# Add zone IDs to labels when there are duplicates
def disambiguate_zone_labels(zones_and_items)
label_counts = zones_and_items.group_by { |g| g[:zone_label] }
.transform_values(&:count)
zones_and_items.each do |group|
if label_counts[group[:zone_label]] > 1
group[:zone_label] = "#{group[:zone_label]} (##{group[:zone_id]})"
end
end
zones_and_items
end
def parse_outfit_viewer_options(
outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options
)

View file

@ -1,167 +0,0 @@
module WardrobeHelper
# Generate hidden fields to preserve outfit state in URL params.
# Use the `except` parameter to skip certain fields, e.g. to override
# them with specific values, like in the species/color picker.
def outfit_state_params(outfit = @outfit, except: [])
fields = []
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
fields << hidden_field_tag(:pose, @selected_pose) if @selected_pose && !except.include?(:pose)
unless except.include?(:worn_items)
outfit.worn_items.each do |item|
fields << hidden_field_tag('objects[]', item.id)
end
end
unless except.include?(:q)
(params[:q] || {}).each do |key, value|
fields << hidden_field_tag("q[#{key}]", value) if value.present?
end
end
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.
def outfit_items_by_zone(outfit)
return [] if outfit.pet_type.nil?
# Get item appearances for this outfit
item_appearances = Item.appearances_for(
outfit.worn_items,
outfit.pet_type,
swf_asset_includes: [:zone]
)
# Separate incompatible items (no layers for this pet)
compatible_items = []
incompatible_items = []
outfit.worn_items.each do |item|
appearance = item_appearances[item.id]
if appearance&.present?
compatible_items << {item: item, appearance: appearance}
else
incompatible_items << item
end
end
# Group items by zone - multi-zone items appear in each zone
items_by_zone = Hash.new { |h, k| h[k] = [] }
zones_by_id = {}
compatible_items.each do |item_with_appearance|
item = item_with_appearance[:item]
appearance = item_with_appearance[:appearance]
# Get unique zones for this item (an item may have multiple assets per zone)
appearance.swf_assets.map(&:zone).uniq.each do |zone|
zones_by_id[zone.id] = zone
items_by_zone[zone.id] << item
end
end
# Create zone groups with sorted items
zones_and_items = items_by_zone.map do |zone_id, items|
{
zone_id: zone_id,
zone_label: zones_by_id[zone_id].label,
items: items.sort_by { |item| item.name.downcase }
}
end
# Sort zone groups alphabetically by label, then by ID for tiebreaking
zones_and_items.sort_by! do |group|
[group[:zone_label].downcase, group[:zone_id]]
end
# Apply multi-zone simplification: remove redundant single-item groups
zones_and_items = simplify_multi_zone_groups(zones_and_items)
# Add zone ID disambiguation for duplicate labels
zones_and_items = disambiguate_zone_labels(zones_and_items)
# Add incompatible items section if any
if incompatible_items.any?
zones_and_items << {
zone_id: nil,
zone_label: "Incompatible",
items: incompatible_items.sort_by { |item| item.name.downcase }
}
end
zones_and_items
end
private
# Simplify zone groups by removing redundant single-item groups.
# Keep groups with multiple items (conflicts). For single-item groups,
# only keep them if the item doesn't appear in a multi-item group.
def simplify_multi_zone_groups(zones_and_items)
# Find groups with conflicts (multiple items)
groups_with_conflicts = zones_and_items.select { |g| g[:items].length > 1 }
# Track which items appear in conflict groups
items_with_conflicts = Set.new(
groups_with_conflicts.flat_map { |g| g[:items].map(&:id) }
)
# Track which items we've already shown
items_we_have_seen = Set.new
# Filter groups
zones_and_items.select do |group|
# Always keep groups with multiple items
if group[:items].length > 1
group[:items].each { |item| items_we_have_seen.add(item.id) }
true
else
# For single-item groups, only keep if:
# - Item hasn't been seen yet AND
# - Item won't appear in a conflict group
item = group[:items].first
item_id = item.id
if items_we_have_seen.include?(item_id) || items_with_conflicts.include?(item_id)
false
else
items_we_have_seen.add(item_id)
true
end
end
end
end
# Add zone IDs to labels when there are duplicates
def disambiguate_zone_labels(zones_and_items)
label_counts = zones_and_items.group_by { |g| g[:zone_label] }
.transform_values(&:count)
zones_and_items.each do |group|
if label_counts[group[:zone_label]] > 1
group[:zone_label] = "#{group[:zone_label]} (##{group[:zone_id]})"
end
end
zones_and_items
end
end

View file

@ -1,6 +1,11 @@
.search-results
.search-results-header
= button_to wardrobe_v2_path, method: :get, class: "back-button" do
← Back to outfit
= outfit_state_params except: [:q]
- if @search_results.any?
= will_paginate @search_results, page_links: false, param_name: "q[page]", params: { q: params[:q], species: @outfit.species_id, color: @outfit.color_id, objects: params[:objects] }
= will_paginate @search_results, param_name: "q[page]", params: { q: params[:q], species: @outfit.species_id, color: @outfit.color_id, objects: params[:objects] }
%ul.search-results-list
- @search_results.each do |item|

View file

@ -0,0 +1,71 @@
- title "Wardrobe v2"
!!! 5
%html
%head
%meta{charset: 'utf-8'}
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1'}
%title= yield :title
%link{href: image_path('favicon.png'), rel: 'icon'}
= stylesheet_link_tag "application/hanger-spinner"
= stylesheet_link_tag "application/outfit-viewer"
= page_stylesheet_link_tag "outfits/new_v2"
= javascript_include_tag "application", async: true
= javascript_include_tag "idiomorph", async: true
= javascript_include_tag "outfit-viewer", async: true
= javascript_include_tag "species-color-picker", async: true
= javascript_include_tag "outfits/new_v2", async: true
= csrf_meta_tags
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
%body.wardrobe-v2
.wardrobe-container
.outfit-preview-section
- if @pet_type.nil?
.no-preview-message
%p
We haven't seen this kind of pet before! Try a different species/color
combination.
- else
= outfit_viewer @outfit
%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
%h1 Customize your pet
= form_with url: wardrobe_v2_path, method: :get, class: "item-search-form" do |f|
= outfit_state_params
= f.text_field "q[name]", placeholder: "Search for items...", value: params.dig(:q, :name), "aria-label": "Search for items"
= f.submit "Search"
- if @search_mode
= render "search_results"
- elsif @outfit.worn_items.any?
.worn-items
- outfit_items_by_zone(@outfit).each do |zone_group|
.zone-group
%h3.zone-label= zone_group[:zone_label]
%ul.items-list
- zone_group[:items].each do |item|
%li.item-card
.item-thumbnail
= image_tag item.thumbnail_url, alt: item.name, loading: "lazy"
.item-info
.item-name= item.name
.item-badges
= render "items/badges/kind", item: item
= render "items/badges/first_seen", item: item
= button_to wardrobe_v2_path, method: :get, class: "item-remove-button", title: "Remove #{item.name}", "aria-label": "Remove #{item.name}" do
= outfit_state_params @outfit.without_item(item)

View file

@ -1,21 +0,0 @@
-# Renders a single pose option in the pose picker grid
-# @param pose [String] The pose name (e.g., "HAPPY_MASC")
-# @param pet_state [PetState, nil] The pet state for this pose, or nil if unavailable
-# @param selected [Boolean] Whether this pose is currently selected
- is_available = pet_state.present?
- pose_label = pose.split('_').map(&:capitalize).join(' ')
%label.pose-option{class: [is_available ? 'available' : 'unavailable', selected ? 'selected' : nil]}
= radio_button_tag :pose, pose, selected,
disabled: !is_available,
"aria-label": pose_label + (is_available ? "" : " (not available)")
.pose-thumbnail
- if is_available
-# Create a minimal outfit with just this pet state for the thumbnail
- thumbnail_outfit = Outfit.new(pet_state: pet_state, worn_items: [])
= outfit_viewer thumbnail_outfit, class: "pose-thumbnail-viewer"
- else
.pose-unavailable
%span.question-mark{title: "Not available"} ❓

View file

@ -1,119 +0,0 @@
- title "Wardrobe v2"
!!! 5
%html
%head
%meta{charset: 'utf-8'}
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1'}
%title= yield :title
%link{href: image_path('favicon.png'), rel: 'icon'}
= stylesheet_link_tag "application/hanger-spinner"
= stylesheet_link_tag "application/outfit-viewer"
= page_stylesheet_link_tag "wardrobe/show"
= javascript_include_tag "application", async: true
= javascript_include_tag "idiomorph", async: true
= javascript_include_tag "outfit-viewer", async: true
= javascript_include_tag "species-color-picker", async: true
= javascript_include_tag "pose-picker", async: true
= javascript_include_tag "wardrobe/show", async: true
= csrf_meta_tags
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
%body.wardrobe-v2
.wardrobe-container
.outfit-preview-section
- if @pet_type.nil?
.no-preview-message
%p
We haven't seen this kind of pet before! Try a different species/color
combination.
- else
= 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
- 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
.item-search-form
- if @search_mode
= button_to wardrobe_v2_path, method: :get, class: "back-button" do
= outfit_state_params except: [:q]
= form_with url: wardrobe_v2_path, method: :get, class: "search-form" do |f|
= outfit_state_params
= f.text_field "q[name]", placeholder: "Search for items...", value: params.dig(:q, :name), "aria-label": "Search for items"
= f.submit "Search"
- if @search_mode
= render "search_results"
- else
%h1 Untitled outfit
- if @outfit.worn_items.any?
.worn-items
- outfit_items_by_zone(@outfit).each do |zone_group|
.zone-group
%h3.zone-label= zone_group[:zone_label]
%ul.items-list
- zone_group[:items].each do |item|
%li.item-card
.item-thumbnail
= image_tag item.thumbnail_url, alt: item.name, loading: "lazy"
.item-info
.item-name= item.name
.item-badges
= render "items/badges/kind", item: item
= render "items/badges/first_seen", item: item
= button_to wardrobe_v2_path, method: :get, class: "item-remove-button", title: "Remove #{item.name}", "aria-label": "Remove #{item.name}" do
= outfit_state_params @outfit.without_item(item)

6
bin/ci
View file

@ -1,6 +0,0 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "active_support/continuous_integration"
CI = ActiveSupport::ContinuousIntegration
require_relative "../config/ci.rb"

View file

@ -1,8 +0,0 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"
# Explicit RuboCop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
load Gem.bin_path("rubocop", "rubocop")

View file

@ -23,8 +23,6 @@ FileUtils.chdir APP_ROOT do
puts "\n== Preparing database =="
system! "bin/rails db:prepare"
system! "bin/rails db:reset" if ARGV.include?("--reset")
puts "\n== Importing public modeling data =="
system! "bin/rails public_data:pull"

View file

@ -25,7 +25,7 @@ Bundler.require(*Rails.groups)
module OpenneoImpressItems
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 8.1
config.load_defaults 8.0
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.

View file

@ -1,21 +0,0 @@
# Run using bin/ci
CI.run do
step "Setup", "bin/setup --skip-server"
step "Style: Ruby", "bin/rubocop"
step "Security: Importmap vulnerability audit", "bin/importmap audit"
step "Tests: Rails", "bin/rails test"
step "Tests: System", "bin/rails test:system"
step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
# Optional: set a green GitHub commit status to unblock PR merge.
# Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
# if success?
# step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
# else
# failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
# end
end

View file

@ -3,7 +3,7 @@ development:
adapter: mysql2
host: <%= ENV.fetch("DB_HOST", "localhost") %>
database: openneo_impress
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
username: root
pool: 5
encoding: utf8mb4
collation: utf8mb4_unicode_520_ci
@ -14,7 +14,7 @@ development:
adapter: mysql2
host: <%= ENV.fetch("DB_HOST", "localhost") %>
database: openneo_id
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
username: root
pool: 2
variables:
sql_mode: TRADITIONAL
@ -25,7 +25,7 @@ test:
adapter: mysql2
host: <%= ENV.fetch("DB_HOST", "localhost") %>
database: openneo_impress_test
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
username: root
pool: 5
encoding: utf8mb4
collation: utf8mb4_unicode_520_ci
@ -36,7 +36,7 @@ test:
adapter: mysql2
host: <%= ENV.fetch("DB_HOST", "localhost") %>
database: openneo_id_test
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
username: root
pool: 2
variables:
sql_mode: TRADITIONAL

View file

@ -56,8 +56,7 @@ Rails.application.configure do
# Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
# Highlight code that triggered redirect in logs.
config.action_dispatch.verbose_redirect_logs = true
config.react.variant = :development
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true

View file

@ -18,6 +18,9 @@ Rails.application.configure do
# Cache assets for far-future expiry since they are all digest stamped.
config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
# Enable static file serving from the `/public` folder (turn off if using NGINX/Apache for it).
config.public_file_server.enabled = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
@ -34,7 +37,9 @@ Rails.application.configure do
config.log_tags = [ :request_id ]
config.logger = ActiveSupport::TaggedLogging.logger(STDOUT)
# Change to "debug" to log everything (including potentially personally-identifiable information!).
config.react.variant = :production
# Change to "debug" to log everything (including potentially personally-identifiable information!)
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
# Prevent health checks from clogging up the logs.

View file

@ -20,10 +20,6 @@
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src style-src)
#
# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
# # config.content_security_policy_nonce_auto = true
#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
# end

View file

@ -1,74 +0,0 @@
# Be sure to restart your server when you modify this file.
#
# This file eases your Rails 8.1 framework defaults upgrade.
#
# Uncomment each configuration one by one to switch to the new default.
# Once your application is ready to run with all new defaults, you can remove
# this file and set the `config.load_defaults` to `8.1`.
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
###
# Skips escaping HTML entities and line separators. When set to `false`, the
# JSON renderer no longer escapes these to improve performance.
#
# Example:
# class PostsController < ApplicationController
# def index
# render json: { key: "\u2028\u2029<>&" }
# end
# end
#
# Renders `{"key":"\u2028\u2029\u003c\u003e\u0026"}` with the previous default, but `{"key":"<>&"}` with the config
# set to `false`.
#
# Applications that want to keep the escaping behavior can set the config to `true`.
#++
# Rails.configuration.action_controller.escape_json_responses = false
###
# Skips escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON.
#
# Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019.
# As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset.
#++
# Rails.configuration.active_support.escape_js_separators_in_json = false
###
# Raises an error when order dependent finder methods (e.g. `#first`, `#second`) are called without `order` values
# on the relation, and the model does not have any order columns (`implicit_order_column`, `query_constraints`, or
# `primary_key`) to fall back on.
#
# The current behavior of not raising an error has been deprecated, and this configuration option will be removed in
# Rails 8.2.
#++
# Rails.configuration.active_record.raise_on_missing_required_finder_order_columns = true
###
# Controls how Rails handles path relative URL redirects.
# When set to `:raise`, Rails will raise an `ActionController::Redirecting::UnsafeRedirectError`
# for relative URLs without a leading slash, which can help prevent open redirect vulnerabilities.
#
# Example:
# redirect_to "example.com" # Raises UnsafeRedirectError
# redirect_to "@attacker.com" # Raises UnsafeRedirectError
# redirect_to "/safe/path" # Works correctly
#
# Applications that want to allow these redirects can set the config to `:log` (previous default)
# to only log warnings, or `:notify` to send ActiveSupport notifications.
#++
# Rails.configuration.action_controller.action_on_path_relative_redirect = :raise
###
# Use a Ruby parser to track dependencies between Action View templates
#++
# Rails.configuration.action_view.render_tracker = :ruby
###
# When enabled, hidden inputs generated by `form_tag`, `token_tag`, `method_tag`, and the hidden parameter fields
# included in `button_to` forms will omit the `autocomplete="off"` attribute.
#
# Applications that want to keep generating the `autocomplete` attribute for those tags can set it to `false`.
#++
# Rails.configuration.action_view.remove_hidden_field_autocomplete = true

View file

@ -10,8 +10,8 @@ OpenneoImpressItems::Application.routes.draw do
# TODO: It's a bit silly that outfits/new points to outfits#edit.
# Should we refactor the controller/view structure here?
get '/outfits/new', to: 'outfits#edit', as: :wardrobe
get '/outfits/new/v2', to: 'outfits#new_v2', as: :wardrobe_v2
get '/wardrobe' => redirect('/outfits/new')
get '/wardrobe/v2', to: 'wardrobe#show', as: :wardrobe_v2
get '/start/:color_name/:species_name' => 'outfits#start'
# The outfits users have created!

View file

@ -13,34 +13,34 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
## Current Status
**Wardrobe V2 is in early prototype/proof-of-concept stage.** It's accessible at `/wardrobe/v2` but is not yet linked from the main UI.
**Wardrobe V2 is in early prototype/proof-of-concept stage.** It's accessible at `/outfits/new/v2` but is not yet linked from the main UI.
### What's Implemented
#### Core Infrastructure
**Route & Controller** ([wardrobe_controller.rb](app/controllers/wardrobe_controller.rb))
- `GET /wardrobe/v2` - Main wardrobe endpoint
**Route & Controller** ([outfits_controller.rb:80-115](app/controllers/outfits_controller.rb#L80-L115))
- `GET /outfits/new/v2` - Main wardrobe endpoint
- Takes URL params: `species`, `color`, `objects[]` (item IDs)
- Returns full HTML page (no layout, designed to work standalone)
- Defaults to Blue Acara if no pet specified
**View Layer** ([show.html.haml](app/views/wardrobe/show.html.haml))
**View Layer** ([new_v2.html.haml](app/views/outfits/new_v2.html.haml))
- Full-page layout with preview (left) and controls (right)
- Responsive: stacks vertically on mobile (< 800px)
- Uses existing `outfit_viewer` partial for rendering
- Custom CSS in [wardrobe/show.css](app/assets/stylesheets/wardrobe/show.css)
- Minimal JavaScript in [wardrobe/show.js](app/assets/javascripts/wardrobe/show.js)
- Custom CSS in [outfits/new_v2.css](app/assets/stylesheets/outfits/new_v2.css)
- Minimal JavaScript in [outfits/new_v2.js](app/assets/javascripts/outfits/new_v2.js)
**Pet Selection** ([show.html.haml:31-42](app/views/wardrobe/show.html.haml#L31-L42))
**Pet Selection** ([new_v2.html.haml:31-42](app/views/outfits/new_v2.html.haml#L31-L42))
- Species/color picker using `<species-color-picker>` web component
- Floats over preview area (bottom), reveals on hover/focus
- Progressive enhancement: submit button appears if JS slow/disabled
- Auto-submits form on change when JS loaded
- Filters colors to only those compatible with selected species
- Advanced fallback: if species+color combo doesn't exist, falls back to simple color ([wardrobe_controller.rb:13-16](app/controllers/wardrobe_controller.rb#L13-L16))
- Advanced fallback: if species+color combo doesn't exist, falls back to simple color ([outfits_controller.rb:89-98](app/controllers/outfits_controller.rb#L89-L98))
**Item Display** ([show.html.haml:47-64](app/views/wardrobe/show.html.haml#L47-L64))
**Item Display** ([new_v2.html.haml:47-64](app/views/outfits/new_v2.html.haml#L47-L64))
- Groups worn items by zone (Hat, Jacket, Wings, etc.)
- Smart multi-zone simplification: hides redundant single-item zones
- Shows items that occupy multiple zones in conflict zones only
@ -49,7 +49,7 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
- Displays item thumbnails, names, and badges (NC/NP, first seen date)
- Alphabetical sorting within zones
**Item Search** ([show.html.haml:47-50](app/views/wardrobe/show.html.haml#L47-L50))
**Item Search** ([new_v2.html.haml:47-50](app/views/outfits/new_v2.html.haml#L47-L50))
- Search form at top of controls section
- Auto-filters to items that fit current pet (species + color + alt_style)
- Uses `Item::Search::Query.from_params` for structured search
@ -58,20 +58,20 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
- All search state scoped under `q[...]` params (name, page, etc.)
- "Back to outfit" button to exit search
**Item Addition** ([_search_results.html.haml](app/views/wardrobe/_search_results.html.haml))
**Item Addition** ([_search_results.html.haml](app/views/outfits/_search_results.html.haml))
- Add button () on each search result item
- Adds item to outfit via GET request with updated `objects[]` params
- Button hidden by default, appears on hover/focus
- Preserves search state when adding items
- Uses `outfit.with_item(item)` helper to generate new state
**Item Removal** ([show.html.haml:70-72](app/views/wardrobe/show.html.haml#L70-L72))
**Item Removal** ([new_v2.html.haml:70-72](app/views/outfits/new_v2.html.haml#L70-L72))
- Remove button (❌) on each worn item
- Removes item from outfit via GET request with updated `objects[]` params
- Button hidden by default, appears on hover/focus
- Uses `outfit.without_item(item)` helper to generate new state
**State Management** ([wardrobe_helper.rb:68-90](app/helpers/wardrobe_helper.rb#L68-L90))
**State Management** ([outfits_helper.rb:68-90](app/helpers/outfits_helper.rb#L68-L90))
- All state lives in URL params (no client-side state)
- `outfit_state_params` helper generates hidden fields for outfit state
- Preserves: species, color, worn items (`objects[]`), search query (`q[...]`)
@ -80,11 +80,11 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
#### Supporting Helpers
**`outfit_items_by_zone`** ([wardrobe_helper.rb:96-167](app/helpers/wardrobe_helper.rb#L96-L167))
**`outfit_items_by_zone`** ([outfits_helper.rb:96-167](app/helpers/outfits_helper.rb#L96-L167))
- Core grouping logic for items by zone
- Matches wardrobe-2020's `getZonesAndItems` behavior
- Returns array of `{zone_id:, zone_label:, items:}` hashes
- Extensively tested in [wardrobe_helper_spec.rb](spec/helpers/wardrobe_helper_spec.rb)
- Extensively tested in [outfits_helper_spec.rb](spec/helpers/outfits_helper_spec.rb)
**`outfit_viewer`** ([outfits_helper.rb:86-89](app/helpers/outfits_helper.rb#L86-L89))
- Renders `<outfit-viewer>` web component
@ -969,13 +969,12 @@ This section documents ALL features in the React-based Wardrobe 2020 for referen
- [Customization Architecture](./customization-architecture.md) - Data model deep dive
**Wardrobe V2 Files:**
- Controller: [app/controllers/wardrobe_controller.rb](../app/controllers/wardrobe_controller.rb)
- View: [app/views/wardrobe/show.html.haml](../app/views/wardrobe/show.html.haml)
- Search results partial: [app/views/wardrobe/_search_results.html.haml](../app/views/wardrobe/_search_results.html.haml)
- Helpers: [app/helpers/wardrobe_helper.rb](../app/helpers/wardrobe_helper.rb)
- Tests: [spec/helpers/wardrobe_helper_spec.rb](../spec/helpers/wardrobe_helper_spec.rb)
- Styles: [app/assets/stylesheets/wardrobe/show.css](../app/assets/stylesheets/wardrobe/show.css)
- JavaScript: [app/assets/javascripts/wardrobe/show.js](../app/assets/javascripts/wardrobe/show.js)
- Controller: [app/controllers/outfits_controller.rb](../app/controllers/outfits_controller.rb)
- View: [app/views/outfits/new_v2.html.haml](../app/views/outfits/new_v2.html.haml)
- Helpers: [app/helpers/outfits_helper.rb](../app/helpers/outfits_helper.rb)
- Tests: [spec/helpers/outfits_helper_spec.rb](../spec/helpers/outfits_helper_spec.rb)
- Styles: [app/assets/stylesheets/outfits/new_v2.css](../app/assets/stylesheets/outfits/new_v2.css)
- JavaScript: [app/assets/javascripts/outfits/new_v2.js](../app/assets/javascripts/outfits/new_v2.js)
- Web Components:
- [app/assets/javascripts/species-color-picker.js](../app/assets/javascripts/species-color-picker.js)
- [app/assets/javascripts/outfit-viewer.js](../app/assets/javascripts/outfit-viewer.js)

View file

@ -35,35 +35,12 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
min-height: 100dvh;
min-height: 100vh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
#error-description {
fill: #d30001;
}
#error-id {
fill: #f0eff0;
}
@media (prefers-color-scheme: dark) {
body {
background: #101010;
color: #e0e0e0;
}
#error-description {
fill: #FF6161;
}
#error-id {
fill: #2c2c2c;
}
}
a {
color: inherit;
font-weight: 700;
@ -106,11 +83,13 @@
}
main article br {
display: none;
@media(min-width: 48em) {
display: inline;
}
}
</style>
@ -123,10 +102,10 @@
<main>
<header>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" id="error-id"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" id="error-description"/></svg>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" fill="#f0eff0"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" fill="#d30001"/></svg>
</header>
<article>
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you're the application owner check the logs for more information.</p>
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If youre the application owner check the logs for more information.</p>
</article>
</main>

View file

@ -35,35 +35,12 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
min-height: 100dvh;
min-height: 100vh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
#error-description {
fill: #d30001;
}
#error-id {
fill: #f0eff0;
}
@media (prefers-color-scheme: dark) {
body {
background: #101010;
color: #e0e0e0;
}
#error-description {
fill: #FF6161;
}
#error-id {
fill: #2c2c2c;
}
}
a {
color: inherit;
font-weight: 700;
@ -106,11 +83,13 @@
}
main article br {
display: none;
@media(min-width: 48em) {
display: inline;
}
}
</style>
@ -123,7 +102,7 @@
<main>
<header>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" id="error-id"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" id="error-description"/></svg>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" fill="#f0eff0"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" fill="#d30001"/></svg>
</header>
<article>
<p><strong>Your browser is not supported.</strong><br> Please upgrade your browser to continue.</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
require_relative '../rails_helper'
RSpec.describe WardrobeHelper, type: :helper do
RSpec.describe OutfitsHelper, type: :helper do
fixtures :zones, :colors, :species, :pet_types
# Use the Blue Acara's body_id throughout tests

Binary file not shown.

BIN
vendor/cache/actioncable-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/actionmailbox-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/actionmailer-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/actionpack-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/actiontext-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/actionview-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/activejob-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/activemodel-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/activerecord-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/activestorage-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/activesupport-8.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/addressable-2.8.7.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/async-2.27.0.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/async-container-0.24.0.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/async-http-cache-0.4.5.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/async-pool-0.11.0.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/async-service-0.13.0.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/babel-source-5.8.35.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/babel-transpiler-0.7.0.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/benchmark-0.4.1.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/bigdecimal-3.2.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/bootsnap-1.18.6.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/concurrent-ruby-1.3.5.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/connection_pool-2.5.3.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/console-1.32.0.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/crack-1.0.0.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/date-3.4.1.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/erb-5.0.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/faraday-2.13.4.gem vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/faraday-net_http-3.4.1.gem vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/globalid-1.2.1.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/haml-6.3.0.gem vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/hashdiff-1.2.0.gem vendored Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more