From c8978fe989b7cf42e3a353fae28f7b5310062297 Mon Sep 17 00:00:00 2001 From: Renato Teixeira Date: Sun, 29 Mar 2026 10:50:49 -0300 Subject: [PATCH 1/7] feat: add TableLoading component with context prop and export from index --- .../Table/TableLoading/TableLoading.tsx | 16 ++++++++++++++++ .../Table/TableLoading/TableLoading.types.ts | 3 +++ .../components/Table/TableLoading/index.ts | 1 + 3 files changed, 20 insertions(+) create mode 100644 src/shared/components/Table/TableLoading/TableLoading.tsx create mode 100644 src/shared/components/Table/TableLoading/TableLoading.types.ts create mode 100644 src/shared/components/Table/TableLoading/index.ts diff --git a/src/shared/components/Table/TableLoading/TableLoading.tsx b/src/shared/components/Table/TableLoading/TableLoading.tsx new file mode 100644 index 0000000..769232c --- /dev/null +++ b/src/shared/components/Table/TableLoading/TableLoading.tsx @@ -0,0 +1,16 @@ +import type { TableLoadingProps } from "./TableLoading.types"; + +export function TableLoading({ context = "" }: TableLoadingProps) { + return ( +
+
+ ); +} diff --git a/src/shared/components/Table/TableLoading/TableLoading.types.ts b/src/shared/components/Table/TableLoading/TableLoading.types.ts new file mode 100644 index 0000000..2d2edd2 --- /dev/null +++ b/src/shared/components/Table/TableLoading/TableLoading.types.ts @@ -0,0 +1,3 @@ +export interface TableLoadingProps { + context?: string; +} diff --git a/src/shared/components/Table/TableLoading/index.ts b/src/shared/components/Table/TableLoading/index.ts new file mode 100644 index 0000000..d8e426b --- /dev/null +++ b/src/shared/components/Table/TableLoading/index.ts @@ -0,0 +1 @@ +export { TableLoading } from "./TableLoading"; From f3d0975bf0522baf2f300196801f44e2af03a32e Mon Sep 17 00:00:00 2001 From: Renato Teixeira Date: Sun, 29 Mar 2026 10:51:07 -0300 Subject: [PATCH 2/7] feat: add TableEmpty component with context and emptyState props and export from index --- src/shared/components/Table/TableEmpty/TableEmpty.tsx | 11 +++++++++++ .../components/Table/TableEmpty/TableEmpty.types.ts | 4 ++++ src/shared/components/Table/TableEmpty/index.ts | 1 + 3 files changed, 16 insertions(+) create mode 100644 src/shared/components/Table/TableEmpty/TableEmpty.tsx create mode 100644 src/shared/components/Table/TableEmpty/TableEmpty.types.ts create mode 100644 src/shared/components/Table/TableEmpty/index.ts diff --git a/src/shared/components/Table/TableEmpty/TableEmpty.tsx b/src/shared/components/Table/TableEmpty/TableEmpty.tsx new file mode 100644 index 0000000..fafadcd --- /dev/null +++ b/src/shared/components/Table/TableEmpty/TableEmpty.tsx @@ -0,0 +1,11 @@ +import type { TableEmptyProps } from "./TableEmpty.types"; + +export function TableEmpty({ context, emptyState }: TableEmptyProps) { + return ( +
+

+ {emptyState ?? (context ? `No ${context} found` : "No registers found")} +

+
+ ); +} diff --git a/src/shared/components/Table/TableEmpty/TableEmpty.types.ts b/src/shared/components/Table/TableEmpty/TableEmpty.types.ts new file mode 100644 index 0000000..0158e91 --- /dev/null +++ b/src/shared/components/Table/TableEmpty/TableEmpty.types.ts @@ -0,0 +1,4 @@ +export interface TableEmptyProps { + context?: string; + emptyState?: string; +} diff --git a/src/shared/components/Table/TableEmpty/index.ts b/src/shared/components/Table/TableEmpty/index.ts new file mode 100644 index 0000000..f58bbed --- /dev/null +++ b/src/shared/components/Table/TableEmpty/index.ts @@ -0,0 +1 @@ +export { TableEmpty } from "./TableEmpty"; From 52d370d852aa5c17de794e85a83b59a16a7f2d4e Mon Sep 17 00:00:00 2001 From: Renato Teixeira Date: Sun, 29 Mar 2026 10:51:25 -0300 Subject: [PATCH 3/7] feat: add TableHead component with search and page size controls and export from index --- .../components/Table/TableHead/TableHead.tsx | 40 +++++++++++++++++++ .../Table/TableHead/TableHead.types.ts | 8 ++++ .../components/Table/TableHead/index.ts | 1 + 3 files changed, 49 insertions(+) create mode 100644 src/shared/components/Table/TableHead/TableHead.tsx create mode 100644 src/shared/components/Table/TableHead/TableHead.types.ts create mode 100644 src/shared/components/Table/TableHead/index.ts diff --git a/src/shared/components/Table/TableHead/TableHead.tsx b/src/shared/components/Table/TableHead/TableHead.tsx new file mode 100644 index 0000000..ba234e7 --- /dev/null +++ b/src/shared/components/Table/TableHead/TableHead.tsx @@ -0,0 +1,40 @@ +import { InputField } from "@/shared/components/InputField"; +import type { TableHeadProps } from "./TableHead.types"; +import { SearchIcon } from "lucide-react"; + +export function TableHead({ searchable, query, pageSize, pageSizeOptions, setQuery, setPageSize }: TableHeadProps) { + return ( +
+
+ {searchable && ( + } + value={query} + onChange={(e) => setQuery(e.target.value)} + /> + )} +
+ +
+ + + +
+
+ ); +} diff --git a/src/shared/components/Table/TableHead/TableHead.types.ts b/src/shared/components/Table/TableHead/TableHead.types.ts new file mode 100644 index 0000000..67b6f65 --- /dev/null +++ b/src/shared/components/Table/TableHead/TableHead.types.ts @@ -0,0 +1,8 @@ +export interface TableHeadProps { + searchable: boolean; + query: string; + pageSize: number; + pageSizeOptions: number[]; + setQuery: (q: string) => void; + setPageSize: (size: number) => void; +} diff --git a/src/shared/components/Table/TableHead/index.ts b/src/shared/components/Table/TableHead/index.ts new file mode 100644 index 0000000..fa648d8 --- /dev/null +++ b/src/shared/components/Table/TableHead/index.ts @@ -0,0 +1 @@ +export { TableHead } from "./TableHead"; From ebf3a5dbffe5ef433b603670c41730b1d855884f Mon Sep 17 00:00:00 2001 From: Renato Teixeira Date: Sun, 29 Mar 2026 10:51:42 -0300 Subject: [PATCH 4/7] feat: add TablePagination component with navigation controls and export from index --- .../Table/TablePagination/TablePagination.tsx | 34 +++++++++++++++++++ .../TablePagination/TablePagination.types.ts | 6 ++++ .../components/Table/TablePagination/index.ts | 1 + 3 files changed, 41 insertions(+) create mode 100644 src/shared/components/Table/TablePagination/TablePagination.tsx create mode 100644 src/shared/components/Table/TablePagination/TablePagination.types.ts create mode 100644 src/shared/components/Table/TablePagination/index.ts diff --git a/src/shared/components/Table/TablePagination/TablePagination.tsx b/src/shared/components/Table/TablePagination/TablePagination.tsx new file mode 100644 index 0000000..7c3c964 --- /dev/null +++ b/src/shared/components/Table/TablePagination/TablePagination.tsx @@ -0,0 +1,34 @@ +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import type { TablePaginationProps } from "./TablePagination.types"; + +export function TablePagination({ page, pageSize, total, setPage }: TablePaginationProps) { + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + + return ( +
+
+ Showing {(page - 1) * pageSize + 1}–{Math.min(page * pageSize, total)} of {total} +
+ +
+ +
+ {page} / {totalPages} +
+ +
+
+ ); +} diff --git a/src/shared/components/Table/TablePagination/TablePagination.types.ts b/src/shared/components/Table/TablePagination/TablePagination.types.ts new file mode 100644 index 0000000..f9df4bb --- /dev/null +++ b/src/shared/components/Table/TablePagination/TablePagination.types.ts @@ -0,0 +1,6 @@ +export interface TablePaginationProps { + page: number; + pageSize: number; + total: number; + setPage: (page: number | ((prev: number) => number)) => void; +} diff --git a/src/shared/components/Table/TablePagination/index.ts b/src/shared/components/Table/TablePagination/index.ts new file mode 100644 index 0000000..6b955fc --- /dev/null +++ b/src/shared/components/Table/TablePagination/index.ts @@ -0,0 +1 @@ +export { TablePagination } from "./TablePagination"; From 06e9097aa7ecb11c12dd4e542439e1ab60ca6c7f Mon Sep 17 00:00:00 2001 From: Renato Teixeira Date: Sun, 29 Mar 2026 10:51:54 -0300 Subject: [PATCH 5/7] feat: add Table component with sorting, pagination, search, and customizable columns --- .../components/Table/Table/Table.styles.ts | 26 +++ src/shared/components/Table/Table/Table.tsx | 214 ++++++++++++++++++ .../components/Table/Table/Table.types.ts | 34 +++ src/shared/components/Table/Table/index.ts | 1 + src/shared/components/Table/index.ts | 1 + 5 files changed, 276 insertions(+) create mode 100644 src/shared/components/Table/Table/Table.styles.ts create mode 100644 src/shared/components/Table/Table/Table.tsx create mode 100644 src/shared/components/Table/Table/Table.types.ts create mode 100644 src/shared/components/Table/Table/index.ts create mode 100644 src/shared/components/Table/index.ts diff --git a/src/shared/components/Table/Table/Table.styles.ts b/src/shared/components/Table/Table/Table.styles.ts new file mode 100644 index 0000000..36fced0 --- /dev/null +++ b/src/shared/components/Table/Table/Table.styles.ts @@ -0,0 +1,26 @@ +import clsx from "clsx"; + +export const rowPaddingClass = "px-4 py-3"; + +export const thBaseClass = "sticky top-0 whitespace-nowrap"; +export const thBgClass = "bg-muted"; +export const thTextClass = "text-muted-foreground text-sm font-medium tracking-wider uppercase"; +export const thColClass = "z-10"; +export const thActionColClass = "z-20 right-0 text-left"; + +export const tdBaseClass = "overflow-hidden text-ellipsis whitespace-nowrap min-w-26"; +export const tdBgClass = "bg-card group-hover:bg-card-hover"; +export const tdTextClass = "text-muted-foreground text-sm"; +export const tdActionColClass = "sticky right-0 w-28 bg-card-hover"; + +export const sortButtonClass = "text-muted-foreground hover:text-foreground hover:cursor-pointer"; + +export function cellAlignClass(align?: "left" | "center" | "right") { + if (align === "center") return "text-center"; + if (align === "right") return "text-right"; + return "text-left"; +} + +export function getTableClasses(...parts: Array) { + return clsx(...parts); +} diff --git a/src/shared/components/Table/Table/Table.tsx b/src/shared/components/Table/Table/Table.tsx new file mode 100644 index 0000000..f4c6c87 --- /dev/null +++ b/src/shared/components/Table/Table/Table.tsx @@ -0,0 +1,214 @@ +import { TableEmpty } from "../TableEmpty"; +import { TableHead } from "../TableHead"; +import { TableLoading } from "../TableLoading"; +import { TablePagination } from "../TablePagination"; +import { useEffect, useMemo, useState } from "react"; +import type { TableColumn, SortState, TableProps, TableRow } from "./Table.types"; +import * as S from "./Table.styles"; + +export function Table(props: TableProps) { + const { + columns, + data, + context, + defaultPageSize = 10, + pageSizeOptions = [5, 10, 25, 50], + searchable = false, + onRowClick, + actions, + emptyState, + loading = false, + getRowId = (r: T) => r.id ?? r.uid ?? JSON.stringify(r), + } = props; + + const [query, setQuery] = useState(""); + const [sort, setSort] = useState(null); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(defaultPageSize); + + useEffect(() => setPage(1), [data, pageSize, query, sort]); + + const filtered = useMemo(() => { + if (!query) return data; + + const q = query.toLowerCase(); + + return data.filter((row) => + columns.some((col) => { + const v = col.accessor ? col.accessor(row) : (row as T)[col.key]; + + return String(v ?? "") + .toLowerCase() + .includes(q); + }) + ); + }, [data, query, columns]); + + const sorted = useMemo(() => { + if (!sort) return filtered; + + const { key, dir } = sort; + const col = columns.find((c) => c.key === key); + + if (!col) return filtered; + + const accessor = (row: T) => (col.accessor ? col.accessor(row) : (row as T)[col.key]); + const copy = [...filtered]; + + copy.sort((a, b) => { + const A = accessor(a); + const B = accessor(b); + + if (A == null && B == null) return 0; + if (A == null) return dir === "asc" ? -1 : 1; + if (B == null) return dir === "asc" ? 1 : -1; + + const sa = String(A); + const sb = String(B); + + return dir === "asc" + ? sa.localeCompare(sb, undefined, { numeric: true }) + : sb.localeCompare(sa, undefined, { numeric: true }); + }); + + return copy; + }, [filtered, sort, columns]); + + const total = sorted.length; + + const pageData = useMemo(() => { + const start = (page - 1) * pageSize; + return sorted.slice(start, start + pageSize); + }, [sorted, page, pageSize]); + + const toggleSort = (key: string) => { + if (!sort || sort.key !== key) { + setSort({ key, dir: "asc" }); + } else if (sort.dir === "asc") { + setSort({ key, dir: "desc" }); + } else { + setSort(null); + } + setPage(1); + }; + + const renderCell = (col: TableColumn, row: T) => { + if (col.render) return col.render(row); + if (col.accessor) return col.accessor(row); + const value = row[col.key as keyof T]; + return typeof value === "string" || typeof value === "number" ? value : String(value ?? ""); + }; + + return ( +
+ + + {loading ? ( + + ) : total === 0 ? ( + + ) : ( + <> +
+ + + + {columns.map((col) => { + return ( + + ); + })} + {actions && ( + + )} + + + + + {pageData.map((row) => ( + onRowClick?.(row)} + > + {columns.map((col) => { + return ( + + ); + })} + {actions && ( + + )} + + ))} + +
+
+ {col.header} + + {col.sortable && ( + + )} +
+
+ Actions +
+ {renderCell(col, row)} + e.stopPropagation()} + > + {actions(row)} +
+
+ + + + )} +
+ ); +} diff --git a/src/shared/components/Table/Table/Table.types.ts b/src/shared/components/Table/Table/Table.types.ts new file mode 100644 index 0000000..c372c8b --- /dev/null +++ b/src/shared/components/Table/Table/Table.types.ts @@ -0,0 +1,34 @@ +import type { ReactNode } from "react"; + +export type Align = "left" | "center" | "right"; + +export type TableColumn = { + key: string; + header: ReactNode; + accessor?: (row: T) => ReactNode; + render?: (row: T) => ReactNode; + sortable?: boolean; + align?: Align; +}; + +export type TableRow = { + id?: string | number; + uid?: string | number; + [key: string]: unknown; +}; + +export type SortState = { key: string; dir: "asc" | "desc" } | null; + +export interface TableProps { + columns: TableColumn[]; + data: T[]; + context: string; + defaultPageSize?: number; + pageSizeOptions?: number[]; + searchable?: boolean; + onRowClick?: (row: T) => void; + actions?: (row: T) => ReactNode; + emptyState?: string; + loading?: boolean; + getRowId?: (row: T) => string; +} diff --git a/src/shared/components/Table/Table/index.ts b/src/shared/components/Table/Table/index.ts new file mode 100644 index 0000000..f8a7e00 --- /dev/null +++ b/src/shared/components/Table/Table/index.ts @@ -0,0 +1 @@ +export { Table } from "./Table"; diff --git a/src/shared/components/Table/index.ts b/src/shared/components/Table/index.ts new file mode 100644 index 0000000..f8a7e00 --- /dev/null +++ b/src/shared/components/Table/index.ts @@ -0,0 +1 @@ +export { Table } from "./Table"; From f9bf0ffe43ba4ecca31869e365d94c08a8c3e065 Mon Sep 17 00:00:00 2001 From: Renato Teixeira Date: Sun, 29 Mar 2026 10:52:17 -0300 Subject: [PATCH 6/7] refactor: update InputField addon text color to use text-foreground/30 --- src/shared/components/InputField/InputField.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/components/InputField/InputField.tsx b/src/shared/components/InputField/InputField.tsx index fd1c423..0fafa39 100644 --- a/src/shared/components/InputField/InputField.tsx +++ b/src/shared/components/InputField/InputField.tsx @@ -18,7 +18,7 @@ export const InputField = forwardRef(
- {leftAddon && {leftAddon}} + {leftAddon && {leftAddon}} ( {...rest} /> - {rightAddon && {rightAddon}} + {rightAddon && {rightAddon}}
{hasError && ( From 2649ed1129b00060729c2fddaa1116f6235d1b26 Mon Sep 17 00:00:00 2001 From: Renato Teixeira Date: Sun, 29 Mar 2026 10:52:29 -0300 Subject: [PATCH 7/7] feat: add hover color variables for card, primary, and destructive in light and dark themes --- src/styles/tailwind.css | 3 +++ src/styles/themes/dark.css | 3 +++ src/styles/themes/light.css | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 546c4be..4e65143 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -4,6 +4,7 @@ --color-card: var(--card); --color-card-foreground: var(--card-foreground); + --color-card-hover: var(--card-hover); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); @@ -13,6 +14,7 @@ --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); + --color-primary-hover: var(--primary-hover); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); @@ -22,6 +24,7 @@ --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); + --color-destructive-hover: var(--destructive-hover); --color-success: var(--success); --color-success-text: var(--success-text); diff --git a/src/styles/themes/dark.css b/src/styles/themes/dark.css index 3f3756c..9f65ae8 100644 --- a/src/styles/themes/dark.css +++ b/src/styles/themes/dark.css @@ -4,6 +4,7 @@ --card: #0f172a; --card-foreground: #f8fafc; + --card-hover: #161f32; --muted: #1e293b; --muted-foreground: #94a3b8; @@ -13,6 +14,7 @@ --primary: #3b82f6; --primary-foreground: #fafafa; + --primary-hover: #1d4ed8; --secondary: #1e293b; --secondary-foreground: #f8fafc; @@ -22,6 +24,7 @@ --destructive: #ef4444; --destructive-foreground: #fafafa; + --destructive-hover: #b91c1c; --success: #d1fae5; --success-text: #065f46; diff --git a/src/styles/themes/light.css b/src/styles/themes/light.css index 9169ce9..8582f03 100644 --- a/src/styles/themes/light.css +++ b/src/styles/themes/light.css @@ -4,8 +4,9 @@ --card: #fafafa; --card-foreground: #0f172a; + --card-hover: #eef1f5; - --muted: #eff6ff; + --muted: #e2e8f0; --muted-foreground: #64748b; --border: #e2e8f0; @@ -13,6 +14,7 @@ --primary: #3b82f6; --primary-foreground: #fafafa; + --primary-hover: #60a5fa; --secondary: #dbeafe; --secondary-foreground: #172554; @@ -22,6 +24,7 @@ --destructive: #ef4444; --destructive-foreground: #fafafa; + --destructive-hover: #f87171; --success: #d1fae5; --success-text: #065f46;