A reusable React + MUI component library for HSMS applications, including a configurable layout, theme tools, and UI primitives.
npm install @crystaltech/hsms-shared-uiA reusable React + MUI component library for HSMS applications, including a configurable layout, theme tools, and UI primitives.
- pnpm: pnpm add @crystaltech/hsms-shared-ui
- yarn: yarn add @crystaltech/hsms-shared-ui
- npm: npm i @crystaltech/hsms-shared-ui
Peer deps (match your app): @mui/material, @mui/icons-material, @mui/x-date-pickers, @emotion/*, react, react-dom, dayjs, react-perfect-scrollbar, simplebar-react.
- Install typings (TypeScript projects):
- pnpm add -D typescript @types/react @types/react-dom
- Configure TypeScript:
- tsconfig.json:
- compilerOptions.jsx: "react-jsx"
- compilerOptions.moduleResolution: "bundler" (or "node")
- compilerOptions.skipLibCheck: true (optional)
- Import components:
- import { MainLayout, ThemeSetting, CustomButton } from '@crystaltech/hsms-shared-ui';
- Import types (optional):
- import type { ITheme } from '@crystaltech/hsms-shared-ui/dist/theme/defaultTheme';
- import type { ILayoutConfig } from '@crystaltech/hsms-shared-ui/dist/components/layout/MainLayout';
- JavaScript projects:
- Enable type suggestions: add // @ts-check to files or use JSDoc typedefs.
- In VS Code ensure: javascript.suggest.autoImports and typescript.suggest.autoImports are enabled.
- Monorepo linking:
- Use workspace linking or pnpm link and restart the TS server if auto-import suggestions don’t show.
Wrap your app with ThemeCustomization and use ThemeSetting to edit and persist the theme.
``tsx
import { ThemeCustomization, ThemeSetting } from "@crystaltech/hsms-shared-ui";
import { defaultTheme, ITheme } from "@crystaltech/hsms-shared-ui/dist/theme/defaultTheme";
export default function App() {
const [theme, setTheme] = useState
const handleSave = async (next: ITheme) => { setTheme(next); return next; };
return (
);
}
`
- ITheme lives in src/theme/defaultTheme.ts and includes palette (navbar, sidebar, primary/secondary, background, text, neutral) and typography.defaultTheme
- Use as a starting point; pass a new object to re-theme.
, layoutConfig: ILayoutConfig, logoutHandler?: () => Promise
- ILayoutConfig:
- userManage?: { user: UserInfo; redirectUrl: string }
- navbar: { showHamburgerInMobile: boolean; themeToggler: boolean }
- sideDrawer: { menuConfig: IMenuConfig[]; settingsConfig: ISettingsConfig; isMinimized: boolean; drawerWidth: number }
- footerText: stringExample:
`tsx
import { MainLayout } from "@crystaltech/hsms-shared-ui";
import { menuConfig, settingsConfig } from "@crystaltech/hsms-shared-ui/dist/components/layout/layoutConfig";const layoutConfig = {
navbar: { showHamburgerInMobile: true, themeToggler: false },
sideDrawer: { menuConfig, settingsConfig, isMinimized: false, drawerWidth: 260 },
userManage: { user, redirectUrl: "/login" },
footerText: "© 2025 All rights reserved",
};
{/ children /}
`$3
- Wraps MainLayout with a BrowserRouter. Use when you don’t already have a router at the app root.$3
- Props: pageTitle, user, authenticated, userRoles?, clients?, loginMethod, logoutHandler, redirectPath?, pages, publicRoutes?, Layout?, mainLayoutConfig, NotFoundPage?.
- Builds routes from pages and protects them via ProtectedRoute.Example (static pages map):
`tsx
import { RoutesConfigLayout } from "@crystaltech/hsms-shared-ui";
import Home from "./pages/index";
import TablePage from "./pages/table";const pages = {
"/src/pages/index.tsx": { default: Home },
"/src/pages/table/index.tsx": { default: TablePage, roles: ["admin"] },
} satisfies import("@crystaltech/hsms-shared-ui/dist/routes/RoutesConfigLayout").IPages;
pageTitle="HSMS"
user={user}
authenticated={isAuthenticated}
loginMethod={login}
logoutHandler={logout}
redirectPath="/login"
pages={pages}
publicRoutes={[{ path: "/about", element: }]}
mainLayoutConfig={layoutConfig}
/>
`UserInfo supports realm_access.roles and per-client roles (resource_access[client].roles). Route-level roles override global roles if provided.Data Table
$3
- Props: tableData: DataStructure, rowsPerPage: number, page: number, handleChangePage, handleChangeRowsPerPage.
- Features: sticky headers, column visibility toggling, keyword filter (CustomInput), pagination.Types (inferred):
`ts
export type FieldContentActionsProps = {
label: string;
icon?: React.ReactNode;
onClick: (item: FieldItem) => void;
variant?: "outlined" | "filled";
};
export type DatePickerProps = { date: string; onChange?: (item: FieldItem, date: Date) => void };
export type FieldItem = {
id: number; // same across rows for a column
fieldName: string;
isVisible: boolean;
fieldType: "text" | "date" | "link" | "label" | "actions" | "date-picker";
fieldContent: string | DatePickerProps | FieldContentActionsProps[];
to?: string; // for link
};
export type FieldItemArray = FieldItem[];
export type DataStructure = FieldItem[][]; // rows => columns
`Example data:
`tsx
import { MultiDynamicTable } from "@crystaltech/hsms-shared-ui";
import Visibility from "@mui/icons-material/Visibility";const dynamicTableData: DataStructure = [
[
{ id: 1, fieldName: "Name", isVisible: true, fieldType: "link", fieldContent: "Alice", to: "/users/1" },
{ id: 2, fieldName: "Status", isVisible: true, fieldType: "label", fieldContent: "Active" },
{ id: 3, fieldName: "Joined", isVisible: true, fieldType: "date", fieldContent: "2025-01-01" },
{ id: 4, fieldName: "Actions", isVisible: true, fieldType: "actions", fieldContent: [
{ label: "View", icon: , variant: "outlined", onClick: (item) => console.log(item) },
] },
{ id: 5, fieldName: "Reminder", isVisible: true, fieldType: "date-picker", fieldContent: {
date: "2025-01-05", onChange: (item, date) => console.log(item, date)
} },
],
[
{ id: 1, fieldName: "Name", isVisible: true, fieldType: "link", fieldContent: "Bob", to: "/users/2" },
{ id: 2, fieldName: "Status", isVisible: true, fieldType: "label", fieldContent: "Inactive" },
{ id: 3, fieldName: "Joined", isVisible: true, fieldType: "date", fieldContent: "2024-12-01" },
{ id: 4, fieldName: "Actions", isVisible: true, fieldType: "actions", fieldContent: [] },
{ id: 5, fieldName: "Reminder", isVisible: true, fieldType: "date-picker", fieldContent: { date: "2025-02-01" } },
],
];
tableData={dynamicTableData}
page={0}
rowsPerPage={10}
handleChangePage={(_, p) => setPage(p)}
handleChangeRowsPerPage={(e) => setRowsPerPage(parseInt(e.target.value, 10))}
/>
`UI Components
All components are re-exported from
src/index.ts.- CustomButton
- Extends MUI
ButtonProps; adds startIcon, endIcon, loading.
- loading disables the button and shows a spinner.- CustomIconButton
- Thin wrapper over MUI
IconButton that always renders children icon.- CustomInput
- Props:
startIcon?, placeholder?, onChange?(value), count?, onClear?.
- Shows count badge and clear button when not empty.- CustomCheckbox
- Props:
checked?, onChange?(checked), label?. Manages internal state.- CustomRadioGroup
- Props:
label, options: {label,value}[], value, onChange(value).- CustomSelect
- Props:
label, value, options: { value: string|number; label: string }[], onChange(SelectChangeEvent).- CustomSwitch
- Props:
checked, onChange(checked).- CustomTabs
- Props:
tabs: {label,value}[], value, onChange(event,value), plus TabsProps.- CustomDatePicker
- Props:
label, value: Dayjs|null, onChange(Dayjs|null|undefined); clearable.- ControlledDatePicker
- Props:
label?, initialValue?: string, onChange?(Dayjs|null); manages its own state.- CustomColorPicker
- Props:
initialColor?, onChange(color); uses react-color SketchPicker.- CustomScrollbar
- Props:
children, maxWidth?, maxHeight?; wraps simplebar-react.- AsyncAutocomplete
- Props:
label, multiple?, fetchOptions(), getOptionLabel, isOptionEqualToValue, onChange?.
- Fetches options on open; shows loading indicator.
- Example:
`tsx
label="Users"
fetchOptions={async () => [{ id:1, name:"Alice" }]}
getOptionLabel={(o) => o.name}
isOptionEqualToValue={(o,v) => o.id === v.id}
onChange={(sel) => console.log(sel)}
/>
`- CheckboxListWithAvatar
- Props:
items: { id:number; label:string; avatarSrc? }[], checkedItems?, onChange(checkedIds).- CustomSelectableList
- Props:
items: { label:string; icon? }[], onSelect?(index); supports selection and a divider after the second item.- Loader
- Props:
size?, text?, fullScreen?.
- Shows animated spinner with module logo; optionally full-screen overlay.Exports
`ts
import {
ThemeSetting,
ThemeCustomization,
MainLayout,
MainlayoutWithWrapper,
RoutesConfigLayout,
MultiDynamicTable,
ProfilePages,
// UI primitives
AsyncAutocomplete,
CheckboxListWithAvatar,
ControlledDatePicker,
CustomButton,
CustomCheckbox,
CustomColorPicker,
CustomDatePicker,
CustomIconButton,
CustomInput,
CustomRadioGroup,
CustomScrollbar,
CustomSelect,
CustomSelectableList,
CustomSwitch,
CustomTabs,
CustomTextField,
Loader,
} from "@crystaltech/hsms-shared-ui";
`Notes
-
RoutesConfigLayout uses ProtectedRoute to enforce roles and clients; route-level roles override global ones.
- Column visibility toggling in MultiDynamicTable relies on shared id values across rows for a given column.
- Ensure peer versions of MUI and React match your app.
- Refer to src/theme/defaultTheme.ts for the complete ITheme shape.Pre-Mount Splash Injector
Prevent initial white screen and show a full-screen Loader while the app initializes.
- ESM (Vite apps): add at the very top of your entry file
-
import '@crystaltech/hsms-shared-ui/preMountSplash';
- Signal lifecycle with events or call markAppReady() when ready.
- IIFE (microfrontend host or legacy HTML): include the CDN script before your app bundles
-
- Optional manual init with a custom root
- import { initPreMountSplash } from '@crystaltech/hsms-shared-ui/preMountSplash';
- initPreMountSplash({ rootId: 'portal-root' });
- In IIFE hosts, signal readiness:
`html
`LocalStorage keys used
- Theme:
theme.mode, theme.themeColor
- Branding: organization_namePWA and CSP
- Vite PWA: ESM import gets precached; CDN IIFE is external and fetched network-first.
- CSP: injector inserts a small