use grid for source and target

This commit is contained in:
Josh Hawkins 2026-05-28 15:03:45 -05:00
parent f8172d6ff7
commit 2aceeb4a1c

View File

@ -51,7 +51,7 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { LuChevronDown, LuTriangleAlert } from "react-icons/lu"; import { LuArrowRight, LuChevronDown, LuTriangleAlert } from "react-icons/lu";
import { import {
CLONE_CATEGORIES, CLONE_CATEGORIES,
type CloneCategoryKey, type CloneCategoryKey,
@ -578,255 +578,270 @@ export default function CloneCameraDialog({
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-3"> <div className="relative flex flex-col gap-6 md:grid md:grid-cols-2 md:items-start md:gap-12">
<Label className="text-base"> <div className="space-y-3">
{t("cameraManagement.clone.source.label")} <Label className="text-base">
</Label> {t("cameraManagement.clone.source.label")}
<FormField </Label>
control={form.control} <FormField
name="sourceCamera" control={form.control}
render={({ field }) => ( name="sourceCamera"
<FormItem> render={({ field }) => (
<FormControl> <FormItem>
<Select <FormControl>
value={field.value} <Select
onValueChange={field.onChange} value={field.value}
disabled={isSubmitting} onValueChange={field.onChange}
> disabled={isSubmitting}
<SelectTrigger className="w-full max-w-xs"> >
<SelectValue <SelectTrigger className="w-full max-w-xs md:max-w-none">
placeholder={t( <SelectValue
"cameraManagement.clone.source.placeholder", placeholder={t(
)} "cameraManagement.clone.source.placeholder",
/> )}
</SelectTrigger>
<SelectContent>
{sourceCameras.map((cam) => (
<SelectItem key={cam} value={cam}>
{config?.cameras?.[cam]?.friendly_name ?? cam}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-3">
<Label className="text-base">
{t("cameraManagement.clone.target.legend")}
</Label>
<FormField
control={form.control}
name="targetMode"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="space-y-3"
>
<div className="space-y-2">
<div className="flex items-center space-x-3 space-y-0">
<RadioGroupItem
value="new"
id="clone-target-new"
className={
targetMode === "new"
? "bg-selected from-selected/50 to-selected/90 text-selected"
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
}
/> />
<Label </SelectTrigger>
htmlFor="clone-target-new" <SelectContent>
className="font-normal" {sourceCameras.map((cam) => (
> <SelectItem key={cam} value={cam}>
{t("cameraManagement.clone.target.newRadio")} {config?.cameras?.[cam]?.friendly_name ?? cam}
</Label> </SelectItem>
</div> ))}
{targetMode === "new" && ( </SelectContent>
<FormItem className="ml-7"> </Select>
<FormLabel className="sr-only"> </FormControl>
{t( <FormMessage />
"cameraManagement.clone.target.newNameLabel", </FormItem>
)} )}
</FormLabel> />
<FormControl> </div>
<Input
{...form.register("newName")} <div className="space-y-3">
className="max-w-xs" <Label className="text-base">
placeholder={t( {t("cameraManagement.clone.target.legend")}
"cameraManagement.clone.target.newNamePlaceholder", </Label>
<FormField
control={form.control}
name="targetMode"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="space-y-3"
>
<div className="space-y-2">
<div className="flex items-center space-x-3 space-y-0">
<RadioGroupItem
value="new"
id="clone-target-new"
className={
targetMode === "new"
? "bg-selected from-selected/50 to-selected/90 text-selected"
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
}
/>
<Label
htmlFor="clone-target-new"
className="font-normal"
>
{t("cameraManagement.clone.target.newRadio")}
</Label>
</div>
{targetMode === "new" && (
<FormItem>
<FormLabel className="sr-only">
{t(
"cameraManagement.clone.target.newNameLabel",
)} )}
disabled={isSubmitting} </FormLabel>
autoFocus <FormControl>
/> <Input
</FormControl> {...form.register("newName")}
{form.formState.errors.newName?.message && ( className="max-w-xs md:max-w-none"
<p className="text-sm font-medium text-destructive"> placeholder={t(
{String( "cameraManagement.clone.target.newNamePlaceholder",
form.formState.errors.newName.message, )}
disabled={isSubmitting}
autoFocus
/>
</FormControl>
{form.formState.errors.newName?.message && (
<p className="text-sm font-medium text-destructive">
{String(
form.formState.errors.newName.message,
)}
</p>
)}
<p className="text-xs text-muted-foreground">
{t(
"cameraManagement.clone.target.newStreamsForced",
)} )}
</p> </p>
)} </FormItem>
<p className="text-xs text-muted-foreground"> )}
{t(
"cameraManagement.clone.target.newStreamsForced",
)}
</p>
</FormItem>
)}
</div>
<div className="space-y-2">
<div className="flex items-center space-x-3 space-y-0">
<RadioGroupItem
value="existing"
id="clone-target-existing"
disabled={otherCameras.length === 0}
className={
targetMode === "existing"
? "bg-selected from-selected/50 to-selected/90 text-selected"
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
}
/>
<Label
htmlFor="clone-target-existing"
className={cn(
"font-normal",
otherCameras.length === 0 &&
"text-muted-foreground",
)}
>
{t(
"cameraManagement.clone.target.existingCamerasRadio",
)}
{otherCameras.length === 0 && (
<span className="ml-2 text-xs">
(
{t(
"cameraManagement.clone.target.existingDisabled",
)}
)
</span>
)}
</Label>
</div> </div>
{targetMode === "existing" && <div className="space-y-2">
otherCameras.length > 0 && ( <div className="flex items-center space-x-3 space-y-0">
<FormField <RadioGroupItem
control={form.control} value="existing"
name="existingTargets" id="clone-target-existing"
render={({ field: tgtField }) => { disabled={otherCameras.length === 0}
const selected = tgtField.value ?? []; className={
const allSelected = targetMode === "existing"
otherCameras.length > 0 && ? "bg-selected from-selected/50 to-selected/90 text-selected"
otherCameras.every((c) => : "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
selected.includes(c), }
); />
const selectedNames = otherCameras <Label
.filter((c) => selected.includes(c)) htmlFor="clone-target-existing"
.map( className={cn(
(c) => "font-normal",
config?.cameras?.[c]?.friendly_name ?? otherCameras.length === 0 &&
c, "text-muted-foreground",
); )}
const summary = allSelected >
? t( {t(
"cameraManagement.clone.target.allCameras", "cameraManagement.clone.target.existingCamerasRadio",
) )}
: selectedNames.length > 0 {otherCameras.length === 0 && (
? selectedNames.join(", ") <span className="ml-2 text-xs">
: t( (
"cameraManagement.clone.target.existingPlaceholder", {t(
); "cameraManagement.clone.target.existingDisabled",
return ( )}
<FormItem className="ml-7"> )
<Popover> </span>
<FormControl> )}
<PopoverTrigger asChild> </Label>
<Button </div>
type="button" {targetMode === "existing" &&
variant="outline" otherCameras.length > 0 && (
disabled={isSubmitting} <FormField
className="w-full max-w-xs justify-between font-normal" control={form.control}
> name="existingTargets"
<span render={({ field: tgtField }) => {
className={cn( const selected = tgtField.value ?? [];
"truncate", const allSelected =
selectedNames.length === 0 && otherCameras.length > 0 &&
"text-muted-foreground", otherCameras.every((c) =>
)} selected.includes(c),
);
const selectedNames = otherCameras
.filter((c) => selected.includes(c))
.map(
(c) =>
config?.cameras?.[c]?.friendly_name ??
c,
);
const summary = allSelected
? t(
"cameraManagement.clone.target.allCameras",
)
: selectedNames.length > 0
? selectedNames.join(", ")
: t(
"cameraManagement.clone.target.existingPlaceholder",
);
return (
<FormItem>
<Popover>
<FormControl>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
disabled={isSubmitting}
className="w-full max-w-xs justify-between font-normal md:max-w-none"
> >
{summary} <span
</span> className={cn(
<LuChevronDown className="ml-2 size-4 shrink-0 opacity-50" /> "truncate",
</Button> selectedNames.length ===
</PopoverTrigger> 0 &&
</FormControl> "text-muted-foreground",
<PopoverContent )}
align="start" >
disablePortal {summary}
className="w-[--radix-popover-trigger-width] p-0" </span>
> <LuChevronDown className="ml-2 size-4 shrink-0 opacity-50" />
<div className="scrollbar-container max-h-60 space-y-2.5 overflow-y-auto p-4"> </Button>
<FilterSwitch </PopoverTrigger>
label={t( </FormControl>
"cameraManagement.clone.target.allCameras", <PopoverContent
)} align="start"
isChecked={allSelected} disablePortal
disabled={isSubmitting} className="w-[--radix-popover-trigger-width] p-0"
onCheckedChange={(checked) => >
tgtField.onChange( <div className="scrollbar-container max-h-60 space-y-4 overflow-y-auto p-4">
checked
? [...otherCameras]
: [],
)
}
/>
<div className="border-t border-border/40" />
{otherCameras.map((cam) => (
<FilterSwitch <FilterSwitch
key={cam} label={t(
label={cam} "cameraManagement.clone.target.allCameras",
type="camera"
isChecked={selected.includes(
cam,
)} )}
isChecked={allSelected}
disabled={isSubmitting} disabled={isSubmitting}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
tgtField.onChange( tgtField.onChange(
checked checked
? [...selected, cam] ? [...otherCameras]
: selected.filter( : [],
(c) => c !== cam,
),
) )
} }
/> />
))} <div className="space-y-2.5">
</div> {otherCameras.map((cam) => (
</PopoverContent> <FilterSwitch
</Popover> key={cam}
<FormMessage /> label={cam}
</FormItem> type="camera"
); isChecked={selected.includes(
}} cam,
/> )}
)} disabled={isSubmitting}
</div> onCheckedChange={(
</RadioGroup> checked,
</FormControl> ) =>
</FormItem> tgtField.onChange(
)} checked
/> ? [...selected, cam]
: selected.filter(
(c) => c !== cam,
),
)
}
/>
))}
</div>
</div>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/>
)}
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="pointer-events-none absolute left-1/2 top-0 hidden -translate-x-1/2 flex-col space-y-3 md:flex">
<Label className="invisible text-base" aria-hidden="true">
{" "}
</Label>
<div className="flex h-10 items-center justify-center">
<LuArrowRight className="size-6 text-muted-foreground" />
</div>
</div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-row items-start justify-between gap-2"> <div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-base"> <Label className="text-base">
{t("cameraManagement.clone.categories.legend")} {t("cameraManagement.clone.categories.legend")}
@ -835,16 +850,16 @@ export default function CloneCameraDialog({
{t("cameraManagement.clone.categories.description")} {t("cameraManagement.clone.categories.description")}
</p> </p>
</div> </div>
<div className="flex flex-row items-center gap-2 text-right text-xs text-muted-foreground"> <div className="flex flex-row items-center gap-2 text-xs text-muted-foreground">
<span <span
className="cursor-pointer" className="cursor-pointer whitespace-nowrap"
onClick={isSubmitting ? undefined : selectAllCategories} onClick={isSubmitting ? undefined : selectAllCategories}
> >
{t("cameraManagement.clone.categories.selectAll")} {t("cameraManagement.clone.categories.selectAll")}
</span> </span>
<span aria-hidden="true">|</span> <span aria-hidden="true">|</span>
<span <span
className="cursor-pointer" className="cursor-pointer whitespace-nowrap"
onClick={isSubmitting ? undefined : selectNoneCategories} onClick={isSubmitting ? undefined : selectNoneCategories}
> >
{t("cameraManagement.clone.categories.selectNone")} {t("cameraManagement.clone.categories.selectNone")}
@ -856,7 +871,7 @@ export default function CloneCameraDialog({
<Label className="font-medium"> <Label className="font-medium">
{t("cameraManagement.clone.categories.general")} {t("cameraManagement.clone.categories.general")}
</Label> </Label>
<div className="grid grid-cols-1 gap-3 rounded-lg bg-secondary p-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-x-12 gap-y-3 rounded-lg bg-secondary p-4 sm:grid-cols-2">
{groupedCategories.general.map((cat) => ( {groupedCategories.general.map((cat) => (
<label <label
key={cat.key} key={cat.key}
@ -911,7 +926,7 @@ export default function CloneCameraDialog({
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<div className="grid grid-cols-1 gap-3 rounded-lg bg-secondary p-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-x-12 gap-y-3 rounded-lg bg-secondary p-4 sm:grid-cols-2">
{groupedCategories.spatial.map((cat) => ( {groupedCategories.spatial.map((cat) => (
<label <label
key={cat.key} key={cat.key}