diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index 4ebfd03..c06eb46 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -8,7 +8,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - Déclarer l'outil dans `website/src/features/toolboxes/modules/index.jsx`. - Ajouter les textes nécessaires dans `website/public/data/site.json`. - Mettre à jour la validation de `site.json` dans `tests/helpers/data-validation.mjs`. -- Mettre à jour la normalisation et le stockage compact dans `website/src/features/toolboxes/storage/modules/.js`, puis vérifier l'export public dans `website/src/features/toolboxes/storage/toolboxStorage.js`. +- Mettre à jour la normalisation et le stockage compact dans `website/src/features/toolboxes/storage/toolboxStorage.js`. - Mettre à jour `docs/STORAGE_SCHEMA.md`. - Vérifier l'import et l'export si l'outil stocke des données. - Si l'outil a un contenu représentable en texte, ajouter ou vérifier l'import/export texte round-trip dans le panneau d'ajout. diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 543207e..500c63a 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -4,9 +4,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { SOCKET_SHAPES, formatValue, getSocketItemName, setItemCategory } from "../website/src/features/toolboxes/modules/equipmentPlannerOptions.js"; import { applyDueResets, getTaskParentMap, getTaskTreeIds, moveTask } from "../website/src/features/toolboxes/modules/taskPlannerUtils.js"; -import { createInput, getInputLabel, normalizeCustomKey, parseHoldSeconds } from "../website/src/features/toolboxes/modules/combosUtils.js"; -import { getBoundsFromStartAndSize, getSelectionBounds, moveSelectionCells, parseTsvBlock, pasteCellBlock } from "../website/src/features/toolboxes/modules/tableUtils.js"; -import { canToggleAutoRefresh, getCountdownValidationError, isAlertGuarded } from "../website/src/features/toolboxes/modules/timerOptions.js"; test("toolbox storage, cards and pages are wired", async () => { const source = await readFile("website/src/main.jsx", "utf8"); @@ -14,10 +11,6 @@ test("toolbox storage, cards and pages are wired", async () => { const toolboxPages = await readFile("website/src/features/toolboxes/ToolboxPages.jsx", "utf8"); const toolboxActions = await readFile("website/src/features/toolboxes/useToolboxActions.js", "utf8"); const toolboxStorage = await readFile("website/src/features/toolboxes/storage/toolboxStorage.js", "utf8"); - const storageCompactorRegistry = await readFile("website/src/features/toolboxes/storage/modules/index.js", "utf8"); - const checklistStorage = await readFile("website/src/features/toolboxes/storage/modules/checklist.js", "utf8"); - const timerStorage = await readFile("website/src/features/toolboxes/storage/modules/timer.js", "utf8"); - const equipmentStorage = await readFile("website/src/features/toolboxes/storage/modules/equipmentPlanner.js", "utf8"); const indexedToolboxesHook = await readFile("website/src/features/toolboxes/storage/useIndexedToolboxes.js", "utf8"); const storageQuota = await readFile("website/src/components/StorageQuota.jsx", "utf8"); const importButton = await readFile("website/src/components/ImportButton.jsx", "utf8"); @@ -38,27 +31,25 @@ test("toolbox storage, cards and pages are wired", async () => { assert.match(toolboxStorage, /TOOLBOX_ICON_FILES/); assert.match(toolboxStorage, /export function normalizeToolboxIcon/); assert.match(toolboxStorage, /export function createGlobalExportPayload/); - assert.match(checklistStorage, /qtyTarget/); - assert.match(checklistStorage, /qtyCurrent/); - assert.match(checklistStorage, /hideCompletedSections/); - assert.match(checklistStorage, /hideCompletedSectionsFully/); - assert.match(checklistStorage, /hideWhenComplete/); - assert.match(equipmentStorage, /collapsed/); + assert.match(toolboxStorage, /qtyTarget/); + assert.match(toolboxStorage, /qtyCurrent/); + assert.match(toolboxStorage, /hideCompletedSections/); + assert.match(toolboxStorage, /hideCompletedSectionsFully/); + assert.match(toolboxStorage, /hideWhenComplete/); + assert.match(toolboxStorage, /collapsed/); assert.match(toolboxStorage, /module\.scrollable/); - assert.match(toolboxStorage, /normalizeLinksData/); - assert.match(toolboxStorage, /normalizeCountersData/); - assert.match(toolboxStorage, /normalizeCombosData/); - assert.match(toolboxStorage, /normalizeCalculatorData/); - assert.match(toolboxStorage, /normalizeTableData/); - assert.match(toolboxStorage, /normalizeTimerData/); - assert.match(toolboxStorage, /normalizeTaskPlannerData/); - assert.match(toolboxStorage, /normalizeEquipmentPlannerData/); - assert.match(toolboxStorage, /summarizeEquipmentPlannerData/); - assert.match(toolboxStorage, /normalizeImageAnnotationData/); + assert.match(toolboxStorage, /export function normalizeLinksData/); + assert.match(toolboxStorage, /export function normalizeCountersData/); + assert.match(toolboxStorage, /export function normalizeCombosData/); + assert.match(toolboxStorage, /export function normalizeCalculatorData/); + assert.match(toolboxStorage, /export function normalizeTableData/); + assert.match(toolboxStorage, /export function normalizeTimerData/); + assert.match(toolboxStorage, /export function normalizeTaskPlannerData/); + assert.match(toolboxStorage, /export function normalizeEquipmentPlannerData/); + assert.match(toolboxStorage, /export function summarizeEquipmentPlannerData/); + assert.match(toolboxStorage, /export function normalizeImageAnnotationData/); assert.match(toolboxStorage, /export function compactModuleDataForStorage\(type, value\)/); - assert.match(storageCompactorRegistry, /export const MODULE_DATA_COMPACTORS/); - assert.match(storageCompactorRegistry, /timer: compactTimerDataForStorage/); - assert.match(timerStorage, /scrollResults/); + assert.match(toolboxStorage, /scrollResults/); assert.match(toolboxCard, /DEFAULT_TOOLBOX_ICON/); assert.match(toolboxCard, /export function ToolboxCard/); assert.match(toolboxCard, /export function ToolboxIconPicker/); @@ -117,24 +108,14 @@ test("toolbox module registry and modules expose expected behavior", async () => const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "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 combosParts = await readFile("website/src/features/toolboxes/modules/CombosParts.jsx", "utf8"); - const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8"); const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8"); const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8"); - const tableGridParts = await readFile("website/src/features/toolboxes/modules/TableGridParts.jsx", "utf8"); - const tableUtils = await readFile("website/src/features/toolboxes/modules/tableUtils.js", "utf8"); const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8"); const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8"); - const timerParts = await readFile("website/src/features/toolboxes/modules/TimerParts.jsx", "utf8"); - const timerOptions = await readFile("website/src/features/toolboxes/modules/timerOptions.js", "utf8"); const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8"); const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8"); const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8"); const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8"); - const equipmentPlannerEditors = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerEditors.jsx", "utf8"); - const equipmentPlannerItem = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerItem.jsx", "utf8"); - const equipmentPlannerPickers = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerPickers.jsx", "utf8"); - const equipmentPlannerSummary = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerSummary.jsx", "utf8"); const equipmentPlannerOptions = await readFile("website/src/features/toolboxes/modules/equipmentPlannerOptions.js", "utf8"); const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8"); const imagesModule = await readFile("website/src/features/toolboxes/modules/ImagesModule.jsx", "utf8"); @@ -189,26 +170,26 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(notepadModule, /TextExchangeActions/); assert.match(notepadModule, /type="notepad"/); assert.match(combosModule, /export function CombosModule/); - assert.match(combosUtils, /keyboardMouse/); - assert.match(combosUtils, /Nintendo 64/); - assert.match(combosParts, /export function ComboInputToken/); - assert.match(combosParts, /export function ComboSequence/); + assert.match(combosModule, /keyboardMouse/); + assert.match(combosModule, /Nintendo 64/); + assert.match(combosModule, /ComboInputToken/); + assert.match(combosModule, /ComboSequence/); assert.match(combosModule, /keyboardLayout/); assert.match(combosModule, /simultaneousMode/); - assert.match(combosUtils, /SPECIAL_INPUT_GROUP/); - assert.match(combosUtils, /grab/); - assert.match(combosUtils, /protect/); - assert.match(combosUtils, /shoot/); + assert.match(combosModule, /SPECIAL_INPUT_GROUP/); + assert.match(combosModule, /grab/); + assert.match(combosModule, /protect/); + assert.match(combosModule, /shoot/); assert.match(combosModule, /holdMs/); assert.match(combosModule, /toggleSelectedInputHold/); assert.match(combosModule, /combo-hold-field/); - assert.match(combosParts, /useInlineEdit/); + assert.match(combosModule, /useInlineEdit/); assert.match(combosModule, /useGroupedReorder/); assert.match(combosModule, /reorderFeatures/); assert.match(combosModule, /namespace: `combos-\$\{moduleId\}`/); assert.match(combosModule, /getGroupBoundaryProps/); assert.match(combosModule, /combos-category-boundary-drop-zone/); - assert.match(combosUtils, /function setComboItemCategory/); + assert.match(combosModule, /function setComboItemCategory/); assert.match(combosModule, /setItemGroup: setComboItemCategory/); assert.doesNotMatch(combosModule, /canMoveItem:/); assert.doesNotMatch(combosModule, /canMoveGroup:/); @@ -247,11 +228,11 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(tableModule, /draggingSelectionRef/); assert.match(tableModule, /movingSelectionRef/); assert.match(tableModule, /copiedSelectionRef/); - assert.match(tableUtils, /moveSelectionCells/); - assert.match(tableUtils, /pasteCellBlock/); - assert.match(tableUtils, /parseTsvBlock/); - assert.match(tableGridParts, /is-move-target/); - assert.match(tableUtils, /isMultiCellSelection/); + assert.match(tableModule, /moveSelectionCells/); + assert.match(tableModule, /pasteCellBlock/); + assert.match(tableModule, /parseTsvBlock/); + assert.match(tableModule, /is-move-target/); + assert.match(tableModule, /isMultiCellSelection/); assert.match(tableModule, /handleCellKeyDown/); assert.match(tableModule, /clearSelection/); assert.match(tableModule, /event\.key === "Delete"/); @@ -259,10 +240,10 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(tableModule, /ArrowDown/); assert.match(tableModule, /ArrowLeft/); assert.match(tableModule, /ArrowRight/); - assert.match(tableGridParts, /export function HeaderLabelInput/); - assert.match(tableGridParts, /useInlineEdit/); + assert.match(tableModule, /function HeaderLabelInput/); + assert.match(tableModule, /useInlineEdit/); assert.match(tableModule, /TextExchangeActions/); - assert.match(tableGridParts, /event\.target\.select/); + assert.match(tableModule, /event\.target\.select/); assert.match(tableFormulaEngine, /export function evaluateTableCell/); assert.doesNotMatch(tableFormulaEngine, /Function\(/); assert.doesNotMatch(tableFormulaEngine, /\beval\(/); @@ -270,7 +251,7 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(timerModule, /Tabs/); assert.match(timerModule, /stopwatch/); assert.match(timerModule, /countdown/); - assert.match(timerParts, /useInlineEdit/); + assert.match(timerModule, /useInlineEdit/); assert.match(timerModule, /useDraftForm/); assert.match(timerModule, /normalizeTimerData/); assert.match(timerModule, /TextExchangeActions/); @@ -296,16 +277,11 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(equipmentPlannerModule, /export function EquipmentPlannerModule/); assert.match(equipmentPlannerModule, /normalizeEquipmentPlannerData/); assert.match(equipmentPlannerModule, /summarizeEquipmentPlannerData/); - assert.match(equipmentPlannerModule, /EquipmentItem/); - assert.match(equipmentPlannerModule, /EquipmentPlannerSummary/); - assert.match(equipmentPlannerEditors, /socketItems/); - assert.match(equipmentPlannerEditors, /socketLinks/); + assert.match(equipmentPlannerModule, /socketItems/); + assert.match(equipmentPlannerModule, /socketLinks/); assert.match(equipmentPlannerModule, /useGroupedReorder/); - assert.match(equipmentPlannerEditors, /reorderFeatures/); + assert.match(equipmentPlannerModule, /reorderFeatures/); assert.match(equipmentPlannerModule, /TextExchangeActions/); - assert.match(equipmentPlannerItem, /export function EquipmentItem/); - assert.match(equipmentPlannerPickers, /export function IconChoiceDropdown/); - assert.match(equipmentPlannerSummary, /export function EquipmentPlannerSummary/); assert.match(equipmentPlannerOptions, /export const EQUIPMENT_ICONS/); assert.match(equipmentPlannerOptions, /export function formatValue/); assert.match(checklistModule, /export function ChecklistModule/); @@ -374,11 +350,7 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(timerModule, /useGroupedReorder/); assert.match(timerModule, /reorderEnabled={!data\.sortResults}/); assert.match(timerModule, /parentId=\{moduleId\}/); - assert.match(timerParts, /timer-drag-handle/); - assert.match(timerParts, /export function StopwatchControls/); - assert.match(timerParts, /export function CountdownControls/); - assert.match(timerParts, /export function CountdownList/); - assert.match(timerOptions, /export const ALERT_MIN_DELAY_MS/); + assert.match(timerModule, /timer-drag-handle/); assert.match(textImport, /export function parseColonImportLines/); assert.match(textImport, /export function exportModuleText/); assert.match(textImport, /export function importModuleText/); @@ -424,46 +396,3 @@ test("equipment planner options expose stable formatting and socket helpers", () assert.deepEqual(setItemCategory({ id: "item", category: "DPS" }, ""), { id: "item" }); assert.equal(getSocketItemName({ socketItems: [{ id: "gem", name: "Ruby" }] }, "gem"), "Ruby"); }); - -test("combos helpers keep labels, hold parsing and custom keys stable", () => { - assert.deepEqual(createInput("button", "a", 1500), { kind: "button", value: "a", holdMs: 1500 }); - assert.deepEqual(createInput("button", "a"), { kind: "button", value: "a" }); - assert.equal(getInputLabel({ kind: "key", value: "q" }), "Q"); - assert.equal(getInputLabel({ kind: "mouse", value: "left" }), "Clic G"); - assert.equal(parseHoldSeconds("120s"), 99); - assert.equal(parseHoldSeconds("abc"), 0); - assert.equal(normalizeCustomKey(" Ctrl Alt Shift "), "Ctrl Alt Shift"); -}); - -test("table helpers keep selection, movement and TSV behavior stable", () => { - const selection = { - anchor: { rowIndex: 2, columnIndex: 3 }, - focus: { rowIndex: 1, columnIndex: 1 } - }; - assert.deepEqual(getSelectionBounds(selection), { - startRow: 1, - endRow: 2, - startColumn: 1, - endColumn: 3 - }); - assert.deepEqual(parseTsvBlock("A\tB\nC\tD"), { - height: 2, - width: 2, - values: [["A", "B"], ["C", "D"]] - }); - - const moved = moveSelectionCells({ A1: "a", B1: "b", A2: "c" }, getBoundsFromStartAndSize(0, 0, 1, 2), getBoundsFromStartAndSize(1, 1, 1, 2)); - assert.deepEqual(moved, { A2: "c", B2: "a", C2: "b" }); - - const pasted = pasteCellBlock({}, parseTsvBlock("A\t\nC\tD"), 0, 0, 2, 2); - assert.deepEqual(pasted.cells, { A1: "A", A2: "C", B2: "D" }); -}); - -test("timer options keep alert guards and countdown validation stable", () => { - assert.equal(isAlertGuarded({ type: "interval", intervalMs: 60_000 }), true); - assert.equal(isAlertGuarded({ type: "interval", intervalMs: 600_000 }), false); - assert.equal(canToggleAutoRefresh({ type: "daily_time" }), true); - assert.equal(canToggleAutoRefresh({ type: "interval", intervalMs: 60_000 }), false); - assert.equal(getCountdownValidationError("duration", { duration: { hours: "", minutes: "", seconds: "" } }, {}), "Valeur requise."); - assert.equal(getCountdownValidationError("daily_time", { time: { hours: "12", minutes: "30", seconds: "" } }, {}), ""); -}); diff --git a/website/src/features/toolboxes/modules/CombosModule.jsx b/website/src/features/toolboxes/modules/CombosModule.jsx index 89d7498..47f2644 100644 --- a/website/src/features/toolboxes/modules/CombosModule.jsx +++ b/website/src/features/toolboxes/modules/CombosModule.jsx @@ -1,25 +1,333 @@ // Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte. import { useMemo, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; +import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; import { applyGroupedReorderOperation, getGroupedEntries, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; -import { ComboInputToken, ComboSequence, InlineTextInput } from "./CombosParts.jsx"; -import { - DEVICE_OPTIONS, - createCombo, - createInput, - getComboCategory, - getHoldSeconds, - getPaletteColumns, - getPaletteGroups, - normalizeCustomKey, - parseHoldSeconds, - setComboItemCategory -} from "./combosUtils.js"; + +const DEVICE_OPTIONS = [ + { value: "playstation", label: "PlayStation" }, + { value: "xbox", label: "Xbox" }, + { value: "switch", label: "Switch" }, + { value: "n64", label: "Nintendo 64" }, + { value: "keyboardMouse", label: "Clavier/Souris" } +]; + +const SPECIAL_INPUT_GROUP = { title: "Spéciale", column: "actions", inputs: [["button", "fist"], ["button", "foot"], ["button", "grab"], ["button", "protect"], ["button", "shoot"]] }; + +const DEVICE_GROUPS = { + playstation: [ + { title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, + { title: "Actions", column: "actions", inputs: [["button", "triangle"], ["button", "circle"], ["button", "cross"], ["button", "square"], ["button", "l1"], ["button", "r1"], ["button", "l2"], ["button", "r2"]] }, + SPECIAL_INPUT_GROUP, + { title: "Système", inputs: [["button", "options"], ["button", "share"], ["button", "l3"], ["button", "r3"]] } + ], + xbox: [ + { title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, + { title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "x"], ["button", "y"], ["button", "lb"], ["button", "rb"], ["button", "lt"], ["button", "rt"]] }, + SPECIAL_INPUT_GROUP, + { title: "Système", inputs: [["button", "menu"], ["button", "view"], ["button", "ls"], ["button", "rs"]] } + ], + switch: [ + { title: "Directions", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, + { title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "x"], ["button", "y"], ["button", "l"], ["button", "r"], ["button", "zl"], ["button", "zr"]] }, + SPECIAL_INPUT_GROUP, + { title: "Système", inputs: [["button", "plus"], ["button", "minus"], ["button", "ls"], ["button", "rs"]] } + ], + n64: [ + { title: "Stick", kind: "directions", column: "movement", inputs: [["direction", "stick-up-left"], ["direction", "stick-up"], ["direction", "stick-up-right"], ["direction", "stick-left"], null, ["direction", "stick-right"], ["direction", "stick-down-left"], ["direction", "stick-down"], ["direction", "stick-down-right"]] }, + { title: "D-Pad", kind: "directions", column: "movement", inputs: [["direction", "up-left"], ["direction", "up"], ["direction", "up-right"], ["direction", "left"], null, ["direction", "right"], ["direction", "down-left"], ["direction", "down"], ["direction", "down-right"]] }, + { title: "Actions", column: "actions", inputs: [["button", "a"], ["button", "b"], ["button", "z"], ["button", "start"], ["button", "l"], ["button", "r"]] }, + SPECIAL_INPUT_GROUP, + { title: "C-buttons", kind: "directions", inputs: [["button", "c-up"], ["button", "c-down"], ["button", "c-left"], ["button", "c-right"]] } + ] +}; + +const KEYBOARD_GROUPS = { + azerty: [ + { title: "ZQSD", kind: "directions", column: "movement", inputs: [null, ["key", "z"], null, ["key", "q"], ["key", "s"], ["key", "d"], null, null, null] }, + { title: "Flèches", kind: "directions", column: "movement", inputs: [null, ["direction", "up"], null, ["direction", "left"], ["direction", "down"], ["direction", "right"], null, null, null] }, + { title: "Touches", column: "actions", inputs: [["key", "ctrl"], ["key", "alt"], ["key", "shift"], ["key", "space"], ["key", "enter"], ["key", "tab"], ["key", "e"], ["key", "a"], ["key", "r"], ["key", "f"]] }, + SPECIAL_INPUT_GROUP, + { title: "Souris", inputs: [["mouse", "left"], ["mouse", "right"], ["mouse", "middle"], ["mouse", "wheel-up"], ["mouse", "wheel-down"]] } + ], + qwerty: [ + { title: "WASD", kind: "directions", column: "movement", inputs: [null, ["key", "w"], null, ["key", "a"], ["key", "s"], ["key", "d"], null, null, null] }, + { title: "Flèches", kind: "directions", column: "movement", inputs: [null, ["direction", "up"], null, ["direction", "left"], ["direction", "down"], ["direction", "right"], null, null, null] }, + { title: "Touches", column: "actions", inputs: [["key", "ctrl"], ["key", "alt"], ["key", "shift"], ["key", "space"], ["key", "enter"], ["key", "tab"], ["key", "e"], ["key", "q"], ["key", "r"], ["key", "f"]] }, + SPECIAL_INPUT_GROUP, + { title: "Souris", inputs: [["mouse", "left"], ["mouse", "right"], ["mouse", "middle"], ["mouse", "wheel-up"], ["mouse", "wheel-down"]] } + ] +}; + +const INPUT_LABELS = { + triangle: "△", + circle: "○", + cross: "×", + square: "□", + options: "Options", + share: "Share", + up: "↑", + down: "↓", + left: "←", + right: "→", + "up-left": "↖", + "up-right": "↗", + "down-left": "↙", + "down-right": "↘", + "stick-up": "↑", + "stick-down": "↓", + "stick-left": "←", + "stick-right": "→", + "stick-up-left": "↖", + "stick-up-right": "↗", + "stick-down-left": "↙", + "stick-down-right": "↘", + plus: "+", + minus: "-", + "c-up": "C↑", + "c-down": "C↓", + "c-left": "C←", + "c-right": "C→", + fist: "Poing", + foot: "Pied", + grab: "Grab", + protect: "Protect", + shoot: "Shoot", + space: "Espace" +}; + +const INPUT_ICONS = { + fist: "fist", + foot: "foot", + grab: "grab", + protect: "shield", + shoot: "gun" +}; + +const MOUSE_LABELS = { + left: "Clic G", + right: "Clic D", + middle: "Molette", + "wheel-up": "Molette ↑", + "wheel-down": "Molette ↓" +}; + +function getInputLabel(input) { + const value = String(input?.value || ""); + if (input?.kind === "key" && value.length === 1) return value.toUpperCase(); + if (input?.kind === "mouse") return MOUSE_LABELS[value] || value; + return INPUT_LABELS[value] || value.toUpperCase(); +} + +function inputKey(input) { + return `${input.kind}:${input.value}:${input.hold ? "hold" : ""}:${input.holdMs || 0}`; +} + +function createInput(kind, value, holdMs = 0) { + const input = { kind, value }; + if (holdMs > 0) input.holdMs = holdMs; + return input; +} + +function getHoldSeconds(input) { + return Math.round((Number(input?.holdMs) || 0) / 1000); +} + +function formatHoldLabel(input) { + const seconds = getHoldSeconds(input); + if (!seconds) return input?.hold ? "hold" : ""; + return `${seconds}s`; +} + +function parseHoldSeconds(value) { + const seconds = Number.parseInt(String(value || "").replace(/[^\d]/g, ""), 10); + if (!Number.isInteger(seconds) || seconds <= 0) return 0; + return Math.min(99, seconds); +} + +function isDirectionInput(input) { + return input?.kind === "direction"; +} + +function createCombo(context, id, name, category, inputs, fallbackName, device) { + const combo = { + id: id || context.uid("combo"), + name: name.trim() || fallbackName, + device, + inputs: inputs.map((step) => step.map((input) => ({ ...input }))).filter((step) => step.length) + }; + if (category.trim()) combo.category = category.trim(); + return combo; +} + +function normalizeCustomKey(value) { + return String(value || "") + .trim() + .replace(/\s+/g, " ") + .slice(0, 16); +} + +function getPaletteGroups(device, keyboardLayout) { + return device === "keyboardMouse" ? KEYBOARD_GROUPS[keyboardLayout] : DEVICE_GROUPS[device] || DEVICE_GROUPS.playstation; +} + +function getPaletteColumns(groups) { + return [ + groups.filter((group) => group.column === "movement"), + groups.filter((group) => group.column !== "movement") + ]; +} + +function getComboCategory(combo) { + return String(combo?.category || "").trim(); +} + +function setComboItemCategory(combo, category) { + const nextCombo = { ...combo }; + if (category) nextCombo.category = category; + else delete nextCombo.category; + return nextCombo; +} + + +function ComboInputToken({ input, device, palette = false, onClick, onDragStart }) { + const label = getInputLabel(input); + const iconName = INPUT_ICONS[input.value]; + const holdLabel = !palette ? formatHoldLabel(input) : ""; + const classNames = [ + "combo-input-token", + `combo-input-${input.kind}`, + iconName ? "combo-input-icon" : "", + holdLabel ? "has-hold" : "", + input.kind === "key" && label.length === 1 ? "combo-input-short" : "", + `combo-device-${device}`, + `combo-value-${String(input.value || "").replace(/[^a-z0-9]+/gi, "-").toLowerCase()}` + ].filter(Boolean).join(" "); + + if (palette) { + return ( + + ); + } + + return ( + + {iconName ? : label} + {holdLabel && {holdLabel}} + + ); +} + +function ComboStep({ step, stepIndex, device, active = false, selectedInputIndex = -1, onClick, onSelectInput, onDropInput, onDragInput }) { + const className = [ + "combo-step-inputs", + step.length > 1 ? "is-simultaneous" : "", + step.length <= 1 ? "is-single" : "", + active ? "is-active" : "" + ].filter(Boolean).join(" "); + const content = step.length ? step.map((input, inputIndex) => ( + + {inputIndex > 0 && } + { + event.stopPropagation(); + onSelectInput(stepIndex, inputIndex); + } : null} + onDragStart={onDragInput ? (event) => onDragInput(event, stepIndex, inputIndex, input) : null} + /> + + )) : Étape vide; + + if (onClick) { + return ( + + ); + } + + return {content}; +} + +function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, selectedInput, onSelectStep, onSelectInput, onDropInput, onDropNewStep, onDragInput }) { + const visibleInputs = inputs.map((step, index) => ({ step, index })).filter(({ step }) => step.length || onSelectStep); + if (!visibleInputs.length) return {emptyLabel}; + return ( +
{ + if (!onDropNewStep) return; + event.preventDefault(); + }} + onDrop={(event) => { + if (!onDropNewStep || event.target.closest(".combo-step-inputs")) return; + event.preventDefault(); + onDropNewStep(event); + }} + > + {visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => { + const nextStep = visibleInputs[visibleIndex + 1]?.step || []; + const hideSeparator = step.every(isDirectionInput) && nextStep.every(isDirectionInput); + return ( + + onSelectStep(stepIndex) : null} + onSelectInput={onSelectInput} + onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null} + onDragInput={onDragInput} + /> + {visibleIndex < visibleInputs.length - 1 && !hideSeparator && } + + ); + })} +
+ ); +} + +function InlineTextInput({ value, onCommit, className, ariaLabel, placeholder = "" }) { + const inlineEdit = useInlineEdit({ + value, + onCommit, + blurOnEscape: true + }); + + return ( + + ); +} export function CombosModule({ toolboxId, moduleId, context, editing }) { const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [] })); diff --git a/website/src/features/toolboxes/modules/CombosParts.jsx b/website/src/features/toolboxes/modules/CombosParts.jsx deleted file mode 100644 index 24691f7..0000000 --- a/website/src/features/toolboxes/modules/CombosParts.jsx +++ /dev/null @@ -1,138 +0,0 @@ -// Rôle : rend les tokens, étapes et séquences visuelles de l'outil Combos. -import { Icon } from "../../../components/Icon.jsx"; -import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; -import { formatHoldLabel, getInputLabel, inputKey, INPUT_ICONS, isDirectionInput } from "./combosUtils.js"; - -export function ComboInputToken({ input, device, palette = false, onClick, onDragStart }) { - const label = getInputLabel(input); - const iconName = INPUT_ICONS[input.value]; - const holdLabel = !palette ? formatHoldLabel(input) : ""; - const classNames = [ - "combo-input-token", - `combo-input-${input.kind}`, - iconName ? "combo-input-icon" : "", - holdLabel ? "has-hold" : "", - input.kind === "key" && label.length === 1 ? "combo-input-short" : "", - `combo-device-${device}`, - `combo-value-${String(input.value || "").replace(/[^a-z0-9]+/gi, "-").toLowerCase()}` - ].filter(Boolean).join(" "); - - if (palette) { - return ( - - ); - } - - return ( - - {iconName ? : label} - {holdLabel && {holdLabel}} - - ); -} - -export function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, selectedInput, onSelectStep, onSelectInput, onDropInput, onDropNewStep, onDragInput }) { - const visibleInputs = inputs.map((step, index) => ({ step, index })).filter(({ step }) => step.length || onSelectStep); - if (!visibleInputs.length) return {emptyLabel}; - return ( -
{ - if (!onDropNewStep) return; - event.preventDefault(); - }} - onDrop={(event) => { - if (!onDropNewStep || event.target.closest(".combo-step-inputs")) return; - event.preventDefault(); - onDropNewStep(event); - }} - > - {visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => { - const nextStep = visibleInputs[visibleIndex + 1]?.step || []; - const hideSeparator = step.every(isDirectionInput) && nextStep.every(isDirectionInput); - return ( - - onSelectStep(stepIndex) : null} - onSelectInput={onSelectInput} - onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null} - onDragInput={onDragInput} - /> - {visibleIndex < visibleInputs.length - 1 && !hideSeparator && } - - ); - })} -
- ); -} - -export function InlineTextInput({ value, onCommit, className, ariaLabel, placeholder = "" }) { - const inlineEdit = useInlineEdit({ - value, - onCommit, - blurOnEscape: true - }); - - return ( - - ); -} - -function ComboStep({ step, stepIndex, device, active = false, selectedInputIndex = -1, onClick, onSelectInput, onDropInput, onDragInput }) { - const className = [ - "combo-step-inputs", - step.length > 1 ? "is-simultaneous" : "", - step.length <= 1 ? "is-single" : "", - active ? "is-active" : "" - ].filter(Boolean).join(" "); - const content = step.length ? step.map((input, inputIndex) => ( - - {inputIndex > 0 && } - { - event.stopPropagation(); - onSelectInput(stepIndex, inputIndex); - } : null} - onDragStart={onDragInput ? (event) => onDragInput(event, stepIndex, inputIndex, input) : null} - /> - - )) : Étape vide; - - if (onClick) { - return ( - - ); - } - - return {content}; -} diff --git a/website/src/features/toolboxes/modules/EquipmentPlannerEditors.jsx b/website/src/features/toolboxes/modules/EquipmentPlannerEditors.jsx deleted file mode 100644 index 82a2a42..0000000 --- a/website/src/features/toolboxes/modules/EquipmentPlannerEditors.jsx +++ /dev/null @@ -1,493 +0,0 @@ -// Rôle : contient les panneaux d'affichage et d'édition détaillée du planificateur d'équipements. -import { useState } from "react"; -import { Icon } from "../../../components/Icon.jsx"; -import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; -import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; -import { - TRAIT_ICONS, - createLocalId, - formatValue, - getCharacteristicCategories, - getSocketItemName, - setItemCategory -} from "./equipmentPlannerOptions.js"; -import { ColorChoiceDropdown, IconChoiceDropdown, ShapeChoiceDropdown, SocketShapeIcon } from "./EquipmentPlannerPickers.jsx"; - -export function EquipmentDisplay({ equipment, textContent }) { - const hasStats = equipment.characteristics.length > 0; - const hasSockets = equipment.socketItems.length > 0; - const hasCraft = Boolean(equipment.obtain || equipment.materials.length); - if (!hasStats && !hasSockets && !hasCraft) { - return

{textContent.emptyEquipmentDetails || "Aucun détail renseigné."}

; - } - - return ( -
- {hasStats && ( -
-

{textContent.displayStatsTitle || "Statistiques"}

-
    - {equipment.characteristics.map((trait) => ( -
  • - - {trait.name} - {trait.value !== "" && {formatValue(trait.value)}} -
  • - ))} -
-
- )} - {hasSockets && ( -
-

{textContent.displaySocketTitle || "Objets sertis"}

-
    - {equipment.socketItems.map((socketItem) => ( -
  • - -
    - {socketItem.name} - {socketItem.bonuses.map((bonus) => ( - {bonus.name}{bonus.value !== "" ? ` +${formatValue(bonus.value)}` : ""} - ))} -
    -
  • - ))} -
-
- )} - {hasCraft && ( -
-

{textContent.craftTab || "Craft"}

- {equipment.obtain ?

{equipment.obtain}

: null} - {equipment.materials.length ? ( -
    - {equipment.materials.map((material) =>
  • {material.name}x{material.qty}
  • )} -
- ) : null} -
- )} -
- ); -} - -export function TabIcon({ tab }) { - if (tab === "stats") return ; - if (tab === "sockets") return ; - return ; -} - -export function CharacteristicsEditor({ equipment, textContent, editing, onUpdate }) { - const [draft, setDraft] = useState({ icon: "sword", category: "", name: "", value: "" }); - const categories = getCharacteristicCategories(equipment); - const { - itemReorder, - groupReorder, - getItemProps, - getGroupProps, - isItemDragging, - isItemDropTarget, - isGroupDragging, - isGroupDropTarget, - getDropPlacement - } = useGroupedReorder({ - namespace: `equipment-characteristics-${equipment.id}`, - items: equipment.characteristics, - getItemId: (trait) => trait.id, - getItemGroup: (trait) => trait.category, - getEffectiveGroup: (trait) => trait.category, - reorderFeatures: { - item: { groupChange: true }, - group: { reorder: true, itemDrop: true } - }, - onItemMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, { - operation, - itemsKey: "characteristics", - groupOrderKey: "categoryOrder", - collapsedGroupsKey: "collapsedCategories", - getItemGroup: (trait) => trait.category, - setItemGroup: setItemCategory - })), - onGroupMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, { - operation, - itemsKey: "characteristics", - groupOrderKey: "categoryOrder", - collapsedGroupsKey: "collapsedCategories", - getItemGroup: (trait) => trait.category, - setItemGroup: setItemCategory - })) - }); - const reorder = { itemReorder, groupReorder, getItemProps, getGroupProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, getDropPlacement }; - - function addTrait(event) { - event.preventDefault(); - const cleanName = draft.name.trim(); - if (!cleanName) return; - onUpdate((current) => ({ - ...current, - characteristics: [...current.characteristics, { id: createLocalId("trait"), icon: draft.icon, category: draft.category.trim(), name: cleanName, value: draft.value.trim() }], - categoryOrder: draft.category.trim() && !current.categoryOrder.includes(draft.category.trim()) ? [...current.categoryOrder, draft.category.trim()] : current.categoryOrder - })); - setDraft({ icon: draft.icon, category: draft.category, name: "", value: "" }); - } - - function updateTrait(traitId, updater) { - onUpdate((current) => ({ ...current, characteristics: current.characteristics.map((trait) => trait.id === traitId ? updater(trait) : trait).filter((trait) => trait.name) })); - } - - return ( -
-

{textContent.characteristicsTitle || "Caractéristiques"}

- {editing && ( -
- ({ icon, label: icon }))} - onChange={(icon) => setDraft((current) => ({ ...current, icon }))} - /> - setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} /> - setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} /> - setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} /> - - - )} - onUpdate((current) => ({ - ...current, - collapsedCategories: current.collapsedCategories.includes(category) - ? current.collapsedCategories.filter((item) => item !== category) - : [...current.collapsedCategories, category] - }))} - onUpdateTrait={updateTrait} - editing={editing} - /> -
- ); -} - -function TraitCategoryList({ traits, categories, collapsedCategories, reorder, textContent, onToggleCategory, onUpdateTrait, editing }) { - const ungrouped = traits.filter((trait) => !trait.category); - - function renderTrait(trait) { - const traitClassName = [ - "equipment-planner-trait", - reorder.isItemDragging(trait.id) ? "is-dragging" : "", - reorder.isItemDropTarget(trait.id) ? "is-drop-target" : "", - reorder.getDropPlacement("item", trait.id) === "after" ? "drop-after" : "" - ].filter(Boolean).join(" "); - return ( - reorder.itemReorder.startDrag(event, trait.id)} - onUpdateTrait={onUpdateTrait} - /> - ); - } - - return ( -
- {ungrouped.length ?
    {ungrouped.map(renderTrait)}
: null} - {categories.map((category) => { - const categoryTraits = traits.filter((trait) => trait.category === category); - const collapsed = collapsedCategories.includes(category); - const className = [ - "equipment-planner-trait-category", - collapsed ? "is-collapsed" : "", - reorder.isGroupDragging(category) ? "is-dragging" : "", - reorder.isGroupDropTarget(category) ? "is-drop-target" : "" - ].filter(Boolean).join(" "); - return ( -
-
- - {category} - {categoryTraits.length} - -
- {!collapsed &&
    {categoryTraits.map(renderTrait)}
} -
- ); - })} - {!traits.length &&

{textContent.emptyCharacteristics || "Aucune caractéristique."}

} -
- ); -} - -function EquipmentTraitRow({ trait, className, itemProps, editing, textContent, onStartDrag, onUpdateTrait }) { - const categoryEdit = useInlineEdit({ - value: trait.category || "", - onCommit: (category) => onUpdateTrait(trait.id, (current) => ({ ...current, category })) - }); - const nameEdit = useInlineEdit({ - value: trait.name, - transform: (draft) => String(draft || "").trim() || trait.name, - onCommit: (name) => onUpdateTrait(trait.id, (current) => ({ ...current, name })) - }); - const valueEdit = useInlineEdit({ - value: formatValue(trait.value), - onCommit: (value) => onUpdateTrait(trait.id, (current) => ({ ...current, value })) - }); - - return ( -
  • - {editing ? ( - <> - - ({ icon, label: icon }))} - onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))} - /> - - - - - - ) : ( -
    - - {trait.name} - {trait.value !== "" && {formatValue(trait.value)}} -
    - )} -
  • - ); -} - -export function MaterialsEditor({ equipment, textContent, editing, onUpdate }) { - const [draft, setDraft] = useState({ name: "", qty: 1 }); - - function addMaterial(event) { - event.preventDefault(); - const cleanName = draft.name.trim(); - if (!cleanName) return; - onUpdate((current) => ({ - ...current, - materials: [...current.materials, { id: createLocalId("material"), name: cleanName, qty: Math.max(1, Number.parseInt(draft.qty, 10) || 1) }] - })); - setDraft({ name: "", qty: 1 }); - } - - return ( -
    -

    {textContent.materialsTitle || "Matériaux"}

    - {editing && ( -
    - setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} /> - setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} /> - -
    - )} -
      - {equipment.materials.map((material) => ( - - ))} -
    -
    - ); -} - -function MaterialRow({ material, editing, textContent, onUpdate }) { - const nameEdit = useInlineEdit({ - value: material.name, - transform: (draft) => String(draft || "").trim() || material.name, - onCommit: (name) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, name } : item) })) - }); - - return ( -
  • - {editing ? ( - <> - - onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} /> - - - ) : ( -
    - {material.name} - x{material.qty} -
    - )} -
  • - ); -} - -export function SocketItemsEditor({ equipment, textContent, editing, onUpdate }) { - const [draft, setDraft] = useState({ name: "", shape: "jewel", color: "yellow" }); - - function addSocketItem(event) { - event.preventDefault(); - const cleanName = draft.name.trim(); - if (!cleanName) return; - onUpdate((current) => ({ - ...current, - socketItems: [...current.socketItems, { id: createLocalId("socket"), name: cleanName, shape: draft.shape, color: draft.color, bonuses: [] }] - })); - setDraft({ ...draft, name: "" }); - } - - function updateSocketItem(socketItemId, updater) { - onUpdate((current) => ({ - ...current, - socketItems: current.socketItems.map((socketItem) => socketItem.id === socketItemId ? updater(socketItem) : socketItem).filter((socketItem) => socketItem.name) - })); - } - - function deleteSocketItem(socketItemId) { - onUpdate((current) => ({ - ...current, - socketItems: current.socketItems.filter((socketItem) => socketItem.id !== socketItemId), - socketLinks: current.socketLinks.filter((link) => link.fromSocketItemId !== socketItemId && link.toSocketItemId !== socketItemId) - })); - } - - function addSocketLink(fromSocketItemId, toSocketItemId) { - if (!toSocketItemId || fromSocketItemId === toSocketItemId) return; - const pair = [fromSocketItemId, toSocketItemId].sort().join(":"); - if (equipment.socketLinks.some((link) => [link.fromSocketItemId, link.toSocketItemId].sort().join(":") === pair)) return; - onUpdate((current) => ({ - ...current, - socketLinks: [...current.socketLinks, { id: createLocalId("socket-link"), fromSocketItemId, toSocketItemId }] - })); - } - - return ( -
    -

    {textContent.socketItemsTitle || "Objets sertis"}

    - {editing && ( -
    - setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} /> - setDraft((current) => ({ ...current, shape }))} /> - setDraft((current) => ({ ...current, color }))} /> - - - )} -
    - {equipment.socketItems.map((socketItem) => ( - updateSocketItem(socketItem.id, updater)} - onDelete={() => deleteSocketItem(socketItem.id)} - onAddLink={(toSocketItemId) => addSocketLink(socketItem.id, toSocketItemId)} - onDeleteLink={(linkId) => onUpdate((current) => ({ ...current, socketLinks: current.socketLinks.filter((link) => link.id !== linkId) }))} - /> - ))} -
    -
    - ); -} - -function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdate, onDelete, onAddLink, onDeleteLink }) { - const [bonusDraft, setBonusDraft] = useState({ name: "", value: "" }); - const links = equipment.socketLinks.filter((link) => link.fromSocketItemId === socketItem.id || link.toSocketItemId === socketItem.id); - const nameEdit = useInlineEdit({ - value: socketItem.name, - transform: (draft) => String(draft || "").trim() || socketItem.name, - onCommit: (name) => onUpdate((current) => ({ ...current, name })) - }); - - function addBonus(event) { - event.preventDefault(); - const cleanName = bonusDraft.name.trim(); - if (!cleanName) return; - onUpdate((current) => ({ - ...current, - bonuses: [...current.bonuses, { id: createLocalId("bonus"), category: "", name: cleanName, value: bonusDraft.value.trim() }] - })); - setBonusDraft({ name: "", value: "" }); - } - - return ( -
    -
    - - {editing ? ( - <> - - - - ) : ( - {socketItem.name} - )} -
    - {editing && ( - <> -
    - onUpdate((current) => ({ ...current, shape }))} /> - onUpdate((current) => ({ ...current, color }))} /> - {equipment.socketItems.length > 1 && ( - - )} -
    -
    - setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} /> - setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} /> - -
    - - )} -
      - {socketItem.bonuses.map((bonus) => ( -
    • - {bonus.category ? `${bonus.category} · ` : ""}{bonus.name} - {formatValue(bonus.value)} - {editing && ( - - )} -
    • - ))} -
    - {links.length ? ( -
    - {links.map((link) => { - const otherSocketItemId = link.fromSocketItemId === socketItem.id ? link.toSocketItemId : link.fromSocketItemId; - return ( - - ); - })} -
    - ) : null} -
    - ); -} diff --git a/website/src/features/toolboxes/modules/EquipmentPlannerItem.jsx b/website/src/features/toolboxes/modules/EquipmentPlannerItem.jsx deleted file mode 100644 index fa24c65..0000000 --- a/website/src/features/toolboxes/modules/EquipmentPlannerItem.jsx +++ /dev/null @@ -1,106 +0,0 @@ -// Rôle : rend une ligne d'équipement et délègue ses panneaux d'édition spécialisés. -import { useState } from "react"; -import { Icon } from "../../../components/Icon.jsx"; -import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; -import { DETAIL_TABS, getEquipmentTabLabel } from "./equipmentPlannerOptions.js"; -import { CharacteristicsEditor, EquipmentDisplay, MaterialsEditor, SocketItemsEditor, TabIcon } from "./EquipmentPlannerEditors.jsx"; - -export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDelete }) { - const [activeTab, setActiveTab] = useState("stats"); - const [detailEditing, setDetailEditing] = useState(false); - const nameEdit = useInlineEdit({ - value: equipment.name, - onCommit: (name) => onUpdate((current) => ({ ...current, name: name || current.name })), - blurOnEscape: true - }); - const obtainEdit = useInlineEdit({ - value: equipment.obtain || "", - onCommit: (obtain) => onUpdate((current) => ({ ...current, obtain })), - commitOnEnter: false - }); - const className = [ - "equipment-planner-item", - equipment.active ? "is-active" : "is-inactive", - reorder.isItemDragging(equipment.id) ? "is-dragging" : "", - reorder.isItemDropTarget(equipment.id) ? "is-drop-target" : "", - reorder.getDropPlacement("item", equipment.id) === "after" ? "drop-after" : "" - ].filter(Boolean).join(" "); - - return ( -
  • -
    - -
    - - {equipment.name} -
    - - - -
    -
    - {detailEditing ? ( - <> -
    - {DETAIL_TABS.map((tab) => ( - - ))} -
    - {activeTab === "stats" && } - {activeTab === "sockets" && } - {activeTab === "craft" && ( -
    -