@qkit-emr/web-ui
v2.0.8
Published
Shared UI Component Library for EMR Monorepo
Readme
@qkit-emr/web-ui
Thư viện UI component dùng chung cho hệ thống EMR (Electronic Medical Record) Monorepo, được xây dựng trên nền tảng React, TypeScript và Tailwind CSS với tích hợp shadcn/ui.
🚀 Tính năng chính
- Atomic Design: Tổ chức component theo nguyên tắc Atomic Design (atoms, molecules, organisms, templates, pages)
- shadcn/ui Integration: Tích hợp đầy đủ các component từ shadcn/ui với customization
- Separate Client/Server Exports: Tối ưu bundle size với entry points riêng cho client và server ⚡
- TypeScript: Full TypeScript support với type safety
- Tailwind CSS: Built on Tailwind CSS với utility-first approach
- Responsive Design: Mobile-first responsive design với breakpoints chuẩn
- Accessibility: WCAG 2.1 AA compliant components
- Theme Support: Light/dark theme switching
- Healthcare UI: Components được tối ưu cho giao diện y tế
- Performance: Optimized cho healthcare applications với tree-shaking tối ưu
📦 Cài đặt
npm install @qkit-emr/web-ui🎯 Sử dụng cơ bản
Import Strategy - Hybrid Approach ⚡
Từ phiên bản 1.7.0, @qkit-emr/web-ui hỗ trợ hybrid import pattern - kết hợp tốt nhất của simple imports và optimized server utilities:
✅ Recommended: Client Components (Simple & Clean)
'use client'
// ✅ Import client components & hooks từ root (simple)
import {
Button,
Card,
Input,
useTheme,
useFormField,
cn
} from '@qkit-emr/web-ui'
export function PatientForm() {
const { theme } = useTheme()
return (
<Card className={cn('p-6', theme === 'dark' && 'bg-gray-800')}>
<h2 className="text-xl font-bold mb-4">Quản lý bệnh nhân</h2>
<Input placeholder="Tìm kiếm bệnh nhân..." className="mb-4" />
<Button>Thêm bệnh nhân mới</Button>
</Card>
)
}✅ Server Components (Explicit & Optimized)
// ✅ Import server utilities & types từ /server (explicit)
import { cn, colors, BREAKPOINTS } from '@qkit-emr/web-ui/server'
import { PatientForm } from '@/components/PatientForm' // Client component
export default function PatientsPage() {
return (
<div
className={cn('container', 'mx-auto', 'p-4')}
style={{
maxWidth: BREAKPOINTS.xl,
backgroundColor: colors.background.default
}}
>
<h1 className="text-2xl font-bold">Patients</h1>
<PatientForm />
</div>
)
}💡 Alternative: Pure UI Components
'use client'
// Alternative: Import từ /ui nếu chỉ cần pure shadcn/ui
import { Button, Card, Input } from '@qkit-emr/web-ui/ui'
export function SimpleCard() {
return (
<Card>
<Input placeholder="Search..." />
<Button variant="default" size="lg">
Click me
</Button>
</Card>
)
}🔄 Advanced: Full Separation (Optional)
'use client'
// Optional: Import từ /client nếu muốn explicit separation
import { Button, Card, useTheme } from '@qkit-emr/web-ui/client'
function App() {
const { theme } = useTheme()
return (
<Card className="p-6">
<Button>Themed Button</Button>
</Card>
)
}📊 Bundle Size & Usage Comparison
| Pattern | Client Import | Server Import | Total Bundle | Use Case |
|---------|--------------|---------------|--------------|----------|
| Hybrid (Recommended) ⭐ | Root (~250KB) | /server (~20KB) | ~270KB | ✅ Best DX + Server optimization |
| Full Separation | /client (~180KB) | /server (~20KB) | ~200KB | ✅ Maximum optimization |
| Pure UI Only | /ui (~150KB) | /server (~20KB) | ~170KB | ✅ Lightweight UI-only |
Why Hybrid Pattern?
- ✅ Simple client imports - không cần remember paths
- ✅ Explicit server separation - clear intent, minimal bundle
- ✅ Best developer experience - easy to read & maintain
- ✅ Server optimization - chỉ ~20KB cho server utilities
Note: Root import (~250KB) là acceptable cho client vì client components thường cần nhiều features. Optimization tập trung vào server components với /server entry point (~20KB).
💡 Learn More: Xem MIGRATION-GUIDE-CLIENT-SERVER.md để biết chi tiết patterns và best practices.
Setup Tailwind CSS
Bắt buộc thêm content path tới dist của thư viện (để Tailwind sinh đủ class cho component) và preset:
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/@qkit-emr/web-ui/dist/**/*.{js,ts,jsx,tsx}",
],
presets: [require("@qkit-emr/web-ui/tailwind.preset")],
}Dùng từ NPM: Xem docs/CONSUMING-NPM.md để biết vì sao bản npm có thể “không giống Storybook” và checklist đầy đủ (content, preset, thứ tự CSS, peer deps).
Import styles
import '@qkit-emr/web-ui/styles';🏗️ Cấu trúc component chi tiết
📋 ATOMS - Thành phần cơ bản nhất
Self-Implemented Atoms (Complex - có thư mục riêng)
import {
Button,
ButtonGroup,
ButtonGroupItem,
useButtonGroup,
Input,
InputAtom,
MemoizedInputAtom,
Label,
LabelAtom,
MemoizedLabelAtom,
Icon
} from '@qkit-emr/web-ui';
// Button với variants và medical-specific features
<Button variant="primary" size="md" medical>Khám bệnh</Button>
<ButtonGroup>
<ButtonGroupItem>Lưu</ButtonGroupItem>
<ButtonGroupItem>Hủy</ButtonGroupItem>
</ButtonGroup>
// Input với validation và medical masks
<Input placeholder="Nhập tên bệnh nhân" />
<InputAtom type="phone" mask="(999) 999-9999" />
// Label với accessibility
<Label htmlFor="patient-name">Họ và tên</Label>
// Icon với medical icon mapping
<Icon name="stethoscope" size="md" />UI-Dependent Atoms (Simple - single file)
import {
// Data Display
BadgeAtom,
StatusBadgeAtom,
ProgressAtom,
TimelineAtom,
DataTableAtom,
// Form Controls
DatePickerAtom,
TimePickerAtom,
MultiSelectAtom,
RadioGroupAtom,
InputOTPAtom,
SecureInputAtom,
// UI Enhancement
ModalAtom,
DrawerAtom,
TabsAtom,
AccordionAtom,
TooltipAtom,
CarouselAtom,
// Medical-Specific
LabResultAtom,
// Utilities
SpinnerAtom as Spinner,
DividerAtom as Divider,
TypographyAtom as Typography,
FileInputAtom
} from '@qkit-emr/web-ui';
// Medical status badges
<StatusBadgeAtom status="critical" />
<StatusBadgeAtom status="stable" />
<StatusBadgeAtom status="discharged" />
// Progress indicators
<ProgressAtom value={75} label="Tiến độ điều trị" />
// Medical timeline
<TimelineAtom items={medicalEvents} />
// Lab results display
<LabResultAtom
testName="Xét nghiệm máu"
result="120 mg/dL"
normalRange="70-100 mg/dL"
status="high"
/>
// Secure input for sensitive data
<SecureInputAtom
placeholder="Nhập mật khẩu"
showToggle={true}
/>
// OTP input for 2FA
<InputOTPAtom length={6} />
// Medical file upload
<FileInputAtom
accept=".pdf,.jpg,.png"
maxSize={5 * 1024 * 1024} // 5MB
medical={true}
/>Typography Atoms
import {
Heading1,
Heading2,
Heading3,
Paragraph,
Caption,
ErrorText,
SuccessText
} from '@qkit-emr/web-ui';
<Heading1>Tiêu đề chính</Heading1>
<Heading2>Tiêu đề phụ</Heading2>
<Paragraph>Nội dung văn bản</Paragraph>
<ErrorText>Thông báo lỗi</ErrorText>
<SuccessText>Thông báo thành công</SuccessText>🧬 MOLECULES - Kết hợp Atoms
import {
FormField,
SearchBar,
Alert,
Badge,
Card,
Modal
} from '@qkit-emr/web-ui';
// Form field với validation
<FormField
label="Họ và tên"
error="Tên không được để trống"
required
>
<Input placeholder="Nhập họ và tên" />
</FormField>
// Search bar với medical context
<SearchBar
placeholder="Tìm kiếm bệnh nhân, mã BHYT..."
onSearch={(value) => console.log(value)}
medical={true}
/>
// Medical alert
<Alert
type="warning"
title="Cảnh báo thuốc"
description="Bệnh nhân có tiền sử dị ứng với Penicillin"
/>
// Medical calendar - FullCalendar
<FullCalendar
events={medicalEvents}
initialView="timeGridWeek"
medical={true}
selectable={true}
onEventClick={(info) => console.log(info.event)}
onDateClick={(info) => console.log('Clicked date:', info.date)}
/>📅 FullCalendar - Medical Scheduling
✨ Fully integrated with shadcn/ui design system!
⚠️ Important: FullCalendar CSS is separated from main bundle for optimal performance:
// 1️⃣ Import FullCalendar CSS ONLY when using calendar (không tự động load)
import '@qkit-emr/web-ui/fullcalendar-styles';
// 2️⃣ Import component
import { FullCalendar, type MedicalEvent } from '@qkit-emr/web-ui';
// 3️⃣ Define medical events
const appointments: MedicalEvent[] = [
{
id: '1',
title: 'Khám bệnh - Nguyễn Văn A',
start: '2024-01-15T09:00:00',
end: '2024-01-15T09:30:00',
medicalType: 'appointment',
status: 'confirmed',
priority: 'normal',
patientName: 'Nguyễn Văn A',
doctorName: 'BS. Trần Thị B',
department: 'Khoa Nội',
location: 'Phòng 101',
},
{
id: '2',
title: 'Phẫu thuật - Lê Thị C',
start: '2024-01-15T14:00:00',
end: '2024-01-15T16:00:00',
medicalType: 'surgery',
status: 'scheduled',
priority: 'high',
patientName: 'Lê Thị C',
doctorName: 'PGS.TS Nguyễn Văn D',
department: 'Khoa Ngoại',
location: 'Phòng mổ 1',
},
{
id: '3',
title: 'Cấp cứu - BHYT',
start: '2024-01-16T10:30:00',
medicalType: 'emergency',
status: 'inProgress',
priority: 'emergency',
department: 'Khoa Cấp cứu',
},
];
// Month view with full event details
<FullCalendar
events={appointments}
initialView="dayGridMonth"
medical={true}
/>
// Month view with dot display (compact)
<FullCalendar
events={appointments}
initialView="dayGridMonth"
medical={true}
dotView={true}
dayMaxEvents={3} // Show max 3 events, rest in "+X more"
/>
// Week view with time slots
<FullCalendar
events={appointments}
initialView="timeGridWeek"
medical={true}
selectable={true}
editable={true}
slotMinTime="08:00:00"
slotMaxTime="18:00:00"
businessHours={{
daysOfWeek: [1, 2, 3, 4, 5],
startTime: '08:00',
endTime: '18:00',
}}
onEventClick={(info) => {
console.log('Event clicked:', info.event);
}}
onSelect={(info) => {
console.log('Time slot selected:', info.start, 'to', info.end);
}}
/>
// Day view with drag & drop
<FullCalendar
events={appointments}
initialView="timeGridDay"
medical={true}
selectable={true}
editable={true}
droppable={true}
nowIndicator={true}
onEventDrop={(info) => {
console.log('Event moved:', info.event, 'Delta:', info.delta);
}}
onEventResize={(info) => {
console.log('Event resized:', info.event);
}}
/>
// List view
<FullCalendar
events={appointments}
initialView="listWeek"
medical={true}
/>Medical Event Types:
appointment- Lịch khám bệnhsurgery- Phẫu thuậtshift- Ca trựcmeeting- Họpemergency- Cấp cứucheckup- Khám định kỳfollowup- Tái khámvaccination- Tiêm chủng
Event Status:
scheduled- Đã lên lịchconfirmed- Đã xác nhậninProgress- Đang diễn racompleted- Hoàn thànhcancelled- Đã hủynoShow- Không đếnrescheduled- Đã dời lịch
Priority Levels:
low- Thấpnormal- Bình thườnghigh- Caourgent- Khẩn cấpemergency- Cấp cứu
🧠 ORGANISMS - Components phức tạp
import {
Header,
Sidebar,
Navigation,
DataTable,
Breadcrumb,
Pagination
} from '@qkit-emr/web-ui';
// Medical data table
<DataTable
data={patients}
columns={[
{ key: 'name', label: 'Họ tên' },
{ key: 'age', label: 'Tuổi' },
{ key: 'diagnosis', label: 'Chẩn đoán' },
{ key: 'status', label: 'Trạng thái' }
]}
pagination={{
currentPage: 1,
totalPages: 10,
onPageChange: (page) => console.log(page)
}}
medical={true}
/>
// Medical navigation
<Navigation
items={[
{ label: 'Dashboard', icon: 'dashboard', href: '/' },
{ label: 'Bệnh nhân', icon: 'users', href: '/patients' },
{ label: 'Lịch hẹn', icon: 'calendar', href: '/appointments' },
{ label: 'Báo cáo', icon: 'chart', href: '/reports' }
]}
medical={true}
/>📄 TEMPLATES - Layout patterns
import {
AuthTemplate,
DashboardTemplate,
DetailTemplate,
ErrorTemplate,
FormTemplate,
ListTemplate
} from '@qkit-emr/web-ui';
// Medical dashboard template
<DashboardTemplate
header={<Header title="Dashboard Bệnh viện" />}
sidebar={<Sidebar />}
content={<DashboardContent />}
medical={true}
/>
// Patient detail template
<DetailTemplate
header={<PatientHeader patient={patient} />}
content={<PatientDetails patient={patient} />}
actions={<PatientActions patient={patient} />}
medical={true}
/>
// Medical form template
<FormTemplate
title="Thêm bệnh nhân mới"
form={<PatientForm />}
actions={<FormActions />}
medical={true}
/>📱 RESPONSIVE SYSTEM - Mobile-first development
import {
// Responsive Providers & Hooks
ResponsiveProvider,
useResponsive,
// Responsive Layout Components
ResponsiveLayoutContainer,
ResponsiveGrid,
// Mobile Components
MobileLayout,
MobileStack,
MobileContainer,
MobileBottomNavigation,
MobileDrawerNavigation,
MobileHamburgerMenu,
// Tablet Components
TabletLayout,
TabletContainer,
TabletStack,
// Desktop Components
DesktopLayout,
DesktopContainer,
DesktopStack,
DesktopGrid,
// Responsive Form Components
ResponsiveForm,
ResponsiveFormField,
ResponsiveInputField,
ResponsiveSelectField,
ResponsiveTextareaField,
ResponsiveCheckboxField,
ResponsiveRadioField,
ResponsiveFormActions
} from '@qkit-emr/web-ui';
// Responsive medical form
<ResponsiveProvider>
<ResponsiveForm>
<ResponsiveFormField label="Họ và tên">
<ResponsiveInputField placeholder="Nhập họ và tên" />
</ResponsiveFormField>
<ResponsiveFormField label="Ngày sinh">
<ResponsiveInputField type="date" />
</ResponsiveFormField>
<ResponsiveFormActions>
<Button>Lưu</Button>
<Button variant="outline">Hủy</Button>
</ResponsiveFormActions>
</ResponsiveForm>
</ResponsiveProvider>🎨 Theme và Styling
Theme Provider
import { ThemeProvider } from '@qkit-emr/web-ui';
function App() {
return (
<ThemeProvider>
<YourApp />
</ThemeProvider>
);
}Theme Toggle
import { useThemeToggle } from '@qkit-emr/web-ui';
function ThemeToggle() {
const { toggleTheme, theme } = useThemeToggle();
return (
<Button onClick={toggleTheme}>
{theme === 'dark' ? '🌞' : '🌙'}
</Button>
);
}Medical Theme Colors
// Primary medical colors
className="bg-blue-600 text-white" // Medical blue
className="bg-green-500 text-white" // Success green
className="bg-red-600 text-white" // Error red
className="bg-yellow-500 text-white" // Warning yellow
// Medical status colors
className="bg-red-100 text-red-800" // Critical
className="bg-yellow-100 text-yellow-800" // Warning
className="bg-green-100 text-green-800" // Stable
className="bg-blue-100 text-blue-800" // Info🔧 Customization
Override component styles
import { Button } from '@qkit-emr/web-ui';
// Sử dụng className để override
<Button className="bg-blue-600 hover:bg-blue-700">
Custom Button
</Button>Medical variants
import { Button } from '@qkit-emr/web-ui';
// Button với medical variant
<Button variant="medical" size="lg">
Khám bệnh
</Button>📱 Responsive Design
Tất cả components đều hỗ trợ responsive design với breakpoints:
- Mobile: 320px - 640px (default)
- Tablet: 640px+ (
sm:) - Desktop: 1024px+ (
lg:,xl:,2xl:)
<Card className="p-4 sm:p-6 lg:p-8">
<h1 className="text-xl sm:text-2xl lg:text-3xl">
Responsive Title
</h1>
</Card>♿ Accessibility
Tất cả components đều tuân thủ WCAG 2.1 AA:
- Keyboard navigation support
- Screen reader compatibility
- Focus management
- ARIA labels
- Color contrast compliance
<Button aria-label="Thêm bệnh nhân mới">
<Icon name="plus" />
</Button>🧪 Testing
Unit Testing
import { render, screen } from '@testing-library/react';
import { Button } from '@qkit-emr/web-ui';
test('Button renders correctly', () => {
render(<Button>Test Button</Button>);
expect(screen.getByRole('button')).toBeInTheDocument();
});Storybook
Chạy Storybook để xem tất cả components:
npm run storybookTruy cập online: https://qkit-emr-share-ui.web.app/
📚 API Reference
Button Component
interface ButtonProps {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'medical';
size?: 'default' | 'sm' | 'lg' | 'icon';
disabled?: boolean;
loading?: boolean;
children: React.ReactNode;
onClick?: () => void;
className?: string;
medical?: boolean; // Medical-specific styling
}Input Component
interface InputProps {
type?: 'text' | 'email' | 'password' | 'number' | 'tel';
placeholder?: string;
value?: string;
onChange?: (value: string) => void;
error?: string;
disabled?: boolean;
className?: string;
mask?: string; // Input mask for phone, date, etc.
medical?: boolean; // Medical-specific validation
}Medical Components
// LabResultAtom
interface LabResultAtomProps {
testName: string;
result: string;
normalRange: string;
status: 'normal' | 'high' | 'low' | 'critical';
unit?: string;
date?: Date;
}
// StatusBadgeAtom
interface StatusBadgeAtomProps {
status: 'critical' | 'warning' | 'stable' | 'discharged' | 'admitted';
size?: 'sm' | 'md' | 'lg';
}
// SecureInputAtom
interface SecureInputAtomProps {
placeholder?: string;
showToggle?: boolean;
medical?: boolean; // HIPAA compliance
}🔗 Dependencies
Peer Dependencies
{
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@radix-ui/react-*": "^1.0.0",
"tailwindcss": "^3.3.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
}Dev Dependencies
{
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "~5.8.2"
}🚀 Development
Build
npm run buildDevelopment mode
npm run devStorybook
npm run storybook
npm run build-storybookTesting
npm test
npm run test:watchLinting
npm run lint
npm run lint:fixPublishing to npm
- Đăng nhập: Chạy
npm login(hoặc đã login rồi thì bỏ qua). pnpm dùng chung credential với npm. - Publish (từ root repo):
pnpm run publish:web-uiHoặc từ thư mục libs/web-ui: npm run publish:npm.
(CI: set NPM_TOKEN trong env hoặc .env.local nếu dùng token thay cho login.)
📦 Exports Strategy
1️⃣ Root Entry Point (@qkit-emr/web-ui) ⭐ Recommended for Client
Use for: Client components, hooks, providers (cần 'use client' directive)
Why: Simple, clean imports - không cần remember complex paths
'use client'
import {
// Atoms
Button, Input, Label, Icon,
// Molecules
Card, Badge, Alert, FormField,
// Organisms
DataTable, Header, Sidebar,
// Hooks
useTheme, useFormField, useResponsive,
// Providers
ThemeProvider, ResponsiveProvider,
// UI Components
Dialog, DropdownMenu, Select,
// Utils
cn
} from '@qkit-emr/web-ui'Bundle Size: ~250KB (comprehensive, includes everything you need)
2️⃣ Server Entry Point (@qkit-emr/web-ui/server) ⭐ Recommended for Server
Use for: Server utilities, types, constants (KHÔNG cần 'use client')
Why: Explicit server-only code, minimal bundle (~20KB), clear separation
// Server Component - NO 'use client' needed
import {
// Utilities
cn,
// Theme Configuration
colors, typography, spacing, shadows,
BREAKPOINTS, animations,
// Types
type ButtonProps,
type CardProps,
type Theme,
// Validators
emailSchema, phoneSchema
} from '@qkit-emr/web-ui/server'Bundle Size: ~20KB (minimal, server-safe, optimized) ✅
3️⃣ UI Entry Point (@qkit-emr/web-ui/ui) 💡 Alternative
Use for: Pure shadcn/ui components only (lighter weight alternative)
'use client'
import {
// Pure shadcn/ui components
Button, Input, Card, Dialog,
Alert, Badge, Checkbox, Select,
Table, Tabs, Toast, Tooltip
} from '@qkit-emr/web-ui/ui'Bundle Size: ~150KB (pure UI components)
When to use: Khi bạn chỉ cần UI components mà không cần custom atoms/molecules/organisms
4️⃣ Client Entry Point (@qkit-emr/web-ui/client) 🔄 Advanced
Use for: Explicit client-side separation (advanced use cases)
'use client'
import {
Button,
Card,
useTheme,
cn
} from '@qkit-emr/web-ui/client'Bundle Size: ~180KB (optimized client bundle)
When to use: Khi bạn muốn explicit separation và maximum optimization
📋 Import Mapping Table (Hybrid Pattern)
| Component/Hook | Root (Client) | Server | UI | Client |
|---------------|---------------|--------|-----|--------|
| Button | ✅ Recommended | ❌ | ✅ | ✅ |
| useTheme | ✅ Recommended | ❌ | ❌ | ✅ |
| cn (in client) | ✅ Recommended | ❌ | ❌ | ✅ |
| cn (in server) | ❌ | ✅ Recommended | ❌ | ❌ |
| colors | ❌ | ✅ Only here | ❌ | ❌ |
| BREAKPOINTS | ❌ | ✅ Only here | ❌ | ❌ |
| type ButtonProps | ❌ | ✅ Only here | ❌ | ❌ |
Recommended Pattern:
- ✅ Client components → Import từ root (
@qkit-emr/web-ui) - ✅ Server components → Import từ /server (
@qkit-emr/web-ui/server)
🤝 Contributing
- Fork repository
- Tạo feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Tạo Pull Request
Development Guidelines
- Tuân thủ Atomic Design principles
- Viết TypeScript với strict mode
- Thêm unit tests cho mọi component
- Tạo Storybook stories
- Đảm bảo accessibility compliance
- Responsive design cho mọi component
- Medical-specific features cho healthcare UI
📄 License
MIT License - xem file LICENSE để biết thêm chi tiết.
🆘 Support
- Issues: GitHub Issues
- Documentation: Storybook
- Email: [email protected]
🔄 Changelog
Xem CHANGELOG.md để biết lịch sử thay đổi.
Latest Changes (v1.7.0)
- ✅ Separate Client/Server Exports: Tối ưu bundle size với entry points riêng
- ✅ Bundle Size Optimization: Giảm 28% bundle size với optimized imports
- ✅ Next.js 13+ Support: Full support cho App Router với proper 'use client' directives
- ✅ Migration Guide: Comprehensive guide để migrate từ legacy imports
- ✅ Backward Compatible: Legacy imports vẫn hoạt động
📖 Additional Documentation
- 📚 Migration Guide - Hướng dẫn migrate từ legacy imports
- 🎨 Storybook - Component documentation và examples
- 📦 Build Guide - Hướng dẫn build và deploy
- 🔧 Architecture - Chi tiết về architecture và best practices
Made with ❤️ by EMR Team
