Improve toolbox architecture and initial bundle loading
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
f76cddcc95
commit
b221257616
34 changed files with 8358 additions and 6744 deletions
65
eslint.config.js
Normal file
65
eslint.config.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Rôle : configure ESLint pour le code React, les hooks et les scripts Node du projet.
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"node_modules/**",
|
||||||
|
"website/dist/**",
|
||||||
|
".codex-home/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["website/src/**/*.{js,jsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2024
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-hooks/purity": "off",
|
||||||
|
"react-hooks/set-state-in-effect": "off",
|
||||||
|
"no-unused-vars": ["error", {
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_"
|
||||||
|
}],
|
||||||
|
"react-refresh/only-export-components": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.js", "*.mjs", "scripts/**/*.mjs", "tests/**/*.mjs"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.es2024
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": ["error", {
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
1463
package-lock.json
generated
1463
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,8 @@
|
||||||
"build": "npm run lists:index && vite build",
|
"build": "npm run lists:index && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lists:index": "node scripts/generate-list-indexes.mjs",
|
"lists:index": "node scripts/generate-list-indexes.mjs",
|
||||||
"check": "node --check server.mjs && node --check vite.config.js && npm run lists:index && npm test && npm run build",
|
"lint": "eslint .",
|
||||||
|
"check": "node --check server.mjs && node --check vite.config.js && npm run lint && npm run lists:index && npm test && npm run build",
|
||||||
"test": "node --test"
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -19,6 +20,11 @@
|
||||||
"vite": "^8.1.5"
|
"vite": "^8.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"eslint": "^10.8.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.4",
|
||||||
|
"globals": "^17.11.0",
|
||||||
"sass": "^1.101.3"
|
"sass": "^1.101.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Rôle : génère les index JSON des listes de jeux à partir des fichiers éditables.
|
||||||
import { access, readdir, readFile, writeFile } from "node:fs/promises";
|
import { access, readdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Rôle : sert l'application Vite construite ou les sources publiques en local.
|
||||||
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { extname, join, relative, resolve } from "node:path";
|
import { extname, join, relative, resolve } from "node:path";
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ test("style entrypoint and theme layers are wired", async () => {
|
||||||
const styleShell = await readFile("website/src/styles/_shell.scss", "utf8");
|
const styleShell = await readFile("website/src/styles/_shell.scss", "utf8");
|
||||||
const styleCards = await readFile("website/src/styles/_cards.scss", "utf8");
|
const styleCards = await readFile("website/src/styles/_cards.scss", "utf8");
|
||||||
const styleHome = await readFile("website/src/styles/_home.scss", "utf8");
|
const styleHome = await readFile("website/src/styles/_home.scss", "utf8");
|
||||||
const styleToolboxes = await readFile("website/src/styles/_toolboxes.scss", "utf8");
|
const styleToolboxesEntry = await readFile("website/src/styles/_toolboxes.scss", "utf8");
|
||||||
|
const styleToolboxes = await readFile("website/src/styles/toolboxes/_core.scss", "utf8");
|
||||||
const styleOverlays = await readFile("website/src/styles/_overlays.scss", "utf8");
|
const styleOverlays = await readFile("website/src/styles/_overlays.scss", "utf8");
|
||||||
const styleIcons = await readFile("website/src/styles/_icons.scss", "utf8");
|
const styleIcons = await readFile("website/src/styles/_icons.scss", "utf8");
|
||||||
const styleGames = await readFile("website/src/styles/_games.scss", "utf8");
|
const styleGames = await readFile("website/src/styles/_games.scss", "utf8");
|
||||||
|
|
@ -22,6 +23,7 @@ test("style entrypoint and theme layers are wired", async () => {
|
||||||
assert.match(styles, /@use "tokens"/);
|
assert.match(styles, /@use "tokens"/);
|
||||||
assert.match(styles, /@use "nebula"/);
|
assert.match(styles, /@use "nebula"/);
|
||||||
assert.match(styles, /@use "toolboxes"/);
|
assert.match(styles, /@use "toolboxes"/);
|
||||||
|
assert.match(styleToolboxesEntry, /@use "toolboxes\/core"/);
|
||||||
assert.match(styles, /@use "mhwilds"/);
|
assert.match(styles, /@use "mhwilds"/);
|
||||||
assert.match(styles, /@use "diablo4"/);
|
assert.match(styles, /@use "diablo4"/);
|
||||||
assert.match(styleTokens, /--gradient-brand/);
|
assert.match(styleTokens, /--gradient-brand/);
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ test("toolbox storage, cards and pages are wired", async () => {
|
||||||
test("toolbox module registry and modules expose expected behavior", async () => {
|
test("toolbox module registry and modules expose expected behavior", async () => {
|
||||||
const source = await readFile("website/src/main.jsx", "utf8");
|
const source = await readFile("website/src/main.jsx", "utf8");
|
||||||
const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8");
|
const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8");
|
||||||
|
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
|
||||||
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
||||||
const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
|
const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
|
||||||
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
||||||
|
|
@ -122,7 +123,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||||
const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8");
|
const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8");
|
||||||
|
|
||||||
assert.match(moduleRegistry, /export const TOOLBOX_MODULES/);
|
assert.match(moduleMetadataRegistry, /export const TOOLBOX_MODULES/);
|
||||||
|
assert.match(moduleMetadataRegistry, /export const TOOLBOX_MODULE_DEFINITIONS/);
|
||||||
|
assert.match(moduleMetadataRegistry, /export function getDefaultModuleTitle/);
|
||||||
assert.match(moduleRegistry, /export function AddToolControls/);
|
assert.match(moduleRegistry, /export function AddToolControls/);
|
||||||
assert.match(moduleRegistry, /export function ToolboxModules/);
|
assert.match(moduleRegistry, /export function ToolboxModules/);
|
||||||
assert.match(moduleRegistry, /MODULE_COMPONENTS/);
|
assert.match(moduleRegistry, /MODULE_COMPONENTS/);
|
||||||
|
|
@ -135,14 +138,14 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(moduleRegistry, /CombosModule/);
|
assert.match(moduleRegistry, /CombosModule/);
|
||||||
assert.match(moduleRegistry, /calculator:/);
|
assert.match(moduleRegistry, /calculator:/);
|
||||||
assert.match(moduleRegistry, /table:/);
|
assert.match(moduleRegistry, /table:/);
|
||||||
assert.match(moduleRegistry, /table: \{ label: "Tableau", icon: "table", Component: TableModule, editable: true, scrollable: true \}/);
|
assert.match(moduleRegistry, /table: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.table, Component: TableModule \}/);
|
||||||
assert.match(moduleRegistry, /timer:/);
|
assert.match(moduleRegistry, /timer:/);
|
||||||
assert.match(moduleRegistry, /taskPlanner:/);
|
assert.match(moduleRegistry, /taskPlanner:/);
|
||||||
assert.match(moduleRegistry, /equipmentPlanner:/);
|
assert.match(moduleRegistry, /equipmentPlanner:/);
|
||||||
assert.match(moduleRegistry, /Planificateur d'équipements/);
|
assert.match(moduleMetadataRegistry, /Planificateur d'équipements/);
|
||||||
assert.match(moduleRegistry, /imageAnnotation:/);
|
assert.match(moduleRegistry, /imageAnnotation:/);
|
||||||
assert.match(moduleRegistry, /Annotation d'images/);
|
assert.match(moduleMetadataRegistry, /Annotation d'images/);
|
||||||
assert.match(moduleRegistry, /notepad: \{ label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: true \}/);
|
assert.match(moduleRegistry, /notepad: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.notepad, Component: NotepadModule \}/);
|
||||||
assert.match(moduleRegistry, /module-edit-button/);
|
assert.match(moduleRegistry, /module-edit-button/);
|
||||||
assert.match(moduleRegistry, /itemReorder\.startDrag/);
|
assert.match(moduleRegistry, /itemReorder\.startDrag/);
|
||||||
assert.match(moduleRegistry, /onPointerDown/);
|
assert.match(moduleRegistry, /onPointerDown/);
|
||||||
|
|
@ -152,7 +155,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(moduleRegistry, /tool-add-card/);
|
assert.match(moduleRegistry, /tool-add-card/);
|
||||||
assert.match(moduleRegistry, /tool-quick-add-button/);
|
assert.match(moduleRegistry, /tool-quick-add-button/);
|
||||||
assert.match(moduleRegistry, /tool-add-quick-toggle/);
|
assert.match(moduleRegistry, /tool-add-quick-toggle/);
|
||||||
assert.match(moduleRegistry, /scrollable: true/);
|
assert.match(moduleMetadataRegistry, /scrollable: true/);
|
||||||
assert.match(moduleRegistry, /module-content/);
|
assert.match(moduleRegistry, /module-content/);
|
||||||
assert.match(moduleRegistry, /module-scroll-button/);
|
assert.match(moduleRegistry, /module-scroll-button/);
|
||||||
assert.match(moduleRegistry, /Icon name="dropdown"/);
|
assert.match(moduleRegistry, /Icon name="dropdown"/);
|
||||||
|
|
@ -335,14 +338,14 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(countersModule, /counter-drag-handle/);
|
assert.match(countersModule, /counter-drag-handle/);
|
||||||
assert.match(countersModule, /TextExchangeActions/);
|
assert.match(countersModule, /TextExchangeActions/);
|
||||||
assert.match(timerModule, /useGroupedReorder/);
|
assert.match(timerModule, /useGroupedReorder/);
|
||||||
assert.match(timerModule, /reorderEnabled=\{!data\.sortResults\}/);
|
assert.match(timerModule, /reorderEnabled={!data\.sortResults}/);
|
||||||
assert.match(timerModule, /parentId=\{moduleId\}/);
|
assert.match(timerModule, /parentId=\{moduleId\}/);
|
||||||
assert.match(timerModule, /timer-drag-handle/);
|
assert.match(timerModule, /timer-drag-handle/);
|
||||||
assert.match(textImport, /export function parseColonImportLines/);
|
assert.match(textImport, /export function parseColonImportLines/);
|
||||||
assert.match(textImport, /export function exportModuleText/);
|
assert.match(textImport, /export function exportModuleText/);
|
||||||
assert.match(textImport, /export function importModuleText/);
|
assert.match(textImport, /export function importModuleText/);
|
||||||
assert.match(textImport, /export function hasTextExchangeContent/);
|
assert.match(textImport, /export function hasTextExchangeContent/);
|
||||||
assert.match(textImport, /line\.indexOf\(":\"\)/);
|
assert.match(textImport, /line\.indexOf\(":"\)/);
|
||||||
assert.match(imageViewer, /image-viewer-media/);
|
assert.match(imageViewer, /image-viewer-media/);
|
||||||
assert.match(imageViewer, /image-viewer-image-frame/);
|
assert.match(imageViewer, /image-viewer-image-frame/);
|
||||||
assert.match(imageViewer, /image-viewer-marker/);
|
assert.match(imageViewer, /image-viewer-marker/);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// Rôle : regroupe les modales, notifications et viewers rendus au-dessus de l'application.
|
// Rôle : regroupe les modales, notifications et viewers rendus au-dessus de l'application.
|
||||||
import React from "react";
|
|
||||||
import { Icon } from "./Icon.jsx";
|
import { Icon } from "./Icon.jsx";
|
||||||
import { ImageViewer } from "./ImageViewer.jsx";
|
import { ImageViewer } from "./ImageViewer.jsx";
|
||||||
import { ConfirmModal, CreateToolboxModal, LinkToolboxModal, NotificationToast } from "./ToolboxModals.jsx";
|
import { ConfirmModal, CreateToolboxModal, LinkToolboxModal, NotificationToast } from "./ToolboxModals.jsx";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// Rôle : structure le layout global avec sidebar, topbar, navigation et contenu.
|
// Rôle : structure le layout global avec sidebar, topbar, navigation et contenu.
|
||||||
import React from "react";
|
|
||||||
import { Icon } from "./Icon.jsx";
|
import { Icon } from "./Icon.jsx";
|
||||||
import { ImportButton } from "./ImportButton.jsx";
|
import { ImportButton } from "./ImportButton.jsx";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// Rôle : affiche le switch global de position des notifications internes.
|
// Rôle : affiche le switch global de position des notifications internes.
|
||||||
import React from "react";
|
|
||||||
import { Icon } from "./Icon.jsx";
|
import { Icon } from "./Icon.jsx";
|
||||||
|
|
||||||
export function ToastPositionSwitch({ position, labels, onChange }) {
|
export function ToastPositionSwitch({ position, labels, onChange }) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Rôle : fournit une infobulle accessible et réutilisable pour les contrôles et données denses.
|
// Rôle : fournit une infobulle accessible et réutilisable pour les contrôles et données denses.
|
||||||
import { useEffect, useId, useRef, useState } from "react";
|
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
export function Tooltip({ as: Component = "span", children, content, className = "", tooltipClassName = "", position = "top", ...props }) {
|
export function Tooltip({ as: Component = "span", children, content, className = "", tooltipClassName = "", position = "top", ...props }) {
|
||||||
|
|
@ -8,7 +8,7 @@ export function Tooltip({ as: Component = "span", children, content, className =
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [style, setStyle] = useState({});
|
const [style, setStyle] = useState({});
|
||||||
|
|
||||||
function placeTooltip() {
|
const placeTooltip = useCallback(() => {
|
||||||
const anchor = anchorRef.current;
|
const anchor = anchorRef.current;
|
||||||
if (!anchor) return;
|
if (!anchor) return;
|
||||||
const rect = anchor.getBoundingClientRect();
|
const rect = anchor.getBoundingClientRect();
|
||||||
|
|
@ -17,7 +17,7 @@ export function Tooltip({ as: Component = "span", children, content, className =
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStyle({ top: `${Math.max(8, rect.top - 8)}px`, left: `${Math.min(window.innerWidth - 8, Math.max(8, rect.left + rect.width / 2))}px` });
|
setStyle({ top: `${Math.max(8, rect.top - 8)}px`, left: `${Math.min(window.innerWidth - 8, Math.max(8, rect.left + rect.width / 2))}px` });
|
||||||
}
|
}, [position]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return undefined;
|
if (!open) return undefined;
|
||||||
|
|
@ -28,7 +28,7 @@ export function Tooltip({ as: Component = "span", children, content, className =
|
||||||
window.removeEventListener("resize", placeTooltip);
|
window.removeEventListener("resize", placeTooltip);
|
||||||
window.removeEventListener("scroll", placeTooltip, true);
|
window.removeEventListener("scroll", placeTooltip, true);
|
||||||
};
|
};
|
||||||
}, [open, position]);
|
}, [open, placeTooltip]);
|
||||||
|
|
||||||
if (!content) return <Component className={className} {...props}>{children}</Component>;
|
if (!content) return <Component className={className} {...props}>{children}</Component>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// Rôle : affiche les listes de jeu transformables en checklist.
|
// Rôle : affiche les listes de jeu transformables en checklist.
|
||||||
import { Icon } from "../../components/Icon.jsx";
|
|
||||||
import { CopyChecklistItemsButton } from "./CopyChecklistItemsButton.jsx";
|
import { CopyChecklistItemsButton } from "./CopyChecklistItemsButton.jsx";
|
||||||
import { GameBreadcrumb } from "./GameBreadcrumb.jsx";
|
import { GameBreadcrumb } from "./GameBreadcrumb.jsx";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
// Rôle : affiche les listes Monster Hunter Wilds exportables en checklist.
|
// Rôle : affiche les listes Monster Hunter Wilds exportables en checklist.
|
||||||
import { useMemo, useState } from "react";
|
import { useState } from "react";
|
||||||
import { GameListsPage } from "../GameListsPage.jsx";
|
import { GameListsPage } from "../GameListsPage.jsx";
|
||||||
|
|
||||||
export function MhwildsLists({ game, mhwilds, linkedToolboxId, actions }) {
|
export function MhwildsLists({ game, mhwilds, linkedToolboxId, actions }) {
|
||||||
const lists = mhwilds.lists || [];
|
const lists = mhwilds.lists || [];
|
||||||
const [selectedListId, setSelectedListId] = useState("");
|
const [selectedListId, setSelectedListId] = useState("");
|
||||||
const selectedList = useMemo(
|
const selectedList = lists.find((list) => list.id === selectedListId) || lists[0] || null;
|
||||||
() => lists.find((list) => list.id === selectedListId) || lists[0] || null,
|
|
||||||
[lists, selectedListId]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameListsPage
|
<GameListsPage
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// Rôle : affiche une toolbox dans la liste, avec actions rapides et icône personnalisable.
|
// Rôle : affiche une toolbox dans la liste, avec actions rapides et icône personnalisable.
|
||||||
import React from "react";
|
|
||||||
import { CompactDropdown } from "../../components/CompactDropdown.jsx";
|
import { CompactDropdown } from "../../components/CompactDropdown.jsx";
|
||||||
import { Icon } from "../../components/Icon.jsx";
|
import { Icon } from "../../components/Icon.jsx";
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Rôle : affiche les pages toolbox, le panneau latéral et leurs contrôles.
|
// Rôle : affiche les pages toolbox, le panneau latéral et leurs contrôles.
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Icon } from "../../components/Icon.jsx";
|
import { Icon } from "../../components/Icon.jsx";
|
||||||
import { ImportButton } from "../../components/ImportButton.jsx";
|
import { ImportButton } from "../../components/ImportButton.jsx";
|
||||||
import { StorageQuota } from "../../components/StorageQuota.jsx";
|
import { StorageQuota } from "../../components/StorageQuota.jsx";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
|
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,6 @@ const EQUIPMENT_ICONS = [
|
||||||
const TRAIT_ICONS = ["sword", "shield"];
|
const TRAIT_ICONS = ["sword", "shield"];
|
||||||
const DETAIL_TABS = ["stats", "sockets", "craft"];
|
const DETAIL_TABS = ["stats", "sockets", "craft"];
|
||||||
|
|
||||||
function getEquipmentIconLabel(icon) {
|
|
||||||
return EQUIPMENT_ICONS.find((item) => item.icon === icon)?.label || "Équipement";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCharacteristicCategories(equipment) {
|
function getCharacteristicCategories(equipment) {
|
||||||
const categories = equipment.characteristics.map((trait) => trait.category).filter(Boolean);
|
const categories = equipment.characteristics.map((trait) => trait.category).filter(Boolean);
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -396,12 +396,6 @@ export function TableModule({ toolboxId, moduleId, context, editing }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopEditing(address) {
|
|
||||||
setEditingCell("");
|
|
||||||
const parsed = parseCellAddress(address);
|
|
||||||
if (parsed) focusCell(parsed.rowIndex, parsed.columnIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertFormulaText(text, options = {}) {
|
function insertFormulaText(text, options = {}) {
|
||||||
const input = getActiveFormulaInput();
|
const input = getActiveFormulaInput();
|
||||||
if (!input || !text) return false;
|
if (!input || !text) return false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Rôle : fournit l'outil timer avec chronomètre, comptes à rebours et échange texte.
|
// Rôle : fournit l'outil timer avec chronomètre, comptes à rebours et échange texte.
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { Tabs } from "../../../components/Tabs.jsx";
|
import { Tabs } from "../../../components/Tabs.jsx";
|
||||||
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||||
|
import { TOOLBOX_MODULE_DEFINITIONS, TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||||
import { CombosModule } from "./CombosModule.jsx";
|
import { CombosModule } from "./CombosModule.jsx";
|
||||||
|
|
@ -20,26 +21,23 @@ import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
|
||||||
import { TimerModule } from "./TimerModule.jsx";
|
import { TimerModule } from "./TimerModule.jsx";
|
||||||
|
|
||||||
const MODULE_COMPONENTS = {
|
const MODULE_COMPONENTS = {
|
||||||
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: true },
|
notepad: { ...TOOLBOX_MODULE_DEFINITIONS.notepad, Component: NotepadModule },
|
||||||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true },
|
checklist: { ...TOOLBOX_MODULE_DEFINITIONS.checklist, Component: ChecklistModule },
|
||||||
images: { label: "Images", icon: "picture", Component: ImagesModule, editable: true, scrollable: true },
|
images: { ...TOOLBOX_MODULE_DEFINITIONS.images, Component: ImagesModule },
|
||||||
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true, scrollable: true },
|
links: { ...TOOLBOX_MODULE_DEFINITIONS.links, Component: LinksModule },
|
||||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true, scrollable: true },
|
counters: { ...TOOLBOX_MODULE_DEFINITIONS.counters, Component: CountersModule },
|
||||||
combos: { label: "Combos", icon: "controller", Component: CombosModule, editable: true, scrollable: true },
|
combos: { ...TOOLBOX_MODULE_DEFINITIONS.combos, Component: CombosModule },
|
||||||
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: true },
|
calculator: { ...TOOLBOX_MODULE_DEFINITIONS.calculator, Component: CalculatorModule },
|
||||||
table: { label: "Tableau", icon: "table", Component: TableModule, editable: true, scrollable: true },
|
table: { ...TOOLBOX_MODULE_DEFINITIONS.table, Component: TableModule },
|
||||||
timer: { label: "Timer", icon: "clock", Component: TimerModule, editable: true },
|
timer: { ...TOOLBOX_MODULE_DEFINITIONS.timer, Component: TimerModule },
|
||||||
taskPlanner: { label: "Planificateur de tâches", icon: "tasklist", Component: TaskPlannerModule, editable: true, scrollable: true },
|
taskPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.taskPlanner, Component: TaskPlannerModule },
|
||||||
equipmentPlanner: { label: "Planificateur d'équipements", icon: "chest-armor", Component: EquipmentPlannerModule, editable: true, scrollable: true },
|
equipmentPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.equipmentPlanner, Component: EquipmentPlannerModule },
|
||||||
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
|
imageAnnotation: { ...TOOLBOX_MODULE_DEFINITIONS.imageAnnotation, Component: ImageAnnotationModule }
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODULE_COLUMN_IDS = ["left", "right"];
|
const MODULE_COLUMN_IDS = ["left", "right"];
|
||||||
|
|
||||||
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
|
export { TOOLBOX_MODULES };
|
||||||
type,
|
|
||||||
{ label: module.label, icon: module.icon }
|
|
||||||
]));
|
|
||||||
|
|
||||||
function getModuleMap(modules) {
|
function getModuleMap(modules) {
|
||||||
return new Map(modules.map((module) => [module.id, module]));
|
return new Map(modules.map((module) => [module.id, module]));
|
||||||
|
|
@ -191,13 +189,12 @@ export function AddToolControls({ onAdd }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onUpdateModule, onDelete, onModuleOrderChange }) {
|
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onUpdateModule, onDelete, onModuleOrderChange }) {
|
||||||
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
|
const twoColumnOrder = useMemo(() => toolbox.moduleOrder?.two || [[], []], [toolbox.moduleOrder?.two]);
|
||||||
const twoColumnOrder = toolbox.moduleOrder?.two || [[], []];
|
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, toolbox.moduleOrder?.one || []), [toolbox.modules, toolbox.moduleOrder?.one]);
|
||||||
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, toolbox.moduleOrder?.one || []), [toolbox.modules, toolbox.moduleOrder?.one, moduleIdSignature]);
|
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(() => [
|
||||||
getModulesByIds(toolbox.modules, twoColumnOrder[0]),
|
getModulesByIds(toolbox.modules, twoColumnOrder[0]),
|
||||||
getModulesByIds(toolbox.modules, twoColumnOrder[1])
|
getModulesByIds(toolbox.modules, twoColumnOrder[1])
|
||||||
], [toolbox.modules, twoColumnOrder, moduleIdSignature]);
|
], [toolbox.modules, twoColumnOrder]);
|
||||||
const {
|
const {
|
||||||
itemReorder,
|
itemReorder,
|
||||||
getItemProps,
|
getItemProps,
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function tokenize(expression) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberMatch = expression.slice(index).match(/^(?:\d+(?:[\.,]\d*)?|[\.,]\d+)/);
|
const numberMatch = expression.slice(index).match(/^(?:\d+(?:[.,]\d*)?|[.,]\d+)/);
|
||||||
if (numberMatch) {
|
if (numberMatch) {
|
||||||
const value = Number(numberMatch[0].replace(",", "."));
|
const value = Number(numberMatch[0].replace(",", "."));
|
||||||
if (!Number.isFinite(value)) throw new Error("invalid");
|
if (!Number.isFinite(value)) throw new Error("invalid");
|
||||||
|
|
|
||||||
|
|
@ -425,7 +425,7 @@ function exportComboInput(input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseComboInput(text) {
|
function parseComboInput(text) {
|
||||||
const match = String(text || "").trim().match(/^([a-zA-Z]+):([^\[]+?)(?:\[(hold|\d+s)\])?$/);
|
const match = String(text || "").trim().match(/^([a-zA-Z]+):([^[]+?)(?:\[(hold|\d+s)\])?$/);
|
||||||
if (!match || !COMBO_INPUT_KINDS.has(match[1])) throw new Error(`Input combo invalide: ${text}`);
|
if (!match || !COMBO_INPUT_KINDS.has(match[1])) throw new Error(`Input combo invalide: ${text}`);
|
||||||
const input = { kind: match[1], value: match[2].trim() };
|
const input = { kind: match[1], value: match[2].trim() };
|
||||||
if (!input.value) throw new Error(`Input combo invalide: ${text}`);
|
if (!input.value) throw new Error(`Input combo invalide: ${text}`);
|
||||||
|
|
@ -481,7 +481,7 @@ function importCombosText(text, context) {
|
||||||
const sequence = cleanLine.split("|").map((part) => part.trim()).filter((part, index) => index > 0 && !part.includes("=")).join(" | ");
|
const sequence = cleanLine.split("|").map((part) => part.trim()).filter((part, index) => index > 0 && !part.includes("=")).join(" | ");
|
||||||
const comboDevice = lineMeta.device || device;
|
const comboDevice = lineMeta.device || device;
|
||||||
if (!COMBO_DEVICES.has(comboDevice)) throw lineError(index, `périphérique combo invalide: ${comboDevice}`);
|
if (!COMBO_DEVICES.has(comboDevice)) throw lineError(index, `périphérique combo invalide: ${comboDevice}`);
|
||||||
let inputs = [];
|
let inputs;
|
||||||
try {
|
try {
|
||||||
inputs = sequence ? String(sequence).split(">").map((step) => step.split("+").map(parseComboInput)) : [];
|
inputs = sequence ? String(sequence).split(">").map((step) => step.split("+").map(parseComboInput)) : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
173
website/src/features/toolboxes/storage/moduleDataCompactors.js
Normal file
173
website/src/features/toolboxes/storage/moduleDataCompactors.js
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
// Rôle : fournit les compacteurs de données par type d'outil toolbox.
|
||||||
|
export function createModuleDataCompactors(deps) {
|
||||||
|
return {
|
||||||
|
notepad(value) {
|
||||||
|
const normalized = deps.normalizeNotepadData(value);
|
||||||
|
const compact = {};
|
||||||
|
if (normalized.html && normalized.text) compact.html = normalized.html;
|
||||||
|
if (normalized.text) compact.text = normalized.text;
|
||||||
|
if (normalized.updatedAt) compact.updatedAt = normalized.updatedAt;
|
||||||
|
if (normalized.drawingMode === "permanent") compact.drawingMode = "permanent";
|
||||||
|
if (normalized.drawingMode === "permanent" && normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
|
||||||
|
return Object.keys(compact).length ? compact : null;
|
||||||
|
},
|
||||||
|
checklist(value) {
|
||||||
|
const normalized = deps.normalizeChecklistData(value);
|
||||||
|
const sections = normalized.sections.map(deps.compactChecklistSectionForStorage).filter((section) => section.items.length);
|
||||||
|
if (!sections.length) return null;
|
||||||
|
const settings = {
|
||||||
|
...(normalized.hideCompletedSections ? { hideCompletedSections: true } : {}),
|
||||||
|
...(normalized.hideCompletedSectionsFully ? { hideCompletedSectionsFully: true } : {})
|
||||||
|
};
|
||||||
|
if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items };
|
||||||
|
return { ...settings, sections };
|
||||||
|
},
|
||||||
|
images(value) {
|
||||||
|
const images = (Array.isArray(value?.images) ? value.images : [])
|
||||||
|
.filter((image) => image?.dataUrl)
|
||||||
|
.map((image) => {
|
||||||
|
const compact = { id: image.id || deps.uid("image"), dataUrl: image.dataUrl };
|
||||||
|
if (image.label) compact.label = image.label;
|
||||||
|
return compact;
|
||||||
|
});
|
||||||
|
return images.length ? { images } : null;
|
||||||
|
},
|
||||||
|
imageAnnotation(value) {
|
||||||
|
const normalized = deps.normalizeImageAnnotationData(value);
|
||||||
|
if (!normalized.image) return null;
|
||||||
|
const compact = {
|
||||||
|
image: normalized.image,
|
||||||
|
markers: normalized.markers.map((marker) => {
|
||||||
|
const compact = { id: marker.id, x: marker.x, y: marker.y };
|
||||||
|
if (marker.label) compact.label = marker.label;
|
||||||
|
return compact;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if (normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
|
||||||
|
return compact;
|
||||||
|
},
|
||||||
|
links(value) {
|
||||||
|
const links = deps.normalizeLinksData(value).links.map((link) => {
|
||||||
|
const compact = { id: link.id, url: link.url };
|
||||||
|
if (link.title) compact.title = link.title;
|
||||||
|
return compact;
|
||||||
|
});
|
||||||
|
return links.length ? { links } : null;
|
||||||
|
},
|
||||||
|
counters(value) {
|
||||||
|
const counters = deps.normalizeCountersData(value).counters.map((counter) => ({
|
||||||
|
id: counter.id,
|
||||||
|
label: counter.label,
|
||||||
|
value: counter.value
|
||||||
|
}));
|
||||||
|
return counters.length ? { counters } : null;
|
||||||
|
},
|
||||||
|
combos(value) {
|
||||||
|
const normalized = deps.normalizeCombosData(value);
|
||||||
|
const compact = {
|
||||||
|
combos: normalized.combos.map((combo) => {
|
||||||
|
const compactCombo = { id: combo.id, name: combo.name, inputs: combo.inputs };
|
||||||
|
if (combo.category) compactCombo.category = combo.category;
|
||||||
|
if (combo.device) compactCombo.device = combo.device;
|
||||||
|
return compactCombo;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if (normalized.device !== "playstation") compact.device = normalized.device;
|
||||||
|
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
||||||
|
if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
|
||||||
|
if (!compact.combos.length) delete compact.combos;
|
||||||
|
return compact.combos ? compact : null;
|
||||||
|
},
|
||||||
|
calculator(value) {
|
||||||
|
const normalized = deps.normalizeCalculatorData(value);
|
||||||
|
const entries = normalized.entries.map((entry) => {
|
||||||
|
const compact = { id: entry.id, label: entry.label, value: entry.value };
|
||||||
|
if (entry.parentId) compact.parentId = entry.parentId;
|
||||||
|
return compact;
|
||||||
|
});
|
||||||
|
if (!entries.length && !normalized.scrollResults) return null;
|
||||||
|
return normalized.scrollResults ? { entries, scrollResults: true } : { entries };
|
||||||
|
},
|
||||||
|
table(value) {
|
||||||
|
const normalized = deps.normalizeTableData(value);
|
||||||
|
const compact = { cells: normalized.cells };
|
||||||
|
if (normalized.rows !== 10) compact.rows = normalized.rows;
|
||||||
|
if (normalized.columns !== 6) compact.columns = normalized.columns;
|
||||||
|
if (Object.keys(normalized.rowLabels).length) compact.rowLabels = normalized.rowLabels;
|
||||||
|
if (Object.keys(normalized.columnLabels).length) compact.columnLabels = normalized.columnLabels;
|
||||||
|
return Object.keys(compact.cells).length || compact.rows || compact.columns || compact.rowLabels || compact.columnLabels ? compact : null;
|
||||||
|
},
|
||||||
|
timer(value) {
|
||||||
|
const normalized = deps.normalizeTimerData(value);
|
||||||
|
const compact = {};
|
||||||
|
if (normalized.activeTab !== "stopwatch") compact.activeTab = normalized.activeTab;
|
||||||
|
if (normalized.scrollResults) compact.scrollResults = true;
|
||||||
|
if (normalized.sortResults) compact.sortResults = true;
|
||||||
|
if (normalized.stopwatch.elapsedMs || normalized.stopwatch.startedAt || normalized.stopwatch.laps.length) {
|
||||||
|
compact.stopwatch = {
|
||||||
|
...(normalized.stopwatch.elapsedMs ? { elapsedMs: normalized.stopwatch.elapsedMs } : {}),
|
||||||
|
...(normalized.stopwatch.startedAt ? { startedAt: normalized.stopwatch.startedAt } : {}),
|
||||||
|
...(normalized.stopwatch.laps.length ? { laps: normalized.stopwatch.laps } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (normalized.countdowns.length) {
|
||||||
|
compact.countdowns = normalized.countdowns.map((countdown) => {
|
||||||
|
const { alertMode, autoRefresh, startMode, startTime, ...compactCountdown } = countdown;
|
||||||
|
if (alertMode !== "off") compactCountdown.alertMode = alertMode;
|
||||||
|
if (autoRefresh) compactCountdown.autoRefresh = true;
|
||||||
|
if (startMode === "time" && startTime) {
|
||||||
|
compactCountdown.startMode = startMode;
|
||||||
|
compactCountdown.startTime = startTime;
|
||||||
|
}
|
||||||
|
return compactCountdown;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Object.keys(compact).length ? compact : null;
|
||||||
|
},
|
||||||
|
taskPlanner(value) {
|
||||||
|
const normalized = deps.normalizeTaskPlannerData(value);
|
||||||
|
const compact = {};
|
||||||
|
if (normalized.weeklyResetDay !== deps.defaultTaskPlannerWeeklyResetDay) compact.weeklyResetDay = normalized.weeklyResetDay;
|
||||||
|
if (normalized.resetTime !== deps.defaultTaskPlannerResetTime) compact.resetTime = normalized.resetTime;
|
||||||
|
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
|
||||||
|
if (normalized.hideCompleted) compact.hideCompleted = true;
|
||||||
|
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
||||||
|
if (normalized.tasks.length) {
|
||||||
|
compact.tasks = normalized.tasks.map((task) => {
|
||||||
|
const compactTask = { id: task.id, title: task.title, type: task.type };
|
||||||
|
if (task.description) compactTask.description = task.description;
|
||||||
|
if (task.category) compactTask.category = task.category;
|
||||||
|
if (task.checked) compactTask.checked = true;
|
||||||
|
if (task.checked && task.checkedAt) compactTask.checkedAt = task.checkedAt;
|
||||||
|
if (task.type === "daily" && task.dailyResetTime && task.dailyResetTime !== normalized.resetTime) compactTask.dailyResetTime = task.dailyResetTime;
|
||||||
|
if (task.type === "weekly" && task.weeklyResetDay !== undefined) compactTask.weeklyResetDay = task.weeklyResetDay;
|
||||||
|
return compactTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
|
||||||
|
if (normalized.relations.length) {
|
||||||
|
compact.relations = normalized.relations.map((relation) => {
|
||||||
|
const compactRelation = { id: relation.id, fromTaskId: relation.fromTaskId, toTaskId: relation.toTaskId };
|
||||||
|
if (relation.prerequisite) compactRelation.prerequisite = true;
|
||||||
|
return compactRelation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Object.keys(compact).length ? compact : null;
|
||||||
|
},
|
||||||
|
equipmentPlanner(value) {
|
||||||
|
const normalized = deps.normalizeEquipmentPlannerData(value);
|
||||||
|
if (!normalized.equipments.length) return null;
|
||||||
|
const compact = {
|
||||||
|
types: normalized.types.map((type) => {
|
||||||
|
const compactType = { id: type.id, title: type.title };
|
||||||
|
if (type.icon !== "shield") compactType.icon = type.icon;
|
||||||
|
if (type.collapsed) compactType.collapsed = true;
|
||||||
|
return compactType;
|
||||||
|
}),
|
||||||
|
equipments: normalized.equipments.map(deps.compactEquipmentPlannerEquipmentForStorage)
|
||||||
|
};
|
||||||
|
if (normalized.typeOrder.length) compact.typeOrder = normalized.typeOrder;
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
// Rôle : normalise, compacte et sérialise les données toolbox persistées.
|
// Rôle : normalise, compacte et sérialise les données toolbox persistées.
|
||||||
|
import { getDefaultModuleTitle } from "../toolboxModuleRegistry.js";
|
||||||
|
import { createModuleDataCompactors } from "./moduleDataCompactors.js";
|
||||||
|
|
||||||
const ID_PREFIXES = {
|
const ID_PREFIXES = {
|
||||||
tbx: "t",
|
tbx: "t",
|
||||||
mod: "m",
|
mod: "m",
|
||||||
|
|
@ -71,21 +74,6 @@ const TOOLBOX_ICON_FILES = [
|
||||||
"tower.png",
|
"tower.png",
|
||||||
"wheel.png"
|
"wheel.png"
|
||||||
];
|
];
|
||||||
const DEFAULT_MODULE_TITLES = {
|
|
||||||
notepad: "Bloc notes",
|
|
||||||
checklist: "Checklist",
|
|
||||||
images: "Images",
|
|
||||||
links: "Liens",
|
|
||||||
counters: "Compteurs",
|
|
||||||
combos: "Combos",
|
|
||||||
calculator: "Calculateur",
|
|
||||||
table: "Tableau",
|
|
||||||
timer: "Timer",
|
|
||||||
taskPlanner: "Planificateur de tâches",
|
|
||||||
equipmentPlanner: "Planificateur d'équipements",
|
|
||||||
imageAnnotation: "Annotation d'images"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
|
export const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
|
||||||
export const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;
|
export const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;
|
||||||
|
|
||||||
|
|
@ -102,10 +90,6 @@ export function uid(prefix) {
|
||||||
return `${ID_PREFIXES[prefix] || "x"}${randomToken()}`;
|
return `${ID_PREFIXES[prefix] || "x"}${randomToken()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultModuleTitle(type) {
|
|
||||||
return DEFAULT_MODULE_TITLES[type] || "Outil";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeToolboxIcon(icon) {
|
export function normalizeToolboxIcon(icon) {
|
||||||
const value = String(icon || "").trim();
|
const value = String(icon || "").trim();
|
||||||
const file = value.replace(TOOLBOX_ICON_BASE, "").split("/").pop();
|
const file = value.replace(TOOLBOX_ICON_BASE, "").split("/").pop();
|
||||||
|
|
@ -859,7 +843,7 @@ function normalizeEquipmentPlannerEquipment(equipment, typeIds) {
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((link, index, links) => links.findIndex((item) => item.pairKey === link.pairKey) === index)
|
.filter((link, index, links) => links.findIndex((item) => item.pairKey === link.pairKey) === index)
|
||||||
.map(({ pairKey, ...link }) => link);
|
.map(({ pairKey: _pairKey, ...link }) => link);
|
||||||
const categories = [
|
const categories = [
|
||||||
...characteristics.map((trait) => trait.category),
|
...characteristics.map((trait) => trait.category),
|
||||||
...socketItems.flatMap((socketItem) => socketItem.bonuses.map((bonus) => bonus.category))
|
...socketItems.flatMap((socketItem) => socketItem.bonuses.map((bonus) => bonus.category))
|
||||||
|
|
@ -999,7 +983,7 @@ export function summarizeEquipmentPlannerData(data) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const numericTotals = [...totals.values()]
|
const numericTotals = [...totals.values()]
|
||||||
.map(({ iconSources, sourceKinds, color, sourceKind, ...total }) => ({
|
.map(({ iconSources: _iconSources, sourceKinds: _sourceKinds, color, sourceKind, ...total }) => ({
|
||||||
...total,
|
...total,
|
||||||
...(color ? { color } : {}),
|
...(color ? { color } : {}),
|
||||||
sourceKind
|
sourceKind
|
||||||
|
|
@ -1122,195 +1106,28 @@ function compactEquipmentPlannerEquipmentForStorage(equipment) {
|
||||||
return compact;
|
return compact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MODULE_DATA_COMPACTORS = createModuleDataCompactors({
|
||||||
|
uid,
|
||||||
|
normalizeNotepadData,
|
||||||
|
normalizeChecklistData,
|
||||||
|
normalizeLinksData,
|
||||||
|
normalizeCountersData,
|
||||||
|
normalizeCombosData,
|
||||||
|
normalizeCalculatorData,
|
||||||
|
normalizeTableData,
|
||||||
|
normalizeTimerData,
|
||||||
|
normalizeTaskPlannerData,
|
||||||
|
normalizeEquipmentPlannerData,
|
||||||
|
normalizeImageAnnotationData,
|
||||||
|
compactChecklistSectionForStorage,
|
||||||
|
compactEquipmentPlannerEquipmentForStorage,
|
||||||
|
defaultTaskPlannerWeeklyResetDay: DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY,
|
||||||
|
defaultTaskPlannerResetTime: DEFAULT_TASK_PLANNER_RESET_TIME
|
||||||
|
});
|
||||||
|
|
||||||
export function compactModuleDataForStorage(type, value) {
|
export function compactModuleDataForStorage(type, value) {
|
||||||
if (type === "notepad") {
|
const compactor = MODULE_DATA_COMPACTORS[type];
|
||||||
const normalized = normalizeNotepadData(value);
|
return compactor ? compactor(value) : value;
|
||||||
const compact = {};
|
|
||||||
if (normalized.html && normalized.text) compact.html = normalized.html;
|
|
||||||
if (normalized.text) compact.text = normalized.text;
|
|
||||||
if (normalized.updatedAt) compact.updatedAt = normalized.updatedAt;
|
|
||||||
if (normalized.drawingMode === "permanent") compact.drawingMode = "permanent";
|
|
||||||
if (normalized.drawingMode === "permanent" && normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
|
|
||||||
return Object.keys(compact).length ? compact : null;
|
|
||||||
}
|
|
||||||
if (type === "checklist") {
|
|
||||||
const normalized = normalizeChecklistData(value);
|
|
||||||
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length);
|
|
||||||
if (!sections.length) return null;
|
|
||||||
const settings = {
|
|
||||||
...(normalized.hideCompletedSections ? { hideCompletedSections: true } : {}),
|
|
||||||
...(normalized.hideCompletedSectionsFully ? { hideCompletedSectionsFully: true } : {})
|
|
||||||
};
|
|
||||||
if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items };
|
|
||||||
return { ...settings, sections };
|
|
||||||
}
|
|
||||||
if (type === "images") {
|
|
||||||
const images = (Array.isArray(value?.images) ? value.images : [])
|
|
||||||
.filter((image) => image?.dataUrl)
|
|
||||||
.map((image) => {
|
|
||||||
const compact = { id: image.id || uid("image"), dataUrl: image.dataUrl };
|
|
||||||
if (image.label) compact.label = image.label;
|
|
||||||
return compact;
|
|
||||||
});
|
|
||||||
return images.length ? { images } : null;
|
|
||||||
}
|
|
||||||
if (type === "imageAnnotation") {
|
|
||||||
const normalized = normalizeImageAnnotationData(value);
|
|
||||||
if (!normalized.image) return null;
|
|
||||||
const compact = {
|
|
||||||
image: normalized.image,
|
|
||||||
markers: normalized.markers.map((marker) => {
|
|
||||||
const compact = { id: marker.id, x: marker.x, y: marker.y };
|
|
||||||
if (marker.label) compact.label = marker.label;
|
|
||||||
return compact;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
if (normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
|
|
||||||
return compact;
|
|
||||||
}
|
|
||||||
if (type === "links") {
|
|
||||||
const links = normalizeLinksData(value).links.map((link) => {
|
|
||||||
const compact = {
|
|
||||||
id: link.id,
|
|
||||||
url: link.url
|
|
||||||
};
|
|
||||||
if (link.title) compact.title = link.title;
|
|
||||||
return compact;
|
|
||||||
});
|
|
||||||
return links.length ? { links } : null;
|
|
||||||
}
|
|
||||||
if (type === "counters") {
|
|
||||||
const counters = normalizeCountersData(value).counters.map((counter) => ({
|
|
||||||
id: counter.id,
|
|
||||||
label: counter.label,
|
|
||||||
value: counter.value
|
|
||||||
}));
|
|
||||||
return counters.length ? { counters } : null;
|
|
||||||
}
|
|
||||||
if (type === "combos") {
|
|
||||||
const normalized = normalizeCombosData(value);
|
|
||||||
const compact = {
|
|
||||||
combos: normalized.combos.map((combo) => {
|
|
||||||
const compactCombo = {
|
|
||||||
id: combo.id,
|
|
||||||
name: combo.name,
|
|
||||||
inputs: combo.inputs
|
|
||||||
};
|
|
||||||
if (combo.category) compactCombo.category = combo.category;
|
|
||||||
if (combo.device) compactCombo.device = combo.device;
|
|
||||||
return compactCombo;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
if (normalized.device !== "playstation") compact.device = normalized.device;
|
|
||||||
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
|
||||||
if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
|
|
||||||
if (!compact.combos.length) delete compact.combos;
|
|
||||||
return compact.combos ? compact : null;
|
|
||||||
}
|
|
||||||
if (type === "calculator") {
|
|
||||||
const normalized = normalizeCalculatorData(value);
|
|
||||||
const entries = normalized.entries.map((entry) => {
|
|
||||||
const compact = {
|
|
||||||
id: entry.id,
|
|
||||||
label: entry.label,
|
|
||||||
value: entry.value
|
|
||||||
};
|
|
||||||
if (entry.parentId) compact.parentId = entry.parentId;
|
|
||||||
return compact;
|
|
||||||
});
|
|
||||||
if (!entries.length && !normalized.scrollResults) return null;
|
|
||||||
return normalized.scrollResults ? { entries, scrollResults: true } : { entries };
|
|
||||||
}
|
|
||||||
if (type === "table") {
|
|
||||||
const normalized = normalizeTableData(value);
|
|
||||||
const compact = { cells: normalized.cells };
|
|
||||||
if (normalized.rows !== 10) compact.rows = normalized.rows;
|
|
||||||
if (normalized.columns !== 6) compact.columns = normalized.columns;
|
|
||||||
if (Object.keys(normalized.rowLabels).length) compact.rowLabels = normalized.rowLabels;
|
|
||||||
if (Object.keys(normalized.columnLabels).length) compact.columnLabels = normalized.columnLabels;
|
|
||||||
return Object.keys(compact.cells).length || compact.rows || compact.columns || compact.rowLabels || compact.columnLabels ? compact : null;
|
|
||||||
}
|
|
||||||
if (type === "timer") {
|
|
||||||
const normalized = normalizeTimerData(value);
|
|
||||||
const compact = {};
|
|
||||||
if (normalized.activeTab !== "stopwatch") compact.activeTab = normalized.activeTab;
|
|
||||||
if (normalized.scrollResults) compact.scrollResults = true;
|
|
||||||
if (normalized.sortResults) compact.sortResults = true;
|
|
||||||
if (normalized.stopwatch.elapsedMs || normalized.stopwatch.startedAt || normalized.stopwatch.laps.length) {
|
|
||||||
compact.stopwatch = {
|
|
||||||
...(normalized.stopwatch.elapsedMs ? { elapsedMs: normalized.stopwatch.elapsedMs } : {}),
|
|
||||||
...(normalized.stopwatch.startedAt ? { startedAt: normalized.stopwatch.startedAt } : {}),
|
|
||||||
...(normalized.stopwatch.laps.length ? { laps: normalized.stopwatch.laps } : {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (normalized.countdowns.length) {
|
|
||||||
compact.countdowns = normalized.countdowns.map((countdown) => {
|
|
||||||
const { alertMode, autoRefresh, startMode, startTime, ...compactCountdown } = countdown;
|
|
||||||
if (alertMode !== "off") compactCountdown.alertMode = alertMode;
|
|
||||||
if (autoRefresh) compactCountdown.autoRefresh = true;
|
|
||||||
if (startMode === "time" && startTime) {
|
|
||||||
compactCountdown.startMode = startMode;
|
|
||||||
compactCountdown.startTime = startTime;
|
|
||||||
}
|
|
||||||
return compactCountdown;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Object.keys(compact).length ? compact : null;
|
|
||||||
}
|
|
||||||
if (type === "taskPlanner") {
|
|
||||||
const normalized = normalizeTaskPlannerData(value);
|
|
||||||
const compact = {};
|
|
||||||
if (normalized.weeklyResetDay !== DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) compact.weeklyResetDay = normalized.weeklyResetDay;
|
|
||||||
if (normalized.resetTime !== DEFAULT_TASK_PLANNER_RESET_TIME) compact.resetTime = normalized.resetTime;
|
|
||||||
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
|
|
||||||
if (normalized.hideCompleted) compact.hideCompleted = true;
|
|
||||||
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
|
||||||
if (normalized.tasks.length) {
|
|
||||||
compact.tasks = normalized.tasks.map((task) => {
|
|
||||||
const compactTask = {
|
|
||||||
id: task.id,
|
|
||||||
title: task.title,
|
|
||||||
type: task.type
|
|
||||||
};
|
|
||||||
if (task.description) compactTask.description = task.description;
|
|
||||||
if (task.category) compactTask.category = task.category;
|
|
||||||
if (task.checked) compactTask.checked = true;
|
|
||||||
if (task.checked && task.checkedAt) compactTask.checkedAt = task.checkedAt;
|
|
||||||
if (task.type === "daily" && task.dailyResetTime && task.dailyResetTime !== normalized.resetTime) compactTask.dailyResetTime = task.dailyResetTime;
|
|
||||||
if (task.type === "weekly" && task.weeklyResetDay !== undefined) compactTask.weeklyResetDay = task.weeklyResetDay;
|
|
||||||
return compactTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
|
|
||||||
if (normalized.relations.length) {
|
|
||||||
compact.relations = normalized.relations.map((relation) => {
|
|
||||||
const compactRelation = {
|
|
||||||
id: relation.id,
|
|
||||||
fromTaskId: relation.fromTaskId,
|
|
||||||
toTaskId: relation.toTaskId
|
|
||||||
};
|
|
||||||
if (relation.prerequisite) compactRelation.prerequisite = true;
|
|
||||||
return compactRelation;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Object.keys(compact).length ? compact : null;
|
|
||||||
}
|
|
||||||
if (type === "equipmentPlanner") {
|
|
||||||
const normalized = normalizeEquipmentPlannerData(value);
|
|
||||||
if (!normalized.equipments.length) return null;
|
|
||||||
const compact = {
|
|
||||||
types: normalized.types.map((type) => {
|
|
||||||
const compactType = { id: type.id, title: type.title };
|
|
||||||
if (type.icon !== "shield") compactType.icon = type.icon;
|
|
||||||
if (type.collapsed) compactType.collapsed = true;
|
|
||||||
return compactType;
|
|
||||||
}),
|
|
||||||
equipments: normalized.equipments.map(compactEquipmentPlannerEquipmentForStorage)
|
|
||||||
};
|
|
||||||
if (normalized.typeOrder.length) compact.typeOrder = normalized.typeOrder;
|
|
||||||
return compact;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") {
|
export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") {
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export function useIndexedToolboxes(onError) {
|
||||||
}
|
}
|
||||||
loadStore();
|
loadStore();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, []);
|
}, [onError]);
|
||||||
|
|
||||||
function persistToolboxes(nextToolboxes) {
|
function persistToolboxes(nextToolboxes) {
|
||||||
const normalized = compactToolboxesForStorage(nextToolboxes).map(normalizeToolbox).filter(Boolean);
|
const normalized = compactToolboxesForStorage(nextToolboxes).map(normalizeToolbox).filter(Boolean);
|
||||||
|
|
|
||||||
128
website/src/features/toolboxes/toolboxModuleRegistry.js
Normal file
128
website/src/features/toolboxes/toolboxModuleRegistry.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Rôle : centralise les métadonnées légères des outils toolbox sans importer leurs composants.
|
||||||
|
export const TOOLBOX_MODULE_DEFINITIONS = {
|
||||||
|
notepad: {
|
||||||
|
label: "Bloc notes",
|
||||||
|
defaultTitle: "Bloc notes",
|
||||||
|
mode: "Texte riche",
|
||||||
|
icon: "notepad",
|
||||||
|
editable: true,
|
||||||
|
libraryCategory: "notesTracking"
|
||||||
|
},
|
||||||
|
checklist: {
|
||||||
|
label: "Checklist",
|
||||||
|
defaultTitle: "Checklist",
|
||||||
|
mode: "Catégories + sans catégorie",
|
||||||
|
icon: "checklist",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "notesTracking"
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
label: "Images",
|
||||||
|
defaultTitle: "Images",
|
||||||
|
mode: "Média",
|
||||||
|
icon: "picture",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "references"
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
label: "Liens",
|
||||||
|
defaultTitle: "Liens",
|
||||||
|
mode: "Liste simple",
|
||||||
|
icon: "link",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "references"
|
||||||
|
},
|
||||||
|
counters: {
|
||||||
|
label: "Compteurs",
|
||||||
|
defaultTitle: "Compteurs",
|
||||||
|
mode: "Valeurs",
|
||||||
|
icon: "abacus",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "notesTracking"
|
||||||
|
},
|
||||||
|
combos: {
|
||||||
|
label: "Combos",
|
||||||
|
defaultTitle: "Combos",
|
||||||
|
mode: "Catégories",
|
||||||
|
icon: "controller",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "buildsCommands"
|
||||||
|
},
|
||||||
|
calculator: {
|
||||||
|
label: "Calculateur",
|
||||||
|
defaultTitle: "Calculateur",
|
||||||
|
mode: "Arborescence",
|
||||||
|
icon: "calculator",
|
||||||
|
editable: true,
|
||||||
|
libraryCategory: "calculationData"
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
label: "Tableau",
|
||||||
|
defaultTitle: "Tableau",
|
||||||
|
mode: "Grille + formules",
|
||||||
|
icon: "table",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "calculationData"
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
label: "Timer",
|
||||||
|
defaultTitle: "Timer",
|
||||||
|
mode: "Chrono + comptes à rebours",
|
||||||
|
icon: "clock",
|
||||||
|
editable: true,
|
||||||
|
libraryCategory: "timeRoutines"
|
||||||
|
},
|
||||||
|
taskPlanner: {
|
||||||
|
label: "Planificateur de tâches",
|
||||||
|
defaultTitle: "Planificateur de tâches",
|
||||||
|
mode: "Catégories + parent/enfant",
|
||||||
|
icon: "tasklist",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "timeRoutines"
|
||||||
|
},
|
||||||
|
equipmentPlanner: {
|
||||||
|
label: "Planificateur d'équipements",
|
||||||
|
defaultTitle: "Planificateur d'équipements",
|
||||||
|
mode: "Build + sertissages",
|
||||||
|
icon: "chest-armor",
|
||||||
|
editable: true,
|
||||||
|
scrollable: true,
|
||||||
|
libraryCategory: "buildsCommands"
|
||||||
|
},
|
||||||
|
imageAnnotation: {
|
||||||
|
label: "Annotation d'images",
|
||||||
|
defaultTitle: "Annotation d'images",
|
||||||
|
mode: "Image + marqueurs",
|
||||||
|
icon: "map",
|
||||||
|
editable: true,
|
||||||
|
libraryCategory: "references"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(TOOLBOX_MODULE_DEFINITIONS).map(([type, definition]) => [
|
||||||
|
type,
|
||||||
|
{ label: definition.label, icon: definition.icon }
|
||||||
|
]));
|
||||||
|
|
||||||
|
export const LIBRARY_CATEGORY_DEFINITIONS = [
|
||||||
|
{ key: "notesTracking", types: ["notepad", "checklist", "counters"] },
|
||||||
|
{ key: "references", types: ["images", "imageAnnotation", "links"] },
|
||||||
|
{ key: "calculationData", types: ["calculator", "table"] },
|
||||||
|
{ key: "timeRoutines", types: ["timer", "taskPlanner"] },
|
||||||
|
{ key: "buildsCommands", types: ["combos", "equipmentPlanner"] }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getToolboxModuleDefinition(type) {
|
||||||
|
return TOOLBOX_MODULE_DEFINITIONS[type] || TOOLBOX_MODULE_DEFINITIONS.notepad;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultModuleTitle(type) {
|
||||||
|
return getToolboxModuleDefinition(type).defaultTitle || "Outil";
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
// Rôle : point d'entrée React, assemble données, routes, shell et overlays.
|
// Rôle : point d'entrée React, assemble données, routes, shell et overlays.
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import "./styles/main.scss";
|
import "./styles/main.scss";
|
||||||
import { AppOverlays } from "./components/AppOverlays.jsx";
|
import { AppOverlays } from "./components/AppOverlays.jsx";
|
||||||
import { Shell } from "./components/Shell.jsx";
|
import { Shell } from "./components/Shell.jsx";
|
||||||
import { ToolboxDrawer } from "./features/toolboxes/ToolboxPages.jsx";
|
|
||||||
import { useIndexedToolboxes } from "./features/toolboxes/storage/useIndexedToolboxes.js";
|
import { useIndexedToolboxes } from "./features/toolboxes/storage/useIndexedToolboxes.js";
|
||||||
import { useToolboxActions } from "./features/toolboxes/useToolboxActions.js";
|
import { useToolboxActions } from "./features/toolboxes/useToolboxActions.js";
|
||||||
import { useAppData } from "./hooks/useAppData.js";
|
import { useAppData } from "./hooks/useAppData.js";
|
||||||
|
|
@ -14,6 +13,8 @@ import { RouteContent } from "./router/RouteContent.jsx";
|
||||||
import { useHashRoute } from "./router/hashRouter.js";
|
import { useHashRoute } from "./router/hashRouter.js";
|
||||||
import { normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
import { normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
||||||
|
|
||||||
|
const ToolboxDrawer = lazy(() => import("./features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxDrawer })));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const route = useHashRoute();
|
const route = useHashRoute();
|
||||||
const { siteContent, siteContentError, games, gamesError, mhwilds, diablo4 } = useAppData(route);
|
const { siteContent, siteContentError, games, gamesError, mhwilds, diablo4 } = useAppData(route);
|
||||||
|
|
@ -25,7 +26,8 @@ function App() {
|
||||||
const [image, setImage] = useState(null);
|
const [image, setImage] = useState(null);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [storageError, setStorageError] = useState("");
|
const [storageError, setStorageError] = useState("");
|
||||||
const store = useIndexedToolboxes((message) => setStorageError(message));
|
const handleStorageError = useCallback((message) => setStorageError(message), []);
|
||||||
|
const store = useIndexedToolboxes(handleStorageError);
|
||||||
|
|
||||||
const notify = useCallback((message) => {
|
const notify = useCallback((message) => {
|
||||||
setNotifications((current) => [
|
setNotifications((current) => [
|
||||||
|
|
@ -113,6 +115,7 @@ function App() {
|
||||||
addImageFiles={actions.addImageFiles}
|
addImageFiles={actions.addImageFiles}
|
||||||
/>
|
/>
|
||||||
{drawerGameId && (
|
{drawerGameId && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
<ToolboxDrawer
|
<ToolboxDrawer
|
||||||
gameId={drawerGameId}
|
gameId={drawerGameId}
|
||||||
game={getGame(drawerGameId)}
|
game={getGame(drawerGameId)}
|
||||||
|
|
@ -126,6 +129,7 @@ function App() {
|
||||||
updateModuleData={store.updateModuleData}
|
updateModuleData={store.updateModuleData}
|
||||||
addImageFiles={actions.addImageFiles}
|
addImageFiles={actions.addImageFiles}
|
||||||
/>
|
/>
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
<AppOverlays
|
<AppOverlays
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Rôle : présente le projet, son fonctionnement local et les repères de contribution.
|
// Rôle : présente le projet, son fonctionnement local et les repères de contribution.
|
||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Icon } from "../components/Icon.jsx";
|
import { Icon } from "../components/Icon.jsx";
|
||||||
import { RichText } from "../components/RichText.jsx";
|
import { RichText } from "../components/RichText.jsx";
|
||||||
import { HomeOnboardingSection } from "./HomeOnboardingSection.jsx";
|
import { HomeOnboardingSection } from "./HomeOnboardingSection.jsx";
|
||||||
|
|
@ -30,7 +30,7 @@ export function AboutPage({ siteContent }) {
|
||||||
<h2 id="about-story-title">{content.sectionsTitle}</h2>
|
<h2 id="about-story-title">{content.sectionsTitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="about-story-list">
|
<div className="about-story-list">
|
||||||
{content.sections.map((section, index) => (
|
{content.sections.map((section) => (
|
||||||
<article className="about-story-item" key={section.title}>
|
<article className="about-story-item" key={section.title}>
|
||||||
<h3>
|
<h3>
|
||||||
<span className="module-icon" aria-hidden="true">
|
<span className="module-icon" aria-hidden="true">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Rôle : affiche la section de prise en main de la home avec étapes, slider et accès rapides.
|
// Rôle : affiche la section de prise en main de la home avec étapes, slider et accès rapides.
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
const SLIDE_INTERVAL_MS = 4500;
|
const SLIDE_INTERVAL_MS = 4500;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// Rôle : affiche la page d'accueil et les contenus éditoriaux principaux.
|
// Rôle : affiche la page d'accueil et les contenus éditoriaux principaux.
|
||||||
import React from "react";
|
import { TOOLBOX_MODULES } from "../features/toolboxes/toolboxModuleRegistry.js";
|
||||||
import { TOOLBOX_MODULES } from "../features/toolboxes/modules/index.jsx";
|
|
||||||
import { RichText } from "../components/RichText.jsx";
|
import { RichText } from "../components/RichText.jsx";
|
||||||
import { HomeOnboardingSection } from "./HomeOnboardingSection.jsx";
|
import { HomeOnboardingSection } from "./HomeOnboardingSection.jsx";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,7 @@
|
||||||
// Rôle : affiche la librairie des outils avec des exemples locaux non persistés.
|
// Rôle : affiche la librairie des outils avec des exemples locaux non persistés.
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Icon } from "../components/Icon.jsx";
|
import { Icon } from "../components/Icon.jsx";
|
||||||
import { CalculatorModule } from "../features/toolboxes/modules/CalculatorModule.jsx";
|
import { LIBRARY_CATEGORY_DEFINITIONS, getToolboxModuleDefinition } from "../features/toolboxes/toolboxModuleRegistry.js";
|
||||||
import { ChecklistModule } from "../features/toolboxes/modules/ChecklistModule.jsx";
|
|
||||||
import { CombosModule } from "../features/toolboxes/modules/CombosModule.jsx";
|
|
||||||
import { CountersModule } from "../features/toolboxes/modules/CountersModule.jsx";
|
|
||||||
import { EquipmentPlannerModule } from "../features/toolboxes/modules/EquipmentPlannerModule.jsx";
|
|
||||||
import { ImageAnnotationModule } from "../features/toolboxes/modules/ImageAnnotationModule.jsx";
|
|
||||||
import { ImagesModule } from "../features/toolboxes/modules/ImagesModule.jsx";
|
|
||||||
import { LinksModule } from "../features/toolboxes/modules/LinksModule.jsx";
|
|
||||||
import { NotepadModule } from "../features/toolboxes/modules/NotepadModule.jsx";
|
|
||||||
import { TableModule } from "../features/toolboxes/modules/TableModule.jsx";
|
|
||||||
import { TaskPlannerModule } from "../features/toolboxes/modules/TaskPlannerModule.jsx";
|
|
||||||
import { TimerModule } from "../features/toolboxes/modules/TimerModule.jsx";
|
|
||||||
import {
|
import {
|
||||||
clampQty,
|
clampQty,
|
||||||
hostnameFromUrl,
|
hostnameFromUrl,
|
||||||
|
|
@ -35,28 +24,20 @@ import {
|
||||||
const LIBRARY_TOOLBOX_ID = "library";
|
const LIBRARY_TOOLBOX_ID = "library";
|
||||||
|
|
||||||
const MODULE_COMPONENTS = {
|
const MODULE_COMPONENTS = {
|
||||||
notepad: { label: "Bloc notes", mode: "Texte riche", icon: "notepad", Component: NotepadModule },
|
notepad: lazy(() => import("../features/toolboxes/modules/NotepadModule.jsx").then((module) => ({ default: module.NotepadModule }))),
|
||||||
checklist: { label: "Checklist", mode: "Catégories + sans catégorie", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true },
|
checklist: lazy(() => import("../features/toolboxes/modules/ChecklistModule.jsx").then((module) => ({ default: module.ChecklistModule }))),
|
||||||
images: { label: "Images", mode: "Média", icon: "picture", Component: ImagesModule, editable: true, scrollable: true },
|
images: lazy(() => import("../features/toolboxes/modules/ImagesModule.jsx").then((module) => ({ default: module.ImagesModule }))),
|
||||||
links: { label: "Liens", mode: "Liste simple", icon: "link", Component: LinksModule, editable: true, scrollable: true },
|
links: lazy(() => import("../features/toolboxes/modules/LinksModule.jsx").then((module) => ({ default: module.LinksModule }))),
|
||||||
counters: { label: "Compteurs", mode: "Valeurs", icon: "abacus", Component: CountersModule, editable: true, scrollable: true },
|
counters: lazy(() => import("../features/toolboxes/modules/CountersModule.jsx").then((module) => ({ default: module.CountersModule }))),
|
||||||
combos: { label: "Combos", mode: "Catégories", icon: "controller", Component: CombosModule, editable: true, scrollable: true },
|
combos: lazy(() => import("../features/toolboxes/modules/CombosModule.jsx").then((module) => ({ default: module.CombosModule }))),
|
||||||
calculator: { label: "Calculateur", mode: "Arborescence", icon: "calculator", Component: CalculatorModule },
|
calculator: lazy(() => import("../features/toolboxes/modules/CalculatorModule.jsx").then((module) => ({ default: module.CalculatorModule }))),
|
||||||
table: { label: "Tableau", mode: "Grille + formules", icon: "table", Component: TableModule, scrollable: true },
|
table: lazy(() => import("../features/toolboxes/modules/TableModule.jsx").then((module) => ({ default: module.TableModule }))),
|
||||||
timer: { label: "Timer", mode: "Chrono + comptes à rebours", icon: "clock", Component: TimerModule },
|
timer: lazy(() => import("../features/toolboxes/modules/TimerModule.jsx").then((module) => ({ default: module.TimerModule }))),
|
||||||
taskPlanner: { label: "Planificateur de tâches", mode: "Catégories + parent/enfant", icon: "tasklist", Component: TaskPlannerModule, editable: true, scrollable: true },
|
taskPlanner: lazy(() => import("../features/toolboxes/modules/TaskPlannerModule.jsx").then((module) => ({ default: module.TaskPlannerModule }))),
|
||||||
equipmentPlanner: { label: "Planificateur d'équipements", mode: "Build + sertissages", icon: "chest-armor", Component: EquipmentPlannerModule, editable: true, scrollable: true },
|
equipmentPlanner: lazy(() => import("../features/toolboxes/modules/EquipmentPlannerModule.jsx").then((module) => ({ default: module.EquipmentPlannerModule }))),
|
||||||
imageAnnotation: { label: "Annotation d'images", mode: "Image + marqueurs", icon: "map", Component: ImageAnnotationModule, editable: true }
|
imageAnnotation: lazy(() => import("../features/toolboxes/modules/ImageAnnotationModule.jsx").then((module) => ({ default: module.ImageAnnotationModule })))
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIBRARY_CATEGORY_DEFINITIONS = [
|
|
||||||
{ key: "notesTracking", types: ["notepad", "checklist", "counters"] },
|
|
||||||
{ key: "references", types: ["images", "imageAnnotation", "links"] },
|
|
||||||
{ key: "calculationData", types: ["calculator", "table"] },
|
|
||||||
{ key: "timeRoutines", types: ["timer", "taskPlanner"] },
|
|
||||||
{ key: "buildsCommands", types: ["combos", "equipmentPlanner"] }
|
|
||||||
];
|
|
||||||
|
|
||||||
function cloneData(value) {
|
function cloneData(value) {
|
||||||
if (value === undefined) return {};
|
if (value === undefined) return {};
|
||||||
if (typeof structuredClone === "function") return structuredClone(value);
|
if (typeof structuredClone === "function") return structuredClone(value);
|
||||||
|
|
@ -262,7 +243,7 @@ export function LibraryPage({ siteContent, actions }) {
|
||||||
key={module.id}
|
key={module.id}
|
||||||
module={module}
|
module={module}
|
||||||
sourceData={libraryPayload.modules?.[module.id] || {}}
|
sourceData={libraryPayload.modules?.[module.id] || {}}
|
||||||
description={getToolDescription(siteContent, MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad)}
|
description={getToolDescription(siteContent, getToolboxModuleDefinition(module.type))}
|
||||||
categoryTitle={activeCategory.title}
|
categoryTitle={activeCategory.title}
|
||||||
docs={content.toolDocs?.[module.type]}
|
docs={content.toolDocs?.[module.type]}
|
||||||
featureHeading={content.featureHeading}
|
featureHeading={content.featureHeading}
|
||||||
|
|
@ -299,7 +280,7 @@ function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategor
|
||||||
}}>{category.title}</a>
|
}}>{category.title}</a>
|
||||||
<ul hidden={category.key !== activeCategoryKey}>
|
<ul hidden={category.key !== activeCategoryKey}>
|
||||||
{category.modules.map((module) => {
|
{category.modules.map((module) => {
|
||||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
const definition = getToolboxModuleDefinition(module.type);
|
||||||
const title = module.title || definition.label || "Outil";
|
const title = module.title || definition.label || "Outil";
|
||||||
const anchorId = getLibraryAnchorId(module);
|
const anchorId = getLibraryAnchorId(module);
|
||||||
return (
|
return (
|
||||||
|
|
@ -328,8 +309,8 @@ function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategor
|
||||||
}
|
}
|
||||||
|
|
||||||
function LibraryToolExample({ module, sourceData, description, categoryTitle, docs, featureHeading, advancedHeading, advancedCopyLabel, advancedCopySuccess, controlLegendHeading, context }) {
|
function LibraryToolExample({ module, sourceData, description, categoryTitle, docs, featureHeading, advancedHeading, advancedCopyLabel, advancedCopySuccess, controlLegendHeading, context }) {
|
||||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
const definition = getToolboxModuleDefinition(module.type);
|
||||||
const Component = definition.Component;
|
const Component = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||||
const [scrollable, setScrollable] = useState(false);
|
const [scrollable, setScrollable] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const title = module.title || definition.label || "Outil";
|
const title = module.title || definition.label || "Outil";
|
||||||
|
|
@ -388,7 +369,9 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||||
|
<Suspense fallback={<div className="empty"><p>Chargement de l'outil...</p></div>}>
|
||||||
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={editing} />
|
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={editing} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,28 @@
|
||||||
// Rôle : choisit la page à rendre selon la route active.
|
// Rôle : choisit la page à rendre selon la route active.
|
||||||
import React from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { GameRoute } from "../features/games/GameRoute.jsx";
|
|
||||||
import { GamesPage } from "../features/games/GamesPage.jsx";
|
|
||||||
import { ToolboxesPage, ToolboxPage } from "../features/toolboxes/ToolboxPages.jsx";
|
|
||||||
import { AboutPage } from "../pages/AboutPage.jsx";
|
import { AboutPage } from "../pages/AboutPage.jsx";
|
||||||
import { HomePage } from "../pages/HomePage.jsx";
|
import { HomePage } from "../pages/HomePage.jsx";
|
||||||
import { LibraryPage } from "../pages/LibraryPage.jsx";
|
|
||||||
import { navigate } from "./hashRouter.js";
|
import { navigate } from "./hashRouter.js";
|
||||||
|
|
||||||
|
const GameRoute = lazy(() => import("../features/games/GameRoute.jsx").then((module) => ({ default: module.GameRoute })));
|
||||||
|
const GamesPage = lazy(() => import("../features/games/GamesPage.jsx").then((module) => ({ default: module.GamesPage })));
|
||||||
|
const LibraryPage = lazy(() => import("../pages/LibraryPage.jsx").then((module) => ({ default: module.LibraryPage })));
|
||||||
|
const ToolboxPage = lazy(() => import("../features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxPage })));
|
||||||
|
const ToolboxesPage = lazy(() => import("../features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxesPage })));
|
||||||
|
|
||||||
|
function RouteLoader() {
|
||||||
|
return <section className="empty"><p>Chargement...</p></section>;
|
||||||
|
}
|
||||||
|
|
||||||
export function RouteContent(props) {
|
export function RouteContent(props) {
|
||||||
const { route } = props;
|
const { route } = props;
|
||||||
if (route === "/") return <HomePage {...props} />;
|
if (route === "/") return <HomePage {...props} />;
|
||||||
if (route === "/library") return <LibraryPage {...props} />;
|
|
||||||
if (route === "/about") return <AboutPage siteContent={props.siteContent} />;
|
if (route === "/about") return <AboutPage siteContent={props.siteContent} />;
|
||||||
if (route === "/toolboxes") return <ToolboxesPage {...props} />;
|
if (route === "/library") return <Suspense fallback={<RouteLoader />}><LibraryPage {...props} /></Suspense>;
|
||||||
if (route.startsWith("/toolbox/")) return <ToolboxPage {...props} toolboxId={route.split("/")[2]} />;
|
if (route === "/toolboxes") return <Suspense fallback={<RouteLoader />}><ToolboxesPage {...props} /></Suspense>;
|
||||||
if (route === "/games") return <GamesPage {...props} />;
|
if (route.startsWith("/toolbox/")) return <Suspense fallback={<RouteLoader />}><ToolboxPage {...props} toolboxId={route.split("/")[2]} /></Suspense>;
|
||||||
if (route.startsWith("/games/")) return <GameRoute {...props} gameId={route.split("/")[2]} category={route.split("/")[3] || ""} />;
|
if (route === "/games") return <Suspense fallback={<RouteLoader />}><GamesPage {...props} /></Suspense>;
|
||||||
|
if (route.startsWith("/games/")) return <Suspense fallback={<RouteLoader />}><GameRoute {...props} gameId={route.split("/")[2]} category={route.split("/")[3] || ""} /></Suspense>;
|
||||||
navigate("/");
|
navigate("/");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
6396
website/src/styles/toolboxes/_core.scss
Normal file
6396
website/src/styles/toolboxes/_core.scss
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue