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 React, { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
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 { PencilEdit01Icon } from '@zen/core/shared/icons';
|
||||||
import { useToast } from '@zen/core/toast';
|
import { useToast } from '@zen/core/toast';
|
||||||
|
|
||||||
@@ -185,18 +185,12 @@ const UsersPageClient = () => {
|
|||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
emptyMessage="Aucun utilisateur trouvé"
|
emptyMessage="Aucun utilisateur trouvé"
|
||||||
emptyDescription="La base de données est vide"
|
emptyDescription="La base de données est vide"
|
||||||
/>
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
currentPage={pagination.page}
|
currentPage={pagination.page}
|
||||||
totalPages={pagination.totalPages}
|
totalPages={pagination.totalPages}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onLimitChange={handleLimitChange}
|
onLimitChange={handleLimitChange}
|
||||||
limit={pagination.limit}
|
limit={pagination.limit}
|
||||||
total={pagination.total}
|
total={pagination.total}
|
||||||
loading={loading}
|
|
||||||
showPerPage={true}
|
|
||||||
showStats={true}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ const Badge = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
sm: 'px-2 py-0.5 rounded text-[12px]',
|
sm: 'px-2 py-0.5 rounded-lg text-[12px]',
|
||||||
md: 'px-2.5 py-0.5 rounded text-[12px]',
|
md: 'px-2.5 py-0.5 rounded-lg text-[12px]',
|
||||||
lg: 'px-3 py-1 rounded text-xs'
|
lg: 'px-3 py-1 rounded-lg text-xs'
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const Button = ({
|
|||||||
className = '',
|
className = '',
|
||||||
...props
|
...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 = {
|
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',
|
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',
|
||||||
|
|||||||
+148
-52
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Badge from './Badge';
|
import Badge from './Badge';
|
||||||
|
import Button from './Button';
|
||||||
import { TorriGateIcon } from '../icons/index.js';
|
import { TorriGateIcon } from '../icons/index.js';
|
||||||
|
|
||||||
const ROW_SIZE = {
|
const ROW_SIZE = {
|
||||||
@@ -23,18 +24,26 @@ const Table = ({
|
|||||||
emptyDescription = 'Aucun élément à afficher',
|
emptyDescription = 'Aucun élément à afficher',
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
className = '',
|
className = '',
|
||||||
|
// Pagination props
|
||||||
|
currentPage = 1,
|
||||||
|
totalPages = 1,
|
||||||
|
onPageChange,
|
||||||
|
onLimitChange,
|
||||||
|
limit = 20,
|
||||||
|
total = 0,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const sizeClasses = ROW_SIZE[size] ?? ROW_SIZE.md;
|
const sizeClasses = ROW_SIZE[size] ?? ROW_SIZE.md;
|
||||||
|
const showPagination = total > 20;
|
||||||
|
|
||||||
const SortIcon = ({ column }) => {
|
const SortIcon = ({ column }) => {
|
||||||
const isActive = sortBy === column.key;
|
const isActive = sortBy === column.key;
|
||||||
const isDesc = isActive && sortOrder === 'desc';
|
const isDesc = isActive && sortOrder === 'desc';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="ml-1">
|
<span className="ml-1">
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 transition-transform ${isActive ? (isDesc ? 'rotate-180' : '') : 'text-neutral-500 dark:text-neutral-400'}`}
|
className={`w-4 h-4 transition-transform ${isActive ? (isDesc ? 'rotate-180' : '') : 'text-neutral-500 dark:text-neutral-400'}`}
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
>
|
>
|
||||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
@@ -43,9 +52,9 @@ const Table = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Skeleton = ({ className = "", width = "100%", height = "h-4" }) => (
|
const Skeleton = ({ className: cx = '', width = '100%', height = 'h-4' }) => (
|
||||||
<div
|
<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 }}
|
style={{ width }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -57,22 +66,11 @@ const Table = ({
|
|||||||
{column.skeleton ? (
|
{column.skeleton ? (
|
||||||
column.skeleton.secondary ? (
|
column.skeleton.secondary ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton
|
<Skeleton cx={column.skeleton.className} height={column.skeleton.height} width={column.skeleton.width} />
|
||||||
className={column.skeleton.className}
|
<Skeleton height={column.skeleton.secondary.height} width={column.skeleton.secondary.width} />
|
||||||
height={column.skeleton.height}
|
|
||||||
width={column.skeleton.width}
|
|
||||||
/>
|
|
||||||
<Skeleton
|
|
||||||
height={column.skeleton.secondary.height}
|
|
||||||
width={column.skeleton.secondary.width}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton
|
<Skeleton cx={column.skeleton.className} height={column.skeleton.height} width={column.skeleton.width} />
|
||||||
className={column.skeleton.className}
|
|
||||||
height={column.skeleton.height}
|
|
||||||
width={column.skeleton.width}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Skeleton height="h-4" width="60%" />
|
<Skeleton height="h-4" width="60%" />
|
||||||
@@ -95,24 +93,11 @@ const Table = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renderCellContent = (item, column) => {
|
const renderCellContent = (item, column) => {
|
||||||
if (column.render) {
|
if (column.render) return column.render(item);
|
||||||
return column.render(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = column.key.split('.').reduce((obj, key) => obj?.[key], 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 === 'badge') {
|
if (column.type === 'date') return new Date(value).toLocaleDateString();
|
||||||
return <Badge variant={column.badgeVariant}>{value}</Badge>;
|
if (column.type === 'currency') return `$${parseFloat(value || 0).toFixed(2)}`;
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'date') {
|
|
||||||
return new Date(value).toLocaleDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'currency') {
|
|
||||||
return `$${parseFloat(value || 0).toFixed(2)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value || '-';
|
return value || '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,9 +113,7 @@ const Table = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 ml-4">
|
<div className="flex flex-col gap-2 ml-4">
|
||||||
{columns.slice(2, 4).map((column) => (
|
{columns.slice(2, 4).map((column) => (
|
||||||
<div key={column.key}>
|
<div key={column.key}>{renderCellContent(item, column)}</div>
|
||||||
{renderCellContent(item, column)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,14 +137,131 @@ const Table = ({
|
|||||||
<Skeleton height="h-3" width="40%" />
|
<Skeleton height="h-3" width="40%" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 ml-4">
|
<div className="flex flex-col gap-2 ml-4">
|
||||||
<Skeleton className="rounded-full" height="h-6" width="80px" />
|
<Skeleton cx="rounded-full" height="h-6" width="80px" />
|
||||||
<Skeleton className="rounded-full" height="h-6" width="90px" />
|
<Skeleton cx="rounded-full" height="h-6" width="90px" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Skeleton height="h-3" width="50%" />
|
<Skeleton height="h-3" width="50%" />
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<div className={className} {...props}>
|
<div className={className} {...props}>
|
||||||
{/* Desktop Table */}
|
{/* Desktop Table */}
|
||||||
@@ -187,9 +287,7 @@ const Table = ({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-700/30 text-[13px]">
|
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-700/30 text-[13px]">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
Array.from({ length: 5 }).map((_, index) => (
|
Array.from({ length: 5 }).map((_, index) => <SkeletonRow key={index} />)
|
||||||
<SkeletonRow key={index} />
|
|
||||||
))
|
|
||||||
) : data.length === 0 ? (
|
) : data.length === 0 ? (
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
) : (
|
) : (
|
||||||
@@ -218,15 +316,11 @@ const Table = ({
|
|||||||
{/* Mobile Cards */}
|
{/* Mobile Cards */}
|
||||||
<div className="lg:hidden divide-y divide-neutral-200 dark:divide-neutral-700/30">
|
<div className="lg:hidden divide-y divide-neutral-200 dark:divide-neutral-700/30">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
Array.from({ length: 3 }).map((_, index) => (
|
Array.from({ length: 3 }).map((_, index) => <MobileSkeletonCard key={index} />)
|
||||||
<MobileSkeletonCard key={index} />
|
|
||||||
))
|
|
||||||
) : data.length === 0 ? (
|
) : data.length === 0 ? (
|
||||||
<div className={`${sizeClasses.mobile} py-12 text-center`}>
|
<div className={`${sizeClasses.mobile} py-12 text-center`}>
|
||||||
<div className="text-neutral-500 dark:text-neutral-400 mb-2">
|
<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">
|
<TorriGateIcon className="mx-auto h-12 w-12 mb-4" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-medium text-neutral-700 dark:text-neutral-300 mb-1">{emptyMessage}</p>
|
<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>
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">{emptyDescription}</p>
|
||||||
@@ -247,8 +341,10 @@ const Table = ({
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showPagination && <PaginationBar />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Table;
|
export default Table;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export {
|
|||||||
} from './LoadingState';
|
} from './LoadingState';
|
||||||
export { default as Loading } from './Loading';
|
export { default as Loading } from './Loading';
|
||||||
export { default as Modal } from './Modal';
|
export { default as Modal } from './Modal';
|
||||||
export { default as Pagination } from './Pagination';
|
|
||||||
export { default as Select } from './Select';
|
export { default as Select } from './Select';
|
||||||
export { default as StatCard } from './StatCard';
|
export { default as StatCard } from './StatCard';
|
||||||
export { default as Table } from './Table';
|
export { default as Table } from './Table';
|
||||||
|
|||||||
Reference in New Issue
Block a user