This commit is contained in:
parent
025cd4bf22
commit
7b4cfb7dc2
13 changed files with 1829 additions and 2 deletions
|
|
@ -289,6 +289,38 @@
|
|||
"resetTitle": "Réinitialiser",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"combos": {
|
||||
"deviceLabel": "Périphérique",
|
||||
"keyboardLayoutLabel": "Disposition clavier",
|
||||
"categoryPlaceholder": "Catégorie",
|
||||
"targetCategoryLabel": "Catégorie",
|
||||
"noCategoryLabel": "Sans catégorie",
|
||||
"comboNameLabel": "Nom",
|
||||
"comboNamePlaceholder": "Nom du combo",
|
||||
"defaultComboName": "Combo",
|
||||
"emptyDraft": "Aucune touche sélectionnée.",
|
||||
"simultaneousModeTitle": "Ajouter dans l'étape active",
|
||||
"successiveModeLabel": "A › B",
|
||||
"simultaneousModeLabel": "A + B",
|
||||
"removeStepTitle": "Supprimer l'étape",
|
||||
"removeLastInputTitle": "Retirer la dernière touche",
|
||||
"addComboButton": "Ajouter le combo",
|
||||
"saveComboButton": "Enregistrer",
|
||||
"cancelEditTitle": "Annuler l'édition",
|
||||
"cancelEditButton": "Annuler",
|
||||
"paletteLabel": "Palette de touches",
|
||||
"customKeyPlaceholder": "Touche personnalisée",
|
||||
"addCustomKeyButton": "Ajouter touche",
|
||||
"emptyCombos": "Aucun combo enregistré.",
|
||||
"emptyCategory": "Aucun combo dans cette catégorie.",
|
||||
"emptySequence": "Combo sans touches",
|
||||
"showCategoryTitle": "Afficher la catégorie",
|
||||
"hideCategoryTitle": "Réduire la catégorie",
|
||||
"reorderCategoryTitle": "Déplacer la catégorie",
|
||||
"reorderComboTitle": "Déplacer le combo",
|
||||
"editComboTitle": "Modifier le combo",
|
||||
"deleteComboTitle": "Supprimer le combo"
|
||||
},
|
||||
"calculator": {
|
||||
"expressionLabel": "Calcul",
|
||||
"expressionPlaceholder": "10*10",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
hostnameFromUrl,
|
||||
normalizeCalculatorData,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeLinksData,
|
||||
|
|
@ -144,6 +145,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
getModuleData,
|
||||
moduleText,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCalculatorData,
|
||||
|
|
|
|||
807
website/src/features/toolboxes/modules/CombosModule.jsx
Normal file
807
website/src/features/toolboxes/modules/CombosModule.jsx
Normal file
|
|
@ -0,0 +1,807 @@
|
|||
// Rôle : fournit l'outil Combos avec palettes d'inputs et rendu visuel par périphérique.
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { usePointerReorder } from "../../../hooks/usePointerReorder.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 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"]] },
|
||||
{ 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"]] },
|
||||
{ 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"]] },
|
||||
{ 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"]] },
|
||||
{ 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"]] },
|
||||
{ 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"]] },
|
||||
{ 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→",
|
||||
space: "Espace"
|
||||
};
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
function createInput(kind, value) {
|
||||
return { kind, value };
|
||||
}
|
||||
|
||||
function createCombo(context, id, name, inputs, fallbackName, device) {
|
||||
return {
|
||||
id: id || context.uid("combo"),
|
||||
name: name.trim() || fallbackName,
|
||||
device,
|
||||
inputs: inputs.map((step) => step.map((input) => ({ ...input }))).filter((step) => step.length)
|
||||
};
|
||||
}
|
||||
|
||||
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 moveArrayItem(items, fromId, toId, placement, getId = (item) => item.id) {
|
||||
if (!fromId || !toId || fromId === toId) return items;
|
||||
const nextItems = [...items];
|
||||
const fromIndex = nextItems.findIndex((item) => getId(item) === fromId);
|
||||
const toIndex = nextItems.findIndex((item) => getId(item) === toId);
|
||||
if (fromIndex < 0 || toIndex < 0) return items;
|
||||
const [moved] = nextItems.splice(fromIndex, 1);
|
||||
const targetIndex = nextItems.findIndex((item) => getId(item) === toId);
|
||||
nextItems.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, moved);
|
||||
return nextItems;
|
||||
}
|
||||
|
||||
function removeComboFromData(data, comboId) {
|
||||
let movedCombo = null;
|
||||
const combos = data.combos.filter((combo) => {
|
||||
if (combo.id !== comboId) return true;
|
||||
movedCombo = combo;
|
||||
return false;
|
||||
});
|
||||
const categories = data.categories.map((category) => ({
|
||||
...category,
|
||||
combos: category.combos.filter((combo) => {
|
||||
if (combo.id !== comboId) return true;
|
||||
movedCombo = combo;
|
||||
return false;
|
||||
})
|
||||
}));
|
||||
return { movedCombo, data: { ...data, combos, categories } };
|
||||
}
|
||||
|
||||
function insertComboInList(list, combo, targetComboId = "", placement = "after") {
|
||||
if (!targetComboId) return [...list, combo];
|
||||
const targetIndex = list.findIndex((item) => item.id === targetComboId);
|
||||
if (targetIndex < 0) return [...list, combo];
|
||||
const nextList = [...list];
|
||||
nextList.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, combo);
|
||||
return nextList;
|
||||
}
|
||||
|
||||
function ComboInputToken({ input, device, palette = false, onClick, onDragStart }) {
|
||||
const label = getInputLabel(input);
|
||||
const classNames = [
|
||||
"combo-input-token",
|
||||
`combo-input-${input.kind}`,
|
||||
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 (
|
||||
<button className={classNames} type="button" draggable onClick={onClick} onDragStart={onDragStart} title={label} aria-label={label}>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={classNames} draggable={Boolean(onDragStart)} onDragStart={onDragStart} title={label}>
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboStep({ step, stepIndex, device, active = false, onClick, 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) => (
|
||||
<span className="combo-step-input" key={`${inputKey(input)}-${inputIndex}`}>
|
||||
{inputIndex > 0 && <span className="combo-plus" aria-hidden="true">+</span>}
|
||||
<ComboInputToken input={input} device={device} onDragStart={onDragInput ? (event) => onDragInput(event, stepIndex, inputIndex, input) : null} />
|
||||
</span>
|
||||
)) : <span className="combo-empty-step">Étape vide</span>;
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
onDragOver={(event) => {
|
||||
if (!onDropInput) return;
|
||||
event.preventDefault();
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (!onDropInput) return;
|
||||
event.preventDefault();
|
||||
onDropInput(event);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={className}>{content}</span>;
|
||||
}
|
||||
|
||||
function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, onSelectStep, onDropInput, onDropNewStep, onDragInput }) {
|
||||
const visibleInputs = inputs.map((step, index) => ({ step, index })).filter(({ step }) => step.length || onSelectStep);
|
||||
if (!visibleInputs.length) return <span className="combo-empty-sequence">{emptyLabel}</span>;
|
||||
return (
|
||||
<div
|
||||
className="combo-sequence"
|
||||
onDragOver={(event) => {
|
||||
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) => (
|
||||
<span className="combo-step" key={`${stepIndex}-${step.map(inputKey).join("+")}`}>
|
||||
<ComboStep step={step} stepIndex={stepIndex} device={device} active={activeStepIndex === stepIndex} onClick={onSelectStep ? () => onSelectStep(stepIndex) : null} onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null} onDragInput={onDragInput} />
|
||||
{visibleIndex < visibleInputs.length - 1 && <span className="combo-step-separator" aria-hidden="true">›</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineTextInput({ value, onCommit, className, ariaLabel, placeholder = "" }) {
|
||||
const [draft, setDraft] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(value);
|
||||
}, [value]);
|
||||
|
||||
function commit() {
|
||||
const cleanDraft = draft.trim();
|
||||
if (cleanDraft !== value) onCommit(cleanDraft);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
className={className}
|
||||
value={draft}
|
||||
placeholder={placeholder}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") event.currentTarget.blur();
|
||||
if (event.key === "Escape") {
|
||||
setDraft(value);
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [], categories: [] }));
|
||||
const textContent = context.moduleText?.combos || {};
|
||||
const [comboCategory, setComboCategory] = useState("");
|
||||
const [comboName, setComboName] = useState("");
|
||||
const [draftInputs, setDraftInputs] = useState([[]]);
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
||||
const [simultaneousMode, setSimultaneousMode] = useState(false);
|
||||
const [keyboardLayout, setKeyboardLayout] = useState("azerty");
|
||||
const [customKey, setCustomKey] = useState("");
|
||||
const [editingComboId, setEditingComboId] = useState("");
|
||||
const paletteGroups = useMemo(() => getPaletteGroups(data.device, keyboardLayout), [data.device, keyboardLayout]);
|
||||
const paletteColumns = useMemo(() => getPaletteColumns(paletteGroups), [paletteGroups]);
|
||||
const cleanDraftInputs = draftInputs.map((step) => step.filter(Boolean)).filter((step) => step.length);
|
||||
const {
|
||||
draggingId: draggingCategoryId,
|
||||
dropTarget: categoryDropTarget,
|
||||
startDrag: startCategoryDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".combos-category",
|
||||
getTargetId: (target) => target.dataset.categoryId,
|
||||
onMove: moveCategory
|
||||
});
|
||||
const {
|
||||
draggingId: draggingComboId,
|
||||
dropTarget: comboDropTarget,
|
||||
startDrag: startComboDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".combo-card, .combos-root-drop-zone, .combos-category",
|
||||
getTargetId: (target) => {
|
||||
if (target.classList.contains("combo-card")) return `combo:${target.dataset.comboId}`;
|
||||
if (target.classList.contains("combos-category")) return `category:${target.dataset.categoryId}`;
|
||||
return "root";
|
||||
},
|
||||
canDropOn: (target, draggingId) => {
|
||||
if (target.classList.contains("combo-card")) return target.dataset.comboId !== draggingId;
|
||||
if (target.classList.contains("combos-category")) return !target.querySelector(`[data-combo-id="${draggingId}"]`);
|
||||
return true;
|
||||
},
|
||||
onMove: moveCombo
|
||||
});
|
||||
|
||||
function save(nextData) {
|
||||
context.setModuleData(toolboxId, moduleId, nextData, "combos");
|
||||
}
|
||||
|
||||
function updateDevice(device) {
|
||||
const stampComboDevice = (combo) => combo.device ? combo : { ...combo, device: data.device };
|
||||
save({
|
||||
...data,
|
||||
device,
|
||||
combos: data.combos.map(stampComboDevice),
|
||||
categories: data.categories.map((category) => ({
|
||||
...category,
|
||||
combos: category.combos.map(stampComboDevice)
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
function moveCategory(fromCategoryId, toCategoryId, placement) {
|
||||
save({ ...data, categories: moveArrayItem(data.categories, fromCategoryId, toCategoryId, placement) });
|
||||
}
|
||||
|
||||
function moveCombo(fromComboId, targetId, placement) {
|
||||
const { movedCombo, data: dataWithoutCombo } = removeComboFromData(data, fromComboId);
|
||||
if (!movedCombo) return;
|
||||
if (targetId === "root") {
|
||||
save({ ...dataWithoutCombo, combos: [...dataWithoutCombo.combos, movedCombo] });
|
||||
return;
|
||||
}
|
||||
if (targetId.startsWith("category:")) {
|
||||
const categoryId = targetId.replace("category:", "");
|
||||
save({
|
||||
...dataWithoutCombo,
|
||||
categories: dataWithoutCombo.categories.map((category) => category.id === categoryId
|
||||
? { ...category, combos: [...category.combos, movedCombo], collapsed: false }
|
||||
: category)
|
||||
});
|
||||
return;
|
||||
}
|
||||
const targetComboId = targetId.replace("combo:", "");
|
||||
const targetElement = document.querySelector(`[data-combo-id="${targetComboId}"]`);
|
||||
const targetList = targetElement?.dataset.comboList || "";
|
||||
if (!targetList) {
|
||||
save({ ...dataWithoutCombo, combos: insertComboInList(dataWithoutCombo.combos, movedCombo, targetComboId, placement) });
|
||||
return;
|
||||
}
|
||||
save({
|
||||
...dataWithoutCombo,
|
||||
categories: dataWithoutCombo.categories.map((category) => category.id === targetList
|
||||
? { ...category, combos: insertComboInList(category.combos, movedCombo, targetComboId, placement), collapsed: false }
|
||||
: category)
|
||||
});
|
||||
}
|
||||
|
||||
function updateCategory(categoryId, updater) {
|
||||
save({
|
||||
...data,
|
||||
categories: data.categories.map((category) => category.id === categoryId ? updater(category) : category)
|
||||
});
|
||||
}
|
||||
|
||||
function getCategoryTarget(categories, title) {
|
||||
const cleanTitle = title.trim();
|
||||
if (!cleanTitle) return { categoryId: "", categories };
|
||||
const existing = categories.find((category) => category.title.toLowerCase() === cleanTitle.toLowerCase());
|
||||
if (existing) return { categoryId: existing.id, categories };
|
||||
const category = { id: context.uid("section"), title: cleanTitle, collapsed: false, combos: [] };
|
||||
return { categoryId: category.id, categories: [...categories, category] };
|
||||
}
|
||||
|
||||
function addInputToStep(kind, value, stepIndex) {
|
||||
const input = createInput(kind, value);
|
||||
setDraftInputs((steps) => {
|
||||
const nextSteps = steps.length ? steps.map((step) => [...step]) : [[]];
|
||||
const targetIndex = Math.min(Math.max(0, stepIndex), nextSteps.length - 1);
|
||||
nextSteps[targetIndex] = [...nextSteps[targetIndex], input];
|
||||
return nextSteps;
|
||||
});
|
||||
}
|
||||
|
||||
function addDraftInput(kind, value) {
|
||||
const input = createInput(kind, value);
|
||||
if (simultaneousMode) {
|
||||
addInputToStep(kind, value, activeStepIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
setDraftInputs((steps) => {
|
||||
const filledSteps = steps.filter((step) => step.length);
|
||||
const nextSteps = [...filledSteps, [input]];
|
||||
setActiveStepIndex(nextSteps.length - 1);
|
||||
return nextSteps;
|
||||
});
|
||||
}
|
||||
|
||||
function dragPaletteInput(event, kind, value) {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData("application/x-sokkog-combo-input", JSON.stringify(createInput(kind, value)));
|
||||
}
|
||||
|
||||
function dragDraftInput(event, stepIndex, inputIndex, input) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-sokkog-combo-input", JSON.stringify({
|
||||
...input,
|
||||
sourceStepIndex: stepIndex,
|
||||
sourceInputIndex: inputIndex
|
||||
}));
|
||||
}
|
||||
|
||||
function dropInputOnStep(event, stepIndex) {
|
||||
const rawInput = event.dataTransfer.getData("application/x-sokkog-combo-input");
|
||||
if (!rawInput) return;
|
||||
try {
|
||||
const input = JSON.parse(rawInput);
|
||||
if (!input?.kind || !input?.value) return;
|
||||
if (Number.isInteger(input.sourceStepIndex) && Number.isInteger(input.sourceInputIndex)) {
|
||||
moveDraftInput(input.sourceStepIndex, input.sourceInputIndex, stepIndex);
|
||||
} else {
|
||||
addInputToStep(input.kind, input.value, stepIndex);
|
||||
}
|
||||
setActiveStepIndex(stepIndex);
|
||||
} catch {
|
||||
// Ignore invalid drag payloads from outside the app.
|
||||
}
|
||||
}
|
||||
|
||||
function dropInputAsNewStep(event) {
|
||||
const rawInput = event.dataTransfer.getData("application/x-sokkog-combo-input");
|
||||
if (!rawInput) return;
|
||||
try {
|
||||
const input = JSON.parse(rawInput);
|
||||
if (!input?.kind || !input?.value) return;
|
||||
if (Number.isInteger(input.sourceStepIndex) && Number.isInteger(input.sourceInputIndex)) {
|
||||
moveDraftInputToNewStep(input.sourceStepIndex, input.sourceInputIndex);
|
||||
} else {
|
||||
addDraftInput(input.kind, input.value);
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid drag payloads from outside the app.
|
||||
}
|
||||
}
|
||||
|
||||
function moveDraftInput(sourceStepIndex, sourceInputIndex, targetStepIndex) {
|
||||
setDraftInputs((steps) => {
|
||||
if (sourceStepIndex === targetStepIndex) return steps;
|
||||
const nextSteps = steps.map((step) => [...step]);
|
||||
const [input] = nextSteps[sourceStepIndex]?.splice(sourceInputIndex, 1) || [];
|
||||
if (!input || !nextSteps[targetStepIndex]) return steps;
|
||||
nextSteps[targetStepIndex].push(input);
|
||||
return nextSteps.filter((step, index) => step.length || index === targetStepIndex);
|
||||
});
|
||||
}
|
||||
|
||||
function moveDraftInputToNewStep(sourceStepIndex, sourceInputIndex) {
|
||||
setDraftInputs((steps) => {
|
||||
const nextSteps = steps.map((step) => [...step]);
|
||||
const [input] = nextSteps[sourceStepIndex]?.splice(sourceInputIndex, 1) || [];
|
||||
if (!input) return steps;
|
||||
const safeSteps = nextSteps.filter((step) => step.length);
|
||||
setActiveStepIndex(safeSteps.length);
|
||||
return [...safeSteps, [input]];
|
||||
});
|
||||
}
|
||||
|
||||
function removeActiveStep() {
|
||||
setDraftInputs((steps) => {
|
||||
const nextSteps = steps.filter((_, index) => index !== activeStepIndex);
|
||||
const safeSteps = nextSteps.length ? nextSteps : [[]];
|
||||
setActiveStepIndex(Math.min(activeStepIndex, safeSteps.length - 1));
|
||||
return safeSteps;
|
||||
});
|
||||
}
|
||||
|
||||
function removeLastInput() {
|
||||
setDraftInputs((steps) => {
|
||||
const nextSteps = steps.length ? steps.map((step) => [...step]) : [[]];
|
||||
let targetIndex = -1;
|
||||
for (let index = nextSteps.length - 1; index >= 0; index -= 1) {
|
||||
if (nextSteps[index].length) {
|
||||
targetIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetIndex < 0) return [[]];
|
||||
nextSteps[targetIndex].pop();
|
||||
const safeSteps = nextSteps.filter((step) => step.length);
|
||||
setActiveStepIndex(Math.max(0, safeSteps.length - 1));
|
||||
return safeSteps.length ? safeSteps : [[]];
|
||||
});
|
||||
}
|
||||
|
||||
function addCustomKey(event) {
|
||||
event.preventDefault();
|
||||
const value = normalizeCustomKey(customKey);
|
||||
if (!value || data.device !== "keyboardMouse") return;
|
||||
addDraftInput("key", value.toLowerCase());
|
||||
setCustomKey("");
|
||||
}
|
||||
|
||||
function saveCombo(event) {
|
||||
event.preventDefault();
|
||||
if (!comboName.trim() && !cleanDraftInputs.length) return;
|
||||
const combo = createCombo(context, editingComboId, comboName, cleanDraftInputs, textContent.defaultComboName || "Combo", data.device);
|
||||
const nextRootCombos = data.combos.filter((item) => item.id !== editingComboId);
|
||||
const nextCategories = data.categories.map((category) => ({
|
||||
...category,
|
||||
combos: category.combos.filter((item) => item.id !== editingComboId)
|
||||
}));
|
||||
const target = getCategoryTarget(nextCategories, comboCategory);
|
||||
const nextData = target.categoryId
|
||||
? {
|
||||
...data,
|
||||
combos: nextRootCombos,
|
||||
categories: target.categories.map((category) => category.id === target.categoryId ? { ...category, combos: [...category.combos, combo], collapsed: false } : category)
|
||||
}
|
||||
: { ...data, combos: [...nextRootCombos, combo], categories: nextCategories };
|
||||
save(nextData);
|
||||
cancelComboEdit();
|
||||
}
|
||||
|
||||
function startComboEdit(categoryId, combo) {
|
||||
setComboCategory(data.categories.find((category) => category.id === categoryId)?.title || "");
|
||||
setEditingComboId(combo.id);
|
||||
setComboName(combo.name);
|
||||
setDraftInputs(combo.inputs.length ? combo.inputs.map((step) => step.map((input) => ({ ...input }))) : [[]]);
|
||||
setActiveStepIndex(0);
|
||||
}
|
||||
|
||||
function cancelComboEdit() {
|
||||
setEditingComboId("");
|
||||
setComboName("");
|
||||
setComboCategory("");
|
||||
setDraftInputs([[]]);
|
||||
setActiveStepIndex(0);
|
||||
setSimultaneousMode(false);
|
||||
}
|
||||
|
||||
function updateCombo(categoryId, comboId, updater) {
|
||||
if (!categoryId) {
|
||||
save({ ...data, combos: data.combos.map((combo) => combo.id === comboId ? updater(combo) : combo).filter((combo) => combo.name || combo.inputs.length) });
|
||||
return;
|
||||
}
|
||||
updateCategory(categoryId, (category) => ({
|
||||
...category,
|
||||
combos: category.combos.map((combo) => combo.id === comboId ? updater(combo) : combo).filter((combo) => combo.name || combo.inputs.length)
|
||||
}));
|
||||
}
|
||||
|
||||
function deleteCombo(categoryId, comboId) {
|
||||
updateCombo(categoryId, comboId, () => ({ name: "", inputs: [] }));
|
||||
if (editingComboId === comboId) cancelComboEdit();
|
||||
}
|
||||
|
||||
function renderCombo(combo, categoryId = "") {
|
||||
const comboClassName = [
|
||||
"combo-card",
|
||||
"is-editing",
|
||||
draggingComboId === combo.id ? "is-dragging" : "",
|
||||
comboDropTarget.id === `combo:${combo.id}` ? "is-drop-target" : "",
|
||||
comboDropTarget.id === `combo:${combo.id}` && comboDropTarget.placement === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<article className={comboClassName} key={combo.id} data-combo-id={combo.id} data-combo-list={categoryId}>
|
||||
<button
|
||||
className="task-planner-drag-handle combo-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => startComboDrag(event, combo.id)}
|
||||
aria-label={`${textContent.reorderComboTitle || "Déplacer le combo"} ${combo.name}`}
|
||||
title={textContent.reorderComboTitle || "Déplacer le combo"}
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<div className="combo-card-main">
|
||||
<InlineTextInput
|
||||
className="tool-split-entry-label combo-title-input"
|
||||
value={combo.name}
|
||||
onCommit={(name) => updateCombo(categoryId, combo.id, (current) => ({ ...current, name: name || current.name }))}
|
||||
ariaLabel={textContent.comboNameLabel || "Nom"}
|
||||
/>
|
||||
<ComboSequence inputs={combo.inputs} device={combo.device || data.device} emptyLabel={textContent.emptySequence || "Combo sans touches"} />
|
||||
</div>
|
||||
<div className="combo-card-actions">
|
||||
<button type="button" className="checklist-delete-button" onClick={() => startComboEdit(categoryId, combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button type="button" className="checklist-delete-button danger" onClick={() => deleteCombo(categoryId, combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCategory(category) {
|
||||
const categoryClassName = [
|
||||
"combos-category checklist-section is-grouped",
|
||||
category.collapsed ? "is-collapsed" : "",
|
||||
draggingCategoryId === category.id ? "is-dragging" : "",
|
||||
categoryDropTarget.id === category.id || comboDropTarget.id === `category:${category.id}` ? "is-drop-target" : "",
|
||||
(categoryDropTarget.id === category.id && categoryDropTarget.placement === "after") ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<section className={categoryClassName} key={category.id} data-category-id={category.id}>
|
||||
<div className="checklist-section-header combos-category-header">
|
||||
<div className="combos-category-title">
|
||||
<button
|
||||
className="task-planner-category-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => startCategoryDrag(event, category.id)}
|
||||
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category.title}`}
|
||||
title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<h3>{category.title}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<span>{category.combos.length}</span>
|
||||
<button
|
||||
className="checklist-section-collapse-button"
|
||||
type="button"
|
||||
onClick={() => updateCategory(category.id, (item) => ({ ...item, collapsed: !item.collapsed }))}
|
||||
aria-expanded={!category.collapsed}
|
||||
aria-label={category.collapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||
title={category.collapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||
>
|
||||
<Icon name={category.collapsed ? "chevron-down" : "chevron-up"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!category.collapsed && (
|
||||
<div className="combos-list">
|
||||
{category.combos.map((combo) => renderCombo(combo, category.id))}
|
||||
{!category.combos.length && <p className="empty-state">{textContent.emptyCategory || "Aucun combo dans cette catégorie."}</p>}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="combos-module">
|
||||
{(editing || editingComboId) && (
|
||||
<div className="module-add-panel combos-editor">
|
||||
<div className="combos-device-row">
|
||||
<label>
|
||||
<span>{textContent.deviceLabel || "Périphérique"}</span>
|
||||
<select value={data.device} onChange={(event) => updateDevice(event.target.value)}>
|
||||
{DEVICE_OPTIONS.map((device) => (
|
||||
<option key={device.value} value={device.value}>{device.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{data.device === "keyboardMouse" && (
|
||||
<div className="combos-keyboard-layout-switch" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
|
||||
{[
|
||||
{ value: "azerty", label: "AZERTY" },
|
||||
{ value: "qwerty", label: "QWERTY" }
|
||||
].map((layout) => (
|
||||
<button
|
||||
key={layout.value}
|
||||
type="button"
|
||||
className={keyboardLayout === layout.value ? "active" : ""}
|
||||
onClick={() => setKeyboardLayout(layout.value)}
|
||||
aria-pressed={keyboardLayout === layout.value}
|
||||
>
|
||||
{layout.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form className="combos-add-form" onSubmit={saveCombo}>
|
||||
<div className="combos-form-grid">
|
||||
<label>
|
||||
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
|
||||
<input value={comboCategory} onChange={(event) => setComboCategory(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
|
||||
</label>
|
||||
<label>
|
||||
<span>{textContent.comboNameLabel || "Nom"}</span>
|
||||
<input value={comboName} onChange={(event) => setComboName(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="combos-palette" aria-label={textContent.paletteLabel || "Palette de touches"}>
|
||||
{paletteColumns.map((columnGroups, columnIndex) => (
|
||||
<div className="combos-palette-column" key={columnIndex}>
|
||||
{columnGroups.map((group) => (
|
||||
<div className={`combos-palette-group ${group.kind === "directions" ? "is-direction-grid" : ""}`} key={group.title}>
|
||||
<span>{group.title}</span>
|
||||
<div>
|
||||
{group.inputs.map((input, inputIndex) => {
|
||||
if (!input) return <span className="combo-palette-empty-cell" key={`empty-${inputIndex}`} aria-hidden="true" />;
|
||||
const [kind, value] = input;
|
||||
return (
|
||||
<ComboInputToken
|
||||
key={`${kind}-${value}`}
|
||||
input={createInput(kind, value)}
|
||||
device={data.device}
|
||||
palette
|
||||
onClick={() => addDraftInput(kind, value)}
|
||||
onDragStart={(event) => dragPaletteInput(event, kind, value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.device === "keyboardMouse" && (
|
||||
<div className="inline-form combos-custom-key-form">
|
||||
<input value={customKey} onChange={(event) => setCustomKey(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
|
||||
<button type="button" onClick={addCustomKey}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="combo-draft-panel">
|
||||
<ComboSequence inputs={draftInputs} device={data.device} emptyLabel={textContent.emptyDraft || "Aucune touche sélectionnée."} activeStepIndex={activeStepIndex} onSelectStep={setActiveStepIndex} onDropInput={dropInputOnStep} onDropNewStep={dropInputAsNewStep} onDragInput={dragDraftInput} />
|
||||
<div className="combo-draft-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}
|
||||
onClick={() => setSimultaneousMode((enabled) => !enabled)}
|
||||
aria-pressed={simultaneousMode}
|
||||
title={textContent.simultaneousModeTitle || "Ajout simultané"}
|
||||
aria-label={textContent.simultaneousModeTitle || "Ajout simultané"}
|
||||
>
|
||||
<span className={!simultaneousMode ? "active" : ""}>{textContent.successiveModeLabel || "A › B"}</span>
|
||||
<span className={simultaneousMode ? "active" : ""}>{textContent.simultaneousModeLabel || "A + B"}</span>
|
||||
</button>
|
||||
<button type="button" className="notepad-toolbar-button" onClick={removeLastInput} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
|
||||
<Icon name="back" />
|
||||
</button>
|
||||
<button type="button" className="notepad-toolbar-button danger" onClick={removeActiveStep} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
<div className="combo-save-actions">
|
||||
{editingComboId && (
|
||||
<button type="button" className="combo-cancel-button" onClick={cancelComboEdit}>
|
||||
{textContent.cancelEditButton || "Annuler"}
|
||||
</button>
|
||||
)}
|
||||
<button className="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
|
||||
{editingComboId ? textContent.saveComboButton || "Enregistrer" : textContent.addComboButton || "Ajouter le combo"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!data.combos.length && !data.categories.length && (
|
||||
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
|
||||
)}
|
||||
|
||||
<div className="combos-category-list">
|
||||
<div className={`combos-root-drop-zone ${draggingComboId ? "is-visible" : ""} ${comboDropTarget.id === "root" ? "is-drop-target" : ""}`} data-combo-root-drop="true">
|
||||
{textContent.noCategoryLabel || "Sans catégorie"}
|
||||
</div>
|
||||
{data.combos.length > 0 && (
|
||||
<div className="combos-list combos-root-list">
|
||||
{data.combos.map((combo) => renderCombo(combo))}
|
||||
</div>
|
||||
)}
|
||||
{data.categories.map(renderCategory)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
|||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CombosModule } from "./CombosModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
|
|
@ -22,6 +23,7 @@ const MODULE_COMPONENTS = {
|
|||
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: false },
|
||||
table: { label: "Tableau", icon: "table", Component: TableModule, editable: false, scrollable: true },
|
||||
timer: { label: "Timer", icon: "clock", Component: TimerModule, editable: false },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Rôle : normalise, compacte et sérialise les données toolbox persistées.
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z", task: "a", relation: "e", stroke: "d" };
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", combo: "o", calc: "r", marker: "k", timer: "z", task: "a", relation: "e", stroke: "d" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const NOTEPAD_ALLOWED_COLORS = new Set(["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#101426", "#202745"]);
|
||||
const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "s", "strike", "ul", "ol", "li", "h3", "h4", "span", "mark"]);
|
||||
|
|
@ -17,6 +17,8 @@ const TIMER_TABS = new Set(["stopwatch", "countdown"]);
|
|||
const COUNTDOWN_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
|
||||
const TIMER_ALERT_MODES = new Set(["off", "visible", "site"]);
|
||||
const TASK_TYPES = new Set(["unique", "daily", "weekly"]);
|
||||
const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboardMouse"]);
|
||||
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
|
||||
const DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY = 1;
|
||||
const DEFAULT_TASK_PLANNER_RESET_TIME = "00:00";
|
||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||
|
|
@ -47,6 +49,7 @@ const DEFAULT_MODULE_TITLES = {
|
|||
images: "Images",
|
||||
links: "Liens",
|
||||
counters: "Compteurs",
|
||||
combos: "Combos",
|
||||
calculator: "Calculateur",
|
||||
table: "Tableau",
|
||||
timer: "Timer",
|
||||
|
|
@ -356,6 +359,56 @@ export function normalizeCountersData(data) {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeComboInput(input) {
|
||||
const kind = COMBO_INPUT_KINDS.has(input?.kind) ? input.kind : "";
|
||||
const value = String(input?.value || "").trim().slice(0, 24);
|
||||
return kind && value ? { kind, value } : null;
|
||||
}
|
||||
|
||||
function normalizeComboSteps(inputs) {
|
||||
return (Array.isArray(inputs) ? inputs : [])
|
||||
.slice(0, 64)
|
||||
.map((step) => (Array.isArray(step) ? step : [])
|
||||
.map(normalizeComboInput)
|
||||
.filter(Boolean)
|
||||
.slice(0, 8))
|
||||
.filter((step) => step.length);
|
||||
}
|
||||
|
||||
function normalizeCombo(combo) {
|
||||
const name = String(combo?.name || "").trim().slice(0, 80);
|
||||
const device = COMBO_DEVICES.has(combo?.device) ? combo.device : "";
|
||||
const inputs = normalizeComboSteps(combo?.inputs);
|
||||
if (!name && !inputs.length) return null;
|
||||
const normalized = {
|
||||
id: combo?.id || uid("combo"),
|
||||
name: name || "Combo",
|
||||
inputs
|
||||
};
|
||||
if (device) normalized.device = device;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeComboCategory(category) {
|
||||
const title = String(category?.title || "").trim().slice(0, 80);
|
||||
const combos = (Array.isArray(category?.combos) ? category.combos : []).map(normalizeCombo).filter(Boolean);
|
||||
if (!combos.length) return null;
|
||||
return {
|
||||
id: category?.id || uid("section"),
|
||||
title: title || "Catégorie",
|
||||
collapsed: category?.collapsed === true,
|
||||
combos
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeCombosData(data) {
|
||||
return {
|
||||
device: COMBO_DEVICES.has(data?.device) ? data.device : "playstation",
|
||||
combos: (Array.isArray(data?.combos) ? data.combos : []).map(normalizeCombo).filter(Boolean),
|
||||
categories: (Array.isArray(data?.categories) ? data.categories : []).map(normalizeComboCategory).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCalculatorValue(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
|
|
@ -697,6 +750,41 @@ export function compactModuleDataForStorage(type, 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.device) compactCombo.device = combo.device;
|
||||
return compactCombo;
|
||||
}),
|
||||
categories: normalized.categories.map((category) => {
|
||||
const compactCategory = {
|
||||
id: category.id,
|
||||
title: category.title,
|
||||
combos: category.combos.map((combo) => {
|
||||
const compactCombo = {
|
||||
id: combo.id,
|
||||
name: combo.name,
|
||||
inputs: combo.inputs
|
||||
};
|
||||
if (combo.device) compactCombo.device = combo.device;
|
||||
return compactCombo;
|
||||
})
|
||||
};
|
||||
if (category.collapsed) compactCategory.collapsed = true;
|
||||
return compactCategory;
|
||||
})
|
||||
};
|
||||
if (normalized.device !== "playstation") compact.device = normalized.device;
|
||||
if (!compact.combos.length) delete compact.combos;
|
||||
if (!compact.categories.length) delete compact.categories;
|
||||
return compact.combos || compact.categories ? compact : null;
|
||||
}
|
||||
if (type === "calculator") {
|
||||
const normalized = normalizeCalculatorData(value);
|
||||
const entries = normalized.entries.map((entry) => {
|
||||
|
|
@ -837,6 +925,20 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) };
|
||||
}
|
||||
|
||||
if (type === "combos") {
|
||||
const remapped = {};
|
||||
if (compact.device) remapped.device = compact.device;
|
||||
if (compact.combos) remapped.combos = compact.combos.map((combo) => ({ ...combo, id: nextId("combo") }));
|
||||
if (compact.categories) {
|
||||
remapped.categories = compact.categories.map((category) => ({
|
||||
...category,
|
||||
id: nextId("section"),
|
||||
combos: category.combos.map((combo) => ({ ...combo, id: nextId("combo") }))
|
||||
}));
|
||||
}
|
||||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "calculator") {
|
||||
const entryIdMap = new Map();
|
||||
compact.entries.forEach((entry) => entryIdMap.set(entry.id, nextId("calc")));
|
||||
|
|
|
|||
|
|
@ -233,6 +233,25 @@
|
|||
grid-row: auto;
|
||||
}
|
||||
|
||||
.combos-device-row,
|
||||
.combos-form-grid,
|
||||
.combos-palette {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.combo-card {
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.combo-card-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.combo-card-actions {
|
||||
grid-column: 2;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.toolbox-actions-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -742,6 +742,11 @@
|
|||
-webkit-mask-image: url("/static/icons/table.svg");
|
||||
}
|
||||
|
||||
.module-icon-controller {
|
||||
mask-image: url("/static/icons/controller.svg");
|
||||
-webkit-mask-image: url("/static/icons/controller.svg");
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
||||
|
|
@ -2271,6 +2276,663 @@ textarea:focus {
|
|||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.combos-module {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.combos-editor {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.combos-device-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.combos-device-row label,
|
||||
.combos-form-grid label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.combos-device-row select,
|
||||
.combos-form-grid select,
|
||||
.combos-form-grid input {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.combos-keyboard-layout-switch {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.26);
|
||||
}
|
||||
|
||||
.combos-keyboard-layout-switch button {
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.combos-keyboard-layout-switch button.active {
|
||||
border-color: rgba(246, 196, 83, 0.26);
|
||||
background: rgba(246, 196, 83, 0.1);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.combos-add-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.combos-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 0.8fr) minmax(0, 1.2fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.combo-draft-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.32);
|
||||
}
|
||||
|
||||
.combo-draft-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.combos-simultaneous-toggle {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
min-width: 116px;
|
||||
min-height: 34px;
|
||||
padding: 3px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.26);
|
||||
color: var(--color-text-secondary);
|
||||
justify-content: stretch;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.combos-simultaneous-toggle span {
|
||||
display: inline-grid;
|
||||
min-width: 50px;
|
||||
min-height: 26px;
|
||||
place-items: center;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.combos-simultaneous-toggle span.active {
|
||||
background: rgba(246, 196, 83, 0.12);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.combo-draft-actions .primary {
|
||||
min-width: 104px;
|
||||
}
|
||||
|
||||
.combos-palette {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 0.8fr) minmax(0, 1.2fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.combos-palette-column {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.combos-palette-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 9px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
}
|
||||
|
||||
.combos-palette-group > span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.combos-palette-group > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.combos-palette-group.is-direction-grid > div {
|
||||
display: grid;
|
||||
width: fit-content;
|
||||
grid-template-columns: repeat(3, max-content);
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.combo-palette-empty-cell {
|
||||
display: block;
|
||||
width: 34px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.combos-custom-key-form {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.combos-category-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.combos-category-title {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.combos-category-title h3 {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.combos-category-header > div {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.combos-root-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.combos-root-drop-zone {
|
||||
display: none;
|
||||
padding: 9px 12px;
|
||||
border: 1px dashed rgba(165, 180, 252, 0.22);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.2);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.combos-root-drop-zone.is-visible,
|
||||
.combos-root-drop-zone.is-drop-target {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.combos-root-drop-zone.is-drop-target {
|
||||
border-color: rgba(246, 196, 83, 0.72);
|
||||
background: rgba(246, 196, 83, 0.08);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.combos-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.combo-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.36);
|
||||
}
|
||||
|
||||
.combo-card.is-editing {
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.combo-card.is-dragging,
|
||||
.combos-category.is-dragging {
|
||||
cursor: grabbing;
|
||||
opacity: 0.58;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.combo-card.is-drop-target,
|
||||
.combos-category.is-drop-target {
|
||||
border-color: rgba(246, 196, 83, 0.72);
|
||||
box-shadow: inset 0 3px 0 rgba(246, 196, 83, 0.8);
|
||||
}
|
||||
|
||||
.combo-card.is-drop-target.drop-after,
|
||||
.combos-category.is-drop-target.drop-after {
|
||||
box-shadow: inset 0 -3px 0 rgba(246, 196, 83, 0.8);
|
||||
}
|
||||
|
||||
.combo-drag-handle {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.combo-card-main {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.combo-title-input {
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.combo-card-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.combo-save-actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.combo-cancel-button {
|
||||
min-width: 104px;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 26, 48, 0.64);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.combo-sequence {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
column-gap: 5px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.combo-step,
|
||||
.combo-step-inputs,
|
||||
.combo-step-input {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.combo-step {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.combo-step-inputs {
|
||||
min-height: 42px;
|
||||
padding: 3px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-pill);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
button.combo-step-inputs {
|
||||
min-width: 0;
|
||||
min-height: 42px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.combo-step-inputs.is-single {
|
||||
min-height: 38px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.combo-step-inputs.is-simultaneous {
|
||||
padding: 5px;
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.07), transparent 64%),
|
||||
rgba(5, 7, 17, 0.28);
|
||||
}
|
||||
|
||||
.combo-step-inputs.is-active {
|
||||
border-color: rgba(139, 92, 246, 0.72);
|
||||
background: rgba(139, 92, 246, 0.08);
|
||||
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.12);
|
||||
}
|
||||
|
||||
.combo-step-inputs.is-single.is-active {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.combo-step-inputs.is-single.is-active .combo-input-token {
|
||||
box-shadow:
|
||||
inset 0 -2px 0 rgba(0, 0, 0, 0.2),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.18);
|
||||
}
|
||||
|
||||
.combo-step-separator,
|
||||
.combo-plus {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.combo-step-separator {
|
||||
margin-inline: 1px;
|
||||
}
|
||||
|
||||
.combo-empty-sequence {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.combo-empty-step {
|
||||
padding: 0 8px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.combo-input-token {
|
||||
--combo-token-bg: rgba(21, 26, 48, 0.82);
|
||||
--combo-token-border: rgba(165, 180, 252, 0.22);
|
||||
--combo-token-color: var(--color-text-primary);
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 1px solid var(--combo-token-border);
|
||||
border-radius: 50%;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.08), transparent),
|
||||
var(--combo-token-bg);
|
||||
color: var(--combo-token-color);
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
line-height: 1;
|
||||
box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button.combo-input-token {
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.combo-input-token:hover {
|
||||
border-color: var(--color-primary-border);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.1), transparent),
|
||||
rgba(32, 39, 69, 0.92);
|
||||
}
|
||||
|
||||
.combo-input-token span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.combo-input-key,
|
||||
.combo-input-mouse,
|
||||
.combo-value-options,
|
||||
.combo-value-share,
|
||||
.combo-value-start,
|
||||
.combo-value-menu,
|
||||
.combo-value-view,
|
||||
.combo-value-c-up,
|
||||
.combo-value-c-down,
|
||||
.combo-value-c-left,
|
||||
.combo-value-c-right {
|
||||
width: auto;
|
||||
min-width: 48px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.combo-input-short {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button.combo-input-token.combo-input-short {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button.combo-input-token.combo-input-key,
|
||||
button.combo-input-token.combo-input-mouse,
|
||||
button.combo-input-token.combo-value-options,
|
||||
button.combo-input-token.combo-value-share,
|
||||
button.combo-input-token.combo-value-start,
|
||||
button.combo-input-token.combo-value-menu,
|
||||
button.combo-input-token.combo-value-view,
|
||||
button.combo-input-token.combo-value-c-up,
|
||||
button.combo-input-token.combo-value-c-down,
|
||||
button.combo-input-token.combo-value-c-left,
|
||||
button.combo-input-token.combo-value-c-right {
|
||||
width: auto;
|
||||
min-width: 48px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
button.combo-input-token.combo-input-short {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.combo-input-key span,
|
||||
.combo-input-mouse span,
|
||||
.combo-value-options span,
|
||||
.combo-value-share span,
|
||||
.combo-value-start span,
|
||||
.combo-value-menu span,
|
||||
.combo-value-view span,
|
||||
.combo-value-c-up span,
|
||||
.combo-value-c-down span,
|
||||
.combo-value-c-left span,
|
||||
.combo-value-c-right span {
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.combo-input-mouse,
|
||||
button.combo-input-token.combo-input-mouse {
|
||||
min-width: 62px;
|
||||
}
|
||||
|
||||
.combo-value-l1,
|
||||
.combo-value-r1,
|
||||
.combo-value-l2,
|
||||
.combo-value-r2,
|
||||
.combo-value-l3,
|
||||
.combo-value-r3,
|
||||
.combo-value-lb,
|
||||
.combo-value-rb,
|
||||
.combo-value-lt,
|
||||
.combo-value-rt,
|
||||
.combo-value-ls,
|
||||
.combo-value-rs,
|
||||
.combo-value-zl,
|
||||
.combo-value-zr {
|
||||
width: 38px;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.combo-input-key {
|
||||
height: 32px;
|
||||
border-radius: 7px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245, 247, 255, 0.11), rgba(5, 7, 17, 0.18)),
|
||||
rgba(16, 20, 38, 0.94);
|
||||
box-shadow:
|
||||
inset 0 -3px 0 rgba(0, 0, 0, 0.28),
|
||||
0 1px 0 rgba(245, 247, 255, 0.08);
|
||||
}
|
||||
|
||||
.combo-value-ctrl,
|
||||
.combo-value-alt,
|
||||
.combo-value-shift,
|
||||
.combo-value-enter,
|
||||
.combo-value-tab {
|
||||
min-width: 58px;
|
||||
}
|
||||
|
||||
.combo-value-space {
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
.combo-input-direction {
|
||||
--combo-token-bg: rgba(34, 211, 238, 0.12);
|
||||
--combo-token-border: rgba(34, 211, 238, 0.3);
|
||||
--combo-token-color: var(--color-accent-cyan);
|
||||
}
|
||||
|
||||
.combo-device-playstation.combo-value-triangle {
|
||||
--combo-token-bg: rgba(55, 214, 122, 0.16);
|
||||
--combo-token-border: rgba(55, 214, 122, 0.4);
|
||||
--combo-token-color: #37d67a;
|
||||
}
|
||||
|
||||
.combo-device-playstation.combo-value-circle {
|
||||
--combo-token-bg: rgba(248, 113, 113, 0.16);
|
||||
--combo-token-border: rgba(248, 113, 113, 0.4);
|
||||
--combo-token-color: #f87171;
|
||||
}
|
||||
|
||||
.combo-device-playstation.combo-value-cross {
|
||||
--combo-token-bg: rgba(96, 165, 250, 0.16);
|
||||
--combo-token-border: rgba(96, 165, 250, 0.4);
|
||||
--combo-token-color: #60a5fa;
|
||||
}
|
||||
|
||||
.combo-device-playstation.combo-value-square {
|
||||
--combo-token-bg: rgba(244, 114, 182, 0.16);
|
||||
--combo-token-border: rgba(244, 114, 182, 0.4);
|
||||
--combo-token-color: #f472b6;
|
||||
}
|
||||
|
||||
.combo-device-xbox.combo-value-a {
|
||||
--combo-token-bg: rgba(101, 212, 110, 0.16);
|
||||
--combo-token-border: rgba(101, 212, 110, 0.4);
|
||||
--combo-token-color: #65d46e;
|
||||
}
|
||||
|
||||
.combo-device-xbox.combo-value-b {
|
||||
--combo-token-bg: rgba(239, 68, 68, 0.16);
|
||||
--combo-token-border: rgba(239, 68, 68, 0.4);
|
||||
--combo-token-color: #ef4444;
|
||||
}
|
||||
|
||||
.combo-device-xbox.combo-value-x {
|
||||
--combo-token-bg: rgba(59, 130, 246, 0.16);
|
||||
--combo-token-border: rgba(59, 130, 246, 0.4);
|
||||
--combo-token-color: #3b82f6;
|
||||
}
|
||||
|
||||
.combo-device-xbox.combo-value-y {
|
||||
--combo-token-bg: rgba(250, 204, 21, 0.16);
|
||||
--combo-token-border: rgba(250, 204, 21, 0.42);
|
||||
--combo-token-color: #facc15;
|
||||
}
|
||||
|
||||
.combo-device-switch.combo-value-a,
|
||||
.combo-device-switch.combo-value-x {
|
||||
--combo-token-bg: rgba(239, 68, 68, 0.14);
|
||||
--combo-token-border: rgba(239, 68, 68, 0.36);
|
||||
--combo-token-color: #fca5a5;
|
||||
}
|
||||
|
||||
.combo-device-switch.combo-value-b,
|
||||
.combo-device-switch.combo-value-y {
|
||||
--combo-token-bg: rgba(34, 211, 238, 0.14);
|
||||
--combo-token-border: rgba(34, 211, 238, 0.34);
|
||||
--combo-token-color: var(--color-accent-cyan);
|
||||
}
|
||||
|
||||
.combo-device-switch.combo-value-l,
|
||||
.combo-device-switch.combo-value-r,
|
||||
.combo-device-switch.combo-value-zl,
|
||||
.combo-device-switch.combo-value-zr {
|
||||
--combo-token-bg: rgba(245, 247, 255, 0.1);
|
||||
--combo-token-border: rgba(245, 247, 255, 0.28);
|
||||
}
|
||||
|
||||
.combo-device-n64.combo-value-a {
|
||||
--combo-token-bg: rgba(34, 197, 94, 0.18);
|
||||
--combo-token-color: #86efac;
|
||||
}
|
||||
|
||||
.combo-device-n64.combo-value-b {
|
||||
--combo-token-bg: rgba(59, 130, 246, 0.18);
|
||||
--combo-token-color: #93c5fd;
|
||||
}
|
||||
|
||||
.combo-device-n64.combo-value-c-up,
|
||||
.combo-device-n64.combo-value-c-down,
|
||||
.combo-device-n64.combo-value-c-left,
|
||||
.combo-device-n64.combo-value-c-right {
|
||||
--combo-token-bg: rgba(246, 196, 83, 0.2);
|
||||
--combo-token-border: rgba(246, 196, 83, 0.42);
|
||||
--combo-token-color: var(--color-accent-gold);
|
||||
}
|
||||
|
||||
.combo-device-n64.combo-value-start {
|
||||
--combo-token-bg: rgba(239, 68, 68, 0.16);
|
||||
--combo-token-color: #fca5a5;
|
||||
}
|
||||
|
||||
.counters-add-form {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue