Initial release of 8BitDo Control Center

Provide a Linux desktop controller configuration app with evdev input, uinput remapping, profiles, calibration, and reconnect recovery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 12:18:10 -07:00
commit 6507f5f8b3
121 changed files with 15415 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import { Button } from "@/components/common/Button";
import { useUIStore } from "@/stores/uiStore";
export function ConfirmDialogHost() {
const confirm = useUIStore((s) => s.confirm);
const resolveConfirm = useUIStore((s) => s.resolveConfirm);
if (!confirm) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-sm rounded-2xl border border-border bg-surface p-5 shadow-[var(--shadow-panel)]">
<h2 className="text-sm font-semibold text-fg">{confirm.title}</h2>
<p className="mt-2 text-sm text-fg-muted">{confirm.message}</p>
<div className="mt-5 flex justify-end gap-2">
<Button variant="ghost" onClick={() => resolveConfirm(false)}>
{confirm.cancelLabel ?? "Cancel"}
</Button>
<Button variant={confirm.danger ? "danger" : "primary"} onClick={() => resolveConfirm(true)}>
{confirm.confirmLabel ?? "Confirm"}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { AlertTriangle, CheckCircle2, Info, X, XCircle } from "lucide-react";
import type { Toast, ToastVariant } from "@/stores/uiStore";
import { useUIStore } from "@/stores/uiStore";
const VARIANT_ICON: Record<ToastVariant, typeof Info> = {
info: Info,
success: CheckCircle2,
warning: AlertTriangle,
danger: XCircle,
};
const VARIANT_CLASSES: Record<ToastVariant, string> = {
info: "border-border text-fg",
success: "border-success/40 text-success",
warning: "border-warning/40 text-warning",
danger: "border-danger/40 text-danger",
};
function ToastItem({ toast }: { toast: Toast }) {
const dismissToast = useUIStore((s) => s.dismissToast);
const Icon = VARIANT_ICON[toast.variant];
return (
<div
className={`flex items-start gap-2.5 rounded-xl border bg-surface px-4 py-3 shadow-[var(--shadow-panel)] ${VARIANT_CLASSES[toast.variant]}`}
>
<Icon size={16} className="mt-0.5 shrink-0" />
<p className="flex-1 text-sm text-fg">{toast.message}</p>
<button
onClick={() => dismissToast(toast.id)}
className="cursor-pointer text-fg-subtle hover:text-fg"
aria-label="Dismiss notification"
>
<X size={14} />
</button>
</div>
);
}
export function ToastStack() {
const toasts = useUIStore((s) => s.toasts);
return (
<div className="pointer-events-none fixed right-5 top-5 z-50 flex w-80 flex-col gap-2">
{toasts.map((toast) => (
<div key={toast.id} className="pointer-events-auto">
<ToastItem toast={toast} />
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { Check, RotateCcw, Save } from "lucide-react";
import { Button } from "@/components/common/Button";
import { useProfileStore } from "@/stores/profileStore";
/** Persistent Apply / Revert / Save Profile bar shown on every editable page. */
export function UnsavedChangesBar() {
const isDirty = useProfileStore((s) => s.isDirty);
const activeProfile = useProfileStore((s) => s.activeProfile);
const applyDraft = useProfileStore((s) => s.applyDraft);
const revertDraft = useProfileStore((s) => s.revertDraft);
const saveDraft = useProfileStore((s) => s.saveDraft);
if (!activeProfile) return null;
return (
<div className="mb-4 flex items-center justify-between rounded-xl border border-border bg-surface-2 px-4 py-2.5">
<div className="flex items-center gap-2 text-sm">
{isDirty ? (
<>
<span className="h-2 w-2 rounded-full bg-warning" />
<span className="text-fg">Unsaved changes to &ldquo;{activeProfile.name}&rdquo;</span>
</>
) : (
<>
<span className="h-2 w-2 rounded-full bg-success" />
<span className="text-fg-muted">
Editing &ldquo;{activeProfile.name}&rdquo; &middot; up to date
</span>
</>
)}
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" icon={<RotateCcw size={14} />} onClick={revertDraft} disabled={!isDirty}>
Revert
</Button>
<Button size="sm" variant="secondary" icon={<Check size={14} />} onClick={applyDraft} disabled={!isDirty}>
Apply
</Button>
<Button size="sm" variant="primary" icon={<Save size={14} />} onClick={() => void saveDraft()} disabled={!isDirty}>
Save Profile
</Button>
</div>
</div>
);
}