Compare commits

...

18 commits

Author SHA1 Message Date
812e8226bb [WV2] Unify button styles 2025-12-26 23:01:40 -08:00
955aeb984e [WV2] Simplify item search layout 2025-12-26 22:43:17 -08:00
74386b45d7 [WV2] More advanced mobile layout 2025-12-26 22:32:20 -08:00
ba0612b694 [WV2] Fix controls area width on mobile 2025-12-26 22:28:24 -08:00
b36b1577b5 [WV2] Fix cursor for outfit viewer play/pause 2025-12-26 22:19:06 -08:00
fb8fb4b27e [WV2] Fix anchor positioning for pose picker popover 2025-12-26 22:15:57 -08:00
02a64ef639 Merge branch 'main' into feature/wardrobe-v2 2025-12-26 21:23:33 -08:00
18a7e8fd9e Lock gem versions for all relevant platforms
We run DTI on a few different architectures in practice, and the `vendor/cache` directory can be a bit confusing to manage when switching dev machines.

In this change, we add all our common dev machine platforms to the Gemfile.lock, so precompiled gems for all of them are cached, granting us resilience against the possibility of Rubygems going down, and speeding up deploys & installation.
2025-12-26 21:21:35 -08:00
9c4a0cd7a3 Remove unused react-rails gem
The connection_pool gem changed their API, which caused a breakage in our react-rails gem.

It turns out though, we're not actually using react-rails anymore. It's primarily for React server-side rendering, which we don't do. Our React code is bundled as normal Javascript via our usual asset pipeline.

So, to resolve the gem incompatibility, we remove react-rails altogether. Neat!
2025-12-26 20:58:14 -08:00
d23f16c217 Merge branch 'main' into feature/wardrobe-v2 2025-12-26 20:44:57 -08:00
63f8768cc3 bundle update 2025-12-26 20:42:18 -08:00
7459037c8a [WV2] Simplify item search pagination
I'll want to do it smarter than this, but for now, just getting rid of the page links altogether seems best
2025-11-26 16:58:38 -08:00
6eace54c34 [WV2] Pose picker popover 2025-11-11 18:07:06 -08:00
76496f8a6d [WV2] Pose picker first draft 2025-11-11 17:41:57 -08:00
78931ddb47 [WV2] Move to a new WardrobeController 2025-11-11 17:21:03 -08:00
811bb3e036 Merge branch 'main' into feature/wardrobe-v2 2025-11-11 12:57:25 -08:00
efd92f6367 Upgrade to Rails 8.1 2025-11-11 12:38:25 -08:00
b257de85f2 Allow more flexible development DB config 2025-11-11 12:20:25 -08:00
236 changed files with 1841 additions and 1122 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,31 @@
/**
* 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,6 +73,7 @@ outfit-viewer
border-radius: 100% border-radius: 100%
border: 2px solid transparent border: 2px solid transparent
transition: all .25s transition: all .25s
cursor: pointer
.playing-label, .paused-label .playing-label, .paused-label
display: none display: none

View file

@ -1,545 +0,0 @@
@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

@ -0,0 +1,713 @@
@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,53 +77,6 @@ class OutfitsController < ApplicationController
@campaign = Fundraising::Campaign.current rescue nil @campaign = Fundraising::Campaign.current rescue nil
end 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 def show
@outfit = Outfit.find(params[:id]) @outfit = Outfit.find(params[:id])
@ -176,51 +129,5 @@ class OutfitsController < ApplicationController
:status => :bad_request :status => :bad_request
end 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 end

View file

@ -0,0 +1,144 @@
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,30 +65,6 @@ module OutfitsHelper
text_field_tag 'name', nil, options text_field_tag 'name', nil, options
end 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(...) def outfit_viewer(...)
render partial: "outfit_viewer", render partial: "outfit_viewer",
locals: parse_outfit_viewer_options(...) locals: parse_outfit_viewer_options(...)
@ -99,133 +75,8 @@ module OutfitsHelper
locals: parse_outfit_viewer_options(...) locals: parse_outfit_viewer_options(...)
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 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( def parse_outfit_viewer_options(
outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options
) )

View file

@ -0,0 +1,167 @@
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,71 +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 "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

@ -0,0 +1,21 @@
-# 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,11 +1,6 @@
.search-results .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? - if @search_results.any?
= will_paginate @search_results, param_name: "q[page]", params: { q: params[:q], species: @outfit.species_id, color: @outfit.color_id, objects: params[:objects] } = 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] }
%ul.search-results-list %ul.search-results-list
- @search_results.each do |item| - @search_results.each do |item|

View file

@ -0,0 +1,119 @@
- 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 Executable file
View file

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

8
bin/rubocop Executable file
View file

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

View file

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

21
config/ci.rb Normal file
View file

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

View file

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

View file

@ -18,9 +18,6 @@ Rails.application.configure do
# Cache assets for far-future expiry since they are all digest stamped. # 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}" } 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. # Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com" # config.asset_host = "http://assets.example.com"
@ -37,9 +34,7 @@ Rails.application.configure do
config.log_tags = [ :request_id ] config.log_tags = [ :request_id ]
config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) config.logger = ActiveSupport::TaggedLogging.logger(STDOUT)
config.react.variant = :production # Change to "debug" to log everything (including potentially personally-identifiable information!).
# Change to "debug" to log everything (including potentially personally-identifiable information!)
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
# Prevent health checks from clogging up the logs. # Prevent health checks from clogging up the logs.

View file

@ -20,6 +20,10 @@
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src style-src) # 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. # # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true # # config.content_security_policy_report_only = true
# end # end

View file

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

View file

@ -35,12 +35,35 @@
font-weight: 400; font-weight: 400;
letter-spacing: -0.0025em; letter-spacing: -0.0025em;
line-height: 1.4; line-height: 1.4;
min-height: 100vh; min-height: 100dvh;
place-items: center; place-items: center;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%; -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 { a {
color: inherit; color: inherit;
font-weight: 700; font-weight: 700;
@ -83,13 +106,11 @@
} }
main article br { main article br {
display: none; display: none;
@media(min-width: 48em) { @media(min-width: 48em) {
display: inline; display: inline;
} }
} }
</style> </style>
@ -102,10 +123,10 @@
<main> <main>
<header> <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" 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> <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>
</header> </header>
<article> <article>
<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> <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>
</article> </article>
</main> </main>

View file

@ -35,12 +35,35 @@
font-weight: 400; font-weight: 400;
letter-spacing: -0.0025em; letter-spacing: -0.0025em;
line-height: 1.4; line-height: 1.4;
min-height: 100vh; min-height: 100dvh;
place-items: center; place-items: center;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%; -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 { a {
color: inherit; color: inherit;
font-weight: 700; font-weight: 700;
@ -83,13 +106,11 @@
} }
main article br { main article br {
display: none; display: none;
@media(min-width: 48em) { @media(min-width: 48em) {
display: inline; display: inline;
} }
} }
</style> </style>
@ -102,7 +123,7 @@
<main> <main>
<header> <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" 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> <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>
</header> </header>
<article> <article>
<p><strong>Your browser is not supported.</strong><br> Please upgrade your browser to continue.</p> <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' require_relative '../rails_helper'
RSpec.describe OutfitsHelper, type: :helper do RSpec.describe WardrobeHelper, type: :helper do
fixtures :zones, :colors, :species, :pet_types fixtures :zones, :colors, :species, :pet_types
# Use the Blue Acara's body_id throughout tests # Use the Blue Acara's body_id throughout tests

BIN
vendor/cache/action_text-trix-2.1.15.gem vendored Normal file

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

BIN
vendor/cache/faraday-2.14.0.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.2.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/ffi-1.17.2.gem vendored Normal file

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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