Add hold and special inputs to combos
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-08-03 12:53:34 +02:00
parent 9b72f4934c
commit 1a35dcae7b
15 changed files with 575 additions and 26 deletions

View file

@ -16,26 +16,32 @@ const DEVICE_OPTIONS = [
{ 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"]] }
]
};
@ -45,12 +51,14 @@ const KEYBOARD_GROUPS = {
{ 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"]] }
]
};
@ -84,9 +92,22 @@ const INPUT_LABELS = {
"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",
@ -103,11 +124,33 @@ function getInputLabel(input) {
}
function inputKey(input) {
return `${input.kind}:${input.value}`;
return `${input.kind}:${input.value}:${input.hold ? "hold" : ""}:${input.holdMs || 0}`;
}
function createInput(kind, value) {
return { kind, value };
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) {
@ -153,9 +196,13 @@ function setComboItemCategory(combo, category) {
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()}`
@ -164,19 +211,20 @@ function ComboInputToken({ input, device, palette = false, onClick, onDragStart
if (palette) {
return (
<button className={classNames} type="button" draggable onClick={onClick} onDragStart={onDragStart} title={label} aria-label={label}>
<span>{label}</span>
<span>{iconName ? <Icon name={iconName} /> : label}</span>
</button>
);
}
return (
<span className={classNames} draggable={Boolean(onDragStart)} onDragStart={onDragStart} title={label}>
<span>{label}</span>
<span className={classNames} draggable={Boolean(onDragStart)} onClick={onClick} onDragStart={onDragStart} title={holdLabel ? `${label} · ${holdLabel}` : label}>
<span>{iconName ? <Icon name={iconName} /> : label}</span>
{holdLabel && <em className="combo-input-hold">{holdLabel}</em>}
</span>
);
}
function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInput, onDragInput }) {
function ComboStep({ step, stepIndex, device, active = false, selectedInputIndex = -1, onClick, onSelectInput, onDropInput, onDragInput }) {
const className = [
"combo-step-inputs",
step.length > 1 ? "is-simultaneous" : "",
@ -184,9 +232,17 @@ function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInp
active ? "is-active" : ""
].filter(Boolean).join(" ");
const content = step.length ? step.map((input, inputIndex) => (
<span className="combo-step-input" key={`${inputKey(input)}-${inputIndex}`}>
<span className={`combo-step-input ${selectedInputIndex === inputIndex ? "is-selected" : ""}`} 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} />
<ComboInputToken
input={input}
device={device}
onClick={onSelectInput ? (event) => {
event.stopPropagation();
onSelectInput(stepIndex, inputIndex);
} : null}
onDragStart={onDragInput ? (event) => onDragInput(event, stepIndex, inputIndex, input) : null}
/>
</span>
)) : <span className="combo-empty-step">Étape vide</span>;
@ -214,7 +270,7 @@ function ComboStep({ step, stepIndex, device, active = false, onClick, onDropInp
return <span className={className}>{content}</span>;
}
function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, onSelectStep, onDropInput, onDropNewStep, onDragInput }) {
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 <span className="combo-empty-sequence">{emptyLabel}</span>;
return (
@ -230,12 +286,26 @@ function ComboSequence({ inputs, device, emptyLabel, activeStepIndex = -1, onSel
onDropNewStep(event);
}}
>
{visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => (
{visibleInputs.map(({ step, index: stepIndex }, visibleIndex) => {
const nextStep = visibleInputs[visibleIndex + 1]?.step || [];
const hideSeparator = step.every(isDirectionInput) && nextStep.every(isDirectionInput);
return (
<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>}
<ComboStep
step={step}
stepIndex={stepIndex}
device={device}
active={activeStepIndex === stepIndex}
selectedInputIndex={selectedInput?.stepIndex === stepIndex ? selectedInput.inputIndex : -1}
onClick={onSelectStep ? () => onSelectStep(stepIndex) : null}
onSelectInput={onSelectInput}
onDropInput={onDropInput ? (event) => onDropInput(event, stepIndex) : null}
onDragInput={onDragInput}
/>
{visibleIndex < visibleInputs.length - 1 && !hideSeparator && <span className="combo-step-separator" aria-hidden="true"></span>}
</span>
))}
);
})}
</div>
);
}
@ -265,6 +335,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const [comboName, setComboName] = useState("");
const [draftInputs, setDraftInputs] = useState([[]]);
const [activeStepIndex, setActiveStepIndex] = useState(0);
const [selectedInput, setSelectedInput] = useState(null);
const [simultaneousMode, setSimultaneousMode] = useState(false);
const [keyboardLayout, setKeyboardLayout] = useState("azerty");
const [customKey, setCustomKey] = useState("");
@ -272,6 +343,9 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
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 selectedDraftInput = Number.isInteger(selectedInput?.stepIndex) && Number.isInteger(selectedInput?.inputIndex)
? draftInputs[selectedInput.stepIndex]?.[selectedInput.inputIndex] || null
: null;
const entries = getGroupedEntries(data.combos, {
groupOrder: data.categoryOrder,
getItemGroup: getComboCategory,
@ -344,6 +418,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const nextSteps = steps.length ? steps.map((step) => [...step]) : [[]];
const targetIndex = Math.min(Math.max(0, stepIndex), nextSteps.length - 1);
nextSteps[targetIndex] = [...nextSteps[targetIndex], input];
setSelectedInput({ stepIndex: targetIndex, inputIndex: nextSteps[targetIndex].length - 1 });
return nextSteps;
});
}
@ -359,6 +434,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const filledSteps = steps.filter((step) => step.length);
const nextSteps = [...filledSteps, [input]];
setActiveStepIndex(nextSteps.length - 1);
setSelectedInput({ stepIndex: nextSteps.length - 1, inputIndex: 0 });
return nextSteps;
});
}
@ -417,6 +493,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const [input] = nextSteps[sourceStepIndex]?.splice(sourceInputIndex, 1) || [];
if (!input || !nextSteps[targetStepIndex]) return steps;
nextSteps[targetStepIndex].push(input);
setSelectedInput({ stepIndex: targetStepIndex, inputIndex: nextSteps[targetStepIndex].length - 1 });
return nextSteps.filter((step, index) => step.length || index === targetStepIndex);
});
}
@ -428,6 +505,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
if (!input) return steps;
const safeSteps = nextSteps.filter((step) => step.length);
setActiveStepIndex(safeSteps.length);
setSelectedInput({ stepIndex: safeSteps.length, inputIndex: 0 });
return [...safeSteps, [input]];
});
}
@ -437,6 +515,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
const nextSteps = steps.filter((_, index) => index !== activeStepIndex);
const safeSteps = nextSteps.length ? nextSteps : [[]];
setActiveStepIndex(Math.min(activeStepIndex, safeSteps.length - 1));
setSelectedInput(null);
return safeSteps;
});
}
@ -455,10 +534,46 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
nextSteps[targetIndex].pop();
const safeSteps = nextSteps.filter((step) => step.length);
setActiveStepIndex(Math.max(0, safeSteps.length - 1));
setSelectedInput(null);
return safeSteps.length ? safeSteps : [[]];
});
}
function updateSelectedInputHold(value) {
if (!selectedDraftInput) return;
const holdMs = Math.round(parseHoldSeconds(value) * 1000);
setDraftInputs((steps) => steps.map((step, stepIndex) => step.map((input, inputIndex) => {
if (stepIndex !== selectedInput.stepIndex || inputIndex !== selectedInput.inputIndex) return input;
const nextInput = { ...input };
if (holdMs > 0) {
nextInput.hold = true;
nextInput.holdMs = holdMs;
}
else delete nextInput.holdMs;
return nextInput;
})));
}
function toggleSelectedInputHold() {
if (!selectedDraftInput) return;
setDraftInputs((steps) => steps.map((step, stepIndex) => step.map((input, inputIndex) => {
if (stepIndex !== selectedInput.stepIndex || inputIndex !== selectedInput.inputIndex) return input;
const nextInput = { ...input };
if (nextInput.hold || nextInput.holdMs) {
delete nextInput.hold;
delete nextInput.holdMs;
} else {
nextInput.hold = true;
}
return nextInput;
})));
}
function selectDraftStep(stepIndex) {
setActiveStepIndex(stepIndex);
setSelectedInput(null);
}
function addCustomKey(event) {
event.preventDefault();
const value = normalizeCustomKey(customKey);
@ -485,6 +600,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
setComboName(combo.name);
setDraftInputs(combo.inputs.length ? combo.inputs.map((step) => step.map((input) => ({ ...input }))) : [[]]);
setActiveStepIndex(0);
setSelectedInput(null);
}
function cancelComboEdit() {
@ -493,6 +609,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
setComboCategory("");
setDraftInputs([[]]);
setActiveStepIndex(0);
setSelectedInput(null);
setSimultaneousMode(false);
}
@ -708,8 +825,47 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
)}
<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} />
<ComboSequence
inputs={draftInputs}
device={data.device}
emptyLabel={textContent.emptyDraft || "Aucune touche sélectionnée."}
activeStepIndex={activeStepIndex}
selectedInput={selectedInput}
onSelectStep={selectDraftStep}
onSelectInput={(stepIndex, inputIndex) => {
setActiveStepIndex(stepIndex);
setSelectedInput({ stepIndex, inputIndex });
}}
onDropInput={dropInputOnStep}
onDropNewStep={dropInputAsNewStep}
onDragInput={dragDraftInput}
/>
<div className="combo-draft-actions">
<label className="combo-hold-field">
<span className="combo-hold-controls">
<button
type="button"
className={selectedDraftInput?.hold || selectedDraftInput?.holdMs ? "active" : ""}
onClick={toggleSelectedInputHold}
disabled={!selectedDraftInput}
aria-pressed={Boolean(selectedDraftInput?.hold || selectedDraftInput?.holdMs)}
>
{textContent.holdToggleLabel || "Hold"}
</button>
<input
type="number"
min="0"
max="99"
step="1"
inputMode="numeric"
value={selectedDraftInput ? getHoldSeconds(selectedDraftInput) || "" : ""}
onChange={(event) => updateSelectedInputHold(event.target.value)}
disabled={!selectedDraftInput}
placeholder="0"
aria-label={textContent.holdLabel || "Maintien en secondes"}
/>
</span>
</label>
<button
type="button"
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}

View file

@ -362,7 +362,15 @@ 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;
if (!kind || !value) return null;
const holdMs = Number(input?.holdMs);
const normalizedHoldMs = Number.isFinite(holdMs) && holdMs > 0
? Math.min(99000, Math.round(holdMs / 1000) * 1000)
: 0;
const normalized = { kind, value };
if (input?.hold === true || normalizedHoldMs > 0) normalized.hold = true;
if (normalizedHoldMs > 0) normalized.holdMs = normalizedHoldMs;
return normalized;
}
function normalizeComboSteps(inputs) {

View file

@ -252,6 +252,31 @@
-webkit-mask-image: url("/static/icons/back.svg");
}
.ui-icon-fist {
mask-image: url("/static/icons/fist.svg");
-webkit-mask-image: url("/static/icons/fist.svg");
}
.ui-icon-foot {
mask-image: url("/static/icons/foot.svg");
-webkit-mask-image: url("/static/icons/foot.svg");
}
.ui-icon-grab {
mask-image: url("/static/icons/grab.svg");
-webkit-mask-image: url("/static/icons/grab.svg");
}
.ui-icon-shield {
mask-image: url("/static/icons/shield.svg");
-webkit-mask-image: url("/static/icons/shield.svg");
}
.ui-icon-gun {
mask-image: url("/static/icons/gun.svg");
-webkit-mask-image: url("/static/icons/gun.svg");
}
.ui-icon-trash {
mask-image: url("/static/icons/trashcan.svg");
-webkit-mask-image: url("/static/icons/trashcan.svg");

View file

@ -2732,6 +2732,20 @@ textarea:focus {
min-width: 0;
}
.combo-step-input.is-selected .combo-input-token {
box-shadow:
var(--combo-token-shadow),
0 0 0 2px rgba(246, 196, 83, 0.22);
}
.combo-step-input.is-selected .combo-input-token.has-hold {
box-shadow:
var(--combo-token-shadow),
0 0 0 2px rgba(246, 196, 83, 0.22),
7px 0 0 -1px var(--combo-hold-shadow),
9px 0 6px -3px var(--combo-hold-shadow);
}
.combo-step-inputs {
min-height: 42px;
padding: 3px;
@ -2775,10 +2789,18 @@ button.combo-step-inputs {
.combo-step-inputs.is-single.is-active .combo-input-token {
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.2),
var(--combo-token-shadow),
0 0 0 2px rgba(139, 92, 246, 0.18);
}
.combo-step-inputs.is-single.is-active .combo-input-token.has-hold {
box-shadow:
var(--combo-token-shadow),
0 0 0 2px rgba(139, 92, 246, 0.18),
7px 0 0 -1px var(--combo-hold-shadow),
9px 0 6px -3px var(--combo-hold-shadow);
}
.combo-step-separator,
.combo-plus {
color: var(--color-text-muted);
@ -2807,6 +2829,8 @@ button.combo-step-inputs {
--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);
--combo-token-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.2);
--combo-hold-shadow: rgba(125, 135, 159, 0.34);
display: inline-flex;
box-sizing: border-box;
width: 34px;
@ -2825,7 +2849,7 @@ button.combo-step-inputs {
font-size: 12px;
font-weight: 950;
line-height: 1;
box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.2);
box-shadow: var(--combo-token-shadow);
}
button.combo-input-token {
@ -2852,6 +2876,36 @@ button.combo-input-token:hover {
white-space: nowrap;
}
.combo-input-token .ui-icon {
width: 17px;
height: 17px;
}
.combo-input-token.has-hold {
position: relative;
box-shadow:
var(--combo-token-shadow),
7px 0 0 -1px var(--combo-hold-shadow),
9px 0 6px -3px var(--combo-hold-shadow);
}
.combo-input-hold {
position: absolute;
right: -7px;
bottom: -6px;
min-width: 20px;
padding: 1px 4px;
border: 1px solid rgba(246, 196, 83, 0.34);
border-radius: var(--radius-pill);
background: rgba(5, 7, 17, 0.92);
color: var(--color-accent-gold);
font-size: 9px;
font-style: normal;
font-weight: 950;
line-height: 1.2;
text-align: center;
}
.combo-input-key,
.combo-input-mouse,
.combo-value-options,
@ -2967,9 +3021,86 @@ button.combo-input-token.combo-input-mouse {
}
.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-token-bg:
radial-gradient(circle at 50% 42%, rgba(34, 211, 238, 0.26), transparent 64%),
rgba(34, 211, 238, 0.12);
--combo-token-border: rgba(34, 211, 238, 0.52);
--combo-token-color: #67e8f9;
--combo-token-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.24),
inset 0 0 0 1px rgba(34, 211, 238, 0.16);
font-size: 15px;
}
button.combo-input-token.combo-input-direction:hover {
border-color: rgba(34, 211, 238, 0.72);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent),
radial-gradient(circle at 50% 42%, rgba(34, 211, 238, 0.34), transparent 64%),
rgba(18, 52, 70, 0.76);
}
.combo-device-n64.combo-value-stick-up-left,
.combo-device-n64.combo-value-stick-up,
.combo-device-n64.combo-value-stick-up-right,
.combo-device-n64.combo-value-stick-left,
.combo-device-n64.combo-value-stick-right,
.combo-device-n64.combo-value-stick-down-left,
.combo-device-n64.combo-value-stick-down,
.combo-device-n64.combo-value-stick-down-right {
--combo-token-bg:
radial-gradient(circle at 38% 30%, rgba(245, 247, 255, 0.2), transparent 28%),
radial-gradient(circle at 50% 54%, rgba(34, 211, 238, 0.3), transparent 66%),
rgba(14, 75, 94, 0.68);
--combo-token-border: rgba(34, 211, 238, 0.62);
--combo-token-color: #a5f3fc;
--combo-token-shadow:
inset 0 -3px 0 rgba(0, 0, 0, 0.24),
inset 0 0 0 1px rgba(34, 211, 238, 0.18);
}
.combo-device-n64.combo-value-up-left,
.combo-device-n64.combo-value-up,
.combo-device-n64.combo-value-up-right,
.combo-device-n64.combo-value-left,
.combo-device-n64.combo-value-right,
.combo-device-n64.combo-value-down-left,
.combo-device-n64.combo-value-down,
.combo-device-n64.combo-value-down-right {
--combo-token-bg:
linear-gradient(180deg, rgba(246, 196, 83, 0.12), transparent),
rgba(31, 34, 54, 0.92);
--combo-token-border: rgba(246, 196, 83, 0.48);
--combo-token-color: #f8d982;
--combo-token-shadow:
inset 0 -3px 0 rgba(0, 0, 0, 0.28),
inset 0 0 0 1px rgba(246, 196, 83, 0.14);
border-radius: 10px;
}
button.combo-device-n64.combo-value-stick-up-left:hover,
button.combo-device-n64.combo-value-stick-up:hover,
button.combo-device-n64.combo-value-stick-up-right:hover,
button.combo-device-n64.combo-value-stick-left:hover,
button.combo-device-n64.combo-value-stick-right:hover,
button.combo-device-n64.combo-value-stick-down-left:hover,
button.combo-device-n64.combo-value-stick-down:hover,
button.combo-device-n64.combo-value-stick-down-right:hover {
border-color: rgba(34, 211, 238, 0.78);
}
button.combo-device-n64.combo-value-up-left:hover,
button.combo-device-n64.combo-value-up:hover,
button.combo-device-n64.combo-value-up-right:hover,
button.combo-device-n64.combo-value-left:hover,
button.combo-device-n64.combo-value-right:hover,
button.combo-device-n64.combo-value-down-left:hover,
button.combo-device-n64.combo-value-down:hover,
button.combo-device-n64.combo-value-down-right:hover {
border-color: rgba(246, 196, 83, 0.7);
background:
linear-gradient(180deg, rgba(246, 196, 83, 0.16), transparent),
rgba(38, 40, 62, 0.96);
}
.combo-device-playstation.combo-value-triangle {
@ -3066,6 +3197,104 @@ button.combo-input-token.combo-input-mouse {
--combo-token-color: #fca5a5;
}
.combo-value-fist,
.combo-value-foot,
.combo-value-grab,
.combo-value-protect,
.combo-value-shoot {
--combo-token-bg: rgba(217, 70, 239, 0.14);
--combo-token-border: rgba(217, 70, 239, 0.34);
--combo-token-color: var(--color-accent-pink);
}
.combo-value-grab .ui-icon {
width: 21px;
height: 21px;
}
button.combo-input-token.combo-value-grab .ui-icon {
width: 23px;
height: 23px;
}
.combo-hold-field {
display: inline-flex;
align-items: center;
margin: 0;
}
.combo-hold-controls {
display: inline-flex;
width: 92px;
min-width: 92px;
min-height: 34px;
align-items: center;
justify-content: stretch;
gap: 3px;
padding: 3px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.26);
}
.combo-hold-controls button {
min-width: 50px;
min-height: 26px;
padding: 0;
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;
}
.combo-hold-controls button.active {
border-color: rgba(246, 196, 83, 0.34);
background: rgba(246, 196, 83, 0.12);
color: var(--color-text-primary);
}
.combo-hold-controls button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.combo-hold-field input {
box-sizing: border-box;
width: 26px;
min-width: 26px;
height: 26px;
min-height: 26px;
margin: 0;
padding: 0 3px;
border: 1px solid transparent;
border-color: transparent;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
line-height: 1;
text-align: center;
}
.combo-hold-field input:focus {
border-color: rgba(246, 196, 83, 0.34);
}
.combo-hold-field input {
appearance: textfield;
-moz-appearance: textfield;
}
.combo-hold-field input::-webkit-outer-spin-button,
.combo-hold-field input::-webkit-inner-spin-button {
margin: 0;
appearance: none;
-webkit-appearance: none;
}
.counters-add-form {
grid-template-columns: minmax(0, 1fr) auto;
}