diff --git a/src/components/ui/ActionCard.tsx b/src/components/ui/ActionCard.tsx
new file mode 100644
index 0000000..b89d82d
--- /dev/null
+++ b/src/components/ui/ActionCard.tsx
@@ -0,0 +1,65 @@
+import { ReactNode } from 'react';
+import { Button } from './Button';
+import { cn } from '@/lib/utils';
+
+interface ActionCardProps {
+ title: string;
+ description?: string;
+ icon?: ReactNode;
+ onClick?: () => void;
+ href?: string;
+ variant?: 'primary' | 'secondary' | 'ghost';
+ className?: string;
+}
+
+export function ActionCard({
+ title,
+ description,
+ icon,
+ onClick,
+ href,
+ variant = 'secondary',
+ className
+}: ActionCardProps) {
+ const content = (
+
+ );
+
+ if (href) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return content;
+}
diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx
index 527609f..298ee90 100644
--- a/src/components/ui/Badge.tsx
+++ b/src/components/ui/Badge.tsx
@@ -2,11 +2,12 @@ import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface BadgeProps extends HTMLAttributes {
- variant?: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray';
+ variant?: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray' | 'outline' | 'danger' | 'warning';
+ size?: 'sm' | 'md' | 'lg';
}
const Badge = forwardRef(
- ({ className, variant = 'default', ...props }, ref) => {
+ ({ className, variant = 'default', size = 'md', ...props }, ref) => {
const variants = {
default: 'bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]',
primary: 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)] text-[var(--primary)] border border-[color-mix(in_srgb,var(--primary)_25%,var(--border))]',
@@ -17,15 +18,25 @@ const Badge = forwardRef(
yellow: 'bg-[color-mix(in_srgb,var(--yellow)_10%,transparent)] text-[var(--yellow)] border border-[color-mix(in_srgb,var(--yellow)_25%,var(--border))]',
green: 'bg-[color-mix(in_srgb,var(--green)_10%,transparent)] text-[var(--green)] border border-[color-mix(in_srgb,var(--green)_25%,var(--border))]',
blue: 'bg-[color-mix(in_srgb,var(--blue)_10%,transparent)] text-[var(--blue)] border border-[color-mix(in_srgb,var(--blue)_25%,var(--border))]',
- gray: 'bg-[color-mix(in_srgb,var(--gray)_10%,transparent)] text-[var(--gray)] border border-[color-mix(in_srgb,var(--gray)_25%,var(--border))]'
+ gray: 'bg-[color-mix(in_srgb,var(--gray)_10%,transparent)] text-[var(--gray)] border border-[color-mix(in_srgb,var(--gray)_25%,var(--border))]',
+ outline: 'bg-transparent text-[var(--foreground)] border border-[var(--border)]',
+ danger: 'bg-[color-mix(in_srgb,var(--destructive)_10%,transparent)] text-[var(--destructive)] border border-[color-mix(in_srgb,var(--destructive)_25%,var(--border))]',
+ warning: 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] text-[var(--accent)] border border-[color-mix(in_srgb,var(--accent)_25%,var(--border))]'
+ };
+
+ const sizes = {
+ sm: 'px-2 py-0.5 text-xs',
+ md: 'px-2.5 py-0.5 text-xs',
+ lg: 'px-3 py-1 text-sm'
};
return (
{
- variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'success' | 'selected';
+ variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'success' | 'selected' | 'danger';
size?: 'sm' | 'md' | 'lg';
}
@@ -14,7 +14,8 @@ const Button = forwardRef
(
ghost: 'text-[var(--foreground)] hover:bg-[var(--card-hover)]',
destructive: 'bg-[var(--destructive)] text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,transparent)]',
success: 'bg-[var(--success)] text-white hover:bg-[color-mix(in_srgb,var(--success)_90%,transparent)]',
- selected: 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)] hover:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)]'
+ selected: 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)] hover:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)]',
+ danger: 'bg-[var(--destructive)] text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,transparent)]'
};
const sizes = {
diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx
index 6d50d7d..c311bf7 100644
--- a/src/components/ui/Input.tsx
+++ b/src/components/ui/Input.tsx
@@ -3,6 +3,8 @@ import { cn } from '@/lib/utils';
interface InputProps extends InputHTMLAttributes {
variant?: 'default' | 'error';
+ label?: string;
+ error?: string;
}
const Input = forwardRef(
diff --git a/src/components/ui/MetricCard.tsx b/src/components/ui/MetricCard.tsx
new file mode 100644
index 0000000..d03bb6f
--- /dev/null
+++ b/src/components/ui/MetricCard.tsx
@@ -0,0 +1,78 @@
+import { ReactNode } from 'react';
+import { Card } from './Card';
+import { cn } from '@/lib/utils';
+
+interface MetricCardProps {
+ title: string;
+ value: string | number;
+ subtitle?: string;
+ icon?: ReactNode;
+ color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive';
+ className?: string;
+}
+
+const colorVariants = {
+ default: {
+ title: 'text-[var(--foreground)]',
+ value: 'text-[var(--foreground)]',
+ border: 'border-[var(--border)] hover:border-[var(--primary)]/50'
+ },
+ primary: {
+ title: 'text-[var(--primary)]',
+ value: 'text-[var(--foreground)]',
+ border: 'border-[var(--border)] hover:border-[var(--primary)]/50'
+ },
+ success: {
+ title: 'text-[var(--success)]',
+ value: 'text-[var(--foreground)]',
+ border: 'border-[var(--border)] hover:border-[var(--success)]/50'
+ },
+ warning: {
+ title: 'text-[var(--accent)]',
+ value: 'text-[var(--foreground)]',
+ border: 'border-[var(--border)] hover:border-[var(--accent)]/50'
+ },
+ destructive: {
+ title: 'text-[var(--destructive)]',
+ value: 'text-[var(--foreground)]',
+ border: 'border-[var(--border)] hover:border-[var(--destructive)]/50'
+ }
+};
+
+export function MetricCard({
+ title,
+ value,
+ subtitle,
+ icon,
+ color = 'default',
+ className
+}: MetricCardProps) {
+ const colors = colorVariants[color];
+
+ return (
+
+
+
+ {title}
+
+
+ {value}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ui/ProgressBar.tsx b/src/components/ui/ProgressBar.tsx
new file mode 100644
index 0000000..ff15d02
--- /dev/null
+++ b/src/components/ui/ProgressBar.tsx
@@ -0,0 +1,56 @@
+import { cn } from '@/lib/utils';
+
+interface ProgressBarProps {
+ value: number; // 0-100
+ label?: string;
+ color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive';
+ showPercentage?: boolean;
+ className?: string;
+}
+
+const colorVariants = {
+ default: 'bg-[var(--primary)]',
+ primary: 'bg-[var(--primary)]',
+ success: 'bg-[var(--success)]',
+ warning: 'bg-[var(--accent)]',
+ destructive: 'bg-[var(--destructive)]'
+};
+
+export function ProgressBar({
+ value,
+ label,
+ color = 'default',
+ showPercentage = true,
+ className
+}: ProgressBarProps) {
+ const clampedValue = Math.min(Math.max(value, 0), 100);
+
+ return (
+
+ {(label || showPercentage) && (
+
+ {label && (
+
+ {label}
+
+ )}
+ {showPercentage && (
+
+ {Math.round(clampedValue)}%
+
+ )}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/ui/StatCard.tsx b/src/components/ui/StatCard.tsx
new file mode 100644
index 0000000..a37457f
--- /dev/null
+++ b/src/components/ui/StatCard.tsx
@@ -0,0 +1,47 @@
+import { ReactNode } from 'react';
+import { Card } from './Card';
+import { cn } from '@/lib/utils';
+
+interface StatCardProps {
+ title: string;
+ value: number | string;
+ icon?: ReactNode;
+ color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive';
+ className?: string;
+}
+
+const colorVariants = {
+ default: 'text-[var(--foreground)]',
+ primary: 'text-[var(--primary)]',
+ success: 'text-[var(--success)]',
+ warning: 'text-[var(--accent)]',
+ destructive: 'text-[var(--destructive)]'
+};
+
+export function StatCard({
+ title,
+ value,
+ icon,
+ color = 'default',
+ className
+}: StatCardProps) {
+ return (
+
+
+
+
+ {title}
+
+
+ {value}
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ui/TaskCard.tsx b/src/components/ui/TaskCard.tsx
new file mode 100644
index 0000000..36dc8f2
--- /dev/null
+++ b/src/components/ui/TaskCard.tsx
@@ -0,0 +1,83 @@
+import { ReactNode } from 'react';
+import { Badge } from './Badge';
+import { cn } from '@/lib/utils';
+
+interface TaskCardProps {
+ title: string;
+ description?: string;
+ status?: string;
+ priority?: string;
+ tags?: ReactNode[];
+ metadata?: ReactNode;
+ actions?: ReactNode;
+ className?: string;
+}
+
+export function TaskCard({
+ title,
+ description,
+ status,
+ priority,
+ tags,
+ metadata,
+ actions,
+ className
+}: TaskCardProps) {
+ return (
+
+
+
+
+
+ {title}
+
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {status && (
+
+ {status}
+
+ )}
+
+ {priority && (
+
+ {priority}
+
+ )}
+
+ {tags && tags.length > 0 && (
+
+ {tags}
+
+ )}
+
+
+
+
+ {metadata && (
+
+ {metadata}
+
+ )}
+ {actions && (
+
+ {actions}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
index d54f3bd..4adc60b 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -5,6 +5,13 @@ export { Alert, AlertTitle, AlertDescription } from './Alert';
export { Input } from './Input';
export { StyledCard } from './StyledCard';
+// Composants Dashboard
+export { StatCard } from './StatCard';
+export { ProgressBar } from './ProgressBar';
+export { ActionCard } from './ActionCard';
+export { TaskCard } from './TaskCard';
+export { MetricCard } from './MetricCard';
+
// Composants existants
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
export { Header } from './Header';
diff --git a/src/services/core/user-preferences.ts b/src/services/core/user-preferences.ts
index ee65e99..4649573 100644
--- a/src/services/core/user-preferences.ts
+++ b/src/services/core/user-preferences.ts
@@ -5,6 +5,7 @@ import {
ColumnVisibility,
UserPreferences,
JiraConfig,
+ Theme,
} from '@/lib/types';
import { TfsConfig } from '@/services/integrations/tfs';
import { prisma } from './database';
@@ -213,7 +214,7 @@ class UserPreferencesService {
/**
* Récupère uniquement le thème pour le SSR (optimisé)
*/
- async getTheme(): Promise<'light' | 'dark'> {
+ async getTheme(): Promise {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const viewPrefs = userPrefs.viewPreferences as ViewPreferences;