From 2255de94a7b9a7eae4c8e094d07ec887b78422e1 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 24 Sep 2026 08:38:31 +0200 Subject: [PATCH] Add a Prismana-only filter with a rainbow star icon --- DESIGN_SYSTEM.md | 1 + e2e/aniimo.spec.js | 32 +++++++++++++++++++ tests/aniimo.test.mjs | 13 ++++++++ tests/helpers/data-validation.mjs | 1 + website/public/data/site.json | 1 + website/public/static/icons/prismana.svg | 13 ++++++++ .../features/games/aniimo/AniimoFilters.jsx | 5 +++ website/src/features/games/aniimo/utils.js | 3 +- website/src/styles/_aniimo.scss | 1 + 9 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 website/public/static/icons/prismana.svg diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index 5ba82b2..27605ca 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -816,6 +816,7 @@ Date picker et menus compacts : - le verso occupe exactement la même surface que le recto ; garder son en-tête fixe et faire défiler les détails dans une zone NDS accessible au clavier ; masquer la face inactive aux technologies d'assistance ; - réduire l'animation selon `prefers-reduced-motion` ; - les filtres se cumulent par famille et sont évalués sur chaque variante avant regroupement ; +- la chip « Prismana uniquement », sous le nom, garde 12 px de marge inférieure, utilise une étoile SVG arc-en-ciel et limite les résultats à `prismana_form` avant regroupement ; inactive par défaut, elle se cumule avec tous les autres critères et se désactive à la réinitialisation ; - les catégories Rôles, Éléments et Compétences de logis partagent un défilement unique via `filter-options`, comme MHW ; des séparateurs discrets délimitent les catégories et leurs en-têtes permettent de les replier sans désactiver leurs sélections ; - rôles et éléments sont sur deux colonnes de chips `sm` ; les compétences élémentaires précèdent les autres sans séparation interne, avec icône, texte, curseur et valeur sur une ligne ; un seuil à 0 reste visible mais ne participe pas aux opérations ET/OU ; - garder le nombre de critères sélectionnés visible dans l'en-tête, même replié. diff --git a/e2e/aniimo.spec.js b/e2e/aniimo.spec.js index 8e093b9..9bc515f 100644 --- a/e2e/aniimo.spec.js +++ b/e2e/aniimo.spec.js @@ -5,6 +5,38 @@ const route = "/#/games/aniimo/aniimos"; const cards = (page) => page.locator(".aniimo-result"); const section = (page, name) => page.getByRole("group", { name, exact: true }).filter({ has: page.locator("legend") }); +for (const width of [1280, 390]) { + test(`Prismana filter combines with search and survives grouping and navigation at ${width}px`, async ({ page }) => { + test.setTimeout(45000); + await page.setViewportSize({ width, height: 900 }); + await page.goto(route); + const prismana = page.getByRole("checkbox", { name: "Prismana uniquement", exact: true }); + await expect(prismana).toBeVisible({ timeout: 30000 }); + await expect(prismana).not.toBeChecked(); + await prismana.focus(); + await prismana.press("Space"); + await expect(cards(page)).toHaveCount(27); + await expect(page.locator('.aniimo-result:not([data-form="prismana_form"])')).toHaveCount(0); + await expect(page.getByRole("tablist")).toHaveCount(0); + expect(await page.locator('img[src="/static/icons/prismana.svg"]').evaluate((image) => image.complete && image.naturalWidth > 0)).toBe(true); + await page.getByRole("searchbox").fill("iris"); + await expect(cards(page)).toHaveCount(3); + await page.locator('.game-breadcrumb a[href="#/games/aniimo"]').click(); + await page.locator('a[href="#/games/aniimo/aniimos"]').click(); + await expect(prismana).toBeChecked(); + await expect(cards(page)).toHaveCount(3); + await page.getByRole("button", { name: "Regrouper les variantes", exact: true }).click(); + await expect(cards(page)).toHaveCount(3); + await page.getByRole("searchbox").fill("emberpup"); + await expect(page.getByRole("heading", { name: "Aucun résultat" })).toBeVisible(); + await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click(); + await expect(prismana).not.toBeChecked(); + await expect(cards(page)).toHaveCount(208); + await expect(page.getByRole("button", { name: "Regrouper les variantes", exact: true })).toHaveAttribute("aria-pressed", "false"); + expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth <= globalThis.innerWidth)).toBe(true); + }); +} + test("Aniimo matches MHW card motion, surfaces and filter controls", async ({ page }) => { test.setTimeout(45000); async function styles(locator, properties) { diff --git a/tests/aniimo.test.mjs b/tests/aniimo.test.mjs index 8ad368d..a90f4b9 100644 --- a/tests/aniimo.test.mjs +++ b/tests/aniimo.test.mjs @@ -54,6 +54,19 @@ test("name searches accept French, English, accents and case", () => { assert.deepEqual(filter({ name: "inexistant-xyz" }), []); }); +test("Prismana-only filtering combines with other criteria before grouping", () => { + assert.equal(createAniimoFilters().prismanaOnly, false); + const expected = data.variants.filter((variant) => variant.form === "prismana_form"); + assert.ok(expected.length > 0); + assert.deepEqual(filter({ prismanaOnly: true }), expected); + const variant = expected[0]; + const criteria = { name: variant.name, elements: variant.elements, roles: [variant.role], abilities: Object.fromEntries(variant.homeland_abilities.map(({ type, level }) => [type, level])) }; + assert.deepEqual(filter({ ...criteria, prismanaOnly: true }), [variant]); + assert.equal(groupAniimoVariants(expected).length, expected.length); + assert.deepEqual(filter({ prismanaOnly: true, name: "emberpup" }), []); + assert.equal(filter({ prismanaOnly: false }).length, data.variants.length); +}); + test("Prismana remains the final variation in data and grouped results", () => { for (const group of groupAniimoVariants(data.variants)) { if (group.some((variant) => variant.form === "prismana_form")) assert.equal(group.at(-1).form, "prismana_form"); diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 2899047..36e0e44 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -112,6 +112,7 @@ export function validateSiteContent(site) { "aniimo.filters", "aniimo.name", "aniimo.searchPlaceholder", + "aniimo.prismanaOnly", "aniimo.elements", "aniimo.roles", "aniimo.role", diff --git a/website/public/data/site.json b/website/public/data/site.json index 4132657..bb2737c 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -2740,6 +2740,7 @@ "filters": "Filtres", "name": "Nom", "searchPlaceholder": "Nom français ou anglais…", + "prismanaOnly": "Prismana uniquement", "elements": "Éléments", "roles": "Rôles", "role": "Rôle", diff --git a/website/public/static/icons/prismana.svg b/website/public/static/icons/prismana.svg new file mode 100644 index 0000000..e7a18b1 --- /dev/null +++ b/website/public/static/icons/prismana.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/website/src/features/games/aniimo/AniimoFilters.jsx b/website/src/features/games/aniimo/AniimoFilters.jsx index d68451f..7ac4206 100644 --- a/website/src/features/games/aniimo/AniimoFilters.jsx +++ b/website/src/features/games/aniimo/AniimoFilters.jsx @@ -31,6 +31,11 @@ export function AniimoFilters({ filters, onChange, options, t, content }) { {content.name} onChange({ ...filters, name: event.target.value })} /> +
{["roles", "elements", "abilities"].map((key) => (
diff --git a/website/src/features/games/aniimo/utils.js b/website/src/features/games/aniimo/utils.js index cc45d8c..311ccf4 100644 --- a/website/src/features/games/aniimo/utils.js +++ b/website/src/features/games/aniimo/utils.js @@ -1,6 +1,6 @@ // Rôle : prépare les variantes Aniimo et applique les critères avant leur regroupement. export function createAniimoFilters() { - return { name: "", elements: [], roles: [], abilities: {}, elementsLogic: "and", abilitiesLogic: "and", grouped: true }; + return { name: "", prismanaOnly: false, elements: [], roles: [], abilities: {}, elementsLogic: "and", abilitiesLogic: "and", grouped: true }; } export const ANIIMO_STATS = ["hp", "atk", "p_def", "m_def", "break", "regen"]; @@ -51,6 +51,7 @@ export function filterAniimoVariants(variants, filters) { const abilities = Object.entries(filters.abilities); return variants.filter((variant) => ( (!search || variant.searchNames.some((name) => name.includes(search))) + && (!filters.prismanaOnly || variant.form === "prismana_form") && matchesSelection(filters.elements, filters.elementsLogic, (element) => variant.elements.includes(element)) && (!filters.roles.length || filters.roles.includes(variant.role)) && matchesSelection(abilities, filters.abilitiesLogic, ([type, minimum]) => variant.homeland_abilities.some((ability) => ability.type === type && ability.level >= minimum)) diff --git a/website/src/styles/_aniimo.scss b/website/src/styles/_aniimo.scss index ea6228a..437afda 100644 --- a/website/src/styles/_aniimo.scss +++ b/website/src/styles/_aniimo.scss @@ -4,6 +4,7 @@ .aniimo-home-card img { padding: 0 15%; object-fit: cover; object-position: 50% 38%; } .aniimo-layout .game-results { min-width: 0; } .aniimo-filters { min-width: 0; } +.aniimo-filters > .filter-chip { margin-bottom: var(--space-3); } .aniimo-filter-sections { gap: 16px; } .aniimo-filter-section-surface + .aniimo-filter-section-surface { padding-top: 12px; border-top: 1px solid var(--color-border); } .aniimo-filter-section {