lily-lark-ui
v0.10.2
Published
Thư viện component React theo phong cách Lark (Universe Design) cho React 19 + Tailwind v4. 40 component + `LarkMotionProvider` (bật/tắt chuyển động), nền Radix headless, phát hành trên npm.
Maintainers
Readme
lily-lark-ui
Thư viện component React theo phong cách Lark (Universe Design) cho React 19 +
Tailwind v4. 40 component + LarkMotionProvider (bật/tắt chuyển động), nền Radix
headless, phát hành trên npm.
npx lily-lark-ui init # cài vào app hiện tại (tự cài Tailwind v4)Phiên bản hiện tại: 0.10.2. Mọi component nhận đủ prop HTML của phần tử gốc
(onBlur, onFocus, onKeyDown, name, form, value, data-*, aria-*,
style, id, ref) — xem mục Prop HTML.
Cài đặt
Xem INSTALL.md để biết đầy đủ. Tóm tắt 2 đường:
- CLI (khuyến nghị):
npx lily-lark-ui init— tự cài Tailwind v4, sửavite.config.ts, chèn CSS +@source. - npm thủ công:
pnpm add lily-lark-uirồi tự thêm 3 dòng CSS. - script:
scripts/install.sh --repo <path> --apply(nếu dùng source checkout).
Yêu cầu bắt buộc
| Thứ | Phiên bản | Ghi chú |
|---|---|---|
| Tailwind CSS | v4 (^4.0.0) | Token dùng @theme/@utility của v4. v3 không chạy. |
| @tailwindcss/vite | ^4.0.0 | Plugin build (không dùng PostCSS config kiểu v3). |
| React / React DOM | ^19.0.0 | peerDependency. |
| Node | >= 20 | ESM + Vite hiện hành. |
npm i -D tailwindcss@^4 @tailwindcss/viteBa dòng bắt buộc trong entry CSS
Tailwind v4 không tự quét node_modules. Thiếu @source thì component
render ra không có style (nút trong suốt, sai chiều cao) dù build không báo lỗi.
@import "tailwindcss";
@import "lily-lark-ui/styles.css";
@source "../node_modules/lily-lark-ui/src";@import "tailwindcss" phải đứng trước import của thư viện. Nếu app đã có CSS
riêng, bọc CSS đó vào @layer base để không đè utility của component.
Kiểm tra sau khi build: grep -c 'bg-brand-secondary' dist/assets/*.css phải
lớn hơn 0.
Cai vao repo moi
Mot lenh (6 buoc + kiem Tailwind v4, copy --components neu co, check-lark report):
npx lily-lark-ui init # app hien tai
npx lily-lark-ui init --repo ../app # app khac
npx lily-lark-ui init --dry-run # chi in ke hoach
npx lily-lark-ui init --no-bootstrap # khong dung Tailwind/vite config
npx lily-lark-ui init --components button,tableHoac goi thang script trong source checkout:
scripts/install.sh --repo <path> --apply [--components button,text-field]- Khong truyen
--applythi dry-run: chi in ke hoach, khong sua file. --skip-install: bo qua buoc cai package (dung cho test offline).--components <list>: chi copy component can (ten cach nhau boi dau phay, vdbutton,text-field); kemsrc/lib/utils.ts+src/styles/lark-tokens.cssneu chua co. Ten sai thi bao loi + liet ke ten hop le.- Idempotent: chay lai khong them trung import/block MCP.
Phát hành (maintainer)
pnpm release:dry # build + test + check-lark + publint + pack + import(), khong publish
pnpm release # publish thật, rồi cài lại từ registry để xác nhậnPackage là lily-lark-ui (không scope). Publish cần token npm có quyền ghi
package. Token hiện bị giới hạn cho staging area: npm publish trả
E_STAGE_REQUIRED, khi đó scripts/publish.sh tự chạy npm stage publish và
in npm stage approve <stage-id> — bước approve cần 2FA của chủ tài khoản. Stage
xong chưa lên npm; bản mới cũng đi qua automated review của npm (vài phút) trước
khi thành latest.
Component
Tất cả component nhận đủ prop HTML của phần tử gốc; bảng dưới chỉ liệt kê prop riêng của thư viện.
| Component | Props riêng | Số đo |
|---|---|---|
| LarkButton | variant (primary, secondary, secondaryBlue, ghost, text, primaryError, secondaryError), size (xs/sm/md/lg), isIconOnly, fullWidth, isPending, asChild | 32px mặc định (h-8), 40px nút chính form (h-10), 28/24 nhỏ; mobile 36/48px |
| LarkTextField (+ inputClass) | label, error, description, isRequired, isInvalid | 42px mobile / 36px PC (md:min-h-9), chữ 16/14 |
| LarkTextArea | label, error, isRequired | tối thiểu 72px (~3 dòng) |
| LarkDropdownMenu | trigger, align (start/center/end); Item (value, onSelect, danger, disabled, checked); Separator | popover min 200 rộng, bo 8, bóng overlay; item 32px; đóng menu trả focus về trigger |
| LarkTabs | defaultValue/value/onValueChange; List (showIndicator mặc định true — tự chèn vạch trượt), Tab, Panel, Indicator (variant underline/pill) | tab cao 34, vạch trượt 3px, tab chọn không nền |
| LarkTooltip (+ LarkTooltipProvider) | content (ReactNode), side, delayDuration, triggerClassName; Provider delayDuration (=300), skipDelayDuration | delay 300ms, lệch 6px, overlay 300ms |
| LarkSelect | options, value/onValueChange, onOpenChange, placeholder, disabled, isInvalid, ariaLabel, containerClassName, onClear, required | trigger 32px bo 6, item 32px, dropdown bo 6; có onClear thì hover/focus hiện nút xoá như picker ngày |
| LarkDatePicker | label, value (YYYY-MM-DD), onChange, onClear, error, isInvalid, required, placeholder, disabled, describedBy, containerClassName | trigger 32px như Select, panel bo 6, lịch 42 ô tuần bắt đầu T2, ô ngày 32px |
| LarkMonthPicker (+ formatMonthValue) | label, value (YYYY-MM), onChange, onClear, error, isInvalid, required, placeholder, disabled, describedBy, containerClassName | trigger 32px, lưới tháng 3 cột, bảng năm 12 ô |
| LarkCheckbox | label, checked (true/false/'indeterminate'), onCheckedChange, controlClassName | khung 16px bo 4, dấu 12px trắng |
| LarkRadioGroup | label, options hoặc children (LarkRadioGroup.Item), value/onValueChange | khung 16px tròn, chấm viền brand 5px |
| LarkSwitch | label, checked/onCheckedChange, controlClassName | rãnh 36×20, núm 16px |
| LarkDialog | open/onOpenChange; Content (title, description), Footer | bo 8, đệm 20/24, tối đa max-w-md |
| LarkDrawer | open/onOpenChange; Content (title, description, position right/bottom, size sm/md/lg, actions, footer, showClose, closeLabel, overlay, bodyClassName) | phải 350/480/680 cao hết viewport; đáy rộng hết, trần 85dvh, bo trên 8; mask 55%; header 56, đệm 24 |
| LarkAlertDialog | title, confirmLabel, cancelLabel (=Hủy), onConfirm, isPending | như dialog; pending chặn Escape, disable nút huỷ |
| LarkToastProvider + useLarkToast | toast({ title, description, variant }); Provider duration (=4000), toastClassName, viewportClassName | nền n900, bo 8 / pill mobile, 4s, quét phải để tắt |
| LarkSpinner / LarkEmptyState | Spinner size (sm/md), label bắt buộc; EmptyState children (một câu trong thẻ <p>), illustration? (mặc định ảnh LILY) | spinner 16/24px; empty căn giữa, mọi chữ một tông n400 |
| LarkEmptyIllustration | size (sm/md/lg), label | ảnh giữ chỗ LILY LOOK & CARE tông n400, rộng 64/88/128px, tỉ lệ 140:81 |
| LarkTable (+ Wrapper/Footer/TruncatedCell) | columns, rows, rowKey, isLoading, emptyState, onRowClick, rowClassName, ref; cột có width, align (left/right/center), pin: 'right' \| 'left', rowHeader, className, headerClassName; bảng có groupedHeader (nhóm rowSpan: 2 nhận width), density (default/compact), tableLayout (auto/fixed), caption, captionHidden, sectionRow, rowCells (hàng tự khai colSpan/rowSpan), sectionRowClassName, showHeader | head 48, body 47, divider n200/n300, ghim z-10/z-20 |
| LarkPagination (+ pageWindow) | page, total, pageSize, onPageChange, pageSizes (=[50,100,200]), onPageSizeChange, isLoading | nút 28, active 28×28 bo 6, footer py-2 px-3 |
| LarkTag | color (blue/gray/success/danger/warning), size (md/sm) | 24 md / 16 sm, bo 4 |
| LarkStatus | label (bắt buộc), tone (success/warning/danger/info/neutral), variant (tag: chữ có nền / icon: chỉ icon không nền), size (md/sm), icon | hai dạng cùng một trạng thái; dạng icon role="img" + aria-label, icon 14/12 |
| LarkBadge | count, max (=99), dot | cao 14, chấm 8px |
| LarkAvatar (+ getInitials) | name, src, team, size (sm/md/lg) | 20/32/36px, tròn |
| LarkNotice | variant (info/success/warning/error), title, icon, action, onClose | thông báo thường trực, icon 16, đệm 12/16; role là alert (warning/error) hoặc status (info/success), không tự tắt |
| LarkBreadcrumb | items (label, href), separator, label (aria-label) | gap cấp 4px, icon phân cách 16px, cấp cuối aria-current="page" |
| LarkPageHeader | title, description, breadcrumb, actions, titleId | tiêu đề <h1> 16px/24, cách breadcrumb 4px |
| LarkProgress | value, label, variant (linear/circular), isIndeterminate, showValue, size, status (normal/success/error) | thanh 4px bo 2px, nhãn cách 12px; vòng 16px, nhãn cách 8px, chữ 12px; không xác định thì bỏ aria-valuenow |
| LarkMotionProvider | disabled (mặc định false) | bọc root; disabled tắt toàn bộ animation/transition của thư viện trong cây con |
| LarkCard | title, description, actions, footer, isHoverable | viền N300 1px, bo 8, đệm 12, cách tiêu đề–nội dung 12; hover mượn bóng overlay |
| LarkStatCard | label, value, unit, hint, note, delta, deltaLabel, sparkline, isCountUp, isLoading | số lớn 24px, delta 12px + icon 14px; đếm tăng 900ms; value={null} hiện "Chưa xác định" |
| LarkKeyValueList | items (label, value, emphasis, missing), orientation (inline/stacked), size (sm/md), divided | nhãn 12px N600, giá trị 12px; md 14px; phân cách 1px N200 |
| LarkChecklist | items (label, value, status done/pending/missing/error), title, label, size (sm/md) | icon 14px + chữ trạng thái sr-only; nhãn 12/14px |
| LarkChartSparkline | values, width, height, tone (neutral/positive/negative), label | mặc định 120×32, nét 2, không trục |
| LarkChartBar | data (label, value, color), orientation, height, maxValue, showValues, caption, label | viewBox 640×160, lưới 1px, nhãn 12px, bo đầu cột 4px, cao tối thiểu 2px |
| LarkChartLine / LarkChartArea | labels, series (name, values: (number \| null)[] — null = kỳ không có số, đường cắt đoạn; color), height, area, showLegend, showDots, caption, label | 4 mốc lưới, đường nét 2, chú giải dưới chart; giới hạn mềm 5 series (line) / 3 (area) |
| LarkChartDonut | segments (label, value, color), size, thickness, centerValue, centerLabel, showLegend, caption, label | 160×160 mặc định, vành dày 24, số giữa 20px; giới hạn mềm 9 đoạn |
| LarkSteps | items (title, description, icon, status), current, orientation (horizontal/vertical), label | nút 24px, đường nối 1px, cách bước 24px; ngang từ md, dưới md xếp dọc |
| LarkTimeline | items (title, time, description, icon, status), label | chấm 8px, cột thời gian 96px ở md, cách item 16px |
Prop HTML
Mọi component kế thừa prop HTML của phần tử gốc, nên gắn được logic React thoải mái:
<LarkSwitch
label="Nhắc lịch hẹn"
name="notify"
checked={on}
onCheckedChange={setOn}
onBlur={trackBlur}
data-testid="notify-switch"
aria-describedby="notify-hint"
/>
<LarkTable
ref={tableRef}
columns={cols}
rows={rows}
rowKey={(r) => r.id}
onRowClick={(row, i) => open(row)}
rowClassName={(row) => (row.urgent ? 'bg-lark-danger/5' : undefined)}
/>name/form/value của LarkSwitch và LarkCheckbox vào hidden input của
Radix nên FormData trong <form> thu được giá trị thật.
Chuyển động
Animation UI nằm trong src/styles/motion.css và lấy số từ token
--motion-* trong src/styles/lark-tokens.css; component không tự viết
duration/easing. File CSS của package tự import motion.css, không cần khai
thêm gì trong app. Chuyển động dữ liệu (số đếm, thanh tiến độ, biểu đồ)
không dùng recipe CSS: tất cả đi qua hook useReveal trong
src/lib/use-count-up.ts, cùng một thời lượng --motion-duration-reveal, nên
tải lại trang là toàn bộ chạy lại cùng lúc.
| Recipe | Vào | Ra | Dùng cho |
| --- | --- | --- | --- |
| motion-fade | 120ms | — | lớp phủ, notice, tag hiện ra |
| motion-popover | 180ms, scale .96 | 100ms | select, dropdown, picker |
| motion-dialog | 240ms, dịch 8px | 100ms | dialog, alert-dialog |
| motion-toast | 240ms | 100ms | toast |
| motion-tooltip | 120ms | 100ms | tooltip |
| motion-press | active:scale(.98) | — | button, icon button |
| motion-indicator | transform + translate 240ms | — | tab indicator, switch, radio |
| motion-list-item | 180ms + trễ 20ms/dòng | — | danh sách ≤ 12 dòng |
| motion-expand | grid-template-rows 240ms | 240ms | accordion, chi tiết |
| motion-skeleton | pulse 1,2s | — | skeleton |
| motion-spin | 800ms/vòng, linear | — | spinner |
| motion-row-flash | giữ 360ms rồi mờ dần 840ms | — | hàng bảng vừa lưu |
Quy ước: chỉ animate opacity/transform; đường ra luôn ngắn hơn đường vào
(100ms); không overshoot; prefers-reduced-motion: reduce tắt hết (riêng
motion-row-flash giữ nền tô sáng, motion-spin đổi sang nhịp mờ dần).
Tham chiếu đầy đủ: specs/motion.md.
Mặc định bật, tắt được. Mọi component có chuyển động tự chạy animation,
app không phải thêm class; LarkTabs.List tự chèn vạch trượt (không cần đặt
LarkTabs.Indicator). Tắt cả thư viện bằng provider ở root:
<LarkMotionProvider disabled>
<App />
</LarkMotionProvider>Provider đặt data-lark-motion="off" (wrapper display: contents, không phá
layout) và useReveal trả thẳng giá trị thật, nên số đếm/thanh/biểu đồ hiện
đúng ngay. Tắt riêng vạch trượt của tab: showIndicator={false}.
Nguồn số đo
Số đo lấy từ style guide chính thức của Lark (MCP lark-design:
design-language/*, components/*) — đây là nguồn quyết định. File Figma
(~/Sites/lily-projects/lark-design-guide/figma/) chỉ để đối chiếu; lệch thì
theo style guide và ghi rõ chỗ lệch trong spec. Chi tiết:
docs/specs/2026-11-05-lark-ui.md và specs/*.md.
Ví dụ
import { LarkButton, LarkTextField } from 'lily-lark-ui'
export function LoginForm() {
return (
<form>
<LarkTextField label="Số điện thoại" placeholder="Nhập số điện thoại" isRequired />
<LarkButton type="submit">Đăng nhập</LarkButton>
</form>
)
}import { LarkTable, LarkTableWrapper, LarkPagination } from 'lily-lark-ui'
export function RecordsTable({ columns, rows, page, total, pageSize, onPageChange }) {
return (
<LarkTableWrapper>
<LarkTable columns={columns} rows={rows} rowKey={(r) => r.id} />
<LarkTable.Footer>
<LarkPagination page={page} total={total} pageSize={pageSize} onPageChange={onPageChange} />
</LarkTable.Footer>
</LarkTableWrapper>
)
}Xem component
Mở lark-ui-components.html ở root repo bằng trình duyệt — một file tự chứa,
có nút chuyển sáng/tối, responsive 320–1600px.
Dùng từ agent: MCP và skill
Repo có sẵn MCP server (mcp/server.mjs, stdio, không dependency ngoài) đọc
trực tiếp specs/ và src/styles/lark-tokens.css:
| Tool | Trả về |
|---|---|
| lui_props {name} | Bảng prop của component (nhận LarkButton, LarkChartLine, LarkDrawer.Content, switch, specs/table.md) |
| lui_get {name} | Toàn bộ trang spec: số đo, class mapping, ví dụ |
| lui_tokens | Bảng token code từ src/styles/lark-tokens.css (màu, bo góc, chuyển động, bóng đổ) |
| lui_search {query} | Tìm theo từ khoá, không dấu tiếng Việt vẫn khớp |
| lui_list | Danh sách trang spec kèm tiêu đề |
"mcp": { "lark-ui": { "type": "local", "command": ["node", "/path/to/lark-ui/mcp/server.mjs"] } }Skill lark-ui nằm trong skill/lark-ui/SKILL.md, phát hành kèm package;
scripts/install.sh symlink vào thư mục skill của agent khi cài vào app.
Phát triển
Xem AGENTS.md trước khi sửa code.
pnpm test # 431 test (vitest + jsdom)
pnpm test:visual # 9 test layout thật (Playwright)
pnpm check:lark # cấm hex, dark:, emoji-icon trong src/
pnpm showcase:build # build lại lark-ui-components.htmlGit hook trước khi push
Repo có .githooks/pre-push: mỗi lần git push (CLI, IDE hay agent) đều chạy
pnpm build, pnpm check:lark, pnpm test trước khi đẩy. Hook không tự sửa
file: nếu lark-ui-components.html lỗi thời, test sẽ FAIL và bạn chạy
pnpm showcase:build rồi commit lại.
- Bật:
pnpm installtự chạyprepare→node scripts/install-hooks.mjs→git config core.hooksPath .githooks. Dev mới chỉ cần setup dự án một lần. - Kiểm tra:
git config --get core.hooksPathphải in.githooks. - Bật tay khi cần:
node scripts/install-hooks.mjs(clone cũ đã cài từ trước, hoặcpnpm installbáo "Already up to date" — pnpm bỏ quapreparekhi không cài gì;pnpm install --forcecũng chạy lạiprepare). - Bỏ qua một lần khi thật cần:
git push --no-verify. - Hook là kiểm tra local, không thay CI: người khác có thể
--no-verify, vàcore.hooksPathkhông đi theo repo. Muốn chặn ở remote thì cần job CI.
Lưu ý: khi đang sửa giao diện, dùng pnpm showcase:dev (port 5210, HMR) để xem
ngay; file lark-ui-components.html chỉ đổi sau pnpm showcase:build, và tab
đang mở phải Ctrl+Shift+R mới thấy bản mới.
