mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-02-05 07:56:30 -05:00
Enhance development standards and UI components
- Updated .rules to enforce stricter UI/UX standards, including exclusive use of Lucide icons and consistent patterns for entity view pages. - Added new UI components for entity views, including headers, sections, and quick actions to improve layout and reusability. - Refactored existing pages (experiments, participants, studies, trials) to utilize the new entity view components, enhancing consistency across the dashboard. - Improved accessibility and user experience by implementing loading states and error boundaries in async operations. - Updated package dependencies to ensure compatibility and performance improvements. Features: - Comprehensive guidelines for component reusability and visual consistency. - Enhanced user interface with new entity view components for better organization and navigation. Breaking Changes: None - existing functionality remains intact.
This commit is contained in:
@@ -45,7 +45,7 @@ export function SystemStats() {
|
||||
{/* Total Users */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
Total Users
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -62,7 +62,7 @@ export function SystemStats() {
|
||||
{/* Total Studies */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
Studies
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -79,7 +79,7 @@ export function SystemStats() {
|
||||
{/* Total Experiments */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
Experiments
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -98,7 +98,7 @@ export function SystemStats() {
|
||||
{/* Total Trials */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
Trials
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -115,7 +115,7 @@ export function SystemStats() {
|
||||
{/* System Health */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
System Health
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -124,11 +124,11 @@ export function SystemStats() {
|
||||
<div className="flex h-3 w-3 items-center justify-center">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
{displayStats.systemHealth === "healthy" ? "Healthy" : "Issues"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
All services operational
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -137,41 +137,49 @@ export function SystemStats() {
|
||||
{/* Uptime */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
Uptime
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-sm font-medium">{displayStats.uptime}</div>
|
||||
<div className="mt-1 text-xs text-slate-500">Since last restart</div>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
Since last restart
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Storage Usage */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
Storage Used
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-sm font-medium">{displayStats.storageUsed}</div>
|
||||
<div className="mt-1 text-xs text-slate-500">Media & database</div>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
Media & database
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-600">
|
||||
<CardTitle className="text-muted-foreground text-sm font-medium">
|
||||
Recent Activity
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-slate-600">2 trials started today</div>
|
||||
<div className="text-xs text-slate-600">1 new user registered</div>
|
||||
<div className="text-xs text-slate-600">
|
||||
<div className="text-muted-foreground text-xs">
|
||||
2 trials started today
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
1 new user registered
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
3 experiments published
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
LogOut,
|
||||
MoreHorizontal,
|
||||
Settings,
|
||||
User,
|
||||
Users,
|
||||
UserCheck,
|
||||
TestTube,
|
||||
@@ -40,6 +39,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from "~/components/ui/sidebar";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "~/components/ui/avatar";
|
||||
import { Logo } from "~/components/ui/logo";
|
||||
|
||||
import { useStudyManagement } from "~/hooks/useStudyManagement";
|
||||
@@ -292,18 +292,60 @@ export function AppSidebar({
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton size="lg">
|
||||
<User className="h-4 w-4" />
|
||||
<span>{session?.user?.name ?? "User"}</span>
|
||||
<MoreHorizontal className="ml-auto h-4 w-4" />
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:h-8 group-data-[collapsible=icon]:w-8 group-data-[collapsible=icon]:justify-center"
|
||||
>
|
||||
<Avatar className="h-6 w-6 border-2 border-slate-300 group-data-[collapsible=icon]:h-6 group-data-[collapsible=icon]:w-6">
|
||||
<AvatarImage
|
||||
src={session?.user?.image ?? undefined}
|
||||
alt={session?.user?.name ?? "User"}
|
||||
/>
|
||||
<AvatarFallback className="bg-slate-600 text-xs text-white">
|
||||
{(session?.user?.name ?? session?.user?.email ?? "U")
|
||||
.charAt(0)
|
||||
.toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
||||
<span className="truncate font-semibold">
|
||||
{session?.user?.name ?? "User"}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{session?.user?.email ?? ""}
|
||||
</span>
|
||||
</div>
|
||||
<MoreHorizontal className="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-popper-anchor-width]"
|
||||
className="w-[--radix-popper-anchor-width] min-w-56 rounded-lg"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel>
|
||||
{session?.user?.name ?? "User"}
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 border-2 border-slate-300">
|
||||
<AvatarImage
|
||||
src={session?.user?.image ?? undefined}
|
||||
alt={session?.user?.name ?? "User"}
|
||||
/>
|
||||
<AvatarFallback className="bg-slate-600 text-xs text-white">
|
||||
{(session?.user?.name ?? session?.user?.email ?? "U")
|
||||
.charAt(0)
|
||||
.toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{session?.user?.name ?? "User"}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{session?.user?.email ?? ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
|
||||
@@ -70,6 +70,7 @@ export type Trial = {
|
||||
logs: number;
|
||||
};
|
||||
userRole?: "owner" | "researcher" | "wizard" | "observer";
|
||||
canAccess?: boolean;
|
||||
canEdit?: boolean;
|
||||
canDelete?: boolean;
|
||||
canExecute?: boolean;
|
||||
@@ -162,12 +163,19 @@ function TrialActionsCell({ trial }: { trial: Trial }) {
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/trials/${trial.id}`}>
|
||||
{trial.canAccess ? (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/trials/${trial.id}`}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Details
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem disabled>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Details
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
View Details (Restricted)
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{trial.canEdit && (
|
||||
<DropdownMenuItem asChild>
|
||||
@@ -272,13 +280,33 @@ export const trialsColumns: ColumnDef<Trial>[] = [
|
||||
const trial = row.original;
|
||||
return (
|
||||
<div className="max-w-[140px] min-w-0">
|
||||
<Link
|
||||
href={`/trials/${trial.id}`}
|
||||
className="block truncate font-medium hover:underline"
|
||||
title={trial.name}
|
||||
>
|
||||
{trial.name}
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
{trial.canAccess ? (
|
||||
<Link
|
||||
href={`/trials/${trial.id}`}
|
||||
className="block truncate font-medium hover:underline"
|
||||
title={trial.name}
|
||||
>
|
||||
{trial.name}
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
className="text-muted-foreground block cursor-not-allowed truncate font-medium"
|
||||
title={`${trial.name} (View access restricted)`}
|
||||
>
|
||||
{trial.name}
|
||||
</div>
|
||||
)}
|
||||
{!trial.canAccess && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-auto shrink-0 border-amber-200 bg-amber-50 text-amber-700"
|
||||
title={`Access restricted - You are an ${trial.userRole || "observer"} on this study`}
|
||||
>
|
||||
{trial.userRole === "observer" ? "View Only" : "Restricted"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -290,16 +318,28 @@ export const trialsColumns: ColumnDef<Trial>[] = [
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue("status") as Trial["status"];
|
||||
const trial = row.original;
|
||||
const config = statusConfig[status];
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`${config.className} whitespace-nowrap`}
|
||||
title={config.description}
|
||||
>
|
||||
{config.label}
|
||||
</Badge>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`${config.className} whitespace-nowrap`}
|
||||
title={config.description}
|
||||
>
|
||||
{config.label}
|
||||
</Badge>
|
||||
{trial.userRole && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
title={`Your role in this study: ${trial.userRole}`}
|
||||
>
|
||||
{trial.userRole}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
filterFn: (row, id, value: string[]) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Plus, TestTube } from "lucide-react";
|
||||
import { Plus, TestTube, Eye } from "lucide-react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { DataTable } from "~/components/ui/data-table";
|
||||
@@ -111,14 +111,19 @@ export function TrialsDataTable() {
|
||||
actions: trial._count?.events ?? 0,
|
||||
logs: trial._count?.mediaCaptures ?? 0,
|
||||
},
|
||||
userRole: undefined,
|
||||
canEdit: trial.status === "scheduled" || trial.status === "aborted",
|
||||
userRole: trial.userRole,
|
||||
canAccess: trial.canAccess ?? false,
|
||||
canEdit:
|
||||
trial.canAccess &&
|
||||
(trial.status === "scheduled" || trial.status === "aborted"),
|
||||
canDelete:
|
||||
trial.status === "scheduled" ||
|
||||
trial.status === "aborted" ||
|
||||
trial.status === "failed",
|
||||
trial.canAccess &&
|
||||
(trial.status === "scheduled" ||
|
||||
trial.status === "aborted" ||
|
||||
trial.status === "failed"),
|
||||
canExecute:
|
||||
trial.status === "scheduled" || trial.status === "in_progress",
|
||||
trial.canAccess &&
|
||||
(trial.status === "scheduled" || trial.status === "in_progress"),
|
||||
}));
|
||||
}, [trialsData]);
|
||||
|
||||
@@ -204,6 +209,29 @@ export function TrialsDataTable() {
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filteredTrials.some((trial) => !trial.canAccess) && (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<div className="rounded-full bg-amber-100 p-1">
|
||||
<Eye className="h-4 w-4 text-amber-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-amber-800">
|
||||
Limited Trial Access
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-amber-700">
|
||||
Some trials are marked as "View Only" or "Restricted" because
|
||||
you have observer-level access to their studies. Only
|
||||
researchers, wizards, and study owners can view detailed trial
|
||||
information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
columns={trialsColumns}
|
||||
data={filteredTrials}
|
||||
|
||||
260
src/components/ui/entity-view.tsx
Normal file
260
src/components/ui/entity-view.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
"use client";
|
||||
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { type ReactNode } from "react";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
|
||||
type IconName = keyof typeof LucideIcons;
|
||||
|
||||
function getIcon(iconName: IconName) {
|
||||
const Icon = LucideIcons[iconName] as React.ComponentType<{
|
||||
className?: string;
|
||||
}>;
|
||||
return Icon;
|
||||
}
|
||||
|
||||
interface EntityViewHeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon: IconName;
|
||||
status?: {
|
||||
label: string;
|
||||
variant: "default" | "secondary" | "destructive" | "outline";
|
||||
icon?: IconName;
|
||||
};
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
interface EntityViewSectionProps {
|
||||
title: string;
|
||||
icon: IconName;
|
||||
description?: string;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface EntityViewSidebarProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface EntityViewProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function EntityViewHeader({
|
||||
title,
|
||||
subtitle,
|
||||
icon,
|
||||
status,
|
||||
actions,
|
||||
}: EntityViewHeaderProps) {
|
||||
const Icon = getIcon(icon);
|
||||
const StatusIcon = status?.icon ? getIcon(status.icon) : null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-primary text-primary-foreground flex h-16 w-16 items-center justify-center rounded-lg">
|
||||
<Icon className="h-8 w-8" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-3xl font-bold">{title}</h1>
|
||||
{status && (
|
||||
<Badge variant={status.variant}>
|
||||
{StatusIcon && <StatusIcon className="mr-1 h-3 w-3" />}
|
||||
{status.label}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="text-muted-foreground text-lg">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EntityViewSection({
|
||||
title,
|
||||
icon,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
}: EntityViewSectionProps) {
|
||||
const Icon = getIcon(icon);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Icon className="h-5 w-5" />
|
||||
<span>{title}</span>
|
||||
</CardTitle>
|
||||
{actions && actions}
|
||||
</div>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function EntityViewSidebar({ children }: EntityViewSidebarProps) {
|
||||
return <div className="space-y-6">{children}</div>;
|
||||
}
|
||||
|
||||
export function EntityView({ children }: EntityViewProps) {
|
||||
return <div className="space-y-6">{children}</div>;
|
||||
}
|
||||
|
||||
// Utility component for empty states
|
||||
interface EmptyStateProps {
|
||||
icon: IconName;
|
||||
title: string;
|
||||
description: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
}: EmptyStateProps) {
|
||||
const Icon = getIcon(icon);
|
||||
|
||||
return (
|
||||
<div className="py-8 text-center">
|
||||
<Icon className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 font-medium">{title}</h3>
|
||||
<p className="text-muted-foreground mb-4 text-sm">{description}</p>
|
||||
{action && action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility component for key-value information display
|
||||
interface InfoGridProps {
|
||||
items: Array<{
|
||||
label: string;
|
||||
value: ReactNode;
|
||||
fullWidth?: boolean;
|
||||
}>;
|
||||
columns?: 1 | 2 | 3;
|
||||
}
|
||||
|
||||
export function InfoGrid({ items, columns = 2 }: InfoGridProps) {
|
||||
return (
|
||||
<div
|
||||
className={`grid gap-4 ${
|
||||
columns === 1
|
||||
? "grid-cols-1"
|
||||
: columns === 2
|
||||
? "md:grid-cols-2"
|
||||
: "md:grid-cols-2 lg:grid-cols-3"
|
||||
}`}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={item.fullWidth ? "md:col-span-full" : undefined}
|
||||
>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">
|
||||
{item.label}
|
||||
</h4>
|
||||
<div className="text-sm">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility component for statistics display
|
||||
interface StatsGridProps {
|
||||
stats: Array<{
|
||||
label: string;
|
||||
value: string | number;
|
||||
color?: "default" | "success" | "warning" | "error";
|
||||
}>;
|
||||
}
|
||||
|
||||
export function StatsGrid({ stats }: StatsGridProps) {
|
||||
const getValueColor = (color?: string) => {
|
||||
switch (color) {
|
||||
case "success":
|
||||
return "text-green-600";
|
||||
case "warning":
|
||||
return "text-amber-600";
|
||||
case "error":
|
||||
return "text-red-600";
|
||||
default:
|
||||
return "font-medium";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="flex justify-between">
|
||||
<span className="text-muted-foreground text-sm">{stat.label}:</span>
|
||||
<span className={getValueColor(stat.color)}>{stat.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility component for quick actions
|
||||
interface QuickActionsProps {
|
||||
actions: Array<{
|
||||
label: string;
|
||||
icon: IconName;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
variant?: "default" | "outline" | "secondary" | "destructive";
|
||||
}>;
|
||||
}
|
||||
|
||||
export function QuickActions({ actions }: QuickActionsProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{actions.map((action, index) => {
|
||||
const ActionIcon = getIcon(action.icon);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant={action.variant ?? "outline"}
|
||||
className="w-full justify-start"
|
||||
asChild={!!action.href}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.href ? (
|
||||
<a href={action.href}>
|
||||
<ActionIcon className="mr-2 h-4 w-4" />
|
||||
{action.label}
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<ActionIcon className="mr-2 h-4 w-4" />
|
||||
{action.label}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,39 @@
|
||||
import { Bot } from "lucide-react"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { Bot } from "lucide-react";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface LogoProps {
|
||||
className?: string
|
||||
iconSize?: "sm" | "md" | "lg"
|
||||
showText?: boolean
|
||||
className?: string;
|
||||
iconSize?: "sm" | "md" | "lg";
|
||||
showText?: boolean;
|
||||
}
|
||||
|
||||
const iconSizes = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-6 w-6",
|
||||
lg: "h-8 w-8"
|
||||
}
|
||||
md: "h-6 w-6",
|
||||
lg: "h-8 w-8",
|
||||
};
|
||||
|
||||
export function Logo({ className, iconSize = "md", showText = true }: LogoProps) {
|
||||
export function Logo({
|
||||
className,
|
||||
iconSize = "md",
|
||||
showText = true,
|
||||
}: LogoProps) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<div className="flex aspect-square items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground p-1">
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square items-center justify-center rounded-lg p-1">
|
||||
<Bot className={iconSizes[iconSize]} />
|
||||
</div>
|
||||
{showText && (
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<div className="flex items-baseline gap-0">
|
||||
<span className="text-base font-extrabold tracking-tight">HRI</span>
|
||||
<span className="text-base font-light tracking-tight">Studio</span>
|
||||
<span className="text-base font-normal tracking-tight">Studio</span>
|
||||
</div>
|
||||
<span className="truncate text-xs text-muted-foreground">Research Platform</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
Research Platform
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user