Improve pet manager filters

This commit is contained in:
Alvin Unreal 2026-05-13 15:39:57 +02:00
parent 534f3cf3bb
commit ddee2628bc
4 changed files with 127 additions and 46 deletions

View file

@ -38,6 +38,8 @@ let activePetManagerSelection = "";
let activePetManagerFilter = "all";
let activePetManagerItems = [];
let activePetManagerDefaultId = "";
let petGalleryInstance = 0;
const remoteCatalogFilters = new Set(["original", "western", "asian"]);
contextBridge.exposeInMainWorld("openPets", api);
contextBridge.exposeInMainWorld("openpetsAgentSetup", agentSetupApi);
@ -649,6 +651,7 @@ async function renderPetManager(state) {
}
function renderPetGallery(catalogState, codexState, state, defaultPetId) {
const instance = ++petGalleryInstance;
const status = requireElement("catalog-status");
const search = requireInput("catalog-search");
const grid = requireElement("catalog-pets");
@ -671,8 +674,7 @@ function renderPetGallery(catalogState, codexState, state, defaultPetId) {
const catalogPageCount = catalogState.pageCount || 1;
let catalogSearchState = null;
let remoteResultLimit = 100;
let activeRemoteResultIds = null;
let activeRemoteHasMore = false;
let renderGeneration = 0;
let pets = createPetManagerItems({ ...catalogState, pets: catalogPets }, codexState, state, defaultPetId, defaultThumbnailSrc);
activePetManagerItems = pets;
activePetManagerDefaultId = defaultPetId;
@ -680,18 +682,33 @@ function renderPetGallery(catalogState, codexState, state, defaultPetId) {
activePetManagerSelection = defaultPetId || pets[0]?.id || "";
}
const resetPetGalleryViewport = () => {
grid.scrollTop = 0;
document.querySelector(".pm-gallery-pane")?.scrollTo?.({ top: 0, behavior: "instant" });
};
const isSupportedFilter = (filterName) => {
if ((filterName === "western" || filterName === "asian") && !catalogState.supportsCategories) return false;
if (filterName === "original" && typeof catalogState.originalsCount !== "number") return false;
return true;
};
for (const filter of document.querySelectorAll("[data-pet-filter]")) {
if ((filter.dataset.petFilter === "western" || filter.dataset.petFilter === "asian") && !catalogState.supportsCategories) {
const filterName = filter.dataset.petFilter || "all";
if (!isSupportedFilter(filterName)) {
filter.hidden = true;
if (activePetManagerFilter === filter.dataset.petFilter) activePetManagerFilter = "all";
if (activePetManagerFilter === filterName) activePetManagerFilter = "all";
} else {
filter.hidden = false;
}
filter.classList.toggle("active", filter.dataset.petFilter === activePetManagerFilter);
filter.setAttribute("aria-pressed", filter.dataset.petFilter === activePetManagerFilter ? "true" : "false");
filter.onclick = () => {
activePetManagerFilter = filter.dataset.petFilter || "all";
const nextFilter = filter.dataset.petFilter || "all";
if (nextFilter === activePetManagerFilter) return;
activePetManagerFilter = nextFilter;
remoteResultLimit = 100;
resetPetGalleryViewport();
void render();
};
}
@ -717,46 +734,56 @@ function renderPetGallery(catalogState, codexState, state, defaultPetId) {
return false;
};
const shouldUseRemoteResults = (query) => catalogState.version === 3 && (Boolean(query) || activePetManagerFilter === "western" || activePetManagerFilter === "asian");
const shouldUseRemoteResults = (filterName, query) => catalogState.version === 3 && ((filterName === "all" && Boolean(query)) || remoteCatalogFilters.has(filterName));
const ensureRemoteResultsLoaded = async (query) => {
activeRemoteResultIds = null;
activeRemoteHasMore = false;
if (!shouldUseRemoteResults(query)) return;
const getRemoteResults = async (filterName, query) => {
if (!shouldUseRemoteResults(filterName, query)) return { ids: null, hasMore: false };
catalogSearchState ||= await api.getCatalogSearch();
if (!isCatalogSearchUiState(catalogSearchState) || catalogSearchState.source === "error") throw new Error(catalogSearchState?.error || "Catalog search unavailable.");
const matches = catalogSearchState.pets.filter((pet) => {
if (activePetManagerFilter !== "all" && activePetManagerFilter !== "installed" && activePetManagerFilter !== "codex" && pet.category !== activePetManagerFilter) return false;
if (filterName === "western" || filterName === "asian") {
if (pet.category !== filterName) return false;
} else if (filterName === "original" && !pet.original) {
return false;
}
return !query || pet.searchText.includes(query);
});
const visibleMatches = matches.slice(0, remoteResultLimit);
activeRemoteResultIds = new Set(visibleMatches.map((pet) => pet.id));
activeRemoteHasMore = matches.length > visibleMatches.length;
const pages = new Set(visibleMatches.map((pet) => pet.catalogPage));
await Promise.all([...pages].map((page) => loadCatalogPage(page)));
return { ids: new Set(visibleMatches.map((pet) => pet.id)), hasMore: matches.length > visibleMatches.length };
};
const render = async () => {
const generation = ++renderGeneration;
const filterName = activePetManagerFilter;
const query = search.value.trim().toLowerCase();
const isStale = () => instance !== petGalleryInstance || generation !== renderGeneration || filterName !== activePetManagerFilter || query !== search.value.trim().toLowerCase();
for (const filter of document.querySelectorAll("[data-pet-filter]")) {
filter.classList.toggle("active", filter.dataset.petFilter === activePetManagerFilter);
filter.setAttribute("aria-pressed", filter.dataset.petFilter === activePetManagerFilter ? "true" : "false");
filter.classList.toggle("active", filter.dataset.petFilter === filterName);
filter.setAttribute("aria-pressed", filter.dataset.petFilter === filterName ? "true" : "false");
}
const query = search.value.trim().toLowerCase();
if (shouldUseRemoteResults(query)) {
try {
await ensureRemoteResultsLoaded(query);
} catch (error) {
renderCaughtError(error);
}
let remoteResults = { ids: null, hasMore: false };
let remoteError = null;
try {
remoteResults = await getRemoteResults(filterName, query);
} catch (error) {
remoteError = error;
}
if (isStale()) {
return;
}
if (remoteError) renderCaughtError(remoteError);
const visiblePets = pets.filter((pet) => {
if (activePetManagerFilter === "installed" && !pet.installed) return false;
if (activePetManagerFilter === "codex" && !pet.codexPet && !pet.codexImported) return false;
if ((activePetManagerFilter === "western" || activePetManagerFilter === "asian") && pet.category !== activePetManagerFilter) return false;
if (filterName === "installed" && !pet.installed) return false;
if (filterName === "codex" && !pet.codexPet && !pet.codexImported) return false;
if (filterName === "original" && !pet.original) return false;
if ((filterName === "western" || filterName === "asian") && pet.category !== filterName) return false;
const haystack = `${pet.id} ${pet.displayName} ${pet.description}`.toLowerCase();
if (activeRemoteResultIds) {
const remoteMatch = Boolean(pet.catalogPet && activeRemoteResultIds.has(pet.id));
if (remoteResults.ids) {
const remoteMatch = Boolean(pet.catalogPet && remoteResults.ids.has(pet.id));
return query ? remoteMatch || haystack.includes(query) : remoteMatch;
}
return haystack.includes(query);
@ -771,12 +798,12 @@ function renderPetGallery(catalogState, codexState, state, defaultPetId) {
if (visiblePets.length === 0) {
const empty = document.createElement("div");
empty.className = "pm-empty-state";
empty.textContent = activePetManagerFilter === "installed" ? "No installed pets match your search." : activePetManagerFilter === "codex" ? "No Codex pets match your search." : activePetManagerFilter === "western" || activePetManagerFilter === "asian" ? `No ${activePetManagerFilter} pets match your search.` : "No pets match your search.";
empty.textContent = createEmptyPetGalleryMessage(filterName);
grid.append(empty);
}
const hasMoreCatalogPages = catalogState.version === 3 && !query && !shouldUseRemoteResults(query) && loadedCatalogPages.size < catalogPageCount;
const hasMoreRemoteResults = activeRemoteHasMore;
const hasMoreCatalogPages = filterName === "all" && catalogState.version === 3 && !query && loadedCatalogPages.size < catalogPageCount;
const hasMoreRemoteResults = remoteResults.hasMore;
if (hasMoreCatalogPages || hasMoreRemoteResults) {
const loadMore = document.createElement("button");
loadMore.className = "pm-load-more";
@ -812,6 +839,7 @@ function renderPetGallery(catalogState, codexState, state, defaultPetId) {
search.oninput = () => {
remoteResultLimit = 100;
resetPetGalleryViewport();
void render();
};
void render();
@ -856,6 +884,8 @@ function createPetManagerItem(id, displayName, description, installed, catalogPe
displayName,
description,
category: catalogPet?.category || "",
original: Boolean(catalogPet?.original),
featured: Boolean(catalogPet?.featured),
installed,
catalogPet,
codexPet,
@ -871,6 +901,15 @@ function createPetManagerItem(id, displayName, description, installed, catalogPe
};
}
function createEmptyPetGalleryMessage(filterName) {
if (filterName === "installed") return "No installed pets match your search.";
if (filterName === "codex") return "No Codex pets match your search.";
if (filterName === "original") return "No OpenPets originals match your search.";
if (filterName === "western") return "No Western pets match your search.";
if (filterName === "asian") return "No Asian pets match your search.";
return "No pets match your search.";
}
function createPetGalleryCard(pet, defaultPetId, onSelect) {
const card = document.createElement("article");
card.className = pet.id === activePetManagerSelection ? "pm-pet-card active" : "pm-pet-card";

View file

@ -13,6 +13,8 @@ export interface CatalogPetV2 {
readonly spritesheet?: string;
readonly category?: "western" | "asian";
readonly subcategory?: string;
readonly original?: boolean;
readonly featured?: boolean;
}
export interface CatalogV3Index {
@ -23,6 +25,8 @@ export interface CatalogV3Index {
readonly search: string;
readonly filters: {
readonly categories: readonly CatalogV3Category[];
readonly originalsCount?: number;
readonly featuredCount?: number;
};
readonly pages: readonly string[];
}
@ -61,6 +65,8 @@ export interface CatalogV3SearchPet {
readonly searchText: string;
readonly category: "western" | "asian";
readonly catalogPage: number;
readonly original?: boolean;
readonly featured?: boolean;
}
export interface CatalogPetV3 {
@ -72,6 +78,8 @@ export interface CatalogPetV3 {
readonly zip: string;
readonly category: "western" | "asian";
readonly subcategory?: string;
readonly original?: boolean;
readonly featured?: boolean;
}
export function validateCatalogV2(value: unknown): CatalogV2 {
@ -111,7 +119,11 @@ export function validateCatalogV3Index(value: unknown): CatalogV3Index {
total,
pageSize,
search,
filters: { categories },
filters: {
categories,
...(value.filters.originalsCount === undefined ? {} : { originalsCount: validateCount(value.filters.originalsCount) }),
...(value.filters.featuredCount === undefined ? {} : { featuredCount: validateCount(value.filters.featuredCount) }),
},
pages,
};
}
@ -198,8 +210,8 @@ function validateCatalogV3Pet(value: unknown, ids: Set<string>): CatalogPetV3 {
zip: validateCatalogUrl(value.zip, "zip"),
category,
};
if (value.subcategory !== undefined) return { ...entry, subcategory: validateString(value.subcategory, "subcategory", 80) };
return entry;
const withSubcategory = value.subcategory === undefined ? entry : { ...entry, subcategory: validateString(value.subcategory, "subcategory", 80) };
return withCatalogMeta(withSubcategory, value);
}
function validateCatalogV3Category(value: unknown): CatalogV3Category {
@ -214,15 +226,28 @@ function validateCatalogV3Category(value: unknown): CatalogV3Category {
function validateCatalogV3SearchPet(value: unknown, catalogPageCount: number): CatalogV3SearchPet {
if (!isRecord(value)) throw new Error("Catalog v3 search pet must be an object.");
const catalogPage = validateInteger(value.catalogPage, "Catalog v3 search catalogPage", 0, Math.max(0, catalogPageCount - 1));
return {
return withCatalogMeta({
id: validateId(value.id),
displayName: validateString(value.displayName, "displayName", 120),
searchText: validateString(value.searchText, "searchText", 400),
category: validateCategory(value.category),
catalogPage,
}, value);
}
function withCatalogMeta<T extends object>(entry: T, value: Record<string, unknown>): T & { readonly original?: boolean; readonly featured?: boolean } {
return {
...entry,
...(value.original === undefined ? {} : { original: validateBoolean(value.original, "original") }),
...(value.featured === undefined ? {} : { featured: validateBoolean(value.featured, "featured") }),
};
}
function validateBoolean(value: unknown, field: string): boolean {
if (typeof value !== "boolean") throw new Error(`Catalog pet ${field} must be a boolean.`);
return value;
}
function validateCategory(value: unknown): "western" | "asian" {
if (value === "western" || value === "asian") return value;
throw new Error(`Invalid catalog category: ${String(value)}`);

View file

@ -23,6 +23,8 @@ export interface CatalogUiState {
readonly page?: number;
readonly pageCount?: number;
readonly supportsCategories?: boolean;
readonly originalsCount?: number;
readonly featuredCount?: number;
}
export interface CatalogSearchUiState {
@ -53,6 +55,8 @@ export async function getCatalogUiState(): Promise<CatalogUiState> {
page: 0,
pageCount: remoteV3.index.pages.length,
supportsCategories: true,
originalsCount: remoteV3.index.filters.originalsCount,
featuredCount: remoteV3.index.filters.featuredCount,
};
}
@ -77,6 +81,8 @@ export async function getCatalogPageUiState(page: number): Promise<CatalogUiStat
page,
pageCount: remoteV3.index.pages.length,
supportsCategories: true,
originalsCount: remoteV3.index.filters.originalsCount,
featuredCount: remoteV3.index.filters.featuredCount,
};
}
@ -195,7 +201,7 @@ async function getRemoteCatalogV3Search(index: CatalogV3Index): Promise<readonly
return await v3SearchPromise;
}
function toCatalogPetV2Compat(pet: { readonly id: string; readonly displayName: string; readonly description: string; readonly thumbnail: string; readonly spritesheet: string; readonly zip: string; readonly category: "western" | "asian"; readonly subcategory?: string }): CatalogPetV2 {
function toCatalogPetV2Compat(pet: { readonly id: string; readonly displayName: string; readonly description: string; readonly thumbnail: string; readonly spritesheet: string; readonly zip: string; readonly category: "western" | "asian"; readonly subcategory?: string; readonly original?: boolean; readonly featured?: boolean }): CatalogPetV2 {
const entry: CatalogPetV2 = {
id: pet.id,
displayName: pet.displayName,
@ -205,8 +211,12 @@ function toCatalogPetV2Compat(pet: { readonly id: string; readonly displayName:
zip: pet.zip,
category: pet.category,
};
if (pet.subcategory) return { ...entry, subcategory: pet.subcategory };
return entry;
return {
...entry,
...(pet.subcategory ? { subcategory: pet.subcategory } : {}),
...(pet.original === undefined ? {} : { original: pet.original }),
...(pet.featured === undefined ? {} : { featured: pet.featured }),
};
}
async function tryLoadRemoteCatalog(): Promise<{ readonly ok: true; readonly catalog: CatalogV2 } | { readonly ok: false; readonly error: string }> {

View file

@ -380,20 +380,23 @@ function createPetManagerHtml(definition: TaskWindowDefinition): string {
<header class="pm-header">
<h1 id="pm-title">Install a Pet</h1>
<p class="lede">Pick a companion for your terminal.</p>
<span id="catalog-status" class="pm-status-pill">Loading…</span>
</header>
<label class="pm-search-wrap" for="catalog-search">
<span class="pm-search-icon" aria-hidden="true"></span>
<input id="catalog-search" type="search" placeholder="Search pets…" />
</label>
<div class="pm-filters" role="group" aria-label="Pet filters">
<div class="pm-filter-buttons">
<div class="pm-filter-buttons primary">
<button id="pm-filter-all" class="pm-filter active" type="button" data-pet-filter="all" aria-pressed="true">All</button>
<button id="pm-filter-western" class="pm-filter" type="button" data-pet-filter="western" aria-pressed="false">Western</button>
<button id="pm-filter-asian" class="pm-filter" type="button" data-pet-filter="asian" aria-pressed="false">Asian</button>
<button id="pm-filter-codex" class="pm-filter" type="button" data-pet-filter="codex" aria-pressed="false">Codex</button>
<button id="pm-filter-installed" class="pm-filter" type="button" data-pet-filter="installed" aria-pressed="false">Installed</button>
</div>
<span id="catalog-status" class="pm-status-pill">Loading…</span>
<div class="pm-filter-buttons secondary">
<button id="pm-filter-original" class="pm-filter" type="button" data-pet-filter="original" aria-pressed="false">OpenPets</button>
<button id="pm-filter-western" class="pm-filter" type="button" data-pet-filter="western" aria-pressed="false">Western</button>
<button id="pm-filter-asian" class="pm-filter" type="button" data-pet-filter="asian" aria-pressed="false">Asian</button>
</div>
</div>
<div id="catalog-pets" class="pm-pet-grid" aria-label="Pets"></div>
</section>
@ -982,10 +985,10 @@ function createTaskWindowStyles(): string {
body[data-openpets-view="pet-manager"] .pm-shell { width: min(1160px, calc(100vw - 36px)); height: calc(100vh - 28px); display: grid; grid-template-columns: minmax(330px, 0.78fr) minmax(510px, 1.22fr); gap: 32px; align-items: stretch; padding: 10px 0 18px; overflow: hidden; }
body[data-openpets-view="pet-manager"] .pm-gallery-pane { position: relative; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; padding-bottom: 28px; }
body[data-openpets-view="pet-manager"] .pm-logo { width: min(218px, 72%); flex: 0 0 auto; display: block; margin: -2px auto 0; filter: drop-shadow(0 10px 14px rgba(42, 80, 138, 0.12)); }
body[data-openpets-view="pet-manager"] .pm-header { flex: 0 0 auto; display: grid; align-items: center; row-gap: 0; margin-bottom: 6px; }
body[data-openpets-view="pet-manager"] .pm-header { flex: 0 0 auto; position: relative; display: grid; align-items: center; row-gap: 0; margin-bottom: 6px; padding-right: 108px; }
body[data-openpets-view="pet-manager"] .pm-header h1 { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 24px; line-height: 1.08; letter-spacing: -0.03em; color: #102149; text-shadow: 0 1px 0 rgba(255,255,255,0.9); }
body[data-openpets-view="pet-manager"] .pm-header .lede { grid-column: 1 / -1; margin: 0; color: #63708f; line-height: 1.25; }
body[data-openpets-view="pet-manager"] .pm-status-pill { position: absolute; right: 8px; bottom: 0; height: 22px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; border: 1px solid rgba(126, 161, 210, 0.28); border-radius: 8px; padding: 0 8px; color: #526483; background: rgba(255, 255, 255, 0.58); font-size: 9px; line-height: 1; font-weight: 850; font-variant-numeric: tabular-nums; white-space: nowrap; box-shadow: 0 8px 18px rgba(61, 99, 160, 0.05), inset 0 1px 0 rgba(255,255,255,0.85); }
body[data-openpets-view="pet-manager"] .pm-status-pill { position: absolute; right: 0; top: 2px; height: 22px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; border: 1px solid rgba(126, 161, 210, 0.28); border-radius: 8px; padding: 0 8px; color: #526483; background: rgba(255, 255, 255, 0.58); font-size: 9px; line-height: 1; font-weight: 850; font-variant-numeric: tabular-nums; white-space: nowrap; box-shadow: 0 8px 18px rgba(61, 99, 160, 0.05), inset 0 1px 0 rgba(255,255,255,0.85); }
body[data-openpets-view="pet-manager"] .pm-status-pill.success { color: #047857; background: rgba(236, 253, 245, 0.82); border-color: rgba(16, 185, 129, 0.26); }
body[data-openpets-view="pet-manager"] .pm-status-pill.error { color: #b91c1c; background: rgba(254, 242, 242, 0.86); border-color: rgba(248, 113, 113, 0.28); }
body[data-openpets-view="pet-manager"] .pm-search-wrap { height: 40px; flex: 0 0 auto; display: flex; align-items: center; gap: 11px; box-sizing: border-box; margin: 0 0 8px; padding: 0 13px; border: 1px solid rgba(126, 161, 210, 0.54); border-radius: 12px; background: rgba(255,255,255,0.82); box-shadow: inset 0 1px 0 rgba(255,255,255,0.92), 0 10px 24px rgba(61, 99, 160, 0.08); color: #526483; }
@ -994,10 +997,14 @@ function createTaskWindowStyles(): string {
body[data-openpets-view="pet-manager"] .pm-search-icon::after { content: ""; width: 7px; height: 2px; border-radius: 999px; background: #5d6e8e; position: absolute; right: -5px; bottom: -2px; transform: rotate(45deg); transform-origin: center; }
body[data-openpets-view="pet-manager"] #catalog-search { width: 100%; border: 0; outline: 0; background: transparent; color: #17284f; font: inherit; font-size: 15px; }
body[data-openpets-view="pet-manager"] #catalog-search::placeholder { color: #8290aa; }
body[data-openpets-view="pet-manager"] .pm-filters { flex: 0 0 auto; display: flex; align-items: center; justify-content: flex-start; gap: 10px; margin-bottom: 9px; }
body[data-openpets-view="pet-manager"] .pm-filter-buttons { display: flex; align-items: center; justify-content: flex-start; gap: 9px; min-width: 0; }
body[data-openpets-view="pet-manager"] .pm-filter { min-width: 64px; min-height: 30px; padding: 5px 12px; border: 1px solid rgba(126, 161, 210, 0.46); border-radius: 10px; background: rgba(255,255,255,0.76); color: #526483; font-weight: 850; box-shadow: 0 8px 20px rgba(61, 99, 160, 0.06); }
body[data-openpets-view="pet-manager"] .pm-filters { flex: 0 0 auto; position: relative; display: grid; gap: 7px; margin-bottom: 9px; }
body[data-openpets-view="pet-manager"] .pm-filter-buttons { display: grid; align-items: center; justify-content: stretch; gap: 8px; min-width: 0; }
body[data-openpets-view="pet-manager"] .pm-filter-buttons.primary { grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: 315px; }
body[data-openpets-view="pet-manager"] .pm-filter-buttons.secondary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
body[data-openpets-view="pet-manager"] .pm-filter { min-width: 0; min-height: 30px; padding: 5px 12px; border: 1px solid rgba(126, 161, 210, 0.46); border-radius: 10px; background: rgba(255,255,255,0.76); color: #526483; font-weight: 850; box-shadow: 0 8px 20px rgba(61, 99, 160, 0.06); display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; }
body[data-openpets-view="pet-manager"] .pm-filter[data-pet-filter="original"] { border-color: rgba(217, 119, 6, 0.36); background: linear-gradient(180deg, rgba(255, 251, 235, 0.96), rgba(254, 243, 199, 0.82)); color: #9a5a08; box-shadow: 0 9px 20px rgba(217, 119, 6, 0.09), inset 0 1px 0 rgba(255,255,255,0.9); }
body[data-openpets-view="pet-manager"] .pm-filter.active { border-color: rgba(29, 113, 255, 0.5); background: linear-gradient(180deg, #53a3ff, #176df2); color: #fff; box-shadow: 0 10px 22px rgba(37, 99, 235, 0.25), inset 0 1px 0 rgba(255,255,255,0.34); }
body[data-openpets-view="pet-manager"] .pm-filter[data-pet-filter="original"].active { border-color: rgba(180, 83, 9, 0.58); background: linear-gradient(180deg, #fbbf24, #d97706); color: #fff7ed; box-shadow: 0 12px 24px rgba(217, 119, 6, 0.24), inset 0 1px 0 rgba(255,255,255,0.34); }
body[data-openpets-view="pet-manager"] .pm-pet-grid { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 2px 8px 14px 2px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; scrollbar-color: rgba(96, 165, 250, 0.42) transparent; }
body[data-openpets-view="pet-manager"] .pm-pet-card { min-height: 164px; display: grid; grid-template-rows: 58px 40px 31px; gap: 7px; justify-items: center; box-sizing: border-box; padding: 11px 10px 12px; border: 1px solid rgba(126, 161, 210, 0.45); border-radius: 14px; background: rgba(255,255,255,0.74); color: #14264d; box-shadow: 0 12px 28px rgba(61, 99, 160, 0.1), inset 0 1px 0 rgba(255,255,255,0.92); transition: box-shadow 140ms ease, border-color 140ms ease, background-color 140ms ease; }
body[data-openpets-view="pet-manager"] .pm-pet-card:hover { border-color: rgba(37, 99, 235, 0.44); background: rgba(255,255,255,0.9); box-shadow: 0 13px 28px rgba(61, 99, 160, 0.13), inset 0 1px 0 rgba(255,255,255,0.94); }