Explorar o código

refactor(ui): enhance code readability and consistency in QuickNav, Button, and Tooltip components

Code_Uwe hai 1 semana
pai
achega
14eb3b362c
Modificáronse 3 ficheiros con 123 adicións e 118 borrados
  1. 36 25
      components/app-shell/QuickNav.jsx
  2. 49 49
      components/ui/button.jsx
  3. 38 44
      components/ui/tooltip.jsx

+ 36 - 25
components/app-shell/QuickNav.jsx

@@ -51,8 +51,12 @@ const BRANCH_LIST_STATE = Object.freeze({
 	ERROR: "error",
 });
 
+// Header polish:
+// - remove subtle outline shadow (crisp header UI)
+// - normalize padding when an icon is present
 const TOPNAV_BUTTON_CLASS = "shadow-none has-[>svg]:px-3";
 
+// Active nav style (blue like multi-branch selection)
 const ACTIVE_NAV_BUTTON_CLASS =
 	"border-blue-600 bg-blue-50 text-blue-900 hover:bg-blue-50 " +
 	"dark:border-blue-900 dark:bg-blue-950 dark:text-blue-50 dark:hover:bg-blue-950";
@@ -67,11 +71,14 @@ export default function QuickNav() {
 	const isAdminDev =
 		isAuthenticated && (user.role === "admin" || user.role === "dev");
 	const isBranchUser = isAuthenticated && user.role === "branch";
-
 	const canRevalidate = typeof retry === "function";
 
+	// Persisted selection for admin/dev:
 	const [selectedBranch, setSelectedBranch] = React.useState(null);
 
+	// Init gate: prevent “localStorage sync” from running on every state change
+	const initRef = React.useRef({ userId: null });
+
 	const [branchList, setBranchList] = React.useState({
 		status: BRANCH_LIST_STATE.IDLE,
 		branches: null,
@@ -101,8 +108,13 @@ export default function QuickNav() {
 		!knownBranches.includes(routeBranch),
 	);
 
+	// A) Initialize selectedBranch once per authenticated admin/dev user
 	React.useEffect(() => {
-		if (!isAuthenticated) return;
+		if (!isAuthenticated) {
+			initRef.current.userId = null;
+			setSelectedBranch(null);
+			return;
+		}
 
 		if (isBranchUser) {
 			const own = user.branchId;
@@ -110,12 +122,19 @@ export default function QuickNav() {
 			return;
 		}
 
+		if (!isAdminDev) return;
+
+		const uid = String(user.userId || "");
+		if (!uid) return;
+
+		if (initRef.current.userId === uid) return;
+		initRef.current.userId = uid;
+
 		const fromStorage = safeReadLocalStorageBranch(STORAGE_KEY_LAST_BRANCH);
-		if (fromStorage && fromStorage !== selectedBranch) {
-			setSelectedBranch(fromStorage);
-		}
-	}, [isAuthenticated, isBranchUser, user?.branchId, selectedBranch]);
+		setSelectedBranch(fromStorage || null);
+	}, [isAuthenticated, isBranchUser, isAdminDev, user?.userId, user?.branchId]);
 
+	// B) Fetch branch list once for admin/dev
 	React.useEffect(() => {
 		if (!isAdminDev) return;
 
@@ -132,7 +151,6 @@ export default function QuickNav() {
 				setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
 			} catch (err) {
 				if (cancelled) return;
-
 				console.error("[QuickNav] getBranches failed:", err);
 				setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
 			}
@@ -143,38 +161,31 @@ export default function QuickNav() {
 		};
 	}, [isAdminDev, user?.userId]);
 
+	// C) Ensure selectedBranch is valid once we have the list
 	React.useEffect(() => {
 		if (!isAdminDev) return;
 		if (!knownBranches || knownBranches.length === 0) return;
 
-		if (!selectedBranch || !knownBranches.includes(selectedBranch)) {
-			const next = knownBranches[0];
-			setSelectedBranch(next);
-			safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, next);
-		}
+		if (selectedBranch && knownBranches.includes(selectedBranch)) return;
+
+		const next = knownBranches[0];
+		setSelectedBranch(next);
+		safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, next);
 	}, [isAdminDev, knownBranches, selectedBranch]);
 
+	// D) Sync selectedBranch to the current route branch ONLY if it exists
 	React.useEffect(() => {
 		if (!isAdminDev) return;
 		if (!routeBranch) return;
 		if (!knownBranches) return;
 
-		const isKnownRouteBranch = knownBranches.includes(routeBranch);
-		if (!isKnownRouteBranch) return;
+		if (!knownBranches.includes(routeBranch)) return;
+		if (routeBranch === selectedBranch) return;
 
-		if (routeBranch !== selectedBranch) {
-			setSelectedBranch(routeBranch);
-			safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, routeBranch);
-		}
+		setSelectedBranch(routeBranch);
+		safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, routeBranch);
 	}, [isAdminDev, routeBranch, knownBranches, selectedBranch]);
 
-	React.useEffect(() => {
-		if (!isAdminDev) return;
-		if (!selectedBranch) return;
-
-		safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, selectedBranch);
-	}, [isAdminDev, selectedBranch]);
-
 	if (!isAuthenticated) return null;
 
 	const effectiveBranch = isBranchUser ? user.branchId : selectedBranch;

+ 49 - 49
components/ui/button.jsx

@@ -1,56 +1,56 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
 import { cva } from "class-variance-authority";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 const buttonVariants = cva(
-  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
-  {
-    variants: {
-      variant: {
-        default: "bg-primary text-primary-foreground hover:bg-primary/90",
-        destructive:
-          "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
-        outline:
-          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
-        secondary:
-          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
-        ghost:
-          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
-        link: "text-primary underline-offset-4 hover:underline",
-      },
-      size: {
-        default: "h-9 px-4 py-2 has-[>svg]:px-3",
-        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
-        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
-        icon: "size-9",
-        "icon-sm": "size-8",
-        "icon-lg": "size-10",
-      },
-    },
-    defaultVariants: {
-      variant: "default",
-      size: "default",
-    },
-  }
-)
+	"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+	{
+		variants: {
+			variant: {
+				default: "bg-primary text-primary-foreground hover:bg-primary/90",
+				destructive:
+					"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+				outline:
+					"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+				secondary:
+					"bg-secondary text-secondary-foreground hover:bg-secondary/80",
+				ghost:
+					"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+				link: "text-primary underline-offset-4 hover:underline",
+			},
+			size: {
+				default: "h-9 px-4 py-2 has-[>svg]:px-3",
+				sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+				lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+				icon: "size-9",
+				"icon-sm": "size-8",
+				"icon-lg": "size-10",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+			size: "default",
+		},
+	},
+);
 
-function Button({
-  className,
-  variant,
-  size,
-  asChild = false,
-  ...props
-}) {
-  const Comp = asChild ? Slot : "button"
+const Button = React.forwardRef(
+	({ className, variant, size, asChild = false, ...props }, ref) => {
+		const Comp = asChild ? Slot : "button";
 
-  return (
-    <Comp
-      data-slot="button"
-      className={cn(buttonVariants({ variant, size, className }))}
-      {...props} />
-  );
-}
+		return (
+			<Comp
+				ref={ref}
+				data-slot="button"
+				className={cn(buttonVariants({ variant, size, className }))}
+				{...props}
+			/>
+		);
+	},
+);
 
-export { Button, buttonVariants }
+Button.displayName = "Button";
+
+export { Button, buttonVariants };

+ 38 - 44
components/ui/tooltip.jsx

@@ -1,55 +1,49 @@
-"use client"
+"use client";
 
-import * as React from "react"
-import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
-function TooltipProvider({
-  delayDuration = 0,
-  ...props
-}) {
-  return (<TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />);
+function TooltipProvider({ delayDuration = 0, ...props }) {
+	return (
+		<TooltipPrimitive.Provider
+			data-slot="tooltip-provider"
+			delayDuration={delayDuration}
+			{...props}
+		/>
+	);
 }
 
-function Tooltip({
-  ...props
-}) {
-  return (
-    <TooltipProvider>
-      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
-    </TooltipProvider>
-  );
+function Tooltip({ ...props }) {
+	return (
+		<TooltipProvider>
+			<TooltipPrimitive.Root data-slot="tooltip" {...props} />
+		</TooltipProvider>
+	);
 }
 
-function TooltipTrigger({
-  ...props
-}) {
-  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
+function TooltipTrigger({ ...props }) {
+	return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
 }
 
-function TooltipContent({
-  className,
-  sideOffset = 0,
-  children,
-  ...props
-}) {
-  return (
-    <TooltipPrimitive.Portal>
-      <TooltipPrimitive.Content
-        data-slot="tooltip-content"
-        sideOffset={sideOffset}
-        className={cn(
-          "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
-          className
-        )}
-        {...props}>
-        {children}
-        <TooltipPrimitive.Arrow
-          className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
-      </TooltipPrimitive.Content>
-    </TooltipPrimitive.Portal>
-  );
+function TooltipContent({ className, sideOffset = 0, children, ...props }) {
+	return (
+		<TooltipPrimitive.Portal>
+			<TooltipPrimitive.Content
+				data-slot="tooltip-content"
+				sideOffset={sideOffset}
+				className={cn(
+					"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+					className,
+				)}
+				{...props}
+			>
+				{children}
+				<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" />
+			</TooltipPrimitive.Content>
+		</TooltipPrimitive.Portal>
+	);
 }
 
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };