@dinhn1202/ui-kit
v0.2.5
Published
DTO shared React TypeScript UI kit based on Ant Design
Readme
@dinhn1202/ui-kit
DTO shared React TypeScript UI kit based on Ant Design v6.
Live demo
Installation
npm install @dinhn1202/ui-kitPeer dependencies:
npm install react react-dom antd@^6.5.0CustomGantt also installs its Gantt renderer and required MUI runtime
dependencies with the UI kit package.
Usage
import { CustomButton, CustomThemeProvider } from "@dinhn1202/ui-kit";
function App() {
return (
<CustomThemeProvider>
<CustomButton type="primary">Save</CustomButton>
</CustomThemeProvider>
);
}
export default App;Override theme
import { CustomThemeProvider } from "@dinhn1202/ui-kit";
const customTheme = {
token: {
colorPrimary: "#722ed1",
borderRadius: 12,
},
};
function App() {
return (
<CustomThemeProvider theme={customTheme}>
<YourApp />
</CustomThemeProvider>
);
}Available components
import {
CustomButton,
CustomDatePicker,
CustomGantt,
CustomInput,
CustomInputNumber,
CustomLoading,
CustomModal,
CustomRadio,
CustomRadioGroup,
CustomSelect,
CustomTable,
CustomThemeProvider,
CustomTimePicker,
CustomTreeSelect,
CustomUpload,
CustomUploadDragger,
CustomUserSelect,
type UserSearchService,
} from "@dinhn1202/ui-kit";CustomButton provides an optional customVariant (primary, secondary, or
danger). CustomSelect, CustomInputNumber, CustomDatePicker, CustomTimePicker, CustomTreeSelect, and CustomTable provide an optional fullWidth prop. By
default, CustomTable adds client-side sorting, value filters, global search, and
multi-column text filtering to flat, simple dataIndex columns. Set a column's
dataType: "date" to use a calendar for its header and multi-column filters,
with matching performed by calendar day and chronological sorting. Header
filters and toolbar filters share the same applied values. Its toolbar
also provides a searchable column chooser with drag-and-drop reordering. Set
enableColumnFeatures={false} to render the unenhanced Ant Design table;
enableGlobalSearch and enableMultiColumnFiltering can be disabled
independently. Pagination is shown by default with a page size selector and
quick jumper; pass pagination={false} to disable it.
Pass a unique columnStorageKey to persist column chooser visibility and
ordering in localStorage. Without this prop, the layout is kept only for the
current component session. A saved layout is reset automatically whenever the
table column schema changes.
When serverSideRender is enabled, header value filters stay available. The
first time a column's filter dropdown opens, the table requests only the first
100 rows that match the configured base filters and lists their distinct
values. Scrolling near the end requests the next 100 rows; repeated scroll
events are coalesced and no request is made after the server reports the end.
Typing in Search in filters is debounced, resets the option list, and sends a
server-side contains filter for that column so matches outside the loaded
batch can be found. Selected values remain available while searching and
loading more options. Selecting values applies them on the server as
columnFilters and reloads the first page. Date columns keep their calendar
filter and are matched by day.
SharePoint server-side paging
Use serverSideRender to let CustomTable own loading, cursor paging, page
cache, sorting, filtering, and loading state. The PnPjs adapter uses the SPFI
instance already configured by the consuming application, so the UI kit does
not bundle a second copy of PnPjs. Cursor paging requires the async item
iterator available in PnPjs v4.
import { useMemo } from "react";
import {
createPnPjsTableService,
CustomTable,
type TableColumnsType,
} from "@dinhn1202/ui-kit";
type ProjectItem = {
ID: number;
Title: string;
Modified: string;
};
const columns: TableColumnsType<ProjectItem> = [
{ dataIndex: "ID", title: "ID" },
{ dataIndex: "Title", title: "Title" },
{ dataIndex: "Modified", dataType: "date", title: "Modified" },
];
const projectService = useMemo(
() => createPnPjsTableService<ProjectItem>(sp),
[sp],
);
<CustomTable<ProjectItem>
columns={columns}
serverSideRender={{
service: projectService,
tableName: "Projects",
pageSize: 50,
filters: [{ field: "Status", value: "Active" }],
sort: { field: "Modified", direction: "descend" },
searchFields: ["Title"],
}}
/>;select defaults to the columns' simple dataIndex fields and rowKey
defaults to ID. Pagination uses read-only simple mode because SharePoint list
items use cursor/skip-token paging rather than numeric offsets. Pass filter
for a base OData filter, sort for an initial server sort, and expand/select
for lookup fields. When a filtered exact count is unavailable, pagination grows
progressively as the user moves forward instead of loading the full list just
to count it. Structured filters support eq, ne, gt, ge, lt, le,
contains, and startsWith; use filter only for advanced raw OData clauses.
Joining lookup fields
SharePoint joins a child list through a Lookup column on the main list.
Declare each lookup column in expand and point the table column's
dataIndex at the child field with an array path:
type ProjectItem = {
ID: number;
Title: string;
Category?: { ID: number; Title: string };
};
const columns: TableColumnsType<ProjectItem> = [
{ dataIndex: "ID", title: "ID" },
{ dataIndex: "Title", title: "Project" },
{ dataIndex: ["Category", "Title"], title: "Category" },
];
<CustomTable<ProjectItem>
columns={columns}
serverSideRender={{
service: projectService,
tableName: "Projects",
expand: ["Category"],
}}
/>;The kit adds the joined path (Category/Title) to select automatically and
expands the lookup through PnPjs, so sorting, column filters, the full filter
option scan, and global search work on the joined field like any plain field.
Person columns follow the same pattern, for example expand: ["Author"] with
dataIndex: ["Author", "Title"]. Multi-select lookups return arrays, so give
those columns a custom render instead of an automatic path. If you pass a
custom select, include the joined paths yourself.
Joining two separate lists without a Lookup
When the two lists share no Lookup relationship, join them in memory with
createJoinedListsTableService. Both lists are loaded completely once per
table instance and cached, so sorting, column filters, global search, and
numbered paging all work on the joined columns:
import { createJoinedListsTableService, CustomTable } from "@dinhn1202/ui-kit";
type ProjectItem = {
ID: number;
Title: string;
CategoryCode?: string;
Category?: { ID: number; Code: string; Title: string };
};
const projectService = useMemo(
() =>
createJoinedListsTableService<ProjectItem>(sp, {
primaryList: "Projects",
primaryJoinColumn: "CategoryCode",
childList: "Categories",
childJoinColumn: "Code",
childPropertyName: "Category",
primarySelect: ["ID", "Title", "CategoryCode"],
childSelect: ["ID", "Code", "Title"],
}),
[sp],
);
const columns: TableColumnsType<ProjectItem> = [
{ dataIndex: "ID", title: "ID" },
{ dataIndex: "Title", title: "Project" },
{ dataIndex: ["Category", "Title"], title: "Category" },
];
<CustomTable<ProjectItem>
columns={columns}
serverSideRender={{
service: projectService,
tableName: "Projects",
searchFields: ["Title", "Category/Title"],
}}
/>;The join matches primaryJoinColumn values against childJoinColumn values;
rows without a match render null. Sorting by Category/Title, filtering the
Category/Title column, and searching across joined fields all work because
the adapter evaluates them over the cached joined rows. Raw OData filter is
not supported by this adapter; use structured filters instead. This pattern
fits lists of up to a few tens of thousands of rows; larger lists should use
a real Lookup or a server-side join.
ASP.NET Core and SQL Server paging
Use the offset adapter for a .NET API that returns { items, total }. Unlike
SharePoint cursor paging, this mode enables numbered pages and quick-jump.
import { useMemo } from "react";
import { createDotNetTableService, CustomTable } from "@dinhn1202/ui-kit";
const projectApi = useMemo(
() =>
createDotNetTableService<ProjectItem>({
endpoint: "/api/table-query",
headers: () => ({ Authorization: `Bearer ${accessToken}` }),
}),
[accessToken],
);
<CustomTable<ProjectItem>
columns={columns}
serverSideRender={{
service: projectApi,
tableName: "Projects",
pageSize: 50,
filters: [{ field: "Status", value: "Active" }],
sort: { field: "Modified", direction: "descend" },
searchFields: ["Title", "ProjectCode"],
}}
/>;The adapter sends a POST body containing tableName, page, pageSize,
select, structured filters, column filters, sorting, and search. The API must
return this shape:
{
"items": [{ "ID": 1, "Title": "Project A" }],
"total": 30000
}Create the service once (outside the component or with useMemo) so its page
cache survives re-renders. Raw filter clauses are intentionally rejected by
the .NET adapter. The API must allowlist table and column names and parameterize
filter values; never concatenate client-provided schema names or values into
SQL. A controller and EF Core reference implementation is available in
examples/dotnet.
All other Ant Design props remain available on their DTO wrappers.
CustomLoading renders the four-color animated dot treatment from the Framas splash reference. Pass fullScreen for a fixed viewport overlay, or wrap content with overlay to cover only that content area. Use logoSrc to render a brand image above the dots.
Gantt
CustomGantt renders a read-only timeline with nested items, dependencies,
progress, optional phase and signal columns, tooltips, context actions, and
drag-to-reorder callbacks. Dates may be Date, ISO strings, or Dayjs values.
Invalid or missing dates are safely rendered from timelineStart while the list
continues to show - for unavailable source values.
import {
CustomGantt,
type GanttItem,
} from "@dinhn1202/ui-kit";
const items: GanttItem<{ sourceId: number }>[] = [
{
id: "release",
title: "Release 1",
type: "project",
start: "2026-08-01",
end: "2026-09-15",
progress: 40,
metadata: { sourceId: 10 },
},
{
id: "implementation",
title: "Implementation",
parentId: "release",
dependencyIds: [],
start: "2026-08-05",
end: "2026-08-28",
owner: "Maya Chen",
signals: {
notes: { available: true, isNew: true },
files: { available: true, tooltip: "Specification.pdf" },
},
metadata: { sourceId: 11 },
},
];
<CustomGantt
items={items}
timelineStart="2026-08-01"
timelineEnd="2026-09-30"
viewMode="weeks"
showPhaseColumn
getContextMenuItems={() => [{ key: "edit", label: "Edit item" }]}
onContextAction={(item, action) => console.log(action, item.metadata)}
onItemClick={(item) => console.log(item)}
onReorder={(source, target) => console.log(source.id, target.id)}
onSignalClick={(item, signal) => console.log(item.id, signal)}
/>The component owns only presentation and transient expansion state. Persisting reordering, edits, context actions, or signal actions remains the responsibility of the consuming application.
User Select service
CustomUserSelect supports two data modes. Pass service to search a directory
with debouncing, cancellation of stale requests, and optional avatar resolution.
Pass dataSource when users are already available locally; this disables the
search input and does not call the service.
import { CustomUserSelect, type UserSearchService } from "@dinhn1202/ui-kit";
const graphUserService: UserSearchService = {
searchUsers: (query, signal) => graphDirectory.searchUsers(query, signal),
getUserAvatar: (user, signal) => graphDirectory.getAvatarUrl(user.id, signal),
};
<CustomUserSelect
fullWidth
placeholder="Search by name or email"
service={graphUserService}
onChange={(userId, user) => console.log(userId, user)}
/><CustomUserSelect
dataSource={users}
placeholder="Select a user"
onChange={(userId, user) => console.log(userId, user)}
/>Users can include { id, displayName, email?, avatarUrl?, jobTitle?,
officeLocation? }. Dropdown items show an avatar and display name; hovering an
item shows its available email, job title, and office location. If avatarUrl
is not included in service mode, implement getUserAvatar to fetch the Graph
profile photo; initials are shown while no photo is available.
Node.js compatibility
This package is configured for:
Node.js v18.20.8
Vite 6.4.3
@vitejs/plugin-react 4.7.0
vite-plugin-dts 4.5.4Do not upgrade Vite to v7+ unless Node.js is upgraded to a compatible version.
Notes
This package wraps Ant Design components while preserving their original props. Each DTO component extends the corresponding Ant Design component props.
