mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 16:38:24 -06:00
Accessibility features (#14518)
* Add screen reader aria labels to buttons and menu items * Fix sub_label score in search detail dialog
This commit is contained in:
parent
c7d9f83638
commit
ad308252a1
@ -121,6 +121,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Login"
|
||||
>
|
||||
{isLoading && <ActivityIndicator className="mr-2 h-4 w-4" />}
|
||||
Login
|
||||
|
@ -46,6 +46,7 @@ export function DownloadVideoButton({
|
||||
disabled={isDownloading}
|
||||
className="flex items-center gap-2"
|
||||
size="sm"
|
||||
aria-label="Download Video"
|
||||
>
|
||||
<a
|
||||
href={source}
|
||||
|
@ -55,7 +55,12 @@ export default function DebugCameraImage({
|
||||
searchParams={searchParams}
|
||||
cameraClasses="relative w-full h-full flex justify-center"
|
||||
/>
|
||||
<Button onClick={handleToggleSettings} variant="link" size="sm">
|
||||
<Button
|
||||
onClick={handleToggleSettings}
|
||||
variant="link"
|
||||
size="sm"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<span className="h-5 w-5">
|
||||
<LuSettings />
|
||||
</span>{" "}
|
||||
|
@ -121,6 +121,7 @@ export function AnimatedEventCard({
|
||||
<Button
|
||||
className="absolute right-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
|
||||
size="xs"
|
||||
aria-label="Mark as Reviewed"
|
||||
onClick={async () => {
|
||||
await axios.post(`reviews/viewed`, { ids: [event.id] });
|
||||
updateEvents();
|
||||
|
@ -113,6 +113,7 @@ export default function ExportCard({
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
aria-label="Save Export"
|
||||
size="sm"
|
||||
variant="select"
|
||||
disabled={(editName?.update?.length ?? 0) == 0}
|
||||
@ -206,6 +207,7 @@ export default function ExportCard({
|
||||
{!exportedRecording.in_progress && (
|
||||
<Button
|
||||
className="absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white"
|
||||
aria-label="Play"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
onSelect(exportedRecording);
|
||||
|
@ -36,6 +36,7 @@ export default function NewReviewData({
|
||||
: "invisible",
|
||||
"mx-auto mt-5 bg-gray-400 text-center text-white",
|
||||
)}
|
||||
aria-label="View new review items"
|
||||
onClick={() => {
|
||||
pullLatestData();
|
||||
if (contentRef.current) {
|
||||
|
@ -34,6 +34,7 @@ export default function CalendarFilterButton({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Select a date to filter by"
|
||||
variant={day == undefined ? "default" : "select"}
|
||||
size="sm"
|
||||
>
|
||||
@ -57,6 +58,7 @@ export default function CalendarFilterButton({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-center p-2">
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
onClick={() => {
|
||||
updateSelectedDay(undefined);
|
||||
}}
|
||||
@ -99,6 +101,7 @@ export function CalendarRangeFilterButton({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Select a date to filter by"
|
||||
variant={range == undefined ? "default" : "select"}
|
||||
size="sm"
|
||||
>
|
||||
|
@ -141,6 +141,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
|
||||
: "bg-secondary text-secondary-foreground focus:bg-secondary focus:text-secondary-foreground"
|
||||
}
|
||||
aria-label="All Cameras"
|
||||
size="xs"
|
||||
onClick={() => (group ? setGroup("default", true) : null)}
|
||||
onMouseEnter={() => (isDesktop ? showTooltip("default") : null)}
|
||||
@ -165,6 +166,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
|
||||
: "bg-secondary text-secondary-foreground"
|
||||
}
|
||||
aria-label="Camera Group"
|
||||
size="xs"
|
||||
onClick={() => setGroup(name, group != "default")}
|
||||
onMouseEnter={() => (isDesktop ? showTooltip(name) : null)}
|
||||
@ -191,6 +193,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
|
||||
<Button
|
||||
className="bg-secondary text-muted-foreground"
|
||||
aria-label="Add camera group"
|
||||
size="xs"
|
||||
onClick={() => setAddGroup(true)}
|
||||
>
|
||||
@ -355,6 +358,7 @@ function NewGroupDialog({
|
||||
"size-6 rounded-md bg-secondary-foreground p-1 text-background",
|
||||
isMobile && "text-secondary-foreground",
|
||||
)}
|
||||
aria-label="Add camera group"
|
||||
onClick={() => {
|
||||
setEditState("add");
|
||||
}}
|
||||
@ -536,10 +540,16 @@ export function CameraGroupRow({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={onEditGroup}>
|
||||
<DropdownMenuItem
|
||||
aria-label="Edit group"
|
||||
onClick={onEditGroup}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)}>
|
||||
<DropdownMenuItem
|
||||
aria-label="Delete group"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@ -793,13 +803,19 @@ export function CameraGroupEdit({
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<div className="flex flex-row gap-2 py-5 md:pb-0">
|
||||
<Button type="button" className="flex flex-1" onClick={onCancel}>
|
||||
<Button
|
||||
type="button"
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
@ -55,6 +55,7 @@ export function CamerasFilterButton({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Cameras Filter"
|
||||
variant={selectedCameras?.length == undefined ? "default" : "select"}
|
||||
size="sm"
|
||||
>
|
||||
@ -202,6 +203,7 @@ export function CamerasFilterContent({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
aria-label="Apply"
|
||||
variant="select"
|
||||
disabled={currentCameras?.length === 0}
|
||||
onClick={() => {
|
||||
@ -212,6 +214,7 @@ export function CamerasFilterContent({
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
onClick={() => {
|
||||
setCurrentCameras(undefined);
|
||||
updateCameraFilter(undefined);
|
||||
|
@ -17,7 +17,11 @@ export function LogLevelFilterButton({
|
||||
updateLabelFilter,
|
||||
}: LogLevelFilterButtonProps) {
|
||||
const trigger = (
|
||||
<Button size="sm" className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Filter log level"
|
||||
>
|
||||
<FaFilter className="text-secondary-foreground" />
|
||||
<div className="hidden text-primary md:block">Filter</div>
|
||||
</Button>
|
||||
|
@ -104,6 +104,7 @@ export default function ReviewActionGroup({
|
||||
{selectedReviews.length == 1 && (
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Export"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onExport(selectedReviews[0]);
|
||||
@ -116,6 +117,7 @@ export default function ReviewActionGroup({
|
||||
)}
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Mark as reviewed"
|
||||
size="sm"
|
||||
onClick={onMarkAsReviewed}
|
||||
>
|
||||
@ -124,6 +126,7 @@ export default function ReviewActionGroup({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Delete"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
|
@ -278,6 +278,7 @@ function ShowReviewFilter({
|
||||
|
||||
<Button
|
||||
className="block duration-0 md:hidden"
|
||||
aria-label="Show reviewed"
|
||||
variant={showReviewedSwitch ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
@ -338,6 +339,7 @@ function GeneralFilterButton({
|
||||
selectedLabels?.length || selectedZones?.length ? "select" : "default"
|
||||
}
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Filter"
|
||||
>
|
||||
<FaFilter
|
||||
className={`${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
@ -538,6 +540,7 @@ export function GeneralFilterContent({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
aria-label="Apply"
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
if (selectedLabels != currentLabels) {
|
||||
@ -554,6 +557,7 @@ export function GeneralFilterContent({
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
onClick={() => {
|
||||
setCurrentLabels(undefined);
|
||||
setCurrentZones?.(undefined);
|
||||
@ -601,6 +605,7 @@ function ShowMotionOnlyButton({
|
||||
<Button
|
||||
size="sm"
|
||||
className="duration-0"
|
||||
aria-label="Show Motion Only"
|
||||
variant={motionOnlyButton ? "select" : "default"}
|
||||
onClick={() => setMotionOnlyButton(!motionOnlyButton)}
|
||||
>
|
||||
|
@ -227,6 +227,7 @@ function GeneralFilterButton({
|
||||
size="sm"
|
||||
variant={selectedLabels?.length ? "select" : "default"}
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Labels"
|
||||
>
|
||||
<MdLabel
|
||||
className={`${selectedLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
@ -336,6 +337,7 @@ export function GeneralFilterContent({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
aria-label="Apply"
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
if (selectedLabels != currentLabels) {
|
||||
@ -348,6 +350,7 @@ export function GeneralFilterContent({
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
onClick={() => {
|
||||
setCurrentLabels(undefined);
|
||||
updateLabelFilter(undefined);
|
||||
|
@ -21,6 +21,7 @@ export function ZoneMaskFilterButton({
|
||||
size="sm"
|
||||
variant={selectedZoneMask?.length ? "select" : "default"}
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Filter by zone mask"
|
||||
>
|
||||
<FaFilter
|
||||
className={`${selectedZoneMask?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
|
@ -66,7 +66,10 @@ export default function IconPicker({
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
{!selectedIcon?.name || !selectedIcon?.Icon ? (
|
||||
<Button className="mt-2 w-full text-muted-foreground">
|
||||
<Button
|
||||
className="mt-2 w-full text-muted-foreground"
|
||||
aria-label="Select an icon"
|
||||
>
|
||||
Select an icon
|
||||
</Button>
|
||||
) : (
|
||||
|
@ -59,11 +59,14 @@ export function SaveSearchDialog({
|
||||
placeholder="Enter a name for your search"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button aria-label="Cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="select"
|
||||
className="mb-2 md:mb-0"
|
||||
aria-label="Save this search"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
@ -72,6 +72,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Log out"
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
|
@ -176,6 +176,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Log out"
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
@ -194,6 +195,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="System metrics"
|
||||
>
|
||||
<LuActivity className="mr-2 size-4" />
|
||||
<span>System metrics</span>
|
||||
@ -206,6 +208,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="System logs"
|
||||
>
|
||||
<LuList className="mr-2 size-4" />
|
||||
<span>System logs</span>
|
||||
@ -224,6 +227,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Settings"
|
||||
>
|
||||
<LuSettings className="mr-2 size-4" />
|
||||
<span>Settings</span>
|
||||
@ -236,6 +240,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Configuration editor"
|
||||
>
|
||||
<LuPenSquare className="mr-2 size-4" />
|
||||
<span>Configuration editor</span>
|
||||
@ -269,6 +274,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Light mode"
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
@ -286,6 +292,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Dark mode"
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
@ -303,6 +310,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Use the system settings for light or dark mode"
|
||||
onClick={() => setTheme("system")}
|
||||
>
|
||||
{theme === "system" ? (
|
||||
@ -343,6 +351,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label={`Color scheme - ${scheme}`}
|
||||
onClick={() => setColorScheme(scheme)}
|
||||
>
|
||||
{scheme === colorScheme ? (
|
||||
@ -370,6 +379,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Frigate documentation"
|
||||
>
|
||||
<LuLifeBuoy className="mr-2 size-4" />
|
||||
<span>Documentation</span>
|
||||
@ -383,6 +393,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Frigate Github"
|
||||
>
|
||||
<LuGithub className="mr-2 size-4" />
|
||||
<span>GitHub</span>
|
||||
@ -393,6 +404,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Restart Frigate"
|
||||
onClick={() => setRestartDialogOpen(true)}
|
||||
>
|
||||
<LuRotateCw className="mr-2 size-4" />
|
||||
@ -446,7 +458,12 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
<p>This page will reload in {countdown} seconds.</p>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Button size="lg" className="mt-5" onClick={handleForceReload}>
|
||||
<Button
|
||||
size="lg"
|
||||
className="mt-5"
|
||||
aria-label="Force reload now"
|
||||
onClick={handleForceReload}
|
||||
>
|
||||
Force Reload Now
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -86,7 +86,7 @@ export default function SearchResultActions({
|
||||
const menuItems = (
|
||||
<>
|
||||
{searchResult.has_clip && (
|
||||
<MenuItem>
|
||||
<MenuItem aria-label="Download video">
|
||||
<a
|
||||
className="flex items-center"
|
||||
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
|
||||
@ -98,7 +98,7 @@ export default function SearchResultActions({
|
||||
</MenuItem>
|
||||
)}
|
||||
{searchResult.has_snapshot && (
|
||||
<MenuItem>
|
||||
<MenuItem aria-label="Download snapshot">
|
||||
<a
|
||||
className="flex items-center"
|
||||
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
|
||||
@ -109,12 +109,18 @@ export default function SearchResultActions({
|
||||
</a>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={showObjectLifecycle}>
|
||||
<MenuItem
|
||||
aria-label="Show the object lifecycle"
|
||||
onClick={showObjectLifecycle}
|
||||
>
|
||||
<FaArrowsRotate className="mr-2 size-4" />
|
||||
<span>View object lifecycle</span>
|
||||
</MenuItem>
|
||||
{config?.semantic_search?.enabled && isContextMenu && (
|
||||
<MenuItem onClick={findSimilar}>
|
||||
<MenuItem
|
||||
aria-label="Find similar tracked objects"
|
||||
onClick={findSimilar}
|
||||
>
|
||||
<MdImageSearch className="mr-2 size-4" />
|
||||
<span>Find similar</span>
|
||||
</MenuItem>
|
||||
@ -124,12 +130,18 @@ export default function SearchResultActions({
|
||||
searchResult.has_snapshot &&
|
||||
searchResult.end_time &&
|
||||
!searchResult.plus_id && (
|
||||
<MenuItem onClick={() => setShowFrigatePlus(true)}>
|
||||
<MenuItem
|
||||
aria-label="Submit to Frigate Plus"
|
||||
onClick={() => setShowFrigatePlus(true)}
|
||||
>
|
||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||
<span>Submit to Frigate+</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => setDeleteDialogOpen(true)}>
|
||||
<MenuItem
|
||||
aria-label="Delete this tracked object"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<LuTrash2 className="mr-2 size-4" />
|
||||
<span>Delete</span>
|
||||
</MenuItem>
|
||||
|
@ -154,6 +154,7 @@ export function MobilePageHeader({
|
||||
>
|
||||
<Button
|
||||
className="absolute left-0 rounded-lg"
|
||||
aria-label="Go back"
|
||||
size="sm"
|
||||
onClick={handleClose}
|
||||
>
|
||||
|
@ -167,7 +167,11 @@ export default function CameraInfoDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="select" onClick={() => onCopyFfprobe()}>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Copy"
|
||||
onClick={() => onCopyFfprobe()}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
@ -98,7 +98,11 @@ export default function CreateUserDialog({
|
||||
)}
|
||||
/>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="select" disabled={isLoading}>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Create user"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <ActivityIndicator className="mr-2 h-4 w-4" />}
|
||||
Create User
|
||||
</Button>
|
||||
|
@ -27,6 +27,7 @@ export default function DeleteUserDialog({
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Confirm delete"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
|
@ -142,6 +142,7 @@ export default function ExportDialog({
|
||||
<Trigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Export"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const now = new Date(latestTime * 1000);
|
||||
@ -307,6 +308,7 @@ export function ExportContent({
|
||||
</div>
|
||||
<Button
|
||||
className={isDesktop ? "" : "w-full"}
|
||||
aria-label="Select or export"
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@ -420,6 +422,7 @@ function CustomTimeSelector({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||
aria-label="Start time"
|
||||
variant={startOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@ -485,6 +488,7 @@ function CustomTimeSelector({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||
aria-label="End time"
|
||||
variant={endOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
|
@ -59,8 +59,17 @@ export default function GPUInfoDialog({
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setShowGpuInfo(false)}>Close</Button>
|
||||
<Button variant="select" onClick={() => onCopyInfo()}>
|
||||
<Button
|
||||
aria-label="Close GPU info"
|
||||
onClick={() => setShowGpuInfo(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Copy GPU info"
|
||||
variant="select"
|
||||
onClick={() => onCopyInfo()}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@ -88,8 +97,17 @@ export default function GPUInfoDialog({
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setShowGpuInfo(false)}>Close</Button>
|
||||
<Button variant="select" onClick={() => onCopyInfo()}>
|
||||
<Button
|
||||
aria-label="Close GPU info"
|
||||
onClick={() => setShowGpuInfo(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Copy GPU info"
|
||||
variant="select"
|
||||
onClick={() => onCopyInfo()}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
@ -23,7 +23,11 @@ export default function MobileCameraDrawer({
|
||||
return (
|
||||
<Drawer open={cameraDrawer} onOpenChange={setCameraDrawer}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button className="rounded-lg capitalize" size="sm">
|
||||
<Button
|
||||
className="rounded-lg capitalize"
|
||||
aria-label="Cameras"
|
||||
size="sm"
|
||||
>
|
||||
<FaVideo className="text-secondary-foreground" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
|
@ -132,6 +132,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{features.includes("export") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label="Export"
|
||||
onClick={() => {
|
||||
setDrawerMode("export");
|
||||
setMode("select");
|
||||
@ -144,6 +145,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{features.includes("calendar") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label="Calendar"
|
||||
variant={filter?.after ? "select" : "default"}
|
||||
onClick={() => setDrawerMode("calendar")}
|
||||
>
|
||||
@ -156,6 +158,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{features.includes("filter") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label="Filter"
|
||||
variant={filter?.labels || filter?.zones ? "select" : "default"}
|
||||
onClick={() => setDrawerMode("filter")}
|
||||
>
|
||||
@ -226,6 +229,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
<SelectSeparator />
|
||||
<div className="flex items-center justify-center p-2">
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
onClick={() => {
|
||||
onUpdateFilter({
|
||||
...filter,
|
||||
@ -306,6 +310,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
className="rounded-lg capitalize"
|
||||
aria-label="Filters"
|
||||
variant={
|
||||
filter?.labels || filter?.after || filter?.zones
|
||||
? "select"
|
||||
|
@ -22,7 +22,11 @@ export default function MobileTimelineDrawer({
|
||||
return (
|
||||
<Drawer open={drawer} onOpenChange={setDrawer}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button className="rounded-lg capitalize" size="sm">
|
||||
<Button
|
||||
className="rounded-lg capitalize"
|
||||
aria-label="Select timeline or events list"
|
||||
size="sm"
|
||||
>
|
||||
<FaFlag className="text-secondary-foreground" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
|
@ -28,6 +28,7 @@ export default function SaveExportOverlay({
|
||||
>
|
||||
<Button
|
||||
className="flex items-center gap-1 text-primary"
|
||||
aria-label="Cancel"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
>
|
||||
@ -36,6 +37,7 @@ export default function SaveExportOverlay({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Preview export"
|
||||
size="sm"
|
||||
onClick={onPreview}
|
||||
>
|
||||
@ -44,6 +46,7 @@ export default function SaveExportOverlay({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Save export"
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={onSave}
|
||||
|
@ -36,6 +36,7 @@ export default function SetPasswordDialog({
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Save Password"
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
|
@ -207,12 +207,14 @@ export function AnnotationSettingsPane({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Apply"
|
||||
onClick={form.handleSubmit(onApply)}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
type="submit"
|
||||
|
@ -242,6 +242,7 @@ export default function ObjectLifecycle({
|
||||
<div className={cn("flex items-center gap-2")}>
|
||||
<Button
|
||||
className="mb-2 mt-3 flex items-center gap-2.5 rounded-lg md:mt-0"
|
||||
aria-label="Go back"
|
||||
size="sm"
|
||||
onClick={() => setPane("overview")}
|
||||
>
|
||||
@ -346,6 +347,7 @@ export default function ObjectLifecycle({
|
||||
<Button
|
||||
variant={showControls ? "select" : "default"}
|
||||
className="size-7 p-1.5"
|
||||
aria-label="Adjust annotation settings"
|
||||
>
|
||||
<LuSettings
|
||||
className="size-5"
|
||||
|
@ -153,6 +153,7 @@ export default function ReviewDetailDialog({
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
aria-label="Share this review item"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
shareOrCopy(`${baseUrl}review?id=${review.id}`)
|
||||
|
@ -296,7 +296,7 @@ function ObjectDetailsTab({
|
||||
}
|
||||
|
||||
if (search.sub_label) {
|
||||
return Math.round((search.data?.top_score ?? 0) * 100);
|
||||
return Math.round((search.data?.sub_label_score ?? 0) * 100);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@ -440,6 +440,7 @@ function ObjectDetailsTab({
|
||||
/>
|
||||
{config?.semantic_search.enabled && (
|
||||
<Button
|
||||
aria-label="Find similar tracked objects"
|
||||
onClick={() => {
|
||||
setSearch(undefined);
|
||||
|
||||
@ -466,6 +467,7 @@ function ObjectDetailsTab({
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="rounded-r-none border-r-0"
|
||||
aria-label="Regenerate tracked object description"
|
||||
onClick={() => regenerateDescription("thumbnails")}
|
||||
>
|
||||
Regenerate
|
||||
@ -473,19 +475,24 @@ function ObjectDetailsTab({
|
||||
{search.has_snapshot && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="rounded-l-none border-l-0 px-2">
|
||||
<Button
|
||||
className="rounded-l-none border-l-0 px-2"
|
||||
aria-label="Expand regeneration menu"
|
||||
>
|
||||
<FaChevronDown className="size-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
aria-label="Regenerate from snapshot"
|
||||
onClick={() => regenerateDescription("snapshot")}
|
||||
>
|
||||
Regenerate from Snapshot
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
aria-label="Regenerate from thumbnails"
|
||||
onClick={() => regenerateDescription("thumbnails")}
|
||||
>
|
||||
Regenerate from Thumbnails
|
||||
@ -495,7 +502,11 @@ function ObjectDetailsTab({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button variant="select" onClick={updateDescription}>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
onClick={updateDescription}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
@ -601,6 +612,7 @@ function ObjectSnapshotTab({
|
||||
<>
|
||||
<Button
|
||||
className="bg-success"
|
||||
aria-label="Confirm this label for Frigate Plus"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(false);
|
||||
@ -610,6 +622,7 @@ function ObjectSnapshotTab({
|
||||
</Button>
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Do not confirm this label for Frigate Plus"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
|
@ -131,9 +131,14 @@ export function FrigatePlusDialog({
|
||||
<DialogFooter className="flex flex-row justify-end gap-2">
|
||||
{state == "reviewing" && (
|
||||
<>
|
||||
{dialog && <Button onClick={onClose}>Cancel</Button>}
|
||||
{dialog && (
|
||||
<Button aria-label="Cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="bg-success"
|
||||
aria-label="Confirm this label for Frigate Plus"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(false);
|
||||
@ -143,6 +148,7 @@ export function FrigatePlusDialog({
|
||||
</Button>
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Do not confirm this label for Frigate Plus"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
|
@ -76,6 +76,7 @@ export default function SearchFilterDialog({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="More Filters"
|
||||
size="sm"
|
||||
variant={moreFiltersSelected ? "select" : "default"}
|
||||
>
|
||||
@ -141,6 +142,7 @@ export default function SearchFilterDialog({
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Apply"
|
||||
onClick={() => {
|
||||
if (currentFilter != filter) {
|
||||
onUpdateFilter(currentFilter);
|
||||
@ -152,6 +154,7 @@ export default function SearchFilterDialog({
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset filters to default values"
|
||||
onClick={() => {
|
||||
setCurrentFilter((prevFilter) => ({
|
||||
...prevFilter,
|
||||
@ -256,6 +259,7 @@ function TimeRangeFilterContent({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"} `}
|
||||
aria-label="Select Start Time"
|
||||
variant={startOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@ -293,6 +297,7 @@ function TimeRangeFilterContent({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||
aria-label="Select End Time"
|
||||
variant={endOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
|
@ -308,11 +308,16 @@ export default function MotionMaskEditPane({
|
||||
/>
|
||||
<div className="flex flex-1 flex-col justify-end">
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button className="flex flex-1" onClick={onCancel}>
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
type="submit"
|
||||
|
@ -335,13 +335,18 @@ export default function ObjectMaskEditPane({
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-end">
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button className="flex flex-1" onClick={onCancel}>
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
@ -74,6 +74,7 @@ export default function PolygonEditControls({
|
||||
<Button
|
||||
variant="default"
|
||||
className="size-6 rounded-md p-1"
|
||||
aria-label="Remove last point"
|
||||
disabled={!polygons[activePolygonIndex].points.length}
|
||||
onClick={undo}
|
||||
>
|
||||
@ -87,6 +88,7 @@ export default function PolygonEditControls({
|
||||
<Button
|
||||
variant="default"
|
||||
className="size-6 rounded-md p-1"
|
||||
aria-label="Clear all points"
|
||||
disabled={!polygons[activePolygonIndex].points.length}
|
||||
onClick={reset}
|
||||
>
|
||||
|
@ -276,6 +276,7 @@ export default function PolygonItem({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
aria-label="Edit"
|
||||
onClick={() => {
|
||||
setActivePolygonIndex(index);
|
||||
setEditPane(polygon.type);
|
||||
@ -283,10 +284,14 @@ export default function PolygonItem({
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleCopyCoordinates(index)}>
|
||||
<DropdownMenuItem
|
||||
aria-label="Copy"
|
||||
onClick={() => handleCopyCoordinates(index)}
|
||||
>
|
||||
Copy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
aria-label="Delete"
|
||||
disabled={isLoading}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
|
@ -44,7 +44,11 @@ export default function SearchSettings({
|
||||
]);
|
||||
|
||||
const trigger = (
|
||||
<Button className="flex items-center gap-2" size="sm">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Search Settings"
|
||||
size="sm"
|
||||
>
|
||||
<FaCog className="text-secondary-foreground" />
|
||||
Settings
|
||||
</Button>
|
||||
|
@ -466,13 +466,18 @@ export default function ZoneEditPane({
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button className="flex flex-1" onClick={onCancel}>
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
@ -283,6 +283,7 @@ export function DateRangePicker({
|
||||
}): JSX.Element => (
|
||||
<Button
|
||||
className={cn(isSelected && "pointer-events-none text-primary")}
|
||||
aria-label={label}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setPreset(preset);
|
||||
@ -417,6 +418,7 @@ export function DateRangePicker({
|
||||
<div className="mx-auto flex w-64 items-center justify-evenly gap-2 py-2">
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Apply"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
if (
|
||||
@ -436,6 +438,7 @@ export function DateRangePicker({
|
||||
onReset?.();
|
||||
}}
|
||||
variant="ghost"
|
||||
aria-label="Reset"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
@ -1,43 +1,43 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
@ -54,69 +54,69 @@ const Carousel = React.forwardRef<
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
@ -143,16 +143,16 @@ const Carousel = React.forwardRef<
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
Carousel.displayName = "Carousel"
|
||||
);
|
||||
},
|
||||
);
|
||||
Carousel.displayName = "Carousel";
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
@ -161,20 +161,20 @@ const CarouselContent = React.forwardRef<
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = "CarouselContent";
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -184,19 +184,19 @@ const CarouselItem = React.forwardRef<
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = "CarouselItem";
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -204,12 +204,13 @@ const CarouselPrevious = React.forwardRef<
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
aria-label="Previous slide"
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
@ -217,15 +218,15 @@ const CarouselPrevious = React.forwardRef<
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = "CarouselPrevious";
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -237,8 +238,9 @@ const CarouselNext = React.forwardRef<
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
aria-label="Next slide"
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
@ -246,9 +248,9 @@ const CarouselNext = React.forwardRef<
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = "CarouselNext";
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
@ -257,4 +259,4 @@ export {
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
};
|
||||
|
@ -192,6 +192,7 @@ function ConfigEditor() {
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Copy config"
|
||||
onClick={() => handleCopyConfig()}
|
||||
>
|
||||
<LuCopy className="text-secondary-foreground" />
|
||||
@ -200,6 +201,7 @@ function ConfigEditor() {
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Save and restart"
|
||||
onClick={() => onHandleSaveConfig("restart")}
|
||||
>
|
||||
<div className="relative size-5">
|
||||
@ -211,6 +213,7 @@ function ConfigEditor() {
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Save only without restarting"
|
||||
onClick={() => onHandleSaveConfig("saveonly")}
|
||||
>
|
||||
<LuSave className="text-secondary-foreground" />
|
||||
|
@ -125,6 +125,7 @@ function Exports() {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Delete Export"
|
||||
variant="destructive"
|
||||
onClick={() => onHandleDelete()}
|
||||
>
|
||||
|
@ -339,6 +339,7 @@ function Logs() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center justify-between gap-2"
|
||||
aria-label="Copy logs to clipboard"
|
||||
size="sm"
|
||||
onClick={handleCopyLogs}
|
||||
>
|
||||
@ -349,6 +350,7 @@ function Logs() {
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center justify-between gap-2"
|
||||
aria-label="Download logs"
|
||||
size="sm"
|
||||
onClick={handleDownloadLogs}
|
||||
>
|
||||
@ -365,6 +367,7 @@ function Logs() {
|
||||
{initialScroll && !endVisible && (
|
||||
<Button
|
||||
className="absolute bottom-8 left-[50%] z-20 flex -translate-x-[50%] items-center gap-1 rounded-md p-2"
|
||||
aria-label="Jump to bottom of logs"
|
||||
onClick={() =>
|
||||
contentRef.current?.scrollTo({
|
||||
top: contentRef.current?.scrollHeight,
|
||||
|
@ -252,6 +252,7 @@ function CameraSelectButton({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2 bg-selected capitalize hover:bg-selected"
|
||||
aria-label="Select a camera"
|
||||
size="sm"
|
||||
>
|
||||
<FaVideo className="text-background dark:text-primary" />
|
||||
|
@ -737,6 +737,7 @@ function DetectionReview({
|
||||
<div className="col-span-full flex items-center justify-center">
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Mark these items as reviewed"
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
setSelectedReviews([]);
|
||||
|
@ -144,6 +144,7 @@ export default function LiveBirdseyeView({
|
||||
{!fullscreen ? (
|
||||
<Button
|
||||
className={`flex items-center gap-2 rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
|
||||
aria-label="Go Back"
|
||||
size={isMobile ? "icon" : "sm"}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
|
@ -352,6 +352,7 @@ export default function LiveCameraView({
|
||||
>
|
||||
<Button
|
||||
className={`flex items-center gap-2.5 rounded-lg`}
|
||||
aria-label="Go back"
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
@ -360,6 +361,7 @@ export default function LiveCameraView({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
aria-label="Show historical footage"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate("review", {
|
||||
@ -388,6 +390,7 @@ export default function LiveCameraView({
|
||||
{fullscreen && (
|
||||
<Button
|
||||
className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary"
|
||||
aria-label="Go back"
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
@ -603,6 +606,7 @@ function PtzControlPanel({
|
||||
{ptz?.features?.includes("pt") && (
|
||||
<>
|
||||
<Button
|
||||
aria-label="Move PTZ camera to the left"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_LEFT");
|
||||
@ -617,6 +621,7 @@ function PtzControlPanel({
|
||||
<FaAngleLeft />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Move PTZ camera up"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_UP");
|
||||
@ -631,6 +636,7 @@ function PtzControlPanel({
|
||||
<FaAngleUp />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Move PTZ camera down"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_DOWN");
|
||||
@ -645,6 +651,7 @@ function PtzControlPanel({
|
||||
<FaAngleDown />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Move PTZ camera to the right"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("MOVE_RIGHT");
|
||||
@ -663,6 +670,7 @@ function PtzControlPanel({
|
||||
{ptz?.features?.includes("zoom") && (
|
||||
<>
|
||||
<Button
|
||||
aria-label="Zoom PTZ camera in"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("ZOOM_IN");
|
||||
@ -677,6 +685,7 @@ function PtzControlPanel({
|
||||
<MdZoomIn />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Zoom PTZ camera out"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
sendPtz("ZOOM_OUT");
|
||||
@ -696,6 +705,7 @@ function PtzControlPanel({
|
||||
<>
|
||||
<Button
|
||||
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
|
||||
aria-label="Click in the frame to center the PTZ camera"
|
||||
onClick={() => setClickOverlay(!clickOverlay)}
|
||||
>
|
||||
<TbViewfinder />
|
||||
@ -705,7 +715,7 @@ function PtzControlPanel({
|
||||
{(ptz?.presets?.length ?? 0) > 0 && (
|
||||
<DropdownMenu modal={!isDesktop}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<Button aria-label="PTZ camera presets">
|
||||
<BsThreeDotsVertical />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@ -717,6 +727,7 @@ function PtzControlPanel({
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={preset}
|
||||
aria-label={preset}
|
||||
className="cursor-pointer"
|
||||
onSelect={() => sendPtz(`preset_${preset}`)}
|
||||
>
|
||||
|
@ -240,6 +240,7 @@ export default function LiveDashboardView({
|
||||
? "bg-blue-900 bg-opacity-60 focus:bg-blue-900 focus:bg-opacity-60"
|
||||
: "bg-secondary"
|
||||
}`}
|
||||
aria-label="Use mobile grid layout"
|
||||
size="xs"
|
||||
onClick={() => setMobileLayout("grid")}
|
||||
>
|
||||
@ -251,6 +252,7 @@ export default function LiveDashboardView({
|
||||
? "bg-blue-900 bg-opacity-60 focus:bg-blue-900 focus:bg-opacity-60"
|
||||
: "bg-secondary"
|
||||
}`}
|
||||
aria-label="Use mobile list layout"
|
||||
size="xs"
|
||||
onClick={() => setMobileLayout("list")}
|
||||
>
|
||||
@ -267,6 +269,7 @@ export default function LiveDashboardView({
|
||||
? "bg-selected text-primary"
|
||||
: "bg-secondary text-secondary-foreground",
|
||||
)}
|
||||
aria-label="Enter layout editing mode"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
setIsEditMode((prevIsEditMode) => !prevIsEditMode)
|
||||
|
@ -380,6 +380,7 @@ export function RecordingView({
|
||||
<div className={cn("flex items-center gap-2")}>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
aria-label="Go back"
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
@ -388,6 +389,7 @@ export function RecordingView({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
aria-label="Go to the main camera live view"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate(`/#${mainCamera}`);
|
||||
|
@ -95,6 +95,7 @@ export default function AuthenticationView() {
|
||||
</Heading>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Add a new user"
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setShowCreate(true);
|
||||
@ -114,6 +115,7 @@ export default function AuthenticationView() {
|
||||
<div className="flex flex-1 justify-end space-x-2">
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Update the user's password"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowSetPassword(true);
|
||||
@ -125,6 +127,7 @@ export default function AuthenticationView() {
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Delete the user"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
|
@ -475,6 +475,7 @@ export default function CameraSettingsView({
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
@ -484,6 +485,7 @@ export default function CameraSettingsView({
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
@ -459,6 +459,7 @@ export default function MasksAndZonesView({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
|
||||
aria-label="Add a new zone"
|
||||
onClick={() => {
|
||||
setEditPane("zone");
|
||||
handleNewPolygon("zone");
|
||||
@ -527,6 +528,7 @@ export default function MasksAndZonesView({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
|
||||
aria-label="Add a new motion mask"
|
||||
onClick={() => {
|
||||
setEditPane("motion_mask");
|
||||
handleNewPolygon("motion_mask");
|
||||
@ -596,6 +598,7 @@ export default function MasksAndZonesView({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
|
||||
aria-label="Add a new object mask"
|
||||
onClick={() => {
|
||||
setEditPane("object_mask");
|
||||
handleNewPolygon("object_mask");
|
||||
|
@ -284,13 +284,18 @@ export default function MotionTunerView({
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-end">
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button className="flex flex-1" onClick={onCancel}>
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Reset"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
disabled={!changedValue || isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
onClick={saveToConfig}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
@ -270,6 +270,7 @@ export default function NotificationView({
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
@ -279,6 +280,7 @@ export default function NotificationView({
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
@ -298,6 +300,7 @@ export default function NotificationView({
|
||||
<div className="space-y-3">
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<Button
|
||||
aria-label="Register or unregister notifications for this device"
|
||||
disabled={
|
||||
!config?.notifications.enabled || publicKey == undefined
|
||||
}
|
||||
|
@ -266,13 +266,14 @@ export default function SearchSettingsView({
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
|
||||
<Button className="flex flex-1" onClick={onCancel}>
|
||||
<Button className="flex flex-1" aria-label="Reset" onClick={onCancel}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
disabled={!changedValue || isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
onClick={saveToConfig}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
@ -125,7 +125,12 @@ export default function UiSettingsView() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={clearStoredLayouts}>Clear All Layouts</Button>
|
||||
<Button
|
||||
aria-label="Clear all saved layouts"
|
||||
onClick={clearStoredLayouts}
|
||||
>
|
||||
Clear All Layouts
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
@ -541,6 +541,7 @@ export default function GeneralMetrics({
|
||||
{canGetGpuInfo && (
|
||||
<Button
|
||||
className="cursor-pointer"
|
||||
aria-label="Hardware information"
|
||||
size="sm"
|
||||
onClick={() => setShowVainfo(true)}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user