@crystaltech/hsms-shared-ui
v0.7.12
Published
A reusable React + MUI component library for HSMS applications, including a configurable layout, theme tools, and UI primitives.
Readme
HSMS Shared UI
A reusable React + MUI component library for HSMS applications, including a configurable layout, theme tools, and UI primitives.
Installation
- 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.
Using in other modules
- 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-checkto files or use JSDoc typedefs. - In VS Code ensure:
javascript.suggest.autoImportsandtypescript.suggest.autoImportsare enabled.
- Enable type suggestions: add
- Monorepo linking:
- Use workspace linking or
pnpm linkand restart the TS server if auto-import suggestions don’t show.
- Use workspace linking or
Quick Start (Theme)
Wrap your app with ThemeCustomization and use ThemeSetting to edit and persist the theme.
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<ITheme>(defaultTheme);
const handleSave = async (next: ITheme) => { setTheme(next); return next; };
return (
<ThemeCustomization defaultTheme={theme}>
<ThemeSetting theme={theme} onSave={handleSave} />
</ThemeCustomization>
);
}Theme Model
IThemelives insrc/theme/defaultTheme.tsand includespalette(navbar, sidebar, primary/secondary, background, text, neutral) andtypography.- Use
defaultThemeas a starting point; pass a new object to re-theme.
Layout
MainLayout
- Props:
title: string,layoutConfig: ILayoutConfig,logoutHandler?: () => Promise<void> ILayoutConfig:userManage?: { user: UserInfo; redirectUrl: string }navbar: { showHamburgerInMobile: boolean; themeToggler: boolean }sideDrawer: { menuConfig: IMenuConfig[]; settingsConfig: ISettingsConfig; isMinimized: boolean; drawerWidth: number }footerText: string
Example:
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",
};
<MainLayout title="HSMS" layoutConfig={layoutConfig}>
{/* children */}
</MainLayout>MainlayoutWithWrapper
- Wraps
MainLayoutwith aBrowserRouter. Use when you don’t already have a router at the app root.
RoutesConfigLayout
- Props:
pageTitle,user,authenticated,userRoles?,clients?,loginMethod,logoutHandler,redirectPath?,pages,publicRoutes?,Layout?,mainLayoutConfig,NotFoundPage?. - Builds routes from
pagesand protects them viaProtectedRoute.
Example (static pages map):
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;
<RoutesConfigLayout
pageTitle="HSMS"
user={user}
authenticated={isAuthenticated}
loginMethod={login}
logoutHandler={logout}
redirectPath="/login"
pages={pages}
publicRoutes={[{ path: "/about", element: <About /> }]}
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
MultiDynamicTable
- Props:
tableData: DataStructure,rowsPerPage: number,page: number,handleChangePage,handleChangeRowsPerPage. - Features: sticky headers, column visibility toggling, keyword filter (
CustomInput), pagination.
Types (inferred):
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 => columnsExample data:
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: <Visibility />, 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" } },
],
];
<MultiDynamicTable
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; addsstartIcon,endIcon,loading. loadingdisables the button and shows a spinner.
- Extends MUI
CustomIconButton
- Thin wrapper over MUI
IconButtonthat always renderschildrenicon.
- Thin wrapper over MUI
CustomInput
- Props:
startIcon?,placeholder?,onChange?(value),count?,onClear?. - Shows count badge and clear button when not empty.
- Props:
CustomCheckbox
- Props:
checked?,onChange?(checked),label?. Manages internal state.
- Props:
CustomRadioGroup
- Props:
label,options: {label,value}[],value,onChange(value).
- Props:
CustomSelect
- Props:
label,value,options: { value: string|number; label: string }[],onChange(SelectChangeEvent).
- Props:
CustomSwitch
- Props:
checked,onChange(checked).
- Props:
CustomTabs
- Props:
tabs: {label,value}[],value,onChange(event,value), plusTabsProps.
- Props:
CustomDatePicker
- Props:
label,value: Dayjs|null,onChange(Dayjs|null|undefined); clearable.
- Props:
ControlledDatePicker
- Props:
label?,initialValue?: string,onChange?(Dayjs|null); manages its own state.
- Props:
CustomColorPicker
- Props:
initialColor?,onChange(color); usesreact-colorSketchPicker.
- Props:
CustomScrollbar
- Props:
children,maxWidth?,maxHeight?; wrapssimplebar-react.
- Props:
AsyncAutocomplete
- Props:
label,multiple?,fetchOptions(),getOptionLabel,isOptionEqualToValue,onChange?. - Fetches options on open; shows loading indicator.
- Example:
<AsyncAutocomplete label="Users" fetchOptions={async () => [{ id:1, name:"Alice" }]} getOptionLabel={(o) => o.name} isOptionEqualToValue={(o,v) => o.id === v.id} onChange={(sel) => console.log(sel)} />
- Props:
CheckboxListWithAvatar
- Props:
items: { id:number; label:string; avatarSrc? }[],checkedItems?,onChange(checkedIds).
- Props:
CustomSelectableList
- Props:
items: { label:string; icon? }[],onSelect?(index); supports selection and a divider after the second item.
- Props:
Loader
- Props:
size?,text?,fullScreen?. - Shows animated spinner with module logo; optionally full-screen overlay.
- Props:
Exports
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
RoutesConfigLayoutusesProtectedRouteto enforcerolesandclients; route-levelrolesoverride global ones.- Column visibility toggling in
MultiDynamicTablerelies on sharedidvalues across rows for a given column. - Ensure peer versions of MUI and React match your app.
- Refer to
src/theme/defaultTheme.tsfor the completeIThemeshape.
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
<script src="https://unpkg.com/@crystaltech/hsms-shared-ui/dist/preMountSplash.iife.js" crossorigin="anonymous"></script>
- Optional manual init with a custom root
import { initPreMountSplash } from '@crystaltech/hsms-shared-ui/preMountSplash';initPreMountSplash({ rootId: 'portal-root' });- In IIFE hosts, signal readiness:
<script> // later in bootstrap window.HSMSPreMountSplash?.markAppReady(); // or: window.dispatchEvent(new Event('app-ready')) </script>
LocalStorage keys used
- Theme:
theme.mode,theme.themeColor - Branding:
organization_name
PWA and CSP
- Vite PWA: ESM import gets precached; CDN IIFE is external and fetched network-first.
- CSP: injector inserts a small
<style>; allow inline styles or configure policy accordingly.
Standard lifecycle
- Adds
app-loadingclass tohtmlon init; removes it and setsapp-readyon hide. - Sets
window.__APP_READY__ = trueand dispatches a globalapp-readyevent when hidden. - Hides when the app signals readiness via
hsms-app-mountedorapp-ready. - You can programmatically call
markAppReady()or dispatchnew Event('app-ready').
Example host page: see examples/host.html.
Quick Start (Splash)
- Choose integration:
- Vite apps: add
import '@crystaltech/hsms-shared-ui/preMountSplash'at the top of your entry file. - Microfrontend/legacy: include the CDN IIFE script in your host HTML.
- Vite apps: add
- Ensure branding/theme keys are available early:
localStorage.organization_name,localStorage.theme.
- Optional manual init and custom root:
initPreMountSplash({ rootId: 'portal-root' }).
- Signal readiness after your app mounts:
- ESM: dispatch
hsms-app-mountedand/or callmarkAppReady(). - IIFE:
window.HSMSPreMountSplash?.markAppReady()orwindow.dispatchEvent(new Event('app-ready')).
- ESM: dispatch
- Verify behavior:
- First paint:
<html>hasapp-loadingand splash is visible. - After readiness: splash fades out and
<html>switches toapp-ready.
- First paint:
ESM example:
import '@crystaltech/hsms-shared-ui/preMountSplash';
// ... your app bootstrap (fetch config, auth, etc.)
window.dispatchEvent(new Event('hsms-app-mounted'));
window.dispatchEvent(new Event('app-ready'));IIFE host example:
<script src="https://unpkg.com/@crystaltech/hsms-shared-ui/dist/preMountSplash.iife.js" crossorigin="anonymous"></script>
<script>
window.HSMSPreMountSplash?.initPreMountSplash({ rootId: 'portal-root' });
// after app is mounted
window.HSMSPreMountSplash?.markAppReady();
// or: window.dispatchEvent(new Event('app-ready'));
// optional: set branding early so it shows on splash
localStorage.setItem('organization_name', 'HSMS');
localStorage.setItem('theme', JSON.stringify({ mode: 'light', themeColor: '#2c5282' }));
</script>Adoption Checklist (Microfrontends)
- Choose one integration per app: ESM import (Vite) or IIFE CDN (host HTML) — avoid double init.
- Standardize the mount root id (e.g.,
portal-root) and pass{ rootId }consistently. - Seed brand/theme early at login/bootstrap:
organization_name,theme. - Signal readiness on mount: call
markAppReady()or dispatchnew Event('app-ready'). - CSP: allow the injected
<style>in policy. - Performance: include the IIFE script before heavy bundles; avoid blocking inline scripts before splash.
- PWA: ensure ESM import is precached; if using CDN IIFE, verify network fallback strategy.
- QA: verify first paint shows splash, fade-out occurs, and
<html>togglesapp-loading→app-ready.
Minimal Steps (Other Modules)
- Vite apps (ESM):
- Install:
pnpm add @crystaltech/hsms-shared-ui - At top of
src/main.tsx(orsrc/index.ts):import '@crystaltech/hsms-shared-ui/preMountSplash' - After your app mounts: dispatch
hsms-app-mountedand/or callmarkAppReady(). - Optional: set early brand/theme keys:
localStorage.setItem('organization_name', 'HSMS');
- Install:
React (Vite) example — recommended wiring
// src/main.tsx
import '@crystaltech/hsms-shared-ui/preMountSplash';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// src/App.tsx
import { useEffect } from 'react';
import { markAppReady } from '@crystaltech/hsms-shared-ui/preMountSplash';
export default function App() {
useEffect(() => { markAppReady(); }, []);
return (
<div>Your UI</div>
);
}- Microfrontend/legacy hosts (IIFE):
- Add script before your bundles:
<script src="https://unpkg.com/@crystaltech/hsms-shared-ui/dist/preMountSplash.iife.js" crossorigin="anonymous"></script> - Optional init with root id:
<script>window.HSMSPreMountSplash?.initPreMountSplash({ rootId: 'portal-root' });</script> - After mount, signal ready:
<script>window.HSMSPreMountSplash?.markAppReady();</script> - Optional: set early brand/theme keys via
localStorage.
- Add script before your bundles:
