@millstreetlabs/millstlabui
v0.0.26
Published
Reusable React UI utilities and components for Millstreet Labs.
Keywords
Readme
@millstreetlabs/millstlabui
Reusable React UI utilities and components for Millstreet Labs.
Install
npm i @millstreetlabs/millstlabuiThis package expects these peer dependencies in your app:
react,react-domreact-router-domzustand- DataTable:
@tanstack/react-table - Radix UI:
@radix-ui/react-avatar,@radix-ui/react-dialog,@radix-ui/react-label,@radix-ui/react-popover,@radix-ui/react-progress,@radix-ui/react-select,@radix-ui/react-separator,@radix-ui/react-slot,@radix-ui/react-tabs,@radix-ui/react-tooltip - Icons/utilities:
lucide-react,@tabler/icons-react,clsx,tailwind-merge,class-variance-authority - Optional (DataTable
ColorCell):react-colorful
Styles
If you want the packaged CSS, import:
@import "@millstreetlabs/millstlabui/styles.css";Exports / import paths
Recommended import style is from the package root:
import { MainSidebar, RecordNavigator } from "@millstreetlabs/millstlabui";You can also import some grouped utilities via subpath exports:
import { cn } from "@millstreetlabs/millstlabui/utils";
import type { MenuItem } from "@millstreetlabs/millstlabui/types";
import { RecordNavigatorProvider } from "@millstreetlabs/millstlabui/providers";Sidebar
Minimal example using the store provider:
import * as React from "react";
import {
MainSidebar,
SidebarStoreProvider,
type MenuItem,
} from "@millstreetlabs/millstlabui";
const menuItems: MenuItem[] = [{ key: "home", label: "Home", path: "/" }];
export function AppSidebar() {
return (
<SidebarStoreProvider storageKey="app" initialSelected="home">
<MainSidebar menuItems={menuItems} onSelect={() => {}} />
</SidebarStoreProvider>
);
}Record / table navigator
Wrap the part of your app that uses navigation with RecordNavigatorProvider, then register context and render RecordNavigator.
import * as React from "react";
import {
RecordNavigator,
RecordNavigatorProvider,
useRegisterRecordNavigator,
type NavigatorConfigMap,
type FetchNavigationPage,
} from "@millstreetlabs/millstlabui";
const navigatorConfig: NavigatorConfigMap = {
orders: {
detailPathPattern: "/orders/:id",
showNavigator: true,
propertySpecific: false,
},
};
const fetchNavigationPage: FetchNavigationPage = async ({
type,
page,
limit,
}) => {
// Return the ids for the requested page and the total count.
// This is app-specific; plug in your API here.
return { ids: [], total: 0 };
};
function OrdersPage() {
useRegisterRecordNavigator({
type: "orders",
page: 1,
limit: 10,
totalRecords: 0,
filters: {},
});
return <RecordNavigator />;
}
export function App() {
return (
<RecordNavigatorProvider
navigatorKey="main"
navigatorConfig={navigatorConfig}
fetchNavigationPage={fetchNavigationPage}
>
<OrdersPage />
</RecordNavigatorProvider>
);
}Data Table
DataTableProvider wraps TanStack Table and exposes a composable set of building blocks:
- Provider:
DataTableProvider(state + TanStack instance) - Core UI:
DataTableTable,DataTablePagination - Toolbar helpers (optional):
DataTableFilters,DataTableSearch,DataTableAllFilters,DataTableSortBy,DataTableReset,DataTableTabs, etc.
Data shape
Your row type must include an id field (string/number). The table uses String(row.id) as the row id for selection + links.
Client-side mode (default)
Use this when you already have all rows in memory and want the table to paginate/sort client-side.
import * as React from "react";
import type { ColumnDef } from "@tanstack/react-table";
import {
DataTableProvider,
DataTableTable,
DataTablePagination,
DataTableFilters,
DataTableSearch,
} from "@millstreetlabs/millstlabui";
type Row = { id: string; name: string; amount: number };
const columns: ColumnDef<Row>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "amount", header: "Amount" },
];
export function ClientSideExample({ rows }: { rows: Row[] }) {
return (
<DataTableProvider
columns={columns}
data={rows}
persistPaginationInUrl
getRowHref={(r) => `/customers/${r.id}`}
onRowClick={(r) => console.log("clicked", r.id)}
>
<DataTableFilters>
<DataTableSearch placeholder="Search..." />
</DataTableFilters>
<DataTableTable />
<DataTablePagination />
</DataTableProvider>
);
}Server-side mode (pagination + sorting)
Use this for large datasets (like inquiries/customers pages). In server mode:
- Set
manualPaginationand pass totals viapaginationConfig.total(orrowCount) - Set
manualSortingand configure mapping viasortingConfig - Provide
initialFilterList+handleServerSideTableChange(usually fromuseServerSidePagination) - Fetch data based on
page/limit/sortBy/sortOrderand any additional filters (search, tabs, toolbar filters)
import * as React from "react";
import type { ColumnDef } from "@tanstack/react-table";
import {
DataTableProvider,
DataTableFilters,
DataTableAllFilters,
DataTableFiltersActions,
DataTablePagination,
DataTablePaginationActions,
DataTablePaginationInfo,
DataTablePaginationLimits,
DataTablePaginationLinks,
DataTablePaginationMeta,
DataTableReset,
DataTableSearch,
DataTableSortBy,
DataTableTable,
useServerSidePagination,
} from "@millstreetlabs/millstlabui";
type Inquiry = { id: string; status: string; customerName: string };
const columns: ColumnDef<Inquiry>[] = [
{ accessorKey: "customerName", header: "Customer" },
{ accessorKey: "status", header: "Status" },
];
export function ServerSideExample({
rows,
total,
isLoading,
onSearch,
onFiltersChange,
filterConfigs,
}: {
rows: Inquiry[];
total: number;
isLoading: boolean;
onSearch: (q: string) => void;
onFiltersChange: (filters: Record<string, string[]>) => void;
filterConfigs: unknown; // your app-specific filter config type
}) {
const { initialFilterList, handleServerSideTableChange } =
useServerSidePagination<{
sortBy?: string;
sortOrder?: "asc" | "desc";
}>({});
return (
<DataTableProvider
columns={columns}
data={rows}
isLoading={isLoading}
manualPagination
manualSorting
sortingConfig={{
mode: "server",
sortByKey: "sortBy",
sortOrderKey: "sortOrder",
}}
paginationConfig={{
mode: "server",
total,
pageIndex: Math.max((initialFilterList.page ?? 1) - 1, 0),
pageSize: initialFilterList.limit ?? 10,
pageSizeOptions: [10, 20, 50, 100],
defaultPageSize: 10,
}}
initialFilterList={initialFilterList}
handleServerSideTableChange={handleServerSideTableChange}
>
<DataTableFilters>
<DataTableSearch
placeholder="Search..."
debounceMs={500}
onDebouncedValueChange={onSearch}
/>
<DataTableFiltersActions>
<DataTableAllFilters
filters={filterConfigs as never}
onFiltersChange={onFiltersChange}
/>
<DataTableSortBy />
<DataTableReset />
</DataTableFiltersActions>
</DataTableFilters>
<DataTableTable />
<DataTablePagination>
<DataTablePaginationMeta>
<DataTablePaginationLimits />
<DataTablePaginationInfo />
</DataTablePaginationMeta>
<DataTablePaginationActions>
<DataTablePaginationLinks maxVisible={5} />
</DataTablePaginationActions>
</DataTablePagination>
</DataTableProvider>
);
}Row selection (bulk actions)
To enable row selection (like the inquiries page), pass enableRowSelection. For controlled selection, provide rowSelection + onRowSelectionChange.
For server-side lists where selected rows may not exist in the current page, enable persistSelectedRecords and use onSelectedRecordsChange to receive selected row objects (persisted by id across page changes).
enableRowSelection: enables TanStack row selection staterowSelection:Record<string, boolean>keyed byString(row.id)persistSelectedRecords: persist selected records across page/data changesonSelectedRecordsChange(selected, selectedById): selected rows as objects, even when paginating
License
UNLICENSED (internal use).
