fix styles in tabs and add d3js component for main section in dashboard-home ,also add chartBar

This commit is contained in:
Saeed AB 2025-08-28 13:02:11 +03:30
parent cc5ee070e0
commit 8cce0a0580
8 changed files with 535 additions and 293 deletions

View File

@ -0,0 +1,241 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
export type D3ImageInfoProps = {
imageUrl?: string;
title?: string;
description?: string;
width?: number; // fallback width if container size not measured yet
height?: number; // fallback height
};
/**
* D3ImageInfo
* - Renders an image and an information box beside it using D3 within an SVG.
* - Includes a clickable "show" chip that opens a popup dialog with more details.
*/
export function D3ImageInfo({
imageUrl = "/placeholder.svg",
title = "عنوان آیتم",
description = "توضیحات تکمیلی در مورد این آیتم در این قسمت نمایش داده می‌شود.",
width = 800,
height = 360,
}: D3ImageInfoProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const svgRef = useRef<SVGSVGElement | null>(null);
const [open, setOpen] = useState(false);
// Redraw helper
const draw = () => {
if (!containerRef.current || !svgRef.current) return;
const container = containerRef.current;
const svg = d3.select(svgRef.current);
const W = Math.max(480, container.clientWidth || width);
const H = Math.max(260, height);
svg.attr("width", W).attr("height", H);
// Clear previous content
svg.selectAll("*").remove();
// Layout
const padding = 16;
const imageAreaWidth = Math.min(300, Math.max(220, W * 0.35));
const infoAreaX = padding + imageAreaWidth + padding;
const infoAreaWidth = W - infoAreaX - padding;
// Image area (with rounded border)
const imgGroup = svg
.append("g")
.attr("transform", `translate(${padding}, ${padding})`);
const imgW = imageAreaWidth;
const imgH = H - 2 * padding;
// Frame
imgGroup
.append("rect")
.attr("width", imgW)
.attr("height", imgH)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill", "#1F2937") // gray-800
.attr("stroke", "#4B5563") // gray-600
.attr("stroke-width", 1.5);
// Image
imgGroup
.append("image")
.attr("href", imageUrl)
.attr("x", 4)
.attr("y", 4)
.attr("width", imgW - 8)
.attr("height", imgH - 8)
.attr("preserveAspectRatio", "xMidYMid slice")
.attr("clip-path", null);
// Info area
const infoGroup = svg
.append("g")
.attr("transform", `translate(${infoAreaX}, ${padding})`);
// Info container
infoGroup
.append("rect")
.attr("width", Math.max(220, infoAreaWidth))
.attr("height", imgH)
.attr("rx", 12)
.attr("ry", 12)
.attr("fill", "url(#infoGradient)")
.attr("stroke", "#6B7280") // gray-500
.attr("stroke-width", 1);
// Background gradient
const defs = svg.append("defs");
const gradient = defs
.append("linearGradient")
.attr("id", "infoGradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%");
gradient.append("stop").attr("offset", "0%").attr("stop-color", "#111827"); // gray-900
gradient
.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#374151"); // gray-700
// Title
infoGroup
.append("text")
.attr("x", 16)
.attr("y", 36)
.attr("fill", "#F9FAFB") // gray-50
.attr("font-weight", 700)
.attr("font-size", 18)
.text(title);
// Description (wrapped)
const wrapText = (text: string, maxWidth: number) => {
const words = text.split(/\s+/).reverse();
const lines: string[] = [];
let line: string[] = [];
let t = "";
while (words.length) {
const word = words.pop()!;
const test = (t + " " + word).trim();
// Approximate measure using character count
const tooLong = test.length * 8 > maxWidth; // 8px avg char width
if (tooLong && t.length) {
lines.push(t);
t = word;
} else {
t = test;
}
}
if (t) lines.push(t);
return lines;
};
const descMaxWidth = Math.max(200, infoAreaWidth - 32);
const descLines = wrapText(description, descMaxWidth);
descLines.forEach((line, i) => {
infoGroup
.append("text")
.attr("x", 16)
.attr("y", 70 + i * 22)
.attr("fill", "#E5E7EB") // gray-200
.attr("font-size", 14)
.text(line);
});
// Show button-like chip
const chipY = Math.min(imgH - 48, 70 + descLines.length * 22 + 16);
const chip = infoGroup
.append("g")
.attr("class", "show-chip")
.style("cursor", "pointer");
const chipW = 120;
const chipH = 36;
chip
.append("rect")
.attr("x", 16)
.attr("y", chipY)
.attr("width", chipW)
.attr("height", chipH)
.attr("rx", 8)
.attr("ry", 8)
.attr("fill", "#3B82F6") // blue-500
.attr("stroke", "#60A5FA")
.attr("stroke-width", 1.5)
.attr("opacity", 0.95);
chip
.append("text")
.attr("x", 16 + chipW / 2)
.attr("y", chipY + chipH / 2 + 5)
.attr("text-anchor", "middle")
.attr("fill", "#FFFFFF")
.attr("font-weight", 700)
.text("نمایش");
// Hover & click
chip
.on("mouseenter", function () {
d3.select(this).select("rect").attr("fill", "#2563EB"); // blue-600
})
.on("mouseleave", function () {
d3.select(this).select("rect").attr("fill", "#3B82F6"); // blue-500
})
.on("click", () => setOpen(true));
};
useEffect(() => {
const ro = new ResizeObserver(() => draw());
if (containerRef.current) ro.observe(containerRef.current);
draw();
return () => ro.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [imageUrl, title, description]);
return (
<div className="w-full h-full">
<div ref={containerRef} className="w-full h-[380px]">
<svg ref={svgRef} className="block w-full h-full"></svg>
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[520px]">
<DialogHeader>
<DialogTitle className="font-persian">{title}</DialogTitle>
<DialogDescription className="font-persian">
{description}
</DialogDescription>
</DialogHeader>
<div className="mt-4">
<img
src={imageUrl}
alt={title}
className="w-full h-60 object-cover rounded-md border border-gray-700"
/>
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -54,7 +54,7 @@ export function DashboardCustomBarChart({
<div className="relative min-h-6 h-10 rounded-lg overflow-hidden">
{/* Animated bar */}
<div
className={`absolute left-0 h-auto top-0 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-between px-2`}
className={`absolute left-0 h-auto gap-2 top-0 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-between px-2`}
style={{ width: `${widthPercentage}%` }}
>
<span className="text-white font-bold text-base">

View File

@ -31,6 +31,8 @@ import {
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
import { InteractiveBarChart } from "./interactive-bar-chart";
import { D3ImageInfo } from "./d3-image-info";
import {
Label,
PolarGrid,
@ -130,12 +132,81 @@ export function DashboardHome() {
},
};
// Skeleton component for cards
const SkeletonCard = ({ className = "" }) => (
<div
className={`bg-gray-700/50 rounded-lg overflow-hidden animate-pulse ${className}`}
>
<div className="p-6">
<div className="h-6 bg-gray-600 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-600 rounded w-1/2 mb-6"></div>
<div className="h-3 bg-gray-600 rounded w-full mb-2"></div>
<div className="h-3 bg-gray-600 rounded w-5/6"></div>
</div>
</div>
);
// Skeleton for the chart
const SkeletonChart = () => (
<div className="bg-gray-700/50 rounded-lg overflow-hidden animate-pulse p-6">
<div className="flex justify-between items-center mb-6">
<div className="h-6 bg-gray-600 rounded w-1/4"></div>
<div className="flex space-x-2 rtl:space-x-reverse">
<div className="h-8 w-24 bg-gray-600 rounded"></div>
<div className="h-8 w-24 bg-gray-600 rounded"></div>
<div className="h-8 w-24 bg-gray-600 rounded"></div>
</div>
</div>
<div className="h-64 bg-gray-800/50 rounded-lg flex items-end space-x-1 rtl:space-x-reverse p-4">
{[...Array(12)].map((_, i) => (
<div key={i} className="flex-1 flex space-x-1 rtl:space-x-reverse">
<div
className="w-full bg-blue-400/30 rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
<div
className="w-full bg-green-400/30 rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
<div
className="w-full bg-red-400/30 rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
</div>
))}
</div>
<div className="flex justify-between mt-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-3 bg-gray-600 rounded w-1/6"></div>
))}
</div>
</div>
);
if (loading) {
return (
<DashboardLayout>
<div className="">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
<div className="p-3 pb-0 grid grid-cols-3 gap-4 animate-pulse">
{/* Top Cards Row */}
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
{/* Middle Section */}
<div className="col-span-2 space-y-6 h-full">
{/* Chart Section */}
<SkeletonChart />
</div>
{/* Right Sidebar */}
<div className="space-y-2">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
</div>
</DashboardLayout>
@ -546,129 +617,33 @@ export function DashboardHome() {
{/* Main Content with Tabs */}
<Tabs
defaultValue="charts"
className=" col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"
className="grid overflow-hidden rounded-lg grid-rows-[max-content] items-center col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"
>
<TabsList className="bg-transparent">
<TabsTrigger
value="charts"
className=" text-white data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-400"
>
مقایسه ای
</TabsTrigger>
<TabsTrigger
value="canvas"
disabled
className="text-gray-500 cursor-not-allowed"
>
شماتیک
</TabsTrigger>
</TabsList>
<div className="flex items-center border-b border-gray-600 justify-between gap-2">
<p className="p-6 font-persian font-semibold text-lg ">
تحقق ارزش ها
</p>
<TabsList className="bg-transparent py-2 border m-6 border-gray-600">
<TabsTrigger value="canvas" className="">
شماتیک
</TabsTrigger>
<TabsTrigger value="charts" className=" text-white font-light ">
مقایسه ای
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="charts" className="">
<div className=" gap-6">
{/* Right Section - Charts */}
<div className="">
{/* Main Chart */}
<Card className="bg-transparent px-2 border-none">
<CardHeader>
<CardTitle className="text-white text-xl">
تحلیل ارزشها
</CardTitle>
<p className="text-gray-400 text-sm">
نمودار مقایسهای عملکرد ماهانه
</p>
</CardHeader>
<CardContent className="border-none">
<div className="h-60 ">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={[
{
month: "فروردین",
ideas: 12,
revenue: 850,
cost: 320,
},
{
month: "اردیبهشت",
ideas: 19,
revenue: 1200,
cost: 450,
},
{
month: "خرداد",
ideas: 8,
revenue: 980,
cost: 280,
},
{
month: "تیر",
ideas: 15,
revenue: 1400,
cost: 520,
},
{
month: "مرداد",
ideas: 22,
revenue: 1650,
cost: 680,
},
{
month: "شهریور",
ideas: 18,
revenue: 1320,
cost: 590,
},
]}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="#374151"
/>
<XAxis
dataKey="month"
stroke="#9CA3AF"
fontSize={11}
/>
<YAxis stroke="#9CA3AF" fontSize={11} />
<Tooltip
contentStyle={{
backgroundColor: "#1F2937",
border: "1px solid #374151",
borderRadius: "8px",
color: "#F9FAFB",
}}
/>
<Line
type="monotone"
dataKey="ideas"
stroke="#3B82F6"
strokeWidth={3}
name="ایده‌ها"
dot={{ fill: "#3B82F6", strokeWidth: 2, r: 4 }}
/>
<Line
type="monotone"
dataKey="revenue"
stroke="#10B981"
strokeWidth={3}
name="درآمد (میلیون)"
dot={{ fill: "#10B981", strokeWidth: 2, r: 4 }}
/>
<Line
type="monotone"
dataKey="cost"
stroke="#F59E0B"
strokeWidth={3}
name="کاهش هزینه (میلیون)"
dot={{ fill: "#F59E0B", strokeWidth: 2, r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
<TabsContent value="charts" className="w-ful h-full">
<InteractiveBarChart />
</TabsContent>
<TabsContent value="canvas" className="w-ful h-full">
<div className="p-4">
<D3ImageInfo
imageUrl="/main-circle.png"
title="نمای شماتیک"
description=":"
/>
</div>
</TabsContent>
</Tabs>

View File

@ -144,145 +144,8 @@ export function DashboardHome() {
<path d="m22 21-3-3" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24</div>
<p className="text-xs text-muted-foreground font-persian">
+2 از ماه گذشته
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium font-persian">
پروژههای فعال
</CardTitle>
<svg
className="h-4 w-4 text-muted-foreground"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<path d="M2 10h20" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground font-persian">
+1 از هفته گذشته
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium font-persian">
پروژههای تکمیل شده
</CardTitle>
<svg
className="h-4 w-4 text-muted-foreground"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">8</div>
<p className="text-xs text-muted-foreground font-persian">
+3 از ماه گذشته
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium font-persian">
درصد موفقیت
</CardTitle>
<svg
className="h-4 w-4 text-muted-foreground"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M12 2v20m8-10H4" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">85%</div>
<p className="text-xs text-muted-foreground font-persian">
+5% از ماه گذشته
</p>
</CardContent>
</Card>
</div>
{/* Recent Projects */}
<Card>
<CardHeader>
<CardTitle className="font-persian">پروژههای اخیر</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[
{
name: "سیستم مدیریت محتوا",
status: "در حال انجام",
progress: 75,
},
{ name: "اپلیکیشن موبایل", status: "تکمیل شده", progress: 100 },
{
name: "پلتفرم تجارت الکترونیک",
status: "شروع شده",
progress: 25,
},
{
name: "سیستم مدیریت مالی",
status: "در حال بررسی",
progress: 10,
},
].map((project, index) => (
<div
key={index}
className="flex items-center space-x-4 space-x-reverse"
>
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white font-persian">
{project.name}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">
{project.status}
</p>
</div>
<div className="flex items-center space-x-2 space-x-reverse">
<div className="w-16 bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div
className="bg-green-600 h-2 rounded-full"
style={{ width: `${project.progress}%` }}
></div>
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
{project.progress}%
</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</DashboardLayout>
);

View File

@ -4,7 +4,7 @@ import { Link } from "react-router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui/button";
import {
PanelLeft,
PanelLeft,
Search,
Bell,
Settings,
@ -54,7 +54,9 @@ export function Header({
)}
{/* Page Title */}
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian"><PanelLeft /> {title}</h1>
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
<PanelLeft /> {title}
</h1>
</div>
{/* Right Section */}

View File

@ -0,0 +1,125 @@
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, LabelList } from "recharts";
import React, { useState, useEffect } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "~/components/ui/chart";
export const description = "An interactive bar chart";
const chartData = [
{ category: "کیمیا", ideas: 12, revenue: 850, cost: 320 },
{ category: "ُفرآروزش", ideas: 19, revenue: 1200, cost: 450 },
{ category: "خوارزمی", ideas: 15, revenue: 1400, cost: 520 },
];
const chartConfig = {
ideas: {
label: "ایده‌ها",
color: "#60A5FA", // Blue-400
},
revenue: {
label: "درآمد (میلیون)",
color: "#4ADE80", // Green-400
},
cost: {
label: "کاهش هزینه (میلیون)",
color: "#F87171", // Red-400
},
} satisfies ChartConfig;
export function InteractiveBarChart() {
const [activeChart, setActiveChart] =
React.useState<keyof typeof chartConfig>("ideas");
const total = React.useMemo(
() => ({
ideas: chartData.reduce((acc, curr) => acc + curr.ideas, 0),
revenue: chartData.reduce((acc, curr) => acc + curr.revenue, 0),
cost: chartData.reduce((acc, curr) => acc + curr.cost, 0),
}),
[],
);
return (
<Card className="py-0 bg-transparent mt-20 border-none h-full">
<CardContent className="px-2 sm:p-6 bg-transparent">
<ChartContainer
config={chartConfig}
className="aspect-auto h-96 w-full"
>
<BarChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
barCategoryGap="42%"
>
<CartesianGrid vertical={false} stroke="#475569" />
<XAxis
dataKey="category"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tick={{ fill: "#94a3b8", fontSize: 12 }}
/>
<YAxis
domain={[0, 100]}
tickLine={false}
axisLine={false}
tickMargin={8}
tick={{ fill: "#94a3b8", fontSize: 12 }}
tickFormatter={(value) => `${value}%`}
/>
<Bar
dataKey="ideas"
fill={chartConfig.ideas.color}
radius={[8, 8, 0, 0]}
>
<LabelList
dataKey="ideas"
position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
/>
</Bar>
<Bar
dataKey="revenue"
fill={chartConfig.revenue.color}
radius={[8, 8, 0, 0]}
>
<LabelList
dataKey="revenue"
position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
/>
</Bar>
<Bar
dataKey="cost"
fill={chartConfig.cost.color}
radius={[8, 8, 0, 0]}
>
<LabelList
dataKey="cost"
position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
/>
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@ -153,7 +153,7 @@ export function Sidebar({
menuItems.forEach((item) => {
if (item.children) {
const hasActiveChild = item.children.some(
(child) => child.href && location.pathname === child.href
(child) => child.href && location.pathname === child.href,
);
if (hasActiveChild) {
newExpandedItems.push(item.id);
@ -171,10 +171,10 @@ export function Sidebar({
setExpandedItems((prev) => {
// If trying to collapse, check if any child is active
if (prev.includes(itemId)) {
const item = menuItems.find(menuItem => menuItem.id === itemId);
const item = menuItems.find((menuItem) => menuItem.id === itemId);
if (item?.children) {
const hasActiveChild = item.children.some(
(child) => child.href && location.pathname === child.href
(child) => child.href && location.pathname === child.href,
);
// Don't collapse if a child is active
if (hasActiveChild) {
@ -200,10 +200,12 @@ export function Sidebar({
const renderMenuItem = (item: MenuItem, level = 0) => {
const isActive = isActiveRoute(item.href, item.children);
const isExpanded = expandedItems.includes(item.id) ||
(item.children && item.children.some(child =>
child.href && location.pathname === child.href
));
const isExpanded =
expandedItems.includes(item.id) ||
(item.children &&
item.children.some(
(child) => child.href && location.pathname === child.href,
));
const hasChildren = item.children && item.children.length > 0;
const ItemIcon = item.icon;
@ -228,7 +230,8 @@ export function Sidebar({
? " text-emerald-400 border-r-2 border-emerald-400"
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400",
item.id === "logout" &&
"hover:bg-red-500/10 hover:text-red-400",
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
@ -269,9 +272,11 @@ export function Sidebar({
className={cn(
"w-full text-right",
// Disable pointer cursor when child is active (cannot collapse)
item.children && item.children.some(child =>
child.href && location.pathname === child.href
) && "cursor-not-allowed"
item.children &&
item.children.some(
(child) => child.href && location.pathname === child.href,
) &&
"cursor-not-allowed",
)}
onClick={handleClick}
>
@ -283,7 +288,8 @@ export function Sidebar({
? " text-emerald-400 border-r-2 border-emerald-400"
: "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300",
isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400",
item.id === "logout" &&
"hover:bg-red-500/10 hover:text-red-400",
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
@ -313,9 +319,13 @@ export function Sidebar({
"w-4 h-4 transition-transform duration-200",
isExpanded ? "rotate-180" : "rotate-0",
// Show different color when child is active (cannot collapse)
item.children && item.children.some(child =>
child.href && location.pathname === child.href
) ? "text-emerald-400" : "text-current"
item.children &&
item.children.some(
(child) =>
child.href && location.pathname === child.href,
)
? "text-emerald-400"
: "text-current",
)}
/>
)}
@ -324,14 +334,12 @@ export function Sidebar({
</div>
</button>
)}
{/* Submenu */}
{hasChildren && isExpanded && !isCollapsed && (
<div className="mt-1 space-y-0.5">
{item.children?.map((child) => renderMenuItem(child, level + 1))}
</div>
)}
{/* Tooltip for collapsed state */}
{isCollapsed && level === 0 && (
<div className="absolute right-full top-1/2 transform -translate-y-1/2 mr-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50">
@ -375,7 +383,12 @@ export function Sidebar({
</div>
) : (
<div className="flex justify-center w-full">
<InogenLogo size="sm" />
<GalleryVerticalEnd
color="black"
size={32}
strokeWidth={1}
className="bg-green-400 p-1.5 rounded-lg"
/>
</div>
)}
</div>

View File

@ -16,17 +16,23 @@ interface TabsProps {
children: React.ReactNode;
}
export function Tabs({ defaultValue, value, onValueChange, className, children }: TabsProps) {
export function Tabs({
defaultValue,
value,
onValueChange,
className,
children,
}: TabsProps) {
const [internalValue, setInternalValue] = useState(defaultValue || "");
const currentValue = value ?? internalValue;
const handleValueChange = onValueChange ?? setInternalValue;
return (
<TabsContext.Provider value={{ value: currentValue, onValueChange: handleValueChange }}>
<div className={cn("w-full", className)}>
{children}
</div>
<TabsContext.Provider
value={{ value: currentValue, onValueChange: handleValueChange }}
>
<div className={cn("w-full", className)}>{children}</div>
</TabsContext.Provider>
);
}
@ -38,7 +44,12 @@ interface TabsListProps {
export function TabsList({ className, children }: TabsListProps) {
return (
<div className={cn("inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", className)}>
<div
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
>
{children}
</div>
);
@ -51,7 +62,12 @@ interface TabsTriggerProps {
children: React.ReactNode;
}
export function TabsTrigger({ value, className, disabled, children }: TabsTriggerProps) {
export function TabsTrigger({
value,
className,
disabled,
children,
}: TabsTriggerProps) {
const context = useContext(TabsContext);
if (!context) throw new Error("TabsTrigger must be used within Tabs");
@ -64,8 +80,10 @@ export function TabsTrigger({ value, className, disabled, children }: TabsTrigge
onClick={() => !disabled && context.onValueChange(value)}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive ? "bg-background text-foreground shadow-sm" : "hover:bg-muted/50",
className
isActive
? "bg-gray-700 text-foreground shadow-sm"
: "hover:bg-muted/50",
className,
)}
>
{children}
@ -86,7 +104,12 @@ export function TabsContent({ value, className, children }: TabsContentProps) {
if (context.value !== value) return null;
return (
<div className={cn("mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", className)}>
<div
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
>
{children}
</div>
);