@juncore/core-ui
v0.1.3
Published
Jun design system — core UI components
Readme
@juncore/core-ui
Component library cho hệ thống design token-based. Tất cả sizing, color, radius, spacing đều lấy từ @juncore/core-theme — không có hardcode giá trị pixel.
Cài đặt
yarn add @juncore/core-ui @juncore/core-themeSetup
// main.tsx — bọc toàn app
import { ThemeProvider } from '@juncore/core-theme';
<ThemeProvider><App /></ThemeProvider>Design token classes (Tailwind)
| Nhóm | Class pattern | Ví dụ |
|------|--------------|-------|
| Spacing | p/px/py/m/gap-theme-{xs\|sm\|md\|lg\|xl} | px-theme-md |
| Radius | rounded-theme-{sm\|md\|lg\|full} | rounded-theme-md |
| Font size | text-theme-{xs\|sm\|base\|lg\|xl\|2xl} | text-theme-sm |
| Primary | bg/text/border-primary-{base\|hover\|light\|dark\|contrast} | bg-primary-base |
| Secondary | bg/text/border-secondary-{base\|hover\|light\|dark\|contrast} | text-secondary-base |
| Neutral | bg/text/border-neutral-{background\|surface\|border\|text\|textLight\|textMuted} | bg-neutral-surface |
| Success | bg/text/border-success-{base\|light\|text} | text-success-text |
| Error | bg/text/border-error-{base\|light\|text} | bg-error-light |
Polymorphic as prop
Nhiều component hỗ trợ as để render thành bất kỳ HTML tag hoặc component nào:
<Text as="span">inline text</Text>
<Group as="nav">navigation</Group>
<Container as="section">content</Container>Components
Button
<Button variant="primary" size="md" onClick={...}>Label</Button>| Prop | Type | Default |
|------|------|---------|
| variant | 'primary' \| 'secondary' \| 'outline' \| 'ghost' | 'primary' |
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| as | React.ElementType | 'button' |
Nhận tất cả React.ButtonHTMLAttributes.
Card
<Card padding="md" hover glow className="...">content</Card>| Prop | Type | Default |
|------|------|---------|
| padding | 'none' \| 'sm' \| 'md' \| 'lg' | 'md' |
| hover | boolean | false |
| glow | boolean | false |
| as | React.ElementType | 'div' |
Text
<Text size="sm" weight="semibold" variant="muted">Hello</Text>
<Text span>inline span</Text>
<Text variant="gradient" gradientColors="from-primary-base to-secondary-base">Title</Text>| Prop | Type | Default |
|------|------|---------|
| size | 'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| '2xl' | 'base' |
| variant | 'normal' \| 'gradient' \| 'muted' \| 'dimmed' | 'normal' |
| weight | 'normal' \| 'medium' \| 'semibold' \| 'bold' | 'normal' |
| mono | boolean | false |
| span | boolean | false — render <span> thay vì <p> |
| lineClamp | 1 \| 2 \| 3 \| 4 | — |
| gradientColors | string | 'from-primary-base to-secondary-base' |
| as | React.ElementType | 'p' |
Title
<Title order={2} variant="gradient">Heading</Title>| Prop | Type | Default |
|------|------|---------|
| order | 1 \| 2 \| 3 \| 4 | 1 — cũng quyết định tag h1–h4 |
| variant | 'normal' \| 'gradient' | 'normal' |
| gradientColors | string | 'from-primary-base to-secondary-base' |
| as | React.ElementType | h{order} |
Badge
<Badge variant="success" size="md">NEW</Badge>| Prop | Type | Default |
|------|------|---------|
| variant | 'primary' \| 'secondary' \| 'success' \| 'danger' \| 'neutral' | 'primary' |
| size | 'sm' \| 'md' | 'sm' |
Alert
<Alert type="error" title="Lỗi xác thực">Vui lòng kiểm tra lại form.</Alert>| Prop | Type | Default |
|------|------|---------|
| type | 'success' \| 'warning' \| 'error' \| 'info' | 'info' |
| title | string | — |
Icon tự động theo type.
Input
<Input placeholder="Email" error="Không hợp lệ" />| Prop | Type | Default |
|------|------|---------|
| error | string | — — hiển thị message đỏ bên dưới |
Nhận tất cả React.InputHTMLAttributes. Height động qua CSS var --components-input-heightMd.
PasswordInput
<PasswordInput placeholder="Mật khẩu" error="..." />Wrapper của Input với toggle show/hide password.
Select
<Select options={[{ value: 'a', label: 'Option A' }]} value={val} onChange={...} />| Prop | Type |
|------|------|
| options | { value: string; label: string }[] |
| sizeVariant | 'sm' \| 'md' \| 'lg' |
Checkbox / Radio / Switch
<Checkbox label="Nhớ mật khẩu" checked={v} onChange={...} />
<Radio label="Option" value="a" checked={v} onChange={...} />
<Switch checked={v} onChange={...} label="Bật thông báo" />Slider
<Slider value={50} min={0} max={100} onChange={...} label="Volume" showValue />| Prop | Type | Default |
|------|------|---------|
| label | string | — |
| showValue | boolean | false |
Avatar
<Avatar email="[email protected]" role="Admin" presence="online" />| Prop | Type | Default |
|------|------|---------|
| email | string | — (dùng để generate initials) |
| role | string | — |
| presence | 'online' \| 'offline' \| 'away' | 'online' |
Layout: Group / Stack / Container / Center / SimpleGrid
<Group gap="sm" align="center" justify="space-between" wrap="nowrap">
<Button>A</Button><Button>B</Button>
</Group>
<Stack gap="lg" align="stretch">
<Card>1</Card><Card>2</Card>
</Stack>
<Container size="lg">page content</Container>
<Center>centered content</Center>
<SimpleGrid cols={3} spacing="md">
<Card /><Card /><Card />
</SimpleGrid>Group props: gap, align (stretch|center|flex-start|flex-end|baseline), justify, grow, wrap
Stack props: gap, align, justify
Container sizes: xs | sm | md | lg | xl | fluid
SimpleGrid: cols (number), spacing
Tất cả đều hỗ trợ as prop.
Divider
<Divider label="hoặc" />Modal / Drawer / BottomSheet
// Modal
<Modal opened={open} onClose={() => setOpen(false)} title="Tiêu đề" size="md">
content
</Modal>
// Drawer
<Drawer opened={open} onClose={...} title="Menu" position="right" size="md">
content
</Drawer>
// BottomSheet (mobile)
<BottomSheet opened={open} onClose={...} title="Tùy chọn" snapPoints={[300, 500]}>
content
</BottomSheet>Modal sizes: sm | md | lg | xl
Drawer positions: left | right | top | bottom
Dropdown
<Dropdown
trigger={<Button>Menu</Button>}
items={[
{ label: 'Edit', icon: <Edit />, onClick: () => {} },
{ label: 'Delete', danger: true, onClick: () => {} },
{ type: 'divider' }
]}
position="bottom-end"
/>Tooltip
<Tooltip content="Giải thích" position="top">
<Button>Hover me</Button>
</Tooltip>Toast
// Dùng hook
const { showToast, ToastContainer } = useToast();
showToast('Lưu thành công!', 'success');
// Render container ở root
<ToastContainer />type: 'success' | 'error' | 'info' | 'warning'
Tab
<Tab
tabs={[{ id: 'a', label: 'Tab A' }, { id: 'b', label: 'Tab B' }]}
activeTabId={active}
onChange={setActive}
/>SegmentedControl
<SegmentedControl
options={[{ value: 'list', label: 'List' }, { value: 'grid', label: 'Grid' }]}
value={view}
onChange={setView}
/>Accordion
<Accordion items={[{ id: 'q1', label: 'Câu hỏi?', content: <p>Trả lời</p> }]} />ProgressBar
<ProgressBar value={75} label="Upload" showPercent color="primary" size="md" />| Prop | Type | Default |
|------|------|---------|
| value | number (0–100) | — |
| color | 'primary' \| 'secondary' \| 'success' \| 'error' | 'primary' |
| size | 'sm' \| 'md' \| 'lg' | 'md' |
Skeleton
<Skeleton width="100%" height={20} rounded="md" />
<Skeleton variant="circle" width={40} height={40} />
<Skeleton variant="text" lines={3} />LoadingOverlay
<LoadingOverlay visible={loading} message="Đang tải..." />EmptyState
<EmptyState
icon={<Inbox />}
title="Chưa có dữ liệu"
description="Tạo mục đầu tiên để bắt đầu."
action={<Button>Tạo mới</Button>}
/>Chip
<Chip label="React" onRemove={() => {}} active />Carousel
<Carousel items={slides} renderItem={(item) => <Card>{item.title}</Card>} autoPlay interval={3000} />Stepper
<Stepper
steps={[{ label: 'Bước 1', description: 'Mô tả' }, ...]}
activeStep={1}
orientation="horizontal"
/>TabBar / TabsBar
// TabBar — bottom nav style
<TabBar
tabs={[{ id: 'home', label: 'Home', icon: <Home /> }]}
activeTabId={tab}
onChange={setTab}
/>
// TabsBar — code editor style
<TabsBar
tabs={[{ id: 'ts', label: 'index.ts' }]}
activeTabId={tab}
onChange={setTab}
/>Pressable
<Pressable onPress={() => {}} scale={0.97}>
<Card>tap me</Card>
</Pressable>CoreImage
<CoreImage src="/img.png" alt="photo" fallback={<Skeleton />} className="rounded-theme-md" />CodeBlock
<CodeBlock code={`const x = 1;`} language="typescript" showLineNumbers />Terminal
<Terminal logs={['[INFO] Server started']} title="Logs" maxHeight={200} />SearchToolbar
<SearchToolbar
value={query}
onChange={setQuery}
placeholder="Tìm kiếm..."
onFilter={() => {}}
onSort={() => {}}
/>ThemeIndicator
Hiển thị dot màu active theme — dùng cho debug/preview.
<ThemeIndicator />Icon
import { Icon } from '@juncore/core-ui';
<Icon name="star" size={20} className="text-primary-base" />Box / Stack (primitives)
// Box — wrapper với spacing/layout props
<Box p="md" m="sm" display="flex">content</Box>
// Stack (primitive) — flex-col
<Stack gap="md">children</Stack>Utility: cn
import { cn } from '@juncore/core-ui';
cn('base-class', condition && 'conditional', className)Wrapper của clsx + tailwind-merge.
Polymorphic types (xuất ra để dùng trong custom components)
import { PolymorphicComponentPropWithRef } from '@juncore/core-ui';
function MyComponent<C extends React.ElementType = 'div'>({
as, ref, children, ...props
}: PolymorphicComponentPropWithRef<C, { myProp: string }>) {
const Component = (as || 'div') as React.ElementType;
return <Component ref={ref as any} {...props}>{children}</Component>;
}