add eslint

This commit is contained in:
Eduard Gert
2026-06-18 13:56:41 +02:00
parent 3175b880e4
commit cc842f17d5
61 changed files with 2631 additions and 364 deletions
+3 -1
View File
@@ -6,5 +6,7 @@
"trailingComma": "all", "trailingComma": "all",
"printWidth": 100, "printWidth": 100,
"arrowParens": "always", "arrowParens": "always",
"endOfLine": "lf" "endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["cn", "clsx", "cva", "tw"]
} }
+75
View File
@@ -0,0 +1,75 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import jsxA11y from "eslint-plugin-jsx-a11y";
import globals from "globals";
export default tseslint.config(
{
ignores: ["dist/**", "node_modules/**", "bindings/**", "sonar/**"],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["src/**/*.{ts,tsx}"],
plugins: {
react,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
"jsx-a11y": jsxA11y,
},
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: { ...globals.browser },
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
settings: {
react: { version: "detect" },
},
rules: {
// ----- a11y / semantic HTML (jsx-a11y recommended) -----
...jsxA11y.configs.recommended.rules,
"jsx-a11y/no-autofocus": ["warn", { ignoreNonDOM: true }],
// ----- React -----
...react.configs.recommended.rules,
...react.configs["jsx-runtime"].rules,
"react/prop-types": "off",
"react/jsx-no-target-blank": ["error", { allowReferrer: true }],
"react/self-closing-comp": "warn",
// ----- React hooks -----
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
// ----- Vite / HMR (Fast Refresh) -----
"react-refresh/only-export-components": "off",
// ----- TypeScript -----
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/consistent-type-imports": [
"warn",
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
],
"@typescript-eslint/no-explicit-any": "warn",
// ----- General correctness -----
eqeqeq: ["error", "smart"],
"no-console": ["warn", { allow: ["warn", "error", "info"] }],
"no-debugger": "error",
"prefer-const": "warn",
},
},
);
+13 -1
View File
@@ -11,7 +11,10 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"bindings": "cd .. && wails3 generate bindings -clean=true -ts", "bindings": "cd .. && wails3 generate bindings -clean=true -ts",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"", "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"" "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
"lint:report": "eslint \"src/**/*.{ts,tsx}\" --format json --output-file eslint-report.json || true"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
@@ -40,16 +43,25 @@
"tailwind-merge": "^2.6.0" "tailwind-merge": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.39.4",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.6.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.8.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.61.1",
"vite": "^6.0.7" "vite": "^6.0.7"
}, },
"packageManager": "pnpm@11.4.0+sha512.f0febc7e37552ab485494a914241b338e0b3580b93d54ce31f00933015880863129038a1b4ae4e414a0ee63ac35bf21197e990172c4a68256450b5636310968f" "packageManager": "pnpm@11.4.0+sha512.f0febc7e37552ab485494a914241b338e0b3580b93d54ce31f00933015880863129038a1b4ae4e414a0ee63ac35bf21197e990172c4a68256450b5636310968f"
+2244
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -35,23 +35,23 @@ Promise.all([
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}> <SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
<HashRouter> <HashRouter>
<Routes> <Routes>
<Route path="dialog"> <Route path={"dialog"}>
<Route <Route
path="browser-login" path={"browser-login"}
element={<LoginWaitingForBrowserDialog />} element={<LoginWaitingForBrowserDialog />}
/> />
<Route path="install-progress" element={<UpdateInProgressDialog />} /> <Route path={"install-progress"} element={<UpdateInProgressDialog />} />
<Route <Route
path="session-expiration" path={"session-expiration"}
element={<SessionExpirationDialog />} element={<SessionExpirationDialog />}
/> />
<Route path="welcome" element={<WelcomeDialog />} /> <Route path={"welcome"} element={<WelcomeDialog />} />
<Route path="error" element={<ErrorDialog />} /> <Route path={"error"} element={<ErrorDialog />} />
</Route> </Route>
<Route element={<AppLayout />}> <Route element={<AppLayout />}>
<Route index element={<MainPage />} /> <Route index element={<MainPage />} />
<Route path="settings" element={<SettingsPage />} /> <Route path={"settings"} element={<SettingsPage />} />
<Route path="*" element={<Navigate to={"/"} replace />} /> <Route path={"*"} element={<Navigate to={"/"} replace />} />
</Route> </Route>
</Routes> </Routes>
</HashRouter> </HashRouter>
@@ -68,13 +68,13 @@ export const CopyToClipboard = ({
return ( return (
<button <button
type="button" type={"button"}
ref={wrapperRef} ref={wrapperRef}
onClick={handleClick} onClick={handleClick}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
tabIndex={tabIndex} tabIndex={tabIndex}
aria-label={resolvedLabel} aria-label={resolvedLabel}
aria-live="polite" aria-live={"polite"}
className={cn( className={cn(
"inline-flex gap-2 items-center group/copy cursor-default wails-no-draggable text-left pointer-events-auto rounded-sm outline-none", "inline-flex gap-2 items-center group/copy cursor-default wails-no-draggable text-left pointer-events-auto rounded-sm outline-none",
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
@@ -90,14 +90,12 @@ export const CopyToClipboard = ({
> >
{children} {children}
<span <span
aria-hidden="true" aria-hidden={"true"}
className={ className={"absolute bottom-0 left-0 right-0 border-b border-dashed border-transparent group-hover/copy:border-nb-gray-500 pointer-events-none"}
"absolute bottom-0 left-0 right-0 border-b border-dashed border-transparent group-hover/copy:border-nb-gray-500 pointer-events-none"
}
/> />
</span> </span>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"shrink-0 inline-flex relative top-[2px] right-[1px]", "shrink-0 inline-flex relative top-[2px] right-[1px]",
iconAlignment === "left" ? "order-first" : "order-last", iconAlignment === "left" ? "order-first" : "order-last",
@@ -41,7 +41,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className={"ml-auto h-4 w-4"} />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)); ));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
@@ -117,7 +117,7 @@ const DropdownMenuItem = React.forwardRef<
{...props} {...props}
> >
{href ? ( {href ? (
<a href={href} target={target} rel={rel} className="flex w-full items-center gap-3"> <a href={href} target={target} rel={rel} className={"flex w-full items-center gap-3"}>
{children} {children}
</a> </a>
) : ( ) : (
@@ -142,9 +142,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className={"absolute left-2 flex h-3.5 w-3.5 items-center justify-center"}>
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className={"h-4 w-4"} />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
@@ -166,9 +166,9 @@ const DropdownMenuRadioItem = React.forwardRef<
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className={"absolute left-2 flex h-3.5 w-3.5 items-center justify-center"}>
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" /> <Circle className={"h-2 w-2 fill-current"} />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
@@ -5,7 +5,7 @@ import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Command } from "cmdk"; import { Command } from "cmdk";
import { CheckIcon, ChevronDown, LanguagesIcon, Search } from "lucide-react"; import { CheckIcon, ChevronDown, LanguagesIcon, Search } from "lucide-react";
import { Preferences } from "@bindings/services"; import { Preferences } from "@bindings/services";
import { LanguageCode, type Language } from "@bindings/i18n/models.js"; import { type LanguageCode, type Language } from "@bindings/i18n/models.js";
import { HelpText } from "@/components/typography/HelpText"; import { HelpText } from "@/components/typography/HelpText";
import { Label } from "@/components/typography/Label"; import { Label } from "@/components/typography/Label";
import { loadLanguages } from "@/lib/i18n"; import { loadLanguages } from "@/lib/i18n";
@@ -88,7 +88,7 @@ export function LanguagePicker() {
disabled={busy || languages.length === 0} disabled={busy || languages.length === 0}
onKeyDown={handleTriggerKeyDown} onKeyDown={handleTriggerKeyDown}
aria-label={t("settings.general.language.label")} aria-label={t("settings.general.language.label")}
aria-haspopup="listbox" aria-haspopup={"listbox"}
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
"inline-flex items-center gap-2 h-[40px] px-3 min-w-[240px]", "inline-flex items-center gap-2 h-[40px] px-3 min-w-[240px]",
@@ -102,7 +102,7 @@ export function LanguagePicker() {
> >
<LanguagesIcon <LanguagesIcon
size={16} size={16}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-200 shrink-0"} className={"text-nb-gray-200 shrink-0"}
/> />
<span className={"truncate flex-1 text-left"}> <span className={"truncate flex-1 text-left"}>
@@ -110,7 +110,7 @@ export function LanguagePicker() {
</span> </span>
<ChevronDown <ChevronDown
size={12} size={12}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-400 shrink-0"} className={"text-nb-gray-400 shrink-0"}
/> />
</button> </button>
@@ -141,12 +141,12 @@ export function LanguagePicker() {
> >
<div className={"px-1 pb-1"}> <div className={"px-1 pb-1"}>
<div <div
role="search" role={"search"}
className={"group flex items-center gap-2 px-1 h-8"} className={"group flex items-center gap-2 px-1 h-8"}
> >
<Search <Search
size={14} size={14}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-200 shrink-0"} className={"text-nb-gray-200 shrink-0"}
/> />
<Command.Input <Command.Input
@@ -166,9 +166,7 @@ export function LanguagePicker() {
<Command.List> <Command.List>
<Command.Empty> <Command.Empty>
<div <div
className={ className={"px-3 py-4 text-center text-[0.7rem] text-nb-gray-400"}
"px-3 py-4 text-center text-[0.7rem] text-nb-gray-400"
}
> >
{t("settings.general.language.empty")} {t("settings.general.language.empty")}
</div> </div>
@@ -191,10 +189,8 @@ export function LanguagePicker() {
{labelFor(lang)} {labelFor(lang)}
</span> </span>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={ className={"w-4 shrink-0 flex items-center justify-center"}
"w-4 shrink-0 flex items-center justify-center"
}
> >
{checked && ( {checked && (
<CheckIcon <CheckIcon
@@ -216,9 +212,7 @@ export function LanguagePicker() {
)} )}
> >
<ScrollArea.Thumb <ScrollArea.Thumb
className={ className={"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"}
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
}
/> />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
</ScrollArea.Root> </ScrollArea.Root>
@@ -25,7 +25,7 @@ export const ManagementServerSwitch = ({ value, onChange, fullWidth = false }: P
<img <img
src={netbirdLogo} src={netbirdLogo}
alt={""} alt={""}
aria-hidden="true" aria-hidden={"true"}
className={"h-[0.8rem] aspect-[31/23] shrink-0"} className={"h-[0.8rem] aspect-[31/23] shrink-0"}
/> />
{t("settings.general.management.cloud")} {t("settings.general.management.cloud")}
@@ -1,5 +1,5 @@
import { ComponentType } from "react"; import { type ComponentType } from "react";
import { LucideProps } from "lucide-react"; import { type LucideProps } from "lucide-react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
export type SquareIconVariant = "default" | "info" | "warning" | "danger"; export type SquareIconVariant = "default" | "info" | "warning" | "danger";
@@ -25,7 +25,7 @@ export const SquareIcon = ({
className, className,
}: SquareIconProps) => ( }: SquareIconProps) => (
<div <div
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"h-11 w-11 rounded-lg flex items-center justify-center border bg-nb-gray-920 border-nb-gray-900", "h-11 w-11 rounded-lg flex items-center justify-center border bg-nb-gray-920 border-nb-gray-900",
variantClass[variant], variantClass[variant],
@@ -1,4 +1,4 @@
import { ReactNode, useEffect, useRef, useState } from "react"; import { type ReactNode, useEffect, useRef, useState } from "react";
import * as RTooltip from "@radix-ui/react-tooltip"; import * as RTooltip from "@radix-ui/react-tooltip";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
@@ -1,6 +1,6 @@
import { ComponentType, ReactNode, forwardRef } from "react"; import { type ComponentType, type ReactNode, forwardRef } from "react";
import * as Tabs from "@radix-ui/react-tabs"; import * as Tabs from "@radix-ui/react-tabs";
import { LucideProps } from "lucide-react"; import { type LucideProps } from "lucide-react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
const Root = forwardRef<HTMLDivElement, Omit<Tabs.TabsProps, "orientation">>( const Root = forwardRef<HTMLDivElement, Omit<Tabs.TabsProps, "orientation">>(
@@ -55,7 +55,7 @@ const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function VerticalTab
> >
<Icon <Icon
size={iconSize} size={iconSize}
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"shrink-0 ml-2 transition-colors duration-150", "shrink-0 ml-2 transition-colors duration-150",
"text-nb-gray-400 group-data-[state=active]:text-nb-gray-100", "text-nb-gray-400 group-data-[state=active]:text-nb-gray-100",
@@ -70,7 +70,7 @@ const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function VerticalTab
{title} {title}
</span> </span>
{adornment && ( {adornment && (
<div aria-hidden="true" className={"ml-auto mr-2 shrink-0"}> <div aria-hidden={"true"} className={"ml-auto mr-2 shrink-0"}>
{adornment} {adornment}
</div> </div>
)} )}
@@ -1,6 +1,6 @@
import { cva, VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { Check, Copy, Loader2 } from "lucide-react"; import { Check, Copy, Loader2 } from "lucide-react";
import { ButtonHTMLAttributes, forwardRef, useEffect, useRef, useState } from "react"; import { type ButtonHTMLAttributes, forwardRef, useEffect, useRef, useState } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
@@ -173,7 +173,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
> >
{loading && ( {loading && (
<span <span
aria-hidden="true" aria-hidden={"true"}
className={"absolute inset-0 flex items-center justify-center"} className={"absolute inset-0 flex items-center justify-center"}
> >
<Loader2 size={iconSize} className={"animate-spin"} /> <Loader2 size={iconSize} className={"animate-spin"} />
@@ -182,9 +182,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
<span className={cn("contents", loading && "invisible")}> <span className={cn("contents", loading && "invisible")}>
{copy !== undefined && {copy !== undefined &&
(copied ? ( (copied ? (
<Check size={iconSize} aria-hidden="true" /> <Check size={iconSize} aria-hidden={"true"} />
) : ( ) : (
<Copy size={iconSize} aria-hidden="true" /> <Copy size={iconSize} aria-hidden={"true"} />
))} ))}
{children} {children}
</span> </span>
@@ -1,5 +1,5 @@
import { ButtonHTMLAttributes, ComponentType, forwardRef } from "react"; import { type ButtonHTMLAttributes, type ComponentType, forwardRef } from "react";
import { LucideProps } from "lucide-react"; import { type LucideProps } from "lucide-react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
type Props = ButtonHTMLAttributes<HTMLButtonElement> & { type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
@@ -1,4 +1,4 @@
import { ReactNode, forwardRef } from "react"; import { type ReactNode, forwardRef } from "react";
import { cn } from "@/lib/cn.ts"; import { cn } from "@/lib/cn.ts";
import { isMacOS } from "@/lib/platform.ts"; import { isMacOS } from "@/lib/platform.ts";
@@ -17,9 +17,7 @@ export const ConfirmDialog = forwardRef<HTMLDivElement, ConfirmDialogProps>(func
open open
aria-label={ariaLabel} aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy} aria-labelledby={ariaLabelledBy}
className={ className={"wails-draggable select-none flex flex-col items-center static bg-transparent text-inherit p-0 m-0 max-w-none max-h-none border-0 w-full"}
"wails-draggable select-none flex flex-col items-center static bg-transparent text-inherit p-0 m-0 max-w-none max-h-none border-0 w-full"
}
> >
<div <div
ref={ref} ref={ref}
@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as Dialog from "@/components/dialog/Dialog"; import * as Dialog from "@/components/dialog/Dialog";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
@@ -43,15 +43,15 @@ export const ConfirmModal = ({
}} }}
> >
<Dialog.Content <Dialog.Content
maxWidthClass="max-w-sm" maxWidthClass={"max-w-sm"}
showClose={false} showClose={false}
className="py-5" className={"py-5"}
srTitle={srTitle} srTitle={srTitle}
srDescription={srDescription} srDescription={srDescription}
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
> >
<div className="flex flex-col gap-5 px-5"> <div className={"flex flex-col gap-5 px-5"}>
<div className="flex flex-col gap-1 pl-1"> <div className={"flex flex-col gap-1 pl-1"}>
<DialogHeading align={"left"}>{title}</DialogHeading> <DialogHeading align={"left"}>{title}</DialogHeading>
<DialogDescription align={"left"} className={"whitespace-pre-line"}> <DialogDescription align={"left"} className={"whitespace-pre-line"}>
{description} {description}
@@ -1,4 +1,4 @@
import { forwardRef, ComponentPropsWithoutRef, ElementRef, HTMLAttributes } from "react"; import { forwardRef, type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes } from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { X } from "lucide-react"; import { X } from "lucide-react";
@@ -96,7 +96,7 @@ export const Content = forwardRef<ElementRef<typeof DialogPrimitive.Content>, Co
)} )}
aria-label={t("common.close")} aria-label={t("common.close")}
> >
<X className="h-4 w-4" aria-hidden="true" /> <X className={"h-4 w-4"} aria-hidden={"true"} />
</DialogPrimitive.Close> </DialogPrimitive.Close>
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
type DialogActionsProps = { type DialogActionsProps = {
@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
type DialogAlign = "left" | "center" | "right"; type DialogAlign = "left" | "center" | "right";
@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
type DialogAlign = "left" | "center" | "right"; type DialogAlign = "left" | "center" | "right";
@@ -18,15 +18,11 @@ export const DaemonOutdatedOverlay = () => {
return ( return (
<div <div
className={ className={"fixed inset-0 z-[100] flex items-center justify-center bg-nb-gray-950 backdrop-blur-sm cursor-default select-none wails-draggable"}
"fixed inset-0 z-[100] flex items-center justify-center bg-nb-gray-950 backdrop-blur-sm cursor-default select-none wails-draggable"
}
> >
<div className={"flex flex-col items-center gap-5 px-8 max-w-lg text-center"}> <div className={"flex flex-col items-center gap-5 px-8 max-w-lg text-center"}>
<div <div
className={ className={"h-11 w-11 rounded-xl flex items-center justify-center bg-nb-gray-920 border border-nb-gray-900 text-amber-500"}
"h-11 w-11 rounded-xl flex items-center justify-center bg-nb-gray-920 border border-nb-gray-900 text-amber-500"
}
> >
<AlertTriangleIcon size={20} /> <AlertTriangleIcon size={20} />
</div> </div>
@@ -18,15 +18,11 @@ export const DaemonUnavailableOverlay = () => {
return ( return (
<div <div
className={ className={"fixed inset-0 z-[100] flex items-center justify-center bg-nb-gray-950 backdrop-blur-sm cursor-default select-none wails-draggable"}
"fixed inset-0 z-[100] flex items-center justify-center bg-nb-gray-950 backdrop-blur-sm cursor-default select-none wails-draggable"
}
> >
<div className={"flex flex-col items-center gap-5 px-8 max-w-lg text-center"}> <div className={"flex flex-col items-center gap-5 px-8 max-w-lg text-center"}>
<div <div
className={ className={"h-11 w-11 rounded-xl flex items-center justify-center bg-nb-gray-920 border border-nb-gray-900 text-red-500"}
"h-11 w-11 rounded-xl flex items-center justify-center bg-nb-gray-920 border border-nb-gray-900 text-red-500"
}
> >
<AlertCircleIcon size={20} /> <AlertCircleIcon size={20} />
</div> </div>
@@ -1,5 +1,5 @@
import { ComponentType } from "react"; import { type ComponentType } from "react";
import { LucideProps } from "lucide-react"; import { type LucideProps } from "lucide-react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
import { SquareIcon } from "@/components/SquareIcon"; import { SquareIcon } from "@/components/SquareIcon";
import { isMacOS } from "@/lib/platform"; import { isMacOS } from "@/lib/platform";
@@ -1,5 +1,5 @@
import { ComponentType } from "react"; import { type ComponentType } from "react";
import { FunnelXIcon, LucideProps } from "lucide-react"; import { FunnelXIcon, type LucideProps } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { EmptyState } from "./EmptyState"; import { EmptyState } from "./EmptyState";
@@ -1,9 +1,9 @@
import { cva, VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { Check, ChevronDown, ChevronUp, Copy, Eye, EyeOff } from "lucide-react"; import { Check, ChevronDown, ChevronUp, Copy, Eye, EyeOff } from "lucide-react";
import { import {
forwardRef, forwardRef,
InputHTMLAttributes, type InputHTMLAttributes,
ReactNode, type ReactNode,
useEffect, useEffect,
useId, useId,
useRef, useRef,
@@ -164,16 +164,16 @@ function NumberStepper({
)} )}
> >
<button <button
type="button" type={"button"}
tabIndex={-1} tabIndex={-1}
aria-label={t("common.increase")} aria-label={t("common.increase")}
onClick={() => onStep(1)} onClick={() => onStep(1)}
className="flex-1 flex items-center justify-center w-9 hover:bg-nb-gray-800 transition-colors text-nb-gray-300 cursor-default" className={"flex-1 flex items-center justify-center w-9 hover:bg-nb-gray-800 transition-colors text-nb-gray-300 cursor-default"}
> >
<ChevronUp size={12} /> <ChevronUp size={12} />
</button> </button>
<button <button
type="button" type={"button"}
tabIndex={-1} tabIndex={-1}
aria-label={t("common.decrease")} aria-label={t("common.decrease")}
onClick={() => onStep(-1)} onClick={() => onStep(-1)}
@@ -262,9 +262,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
const passwordToggle = const passwordToggle =
isPasswordType && showPasswordToggle ? ( isPasswordType && showPasswordToggle ? (
<button <button
type="button" type={"button"}
onClick={() => setShowPassword((s) => !s)} onClick={() => setShowPassword((s) => !s)}
className="hover:text-white transition-all pointer-events-auto" className={"hover:text-white transition-all pointer-events-auto"}
aria-label={t("common.togglePasswordVisibility")} aria-label={t("common.togglePasswordVisibility")}
> >
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />} {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -286,9 +286,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
const copyToggle = copy ? ( const copyToggle = copy ? (
<button <button
type="button" type={"button"}
onClick={onCopy} onClick={onCopy}
className="hover:text-white transition-all pointer-events-auto" className={"hover:text-white transition-all pointer-events-auto"}
aria-label={t("common.copy")} aria-label={t("common.copy")}
> >
{copied ? <Check size={16} /> : <Copy size={16} />} {copied ? <Check size={16} /> : <Copy size={16} />}
@@ -311,7 +311,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
}); });
return ( return (
<div className="flex flex-col w-full min-w-0"> <div className={"flex flex-col w-full min-w-0"}>
{label && <Label htmlFor={inputId}>{label}</Label>} {label && <Label htmlFor={inputId}>{label}</Label>}
<div className={cn("flex relative h-[40px] w-full", maxWidthClass)}> <div className={cn("flex relative h-[40px] w-full", maxWidthClass)}>
{customPrefix && ( {customPrefix && (
@@ -325,7 +325,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{icon && <InputIconSlot icon={icon} disabled={props.disabled} />} {icon && <InputIconSlot icon={icon} disabled={props.disabled} />}
<div className="relative flex flex-grow min-w-0"> <div className={"relative flex flex-grow min-w-0"}>
<input <input
id={inputId} id={inputId}
type={inputType} type={inputType}
@@ -1,4 +1,4 @@
import { forwardRef, InputHTMLAttributes, ReactNode } from "react"; import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SearchIcon } from "lucide-react"; import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
@@ -15,12 +15,12 @@ export const SearchInput = forwardRef<HTMLInputElement, Props>(function SearchIn
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div <div
role="search" role={"search"}
className={cn("flex items-center gap-2 px-1 h-10", disabled && "opacity-50")} className={cn("flex items-center gap-2 px-1 h-10", disabled && "opacity-50")}
> >
<SearchIcon <SearchIcon
size={iconSize} size={iconSize}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-300 shrink-0"} className={"text-nb-gray-300 shrink-0"}
/> />
<input <input
@@ -38,7 +38,7 @@ export const SearchInput = forwardRef<HTMLInputElement, Props>(function SearchIn
/> />
{shortcut && ( {shortcut && (
<span <span
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"shrink-0 select-none", "shrink-0 select-none",
"inline-flex items-center justify-center", "inline-flex items-center justify-center",
@@ -39,9 +39,9 @@ export default function FancyToggleSwitch({
"text-transparent select-none rounded bg-[#25282d] box-decoration-clone animate-pulse"; "text-transparent select-none rounded bg-[#25282d] box-decoration-clone animate-pulse";
return ( return (
<div <div
role="status" role={"status"}
aria-busy="true" aria-busy={"true"}
aria-live="polite" aria-live={"polite"}
className={cn("inline-block text-left w-full", className)} className={cn("inline-block text-left w-full", className)}
> >
<div className={"flex justify-between gap-10"}> <div className={"flex justify-between gap-10"}>
@@ -57,7 +57,7 @@ export default function FancyToggleSwitch({
</div> </div>
<div className={"mt-2 pr-1"}> <div className={"mt-2 pr-1"}>
<div <div
aria-hidden="true" aria-hidden={"true"}
className={"h-[24px] w-[44px] rounded-full bg-[#25282d] animate-pulse"} className={"h-[24px] w-[44px] rounded-full bg-[#25282d] animate-pulse"}
/> />
</div> </div>
@@ -95,7 +95,7 @@ export default function FancyToggleSwitch({
/> />
</div> </div>
</div> </div>
{children && value ? <div className="mt-4">{children}</div> : null} {children && value ? <div className={"mt-4"}>{children}</div> : null}
</div> </div>
); );
} }
@@ -1,6 +1,6 @@
import * as RadioGroup from "@radix-ui/react-radio-group"; import * as RadioGroup from "@radix-ui/react-radio-group";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
import { useSwitchItemGroup } from "@/components/switches/SwitchItemGroup"; import { useSwitchItemGroup } from "@/components/switches/SwitchItemGroup";
@@ -1,5 +1,5 @@
import * as RadioGroup from "@radix-ui/react-radio-group"; import * as RadioGroup from "@radix-ui/react-radio-group";
import { createContext, ReactNode, useContext, useId, useMemo } from "react"; import { createContext, type ReactNode, useContext, useId, useMemo } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
type SwitchItemGroupContextValue = { type SwitchItemGroupContextValue = {
@@ -1,7 +1,7 @@
"use client"; "use client";
import * as SwitchPrimitives from "@radix-ui/react-switch"; import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cva, VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
type Props = { type Props = {
@@ -1,6 +1,6 @@
import * as LabelPrimitive from "@radix-ui/react-label"; import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { ComponentPropsWithoutRef, forwardRef, Ref } from "react"; import { type ComponentPropsWithoutRef, forwardRef, type Ref } from "react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
const labelVariants = cva( const labelVariants = cva(
@@ -1,6 +1,6 @@
import { import {
createContext, createContext,
ReactNode, type ReactNode,
useCallback, useCallback,
useContext, useContext,
useMemo, useMemo,
@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { cn } from "@/lib/cn.ts"; import { cn } from "@/lib/cn.ts";
+1 -1
View File
@@ -3,7 +3,7 @@ import { initReactI18next } from "react-i18next";
import { Events } from "@wailsio/runtime"; import { Events } from "@wailsio/runtime";
import { Preferences, I18n } from "@bindings/services"; import { Preferences, I18n } from "@bindings/services";
import { LanguageCode } from "@bindings/i18n/models.js"; import { type LanguageCode } from "@bindings/i18n/models.js";
// Relative path on purpose — alias globs (`@/…`) silently match nothing in some Vite dev setups. // Relative path on purpose — alias globs (`@/…`) silently match nothing in some Vite dev setups.
type BundleEntry = { message: string; description?: string }; type BundleEntry = { message: string; description?: string };
+1
View File
@@ -111,6 +111,7 @@ function forward(level: Level, args: unknown[]) {
// Don't touch console here — it would recurse back into forward(). // Don't touch console here — it would recurse back into forward().
UILog.Log(level, source, format(args)).catch(() => {}); UILog.Log(level, source, format(args)).catch(() => {});
} catch { } catch {
// Swallow — log forwarding must never throw back into the caller.
} finally { } finally {
inForward = false; inForward = false;
} }
+2
View File
@@ -15,6 +15,8 @@ WE'RE HIRING: https://netbird.io/careers
OPEN SOURCE: https://github.com/netbirdio/netbird OPEN SOURCE: https://github.com/netbirdio/netbird
`; `;
// Intentional NetBird ASCII banner in the devtools console.
// eslint-disable-next-line no-console
console.log( console.log(
message, message,
"color: #f68330; font-family: monospace; font-weight: normal; line-height: 1;", "color: #f68330; font-family: monospace; font-weight: normal; line-height: 1;",
@@ -17,9 +17,7 @@ export const UpdateBadge = forwardRef<HTMLDivElement, Props>(function UpdateBadg
{...rest} {...rest}
> >
<span <span
className={ className={"animate-ping absolute inline-flex h-[15px] w-[15px] rounded-full bg-netbird opacity-20 pointer-events-none"}
"animate-ping absolute inline-flex h-[15px] w-[15px] rounded-full bg-netbird opacity-20 pointer-events-none"
}
/> />
<ArrowUpCircleIcon size={size} className={"text-netbird"} /> <ArrowUpCircleIcon size={size} className={"text-netbird"} />
</div> </div>
@@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { type ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Browser } from "@wailsio/runtime"; import { Browser } from "@wailsio/runtime";
import { DownloadIcon, NotepadText } from "lucide-react"; import { DownloadIcon, NotepadText } from "lucide-react";
@@ -86,9 +86,7 @@ function Link({ url, children }: Readonly<{ url: string; children: ReactNode }>)
<button <button
type={"button"} type={"button"}
onClick={() => openUrl(url)} onClick={() => openUrl(url)}
className={ className={"text-sm text-netbird hover:underline hover:underline-offset-4 hover:decoration-[0.5px] font-medium"}
"text-sm text-netbird hover:underline hover:underline-offset-4 hover:decoration-[0.5px] font-medium"
}
> >
{children} {children}
</button> </button>
@@ -65,9 +65,7 @@ export default function LoginWaitingForBrowserDialog() {
type={"button"} type={"button"}
onClick={tryAgain} onClick={tryAgain}
disabled={!uri} disabled={!uri}
className={ className={"wails-no-draggable text-netbird hover:underline disabled:opacity-40 disabled:cursor-not-allowed"}
"wails-no-draggable text-netbird hover:underline disabled:opacity-40 disabled:cursor-not-allowed"
}
> >
{t("browserLogin.tryAgain")} {t("browserLogin.tryAgain")}
</button> </button>
@@ -246,9 +246,7 @@ export const MainConnectionStatusSwitch = () => {
id={"nb-connection-status"} id={"nb-connection-status"}
role={"status"} role={"status"}
aria-live={"polite"} aria-live={"polite"}
className={ className={"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300 select-none wails-no-draggable mb-1"}
"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300 select-none wails-no-draggable mb-1"
}
> >
{t(STATUS_KEY[connState])} {t(STATUS_KEY[connState])}
</p> </p>
@@ -265,9 +263,7 @@ export const MainConnectionStatusSwitch = () => {
> >
<TruncatedText <TruncatedText
text={shortenDns(fqdn) || " "} text={shortenDns(fqdn) || " "}
className={ className={"block font-mono text-[0.8rem] leading-tight text-nb-gray-300 truncate max-w-[310px] h-[18px]"}
"block font-mono text-[0.8rem] leading-tight text-nb-gray-300 truncate max-w-[310px] h-[18px]"
}
/> />
</CopyToClipboard> </CopyToClipboard>
<LocalIpLine ip={ip} ipv6={ipv6} show={show} /> <LocalIpLine ip={ip} ipv6={ipv6} show={show} />
@@ -314,7 +310,7 @@ const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boole
type={"button"} type={"button"}
tabIndex={show && ip ? 0 : -1} tabIndex={show && ip ? 0 : -1}
aria-label={t("connect.localIp.label")} aria-label={t("connect.localIp.label")}
aria-haspopup="dialog" aria-haspopup={"dialog"}
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
"group relative inline-flex items-center outline-none cursor-default rounded-sm", "group relative inline-flex items-center outline-none cursor-default rounded-sm",
@@ -333,7 +329,7 @@ const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boole
</span> </span>
<ChevronDownIcon <ChevronDownIcon
size={14} size={14}
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"absolute -right-5 top-1/2 -translate-y-1/2", "absolute -right-5 top-1/2 -translate-y-1/2",
"shrink-0 text-nb-gray-300 transition-colors", "shrink-0 text-nb-gray-300 transition-colors",
@@ -395,7 +391,7 @@ const IpRow = ({ value }: { value: string }) => {
> >
<span className={"font-mono text-[0.75rem] truncate min-w-0"}>{value}</span> <span className={"font-mono text-[0.75rem] truncate min-w-0"}>{value}</span>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={"shrink-0 inline-flex items-center text-nb-gray-200"} className={"shrink-0 inline-flex items-center text-nb-gray-200"}
> >
{copied ? <CheckIcon size={11} /> : <CopyIcon size={11} />} {copied ? <CheckIcon size={11} /> : <CopyIcon size={11} />}
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Command } from "cmdk"; import { Command } from "cmdk";
import { Check, ChevronsUpDown, LucideProps, SquareArrowUpRight } from "lucide-react"; import { Check, ChevronsUpDown, type LucideProps, SquareArrowUpRight } from "lucide-react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
import { TruncatedText } from "@/components/TruncatedText"; import { TruncatedText } from "@/components/TruncatedText";
import { useNetworks } from "@/contexts/NetworksContext"; import { useNetworks } from "@/contexts/NetworksContext";
@@ -61,7 +61,7 @@ export const MainExitNodeSwitcher = () => {
disabled={disabled} disabled={disabled}
active={!!active} active={!!active}
aria-label={t("exitNodes.dropdown.trigger")} aria-label={t("exitNodes.dropdown.trigger")}
aria-haspopup="listbox" aria-haspopup={"listbox"}
aria-expanded={open} aria-expanded={open}
onKeyDown={handleTriggerKeyDown} onKeyDown={handleTriggerKeyDown}
/> />
@@ -120,9 +120,7 @@ export const MainExitNodeSwitcher = () => {
)} )}
> >
<ScrollArea.Thumb <ScrollArea.Thumb
className={ className={"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"}
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
}
/> />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
</ScrollArea.Root> </ScrollArea.Root>
@@ -166,7 +164,7 @@ const ExitNodeTriggerCard = forwardRef<HTMLButtonElement, TriggerProps>(
{...props} {...props}
> >
<div <div
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"h-9 w-9 rounded-md flex items-center justify-center shrink-0", "h-9 w-9 rounded-md flex items-center justify-center shrink-0",
active active
@@ -182,14 +180,12 @@ const ExitNodeTriggerCard = forwardRef<HTMLButtonElement, TriggerProps>(
</span> </span>
<TruncatedText <TruncatedText
text={description} text={description}
className={ className={"block text-[0.85rem] font-medium text-nb-gray-400 truncate max-w-full"}
"block text-[0.85rem] font-medium text-nb-gray-400 truncate max-w-full"
}
/> />
</div> </div>
<ChevronsUpDown <ChevronsUpDown
size={16} size={16}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-400 shrink-0"} className={"text-nb-gray-400 shrink-0"}
/> />
</button> </button>
@@ -215,7 +211,7 @@ const NoneRow = ({ isActive, onSelect }: NoneRowProps) => {
)} )}
> >
<span className={"min-w-0 flex-1 truncate"}>{t("exitNodes.dropdown.noneTitle")}</span> <span className={"min-w-0 flex-1 truncate"}>{t("exitNodes.dropdown.noneTitle")}</span>
{isActive && <Check size={16} aria-hidden="true" className={"shrink-0 text-netbird"} />} {isActive && <Check size={16} aria-hidden={"true"} className={"shrink-0 text-netbird"} />}
</Command.Item> </Command.Item>
); );
}; };
@@ -238,7 +234,7 @@ const ExitNodeRow = ({ id, label, isActive, onSelect }: ExitNodeRowProps) => (
)} )}
> >
<span className={"min-w-0 flex-1 truncate"}>{label}</span> <span className={"min-w-0 flex-1 truncate"}>{label}</span>
{isActive && <Check size={16} aria-hidden="true" className={"shrink-0 text-netbird"} />} {isActive && <Check size={16} aria-hidden={"true"} className={"shrink-0 text-netbird"} />}
</Command.Item> </Command.Item>
); );
@@ -76,22 +76,22 @@ export const MainHeader = () => {
iconClassName={"text-nb-gray-200 wails-no-draggable"} iconClassName={"text-nb-gray-200 wails-no-draggable"}
className={"select-none"} className={"select-none"}
aria-label={t("header.menu.open")} aria-label={t("header.menu.open")}
aria-haspopup="menu" aria-haspopup={"menu"}
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align={"end"}
sideOffset={8} sideOffset={8}
className="min-w-52 select-none data-[state=closed]:!animate-none data-[state=closed]:!duration-0" className={"min-w-52 select-none data-[state=closed]:!animate-none data-[state=closed]:!duration-0"}
> >
{updateAvailable && ( {updateAvailable && (
<> <>
<DropdownMenuItem onClick={openAbout}> <DropdownMenuItem onClick={openAbout}>
<div className="flex items-center gap-2"> <div className={"flex items-center gap-2"}>
<ArrowUpCircleIcon <ArrowUpCircleIcon
size={14} size={14}
className={"text-netbird"} className={"text-netbird"}
aria-hidden="true" aria-hidden={"true"}
/> />
<span className={"text-netbird"}> <span className={"text-netbird"}>
{t("header.menu.updateAvailable")} {t("header.menu.updateAvailable")}
@@ -102,9 +102,9 @@ export const MainHeader = () => {
</> </>
)} )}
<DropdownMenuItem onClick={openSettings}> <DropdownMenuItem onClick={openSettings}>
<div className="flex items-center gap-2 w-full"> <div className={"flex items-center gap-2 w-full"}>
<Settings size={14} aria-hidden="true" /> <Settings size={14} aria-hidden={"true"} />
<span className="flex-1">{t("header.menu.settings")}</span> <span className={"flex-1"}>{t("header.menu.settings")}</span>
<DropdownMenuShortcut> <DropdownMenuShortcut>
{formatShortcut(SETTINGS_SHORTCUT)} {formatShortcut(SETTINGS_SHORTCUT)}
</DropdownMenuShortcut> </DropdownMenuShortcut>
@@ -131,15 +131,11 @@ export const MainHeader = () => {
</DropdownMenu> </DropdownMenu>
{updateAvailable && ( {updateAvailable && (
<span <span
aria-hidden="true" aria-hidden={"true"}
className={ className={"pointer-events-none absolute top-1.5 right-1.5 flex h-2.5 w-2.5 items-center justify-center"}
"pointer-events-none absolute top-1.5 right-1.5 flex h-2.5 w-2.5 items-center justify-center"
}
> >
<span <span
className={ className={"absolute inset-0 rounded-full bg-netbird opacity-60 animate-ping"}
"absolute inset-0 rounded-full bg-netbird opacity-60 animate-ping"
}
/> />
<span className={"relative h-1.5 w-1.5 rounded-full bg-netbird"} /> <span className={"relative h-1.5 w-1.5 rounded-full bg-netbird"} />
</span> </span>
@@ -179,11 +175,11 @@ type ViewModeItemProps = {
}; };
const ViewModeItem = ({ icon: Icon, label, selected, onSelect }: ViewModeItemProps) => ( const ViewModeItem = ({ icon: Icon, label, selected, onSelect }: ViewModeItemProps) => (
<DropdownMenuItem onClick={onSelect} role="menuitemradio" aria-checked={selected}> <DropdownMenuItem onClick={onSelect} role={"menuitemradio"} aria-checked={selected}>
<div className="flex items-center gap-2 w-full"> <div className={"flex items-center gap-2 w-full"}>
<Icon size={14} aria-hidden="true" /> <Icon size={14} aria-hidden={"true"} />
<span className="flex-1">{label}</span> <span className={"flex-1"}>{label}</span>
{selected && <Check size={14} className="text-netbird" aria-hidden="true" />} {selected && <Check size={14} className={"text-netbird"} aria-hidden={"true"} />}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
); );
@@ -1,6 +1,6 @@
import { ComponentType, KeyboardEvent, useEffect, useRef } from "react"; import { type ComponentType, type KeyboardEvent, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Layers3Icon, LucideProps, MonitorSmartphoneIcon } from "lucide-react"; import { Layers3Icon, type LucideProps, MonitorSmartphoneIcon } from "lucide-react";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
import { useNavSection, type NavSection } from "@/contexts/NavSectionContext"; import { useNavSection, type NavSection } from "@/contexts/NavSectionContext";
import { useStatus } from "@/contexts/StatusContext"; import { useStatus } from "@/contexts/StatusContext";
@@ -53,7 +53,7 @@ export const Navigation = () => {
if (enabled.length < 2) return; if (enabled.length < 2) return;
const currentIndex = enabled.findIndex((t) => t.value === section); const currentIndex = enabled.findIndex((t) => t.value === section);
if (currentIndex === -1) return; if (currentIndex === -1) return;
let nextIndex: number | null = null; let nextIndex: number;
switch (e.key) { switch (e.key) {
case "ArrowRight": case "ArrowRight":
nextIndex = (currentIndex + 1) % enabled.length; nextIndex = (currentIndex + 1) % enabled.length;
@@ -71,7 +71,7 @@ export const Navigation = () => {
return; return;
} }
e.preventDefault(); e.preventDefault();
if (nextIndex !== null) focusTab(enabled[nextIndex].value); focusTab(enabled[nextIndex].value);
}; };
return ( return (
@@ -113,10 +113,10 @@ export const Navigation = () => {
isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-default", isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-default",
)} )}
> >
<Icon size={14} aria-hidden="true" /> <Icon size={14} aria-hidden={"true"} />
<span className={"text-sm font-normal"}>{tab.label}</span> <span className={"text-sm font-normal"}>{tab.label}</span>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"absolute inset-x-0 bottom-0 h-px transition-all", "absolute inset-x-0 bottom-0 h-px transition-all",
isActive isActive
@@ -48,11 +48,11 @@ export const NetworkFilters = ({ value, onChange, counts, disabled }: Props) =>
"wails-no-draggable cursor-default", "wails-no-draggable cursor-default",
)} )}
> >
<ListFilter size={14} aria-hidden="true" className={"shrink-0"} /> <ListFilter size={14} aria-hidden={"true"} className={"shrink-0"} />
<span> <span>
{active.label} <span className={"tabular-nums"}>({counts[active.value]})</span> {active.label} <span className={"tabular-nums"}>({counts[active.value]})</span>
</span> </span>
<ChevronDown size={14} aria-hidden="true" className={"ml-0.5 shrink-0"} /> <ChevronDown size={14} aria-hidden={"true"} className={"ml-0.5 shrink-0"} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align={"end"} className={"min-w-[10rem]"}> <DropdownMenuContent align={"end"} className={"min-w-[10rem]"}>
{filters.map((f) => { {filters.map((f) => {
@@ -61,7 +61,7 @@ export const NetworkFilters = ({ value, onChange, counts, disabled }: Props) =>
<DropdownMenuItem <DropdownMenuItem
key={f.value} key={f.value}
onClick={() => handleSelect(f.value)} onClick={() => handleSelect(f.value)}
role="menuitemradio" role={"menuitemradio"}
aria-checked={checked} aria-checked={checked}
className={"gap-2"} className={"gap-2"}
> >
@@ -70,7 +70,7 @@ export const NetworkFilters = ({ value, onChange, counts, disabled }: Props) =>
<span className={"tabular-nums"}>({counts[f.value]})</span> <span className={"tabular-nums"}>({counts[f.value]})</span>
</span> </span>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={"w-4 shrink-0 flex items-center justify-center"} className={"w-4 shrink-0 flex items-center justify-center"}
> >
{checked && <CheckIcon size={14} className={"text-netbird"} />} {checked && <CheckIcon size={14} className={"text-netbird"} />}
@@ -1,5 +1,5 @@
import { import {
KeyboardEvent, type KeyboardEvent,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
@@ -9,7 +9,7 @@ import {
} from "react"; } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { GlobeIcon, Layers3Icon, type LucideProps, NetworkIcon, WorkflowIcon } from "lucide-react"; import { GlobeIcon, Layers3Icon, type LucideProps, NetworkIcon, WorkflowIcon } from "lucide-react";
import type { Network } from "@bindings/services/models.js"; import type { Network } from "@bindings/services/models.js";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
@@ -22,7 +22,7 @@ import { EmptyState } from "@/components/empty-state/EmptyState";
import { NoResults } from "@/components/empty-state/NoResults"; import { NoResults } from "@/components/empty-state/NoResults";
import { useStatus } from "@/contexts/StatusContext"; import { useStatus } from "@/contexts/StatusContext";
import { useNetworks } from "@/contexts/NetworksContext"; import { useNetworks } from "@/contexts/NetworksContext";
import { NetworkFilter, NetworkFilters } from "./NetworkFilters"; import { type NetworkFilter, NetworkFilters } from "./NetworkFilters";
// Daemon renders DNS-route prefixes (zero netip.Prefix) as "invalid Prefix". // Daemon renders DNS-route prefixes (zero netip.Prefix) as "invalid Prefix".
const INVALID_PREFIX = "invalid Prefix"; const INVALID_PREFIX = "invalid Prefix";
@@ -193,9 +193,7 @@ export const Networks = () => {
)} )}
> >
<ScrollArea.Thumb <ScrollArea.Thumb
className={ className={"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"}
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
}
/> />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
</ScrollArea.Root> </ScrollArea.Root>
@@ -363,23 +361,19 @@ const NetworkRow = ({ network: n, index, onKeyDown, onToggle, setRowRef }: Netwo
/> />
<ResourceIconBadge type={resourceTypeOf(n)} /> <ResourceIconBadge type={resourceTypeOf(n)} />
<div <div
className={ className={"min-w-0 flex-1 flex flex-col leading-tight relative pointer-events-none"}
"min-w-0 flex-1 flex flex-col leading-tight relative pointer-events-none"
}
> >
<div> <div>
<CopyToClipboard message={n.id} onKeyDown={handleKey}> <CopyToClipboard message={n.id} onKeyDown={handleKey}>
<TruncatedText <TruncatedText
text={n.id} text={n.id}
className={ className={"block text-[0.81rem] font-medium text-nb-gray-100 truncate max-w-[300px]"}
"block text-[0.81rem] font-medium text-nb-gray-100 truncate max-w-[300px]"
}
/> />
</CopyToClipboard> </CopyToClipboard>
</div> </div>
<Subtitle network={n} onKeyDown={handleKey} /> <Subtitle network={n} onKeyDown={handleKey} />
</div> </div>
<div aria-hidden="true" className={"shrink-0 self-center relative pointer-events-none"}> <div aria-hidden={"true"} className={"shrink-0 self-center relative pointer-events-none"}>
<NetworkToggle checked={n.selected} /> <NetworkToggle checked={n.selected} />
</div> </div>
</div> </div>
@@ -390,7 +384,7 @@ const ResourceIconBadge = ({ type }: { type: ResourceType }) => {
const Icon = resourceIconFor(type); const Icon = resourceIconFor(type);
return ( return (
<div <div
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"h-9 w-9 shrink-0 rounded-md flex items-center justify-center mt-[0.25rem]", "h-9 w-9 shrink-0 rounded-md flex items-center justify-center mt-[0.25rem]",
"bg-nb-gray-920 border border-nb-gray-900 text-nb-gray-300", "bg-nb-gray-920 border border-nb-gray-900 text-nb-gray-300",
@@ -419,9 +413,7 @@ const Subtitle = ({ network, onKeyDown }: SubtitleProps) => {
<CopyToClipboard message={network.range} onKeyDown={onKeyDown}> <CopyToClipboard message={network.range} onKeyDown={onKeyDown}>
<TruncatedText <TruncatedText
text={network.range} text={network.range}
className={ className={"block text-xs font-mono text-nb-gray-400 truncate max-w-[300px]"}
"block text-xs font-mono text-nb-gray-400 truncate max-w-[300px]"
}
/> />
</CopyToClipboard> </CopyToClipboard>
</div> </div>
@@ -484,9 +476,7 @@ const ResolvedIpsTooltip = ({ ips }: { ips: string[] }) => {
<li key={ip}> <li key={ip}>
<CopyToClipboard message={ip} className={"px-1 py-0.5"}> <CopyToClipboard message={ip} className={"px-1 py-0.5"}>
<span <span
className={ className={"font-mono text-[0.72rem] text-nb-gray-100 whitespace-nowrap"}
"font-mono text-[0.72rem] text-nb-gray-100 whitespace-nowrap"
}
> >
{ip} {ip}
</span> </span>
@@ -1,7 +1,7 @@
import { import {
ComponentType, type ComponentType,
KeyboardEvent as ReactKeyboardEvent, type KeyboardEvent as ReactKeyboardEvent,
ReactNode, type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
@@ -23,7 +23,7 @@ import {
HandshakeIcon, HandshakeIcon,
KeyRoundIcon, KeyRoundIcon,
Layers3Icon, Layers3Icon,
LucideProps, type LucideProps,
MapPinIcon, MapPinIcon,
MonitorIcon, MonitorIcon,
Radio, Radio,
@@ -125,13 +125,14 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => {
if (!selected) return; if (!selected) return;
// Defer focus until the slide-in animation has started rendering. // Defer focus until the slide-in animation has started rendering.
requestAnimationFrame(() => backButtonRef.current?.focus()); requestAnimationFrame(() => backButtonRef.current?.focus());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected?.pubKey]); }, [selected?.pubKey]);
const getFocusable = (): HTMLElement[] => { const getFocusable = (): HTMLElement[] => {
const root = dialogRef.current; const root = dialogRef.current;
if (!root) return []; if (!root) return [];
const sel = const sel =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]),' + "button:not([disabled]), [href], input:not([disabled]), select:not([disabled])," +
' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; ' textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
return Array.from(root.querySelectorAll<HTMLElement>(sel)).filter( return Array.from(root.querySelectorAll<HTMLElement>(sel)).filter(
(el) => el.offsetParent !== null || el === document.activeElement, (el) => el.offsetParent !== null || el === document.activeElement,
@@ -173,8 +174,8 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => {
> >
<div <div
className={cn( className={cn(
"shrink-0 flex items-center gap-3", "flex shrink-0 items-center gap-3",
"px-3 h-12 border-b border-nb-gray-910", "h-12 border-b border-nb-gray-910 px-3",
)} )}
> >
<button <button
@@ -184,20 +185,20 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => {
onClick={() => setSelected(null)} onClick={() => setSelected(null)}
aria-label={t("common.close")} aria-label={t("common.close")}
className={cn( className={cn(
"shrink-0 h-8 w-8 rounded-md flex items-center justify-center", "flex h-8 w-8 shrink-0 items-center justify-center rounded-md",
"text-nb-gray-300 hover:bg-nb-gray-910 hover:text-nb-gray-100", "text-nb-gray-300 hover:bg-nb-gray-910 hover:text-nb-gray-100",
"transition-colors outline-none cursor-default", "cursor-default outline-none transition-colors",
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
"wails-no-draggable", "wails-no-draggable",
)} )}
> >
<ArrowLeftIcon size={16} aria-hidden="true" /> <ArrowLeftIcon size={16} aria-hidden={"true"} />
</button> </button>
<Tooltip content={t(peerStatusLabelKey(selected.connStatus))} side={"top"}> <Tooltip content={t(peerStatusLabelKey(selected.connStatus))} side={"top"}>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"h-2 w-2 rounded-full shrink-0", "h-2 w-2 shrink-0 rounded-full",
dotClass(selected.connStatus), dotClass(selected.connStatus),
)} )}
/> />
@@ -205,12 +206,12 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => {
<CopyToClipboard <CopyToClipboard
message={selected.fqdn || selected.ip} message={selected.fqdn || selected.ip}
size={11} size={11}
className={"flex-1 min-w-0"} className={"min-w-0 flex-1"}
iconClassName={"top-[2px]"} iconClassName={"top-[2px]"}
> >
<span <span
id={"nb-peer-detail-title"} id={"nb-peer-detail-title"}
className={"text-sm font-medium text-nb-gray-100 truncate"} className={"truncate text-sm font-medium text-nb-gray-100"}
> >
{shortenDns(selected.fqdn) || selected.ip} {shortenDns(selected.fqdn) || selected.ip}
</span> </span>
@@ -224,9 +225,9 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => {
aria-label={t("peers.details.refresh")} aria-label={t("peers.details.refresh")}
aria-busy={refreshing} aria-busy={refreshing}
className={cn( className={cn(
"shrink-0 h-8 w-8 rounded-md flex items-center justify-center", "flex h-8 w-8 shrink-0 items-center justify-center rounded-md",
"text-nb-gray-300 hover:bg-nb-gray-910 hover:text-nb-gray-100", "text-nb-gray-300 hover:bg-nb-gray-910 hover:text-nb-gray-100",
"transition-colors outline-none cursor-default", "cursor-default outline-none transition-colors",
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
"wails-no-draggable", "wails-no-draggable",
"disabled:opacity-50 disabled:hover:bg-transparent", "disabled:opacity-50 disabled:hover:bg-transparent",
@@ -234,26 +235,26 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => {
> >
<RefreshCwIcon <RefreshCwIcon
size={14} size={14}
aria-hidden="true" aria-hidden={"true"}
className={refreshing ? "animate-spin" : undefined} className={refreshing ? "animate-spin" : undefined}
/> />
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
<ScrollArea.Root type={"auto"} className={"flex-1 min-h-0 overflow-hidden"}> <ScrollArea.Root type={"auto"} className={"min-h-0 flex-1 overflow-hidden"}>
<ScrollArea.Viewport className={"h-full w-full"}> <ScrollArea.Viewport className={"h-full w-full"}>
<PeerDetails peer={selected} now={now} /> <PeerDetails peer={selected} now={now} />
</ScrollArea.Viewport> </ScrollArea.Viewport>
<ScrollArea.Scrollbar <ScrollArea.Scrollbar
orientation={"vertical"} orientation={"vertical"}
className={cn( className={cn(
"flex select-none touch-none transition-colors", "flex touch-none select-none transition-colors",
"w-1.5 bg-transparent py-1", "w-1.5 bg-transparent py-1",
)} )}
> >
<ScrollArea.Thumb <ScrollArea.Thumb
className={ className={
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative" "relative flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700"
} }
/> />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
@@ -298,7 +299,7 @@ const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => {
<CopyToClipboard <CopyToClipboard
message={peer.ipv6} message={peer.ipv6}
alwaysShowIcon alwaysShowIcon
className={"max-w-full min-w-0"} className={"min-w-0 max-w-full"}
iconClassName={"top-0"} iconClassName={"top-0"}
> >
<TruncatedRowValue value={peer.ipv6} mono /> <TruncatedRowValue value={peer.ipv6} mono />
@@ -316,7 +317,7 @@ const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => {
<CopyToClipboard <CopyToClipboard
message={peer.relayAddress} message={peer.relayAddress}
alwaysShowIcon alwaysShowIcon
className={"max-w-full min-w-0"} className={"min-w-0 max-w-full"}
iconClassName={"top-0"} iconClassName={"top-0"}
> >
<TruncatedRowValue value={peer.relayAddress} mono /> <TruncatedRowValue value={peer.relayAddress} mono />
@@ -337,20 +338,24 @@ const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => {
<Row icon={ArrowUpDownIcon} label={t("peers.details.bytes")}> <Row icon={ArrowUpDownIcon} label={t("peers.details.bytes")}>
<div <div
className={ className={
"flex items-center gap-3 justify-end text-nb-gray-300 font-medium" "flex items-center justify-end gap-3 font-medium text-nb-gray-300"
} }
> >
<div className={"flex gap-1.5 items-center whitespace-nowrap"}> <div className={"flex items-center gap-1.5 whitespace-nowrap"}>
<ArrowDownIcon <ArrowDownIcon
size={13} size={13}
aria-hidden="true" aria-hidden={"true"}
className={"text-sky-400"} className={"text-sky-400"}
/> />
<span className={"sr-only"}>{t("peers.details.bytesReceived")}:</span> <span className={"sr-only"}>{t("peers.details.bytesReceived")}:</span>
<span className={"tabular-nums"}>{formatBytes(peer.bytesRx)}</span> <span className={"tabular-nums"}>{formatBytes(peer.bytesRx)}</span>
</div> </div>
<div className={"flex gap-1.5 items-center whitespace-nowrap"}> <div className={"flex items-center gap-1.5 whitespace-nowrap"}>
<ArrowUpIcon size={13} aria-hidden="true" className={"text-netbird"} /> <ArrowUpIcon
size={13}
aria-hidden={"true"}
className={"text-netbird"}
/>
<span className={"sr-only"}>{t("peers.details.bytesSent")}:</span> <span className={"sr-only"}>{t("peers.details.bytesSent")}:</span>
<span className={"tabular-nums"}>{formatBytes(peer.bytesTx)}</span> <span className={"tabular-nums"}>{formatBytes(peer.bytesTx)}</span>
</div> </div>
@@ -385,7 +390,7 @@ const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => {
<CopyToClipboard <CopyToClipboard
message={peer.pubKey} message={peer.pubKey}
alwaysShowIcon alwaysShowIcon
className={"max-w-full min-w-0"} className={"min-w-0 max-w-full"}
iconClassName={"top-0"} iconClassName={"top-0"}
> >
<TruncatedRowValue value={peer.pubKey} mono /> <TruncatedRowValue value={peer.pubKey} mono />
@@ -423,7 +428,7 @@ const IceRow = ({ icon, baseLabel, type, endpoint }: IceRowProps) => {
<CopyToClipboard <CopyToClipboard
message={endpoint} message={endpoint}
alwaysShowIcon alwaysShowIcon
className={"max-w-full min-w-0"} className={"min-w-0 max-w-full"}
iconClassName={"top-0"} iconClassName={"top-0"}
> >
<TruncatedRowValue value={endpoint} mono /> <TruncatedRowValue value={endpoint} mono />
@@ -448,10 +453,10 @@ const ResourcesPopover = ({ networks }: { networks: string[] }) => {
<button <button
type={"button"} type={"button"}
tabIndex={0} tabIndex={0}
aria-haspopup="dialog" aria-haspopup={"dialog"}
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
"shrink-0 inline-flex items-center gap-1 rounded", "inline-flex shrink-0 items-center gap-1 rounded",
"bg-nb-gray-930 hover:bg-nb-gray-910/80 data-[state=open]:bg-nb-gray-910", "bg-nb-gray-930 hover:bg-nb-gray-910/80 data-[state=open]:bg-nb-gray-910",
"border border-nb-gray-900", "border border-nb-gray-900",
"px-2 py-1 text-xs font-medium text-nb-gray-300", "px-2 py-1 text-xs font-medium text-nb-gray-300",
@@ -462,7 +467,7 @@ const ResourcesPopover = ({ networks }: { networks: string[] }) => {
{networks.length} {networks.length}
<ChevronDownIcon <ChevronDownIcon
size={12} size={12}
aria-hidden="true" aria-hidden={"true"}
className={cn("transition-transform duration-150", open && "rotate-180")} className={cn("transition-transform duration-150", open && "rotate-180")}
/> />
</button> </button>
@@ -474,7 +479,7 @@ const ResourcesPopover = ({ networks }: { networks: string[] }) => {
sideOffset={6} sideOffset={6}
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
className={cn( className={cn(
"z-50 max-w-[18rem] max-h-72 overflow-auto", "z-50 max-h-72 max-w-[18rem] overflow-auto",
"rounded-lg border border-nb-gray-900 bg-nb-gray-935", "rounded-lg border border-nb-gray-900 bg-nb-gray-935",
"p-2 pr-4 shadow-lg outline-none", "p-2 pr-4 shadow-lg outline-none",
)} )}
@@ -485,7 +490,7 @@ const ResourcesPopover = ({ networks }: { networks: string[] }) => {
<CopyToClipboard message={n} className={"px-1 py-0.5"}> <CopyToClipboard message={n} className={"px-1 py-0.5"}>
<span <span
className={ className={
"font-mono text-[0.72rem] text-nb-gray-200 whitespace-nowrap" "whitespace-nowrap font-mono text-[0.72rem] text-nb-gray-200"
} }
> >
{n} {n}
@@ -504,25 +509,25 @@ const TruncatedRowValue = ({ value, mono }: { value: string; mono?: boolean }) =
<TruncatedText <TruncatedText
text={value} text={value}
className={cn( className={cn(
"inline-block truncate align-middle min-w-0 max-w-[260px]", "inline-block min-w-0 max-w-[260px] truncate align-middle",
mono && "font-mono", mono && "font-mono",
)} )}
/> />
); );
const Row = ({ icon: Icon, iconClassName, label, children }: RowProps) => ( const Row = ({ icon: Icon, iconClassName, label, children }: RowProps) => (
<li className={"flex items-center gap-2 px-5 py-4 text-xs text-nb-gray-100 min-w-0"}> <li className={"flex min-w-0 items-center gap-2 px-5 py-4 text-xs text-nb-gray-100"}>
<Icon <Icon
size={14} size={14}
aria-hidden="true" aria-hidden={"true"}
className={cn("text-nb-gray-100 shrink-0", iconClassName)} className={cn("shrink-0 text-nb-gray-100", iconClassName)}
/> />
<span className={"text-nb-gray-200 shrink-0 font-semibold"}>{label}</span> <span className={"shrink-0 font-semibold text-nb-gray-200"}>{label}</span>
<span <span
className={cn( className={cn(
"min-w-0 flex-1 text-right pl-8", "min-w-0 flex-1 pl-8 text-right",
"text-nb-gray-350 font-medium", "font-medium text-nb-gray-350",
"flex justify-end items-center", "flex items-center justify-end",
)} )}
> >
{children} {children}
@@ -48,11 +48,11 @@ export const PeerFilters = ({ value, onChange, counts, disabled }: Props) => {
"wails-no-draggable cursor-default", "wails-no-draggable cursor-default",
)} )}
> >
<ListFilter size={14} aria-hidden="true" className={"shrink-0"} /> <ListFilter size={14} aria-hidden={"true"} className={"shrink-0"} />
<span> <span>
{active.label} <span className={"tabular-nums"}>({counts[active.value]})</span> {active.label} <span className={"tabular-nums"}>({counts[active.value]})</span>
</span> </span>
<ChevronDown size={14} aria-hidden="true" className={"ml-0.5 shrink-0"} /> <ChevronDown size={14} aria-hidden={"true"} className={"ml-0.5 shrink-0"} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align={"end"} className={"min-w-[10rem]"}> <DropdownMenuContent align={"end"} className={"min-w-[10rem]"}>
{filters.map((f) => { {filters.map((f) => {
@@ -61,7 +61,7 @@ export const PeerFilters = ({ value, onChange, counts, disabled }: Props) => {
<DropdownMenuItem <DropdownMenuItem
key={f.value} key={f.value}
onClick={() => handleSelect(f.value)} onClick={() => handleSelect(f.value)}
role="menuitemradio" role={"menuitemradio"}
aria-checked={checked} aria-checked={checked}
className={"gap-2"} className={"gap-2"}
> >
@@ -70,7 +70,7 @@ export const PeerFilters = ({ value, onChange, counts, disabled }: Props) => {
<span className={"tabular-nums"}>({counts[f.value]})</span> <span className={"tabular-nums"}>({counts[f.value]})</span>
</span> </span>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={"w-4 shrink-0 flex items-center justify-center"} className={"w-4 shrink-0 flex items-center justify-center"}
> >
{checked && <CheckIcon size={14} className={"text-netbird"} />} {checked && <CheckIcon size={14} className={"text-netbird"} />}
@@ -1,7 +1,7 @@
import { KeyboardEvent, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { type KeyboardEvent, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { ChevronRightIcon, MonitorSmartphoneIcon } from "lucide-react"; import { ChevronRightIcon, MonitorSmartphoneIcon } from "lucide-react";
import type { PeerStatus } from "@bindings/services/models.js"; import type { PeerStatus } from "@bindings/services/models.js";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
@@ -15,7 +15,7 @@ import { useStatus } from "@/contexts/StatusContext";
import { usePeerDetail } from "@/contexts/PeerDetailContext"; import { usePeerDetail } from "@/contexts/PeerDetailContext";
import { Tooltip } from "@/components/Tooltip"; import { Tooltip } from "@/components/Tooltip";
import { TruncatedText } from "@/components/TruncatedText"; import { TruncatedText } from "@/components/TruncatedText";
import { PeerFilters, StatusFilter } from "./PeerFilters"; import { PeerFilters, type StatusFilter } from "./PeerFilters";
const isOnline = (connStatus: string) => connStatus === "Connected"; const isOnline = (connStatus: string) => connStatus === "Connected";
@@ -54,7 +54,7 @@ export const Peers = () => {
}, []); }, []);
const isConnected = status?.status === "Connected"; const isConnected = status?.status === "Connected";
const peers = status?.peers ?? []; const peers = useMemo(() => status?.peers ?? [], [status?.peers]);
const counts = useMemo<Record<StatusFilter, number>>(() => { const counts = useMemo<Record<StatusFilter, number>>(() => {
const online = peers.filter((p) => isOnline(p.connStatus)).length; const online = peers.filter((p) => isOnline(p.connStatus)).length;
@@ -120,9 +120,9 @@ export const Peers = () => {
} }
return ( return (
<div className={"flex flex-col w-full h-full min-h-0"}> <div className={"flex h-full min-h-0 w-full flex-col"}>
<div className={"flex items-center gap-2 px-6 py-2.5 border-b border-nb-gray-910"}> <div className={"flex items-center gap-2 border-b border-nb-gray-910 px-6 py-2.5"}>
<div className={"flex-1 min-w-0"}> <div className={"min-w-0 flex-1"}>
<SearchInput <SearchInput
ref={searchRef} ref={searchRef}
placeholder={t("peers.search.placeholder")} placeholder={t("peers.search.placeholder")}
@@ -135,20 +135,20 @@ export const Peers = () => {
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<NoResults /> <NoResults />
) : ( ) : (
<ScrollArea.Root type={"auto"} className={"flex-1 min-h-0 overflow-hidden"}> <ScrollArea.Root type={"auto"} className={"min-h-0 flex-1 overflow-hidden"}>
<ScrollArea.Viewport ref={setScrollParent} className={"h-full w-full"}> <ScrollArea.Viewport ref={setScrollParent} className={"h-full w-full"}>
{scrollParent && <PeersList data={filtered} scrollParent={scrollParent} />} {scrollParent && <PeersList data={filtered} scrollParent={scrollParent} />}
</ScrollArea.Viewport> </ScrollArea.Viewport>
<ScrollArea.Scrollbar <ScrollArea.Scrollbar
orientation={"vertical"} orientation={"vertical"}
className={cn( className={cn(
"flex select-none touch-none transition-colors", "flex touch-none select-none transition-colors",
"w-1.5 bg-transparent py-1", "w-1.5 bg-transparent py-1",
)} )}
> >
<ScrollArea.Thumb <ScrollArea.Thumb
className={ className={
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative" "relative flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700"
} }
/> />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
@@ -273,8 +273,8 @@ const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps)
return ( return (
<div <div
className={cn( className={cn(
"group relative flex items-start gap-2.5 pl-6 pr-4 py-3 min-w-0", "group relative flex min-w-0 items-start gap-2.5 py-3 pl-6 pr-4",
"hover:bg-nb-gray-900/40 transition-colors", "transition-colors hover:bg-nb-gray-900/40",
"wails-no-draggable", "wails-no-draggable",
)} )}
> >
@@ -292,16 +292,16 @@ const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps)
/> />
<Tooltip content={statusLabel} side={"left"}> <Tooltip content={statusLabel} side={"left"}>
<span <span
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"h-2 w-2 rounded-full shrink-0 mt-2 relative", "relative mt-2 h-2 w-2 shrink-0 rounded-full",
dotClass(peer.connStatus), dotClass(peer.connStatus),
)} )}
/> />
</Tooltip> </Tooltip>
<div <div
className={ className={
"min-w-0 flex-1 flex flex-col leading-tight relative pointer-events-none" "pointer-events-none relative flex min-w-0 flex-1 flex-col leading-tight"
} }
> >
<div> <div>
@@ -313,7 +313,7 @@ const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps)
<TruncatedText <TruncatedText
text={shortenDns(peer.fqdn)} text={shortenDns(peer.fqdn)}
className={ className={
"block text-[0.81rem] font-medium text-nb-gray-100 truncate max-w-[300px]" "block max-w-[300px] truncate text-[0.81rem] font-medium text-nb-gray-100"
} }
/> />
</CopyToClipboard> </CopyToClipboard>
@@ -324,7 +324,7 @@ const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps)
className={"pointer-events-auto"} className={"pointer-events-auto"}
onKeyDown={handleKey} onKeyDown={handleKey}
> >
<span className={"text-xs font-mono text-nb-gray-400 truncate"}> <span className={"truncate font-mono text-xs text-nb-gray-400"}>
{peer.ip} {peer.ip}
</span> </span>
</CopyToClipboard> </CopyToClipboard>
@@ -333,7 +333,7 @@ const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps)
{isConnected && peer.latencyMs > 0 && ( {isConnected && peer.latencyMs > 0 && (
<span <span
className={cn( className={cn(
"shrink-0 self-center text-xs tabular-nums relative pointer-events-none", "pointer-events-none relative shrink-0 self-center text-xs tabular-nums",
latencyColor(peer.latencyMs), latencyColor(peer.latencyMs),
)} )}
> >
@@ -342,10 +342,10 @@ const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps)
)} )}
<ChevronRightIcon <ChevronRightIcon
size={16} size={16}
aria-hidden="true" aria-hidden={"true"}
className={cn( className={cn(
"shrink-0 self-center text-nb-gray-300 relative pointer-events-none", "pointer-events-none relative shrink-0 self-center text-nb-gray-300",
"opacity-0 group-hover:opacity-100 transition-opacity", "opacity-0 transition-opacity group-hover:opacity-100",
)} )}
/> />
</div> </div>
@@ -1,4 +1,4 @@
import { ButtonHTMLAttributes, forwardRef } from "react"; import { type ButtonHTMLAttributes, forwardRef } from "react";
import { import {
Briefcase, Briefcase,
Building, Building,
@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useId, useRef, useState } from "react"; import { type FormEvent, useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as Dialog from "@/components/dialog/Dialog"; import * as Dialog from "@/components/dialog/Dialog";
import { Input } from "@/components/inputs/Input"; import { Input } from "@/components/inputs/Input";
@@ -125,16 +125,16 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
return ( return (
<Dialog.Root open={open} onOpenChange={onOpenChange}> <Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content <Dialog.Content
maxWidthClass="max-w-md" maxWidthClass={"max-w-md"}
showClose={false} showClose={false}
className="py-7" className={"py-7"}
srTitle={t("profile.dialog.title")} srTitle={t("profile.dialog.title")}
srDescription={t("profile.dialog.description")} srDescription={t("profile.dialog.description")}
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
> >
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="flex flex-col gap-6 px-7"> <div className={"flex flex-col gap-6 px-7"}>
<div className="flex flex-col gap-2"> <div className={"flex flex-col gap-2"}>
<div className={"pl-1"}> <div className={"pl-1"}>
<Label htmlFor={nameId} className={"mb-0.5"}> <Label htmlFor={nameId} className={"mb-0.5"}>
{t("profile.dialog.nameLabel")} {t("profile.dialog.nameLabel")}
@@ -153,13 +153,13 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
error={nameError ?? undefined} error={nameError ?? undefined}
maxLength={MAX_PROFILE_NAME_LEN} maxLength={MAX_PROFILE_NAME_LEN}
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete={"off"}
autoCapitalize="off" autoCapitalize={"off"}
/> />
</div> </div>
{!managedManagementUrl && ( {!managedManagementUrl && (
<div className="flex flex-col gap-2"> <div className={"flex flex-col gap-2"}>
<div className={"pl-1"}> <div className={"pl-1"}>
<Label as={"div"} className={"mb-0.5"}> <Label as={"div"} className={"mb-0.5"}>
{t("settings.general.management.label")} {t("settings.general.management.label")}
@@ -168,7 +168,7 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
{t("profile.dialog.managementHelp")} {t("profile.dialog.managementHelp")}
</HelpText> </HelpText>
</div> </div>
<div className="flex flex-col gap-3"> <div className={"flex flex-col gap-3"}>
<ManagementServerSwitch <ManagementServerSwitch
value={mode} value={mode}
onChange={setMode} onChange={setMode}
@@ -188,9 +188,9 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
error={urlInputError} error={urlInputError}
warning={urlInputWarning} warning={urlInputWarning}
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete={"off"}
autoCorrect="off" autoCorrect={"off"}
autoCapitalize="off" autoCapitalize={"off"}
/> />
)} )}
</div> </div>
@@ -199,7 +199,7 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
<DialogActions className={"flex-row items-center justify-end gap-2.5 pt-2"}> <DialogActions className={"flex-row items-center justify-end gap-2.5 pt-2"}>
<Button <Button
type="button" type={"button"}
variant={"secondary"} variant={"secondary"}
size={"xs2"} size={"xs2"}
disabled={checking} disabled={checking}
@@ -208,7 +208,7 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
{t("common.cancel")} {t("common.cancel")}
</Button> </Button>
<Button <Button
type="submit" type={"submit"}
variant={"primary"} variant={"primary"}
size={"xs2"} size={"xs2"}
loading={checking} loading={checking}
@@ -80,7 +80,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
</Popover.Trigger> </Popover.Trigger>
<Popover.Portal> <Popover.Portal>
<Popover.Content <Popover.Content
align="center" align={"center"}
sideOffset={8} sideOffset={8}
collisionPadding={12} collisionPadding={12}
onOpenAutoFocus={(e) => { onOpenAutoFocus={(e) => {
@@ -111,8 +111,8 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
> >
{sortedProfiles.length > 0 && ( {sortedProfiles.length > 0 && (
<> <>
<ScrollArea.Root type="auto" className="overflow-hidden -mx-1"> <ScrollArea.Root type={"auto"} className={"overflow-hidden -mx-1"}>
<ScrollArea.Viewport className="max-h-60 px-1"> <ScrollArea.Viewport className={"max-h-60 px-1"}>
{sortedProfiles.map((profile) => ( {sortedProfiles.map((profile) => (
<ProfileRow <ProfileRow
key={profile.id} key={profile.id}
@@ -123,16 +123,16 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
))} ))}
</ScrollArea.Viewport> </ScrollArea.Viewport>
<ScrollArea.Scrollbar <ScrollArea.Scrollbar
orientation="vertical" orientation={"vertical"}
className={cn( className={cn(
"flex select-none touch-none transition-colors", "flex select-none touch-none transition-colors",
"w-1.5 bg-transparent", "w-1.5 bg-transparent",
)} )}
> >
<ScrollArea.Thumb className="flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative" /> <ScrollArea.Thumb className={"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"} />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
</ScrollArea.Root> </ScrollArea.Root>
<div className="-mx-1 h-px bg-nb-gray-910" /> <div className={"-mx-1 h-px bg-nb-gray-910"} />
</> </>
)} )}
@@ -148,8 +148,8 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
"data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none", "data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none",
)} )}
> >
<Settings2 size={14} aria-hidden="true" className="shrink-0" /> <Settings2 size={14} aria-hidden={"true"} className={"shrink-0"} />
<span className="truncate flex-1"> <span className={"truncate flex-1"}>
{t("profile.dropdown.manageProfiles")} {t("profile.dropdown.manageProfiles")}
</span> </span>
</Command.Item> </Command.Item>
@@ -164,16 +164,16 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
const ProfileTriggerSkeleton = () => ( const ProfileTriggerSkeleton = () => (
<div <div
role="status" role={"status"}
aria-busy="true" aria-busy={"true"}
aria-live="polite" aria-live={"polite"}
className="h-10 flex items-center gap-2 px-3 rounded-lg select-none wails-no-draggable" className={"h-10 flex items-center gap-2 px-3 rounded-lg select-none wails-no-draggable"}
> >
<div <div
aria-hidden="true" aria-hidden={"true"}
className="size-4 rounded-full bg-nb-gray-900 animate-pulse shrink-0" className={"size-4 rounded-full bg-nb-gray-900 animate-pulse shrink-0"}
/> />
<div aria-hidden="true" className="h-4 w-24 rounded bg-nb-gray-900 animate-pulse" /> <div aria-hidden={"true"} className={"h-4 w-24 rounded bg-nb-gray-900 animate-pulse"} />
</div> </div>
); );
@@ -188,11 +188,11 @@ const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonP
return ( return (
<button <button
ref={ref} ref={ref}
type="button" type={"button"}
disabled={disabled} disabled={disabled}
tabIndex={disabled ? -1 : 0} tabIndex={disabled ? -1 : 0}
aria-label={t("header.profile.switch")} aria-label={t("header.profile.switch")}
aria-haspopup="listbox" aria-haspopup={"listbox"}
className={cn( className={cn(
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default select-none wails-no-draggable", "h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default select-none wails-no-draggable",
"text-nb-gray-200 hover:bg-nb-gray-900", "text-nb-gray-200 hover:bg-nb-gray-900",
@@ -206,7 +206,7 @@ const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonP
> >
<Icon <Icon
size={16} size={16}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-200 shrink-0 wails-no-draggable"} className={"text-nb-gray-200 shrink-0 wails-no-draggable"}
/> />
<span className={"text-sm font-medium truncate max-w-[140px] wails-no-draggable"}> <span className={"text-sm font-medium truncate max-w-[140px] wails-no-draggable"}>
@@ -214,7 +214,7 @@ const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonP
</span> </span>
<ChevronDown <ChevronDown
size={14} size={14}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-200 shrink-0 wails-no-draggable"} className={"text-nb-gray-200 shrink-0 wails-no-draggable"}
/> />
</button> </button>
@@ -241,14 +241,14 @@ const ProfileRow = ({ profile, isActive, onSelect }: ProfileRowProps) => {
showEmail ? "items-start" : "items-center", showEmail ? "items-start" : "items-center",
)} )}
> >
<div className="flex flex-col min-w-0 flex-1 leading-tight"> <div className={"flex flex-col min-w-0 flex-1 leading-tight"}>
<span className="truncate">{profile.name}</span> <span className={"truncate"}>{profile.name}</span>
{showEmail && <TruncatedEmail email={profile.email} />} {showEmail && <TruncatedEmail email={profile.email} />}
</div> </div>
{isActive && ( {isActive && (
<Check <Check
size={16} size={16}
aria-hidden="true" aria-hidden={"true"}
className={cn("shrink-0 text-netbird", showEmail && "mt-0.5")} className={cn("shrink-0 text-netbird", showEmail && "mt-0.5")}
/> />
)} )}
@@ -267,7 +267,7 @@ const TruncatedEmail = ({ email }: { email: string }) => {
}, [email]); }, [email]);
const span = ( const span = (
<span ref={ref} className="text-xs mt-0.5 text-nb-gray-300 truncate max-w-[180px]"> <span ref={ref} className={"text-xs mt-0.5 text-nb-gray-300 truncate max-w-[180px]"}>
{email} {email}
</span> </span>
); );
@@ -1,4 +1,4 @@
import { KeyboardEvent, useLayoutEffect, useMemo, useRef, useState } from "react"; import { type KeyboardEvent, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CircleMinus, LogIn, PlusCircle, Trash2, UserCircle } from "lucide-react"; import { CircleMinus, LogIn, PlusCircle, Trash2, UserCircle } from "lucide-react";
import type { Profile } from "@bindings/services/models.js"; import type { Profile } from "@bindings/services/models.js";
@@ -138,13 +138,11 @@ export function ProfilesTab() {
{loaded && ordered.length === 0 && ( {loaded && ordered.length === 0 && (
<div <div
className={ className={"flex flex-col items-center justify-center py-10 text-center"}
"flex flex-col items-center justify-center py-10 text-center"
}
> >
<UserCircle <UserCircle
size={28} size={28}
aria-hidden="true" aria-hidden={"true"}
className={"text-nb-gray-500 mb-2"} className={"text-nb-gray-500 mb-2"}
/> />
<p className={"text-sm font-semibold text-nb-gray-200"}> <p className={"text-sm font-semibold text-nb-gray-200"}>
@@ -159,7 +157,7 @@ export function ProfilesTab() {
<SettingsBottomBar> <SettingsBottomBar>
<Button variant={"primary"} size={"md"} onClick={() => setNewOpen(true)}> <Button variant={"primary"} size={"md"} onClick={() => setNewOpen(true)}>
<PlusCircle size={14} aria-hidden="true" /> <PlusCircle size={14} aria-hidden={"true"} />
{t("settings.profiles.addProfile")} {t("settings.profiles.addProfile")}
</Button> </Button>
</SettingsBottomBar> </SettingsBottomBar>
@@ -355,15 +353,13 @@ const ProfileRow = ({
> >
<Icon <Icon
size={15} size={15}
aria-hidden="true" aria-hidden={"true"}
className={cn("text-nb-gray-200 shrink-0", showEmail ? "mt-0.5" : "")} className={cn("text-nb-gray-200 shrink-0", showEmail ? "mt-0.5" : "")}
/> />
<div className={"flex flex-col min-w-0 flex-1 leading-tight"}> <div className={"flex flex-col min-w-0 flex-1 leading-tight"}>
<div className={"flex items-center gap-2 min-w-0"}> <div className={"flex items-center gap-2 min-w-0"}>
<span <span
className={ className={"truncate font-medium text-nb-gray-100 select-text cursor-text"}
"truncate font-medium text-nb-gray-100 select-text cursor-text"
}
> >
{profile.name} {profile.name}
</span> </span>
@@ -505,7 +501,7 @@ const ActionIconButton = ({
"opacity-40 cursor-not-allowed hover:!text-nb-gray-400 hover:!bg-transparent", "opacity-40 cursor-not-allowed hover:!text-nb-gray-400 hover:!bg-transparent",
)} )}
> >
<Icon size={16} aria-hidden="true" /> <Icon size={16} aria-hidden={"true"} />
</button> </button>
); );
if (hidden) return button; if (hidden) return button;
@@ -167,9 +167,7 @@ export default function SessionExpirationDialog() {
{!expired && ( {!expired && (
<div <div
className={ className={"font-mono font-semibold text-2xl tabular-nums text-nb-gray-50 tracking-wider"}
"font-mono font-semibold text-2xl tabular-nums text-nb-gray-50 tracking-wider"
}
aria-live={"polite"} aria-live={"polite"}
> >
{formatRemaining(remaining)} {formatRemaining(remaining)}
@@ -8,18 +8,14 @@ import netbirdFull from "@/assets/logos/netbird-full.svg";
const GithubIcon = (props: SVGProps<SVGSVGElement>) => ( const GithubIcon = (props: SVGProps<SVGSVGElement>) => (
<svg viewBox={"0 0 24 24"} fill={"currentColor"} {...props}> <svg viewBox={"0 0 24 24"} fill={"currentColor"} {...props}>
<path <path
d={ d={"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"}
"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
}
/> />
</svg> </svg>
); );
const SlackIcon = (props: SVGProps<SVGSVGElement>) => ( const SlackIcon = (props: SVGProps<SVGSVGElement>) => (
<svg viewBox={"0 0 24 24"} fill={"currentColor"} {...props}> <svg viewBox={"0 0 24 24"} fill={"currentColor"} {...props}>
<path <path
d={ d={"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"}
"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"
}
/> />
</svg> </svg>
); );
@@ -86,18 +82,14 @@ export function SettingsAbout() {
return ( return (
<div <div
className={ className={"flex flex-col items-center justify-center gap-4 max-w-2xl mx-auto min-h-[calc(100vh-12rem)]"}
"flex flex-col items-center justify-center gap-4 max-w-2xl mx-auto min-h-[calc(100vh-12rem)]"
}
> >
<img src={netbirdFull} alt={t("common.netbird")} className={"h-7 w-auto"} /> <img src={netbirdFull} alt={t("common.netbird")} className={"h-7 w-auto"} />
<div className={"flex flex-col items-center gap-0.5 text-center"}> <div className={"flex flex-col items-center gap-0.5 text-center"}>
<button <button
type={"button"} type={"button"}
onClick={handleVersionClick} onClick={handleVersionClick}
className={ className={"text-sm font-semibold text-nb-gray-100 cursor-text select-text bg-transparent outline-none"}
"text-sm font-semibold text-nb-gray-100 cursor-text select-text bg-transparent outline-none"
}
> >
{daemonVersion === "development" ? ( {daemonVersion === "development" ? (
<span> <span>
@@ -138,11 +130,9 @@ export function SettingsAbout() {
type={"button"} type={"button"}
tabIndex={0} tabIndex={0}
onClick={() => openUrl(url)} onClick={() => openUrl(url)}
className={ className={"inline-flex items-center gap-1.5 rounded-sm outline-none decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"}
"inline-flex items-center gap-1.5 rounded-sm outline-none decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
}
> >
<Icon aria-hidden="true" className={iconClassName ?? "h-3.5 w-3.5"} /> <Icon aria-hidden={"true"} className={iconClassName ?? "h-3.5 w-3.5"} />
<span>{label}</span> <span>{label}</span>
</button> </button>
))} ))}
@@ -156,9 +146,7 @@ export function SettingsAbout() {
type={"button"} type={"button"}
tabIndex={0} tabIndex={0}
onClick={() => openUrl(link.url)} onClick={() => openUrl(link.url)}
className={ className={"rounded-sm outline-none decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"}
"rounded-sm outline-none decoration-[0.5px] underline-offset-4 hover:text-nb-gray-100 hover:underline transition focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
}
> >
{link.label} {link.label}
</button> </button>
@@ -106,9 +106,9 @@ export function SettingsAdvanced() {
error={errors.interfaceName} error={errors.interfaceName}
onChange={(e) => setValues((v) => ({ ...v, interfaceName: e.target.value }))} onChange={(e) => setValues((v) => ({ ...v, interfaceName: e.target.value }))}
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete={"off"}
autoCorrect="off" autoCorrect={"off"}
autoCapitalize="off" autoCapitalize={"off"}
/> />
<div className={mdm.wireguardPort ? "" : "grid grid-cols-2 gap-4"}> <div className={mdm.wireguardPort ? "" : "grid grid-cols-2 gap-4"}>
{!mdm.wireguardPort && ( {!mdm.wireguardPort && (
@@ -156,9 +156,9 @@ export function SettingsAdvanced() {
value={pskInputValue} value={pskInputValue}
onChange={(e) => setPskInputValue(e.target.value)} onChange={(e) => setPskInputValue(e.target.value)}
spellCheck={false} spellCheck={false}
autoComplete="new-password" autoComplete={"new-password"}
autoCorrect="off" autoCorrect={"off"}
autoCapitalize="off" autoCapitalize={"off"}
/> />
</div> </div>
</SectionGroup> </SectionGroup>
@@ -90,9 +90,9 @@ export function SettingsGeneral() {
: undefined : undefined
} }
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete={"off"}
autoCorrect="off" autoCorrect={"off"}
autoCapitalize="off" autoCapitalize={"off"}
/> />
<Button <Button
variant={"primary"} variant={"primary"}
@@ -114,9 +114,7 @@ export const SettingsPage = () => {
)} )}
> >
<ScrollArea.Thumb <ScrollArea.Thumb
className={ className={"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"}
"flex-1 rounded-full bg-nb-gray-800 hover:bg-nb-gray-700 relative"
}
/> />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
</ScrollArea.Root> </ScrollArea.Root>
@@ -29,12 +29,10 @@ export const SectionGroup = ({
export const SettingsBottomBar = ({ children }: { children: ReactNode }) => ( export const SettingsBottomBar = ({ children }: { children: ReactNode }) => (
<> <>
<div className={"h-[3.2rem] shrink-0"} aria-hidden="true" /> <div className={"h-[3.2rem] shrink-0"} aria-hidden={"true"} />
<div className={"absolute bottom-0 left-0 w-full"}> <div className={"absolute bottom-0 left-0 w-full"}>
<div <div
className={ className={"w-full flex justify-end gap-3 px-8 py-5 border-t border-nb-gray-920 bg-nb-gray-940"}
"w-full flex justify-end gap-3 px-8 py-5 border-t border-nb-gray-920 bg-nb-gray-940"
}
> >
{children} {children}
</div> </div>
@@ -136,9 +136,7 @@ export function SettingsTroubleshooting() {
function CenteredPanel({ children }: Readonly<{ children: ReactNode }>) { function CenteredPanel({ children }: Readonly<{ children: ReactNode }>) {
return ( return (
<div <div
className={ className={"absolute inset-0 flex flex-col items-center justify-center gap-5 p-8 text-center"}
"absolute inset-0 flex flex-col items-center justify-center gap-5 p-8 text-center"
}
> >
{children} {children}
</div> </div>
@@ -164,9 +162,7 @@ function ProgressSection({
{stage.kind === "capturing" && ( {stage.kind === "capturing" && (
<div <div
className={ className={"font-mono font-semibold text-2xl tabular-nums text-nb-gray-50 tracking-wider"}
"font-mono font-semibold text-2xl tabular-nums text-nb-gray-50 tracking-wider"
}
aria-live={"polite"} aria-live={"polite"}
> >
{formatRemaining(stage.remainingSec)} {formatRemaining(stage.remainingSec)}
@@ -263,7 +259,7 @@ function DoneResult({
className={"pointer-events-auto hover:text-white transition-all"} className={"pointer-events-auto hover:text-white transition-all"}
aria-label={t("settings.troubleshooting.done.openFileLocation")} aria-label={t("settings.troubleshooting.done.openFileLocation")}
> >
<FolderOpen size={16} aria-hidden="true" /> <FolderOpen size={16} aria-hidden={"true"} />
</button> </button>
} }
/> />
@@ -271,10 +267,8 @@ function DoneResult({
{uploadFailed && ( {uploadFailed && (
<div <div
role="alert" role={"alert"}
className={ className={"rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300"}
"rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300"
}
> >
{result.uploadFailureReason {result.uploadFailureReason
? t("settings.troubleshooting.uploadFailedWithReason", { ? t("settings.troubleshooting.uploadFailedWithReason", {
@@ -305,7 +299,7 @@ function DoneResult({
className={"w-full"} className={"w-full"}
onClick={onRevealPath} onClick={onRevealPath}
> >
<FolderOpen size={14} aria-hidden="true" /> <FolderOpen size={14} aria-hidden={"true"} />
{t("settings.troubleshooting.done.openFolder")} {t("settings.troubleshooting.done.openFolder")}
</Button> </Button>
) )
@@ -104,9 +104,9 @@ export function WelcomeStepManagement({
warning={inputWarning} warning={inputWarning}
autoFocus autoFocus
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete={"off"}
autoCorrect="off" autoCorrect={"off"}
autoCapitalize="off" autoCapitalize={"off"}
/> />
</div> </div>
)} )}