improve style & add icon toolboxes
All checks were successful
Deploy Sokko G / deploy (push) Successful in 5s
|
|
@ -15,6 +15,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
const styles = await readFile("website/src/styles/main.scss", "utf8");
|
||||
const styleTokens = await readFile("website/src/styles/_tokens.scss", "utf8");
|
||||
const styleBase = await readFile("website/src/styles/_base.scss", "utf8");
|
||||
const styleHome = await readFile("website/src/styles/_home.scss", "utf8");
|
||||
const styleToolboxes = await readFile("website/src/styles/_toolboxes.scss", "utf8");
|
||||
const styleIcons = await readFile("website/src/styles/_icons.scss", "utf8");
|
||||
const styleMhwilds = await readFile("website/src/styles/_mhwilds.scss", "utf8");
|
||||
|
|
@ -44,7 +45,12 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(styleTokens, /--gradient-brand/);
|
||||
assert.match(styleBase, /\.card-icon-button,\n\.drawer-action-button/);
|
||||
assert.match(styleBase, /\.import-icon-button/);
|
||||
assert.match(styleHome, /\.dialogue-line i/);
|
||||
assert.match(styleHome, /font-style: italic/);
|
||||
assert.match(styleToolboxes, /\.module-icon-abacus/);
|
||||
assert.match(styleToolboxes, /\.toolbox-icon-picker/);
|
||||
assert.match(styleToolboxes, /\.toolbox-game-icon/);
|
||||
assert.match(styleToolboxes, /\.toolbox-actions-row/);
|
||||
assert.match(styleIcons, /ui-icon-drag/);
|
||||
assert.match(styleIcons, /ui-icon-dropdown/);
|
||||
assert.match(styleIcons, /ui-icon-flip/);
|
||||
|
|
@ -56,6 +62,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(source, /features\/games\/GameRoute\.jsx/);
|
||||
assert.match(iconComponent, /export function Icon/);
|
||||
assert.match(gamesPage, /export function GamesPage/);
|
||||
assert.match(gamesPage, /siteContent\.gamesPage/);
|
||||
assert.match(gameRoute, /export function GameRoute/);
|
||||
assert.match(gameRoute, /MhwildsPage/);
|
||||
assert.match(mhwildsPage, /export function MhwildsPage/);
|
||||
|
|
@ -91,6 +98,11 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(source, /DEFAULT_SITE_CONTENT/);
|
||||
assert.match(source, /\/data\/site\.json/);
|
||||
assert.match(source, /APP_STORAGE_LIMIT_BYTES/);
|
||||
assert.match(source, /DEFAULT_TOOLBOX_ICON/);
|
||||
assert.match(source, /TOOLBOX_ICON_FILES/);
|
||||
assert.match(source, /function normalizeToolboxIcon/);
|
||||
assert.match(source, /function ToolboxIconPicker/);
|
||||
assert.match(source, /function ToolboxGameIcon/);
|
||||
assert.match(source, /function StorageQuota/);
|
||||
assert.match(source, /function writeStorageValue/);
|
||||
assert.match(source, /role="progressbar"/);
|
||||
|
|
@ -102,8 +114,11 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(source, /function CreateToolboxModal/);
|
||||
assert.match(source, /const getToolboxGameId/);
|
||||
assert.match(source, /dragon\.png/);
|
||||
assert.match(source, /value\.split\("\\n"\)/);
|
||||
assert.match(source, /<br key=/);
|
||||
assert.match(source, /getToolboxGame\(toolbox\)/);
|
||||
assert.match(source, /toolbox-card-cover-link/);
|
||||
assert.match(source, /toolbox-icon-cover/);
|
||||
assert.match(source, /moduleColumns/);
|
||||
assert.match(source, /function ToolboxView/);
|
||||
assert.match(moduleRegistry, /onDragStart/);
|
||||
|
|
@ -139,6 +154,8 @@ test("mhwilds data and assets are available", async () => {
|
|||
|
||||
assert.equal(site.brand.name, "Sokko G");
|
||||
assert.equal(site.home.hero.title, "Sokko G");
|
||||
assert.equal(site.gamesPage.title, "Jeux disponibles");
|
||||
assert.match(site.toolboxes.summary, /espaces modulaires/);
|
||||
assert.ok(site.toolboxes.storageHelp.items.length >= 3);
|
||||
assert.equal(games.games[0].id, "mhwilds");
|
||||
assert.equal(games.games[0].eyebrow, "Guide de jeu");
|
||||
|
|
@ -183,5 +200,7 @@ test("legacy svg icons are available for reuse", async () => {
|
|||
await readFile("website/public/static/icons/zoom.svg", "utf8");
|
||||
await readFile("website/public/favicon.ico");
|
||||
await readFile("website/public/static/img/dragon.png");
|
||||
await readFile("website/public/static/img/toolbox-icons/controller.svg", "utf8");
|
||||
await readFile("website/public/static/img/toolbox-icons/toolbox.png");
|
||||
await readFile("website/public/static/img/toolbox-icons/sword.png");
|
||||
await readFile("website/public/static/img/toolbox-icons/puzzle.png");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,9 +15,16 @@
|
|||
},
|
||||
"topbar": {
|
||||
"dashboard": "Dashboard",
|
||||
"toolbox": "Toolbox active",
|
||||
"toolbox": "Toolboxes",
|
||||
"games": "Informations jeu"
|
||||
},
|
||||
"gamesPage": {
|
||||
"eyebrow": "Pages informatives",
|
||||
"title": "Jeux disponibles",
|
||||
"description": "Choisissez un jeu pour consulter ses données maintenues et associer une toolbox locale.",
|
||||
"emptyTitle": "Aucun jeu disponible",
|
||||
"emptyText": "Ajoutez des entrées dans /data/games.json."
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"eyebrow": "Session gaming efficace",
|
||||
|
|
@ -77,6 +84,7 @@
|
|||
"importAll": "Importer tout",
|
||||
"exportAll": "Exporter tout",
|
||||
"importOne": "Importer une toolbox",
|
||||
"summary": "Créez des espaces modulaires pour regrouper vos outils de session : notes, suivis, références, captures et futurs modules restent accessibles rapidement, en local, par jeu ou par usage.",
|
||||
"storageHelp": {
|
||||
"title": "Sauvegardes locales",
|
||||
"text": "Vos toolboxes restent sur cet appareil, dans ce navigateur. Rien n’est envoyé sur un serveur.",
|
||||
|
|
|
|||
BIN
website/public/static/img/toolbox-icons/ball.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
website/public/static/img/toolbox-icons/capture.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
website/public/static/img/toolbox-icons/card.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
website/public/static/img/toolbox-icons/city.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
website/public/static/img/toolbox-icons/compass.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Controller">
|
||||
<rect width="256" height="256" rx="36" fill="#151520"/>
|
||||
<path d="M68 112c10-22 30-28 60-20 30-8 50-2 60 20l18 46c7 18-9 35-26 25l-34-20h-36l-34 20c-17 10-33-7-26-25l18-46Z" fill="#303143" stroke="#ffffff" stroke-opacity=".25" stroke-width="8"/>
|
||||
<path d="M82 132h34M99 115v34" stroke="#f3f3ff" stroke-width="10" stroke-linecap="round"/>
|
||||
<circle cx="164" cy="123" r="8" fill="#b8b9ff"/>
|
||||
<circle cx="180" cy="141" r="8" fill="#7bdff2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 549 B |
BIN
website/public/static/img/toolbox-icons/crash.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Toolbox">
|
||||
<rect width="256" height="256" rx="36" fill="#1d1e25"/>
|
||||
<path d="M72 96h112a20 20 0 0 1 20 20v56a20 20 0 0 1-20 20H72a20 20 0 0 1-20-20v-56a20 20 0 0 1 20-20Z" fill="#2d2d34" stroke="#ffffff" stroke-opacity=".25" stroke-width="8"/>
|
||||
<path d="M96 96V80a16 16 0 0 1 16-16h32a16 16 0 0 1 16 16v16" fill="none" stroke="#ffffff" stroke-opacity=".72" stroke-width="12" stroke-linecap="round"/>
|
||||
<path d="M52 132h152" stroke="#b8b9ff" stroke-width="10" stroke-linecap="round"/>
|
||||
<rect x="112" y="118" width="32" height="28" rx="8" fill="#b8b9ff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 650 B |
BIN
website/public/static/img/toolbox-icons/detective.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
website/public/static/img/toolbox-icons/fire.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
website/public/static/img/toolbox-icons/horror.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
website/public/static/img/toolbox-icons/jump.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
website/public/static/img/toolbox-icons/mining.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
website/public/static/img/toolbox-icons/parachute.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
website/public/static/img/toolbox-icons/puzzle.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -1,9 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Quest">
|
||||
<rect width="256" height="256" rx="36" fill="#18211d"/>
|
||||
<path d="M82 56h76l28 28v116H82V56Z" fill="#26342e" stroke="#ffffff" stroke-opacity=".25" stroke-width="8"/>
|
||||
<path d="M158 58v28h28" fill="none" stroke="#ffffff" stroke-opacity=".45" stroke-width="8" stroke-linejoin="round"/>
|
||||
<path d="M104 112h54M104 138h54M104 164h34" stroke="#b9f6ca" stroke-width="10" stroke-linecap="round"/>
|
||||
<circle cx="84" cy="112" r="6" fill="#b9f6ca"/>
|
||||
<circle cx="84" cy="138" r="6" fill="#b9f6ca"/>
|
||||
<circle cx="84" cy="164" r="6" fill="#b9f6ca"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 644 B |
BIN
website/public/static/img/toolbox-icons/shield.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
website/public/static/img/toolbox-icons/sword.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
website/public/static/img/toolbox-icons/target.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Target">
|
||||
<rect width="256" height="256" rx="36" fill="#21191c"/>
|
||||
<circle cx="128" cy="128" r="76" fill="#36262b" stroke="#ffffff" stroke-opacity=".25" stroke-width="8"/>
|
||||
<circle cx="128" cy="128" r="48" fill="none" stroke="#ffb3c1" stroke-width="10"/>
|
||||
<circle cx="128" cy="128" r="16" fill="#ffb3c1"/>
|
||||
<path d="M128 40v28M128 188v28M40 128h28M188 128h28" stroke="#ffffff" stroke-opacity=".7" stroke-width="10" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 537 B |
BIN
website/public/static/img/toolbox-icons/toolbox.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
website/public/static/img/toolbox-icons/tower.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
website/public/static/img/toolbox-icons/wheel.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -1,11 +1,13 @@
|
|||
export function GamesPage({ games, gamesError }) {
|
||||
export function GamesPage({ siteContent, games, gamesError }) {
|
||||
const content = siteContent.gamesPage;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Pages informatives</p>
|
||||
<h1>Jeux disponibles</h1>
|
||||
<p>Choisissez un jeu pour consulter ses données maintenues et associer une toolbox locale.</p>
|
||||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
<p>{content.description}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="game-list">
|
||||
|
|
@ -25,8 +27,8 @@ export function GamesPage({ games, gamesError }) {
|
|||
</article>
|
||||
)) : (
|
||||
<div className="empty">
|
||||
<h2>Aucun jeu disponible</h2>
|
||||
<p>{gamesError || "Ajoutez des entrées dans /data/games.json."}</p>
|
||||
<h2>{content.emptyTitle}</h2>
|
||||
<p>{gamesError || content.emptyText}</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -16,12 +16,43 @@ const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
|||
const APP_STORAGE_WARNING_RATIO = 0.85;
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||
const TOOLBOX_ICON_FILES = [
|
||||
"toolbox.png",
|
||||
"ball.png",
|
||||
"capture.png",
|
||||
"card.png",
|
||||
"city.png",
|
||||
"compass.png",
|
||||
"crash.png",
|
||||
"detective.png",
|
||||
"fire.png",
|
||||
"horror.png",
|
||||
"jump.png",
|
||||
"mining.png",
|
||||
"parachute.png",
|
||||
"puzzle.png",
|
||||
"shield.png",
|
||||
"sword.png",
|
||||
"target.png",
|
||||
"tower.png",
|
||||
"wheel.png"
|
||||
];
|
||||
const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
|
||||
const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;
|
||||
|
||||
const DEFAULT_SITE_CONTENT = {
|
||||
brand: { name: "Sokko G", homeAriaLabel: "Accueil Sokko G" },
|
||||
navigation: { home: "Accueil", toolboxes: "Toolboxes", games: "Jeux", mobileGames: "Infos" },
|
||||
sidebar: { badge: "Local only", note: "Données stockées dans ce navigateur." },
|
||||
topbar: { dashboard: "Dashboard", toolbox: "Toolbox active", games: "Informations jeu" },
|
||||
gamesPage: {
|
||||
eyebrow: "Pages informatives",
|
||||
title: "Jeux disponibles",
|
||||
description: "Choisissez un jeu pour consulter ses données maintenues et associer une toolbox locale.",
|
||||
emptyTitle: "Aucun jeu disponible",
|
||||
emptyText: "Ajoutez des entrées dans /data/games.json."
|
||||
},
|
||||
home: {
|
||||
hero: {
|
||||
eyebrow: "Session gaming efficace",
|
||||
|
|
@ -52,6 +83,7 @@ const DEFAULT_SITE_CONTENT = {
|
|||
importAll: "Importer tout",
|
||||
exportAll: "Exporter tout",
|
||||
importOne: "Importer une toolbox",
|
||||
summary: "Créez des espaces modulaires pour regrouper vos outils de session : notes, suivis, références, captures et futurs modules restent accessibles rapidement, en local, par jeu ou par usage.",
|
||||
storageHelp: {
|
||||
title: "Sauvegardes locales",
|
||||
text: "Vos toolboxes restent sur cet appareil, dans ce navigateur. Rien n’est envoyé sur un serveur.",
|
||||
|
|
@ -128,6 +160,12 @@ function getDefaultModuleTitle(type) {
|
|||
return TOOLBOX_MODULES[type]?.label || "Outil";
|
||||
}
|
||||
|
||||
function normalizeToolboxIcon(icon) {
|
||||
const value = String(icon || "").trim();
|
||||
const file = value.replace(TOOLBOX_ICON_BASE, "").split("/").pop();
|
||||
return TOOLBOX_ICON_FILES.includes(file) ? `${TOOLBOX_ICON_BASE}${file}` : DEFAULT_TOOLBOX_ICON;
|
||||
}
|
||||
|
||||
function normalizeToolboxModule(module) {
|
||||
if (!module || typeof module !== "object" || !module.type) return null;
|
||||
const title = String(module.title || "").trim();
|
||||
|
|
@ -141,6 +179,7 @@ function normalizeToolbox(toolbox) {
|
|||
return {
|
||||
id: toolbox.id || uid("tbx"),
|
||||
name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox",
|
||||
icon: normalizeToolboxIcon(toolbox.icon),
|
||||
moduleColumns: Number(toolbox.moduleColumns) === 1 ? 1 : 2,
|
||||
modules: (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean),
|
||||
updatedAt: toolbox.updatedAt || new Date().toISOString()
|
||||
|
|
@ -157,6 +196,7 @@ function compactToolboxForStorage(toolbox) {
|
|||
updatedAt: normalized.updatedAt
|
||||
};
|
||||
if (normalized.moduleColumns === 1) compact.moduleColumns = 1;
|
||||
if (normalized.icon !== DEFAULT_TOOLBOX_ICON) compact.icon = normalized.icon;
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
|
@ -935,9 +975,15 @@ function DialogueLine({ line }) {
|
|||
|
||||
function RichText({ text }) {
|
||||
const parts = String(text).split(/(<i>.*?<\/i>)/g).filter(Boolean);
|
||||
return parts.map((part, index) => {
|
||||
return parts.flatMap((part, index) => {
|
||||
const italic = part.match(/^<i>(.*?)<\/i>$/);
|
||||
return italic ? <i key={index}>{italic[1]}</i> : <React.Fragment key={index}>{part}</React.Fragment>;
|
||||
const value = italic ? italic[1] : part;
|
||||
const lines = value.split("\n");
|
||||
return lines.flatMap((line, lineIndex) => {
|
||||
const key = `${index}-${lineIndex}`;
|
||||
const node = italic ? <i key={key}>{line}</i> : <React.Fragment key={key}>{line}</React.Fragment>;
|
||||
return lineIndex === lines.length - 1 ? [node] : [node, <br key={`${key}-br`} />];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -950,16 +996,19 @@ function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions }) {
|
|||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
</div>
|
||||
<div className="actions">
|
||||
</section>
|
||||
<section className="toolbar toolbar-stack">
|
||||
<p className="toolbox-summary">{content.summary}</p>
|
||||
<div className="toolbox-actions-row">
|
||||
<div>
|
||||
<button className="primary" onClick={() => actions.setCreateModal({})}>{content.newButton}</button>
|
||||
<ImportButton className="import-button" label={content.importOne} onFile={actions.importToolbox} />
|
||||
</div>
|
||||
</section>
|
||||
<section className="toolbar toolbar-stack">
|
||||
<div>
|
||||
<ImportButton className="import-button" label={content.importAll} onFile={actions.importAllToolboxes} />
|
||||
<button onClick={actions.exportAllToolboxes}>{content.exportAll}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="cards">
|
||||
<StorageHelpCard help={content.storageHelp} />
|
||||
|
|
@ -990,16 +1039,17 @@ function StorageHelpCard({ help }) {
|
|||
}
|
||||
|
||||
function ToolboxCard({ toolbox, game, actions }) {
|
||||
const coverImage = game?.image || toolbox.icon || DEFAULT_TOOLBOX_ICON;
|
||||
return (
|
||||
<article className="card toolbox-card">
|
||||
<a
|
||||
className={`card-cover toolbox-card-cover-link ${game?.image ? "toolbox-card-cover" : ""}`}
|
||||
className={`card-cover toolbox-card-cover-link ${game?.image ? "toolbox-card-cover" : "toolbox-icon-cover"}`}
|
||||
href={`#/toolbox/${toolbox.id}`}
|
||||
aria-label={`Ouvrir ${toolbox.name}`}
|
||||
title={`Ouvrir ${toolbox.name}`}
|
||||
style={{ "--game-cover": game?.cover || "var(--gradient-nebula)", background: game?.image ? "" : "var(--gradient-nebula)" }}
|
||||
>
|
||||
{game?.image && <img src={game.image} alt={game.title} loading="lazy" />}
|
||||
<img src={coverImage} alt={game ? game.title : `Icône de ${toolbox.name}`} loading="lazy" />
|
||||
</a>
|
||||
<div className="card-body">
|
||||
<p className="eyebrow">{game ? game.title : "Toolbox libre"}</p>
|
||||
|
|
@ -1022,13 +1072,83 @@ function ToolboxCard({ toolbox, game, actions }) {
|
|||
}
|
||||
|
||||
function ToolboxPage(props) {
|
||||
const { toolboxId, toolboxes } = props;
|
||||
const { toolboxId, toolboxes, getToolboxGame } = props;
|
||||
const toolbox = toolboxes.find((item) => item.id === toolboxId);
|
||||
if (!toolbox) return <div className="empty"><h1>Toolbox introuvable</h1><a className="button" href="#/toolboxes">Retour</a></div>;
|
||||
return <ToolboxView {...props} toolbox={toolbox} embedded={false} />;
|
||||
return <ToolboxView {...props} toolbox={toolbox} toolboxGame={getToolboxGame(toolbox)} embedded={false} />;
|
||||
}
|
||||
|
||||
function ToolboxView({ toolbox, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
function ToolboxIconPicker({ toolbox, onChange }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pickerRef = useRef(null);
|
||||
const icon = normalizeToolboxIcon(toolbox.icon);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
|
||||
function handlePointerDown(event) {
|
||||
if (!pickerRef.current?.contains(event.target)) setOpen(false);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === "Escape") setOpen(false);
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
function selectIcon(nextIcon) {
|
||||
onChange(nextIcon);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="toolbox-icon-picker" ref={pickerRef}>
|
||||
<button
|
||||
className="toolbox-icon-picker-button"
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-label="Changer l'icône de la toolbox"
|
||||
aria-expanded={open}
|
||||
title="Changer l'icône"
|
||||
>
|
||||
<img src={icon} alt="" aria-hidden="true" />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="toolbox-icon-picker-menu" aria-label="Icônes de toolbox">
|
||||
{TOOLBOX_ICONS.map((path) => (
|
||||
<button
|
||||
key={path}
|
||||
className={path === icon ? "active" : ""}
|
||||
type="button"
|
||||
onClick={() => selectIcon(path)}
|
||||
aria-label={`Utiliser l'icône ${path.split("/").pop().replace(".png", "")}`}
|
||||
aria-pressed={path === icon}
|
||||
title={path.split("/").pop().replace(".png", "")}
|
||||
>
|
||||
<img src={path} alt="" aria-hidden="true" loading="lazy" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolboxGameIcon({ game }) {
|
||||
return (
|
||||
<div className="toolbox-game-icon" title={game.title} aria-label={`Jeu associé : ${game.title}`}>
|
||||
<img src={game.image} alt="" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
const moduleContext = {
|
||||
getModuleData,
|
||||
|
|
@ -1068,10 +1188,14 @@ function ToolboxView({ toolbox, embedded, actions, updateToolbox, updateModuleDa
|
|||
<section className="toolbox-head">
|
||||
<div>
|
||||
<p className="eyebrow">Toolbox</p>
|
||||
<div className="toolbox-title-row">
|
||||
{!embedded && !toolboxGame && <ToolboxIconPicker toolbox={toolbox} onChange={(icon) => updateToolbox({ ...toolbox, icon })} />}
|
||||
{!embedded && toolboxGame?.image && <ToolboxGameIcon game={toolboxGame} />}
|
||||
{embedded ? <h1>{toolbox.name}</h1> : (
|
||||
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<AddToolControls onAdd={addModule} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@
|
|||
rgba(5, 7, 17, 0.72);
|
||||
}
|
||||
|
||||
.toolbox-icon-cover {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background:
|
||||
radial-gradient(circle at 50% 38%, rgba(34, 211, 238, 0.16), transparent 42%),
|
||||
var(--gradient-nebula);
|
||||
}
|
||||
|
||||
.game-card-cover img,
|
||||
.toolbox-card-cover img {
|
||||
display: block;
|
||||
|
|
@ -27,6 +35,14 @@
|
|||
object-fit: cover;
|
||||
}
|
||||
|
||||
.toolbox-icon-cover img {
|
||||
display: block;
|
||||
width: min(78px, 42%);
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 16px 24px rgba(0, 0, 0, 0.34));
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
min-height: 24px;
|
||||
|
|
|
|||
|
|
@ -182,8 +182,8 @@ span {
|
|||
}
|
||||
|
||||
.dialogue-line i {
|
||||
color: var(--color-text-muted);
|
||||
font-style: normal;
|
||||
color: var(--color-text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dialogue-caption {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,16 @@
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.toolbox-actions-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbox-actions-row > div,
|
||||
.toolbox-actions-row > div:last-child {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tool-add-modal {
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,92 @@
|
|||
cursor: text;
|
||||
}
|
||||
|
||||
.toolbox-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker {
|
||||
position: relative;
|
||||
z-index: 70;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-button,
|
||||
.toolbox-game-icon {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
min-height: 100px;
|
||||
border-color: var(--color-primary-border);
|
||||
background:
|
||||
radial-gradient(circle at 50% 35%, rgba(34, 211, 238, 0.18), transparent 44%),
|
||||
rgba(21, 26, 48, 0.86);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toolbox-game-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-primary-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-button img,
|
||||
.toolbox-game-icon img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.toolbox-game-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
z-index: 120;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 42px);
|
||||
gap: 7px;
|
||||
width: max-content;
|
||||
max-width: min(260px, calc(100vw - 48px));
|
||||
padding: 9px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(12, 15, 32, 0.96);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-menu button {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
min-height: 42px;
|
||||
padding: 0;
|
||||
border-color: var(--color-border);
|
||||
background: rgba(21, 26, 48, 0.72);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-menu button.active {
|
||||
border-color: var(--color-accent-cyan);
|
||||
background: rgba(34, 211, 238, 0.12);
|
||||
box-shadow: 0 0 0 2px rgba(34, 211, 238, 0.12);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-menu img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.toolbox-title:focus {
|
||||
background: rgba(7, 10, 24, 0.72);
|
||||
outline: 1px solid rgba(34, 211, 238, 0.72);
|
||||
|
|
@ -121,6 +207,31 @@
|
|||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.toolbox-summary {
|
||||
max-width: 760px;
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.toolbox-actions-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.toolbox-actions-row > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toolbox-actions-row > div:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modules {
|
||||
align-items: start;
|
||||
}
|
||||
|
|
|
|||