feat: build module selection

This commit is contained in:
Ben Allfree
2025-11-23 09:37:10 -08:00
parent dfea7a358d
commit 6ddf13f2e8
7 changed files with 302 additions and 65 deletions
+62
View File
@@ -0,0 +1,62 @@
interface ModuleCardProps {
name: string;
description: string;
selected: boolean;
onClick: () => void;
}
export function ModuleCard({
name,
description,
selected,
onClick,
}: ModuleCardProps) {
return (
<button
type="button"
onClick={onClick}
className={`
w-full text-left p-4 rounded-lg border-2 transition-all
${
selected
? "border-blue-500 bg-blue-500/10"
: "border-slate-700 bg-slate-900/50 hover:border-slate-600"
}
`}
>
<div className="flex items-start gap-3">
<div className="mt-1">
<div
className={`
w-5 h-5 rounded border-2 flex items-center justify-center
${selected ? "border-blue-500 bg-blue-500" : "border-slate-500"}
`}
>
{selected && (
<svg
className="w-3 h-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<title>Checkmark</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-sm mb-1">{name}</h4>
<p className="text-xs text-slate-400 leading-relaxed">
{description}
</p>
</div>
</div>
</button>
);
}
+46 -33
View File
@@ -5,8 +5,10 @@ import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { api } from "../../convex/_generated/api";
import modulesData from "../../convex/modules.json";
import { TARGETS } from "../constants/targets";
import { VERSIONS } from "../constants/versions";
import { ModuleCard } from "./ModuleCard";
interface ProfileEditorProps {
initialData?: any;
@@ -26,10 +28,7 @@ export default function ProfileEditor({
defaultValues: initialData || {
name: "",
targets: [],
config: {
MESHTASTIC_EXCLUDE_MQTT: false,
MESHTASTIC_EXCLUDE_AUDIO: false,
},
config: {},
version: VERSIONS[0],
},
});
@@ -99,18 +98,22 @@ export default function ProfileEditor({
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium mb-2">Profile Name</label>
<label htmlFor="name" className="block text-sm font-medium mb-2">
Profile Name
</label>
<Input
id="name"
{...register("name")}
className="bg-slate-950 border-slate-800"
placeholder="e.g. Solar Repeater"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
<label htmlFor="version" className="block text-sm font-medium mb-2">
Firmware Version
</label>
<select
id="version"
{...register("version")}
className="w-full h-10 px-3 rounded-md border border-slate-800 bg-slate-950 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-950"
>
@@ -125,8 +128,6 @@ export default function ProfileEditor({
<div>
<label className="block text-sm font-medium mb-2">Targets</label>
{/* ... existing target UI */}
<div className="space-y-4">
{/* Category Pills */}
<div className="flex flex-wrap gap-2">
@@ -173,6 +174,11 @@ export default function ProfileEditor({
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{item.name}
{item.architecture && (
<span className="ml-2 text-xs text-slate-500">
({item.architecture})
</span>
)}
</label>
</div>
))}
@@ -181,36 +187,43 @@ export default function ProfileEditor({
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Configuration Flags
</label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="no_mqtt"
checked={watch("config.MESHTASTIC_EXCLUDE_MQTT")}
onCheckedChange={(checked) =>
setValue("config.MESHTASTIC_EXCLUDE_MQTT", checked)
}
/>
<label htmlFor="no_mqtt">Exclude MQTT</label>
<div className="space-y-6">
<div>
<div className="mb-4">
<h3 className="text-lg font-medium">Modules</h3>
<p className="text-sm text-slate-400">
Select the modules to include in your build.
</p>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="no_audio"
checked={watch("config.MESHTASTIC_EXCLUDE_AUDIO")}
onCheckedChange={(checked) =>
setValue("config.MESHTASTIC_EXCLUDE_AUDIO", checked)
}
/>
<label htmlFor="no_audio">Exclude Audio</label>
<div className="flex flex-col gap-2">
{modulesData.modules.map((module) => {
// Inverted logic:
// config[id] === false -> Explicitly Included
// config[id] === true or undefined -> Excluded
const configValue = watch(`config.${module.id}`);
const isIncluded = configValue === false;
return (
<ModuleCard
key={module.id}
name={module.name}
description={module.description}
selected={isIncluded}
onClick={() => {
// Toggle:
// If currently included (true), we want to exclude (set config to true)
// If currently excluded (false), we want to include (set config to false)
setValue(`config.${module.id}`, !!isIncluded);
}}
/>
);
})}
</div>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button type="button" variant="ghost" onClick={onCancel}>
<div className="flex justify-end gap-4 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Save Profile</Button>
+2
View File
@@ -3,6 +3,7 @@ import hardwareList from "../../vendor/web-flasher/public/data/hardware-list.jso
export interface TargetMetadata {
name: string;
category: string;
architecture?: string;
}
export const TARGETS: Record<string, TargetMetadata> = {};
@@ -17,6 +18,7 @@ sortedHardware.forEach((hw) => {
TARGETS[hw.platformioTarget] = {
name: hw.displayName || hw.platformioTarget,
category: hw.tags?.[0] || "Other",
architecture: hw.architecture,
};
}
});