npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-components

Ensure the following are set up in your project:


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-components

Ensure 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 omit showChart) 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.