fix(ui): fix missing space between rounded-lg and transition-all in Button class
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, Table, StatusBadge, Pagination, Button } from '@zen/core/shared/components';
|
||||
import { Card, Table, StatusBadge, Button } from '@zen/core/shared/components';
|
||||
import { PencilEdit01Icon } from '@zen/core/shared/icons';
|
||||
import { useToast } from '@zen/core/toast';
|
||||
|
||||
@@ -185,18 +185,12 @@ const UsersPageClient = () => {
|
||||
onSort={handleSort}
|
||||
emptyMessage="Aucun utilisateur trouvé"
|
||||
emptyDescription="La base de données est vide"
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
currentPage={pagination.page}
|
||||
totalPages={pagination.totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
limit={pagination.limit}
|
||||
total={pagination.total}
|
||||
loading={loading}
|
||||
showPerPage={true}
|
||||
showStats={true}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,9 @@ const Badge = ({
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-2 py-0.5 rounded text-[12px]',
|
||||
md: 'px-2.5 py-0.5 rounded text-[12px]',
|
||||
lg: 'px-3 py-1 rounded text-xs'
|
||||
sm: 'px-2 py-0.5 rounded-lg text-[12px]',
|
||||
md: 'px-2.5 py-0.5 rounded-lg text-[12px]',
|
||||
lg: 'px-3 py-1 rounded-lg text-xs'
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ const Button = ({
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const baseClassName = 'cursor-pointer inline-flex items-center justify-center font-medium rounded-lgtransition-all duration-[120ms] ease-out focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
const baseClassName = 'cursor-pointer inline-flex items-center justify-center font-medium rounded-lg transition-all duration-[120ms] ease-out focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-neutral-900 text-white hover:bg-neutral-800 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20',
|
||||
|
||||
+144
-48
@@ -2,6 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import Badge from './Badge';
|
||||
import Button from './Button';
|
||||
import { TorriGateIcon } from '../icons/index.js';
|
||||
|
||||
const ROW_SIZE = {
|
||||
@@ -23,13 +24,21 @@ const Table = ({
|
||||
emptyDescription = 'Aucun élément à afficher',
|
||||
size = 'sm',
|
||||
className = '',
|
||||
// Pagination props
|
||||
currentPage = 1,
|
||||
totalPages = 1,
|
||||
onPageChange,
|
||||
onLimitChange,
|
||||
limit = 20,
|
||||
total = 0,
|
||||
...props
|
||||
}) => {
|
||||
const sizeClasses = ROW_SIZE[size] ?? ROW_SIZE.md;
|
||||
const showPagination = total > 20;
|
||||
|
||||
const SortIcon = ({ column }) => {
|
||||
const isActive = sortBy === column.key;
|
||||
const isDesc = isActive && sortOrder === 'desc';
|
||||
|
||||
return (
|
||||
<span className="ml-1">
|
||||
<svg
|
||||
@@ -43,9 +52,9 @@ const Table = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Skeleton = ({ className = "", width = "100%", height = "h-4" }) => (
|
||||
const Skeleton = ({ className: cx = '', width = '100%', height = 'h-4' }) => (
|
||||
<div
|
||||
className={`bg-gradient-to-r from-neutral-200 via-neutral-100 to-neutral-200 dark:from-neutral-700/30 dark:via-neutral-600/30 dark:to-neutral-700/30 bg-[length:200%_100%] animate-shimmer rounded ${height} ${className}`}
|
||||
className={`bg-gradient-to-r from-neutral-200 via-neutral-100 to-neutral-200 dark:from-neutral-700/30 dark:via-neutral-600/30 dark:to-neutral-700/30 bg-[length:200%_100%] animate-shimmer rounded ${height} ${cx}`}
|
||||
style={{ width }}
|
||||
/>
|
||||
);
|
||||
@@ -57,22 +66,11 @@ const Table = ({
|
||||
{column.skeleton ? (
|
||||
column.skeleton.secondary ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton
|
||||
className={column.skeleton.className}
|
||||
height={column.skeleton.height}
|
||||
width={column.skeleton.width}
|
||||
/>
|
||||
<Skeleton
|
||||
height={column.skeleton.secondary.height}
|
||||
width={column.skeleton.secondary.width}
|
||||
/>
|
||||
<Skeleton cx={column.skeleton.className} height={column.skeleton.height} width={column.skeleton.width} />
|
||||
<Skeleton height={column.skeleton.secondary.height} width={column.skeleton.secondary.width} />
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton
|
||||
className={column.skeleton.className}
|
||||
height={column.skeleton.height}
|
||||
width={column.skeleton.width}
|
||||
/>
|
||||
<Skeleton cx={column.skeleton.className} height={column.skeleton.height} width={column.skeleton.width} />
|
||||
)
|
||||
) : (
|
||||
<Skeleton height="h-4" width="60%" />
|
||||
@@ -95,24 +93,11 @@ const Table = ({
|
||||
);
|
||||
|
||||
const renderCellContent = (item, column) => {
|
||||
if (column.render) {
|
||||
return column.render(item);
|
||||
}
|
||||
|
||||
if (column.render) return column.render(item);
|
||||
const value = column.key.split('.').reduce((obj, key) => obj?.[key], item);
|
||||
|
||||
if (column.type === 'badge') {
|
||||
return <Badge variant={column.badgeVariant}>{value}</Badge>;
|
||||
}
|
||||
|
||||
if (column.type === 'date') {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
|
||||
if (column.type === 'currency') {
|
||||
return `$${parseFloat(value || 0).toFixed(2)}`;
|
||||
}
|
||||
|
||||
if (column.type === 'badge') return <Badge variant={column.badgeVariant}>{value}</Badge>;
|
||||
if (column.type === 'date') return new Date(value).toLocaleDateString();
|
||||
if (column.type === 'currency') return `$${parseFloat(value || 0).toFixed(2)}`;
|
||||
return value || '-';
|
||||
};
|
||||
|
||||
@@ -128,9 +113,7 @@ const Table = ({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 ml-4">
|
||||
{columns.slice(2, 4).map((column) => (
|
||||
<div key={column.key}>
|
||||
{renderCellContent(item, column)}
|
||||
</div>
|
||||
<div key={column.key}>{renderCellContent(item, column)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,14 +137,131 @@ const Table = ({
|
||||
<Skeleton height="h-3" width="40%" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 ml-4">
|
||||
<Skeleton className="rounded-full" height="h-6" width="80px" />
|
||||
<Skeleton className="rounded-full" height="h-6" width="90px" />
|
||||
<Skeleton cx="rounded-full" height="h-6" width="80px" />
|
||||
<Skeleton cx="rounded-full" height="h-6" width="90px" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton height="h-3" width="50%" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
const delta = 2;
|
||||
if (totalPages > 0) pages.push(1);
|
||||
if (currentPage - delta > 2) pages.push('...');
|
||||
for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
if (currentPage + delta < totalPages - 1) pages.push('...');
|
||||
if (totalPages > 1) pages.push(totalPages);
|
||||
return pages;
|
||||
};
|
||||
|
||||
const PageButton = ({ onClick, disabled, children, isActive = false }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
className={`px-2 py-1 text-xs font-medium rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
isActive
|
||||
? 'bg-neutral-900 text-white dark:bg-white dark:text-black'
|
||||
: 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-700/30 hover:text-neutral-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
const from = total > 0 ? (currentPage - 1) * limit + 1 : 0;
|
||||
const to = Math.min(currentPage * limit, total);
|
||||
|
||||
const PaginationBar = () => (
|
||||
<div className="px-4 py-3 border-t border-neutral-200 dark:border-neutral-700/30">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{loading ? (
|
||||
<Skeleton height="h-7" width="90px" />
|
||||
) : (
|
||||
<>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">Afficher</span>
|
||||
<select
|
||||
value={limit}
|
||||
onChange={(e) => onLimitChange?.(Number(e.target.value))}
|
||||
disabled={loading}
|
||||
className="h-7 px-2 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700/50 rounded-md text-xs text-neutral-900 dark:text-white focus:outline-none focus:border-neutral-400 dark:focus:border-neutral-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{loading ? (
|
||||
<>
|
||||
<Skeleton height="h-7" width="64px" />
|
||||
<div className="hidden sm:flex items-center gap-1">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} height="h-7" width="28px" />
|
||||
))}
|
||||
</div>
|
||||
<Skeleton height="h-7" width="56px" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{totalPages > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Précédent
|
||||
</Button>
|
||||
)}
|
||||
{totalPages > 1 && (
|
||||
<div className="hidden sm:flex items-center gap-1">
|
||||
{getPageNumbers().map((page, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{page === '...' ? (
|
||||
<span className="px-2 text-xs text-neutral-400 dark:text-neutral-500">…</span>
|
||||
) : (
|
||||
<PageButton onClick={() => onPageChange(page)} isActive={currentPage === page}>
|
||||
{page}
|
||||
</PageButton>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{totalPages > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Suivant
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-neutral-400 dark:text-neutral-500 shrink-0 text-right">
|
||||
{loading ? (
|
||||
<Skeleton height="h-4" width="100px" />
|
||||
) : (
|
||||
`${from}–${to} sur ${total}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{/* Desktop Table */}
|
||||
@@ -187,9 +287,7 @@ const Table = ({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-700/30 text-[13px]">
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<SkeletonRow key={index} />
|
||||
))
|
||||
Array.from({ length: 5 }).map((_, index) => <SkeletonRow key={index} />)
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
@@ -218,15 +316,11 @@ const Table = ({
|
||||
{/* Mobile Cards */}
|
||||
<div className="lg:hidden divide-y divide-neutral-200 dark:divide-neutral-700/30">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, index) => (
|
||||
<MobileSkeletonCard key={index} />
|
||||
))
|
||||
Array.from({ length: 3 }).map((_, index) => <MobileSkeletonCard key={index} />)
|
||||
) : data.length === 0 ? (
|
||||
<div className={`${sizeClasses.mobile} py-12 text-center`}>
|
||||
<div className="text-neutral-500 dark:text-neutral-400 mb-2">
|
||||
<svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2M4 13h2m13-8V4a1 1 0 00-1-1H7a1 1 0 00-1 1v1m8 0V4.5" />
|
||||
</svg>
|
||||
<TorriGateIcon className="mx-auto h-12 w-12 mb-4" />
|
||||
</div>
|
||||
<p className="text-lg font-medium text-neutral-700 dark:text-neutral-300 mb-1">{emptyMessage}</p>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">{emptyDescription}</p>
|
||||
@@ -247,6 +341,8 @@ const Table = ({
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPagination && <PaginationBar />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ export {
|
||||
} from './LoadingState';
|
||||
export { default as Loading } from './Loading';
|
||||
export { default as Modal } from './Modal';
|
||||
export { default as Pagination } from './Pagination';
|
||||
export { default as Select } from './Select';
|
||||
export { default as StatCard } from './StatCard';
|
||||
export { default as Table } from './Table';
|
||||
|
||||
Reference in New Issue
Block a user