Improve toolbox architecture and initial bundle loading
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s

This commit is contained in:
Shinuwa 2026-08-14 09:13:53 +02:00
parent f76cddcc95
commit b221257616
34 changed files with 8358 additions and 6744 deletions

View file

@ -1,5 +1,4 @@
// Rôle : affiche les listes de jeu transformables en checklist.
import { Icon } from "../../components/Icon.jsx";
import { CopyChecklistItemsButton } from "./CopyChecklistItemsButton.jsx";
import { GameBreadcrumb } from "./GameBreadcrumb.jsx";

View file

@ -1,14 +1,11 @@
// 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";
export function MhwildsLists({ game, mhwilds, linkedToolboxId, actions }) {
const lists = mhwilds.lists || [];
const [selectedListId, setSelectedListId] = useState("");
const selectedList = useMemo(
() => lists.find((list) => list.id === selectedListId) || lists[0] || null,
[lists, selectedListId]
);
const selectedList = lists.find((list) => list.id === selectedListId) || lists[0] || null;
return (
<GameListsPage

View file

@ -1,5 +1,4 @@
// 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 { Icon } from "../../components/Icon.jsx";
import {

View file

@ -1,5 +1,5 @@
// 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 { ImportButton } from "../../components/ImportButton.jsx";
import { StorageQuota } from "../../components/StorageQuota.jsx";

View file

@ -1,5 +1,5 @@
// 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 { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import {

View file

@ -43,10 +43,6 @@ const EQUIPMENT_ICONS = [
const TRAIT_ICONS = ["sword", "shield"];
const DETAIL_TABS = ["stats", "sockets", "craft"];
function getEquipmentIconLabel(icon) {
return EQUIPMENT_ICONS.find((item) => item.icon === icon)?.label || "Équipement";
}
function getCharacteristicCategories(equipment) {
const categories = equipment.characteristics.map((trait) => trait.category).filter(Boolean);
return [

View file

@ -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 = {}) {
const input = getActiveFormulaInput();
if (!input || !text) return false;

View file

@ -1,5 +1,5 @@
// 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 { Tabs } from "../../../components/Tabs.jsx";
import { useDraftForm } from "../../../hooks/useDraftForm.js";

View file

@ -6,6 +6,7 @@ import { Icon } from "../../../components/Icon.jsx";
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
import { TOOLBOX_MODULE_DEFINITIONS, TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
import { CalculatorModule } from "./CalculatorModule.jsx";
import { ChecklistModule } from "./ChecklistModule.jsx";
import { CombosModule } from "./CombosModule.jsx";
@ -20,26 +21,23 @@ import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
import { TimerModule } from "./TimerModule.jsx";
const MODULE_COMPONENTS = {
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: true },
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true },
images: { label: "Images", icon: "picture", Component: ImagesModule, editable: true, scrollable: true },
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true, scrollable: true },
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true, scrollable: true },
combos: { label: "Combos", icon: "controller", Component: CombosModule, editable: true, scrollable: true },
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: true },
table: { label: "Tableau", icon: "table", Component: TableModule, editable: true, scrollable: true },
timer: { label: "Timer", icon: "clock", Component: TimerModule, editable: true },
taskPlanner: { label: "Planificateur de tâches", icon: "tasklist", Component: TaskPlannerModule, editable: true, scrollable: true },
equipmentPlanner: { label: "Planificateur d'équipements", icon: "chest-armor", Component: EquipmentPlannerModule, editable: true, scrollable: true },
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
notepad: { ...TOOLBOX_MODULE_DEFINITIONS.notepad, Component: NotepadModule },
checklist: { ...TOOLBOX_MODULE_DEFINITIONS.checklist, Component: ChecklistModule },
images: { ...TOOLBOX_MODULE_DEFINITIONS.images, Component: ImagesModule },
links: { ...TOOLBOX_MODULE_DEFINITIONS.links, Component: LinksModule },
counters: { ...TOOLBOX_MODULE_DEFINITIONS.counters, Component: CountersModule },
combos: { ...TOOLBOX_MODULE_DEFINITIONS.combos, Component: CombosModule },
calculator: { ...TOOLBOX_MODULE_DEFINITIONS.calculator, Component: CalculatorModule },
table: { ...TOOLBOX_MODULE_DEFINITIONS.table, Component: TableModule },
timer: { ...TOOLBOX_MODULE_DEFINITIONS.timer, Component: TimerModule },
taskPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.taskPlanner, Component: TaskPlannerModule },
equipmentPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.equipmentPlanner, Component: EquipmentPlannerModule },
imageAnnotation: { ...TOOLBOX_MODULE_DEFINITIONS.imageAnnotation, Component: ImageAnnotationModule }
};
const MODULE_COLUMN_IDS = ["left", "right"];
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
type,
{ label: module.label, icon: module.icon }
]));
export { TOOLBOX_MODULES };
function getModuleMap(modules) {
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 }) {
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
const twoColumnOrder = toolbox.moduleOrder?.two || [[], []];
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, toolbox.moduleOrder?.one || []), [toolbox.modules, toolbox.moduleOrder?.one, moduleIdSignature]);
const twoColumnOrder = useMemo(() => toolbox.moduleOrder?.two || [[], []], [toolbox.moduleOrder?.two]);
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, toolbox.moduleOrder?.one || []), [toolbox.modules, toolbox.moduleOrder?.one]);
const columns = useMemo(() => [
getModulesByIds(toolbox.modules, twoColumnOrder[0]),
getModulesByIds(toolbox.modules, twoColumnOrder[1])
], [toolbox.modules, twoColumnOrder, moduleIdSignature]);
], [toolbox.modules, twoColumnOrder]);
const {
itemReorder,
getItemProps,

View file

@ -60,7 +60,7 @@ function tokenize(expression) {
continue;
}
const numberMatch = expression.slice(index).match(/^(?:\d+(?:[\.,]\d*)?|[\.,]\d+)/);
const numberMatch = expression.slice(index).match(/^(?:\d+(?:[.,]\d*)?|[.,]\d+)/);
if (numberMatch) {
const value = Number(numberMatch[0].replace(",", "."));
if (!Number.isFinite(value)) throw new Error("invalid");

View file

@ -425,7 +425,7 @@ function exportComboInput(input) {
}
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}`);
const input = { kind: match[1], value: match[2].trim() };
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 comboDevice = lineMeta.device || device;
if (!COMBO_DEVICES.has(comboDevice)) throw lineError(index, `périphérique combo invalide: ${comboDevice}`);
let inputs = [];
let inputs;
try {
inputs = sequence ? String(sequence).split(">").map((step) => step.split("+").map(parseComboInput)) : [];
} catch (error) {

View 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;
}
};
}

View file

@ -1,4 +1,7 @@
// 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 = {
tbx: "t",
mod: "m",
@ -71,21 +74,6 @@ const TOOLBOX_ICON_FILES = [
"tower.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 DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;
@ -102,10 +90,6 @@ export function uid(prefix) {
return `${ID_PREFIXES[prefix] || "x"}${randomToken()}`;
}
function getDefaultModuleTitle(type) {
return DEFAULT_MODULE_TITLES[type] || "Outil";
}
export function normalizeToolboxIcon(icon) {
const value = String(icon || "").trim();
const file = value.replace(TOOLBOX_ICON_BASE, "").split("/").pop();
@ -859,7 +843,7 @@ function normalizeEquipmentPlannerEquipment(equipment, typeIds) {
})
.filter(Boolean)
.filter((link, index, links) => links.findIndex((item) => item.pairKey === link.pairKey) === index)
.map(({ pairKey, ...link }) => link);
.map(({ pairKey: _pairKey, ...link }) => link);
const categories = [
...characteristics.map((trait) => trait.category),
...socketItems.flatMap((socketItem) => socketItem.bonuses.map((bonus) => bonus.category))
@ -999,7 +983,7 @@ export function summarizeEquipmentPlannerData(data) {
});
const numericTotals = [...totals.values()]
.map(({ iconSources, sourceKinds, color, sourceKind, ...total }) => ({
.map(({ iconSources: _iconSources, sourceKinds: _sourceKinds, color, sourceKind, ...total }) => ({
...total,
...(color ? { color } : {}),
sourceKind
@ -1122,195 +1106,28 @@ function compactEquipmentPlannerEquipmentForStorage(equipment) {
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) {
if (type === "notepad") {
const normalized = 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;
}
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;
const compactor = MODULE_DATA_COMPACTORS[type];
return compactor ? compactor(value) : value;
}
export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") {

View file

@ -65,7 +65,7 @@ export function useIndexedToolboxes(onError) {
}
loadStore();
return () => { cancelled = true; };
}, []);
}, [onError]);
function persistToolboxes(nextToolboxes) {
const normalized = compactToolboxesForStorage(nextToolboxes).map(normalizeToolbox).filter(Boolean);

View 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";
}