aravint-ui-table-components
v1.0.3
Published
A lightweight, themeable status badge built with React. Supports preset semantic variants, dot indicators, count bubbles, and full style customization.
Readme
Badge Component
A lightweight, themeable status badge built with React. Supports preset semantic variants, dot indicators, count bubbles, and full style customization.
Installation
npm install @digitus-fci-oa/ui-componentsEnsure the following are set up in your project:
- React
- Tailwind CSS (optional — Badge uses inline styles only)
Usage
1. Import the component
import { Badge } from "@digitus-fci-oa/ui-components";2. Use in your layout
import { Badge } from "@digitus-fci-oa/ui-components";
export default function StatusPage() {
return (
<div className="flex gap-2">
<Badge label="Active" variant="success" />
<Badge label="Failed" variant="error" />
<Badge label="Pending" variant="warning" />
<Badge label="Info" variant="info" />
<Badge label="Draft" variant="default" />
</div>
);
}Variants
Five built-in semantic presets:
| Variant | Text | Background | Border | Dot |
|-----------|------------|-------------|-------------|------------|
| success | green-800 | green-50 | green-300 | green-600 |
| error | red-800 | red-100 | red-300 | red-600 |
| warning | yellow-800 | yellow-100 | yellow-300 | yellow-500 |
| info | blue-800 | blue-100 | blue-300 | blue-600 |
| default | gray-700 | gray-100 | gray-200 | gray-500 |
<Badge label="Approved" variant="success" />
<Badge label="Rejected" variant="error" />
<Badge label="On Hold" variant="warning" />
<Badge label="New" variant="info" />
<Badge label="Draft" variant="default" />Features
Dot indicator
Add a colored status dot to the left of the label:
<Badge label="Live" variant="success" dot />
<Badge label="Offline" variant="error" dot />Count bubble
Render a count pill to the right of the label:
<Badge label="Notifications" variant="info" count={5} />
<Badge label="Errors" variant="error" count={12} />Dot + Count together
<Badge label="Open RFQs" variant="info" dot count={8} />Custom colors
Override individual color tokens while keeping the rest of the variant:
<Badge
label="In Review"
variant="info"
background="#ede9fe"
color="#5b21b6"
borderColor="#c4b5fd"
/>Or go fully custom without any variant:
<Badge
label="Custom"
color="#fff"
background="#7c3aed"
borderColor="#6d28d9"
dotColor="#a78bfa"
dot
/>Shape and size
{/* Square badge */}
<Badge
label="NEW"
variant="warning"
fontSize="10px"
fontWeight={700}
padding="1px 6px"
borderRadius="3px"
/>
{/* Large pill */}
<Badge
label="Active"
variant="success"
fontSize="14px"
padding="4px 16px"
/>Full style escape hatch
The style prop is applied last and wins over everything — including individual override props:
<Badge
label="Override"
variant="success"
style={{ fontSize: "14px", padding: "4px 16px", borderRadius: "4px" }}
/>Props
Core props
| Prop | Type | Required | Default | Description |
|-----------|----------------|----------|-------------|--------------------------------------------------|
| label | string | ✅ | — | Text displayed inside the badge |
| variant | BadgeVariant | — | "default" | Preset color theme |
| dot | boolean | — | false | Shows a colored dot to the left of the label |
| count | number | — | — | Renders a count bubble to the right of the label |
Style override props
| Prop | Type | Default | Description |
|----------------|---------------------------------|-----------------|--------------------------------------------------------|
| color | string | variant default | Text color |
| background | string | variant default | Background color |
| borderColor | string | variant default | Border color |
| dotColor | string | variant default | Dot indicator color |
| borderRadius | string | "9999px" | Border radius — pill by default |
| fontSize | string | "12px" | Font size |
| fontWeight | CSSProperties["fontWeight"] | 500 | Font weight |
| padding | string | "2px 10px" | Inner padding |
| width | string | — | Fixed width if needed |
| style | CSSProperties | — | Full inline style escape hatch — wins over everything |
| className | string | — | Extra CSS class for external stylesheet targeting |
Type reference
export type BadgeVariant =
| "success"
| "error"
| "warning"
| "info"
| "default";
export interface BadgeProps {
label: string;
count?: number;
dot?: boolean;
variant?: BadgeVariant;
color?: string;
background?: string;
borderColor?: string;
borderRadius?: string;
fontSize?: string;
fontWeight?: React.CSSProperties["fontWeight"];
padding?: string;
width?: string;
dotColor?: string;
style?: CSSProperties;
className?: string;
}Key design decisions
Variants as a starting point, not a constraint. Every visual property (color, background, borderColor, dotColor) can be individually overridden while keeping the rest of the variant intact. You don't have to choose between a preset and full custom — you can mix both.
style as a full escape hatch. The style prop is spread last so it wins over everything including individual override props. Use it for one-off adjustments without needing a new variant.
Zero external dependencies. No icon imports, no external CSS files, no theming context required. Drop it into any project.
Reusing across projects
The Badge is fully self-contained. To use it in a different project, no configuration is needed — just import and pass props.
DataTable Component System
A composable, feature-rich table system built for enterprise list views. Includes a sortable data table, smart pagination, page header with search, filter bar, KPI cards, active filter chips, and a status badge — all wired together with minimal boilerplate.
Installation
npm install @digitus-fci-oa/table-componentsEnsure the following are set up in your project:
- React
- PrimeReact (peer dependency for Paginator and DataTable internals)
- Tailwind CSS (optional — components use inline styles by default)
Import the required stylesheet once at the root of your layout:
import "@digitus-fci-oa/table-components/dist/table-components.css";Usage
1. Import the components
import {
DataTable,
TablePaginator,
PageHeader,
FilterBar,
KPISection,
ActiveFiltersBar,
StatusBadge,
} from "@digitus-fci-oa/table-components";
import "@digitus-fci-oa/table-components/dist/table-components.css";2. Define columns
const columns = [
{ field: "RDR no", header: "RDR no", sortable: true },
{ field: "Supplier", header: "Supplier", sortable: true },
{
field: "status",
header: "Status",
sortable: true,
body: (rowData) => (
<StatusBadge status={rowData.status.toLowerCase()} config={statusConfig} />
),
},
];3. Define a status config
const statusConfig = {
draft: { label: "Draft", bgColor: "#9E9E9E" },
initiated: { label: "Initiated", bgColor: "#FFA114" },
closed: { label: "Closed", bgColor: "#059669" },
};4. Compose the layout
export default function RdrManagement() {
const [first, setFirst] = useState(0);
const [rows, setRows] = useState(10);
const [searchValue, setSearch]= useState("");
const [sortField, setSortField] = useState("");
const [sortOrder, setSortOrder] = useState<1 | -1 | null>(null);
const [sortedData, setSortedData] = useState(originalData);
const pagedData = sortedData.slice(first, first + rows);
const totalRecords = originalData.length;
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<div className="table-header">
<PageHeader
title="RDR Management"
searchPlaceholder="Search RDR No, Supplier..."
searchValue={searchValue}
onSearchChange={(v) => { setSearch(v); setFirst(0); }}
onExport={() => console.log("export")}
/>
</div>
<div style={{ flex: 1, margin: "0 15px" }}>
<DataTable
value={pagedData}
dataKey="RDR no"
columns={columns}
lazy
first={0}
rows={rows}
totalRecords={totalRecords}
sortField={sortField}
sortOrder={sortOrder}
onSort={handleSort}
/>
</div>
<TablePaginator
currentPage={Math.ceil(first / rows) + 1}
rows={rows}
totalRecords={totalRecords}
inputValue={inputValue}
onPageChange={handlePageChange}
onRowsChange={handleRowsChange}
onPageInput={handlePageInput}
onPageKeyDown={handlePageKeyDown}
onPageBlur={handlePageBlur}
/>
</div>
);
}Components
DataTable
The core table. Wraps PrimeReact's DataTable with opinionated defaults and a built-in action column slot.
<DataTable
value={pagedData}
dataKey="RDR no"
columns={columns}
lazy
first={0}
rows={rows}
totalRecords={totalRecords}
sortField={sortField}
sortOrder={sortOrder}
scrollHeight="380px"
onSort={handleSort}
actionHeader={() => <span>Action</span>}
actionBody={(rowData) => (
<div className="flex gap-3">
<button onClick={() => alert(`Edit ${rowData["RDR no"]}`)}>Edit</button>
<button onClick={() => alert(`View ${rowData["RDR no"]}`)}>View</button>
</div>
)}
/>Column definition
Each entry in the columns array supports:
| Property | Type | Description |
|------------|-------------------------------|----------------------------------------------------------|
| field | string | Key from the row data object |
| header | string | Column header label |
| sortable | boolean | Enables server-side sort on this column |
| body | (rowData: any) => ReactNode | Custom cell renderer — overrides the default text output |
DataTable props
| Prop | Type | Required | Default | Description |
|-----------------|-----------------------------------|----------|---------|----------------------------------------------------------|
| value | any[] | ✅ | — | The current page's data slice |
| dataKey | string | ✅ | — | Unique row identifier field |
| columns | ColumnDef[] | ✅ | — | Column configuration array |
| lazy | boolean | — | false | Enable server-side pagination and sorting |
| first | number | — | 0 | Index of the first record (used with lazy) |
| rows | number | — | 10 | Records per page |
| totalRecords | number | — | — | Total record count for server-side pagination |
| sortField | string | — | — | Currently sorted column field |
| sortOrder | 1 \| -1 \| null | — | — | Sort direction |
| scrollHeight | string | — | — | Fixed scroll height (e.g. "380px") |
| onSort | (e: DataTableSortEvent) => void | — | — | Callback when a column header is clicked to sort |
| actionHeader | () => ReactNode | — | — | Custom header content for the action column |
| actionBody | (rowData: any) => ReactNode | — | — | Custom cell content for the action column (per row) |
TablePaginator
A fully controlled paginator with row-count selector, page-jump input, and keyboard navigation.
<TablePaginator
datatestid="my-table-paginator"
currentPage={Math.ceil(first / rows) + 1}
rows={rows}
totalRecords={totalRecords}
inputValue={inputValue}
onPageChange={handlePageChange}
onRowsChange={handleRowsChange}
onPageInput={handlePageInput}
onPageKeyDown={handlePageKeyDown}
onPageBlur={handlePageBlur}
/>TablePaginator props
| Prop | Type | Required | Description |
|-----------------|-------------------------------------------------------|----------|--------------------------------------------------------------|
| currentPage | number | ✅ | 1-based current page number |
| rows | number | ✅ | Rows per page |
| totalRecords | number | ✅ | Total records in the dataset |
| inputValue | number \| string | ✅ | Controlled value for the page-jump input |
| onPageChange | (e: PaginatorPageChangeEvent) => void | ✅ | Fired when the user navigates to a new page |
| onRowsChange | (e: React.ChangeEvent<HTMLSelectElement>) => void | ✅ | Fired when the rows-per-page selector changes |
| onPageInput | (e: React.ChangeEvent<HTMLInputElement>) => void | ✅ | Fired on every keystroke in the page-jump input |
| onPageKeyDown | (e: React.KeyboardEvent<HTMLInputElement>) => void | ✅ | Fired on keydown — handle Enter to commit page jump |
| onPageBlur | () => void | ✅ | Fired when the page-jump input loses focus |
| datatestid | string | — | Passed as data-testid for automated tests |
Handling sort and page state
const handleSort = (e: DataTableSortEvent) => {
setSortField(e.sortField);
setSortOrder(e.sortOrder as 1 | -1 | null);
if (!e.sortField || !e.sortOrder) {
setSortedData(originalData);
return;
}
const sorted = [...originalData].sort((a: any, b: any) => {
const valA = a[e.sortField];
const valB = b[e.sortField];
if (valA == null) return 1;
if (valB == null) return -1;
const result = valA > valB ? 1 : valA < valB ? -1 : 0;
return e.sortOrder === 1 ? result : -result;
});
setSortedData(sorted);
setFirst(0);
};
const handlePageChange = (e: PaginatorPageChangeEvent) => {
setFirst(e.first);
setRows(e.rows);
setInputValue(e.page + 1);
};PageHeader
A flexible page header with two display variants: a standard title bar ("default") and a tab-based navigation bar ("tabs"). Both variants share search, export, and import controls, and accept an extraActions slot for custom buttons.
Variant: "default" (title bar with filters)
<PageHeader
variant="default"
title="RDR Management"
searchPlaceholder="Search RDR No, Supplier..."
searchValue={searchValue}
onSearchChange={(v) => { setSearchValue(v); setFirst(0); setInputValue(1); }}
filterCount={activeFilterCount}
filtersVisible={filtersVisible}
onFilterToggle={() => setFiltersVisible(v => !v)}
onExport={() => console.log("Export clicked")}
/>Variant: "tabs" (tab navigation bar)
<PageHeader
variant="tabs"
tabs={[
{ label: "All RDRs", value: "all" },
{ label: "Initiated", value: "initiated" },
{ label: "Closed", value: "closed" },
]}
activeTab={activeTab}
onTabChange={(value) => setActiveTab(value)}
searchValue={searchValue}
onSearchChange={(v) => { setSearchValue(v); setFirst(0); }}
showFilter={false}
onExport={() => console.log("Export clicked")}
/>Extra actions slot
Render any additional buttons or controls to the right of the built-in actions:
<PageHeader
title="RDR Management"
onExport={handleExport}
showImport
onImport={handleImport}
extraActions={
<button onClick={handleCustomAction}>Custom</button>
}
/>PageHeader props
| Prop | Type | Required | Default | Description |
|---------------------|-------------------------|----------|-------------|----------------------------------------------------------------------------------|
| variant | "default" \| "tabs" | — | "default" | Visual variant — "default" shows a title; "tabs" shows tab navigation |
| title | string | — | — | Page title — used in the "default" variant |
| tabs | TabItem[] | — | [] | Tab items array — used in the "tabs" variant |
| activeTab | string | — | — | Currently active tab value |
| onTabChange | (value: string) => void | — | — | Fired when a tab is clicked |
| searchValue | string | — | "" | Controlled search input value |
| onSearchChange | (v: string) => void | — | — | Fired on every search input change — reset pagination alongside this |
| searchPlaceholder | string | — | "Search…" | Placeholder text inside the search input |
| showSearch | boolean | — | true | Show or hide the search bar |
| showFilter | boolean | — | true | Show or hide the Filter button (only applies in the "default" variant) |
| showExport | boolean | — | true | Show or hide the Export button |
| showImport | boolean | — | false | Show or hide the Import button |
| filterCount | number | — | 0 | Number of active filters — shown as a badge on the Filter button |
| filtersVisible | boolean | — | false | Controls the visual active state of the Filter button |
| onFilterToggle | () => void | — | — | Toggles the filter panel open/closed |
| onExport | () => void | — | — | Called when the Export button is clicked |
| onImport | () => void | — | — | Called when the Import button is clicked |
| extraActions | React.ReactNode | — | — | Renders additional controls to the right of the built-in action buttons |
TabItem type
| Field | Type | Description |
|---------|----------|--------------------------------------|
| label | string | Display text shown on the tab button |
| value | string | Identifier used with onTabChange |
Note on search and pagination: When wiring
onSearchChange, always reset pagination state alongside the search value to avoid showing an empty page:onSearchChange={(v) => { setSearchValue(v); setFirst(0); setInputValue(1); }}
FilterBar
Collapsible filter panel with a date-range picker and status checkboxes.
<FilterBar
visible={filtersVisible}
selectedStatuses={selectedStatuses}
onStatusChange={(s) => { setSelectedStatuses(s); setFirst(0); }}
dateRange={dateRange}
onDateRangeChange={(r) => { setDateRange(r); setFirst(0); }}
onClearAll={handleClearAll}
activeFilterCount={activeFilterCount}
/>FilterBar props
| Prop | Type | Required | Description |
|----------------------|---------------------------------------------------|----------|------------------------------------------------------------|
| visible | boolean | ✅ | Controls whether the filter panel is shown |
| selectedStatuses | string[] | ✅ | Array of currently selected status strings |
| onStatusChange | (statuses: string[]) => void | ✅ | Fired when the status selection changes |
| dateRange | { from: Date \| null; to: Date \| null } | ✅ | Controlled date range value |
| onDateRangeChange | (r: { from: Date \| null; to: Date \| null }) => void | ✅ | Fired when the date range is updated |
| onClearAll | () => void | ✅ | Resets all filters at once |
| activeFilterCount | number | — | Used internally to show/hide the clear-all button |
KPISection
Toggleable row of summary cards, each with a label, value, accent color, and an optional sparkline. When collapsed, the header shows the total RDR count in place of the cards.
const kpiCards: KPICardItem[] = [
{
label: "Total RDRs",
value: 28,
accentColor: "#3B82F6",
chartColor: "#059669",
chartData: [4, 7, 15],
showChart: true,
bottomDescription: "This month",
},
{
label: "Initiated",
value: 1,
accentColor: "#F59E0B",
chartColor: "#059669",
chartData: [1, 2, 3],
showChart: true,
},
{
label: "Closed",
value: 26,
accentColor: "#059669",
chartColor: "#059669",
chartData: [8, 12, 18],
showChart: true,
},
{
label: "Draft",
value: 1,
accentColor: "#9CA3AF",
},
];
<KPISection
cards={kpiCards}
visible={kpiVisible}
onToggleVisibility={() => setKpiVisible(v => !v)}
totalRDRs={originalData.length}
/>KPISection props
| Prop | Type | Required | Description |
|----------------------|-----------------|----------|-----------------------------------------------------------|
| cards | KPICardItem[] | ✅ | Array of card definitions (see below) |
| visible | boolean | ✅ | Whether the KPI cards are expanded |
| onToggleVisibility | () => void | ✅ | Toggles the KPI section open/closed |
| totalRDRs | number | — | Shown in the collapsed header as a summary line |
KPICardItem type
| Field | Type | Required | Description |
|---------------------|--------------------|----------|-------------------------------------------------------------------------------|
| label | string | ✅ | Card title |
| value | string \| number | ✅ | Primary display value — numbers are zero-padded to two digits automatically |
| accentColor | string | — | Top border accent color (hex). Falls back to a default palette if omitted |
| showChart | boolean | — | Set to true to render the sparkline chart on this card |
| chartColor | string | — | Sparkline line and fill color (hex). Falls back to accentColor if omitted |
| chartData | number[] | — | Data points for the sparkline — needs at least 2 values for a line to render |
| bottomDescription | string | — | Optional secondary label rendered below the value (e.g. "This month") |
Tip: Set
showChart: false(or omitshowChart) on cards where a trend line would not be meaningful. The card layout adjusts automatically — cards without a chart use the full width for the label and value.
ActiveFiltersBar
Renders removable chips for each active filter. Automatically hidden when there are no active filters.
<ActiveFiltersBar
dateRange={dateRange}
selectedStatuses={selectedStatuses}
onClearDate={() => { setDateRange({ from: null, to: null }); setFirst(0); }}
onClearStatus={(status) => {
setSelectedStatuses(prev => prev.filter(s => s !== status));
setFirst(0);
}}
onClearAll={handleClearAll}
/>ActiveFiltersBar props
| Prop | Type | Required | Description |
|--------------------|------------------------------------------------|----------|--------------------------------------------------|
| dateRange | { from: Date \| null; to: Date \| null } | ✅ | Current date range — renders a chip when set |
| selectedStatuses | string[] | ✅ | Each status renders as an individual chip |
| onClearDate | () => void | ✅ | Removes the date range chip |
| onClearStatus | (status: string) => void | ✅ | Removes a single status chip |
| onClearAll | () => void | ✅ | Clears all filters at once |
StatusBadge
A pill badge that maps a status string to a label and background color via a config object.
const statusConfig = {
draft: { label: "Draft", bgColor: "#9E9E9E" },
initiated: { label: "Initiated", bgColor: "#FFA114" },
closed: { label: "Closed", bgColor: "#059669" },
};
<StatusBadge status="closed" config={statusConfig} />StatusBadge props
| Prop | Type | Required | Description |
|----------|--------------------------------------------------------|----------|-------------------------------------------------------|
| status | string | ✅ | Lowercase status key — looked up in config |
| config | Record<string, { label: string; bgColor: string }> | ✅ | Maps status keys to display label and background color|
Features
Server-side sorting
Set lazy on DataTable and manage sorting externally:
const handleSort = (e: DataTableSortEvent) => {
const sorted = [...originalData].sort((a: any, b: any) => {
const valA = a[e.sortField], valB = b[e.sortField];
const result = valA > valB ? 1 : valA < valB ? -1 : 0;
return e.sortOrder === 1 ? result : -result;
});
setSortedData(sorted);
setFirst(0);
};Client-side search filtering
Filter before slicing for pagination:
const searched = sortedData.filter(row => {
if (!searchValue) return true;
const q = searchValue.toLowerCase();
return (
String(row["RDR no"]).includes(q) ||
row["Supplier Name"].toLowerCase().includes(q)
);
});
const pagedData = searched.slice(first, first + rows);Custom cell renderers
Use the body property in a column definition for full control:
{
field: "RDR no",
header: "RDR no",
sortable: true,
body: (rowData) => (
<div className="flex items-center font-semibold text-[#059669]">
{rowData["RDR no"]}
</div>
),
}Action column
Pass actionHeader and actionBody directly to DataTable — no column definition needed:
<DataTable
...
actionHeader={() => <span>Action</span>}
actionBody={(rowData) => (
<div className="flex gap-3">
<button onClick={() => navigate("/details", { state: rowData })}>View</button>
</div>
)}
/>KPI scroll height adjustment
When the KPI section is expanded, reduce scrollHeight to prevent the table from overflowing:
<DataTable
scrollHeight={kpiVisible ? "300px" : "380px"}
...
/>Active filter count
Drive the filter badge and clear-all visibility from a single derived value:
const activeFilterCount = selectedStatuses.length + (dateRange.from ? 1 : 0);Full clear-all handler
const handleClearAll = () => {
setSelectedStatuses([]);
setDateRange({ from: null, to: null });
setSearchValue("");
setFirst(0);
setInputValue(1);
};Type reference
export type PageHeaderVariant = "default" | "tabs";
export interface TabItem {
label: string;
value: string;
}
export interface PageHeaderProps {
variant?: PageHeaderVariant;
title?: string;
tabs?: TabItem[];
activeTab?: string;
onTabChange?: (value: string) => void;
searchPlaceholder?: string;
searchValue?: string;
onSearchChange?: (value: string) => void;
showSearch?: boolean;
showFilter?: boolean;
showExport?: boolean;
showImport?: boolean;
filterCount?: number;
filtersVisible?: boolean;
onFilterToggle?: () => void;
onExport?: () => void;
onImport?: () => void;
extraActions?: React.ReactNode;
}
export interface KPICardItem {
label: string;
value: string | number;
accentColor?: string;
chartColor?: string;
chartData?: number[];
showChart?: boolean;
bottomDescription?: string;
}
export interface KPISectionProps {
cards: KPICardItem[];
visible?: boolean;
onToggleVisibility?: () => void;
totalRDRs: number;
}
export interface ColumnDef {
field: string;
header: string;
sortable?: boolean;
body?: (rowData: any) => React.ReactNode;
}
export type StatusConfig = Record<string, {
label: string;
bgColor: string;
}>;
export interface DateRange {
from: Date | null;
to: Date | null;
}Key design decisions
Fully controlled state. All pagination, sort, search, and filter state lives in the parent component. The table components are stateless renderers — every interaction fires a callback that the parent handles. This keeps data flow predictable and makes it trivial to sync URL params, persist filters, or drive multiple tables from the same state.
Lazy pagination by default. The DataTable operates in lazy mode, which means the parent always slices data before passing it as value. This pairs naturally with real API pagination — replace originalData.slice(...) with an API call and nothing else changes.
Composable layout, not a monolith. PageHeader, FilterBar, KPISection, ActiveFiltersBar, and DataTable are independent components. Use all of them or just the ones you need. The CSS class table-header wraps the top section by convention, but layout is left to the consumer.
StatusBadge is config-driven. Rather than hardcoding variants, StatusBadge accepts a plain object mapping status keys to label and color. New statuses require no component changes.
PageHeader variants share a common action bar. Whether in "default" or "tabs" mode, the search bar, export/import buttons, and extraActions slot behave identically. Only the left side of the header changes between variants — the filter button is suppressed automatically in "tabs" mode since tab-based navigation implies a different filtering pattern.
KPI cards are opt-in for charts. The sparkline is rendered only when showChart: true is set, so cards that represent totals or static counts don't need placeholder chart data. The card layout adapts — with a chart it splits the space between the text and the sparkline; without, the text fills the full width.
