lily-lark-ui 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/INSTALL.md +14 -13
  3. package/README.md +115 -39
  4. package/dist/components/alert-dialog.d.ts +2 -1
  5. package/dist/components/alert-dialog.js +7 -2
  6. package/dist/components/checkbox.d.ts +4 -2
  7. package/dist/components/checkbox.js +6 -2
  8. package/dist/components/date-picker.d.ts +49 -0
  9. package/dist/components/date-picker.js +416 -0
  10. package/dist/components/dialog.d.ts +7 -13
  11. package/dist/components/dialog.js +29 -6
  12. package/dist/components/dropdown-menu.d.ts +16 -8
  13. package/dist/components/dropdown-menu.js +5 -5
  14. package/dist/components/empty-state.d.ts +2 -3
  15. package/dist/components/empty-state.js +2 -2
  16. package/dist/components/pagination.d.ts +1 -1
  17. package/dist/components/pagination.js +2 -2
  18. package/dist/components/radio-group.d.ts +7 -3
  19. package/dist/components/radio-group.js +6 -4
  20. package/dist/components/select.d.ts +5 -1
  21. package/dist/components/select.js +3 -2
  22. package/dist/components/spinner.d.ts +3 -2
  23. package/dist/components/spinner.js +2 -2
  24. package/dist/components/switch.d.ts +3 -1
  25. package/dist/components/switch.js +4 -3
  26. package/dist/components/table.d.ts +49 -14
  27. package/dist/components/table.js +78 -11
  28. package/dist/components/tabs.d.ts +14 -18
  29. package/dist/components/tabs.js +6 -6
  30. package/dist/components/toast.d.ts +7 -3
  31. package/dist/components/toast.js +4 -4
  32. package/dist/components/tooltip.d.ts +6 -6
  33. package/dist/components/tooltip.js +3 -3
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.js +1 -0
  36. package/lark-ui-components.html +2 -2
  37. package/mcp/server.mjs +68 -2
  38. package/package.json +1 -1
  39. package/registry.json +1 -1
  40. package/scripts/gen-spec-props.mjs +266 -0
  41. package/skill/lark-ui/SKILL.md +53 -8
  42. package/specs/alert-dialog.md +22 -0
  43. package/specs/buttons.md +19 -5
  44. package/specs/checkbox.md +19 -1
  45. package/specs/date-picker.md +108 -0
  46. package/specs/dialog.md +35 -3
  47. package/specs/dropdown-menu.md +30 -0
  48. package/specs/feedback-states.md +26 -0
  49. package/specs/install.md +17 -2
  50. package/specs/pagination.md +20 -0
  51. package/specs/radio-group.md +32 -1
  52. package/specs/select.md +36 -3
  53. package/specs/switch.md +19 -1
  54. package/specs/table.md +65 -1
  55. package/specs/tabs.md +39 -2
  56. package/specs/tag-badge-avatar.md +36 -1
  57. package/specs/text-area.md +15 -0
  58. package/specs/text-field.md +17 -0
  59. package/specs/toast.md +37 -2
  60. package/specs/tooltip.md +29 -3
  61. package/src/components/alert-dialog.tsx +19 -2
  62. package/src/components/checkbox.tsx +37 -20
  63. package/src/components/date-picker.tsx +933 -0
  64. package/src/components/dialog.tsx +42 -23
  65. package/src/components/dropdown-menu.tsx +31 -17
  66. package/src/components/empty-state.tsx +7 -4
  67. package/src/components/pagination.tsx +4 -2
  68. package/src/components/radio-group.tsx +37 -15
  69. package/src/components/select.tsx +32 -6
  70. package/src/components/spinner.tsx +6 -2
  71. package/src/components/switch.tsx +18 -5
  72. package/src/components/table.tsx +283 -56
  73. package/src/components/tabs.tsx +24 -43
  74. package/src/components/toast.tsx +26 -8
  75. package/src/components/tooltip.tsx +21 -13
  76. package/src/styles/lark-tokens.css +8 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,142 @@ Mọi thay đổi đáng chú ý của `lily-lark-ui` được ghi ở đây. Đ
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] — 2026-11-06
10
+
11
+ ### Added — bộ chọn ngày và bộ chọn kỳ
12
+
13
+ - `LarkDatePicker` (`YYYY-MM-DD`) và `LarkMonthPicker` (`YYYY-MM`) trong
14
+ `src/components/date-picker.tsx`: **controlled theo chuỗi**, không cầm engine
15
+ ngày. Trigger 32px như `LarkSelect`, panel bo 6 viền N200 bóng overlay, lật
16
+ lên khi hết chỗ phía dưới.
17
+ - Xoá được: hover/focus trigger khi panel đóng hiện nút X, trả `''` qua
18
+ `onChange` và gọi `onClear`; `required` ẩn nút xoá. Chuỗi rỗng hoặc sai định
19
+ dạng hiển thị placeholder như chưa chọn.
20
+ - Lịch 42 ô tuần bắt đầu thứ hai (`T2…CN`), ngày ngoài tháng làm mờ, footer
21
+ "Hôm nay"; `LarkMonthPicker` có lưới 12 tháng và bảng chọn năm 12 ô.
22
+ - Rollover năm: 12→1 và 1→12 đổi cả năm đang xem; chọn năm tự chuyển về bảng
23
+ tháng; `value` đổi từ ngoài thì view bám theo giá trị mới.
24
+ - Bàn phím: trigger `role=combobox` `aria-haspopup=dialog`, Escape đóng; lưới
25
+ arrow/Home/End di chuyển focus theo hàng; chọn hoặc gõ tay xong trả focus về
26
+ trigger. `LarkDatePicker` nhập tay `YYYY/M/D` hoặc `D/M/YYYY` (dấu `/`, `-`,
27
+ `.`), Enter hoặc blur là commit, sai thì báo lỗi tại chỗ.
28
+
29
+ ### Added — `LarkTable`: header hai tầng, ghim trái, bảng tổng hợp
30
+
31
+ - `groupedHeader` (`LarkGroupedHeader[]`): hàng 1 là ô nhóm `scope=colgroup`
32
+ với `colSpan`/`rowSpan: 2`, hàng 2 là cột con; nhóm một cột con vẫn giữ ô
33
+ nhóm đúng hàng.
34
+ - `pin: 'left'`: cột ghim trái song song `pin: 'right'` đã có (`sticky left-0`,
35
+ `border-e` phân cách, `z-10` cho body và `z-20` cho head).
36
+ - `rowHeader` trên cột: ô dữ liệu render `th scope=row` thay vì `td`.
37
+ - `caption` + `captionHidden`: caption hiện trên bảng hoặc `sr-only`, đặt
38
+ accessible name cho `<table>`.
39
+ - `sectionRow(row, index)`: hàng phân mục trong `tbody`, một ô
40
+ `th scope=colgroup` phủ hết chiều rộng; `sectionRowClassName` đổi class.
41
+ - `showHeader={false}`: bỏ `thead` cho bảng tổng hợp chỉ có `tbody`.
42
+
43
+ ### Added — test
44
+
45
+ - `tests/date-picker.test.tsx`: 16 test cho hai picker (controlled, xoá,
46
+ `required`, rollover năm, ngày ngoài tháng, 29/2, gõ tay, focus sau khi chọn).
47
+ - `tests/table.test.tsx`: 6 test mới cho `groupedHeader` (colSpan/rowSpan,
48
+ scope), `caption`/`captionHidden`, `rowHeader`, `pin: 'left'`,
49
+ `sectionRow` + `showHeader={false}`.
50
+
51
+ ### Added — bảng prop trong spec sinh tự động
52
+
53
+ - `scripts/gen-spec-props.mjs` đọc interface thật của 21 file component rồi sinh
54
+ khối `## Props` (prop riêng, kiểu, bắt buộc, prop HTML kế thừa) cho 18 trang
55
+ `specs/`. `--check` chạy trong `tests/spec-props.test.ts`.
56
+ - MCP `lui_props`: trả bảng prop của một component theo tên (`switch`,
57
+ `LarkSwitch`, `tag`, `spinner`…). `serverInfo.version` đọc từ `package.json`.
58
+
59
+ ### Changed — tài liệu, registry, README, skill
60
+
61
+ - `specs/date-picker.md` (mới) và `specs/table.md` (header hai tầng, ghim trái,
62
+ section row); `SPEC_MAP` thêm `date-picker`.
63
+ - `registry.json`: thêm `lark-date-picker` và `lark-month-picker` (22 item).
64
+ - `README.md`: thêm hai dòng picker, cập nhật dòng `LarkTable` với prop mới,
65
+ số component 22.
66
+ - `skill/lark-ui/SKILL.md`: bảng chọn component thêm hai picker kèm ví dụ
67
+ import; `LarkTable` cập nhật prop mới.
68
+ - `docs/STATUS.md`: số component 22, 21 trang `specs/`, 22 item registry.
69
+ - Showcase: khối "Chọn ngày · Chọn kỳ" (controlled, nút xoá, case `required`,
70
+ disabled) và bảng tổng hợp dùng `groupedHeader` + `caption` + `sectionRow` +
71
+ `rowHeader` + cột ghim trái; `lark-ui-components.html` build lại.
72
+
73
+ ### Changed — đồng bộ tài liệu và skill theo trạng thái 0.3.0
74
+
75
+ - `specs/`: sửa class đã lỗi thời (button viền N300, `text-brand-link` thay
76
+ `brand-secondary`, `focus-visible:focus-ring` thay `ring-2`, select
77
+ placeholder `n600`, tag blue `accent-soft`).
78
+ - `README.md`: bảng component theo prop riêng, mục "Prop HTML", mục phát hành,
79
+ mục xem demo.
80
+ - `INSTALL.md`: bỏ câu "chưa publish", thêm `--version`, ghim bản `0.3.0`.
81
+ - `AGENTS.md`: quy tắc kế thừa prop primitive, quy tắc spec sinh tự động,
82
+ MCP 5 tool, mock `ResizeObserver`, quy trình phát hành.
83
+ - `skill/lark-ui/SKILL.md`: mục prop HTML + bảng prop phụ (`controlClassName`,
84
+ `containerClassName`, `triggerClassName`, `toastClassName`), cập nhật
85
+ `LarkTable`/`LarkRadioGroup`, nhắc `lui_props`.
86
+
87
+ ### Fixed — nền tối cố định, autofocus, khoảng cách
88
+
89
+ - Toast và tooltip giữ nền tối `#1F2329` ở cả hai theme (`--toast-bg`,
90
+ `--tooltip-bg` giữ nguyên trong khối dark; trước đây dùng `lark-n900` nên dark
91
+ mode đảo thành xám nhạt chữ mờ). Mũi tên tooltip ăn theo cùng token.
92
+ - `LarkDialog.Footer` thêm `mt-3` (=12px, cùng nhịp `gap=12` Figma và footer
93
+ AlertDialog): body không còn dính sát viền divider.
94
+ - Mở `LarkDialog` không còn cướp focus vào ô nhập đầu: chặn autofocus mặc
95
+ định của Radix, giữ focus trên khung Content (đã `outline-none`).
96
+ - `LarkDatePicker`/`LarkMonthPicker`: bỏ autofocus khi mở panel (ô nhập tay và
97
+ ô tháng/năm đã chọn); panel lịch trần `max-w-[280px]` căn giữa theo trigger
98
+ (ô ngày gần vuông, ô nhập không còn dãn full form rộng); khe nhãn thứ–lưới
99
+ ngày `mt-1` → `mt-2`.
100
+ - Showcase `TableBlock`: nút Xoá demo nay xoá được dòng (state `items`), cờ
101
+ `LarkSwitch` "Hỏi trước khi xoá", `LarkAlertDialog` xác nhận + toast
102
+ "Đã xoá phiếu".
103
+ - `LarkDatePicker`/`LarkMonthPicker`: Escape trong panel đóng và trả focus về
104
+ trigger (chặn lan truyền để dialog cha không đóng theo); rời ô nhập chỉ
105
+ kiểm hợp lệ, không commit/đóng (bấm ngày khác sau khi gõ vẫn chọn đúng);
106
+ nút xoá hiện khi focus còn trong cụm trigger nên Tab tới được; `className`
107
+ gắn lên wrapper ngoài; xoá prop `triggerProps` và tuỳ chọn cột `MonthGrid`
108
+ không ai dùng.
109
+ - `LarkDialog`: merge ref giữ cleanup của caller (unmount gọi cleanup).
110
+ - `LarkTable`: `thead sticky top-0`, bỏ `sticky top-0` từng `th` nên header
111
+ hai tầng dính theo khối, không đè nhau khi cuộn.
112
+ - Token: bỏ `--toast-bg`/`--tooltip-bg` trùng trong hai khối dark (kế thừa
113
+ từ `:root`, giá trị giữ nguyên `#1F2329` ở cả hai theme).
114
+
115
+ ## [0.3.0] — 2026-11-06
116
+
117
+ ### Added — prop HTML trên toàn bộ component
118
+
119
+ 12 component từng chỉ nhận vài prop (Select, Switch, Checkbox, RadioGroup,
120
+ Pagination, Tabs, Dialog, AlertDialog, DropdownMenu, Tooltip, Spinner,
121
+ EmptyState, Table) nay nhận đủ prop HTML của phần tử gốc: `onBlur`, `onFocus`,
122
+ `onKeyDown`, `name`, `form`, `value`, `data-*`, `aria-*`, `style`, `id`, `ref`.
123
+
124
+ - Interface kế thừa `ComponentPropsWithoutRef<typeof Primitive>` /
125
+ `HTMLAttributes<...>` thay vì khai báo tay vài prop.
126
+ - Thêm prop phụ để không phá API cũ: `controlClassName` (Switch, Checkbox,
127
+ Radio.Item), `containerClassName` (Select), `triggerClassName` (Tooltip),
128
+ `toastClassName`/`viewportClassName` (ToastProvider).
129
+ - `LarkTable` thêm `onRowClick(row, index)`, `rowClassName(row, index)`,
130
+ `ref` tới `<table>`; `LarkColumn` thêm `className`/`headerClassName`.
131
+ - `LarkRadioGroup` nhận `children` (dùng `LarkRadioGroup.Item` tự bố trí) song
132
+ song với `options`.
133
+ - `LarkTooltip.content` nhận `ReactNode` (trước là string).
134
+
135
+ ### Fixed
136
+
137
+ - `LarkEmptyState` và `LarkSpinner` merge `className` qua `cn` nên class riêng
138
+ override được class kích thước (trước ghi đè mất `size-4`/`size-6`).
139
+ - `AlertDialog.onEscapeKeyDown` do người dùng truyền không còn bị bỏ qua khi
140
+ `isPending` (giờ chặn trước rồi gọi handler của người dùng).
141
+ - `tests/prop-passthrough.test.tsx` (15 test) + fixture kiểu
142
+ `tests/fixtures/types-props.tsx` chạy `tsc` để chặn hồi quy.
143
+ - `vitest.setup.ts` mock `ResizeObserver` cho Radix.
144
+
9
145
  ## [0.2.0] — 2026-11-05
10
146
 
11
147
  ### Changed — đổi tên package sang `lily-lark-ui`
package/INSTALL.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # Cài đặt lily-lark-ui
2
2
 
3
- ## Cài từ npm (sau khi package được publish)
3
+ ## Cài từ npm
4
4
 
5
5
  ```bash
6
6
  npx lily-lark-ui init
7
7
  ```
8
8
 
9
- Nếu `npx` báo không tìm thấy package, nghĩa là chưa publish lên registry — xem
10
- mục "Phát hành" bên dưới, hoặc cài từ tarball.
9
+ Package đã publish: `lily-lark-ui@0.3.0` (npm, không scope, `latest`). Nếu
10
+ `npx` báo không tìm thấy, kiểm `npm view lily-lark-ui version`; bản mới có thể
11
+ còn đang qua automated review của npm vài phút.
11
12
 
12
13
  ## Phát hành (dành cho maintainer)
13
14
 
@@ -71,8 +72,9 @@ npx lily-lark-ui init --repo ../app # cài vào app khác
71
72
  npx lily-lark-ui init --dry-run # xem kế hoạch, không ghi
72
73
  npx lily-lark-ui init --no-bootstrap # không cài Tailwind/sửa vite config
73
74
  npx lily-lark-ui init --components button,table
74
- npx lily-lark-ui check # chạy check-lark trên ./src
75
+ npx lily-lark-ui check [<dir>] # chạy check-lark (mặc định ./src)
75
76
  npx lily-lark-ui tokens # in bảng token
77
+ npx lily-lark-ui --version # in phiên bản
76
78
  npx lily-lark-ui --help
77
79
  ```
78
80
 
@@ -84,11 +86,11 @@ Yêu cầu: chạy trong repo đã có `package.json` + `react`.
84
86
  bash ~/Sites/lark-ui/scripts/install.sh --repo . --apply --bootstrap
85
87
  ```
86
88
 
87
- Nếu package chưa publish lên registry, trỏ tới tarball:
89
+ Nếu cần cài từ tarball (bản dev chưa publish, hoặc muốn ghim đúng file):
88
90
 
89
91
  ```bash
90
92
  cd ~/Sites/lark-ui && pnpm pack
91
- LARK_UI_TARBALL=~/Sites/lark-ui/lily-lark-ui-0.2.0.tgz \
93
+ LARK_UI_TARBALL=~/Sites/lark-ui/lily-lark-ui-0.4.0.tgz \
92
94
  bash ~/Sites/lark-ui/scripts/install.sh --repo . --apply --bootstrap
93
95
  ```
94
96
 
@@ -176,16 +178,15 @@ Tailwind v4 **không tự quét `node_modules`**, nên ngoài `@import` còn ph
176
178
  pnpm add lily-lark-ui
177
179
  ```
178
180
 
179
- Nếu package chưa publish lên registry, cài từ tarball:
181
+ Package dùng tên không scope, không cần token cho consumer. Import:
180
182
 
181
- ```bash
182
- # trong repo lark-ui
183
- pnpm pack # sinh lily-lark-ui-<version>.tgz
184
-
185
- # trong repo đích
186
- pnpm add ./lily-lark-ui-0.2.0.tgz
183
+ ```tsx
184
+ import { LarkButton } from 'lily-lark-ui'
187
185
  ```
188
186
 
187
+ Nếu cần ghim đúng file build (bản dev chưa publish, hoặc kiểm tra tarball), cài
188
+ bằng `pnpm add ./lily-lark-ui-0.4.0.tgz` sau khi `pnpm pack` trong repo này.
189
+
189
190
  Dùng component:
190
191
 
191
192
  ```tsx
package/README.md CHANGED
@@ -1,7 +1,15 @@
1
1
  # lily-lark-ui
2
2
 
3
- Thu vien component React theo phong cach Lark (Universe Design), dung kieu shadcn:
4
- copy-paste component Tailwind v4 + Radix headless. Hien co 20 component.
3
+ Thư viện component React theo phong cách Lark (Universe Design) cho React 19 +
4
+ Tailwind v4. 22 component, nền Radix headless, phát hành trên npm.
5
+
6
+ ```bash
7
+ npx lily-lark-ui init # cài vào app hiện tại (tự cài Tailwind v4)
8
+ ```
9
+
10
+ Phiên bản hiện tại: `0.4.0`. Mọi component nhận đủ prop HTML của phần tử gốc
11
+ (`onBlur`, `onFocus`, `onKeyDown`, `name`, `form`, `value`, `data-*`, `aria-*`,
12
+ `style`, `id`, `ref`) — xem mục [Prop HTML](#prop-html).
5
13
 
6
14
  ## Cài đặt
7
15
 
@@ -44,61 +52,117 @@ lớn hơn 0.
44
52
 
45
53
  ## Cai vao repo moi
46
54
 
47
- Mot lenh (6 buoc + kiem Tailwind v4, copy --components neu co, check-lark report):
55
+ Mot lenh (6 buoc + kiem Tailwind v4, copy `--components` neu co, check-lark report):
56
+
57
+ ```bash
58
+ npx lily-lark-ui init # app hien tai
59
+ npx lily-lark-ui init --repo ../app # app khac
60
+ npx lily-lark-ui init --dry-run # chi in ke hoach
61
+ npx lily-lark-ui init --no-bootstrap # khong dung Tailwind/vite config
62
+ npx lily-lark-ui init --components button,table
63
+ ```
64
+
65
+ Hoac goi thang script trong source checkout:
48
66
 
49
67
  ```bash
50
68
  scripts/install.sh --repo <path> --apply [--components button,text-field]
51
69
  ```
52
70
 
53
71
  - Khong truyen `--apply` thi dry-run: chi in ke hoach, khong sua file.
54
- - `--skip-install`: bo qua buoc `pnpm add` (dung cho test offline).
72
+ - `--skip-install`: bo qua buoc cai package (dung cho test offline).
55
73
  - `--components <list>`: chi copy component can (ten cach nhau boi dau phay,
56
74
  vd `button,text-field`); kem `src/lib/utils.ts` + `src/styles/lark-tokens.css`
57
75
  neu chua co. Ten sai thi bao loi + liet ke ten hop le.
58
76
  - Idempotent: chay lai khong them trung import/block MCP.
59
77
 
78
+ ## Phát hành (maintainer)
79
+
80
+ ```bash
81
+ pnpm release:dry # build + test + check-lark + publint + pack + import(), khong publish
82
+ pnpm release # publish thật, rồi cài lại từ registry để xác nhận
83
+ ```
84
+
85
+ Package là `lily-lark-ui` (không scope). Publish cần token npm có quyền ghi
86
+ package; nếu tài khoản bật 2FA thì `npm publish` trả `EOTP`, cần Automation
87
+ token. Bản mới đi qua automated review của npm (vài phút) trước khi thành
88
+ `latest`.
89
+
60
90
  ## Component
61
91
 
62
- | Component | Import | Props chinh | So do |
63
- |---|---|---|---|
64
- | LarkButton | `import { LarkButton } from 'lily-lark-ui'` | `variant` (primary/secondary/danger/ghost/text), `size` (xs/sm/md/lg), `isIconOnly`, `fullWidth`, `isPending`, `asChild` | 32px mac dinh (h-8), 40px nut chinh form (h-10), 28/24 nho; mobile 36/48px |
65
- | LarkTextField (+inputClass) | `import { LarkTextField } from 'lily-lark-ui'` | `label`, `error`, `description`, `isRequired`, `isInvalid` | Cao 42 mobile / 36 PC (`md:min-h-9`), chu 16 mobile / 14 PC |
66
- | LarkDropdownMenu | `import { LarkDropdownMenu } from 'lily-lark-ui'` | `trigger`, `align` (start/center/end); `Item` (`value`, `onSelect?`, `danger?`, `disabled?`, `checked?`) | Popover toi thieu 200 rong, bo goc 8, bong overlay; item 32px, dropdown vien #DEE0E3 qua token; dong menu (Escape/chon/click ngoai) tra focus ve trigger, click lai dong menu |
67
- | LarkTabs | `import { LarkTabs } from 'lily-lark-ui'` | `defaultValue`/`value`/`onValueChange`, `LarkTabs.List/Tab/Panel` | Tab cao 34, gach duoi tab kich hoat |
68
- | LarkTooltip (+LarkTooltipProvider) | `import { LarkTooltip, LarkTooltipProvider } from 'lily-lark-ui'` | `content`, `side` (top/right/bottom/left); Provider `delayDuration` (=300), `skipDelayDuration?` | delay 300ms, lech 6, overlay 300ms ease-out; render doc lap van mo (fallback), trong Provider dung chung |
69
- | LarkTextArea | `import { LarkTextArea } from 'lily-lark-ui'` | `label`, `error`, `isRequired` (ke thua moi prop `textarea`) | toi thieu 72px (~3 dong), chu 16 mobile / 14 PC |
70
- | LarkSelect | `import { LarkSelect } from 'lily-lark-ui'` | `options`, `value`/`onValueChange`, `placeholder`, `disabled`, `isInvalid` | trigger 32px (`h-8`) bo 6 (`rounded-lg`), item 32px, dropdown bo 6 vien `#DEE0E3` qua token |
71
- | LarkCheckbox | `import { LarkCheckbox } from 'lily-lark-ui'` | `label`, `checked` (true/false/'indeterminate'), `onCheckedChange` | khung 16px (`size-4`) bo 4, dau `size-3` trang |
72
- | LarkRadioGroup | `import { LarkRadioGroup } from 'lily-lark-ui'` | `label?`, `options`, `value`/`onValueChange` | khung 16px tron, cham chon vien brand 5px |
73
- | LarkSwitch | `import { LarkSwitch } from 'lily-lark-ui'` | `label?`, `checked`/`onCheckedChange` | ranh 36x20 (`h-5 w-9`), num 16px trang |
74
- | LarkDialog | `import { LarkDialog } from 'lily-lark-ui'` | `open`/`onOpenChange`, `Content` (`title`, `description?`), `Footer` | bo 8 (`rounded-xl`), dem 20 mobile / 24 PC, toi da `max-w-md` |
75
- | LarkAlertDialog | `import { LarkAlertDialog } from 'lily-lark-ui'` | `title`, `confirmLabel`, `cancelLabel` (=`Huy`), `onConfirm`, `isPending` | khung nhu dialog; pending chan Escape + disable nut huy |
76
- | LarkToast (+Provider/useLarkToast) | `import { LarkToastProvider, useLarkToast } from 'lily-lark-ui'` | `toast({ title, description?, variant? })` (default/success/danger) | nen `n900`, bo 8 / pill mobile, hien 4s, quet phai de tat |
77
- | LarkSpinner / LarkEmptyState | `import { LarkSpinner, LarkEmptyState } from 'lily-lark-ui'` | Spinner `size` (sm/md), `label` bat buoc; EmptyState `icon`, `title`, `description?`, `action?` | spinner 16/24px xoay `n500`; empty can giua, icon 24px, dem doc 40px |
78
- | LarkTable (+Wrapper/Footer/TruncatedCell) | `import { LarkTable, LarkTableWrapper, LarkPagination } from 'lily-lark-ui'` | `columns`, `rows`, `rowKey`, `isLoading?`, `emptyState?`; cot `pin: 'right'`; caller tu boc `<LarkTable.Footer>` quanh `<LarkPagination>` | head 48 (`h-12`), body 47 (`h-[47px]`), divider `n200/n300`, ghim `z-10/z-20`, skeleton `animate-pulse` |
79
- | LarkPagination (+pageWindow) | `import { LarkPagination } from 'lily-lark-ui'` | `page`, `total`, `pageSize`, `onPageChange`, `pageSizes?` (mac dinh `[50, 100, 200]`), `onPageSizeChange?`, `isLoading?` | nut 28 (`size-7`), active 28x28 bo 6 (rounded-lg = --radius-lg 6px), footer `py-2 px-3`, pageWindow toi da 7 |
80
- | LarkTag | `import { LarkTag } from 'lily-lark-ui'` | `color` (blue/gray/success/danger/warning), `size` (md/sm) | cao 24 md (`h-6`) / 16 sm (`h-4`), bo 4 (`rounded-sm`) |
81
- | LarkBadge | `import { LarkBadge } from 'lily-lark-ui'` | `count?`, `max` (=`99`), `dot?` | cao 14 (`h-[14px]`), cham `size-2`, pill do `rounded-full` |
82
- | LarkAvatar (+getInitials) | `import { LarkAvatar } from 'lily-lark-ui'` | `name?`, `src?`, `team?`, `size` (sm/md/lg) | 20 sm (`size-5`) / 32 md (`size-8`) / 36 lg (`size-9`), team tron 36px (alias size lg) |
83
-
84
- ## Nguon so do
85
-
86
- Moi so do trong thu vien lay tu `~/Sites/lily-projects/lark-design-guide`:
87
- uu tien mau noi bo `patterns/`, sau do so do goc Figma `figma/`
88
- (buttons, list, navigation, action-menu, feedback).
89
- Chi tiet xem `docs/specs/2026-11-05-lark-ui.md`.
90
-
91
- ## Vi du
92
+ Tất cả component nhận đủ prop HTML của phần tử gốc; bảng dưới chỉ liệt kê prop
93
+ riêng của thư viện.
94
+
95
+ | Component | Props riêng | Số đo |
96
+ |---|---|---|
97
+ | `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 |
98
+ | `LarkTextField` (+ `inputClass`) | `label`, `error`, `description`, `isRequired`, `isInvalid` | 42px mobile / 36px PC (`md:min-h-9`), chữ 16/14 |
99
+ | `LarkTextArea` | `label`, `error`, `isRequired` | tối thiểu 72px (~3 dòng) |
100
+ | `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 |
101
+ | `LarkTabs` | `defaultValue`/`value`/`onValueChange`; `LarkTabs.List/Tab/Panel` | tab cao 34, gạch dưới 3px |
102
+ | `LarkTooltip` (+ `LarkTooltipProvider`) | `content` (`ReactNode`), `side`, `delayDuration`, `triggerClassName`; Provider `delayDuration` (=300), `skipDelayDuration` | delay 300ms, lệch 6px, overlay 300ms |
103
+ | `LarkSelect` | `options`, `value`/`onValueChange`, `onOpenChange`, `placeholder`, `disabled`, `isInvalid`, `ariaLabel`, `containerClassName` | trigger 32px bo 6, item 32px, dropdown bo 6 |
104
+ | `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 |
105
+ | `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 ô |
106
+ | `LarkCheckbox` | `label`, `checked` (`true`/`false`/`'indeterminate'`), `onCheckedChange`, `controlClassName` | khung 16px bo 4, dấu 12px trắng |
107
+ | `LarkRadioGroup` | `label`, `options` **hoặc** `children` (`LarkRadioGroup.Item`), `value`/`onValueChange` | khung 16px tròn, chấm viền brand 5px |
108
+ | `LarkSwitch` | `label`, `checked`/`onCheckedChange`, `controlClassName` | rãnh 36×20, núm 16px |
109
+ | `LarkDialog` | `open`/`onOpenChange`; `Content` (`title`, `description`), `Footer` | bo 8, đệm 20/24, tối đa `max-w-md` |
110
+ | `LarkAlertDialog` | `title`, `confirmLabel`, `cancelLabel` (=`Hủy`), `onConfirm`, `isPending` | như dialog; pending chặn Escape, disable nút huỷ |
111
+ | `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 |
112
+ | `LarkSpinner` / `LarkEmptyState` | Spinner `size` (sm/md), `label` bắt buộc; EmptyState `icon`, `title`, `description`, `action` | spinner 16/24px; empty căn giữa, icon 24px |
113
+ | `LarkTable` (+ `Wrapper`/`Footer`/`TruncatedCell`) | `columns`, `rows`, `rowKey`, `isLoading`, `emptyState`, `onRowClick`, `rowClassName`, `ref`; cột có `pin: 'right' \| 'left'`, `rowHeader`, `className`, `headerClassName`; bảng có `groupedHeader`, `caption`, `captionHidden`, `sectionRow`, `sectionRowClassName`, `showHeader` | head 48, body 47, divider `n200/n300`, ghim `z-10/z-20` |
114
+ | `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` |
115
+ | `LarkTag` | `color` (blue/gray/success/danger/warning), `size` (md/sm) | 24 md / 16 sm, bo 4 |
116
+ | `LarkBadge` | `count`, `max` (=99), `dot` | cao 14, chấm 8px |
117
+ | `LarkAvatar` (+ `getInitials`) | `name`, `src`, `team`, `size` (sm/md/lg) | 20/32/36px, tròn |
118
+
119
+ ### Prop HTML
120
+
121
+ Mọi component kế thừa prop HTML của phần tử gốc, nên gắn được logic React
122
+ thoải mái:
123
+
124
+ ```tsx
125
+ <LarkSwitch
126
+ label="Nhắc lịch hẹn"
127
+ name="notify"
128
+ checked={on}
129
+ onCheckedChange={setOn}
130
+ onBlur={trackBlur}
131
+ data-testid="notify-switch"
132
+ aria-describedby="notify-hint"
133
+ />
134
+
135
+ <LarkTable
136
+ ref={tableRef}
137
+ columns={cols}
138
+ rows={rows}
139
+ rowKey={(r) => r.id}
140
+ onRowClick={(row, i) => open(row)}
141
+ rowClassName={(row) => (row.urgent ? 'bg-lark-danger/5' : undefined)}
142
+ />
143
+ ```
144
+
145
+ `name`/`form`/`value` của `LarkSwitch` và `LarkCheckbox` vào hidden input của
146
+ Radix nên `FormData` trong `<form>` thu được giá trị thật.
147
+
148
+ ## Nguồn số đo
149
+
150
+ Số đo lấy từ style guide chính thức của Lark (MCP `lark-design`:
151
+ `design-language/*`, `components/*`) — đây là nguồn quyết định. File Figma
152
+ (`~/Sites/lily-projects/lark-design-guide/figma/`) chỉ để đối chiếu; lệch thì
153
+ theo style guide và ghi rõ chỗ lệch trong spec. Chi tiết:
154
+ `docs/specs/2026-11-05-lark-ui.md` và `specs/*.md`.
155
+
156
+ ## Ví dụ
92
157
 
93
158
  ```tsx
94
159
  import { LarkButton, LarkTextField } from 'lily-lark-ui'
95
- import 'lily-lark-ui/styles.css'
96
160
 
97
161
  export function LoginForm() {
98
162
  return (
99
163
  <form>
100
- <LarkTextField label="So dien thoai" placeholder="Nhap so dien thoai" isRequired />
101
- <LarkButton type="submit">Dang nhap</LarkButton>
164
+ <LarkTextField label="Số điện thoại" placeholder="Nhập số điện thoại" isRequired />
165
+ <LarkButton type="submit">Đăng nhập</LarkButton>
102
166
  </form>
103
167
  )
104
168
  }
@@ -119,6 +183,18 @@ export function RecordsTable({ columns, rows, page, total, pageSize, onPageChang
119
183
  }
120
184
  ```
121
185
 
122
- ## Phat trien
186
+ ## Xem component
187
+
188
+ Mở `lark-ui-components.html` ở root repo bằng trình duyệt — một file tự chứa,
189
+ có nút chuyển sáng/tối, responsive 320–1600px.
190
+
191
+ ## Phát triển
123
192
 
124
- Xem [AGENTS.md](./AGENTS.md) truoc khi sua code.
193
+ Xem [AGENTS.md](./AGENTS.md) trước khi sửa code.
194
+
195
+ ```bash
196
+ pnpm test # 254 test (vitest + jsdom)
197
+ pnpm test:visual # 9 test layout thật (Playwright)
198
+ pnpm check:lark # cấm hex, dark:, emoji-icon trong src/
199
+ pnpm showcase:build # build lại lark-ui-components.html
200
+ ```
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
- export interface LarkAlertDialogProps {
2
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
3
+ export interface LarkAlertDialogProps extends Omit<React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>, 'title' | 'children' | 'className'> {
3
4
  open?: boolean;
4
5
  onOpenChange?: (open: boolean) => void;
5
6
  title: string;
@@ -4,8 +4,13 @@ import * as React from 'react';
4
4
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
5
5
  import { cn } from '../lib/utils.js';
6
6
  import { LarkButton } from './button.js';
7
- function LarkAlertDialogBase({ open, onOpenChange, title, description, confirmLabel, cancelLabel = 'Hủy', onConfirm, isPending, children, className, }, ref) {
8
- return (_jsx(AlertDialogPrimitive.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogPrimitive.Portal, { children: [_jsx(AlertDialogPrimitive.Overlay, { className: "fixed inset-0 bg-[var(--overlay-mask)] data-[state=open]:anim-overlay-in data-[state=closed]:anim-overlay-out" }), _jsxs(AlertDialogPrimitive.Content, { ref: ref, ...(description ? {} : { 'aria-describedby': undefined }), onEscapeKeyDown: isPending ? (event) => event.preventDefault() : undefined, className: cn('fixed left-1/2 top-1/2 w-[calc(100vw-2.5rem)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-brand-primary p-5 shadow-[var(--shadow-overlay)] outline-none data-[state=open]:anim-overlay-in data-[state=closed]:anim-overlay-out md:w-full md:max-w-md md:p-6', className), children: [_jsx(AlertDialogPrimitive.Title, { className: "text-base font-medium text-lark-n900 md:text-[17px]", children: title }), description ? (_jsx(AlertDialogPrimitive.Description, { className: "mt-3 text-sm text-lark-n600", children: description })) : null, children ? _jsx("div", { className: "mt-3", children: children }) : null, _jsxs("div", { className: "mt-3 flex justify-end gap-3 border-t border-[color-mix(in_srgb,var(--color-lark-n900)_15%,transparent)] px-5 py-4 md:px-6 -mx-5 -mb-5 md:-mx-6 md:-mb-6", children: [_jsx(AlertDialogPrimitive.Cancel, { asChild: true, children: _jsx(LarkButton, { type: "button", disabled: isPending, children: cancelLabel }) }), _jsx(AlertDialogPrimitive.Action, { asChild: true, children: _jsx(LarkButton, { type: "button", variant: "primary", isPending: isPending, onClick: onConfirm, children: confirmLabel }) })] })] })] }) }));
7
+ function LarkAlertDialogBase({ open, onOpenChange, title, description, confirmLabel, cancelLabel = 'Hủy', onConfirm, isPending, children, className, onEscapeKeyDown, ...props }, ref) {
8
+ return (_jsx(AlertDialogPrimitive.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogPrimitive.Portal, { children: [_jsx(AlertDialogPrimitive.Overlay, { className: "fixed inset-0 bg-[var(--overlay-mask)] data-[state=open]:anim-overlay-in data-[state=closed]:anim-overlay-out" }), _jsxs(AlertDialogPrimitive.Content, { ref: ref, ...(description ? {} : { 'aria-describedby': undefined }), onEscapeKeyDown: isPending
9
+ ? (event) => {
10
+ event.preventDefault();
11
+ onEscapeKeyDown?.(event);
12
+ }
13
+ : onEscapeKeyDown, className: cn('fixed left-1/2 top-1/2 w-[calc(100vw-2.5rem)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-brand-primary p-5 shadow-[var(--shadow-overlay)] outline-none data-[state=open]:anim-overlay-in data-[state=closed]:anim-overlay-out md:w-full md:max-w-md md:p-6', className), ...props, children: [_jsx(AlertDialogPrimitive.Title, { className: "text-base font-medium text-lark-n900 md:text-[17px]", children: title }), description ? (_jsx(AlertDialogPrimitive.Description, { className: "mt-3 text-sm text-lark-n600", children: description })) : null, children ? _jsx("div", { className: "mt-3", children: children }) : null, _jsxs("div", { className: "mt-3 flex justify-end gap-3 border-t border-[color-mix(in_srgb,var(--color-lark-n900)_15%,transparent)] px-5 py-4 md:px-6 -mx-5 -mb-5 md:-mx-6 md:-mb-6", children: [_jsx(AlertDialogPrimitive.Cancel, { asChild: true, children: _jsx(LarkButton, { type: "button", disabled: isPending, children: cancelLabel }) }), _jsx(AlertDialogPrimitive.Action, { asChild: true, children: _jsx(LarkButton, { type: "button", variant: "primary", isPending: isPending, onClick: onConfirm, children: confirmLabel }) })] })] })] }) }));
9
14
  }
10
15
  const LarkAlertDialogInner = React.forwardRef(LarkAlertDialogBase);
11
16
  LarkAlertDialogInner.displayName = 'LarkAlertDialog';
@@ -1,10 +1,12 @@
1
1
  import * as React from 'react';
2
- export interface LarkCheckboxProps {
3
- label: string;
2
+ import * as Checkbox from '@radix-ui/react-checkbox';
3
+ export interface LarkCheckboxProps extends Omit<React.ComponentPropsWithoutRef<typeof Checkbox.Root>, 'checked' | 'onCheckedChange' | 'disabled' | 'className'> {
4
+ label?: string;
4
5
  checked?: boolean | 'indeterminate';
5
6
  onCheckedChange?: (checked: boolean | 'indeterminate') => void;
6
7
  disabled?: boolean;
7
8
  className?: string;
9
+ controlClassName?: string;
8
10
  }
9
11
  declare const LarkCheckboxInner: React.ForwardRefExoticComponent<LarkCheckboxProps & React.RefAttributes<HTMLButtonElement>>;
10
12
  export { LarkCheckboxInner as LarkCheckbox };
@@ -5,10 +5,14 @@ import * as Checkbox from '@radix-ui/react-checkbox';
5
5
  import * as Label from '@radix-ui/react-label';
6
6
  import { Check, Minus } from 'lucide-react';
7
7
  import { cn } from '../lib/utils.js';
8
- function LarkCheckboxBase({ label, checked, onCheckedChange, disabled, className }, ref) {
8
+ function LarkCheckboxBase({ label, checked, onCheckedChange, disabled, className, controlClassName, id, ...props }, ref) {
9
9
  const autoId = React.useId();
10
+ const checkboxId = id ?? autoId;
10
11
  const indeterminate = checked === 'indeterminate';
11
- return (_jsxs("div", { className: cn('flex items-center gap-2', className), children: [_jsx(Checkbox.Root, { ref: ref, id: autoId, checked: checked, onCheckedChange: onCheckedChange, disabled: disabled, className: "size-4 shrink-0 rounded-[4px] border border-lark-n500 bg-brand-primary focus-visible:focus-ring transition-colors duration-fast ease-base data-[state=checked]:border-brand-secondary data-[state=checked]:bg-brand-secondary data-[state=indeterminate]:border-brand-secondary data-[state=indeterminate]:bg-brand-secondary disabled:cursor-not-allowed disabled:opacity-50", children: _jsx(Checkbox.Indicator, { className: "flex items-center justify-center", children: indeterminate ? (_jsx(Minus, { className: "size-3 text-white", "aria-hidden": "true" })) : (_jsx(Check, { className: "size-3 text-white", "aria-hidden": "true" })) }) }), _jsx(Label.Root, { htmlFor: autoId, className: "text-sm text-lark-n900", children: label })] }));
12
+ const root = (_jsx(Checkbox.Root, { ref: ref, id: checkboxId, checked: checked, onCheckedChange: onCheckedChange, disabled: disabled, className: cn('size-4 shrink-0 rounded-[4px] border border-lark-n500 bg-brand-primary focus-visible:focus-ring transition-colors duration-fast ease-base data-[state=checked]:border-brand-secondary data-[state=checked]:bg-brand-secondary data-[state=indeterminate]:border-brand-secondary data-[state=indeterminate]:bg-brand-secondary disabled:cursor-not-allowed disabled:opacity-50', controlClassName), ...props, children: _jsx(Checkbox.Indicator, { className: "flex items-center justify-center", children: indeterminate ? (_jsx(Minus, { className: "size-3 text-white", "aria-hidden": "true" })) : (_jsx(Check, { className: "size-3 text-white", "aria-hidden": "true" })) }) }));
13
+ if (!label)
14
+ return _jsx("div", { className: className, children: root });
15
+ return (_jsxs("div", { className: cn('flex items-center gap-2', className), children: [root, _jsx(Label.Root, { htmlFor: checkboxId, className: "text-sm text-lark-n900", children: label })] }));
12
16
  }
13
17
  const LarkCheckboxInner = React.forwardRef(LarkCheckboxBase);
14
18
  LarkCheckboxInner.displayName = 'LarkCheckbox';
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+ /** Nhãn tháng: `2026-10` → `Tháng 10/2026`; chuỗi lạ giữ nguyên. */
3
+ export declare function formatMonthValue(value: string): string;
4
+ export interface LarkMonthPickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue' | 'children' | 'className'> {
5
+ /** Nhãn hiển thị phía trên trigger. */
6
+ label: string;
7
+ /** Kỳ dạng `YYYY-MM`; chuỗi rỗng hoặc sai định nghĩa là chưa chọn. */
8
+ value: string;
9
+ /** Nhận chuỗi `YYYY-MM` vừa chọn; nút xoá nhận `''`. */
10
+ onChange: (value: string) => void;
11
+ /** Báo lỗi dưới trigger; có lỗi thì trigger viền đỏ. */
12
+ error?: string;
13
+ /** Viền đỏ không kèm chữ (khi caller tự báo lỗi ngoài). */
14
+ isInvalid?: boolean;
15
+ /** true thì ẩn nút xoá — không cho bỏ trống; mặc định false cho phép xoá trả `''`. */
16
+ required?: boolean;
17
+ placeholder?: string;
18
+ disabled?: boolean;
19
+ /** id đặt lên trigger để label form trỏ htmlFor. */
20
+ id?: string;
21
+ className?: string;
22
+ /** Class cho wrapper ngoài (nhãn + trigger + lỗi). */
23
+ containerClassName?: string;
24
+ /** aria-describedby thêm vào trigger (mô tả ngoài như hint). */
25
+ describedBy?: string;
26
+ /** Callback gọi cùng `onChange` khi người dùng xoá giá trị. */
27
+ onClear?: () => void;
28
+ }
29
+ export declare function LarkMonthPicker({ label, value, onChange, error, isInvalid, required, placeholder, disabled, id, className, containerClassName, describedBy, onClear: onClearProp, ...props }: LarkMonthPickerProps): React.JSX.Element;
30
+ export interface LarkDatePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue' | 'children' | 'className'> {
31
+ /** Nhãn hiển thị phía trên trigger. */
32
+ label: string;
33
+ /** Ngày dạng `YYYY-MM-DD`; chuỗi rỗng hoặc sai là chưa chọn. */
34
+ value: string;
35
+ /** Nhận `YYYY-MM-DD`; nút xoá nhận `''`. */
36
+ onChange: (value: string) => void;
37
+ error?: string;
38
+ isInvalid?: boolean;
39
+ /** true thì ẩn nút xoá; mặc định false cho phép xoá trả `''`. */
40
+ required?: boolean;
41
+ placeholder?: string;
42
+ disabled?: boolean;
43
+ id?: string;
44
+ className?: string;
45
+ containerClassName?: string;
46
+ describedBy?: string;
47
+ onClear?: () => void;
48
+ }
49
+ export declare function LarkDatePicker({ label, value, onChange, error, isInvalid, required, placeholder, disabled, id, className, containerClassName, describedBy, onClear: onClearProp, ...props }: LarkDatePickerProps): React.JSX.Element;