- );
-}
-
-function formatSource(source) {
- if (!source) return 'Unassigned';
- if (source.kind === 'axisPair') {
- const invert = `${source.invertX ? 'X inv ' : ''}${source.invertY ? 'Y inv' : ''}`.trim();
- return `Axes ${source.x}/${source.y}${invert ? ` (${invert})` : ''}`;
- }
- if (source.kind === 'axis') {
- return `Axis ${source.index}${source.invert ? ' (inv)' : ''}`;
- }
- if (source.kind === 'button') {
- return `Button ${source.index}`;
- }
- if (source.kind === 'buttonAxis') {
- return `Button ${source.index} (analog)`;
- }
- if (source.kind === 'axisButton') {
- const dir = source.direction < 0 ? '<' : '>';
- return `Axis ${source.index} ${dir} ${NUMBER_FORMAT.format(source.threshold ?? 0.6)}`;
- }
- return 'Unassigned';
-}
-
-function groupActions(actions) {
- return actions.reduce((acc, action) => {
- const list = acc[action.section] || (acc[action.section] = []);
- list.push(action);
- return acc;
- }, {});
-}
-
-function pickActivePad(pads, activeSignature) {
- if (!pads || pads.length === 0) return null;
- if (activeSignature) {
- const match = pads.find((pad) => pad.signature === activeSignature);
- if (match) return match;
- }
- return pads[0];
-}
-
-function snapshotBaseline(pad) {
- return {
- axes: Array.from(pad.axes ?? []),
- buttons: (pad.buttons ?? []).map((btn) => ({
- pressed: Boolean(btn?.pressed),
- value: typeof btn?.value === 'number' ? btn.value : btn?.pressed ? 1 : 0,
- })),
- };
-}
-
-function detectAxisCapture(pad, baseline, action) {
- const deltas = (pad.axes ?? []).map((value, index) => ({
- index,
- delta: Math.abs((value ?? 0) - (baseline.axes?.[index] ?? 0)),
- value,
- }));
- deltas.sort((a, b) => b.delta - a.delta);
- if (action.kind === 'axisPair') {
- const top = deltas.filter((entry) => entry.delta > CAPTURE_AXIS_THRESHOLD).slice(0, 2);
- if (top.length < 2) return null;
- return {
- kind: 'axisPair',
- x: top[0].index,
- y: top[1].index,
- ...(action.invertDefaults ?? {}),
- };
- }
- const match = deltas.find((entry) => entry.delta > CAPTURE_AXIS_THRESHOLD);
- if (!match) return null;
- return {
- kind: 'axis',
- index: match.index,
- ...(action.invertDefaults ?? {}),
- };
-}
-
-function detectButtonCapture(pad, baseline, action) {
- const buttons = pad.buttons ?? [];
- for (let i = 0; i < buttons.length; i += 1) {
- const btn = buttons[i];
- const value = typeof btn?.value === 'number' ? btn.value : btn?.pressed ? 1 : 0;
- if (btn?.pressed || value > CAPTURE_BUTTON_THRESHOLD) {
- if (action.kind === 'axis') {
- return { kind: 'buttonAxis', index: i };
- }
- return { kind: 'button', index: i };
- }
- }
- const axes = pad.axes ?? [];
- for (let i = 0; i < axes.length; i += 1) {
- const value = axes[i] ?? 0;
- const delta = Math.abs(value - (baseline.axes?.[i] ?? 0));
- if (Math.abs(value) > 0.7 && delta > 0.5) {
- if (action.kind === 'axis') {
- return { kind: 'axis', index: i, ...(action.invertDefaults ?? {}) };
- }
- return { kind: 'axisButton', index: i, direction: value >= 0 ? 1 : -1, threshold: 0.6 };
- }
- }
- return null;
-}
-
-function buildDescriptorFromCapture(pad, baseline, action) {
- if (!pad) return null;
- if (action.kind === 'axis' || action.kind === 'axisPair') {
- const axisDescriptor = detectAxisCapture(pad, baseline, action);
- if (axisDescriptor) return axisDescriptor;
- }
- return detectButtonCapture(pad, baseline, action);
-}
+} from '../../controls/inputs/gamepadBindings.js';
+import { useGamepadHubState } from '../../controls/inputs/gamepadHub.js';
+import SliderField from './SliderField.jsx';
+import { ACTIONS, NUMBER_FORMAT } from './constants.js';
+import {
+ formatSource,
+ groupActions,
+ pickActivePad,
+ snapshotBaseline,
+ buildDescriptorFromCapture,
+} from './helpers.js';
export default function GamepadMappingSettings() {
const hubState = useGamepadHubState();
diff --git a/webui/src/components/GamepadMappingSettings/SliderField.jsx b/webui/src/components/GamepadMappingSettings/SliderField.jsx
new file mode 100644
index 00000000..f479451c
--- /dev/null
+++ b/webui/src/components/GamepadMappingSettings/SliderField.jsx
@@ -0,0 +1,23 @@
+// Reusable slider field for gamepad calibration controls.
+import { NUMBER_FORMAT } from './constants.js';
+
+export default function SliderField({ label, description, min, max, step, value, onChange }) {
+ return (
+