diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs
index 402a384..8d1f133 100644
--- a/tests/static-toolboxes.test.mjs
+++ b/tests/static-toolboxes.test.mjs
@@ -4,6 +4,7 @@ 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";
test("toolbox storage, cards and pages are wired", async () => {
const source = await readFile("website/src/main.jsx", "utf8");
@@ -114,6 +115,8 @@ 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 tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8");
@@ -180,26 +183,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(combosModule, /keyboardMouse/);
- assert.match(combosModule, /Nintendo 64/);
- assert.match(combosModule, /ComboInputToken/);
- assert.match(combosModule, /ComboSequence/);
+ assert.match(combosUtils, /keyboardMouse/);
+ assert.match(combosUtils, /Nintendo 64/);
+ assert.match(combosParts, /export function ComboInputToken/);
+ assert.match(combosParts, /export function ComboSequence/);
assert.match(combosModule, /keyboardLayout/);
assert.match(combosModule, /simultaneousMode/);
- assert.match(combosModule, /SPECIAL_INPUT_GROUP/);
- assert.match(combosModule, /grab/);
- assert.match(combosModule, /protect/);
- assert.match(combosModule, /shoot/);
+ assert.match(combosUtils, /SPECIAL_INPUT_GROUP/);
+ assert.match(combosUtils, /grab/);
+ assert.match(combosUtils, /protect/);
+ assert.match(combosUtils, /shoot/);
assert.match(combosModule, /holdMs/);
assert.match(combosModule, /toggleSelectedInputHold/);
assert.match(combosModule, /combo-hold-field/);
- assert.match(combosModule, /useInlineEdit/);
+ assert.match(combosParts, /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(combosModule, /function setComboItemCategory/);
+ assert.match(combosUtils, /function setComboItemCategory/);
assert.match(combosModule, /setItemGroup: setComboItemCategory/);
assert.doesNotMatch(combosModule, /canMoveItem:/);
assert.doesNotMatch(combosModule, /canMoveGroup:/);
@@ -411,3 +414,13 @@ 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");
+});
diff --git a/website/src/features/toolboxes/modules/CombosModule.jsx b/website/src/features/toolboxes/modules/CombosModule.jsx
index 47f2644..89d7498 100644
--- a/website/src/features/toolboxes/modules/CombosModule.jsx
+++ b/website/src/features/toolboxes/modules/CombosModule.jsx
@@ -1,333 +1,25 @@
// 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";
-
-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 ?