feat(overview): findings visualizations tabs component (#8999)

This commit is contained in:
Alan Buscaglia
2025-10-30 14:18:14 +01:00
committed by GitHub
parent 23e3ea4a41
commit fff02073cf
6 changed files with 318 additions and 1 deletions
+27 -1
View File
@@ -13,7 +13,33 @@
## Critical Architecture Rules (Non-Negotiable)
### 1. TypeScript Type Patterns (Required)
### 1. React Imports (Required)
**NEVER** import React with `import * as React`. Instead, import only what you need:
**❌ DON'T:**
```typescript
import * as React from "react";
import React, { useState } from "react";
```
**✅ DO:**
```typescript
import { useState, useEffect } from "react";
```
**When you need nothing from React (e.g., only using JSX):**
```typescript
// No React import needed - JSX works without it in modern Next.js
export function MyComponent() {
return <div>Hello</div>;
}
```
### 2. TypeScript Type Patterns (Required)
When defining union types for options, ALWAYS create a const object first, then extract the type:
+78
View File
@@ -16,6 +16,9 @@ shadcn/
│ └── resource-stats-card/
│ ├── resource-stats-card.tsx
│ ├── resource-stats-card.example.tsx
├── tabs/
│ ├── tabs.tsx # Base tab components
│ ├── generic-tabs.tsx # Generic reusable tabs with lazy loading
├── index.ts # Barrel exports
└── README.md
```
@@ -27,8 +30,83 @@ All shadcn components can be imported from `@/components/shadcn`:
```tsx
import { Card, CardHeader, CardContent } from "@/components/shadcn";
import { ResourceStatsCard } from "@/components/shadcn";
import { GenericTabs, type TabItem } from "@/components/shadcn";
```
### GenericTabs Component
The `GenericTabs` component provides a flexible, lazy-loaded tabs interface. Content is only rendered when the tab is active, improving performance.
**Basic Example:**
```tsx
import { lazy } from "react";
import { GenericTabs, type TabItem } from "@/components/shadcn";
const OverviewContent = lazy(() => import("./OverviewContent"));
const DetailsContent = lazy(() => import("./DetailsContent"));
const tabs: TabItem[] = [
{
id: "overview",
label: "Overview",
content: OverviewContent,
},
{
id: "details",
label: "Details",
content: DetailsContent,
},
];
export function MyComponent() {
return <GenericTabs tabs={tabs} defaultTabId="overview" />;
}
```
**With Icons and Props:**
```tsx
import { Eye, Settings } from "lucide-react";
import { GenericTabs, type TabItem } from "@/components/shadcn";
const tabs: TabItem[] = [
{
id: "view",
label: "View",
icon: <Eye size={16} />,
content: ViewContent,
contentProps: { data: myData },
},
{
id: "config",
label: "Config",
icon: <Settings size={16} />,
content: ConfigContent,
},
];
export function MyComponent() {
return (
<GenericTabs
tabs={tabs}
defaultTabId="view"
onTabChange={(tabId) => console.log("Active tab:", tabId)}
/>
);
}
```
**Props:**
- `tabs` - Array of `TabItem` objects
- `defaultTabId` - Initial active tab ID (defaults to first tab)
- `className` - Wrapper class
- `listClassName` - TabsList class
- `triggerClassName` - TabsTrigger class
- `contentClassName` - TabsContent class
- `onTabChange` - Callback fired when tab changes
## Adding New shadcn Components
When adding new shadcn components using the CLI:
+2
View File
@@ -6,3 +6,5 @@ export * from "./card/resource-stats-card/resource-stats-card-content";
export * from "./card/resource-stats-card/resource-stats-card-divider";
export * from "./card/resource-stats-card/resource-stats-card-header";
export * from "./select/select";
export * from "./tabs/generic-tabs";
export * from "./tabs/tabs";
+102
View File
@@ -0,0 +1,102 @@
"use client";
import type { ComponentType, ReactNode } from "react";
import { Suspense, useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
export interface TabItem {
id: string;
label: string;
icon?: ReactNode;
content: ComponentType<{ isActive: boolean }>;
contentProps?: Record<string, unknown>;
}
interface GenericTabsProps {
tabs: TabItem[];
defaultTabId?: string;
className?: string;
listClassName?: string;
triggerClassName?: string;
contentClassName?: string;
onTabChange?: (tabId: string) => void;
}
/**
* A generic tabs component that accepts an array of tab objects with lazy-loaded content.
*
* @example
* const tabs: TabItem[] = [
* {
* id: "tab-1",
* label: "Tab 1",
* content: lazy(() => import("./Tab1Content")),
* contentProps: { key: "value" }
* },
* {
* id: "tab-2",
* label: "Tab 2",
* content: lazy(() => import("./Tab2Content"))
* }
* ];
*
* <GenericTabs tabs={tabs} defaultTabId="tab-1" onTabChange={(id) => console.log(id)} />
*/
export function GenericTabs({
tabs,
defaultTabId,
className,
listClassName,
triggerClassName,
contentClassName,
onTabChange,
}: GenericTabsProps) {
const [activeTab, setActiveTab] = useState<string>(
defaultTabId || tabs[0]?.id || "",
);
const handleTabChange = (tabId: string) => {
setActiveTab(tabId);
onTabChange?.(tabId);
};
if (!tabs || tabs.length === 0) {
return null;
}
return (
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className={className}
>
<TabsList className={listClassName}>
{tabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id} className={triggerClassName}>
{tab.icon && <span className="mr-1">{tab.icon}</span>}
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => {
const ContentComponent = tab.content;
const isActive = activeTab === tab.id;
return (
<TabsContent key={tab.id} value={tab.id} className={contentClassName}>
{isActive && (
<Suspense fallback={<div>Loading...</div>}>
<ContentComponent
isActive={isActive}
{...(tab.contentProps || {})}
/>
</Suspense>
)}
</TabsContent>
);
})}
</Tabs>
);
}
@@ -0,0 +1,43 @@
/**
* Trigger component style parts using semantic class names
*/
const TRIGGER_STYLES = {
base: "relative inline-flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50",
border: "border-r border-[#E9E9F0] last:border-r-0 dark:border-[#171D30]",
text: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white",
active:
"data-[state=active]:text-slate-900 dark:data-[state=active]:text-white",
underline:
"after:absolute after:bottom-0 after:left-1/2 after:h-0.5 after:w-0 after:-translate-x-1/2 after:bg-[#20B853] after:transition-all data-[state=active]:after:w-[calc(100%-theme(spacing.5))]",
focus:
"focus-visible:ring-2 focus-visible:ring-[#20B853] focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:focus-visible:ring-offset-slate-950",
icon: "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
} as const;
/**
* Content component styles
*/
export const CONTENT_STYLES =
"mt-2 focus-visible:rounded-md focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50" as const;
/**
* Build trigger className by combining style parts
*/
export function buildTriggerClassName(): string {
return [
TRIGGER_STYLES.base,
TRIGGER_STYLES.border,
TRIGGER_STYLES.text,
TRIGGER_STYLES.active,
TRIGGER_STYLES.underline,
TRIGGER_STYLES.focus,
TRIGGER_STYLES.icon,
].join(" ");
}
/**
* Build list className
*/
export function buildListClassName(): string {
return "inline-flex w-full items-center border-[#E9E9F0] dark:border-[#171D30]";
}
+66
View File
@@ -0,0 +1,66 @@
"use client";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
import {
buildListClassName,
buildTriggerClassName,
CONTENT_STYLES,
} from "./tabs.constants";
function Tabs({
className,
...props
}: ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("w-full", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(buildListClassName(), className)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(buildTriggerClassName(), className)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn(CONTENT_STYLES, className)}
{...props}
/>
);
}
export { Tabs, TabsContent, TabsList, TabsTrigger };