feat: enhance UI components and add vibrant theme support

- Updated layout and styling for various dashboard pages including stock adjustments, counts, transfers, wastage, and vendors to improve responsiveness and visual consistency.
- Introduced a vibrant theme option in the theme toggle component, allowing users to switch between light, dark, and vibrant themes.
- Refactored button styles across login and forgot password pages for better accessibility and visual feedback.
- Improved sidebar navigation with expanded functionality for items without dedicated pages.
- Enhanced data table styling for better readability and user interaction.
- Added new background images to support the vibrant theme.
This commit is contained in:
2026-07-27 11:11:26 +05:30
parent ae20bc4e34
commit c564916c60
56 changed files with 331 additions and 264 deletions
@@ -47,6 +47,9 @@ const navItems: {
/** Where clicking the row actually navigates, if different from `href`. `href` itself
* stays the section prefix used to decide whether this row is "active". */
landingHref?: string
/** This item's own href has no page of its own (no landingHref either) — clicking the
* row should only expand/collapse its children, never navigate. */
expandOnly?: boolean
icon: LucideIcon
chevron?: boolean
children?: { title: string; code: string; href: string; icon: LucideIcon }[]
@@ -106,6 +109,7 @@ const navItems: {
title: "Settings",
code: "settings",
href: "/dashboard/settings",
expandOnly: true,
icon: Settings,
chevron: true,
children: [
@@ -139,9 +143,13 @@ function SidebarContent({
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
useEffect(() => {
const parent = items.find((i) =>
i.children?.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))
)
const parent = items.find((i) => {
if (!i.children?.length) return false
if (i.children.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))) return true
// Landing on the parent's own hub page (e.g. /dashboard/settings itself,
// not one of its children) should also reveal its sub-items.
return pathname === i.href || pathname.startsWith(`${i.href}/`)
})
if (parent) {
setExpanded((prev) => (prev[parent.code] ? prev : { ...prev, [parent.code]: true }))
}
@@ -153,7 +161,7 @@ function SidebarContent({
return (
<nav
className={cn(
"flex h-full flex-col rounded-3xl bg-card p-3 shadow-sm ring-1 ring-foreground/10 transition-[width] duration-300 ease-in-out",
"sidebar-surface flex h-full flex-col rounded-3xl bg-card p-3 shadow-sm ring-1 ring-foreground/10 transition-[width] duration-300 ease-in-out",
!isMobile && (collapsed ? "w-20" : "w-64")
)}
>
@@ -187,7 +195,7 @@ function SidebarContent({
{/* Nav items — scrolls internally when it overflows, without a visible
scrollbar so the rounded panel stays clean. */}
<ul className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<ul className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{items.map((item) => {
const isActive =
item.href === "/dashboard" ? pathname === item.href : pathname.startsWith(item.href)
@@ -205,15 +213,18 @@ function SidebarContent({
<Link
href={item.landingHref ?? item.href}
title={iconOnly ? item.title : undefined}
onClick={onClose}
onClick={() => {
onClose?.()
if (hasChildren) setExpanded((prev) => (prev[item.code] ? prev : { ...prev, [item.code]: true }))
}}
className={cn(
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-3 text-base font-semibold",
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-1.5 text-base font-normal",
iconOnly && "justify-center px-0",
isActive ? "text-primary" : "text-foreground"
)}
>
<item.icon
className={cn("size-5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
className={cn("size-3.5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
/>
{!iconOnly && (
<>
@@ -270,14 +281,14 @@ function SidebarContent({
onClick={onClose}
tabIndex={isOpen ? undefined : -1}
className={cn(
"flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-colors",
"flex items-center gap-2.5 rounded-xl px-3 py-1 text-sm font-medium transition-colors",
childActive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<child.icon
className={cn("size-4 shrink-0", childActive ? "text-primary" : "text-muted-foreground")}
className={cn("size-3 shrink-0", childActive ? "text-primary" : "text-muted-foreground")}
/>
{child.title}
</Link>
@@ -362,9 +373,9 @@ export function AppSidebar() {
type="button"
onClick={() => setMobileOpen(true)}
aria-label="Open menu"
className="fixed top-5 left-5 z-40 flex size-10 items-center justify-center rounded-2xl bg-card shadow-sm ring-1 ring-foreground/10 text-muted-foreground hover:bg-muted lg:hidden"
className="fixed top-8 left-5 z-40 flex size-8 items-center justify-center rounded-xl bg-card shadow-sm ring-1 ring-foreground/10 text-muted-foreground hover:bg-muted lg:hidden"
>
<Menu className="size-5" />
<Menu className="size-4" />
</button>
{/* ── Mobile overlay backdrop ──────────────────────── */}
@@ -164,8 +164,8 @@ export function Header() {
}
return (
<header className="mx-6 mt-3 mb-6 flex items-center justify-between gap-2 rounded-3xl bg-card py-4 pr-4 pl-14 shadow-sm ring-1 ring-foreground/10 lg:mx-8 lg:mt-4 lg:gap-4 lg:p-4">
<div className="flex items-center gap-3">
<header className="mx-3 mt-3 mb-3 flex items-center justify-between gap-2 rounded-3xl bg-card py-4 pr-3 pl-12 shadow-sm ring-1 ring-foreground/10 sm:mx-6 sm:pr-4 sm:pl-14 lg:mx-8 lg:mt-4 lg:mb-4 lg:gap-4 lg:p-4">
<div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
{showBackButton && (
<button
type="button"
@@ -176,10 +176,10 @@ export function Header() {
<ArrowLeft className="size-5" />
</button>
)}
<h1 className="text-base font-bold tracking-tight text-foreground sm:text-xl">{title}</h1>
<h1 className="min-w-0 truncate text-base font-bold tracking-tight text-foreground sm:text-xl">{title}</h1>
</div>
<div className="flex items-center gap-2 sm:gap-3">
<div className="flex shrink-0 items-center gap-1.5 sm:gap-3">
<ThemeToggle />
<Popover>
+42 -18
View File
@@ -1,38 +1,62 @@
"use client"
import { useEffect, useState } from "react"
import { Moon, Sun } from "lucide-react"
import { Moon, Sun, Monitor } from "lucide-react"
import { useTheme } from "next-themes"
import { cn } from "@/lib/utils"
const options = [
{ value: "light", label: "Light mode", icon: Sun },
{ value: "vibrant", label: "Vibrant theme", icon: Monitor },
{ value: "dark", label: "Dark mode", icon: Moon },
] as const
/**
* Renders an empty slot until mounted: `resolvedTheme` is unknown on the server (and on
* the client's first paint, before next-themes reads localStorage), so rendering an icon
* before that would either be wrong or cause a hydration mismatch.
* Renders an empty slot until mounted: `theme` is unknown on the server (and on the
* client's first paint, before next-themes reads localStorage), so rendering the active
* segment before that would either be wrong or cause a hydration mismatch.
*/
export function ThemeToggle({ className }: { className?: string }) {
const { resolvedTheme, setTheme } = useTheme()
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) {
return <div className={cn("size-10 shrink-0", className)} aria-hidden="true" />
return <div className={cn("h-10 w-27 shrink-0", className)} aria-hidden="true" />
}
const isDark = resolvedTheme === "dark"
return (
<button
type="button"
onClick={() => setTheme(isDark ? "light" : "dark")}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground",
className
)}
<div
role="radiogroup"
aria-label="Theme"
className={cn("flex shrink-0 items-center gap-0.5 rounded-full bg-muted p-1", className)}
>
{isDark ? <Sun className="size-5" /> : <Moon className="size-5" />}
</button>
{options.map(({ value, label, icon: Icon }) => {
const isActive = theme === value
const isVibrant = value === "vibrant"
return (
<button
key={value}
type="button"
role="radio"
aria-checked={isActive}
aria-label={label}
title={label}
onClick={() => setTheme(value)}
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full transition-colors",
isActive
? isVibrant
? "bg-linear-to-br from-indigo-500 to-violet-600 text-white shadow-sm"
: "bg-card text-primary shadow-sm ring-1 ring-foreground/10"
: "text-muted-foreground hover:text-foreground"
)}
>
<Icon className="size-4" />
</button>
)
})}
</div>
)
}
@@ -143,12 +143,11 @@ export function DataTable<T>({
<Table>
<TableHeader>
<TableRow className="bg-indigo-50 hover:bg-indigo-50">
<TableRow className="border-b-0 hover:bg-primary/5">
{columns.map((column, index) => (
<TableHead
key={column.key}
className={cn(
"text-indigo-700",
index === 0 && "rounded-l-lg pl-3",
index === columns.length - 1 && !actions && "rounded-r-lg pr-3",
column.headerClassName
@@ -158,7 +157,7 @@ export function DataTable<T>({
</TableHead>
))}
{actions && (
<TableHead className="rounded-r-lg pr-3 text-right text-indigo-700">
<TableHead className="rounded-r-lg pr-3 text-right">
Actions
</TableHead>
)}
+2 -2
View File
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
className={cn("bg-primary/5 [&_tr]:border-b [&_tr]:hover:bg-primary/5", className)}
{...props}
/>
)
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th
data-slot="table-head"
className={cn(
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
"h-10 px-2 text-left align-middle font-semibold whitespace-nowrap text-primary [&:has([role=checkbox])]:pr-0",
className
)}
{...props}