diff --git a/tests/static-smoke.test.mjs b/tests/static-smoke.test.mjs index 4d6dc60..664ed7b 100644 --- a/tests/static-smoke.test.mjs +++ b/tests/static-smoke.test.mjs @@ -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, /
{ 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"); }); diff --git a/website/public/data/site.json b/website/public/data/site.json index 74584ad..708b2e3 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -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.", diff --git a/website/public/static/img/toolbox-icons/ball.png b/website/public/static/img/toolbox-icons/ball.png new file mode 100644 index 0000000..d86af11 Binary files /dev/null and b/website/public/static/img/toolbox-icons/ball.png differ diff --git a/website/public/static/img/toolbox-icons/capture.png b/website/public/static/img/toolbox-icons/capture.png new file mode 100644 index 0000000..3e6548b Binary files /dev/null and b/website/public/static/img/toolbox-icons/capture.png differ diff --git a/website/public/static/img/toolbox-icons/card.png b/website/public/static/img/toolbox-icons/card.png new file mode 100644 index 0000000..ab82847 Binary files /dev/null and b/website/public/static/img/toolbox-icons/card.png differ diff --git a/website/public/static/img/toolbox-icons/city.png b/website/public/static/img/toolbox-icons/city.png new file mode 100644 index 0000000..07bcdc8 Binary files /dev/null and b/website/public/static/img/toolbox-icons/city.png differ diff --git a/website/public/static/img/toolbox-icons/compass.png b/website/public/static/img/toolbox-icons/compass.png new file mode 100644 index 0000000..edadfb9 Binary files /dev/null and b/website/public/static/img/toolbox-icons/compass.png differ diff --git a/website/public/static/img/toolbox-icons/controller.svg b/website/public/static/img/toolbox-icons/controller.svg deleted file mode 100644 index de7a7aa..0000000 --- a/website/public/static/img/toolbox-icons/controller.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/website/public/static/img/toolbox-icons/crash.png b/website/public/static/img/toolbox-icons/crash.png new file mode 100644 index 0000000..00247fd Binary files /dev/null and b/website/public/static/img/toolbox-icons/crash.png differ diff --git a/website/public/static/img/toolbox-icons/default.svg b/website/public/static/img/toolbox-icons/default.svg deleted file mode 100644 index 5b6da01..0000000 --- a/website/public/static/img/toolbox-icons/default.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/website/public/static/img/toolbox-icons/detective.png b/website/public/static/img/toolbox-icons/detective.png new file mode 100644 index 0000000..1a59651 Binary files /dev/null and b/website/public/static/img/toolbox-icons/detective.png differ diff --git a/website/public/static/img/toolbox-icons/fire.png b/website/public/static/img/toolbox-icons/fire.png new file mode 100644 index 0000000..e797323 Binary files /dev/null and b/website/public/static/img/toolbox-icons/fire.png differ diff --git a/website/public/static/img/toolbox-icons/horror.png b/website/public/static/img/toolbox-icons/horror.png new file mode 100644 index 0000000..d68279a Binary files /dev/null and b/website/public/static/img/toolbox-icons/horror.png differ diff --git a/website/public/static/img/toolbox-icons/jump.png b/website/public/static/img/toolbox-icons/jump.png new file mode 100644 index 0000000..5037a5e Binary files /dev/null and b/website/public/static/img/toolbox-icons/jump.png differ diff --git a/website/public/static/img/toolbox-icons/mining.png b/website/public/static/img/toolbox-icons/mining.png new file mode 100644 index 0000000..b178bd3 Binary files /dev/null and b/website/public/static/img/toolbox-icons/mining.png differ diff --git a/website/public/static/img/toolbox-icons/parachute.png b/website/public/static/img/toolbox-icons/parachute.png new file mode 100644 index 0000000..0b4f2af Binary files /dev/null and b/website/public/static/img/toolbox-icons/parachute.png differ diff --git a/website/public/static/img/toolbox-icons/puzzle.png b/website/public/static/img/toolbox-icons/puzzle.png new file mode 100644 index 0000000..5591900 Binary files /dev/null and b/website/public/static/img/toolbox-icons/puzzle.png differ diff --git a/website/public/static/img/toolbox-icons/quest.svg b/website/public/static/img/toolbox-icons/quest.svg deleted file mode 100644 index e11806c..0000000 --- a/website/public/static/img/toolbox-icons/quest.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/website/public/static/img/toolbox-icons/shield.png b/website/public/static/img/toolbox-icons/shield.png new file mode 100644 index 0000000..2b4bafa Binary files /dev/null and b/website/public/static/img/toolbox-icons/shield.png differ diff --git a/website/public/static/img/toolbox-icons/sword.png b/website/public/static/img/toolbox-icons/sword.png new file mode 100644 index 0000000..fa5f095 Binary files /dev/null and b/website/public/static/img/toolbox-icons/sword.png differ diff --git a/website/public/static/img/toolbox-icons/target.png b/website/public/static/img/toolbox-icons/target.png new file mode 100644 index 0000000..8849379 Binary files /dev/null and b/website/public/static/img/toolbox-icons/target.png differ diff --git a/website/public/static/img/toolbox-icons/target.svg b/website/public/static/img/toolbox-icons/target.svg deleted file mode 100644 index 40f378f..0000000 --- a/website/public/static/img/toolbox-icons/target.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/website/public/static/img/toolbox-icons/toolbox.png b/website/public/static/img/toolbox-icons/toolbox.png new file mode 100644 index 0000000..6859cfe Binary files /dev/null and b/website/public/static/img/toolbox-icons/toolbox.png differ diff --git a/website/public/static/img/toolbox-icons/tower.png b/website/public/static/img/toolbox-icons/tower.png new file mode 100644 index 0000000..ec49413 Binary files /dev/null and b/website/public/static/img/toolbox-icons/tower.png differ diff --git a/website/public/static/img/toolbox-icons/wheel.png b/website/public/static/img/toolbox-icons/wheel.png new file mode 100644 index 0000000..f4559d1 Binary files /dev/null and b/website/public/static/img/toolbox-icons/wheel.png differ diff --git a/website/src/features/games/GamesPage.jsx b/website/src/features/games/GamesPage.jsx index b939e97..03d90e2 100644 --- a/website/src/features/games/GamesPage.jsx +++ b/website/src/features/games/GamesPage.jsx @@ -1,11 +1,13 @@ -export function GamesPage({ games, gamesError }) { +export function GamesPage({ siteContent, games, gamesError }) { + const content = siteContent.gamesPage; + return ( <>
-

Pages informatives

-

Jeux disponibles

-

Choisissez un jeu pour consulter ses données maintenues et associer une toolbox locale.

+

{content.eyebrow}

+

{content.title}

+

{content.description}

@@ -25,8 +27,8 @@ export function GamesPage({ games, gamesError }) { )) : (
-

Aucun jeu disponible

-

{gamesError || "Ajoutez des entrées dans /data/games.json."}

+

{content.emptyTitle}

+

{gamesError || content.emptyText}

)}
diff --git a/website/src/main.jsx b/website/src/main.jsx index 5fb9338..96ba83e 100644 --- a/website/src/main.jsx +++ b/website/src/main.jsx @@ -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>)/g).filter(Boolean); - return parts.map((part, index) => { + return parts.flatMap((part, index) => { const italic = part.match(/^(.*?)<\/i>$/); - return italic ? {italic[1]} : {part}; + const value = italic ? italic[1] : part; + const lines = value.split("\n"); + return lines.flatMap((line, lineIndex) => { + const key = `${index}-${lineIndex}`; + const node = italic ? {line} : {line}; + return lineIndex === lines.length - 1 ? [node] : [node,
]; + }); }); } @@ -950,15 +996,18 @@ function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions }) {

{content.eyebrow}

{content.title}

-
- - -
-
- - +

{content.summary}

+
+
+ + +
+
+ + +
@@ -990,16 +1039,17 @@ function StorageHelpCard({ help }) { } function ToolboxCard({ toolbox, game, actions }) { + const coverImage = game?.image || toolbox.icon || DEFAULT_TOOLBOX_ICON; return (
- {game?.image && {game.title}} + {game

{game ? game.title : "Toolbox libre"}

@@ -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

Toolbox introuvable

Retour
; - return ; + return ; } -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 ( +
+ + {open && ( +
+ {TOOLBOX_ICONS.map((path) => ( + + ))} +
+ )} +
+ ); +} + +function ToolboxGameIcon({ game }) { + return ( +
+ +
+ ); +} + +function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) { const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2; const moduleContext = { getModuleData, @@ -1068,9 +1188,13 @@ function ToolboxView({ toolbox, embedded, actions, updateToolbox, updateModuleDa

Toolbox

- {embedded ?

{toolbox.name}

: ( - name !== toolbox.name && updateToolbox({ ...toolbox, name })} /> - )} +
+ {!embedded && !toolboxGame && updateToolbox({ ...toolbox, icon })} />} + {!embedded && toolboxGame?.image && } + {embedded ?

{toolbox.name}

: ( + name !== toolbox.name && updateToolbox({ ...toolbox, name })} /> + )} +
diff --git a/website/src/styles/_games.scss b/website/src/styles/_games.scss index ed5c36a..37e482f 100644 --- a/website/src/styles/_games.scss +++ b/website/src/styles/_games.scss @@ -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; diff --git a/website/src/styles/_home.scss b/website/src/styles/_home.scss index 274cfbb..be99756 100644 --- a/website/src/styles/_home.scss +++ b/website/src/styles/_home.scss @@ -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 { diff --git a/website/src/styles/_responsive.scss b/website/src/styles/_responsive.scss index b881318..953f659 100644 --- a/website/src/styles/_responsive.scss +++ b/website/src/styles/_responsive.scss @@ -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)); } diff --git a/website/src/styles/_toolboxes.scss b/website/src/styles/_toolboxes.scss index 8ee6d21..6ce6e20 100644 --- a/website/src/styles/_toolboxes.scss +++ b/website/src/styles/_toolboxes.scss @@ -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; }