Add Aniimo guides and link game cards to toolbox checklists
Some checks failed
Deploy Sokko G / deploy (push) Failing after 3s

This commit is contained in:
Shinuwa 2026-09-23 20:05:19 +02:00
parent f0797ccf14
commit 13434f72f6
278 changed files with 10025 additions and 57 deletions

View file

@ -0,0 +1,110 @@
// Rôle : affiche une carte Aniimo retournable et les onglets des variantes correspondantes.
import { useId, useLayoutEffect, useRef, useState } from "react";
import { GuideChecklistMark } from "../GuideChecklistControls.jsx";
import { Icon } from "../../../components/Icon.jsx";
import { Tooltip } from "../../../components/Tooltip.jsx";
import { ANIIMO_STATS, aniimoAsset } from "./utils.js";
function Attributes({ values, t }) {
return <span className="aniimo-attributes">{values.map((value) => <span className="nds-badge" data-variant="neutral" key={value} title={t(value)}><img src={aniimoAsset(value)} alt="" width="22" height="22" /><span>{t(value)}</span></span>)}</span>;
}
function shortForm(label) {
const words = label.split(/[\s’-]+/).filter((word) => !/^(forme|de|des|du|la|le|les|d)$/i.test(word));
// Trois lettres distinguent notamment les plateaux, prairies et prismana.
return (words.length === 1 ? words[0].slice(0, 3) : words.map((word) => word[0]).join("")).toLocaleUpperCase("fr");
}
function CardFaces({ variant, t, content, completed, completedLabel, statsMax }) {
const [flipped, setFlipped] = useState(false);
const frontButton = useRef(null);
const backButton = useRef(null);
const focusAfterFlip = useRef(false);
useLayoutEffect(() => {
if (!focusAfterFlip.current) return;
(flipped ? backButton : frontButton).current?.focus({ preventScroll: true });
focusAfterFlip.current = false;
}, [flipped]);
function flip() {
focusAfterFlip.current = true;
setFlipped((value) => !value);
}
const flipButton = (back, ref) => <button ref={ref} type="button" className="aniimo-flip nds-icon-button" aria-pressed={back} aria-label={`${back ? content.showFront : content.showBack} — ${t(variant.name)}, ${t(variant.form)}`} title={back ? content.showFront : content.showBack} onClick={flip}><Icon name="flip" /></button>;
return (
<div className={`aniimo-card ${flipped ? "is-flipped" : ""}`}>
<div className="aniimo-card-inner">
<div className="aniimo-face aniimo-front nds-card" aria-hidden={flipped} inert={flipped}>
{flipButton(false, frontButton)}
<GuideChecklistMark completed={completed} label={completedLabel} />
<div className="aniimo-portrait"><img src={variant.image} alt={t(variant.name)} width="512" height="840" loading="lazy" /></div>
<div className="aniimo-card-body">
<h2 title={t(variant.name)}>{t(variant.name)}</h2>
<p className="aniimo-form" title={t(variant.form)}>{t(variant.form)}</p>
<span className="aniimo-element-icons">{variant.elements.map((element) => <Tooltip key={element} content={t(element)}><img src={aniimoAsset(element)} alt={t(element)} width="24" height="24" /></Tooltip>)}</span>
</div>
</div>
<div className="aniimo-face aniimo-back nds-card" aria-hidden={!flipped} inert={!flipped}>
{flipButton(true, backButton)}
<GuideChecklistMark completed={completed} label={completedLabel} />
<div className="aniimo-card-body aniimo-back-heading">
<h2 title={t(variant.name)}>{t(variant.name)}</h2>
<p className="aniimo-form" title={t(variant.form)}>{t(variant.form)}</p>
</div>
<div className="aniimo-card-body aniimo-back-details nds-scrollbox" data-variant="ghost" role="region" aria-label={`${content.showBack} — ${t(variant.name)}`} tabIndex="0">
<div className="aniimo-back-summary">
<div><h3>{content.elements}</h3><Attributes values={variant.elements} t={t} /></div>
<div><h3>{content.role}</h3><Attributes values={[variant.role]} t={t} /></div>
</div>
<h3>{content.abilities}</h3>
<ul className="aniimo-abilities">{variant.homeland_abilities.map((ability) => <li key={ability.type}>
<img src={aniimoAsset(ability.type)} alt="" width="24" height="24" />
<span>{t(ability.type)}</span>
<span className="nds-badge" data-variant="gold">{content.level} {ability.level}</span>
</li>)}</ul>
<h3>{content.statsTitle}</h3>
<div className="aniimo-stats">
{ANIIMO_STATS.map((key) => <div className="aniimo-stat" key={key}>
<span title={content.stats[key]}>{content.stats[key]}</span>
<span className="nds-progress" data-size="sm" role="progressbar" aria-label={content.stats[key]} aria-valuemin={0} aria-valuemax={statsMax} aria-valuenow={variant.attributes.stats[key]}>
<span style={{ width: `${variant.attributes.stats[key] / statsMax * 100}%` }} />
</span>
<span className="aniimo-stat-value">{variant.attributes.stats[key]}</span>
</div>)}
<div className="aniimo-stat-total"><span>{content.statsTotal}</span><strong>{variant.attributes.total}</strong></div>
</div>
</div>
</div>
</div>
</div>
);
}
export function AniimoCard({ variants, t, content, isCompleted, completedLabel, statsMax }) {
const id = useId();
const [selectedKey, setSelectedKey] = useState(variants[0].key);
const variant = variants.find((item) => item.key === selectedKey) || variants[0];
// Mémoriser le repli pour ne pas restaurer un ancien onglet lorsque le filtre est retiré.
if (selectedKey !== variant.key) setSelectedKey(variant.key);
const multiple = variants.length > 1;
function selectTab(index, target) {
setSelectedKey(variants[index].key);
target.parentElement.children[index].focus();
}
return (
<article className="aniimo-result" data-checklist-complete={isCompleted(variant.key) || undefined} data-aniimo={variant.aniimoId} data-form={variant.form} aria-label={t(variant.name)}>
<div className="aniimo-tabs-slot">
{multiple && <div className="aniimo-tabs nds-radio-button-group" data-size="xs" role="tablist" aria-label={`${content.variants} — ${t(variant.name)}`}>
{variants.map((item, index) => <Tooltip as="button" content={t(item.form)} aria-label={t(item.form)} key={item.key} type="button" role="tab" data-form={item.form} id={`${id}-${item.form}`} aria-controls={`${id}-panel`} aria-selected={item.key === variant.key} tabIndex={item.key === variant.key ? 0 : -1} className={item.key === variant.key ? "active" : ""} onClick={() => setSelectedKey(item.key)} onKeyDown={(event) => {
const next = event.key === "ArrowRight" ? (index + 1) % variants.length : event.key === "ArrowLeft" ? (index - 1 + variants.length) % variants.length : event.key === "Home" ? 0 : event.key === "End" ? variants.length - 1 : null;
if (next === null) return;
event.preventDefault();
selectTab(next, event.currentTarget);
}}>{shortForm(t(item.form))}</Tooltip>)}
</div>}
</div>
<div id={`${id}-panel`} role={multiple ? "tabpanel" : undefined} aria-labelledby={multiple ? `${id}-${variant.form}` : undefined}>
<CardFaces variant={variant} t={t} content={content} completed={isCompleted(variant.key)} completedLabel={completedLabel} statsMax={statsMax} />
</div>
</article>
);
}