@lucasvu/scope-ui 0.0.1
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.
- package/README.md +394 -0
- package/dist/index.cjs +6587 -0
- package/dist/index.d.cts +842 -0
- package/dist/index.d.ts +842 -0
- package/dist/index.js +6516 -0
- package/dist/styles.css +13 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# @base/ui
|
|
2
|
+
|
|
3
|
+
Bộ component phong cách **shadcn** dùng chung cho các app/micro-frontend trong workspace. Tất cả đều là React component thuần, kèm sẵn theme tối và hiệu ứng kính mờ.
|
|
4
|
+
|
|
5
|
+
## Publish lên npm
|
|
6
|
+
|
|
7
|
+
Nếu muốn publish public để người khác chỉ cần cài và import:
|
|
8
|
+
|
|
9
|
+
1. Đổi `name` trong `package.json` sang package name hoặc scope bạn thực sự sở hữu trên npm.
|
|
10
|
+
2. Chạy `npm install` để cài thêm tool build mới nếu máy chưa có lockfile/deps tương ứng.
|
|
11
|
+
3. Build package:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
4. Kiểm tra package trước khi publish:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm pack --dry-run
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
5. Đăng nhập npm và publish:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm login
|
|
27
|
+
npm publish
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`publishConfig.access: public` đã được bật sẵn, nên với package dạng scope như `@your-scope/ui` không cần nhớ thêm `--access public`.
|
|
31
|
+
|
|
32
|
+
## Cách dùng nhanh
|
|
33
|
+
|
|
34
|
+
1) Cài package:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @your-scope/ui
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2) Import global style một lần ở entry (ví dụ `main.tsx`):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import '@your-scope/ui/styles.css'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
3) Dùng component:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { Button, Card, CardHeader, CardTitle, CardContent, DataTable, Field, Input } from '@your-scope/ui'
|
|
50
|
+
|
|
51
|
+
export function Example() {
|
|
52
|
+
return (
|
|
53
|
+
<Card>
|
|
54
|
+
<CardHeader>
|
|
55
|
+
<CardTitle>Form đăng ký</CardTitle>
|
|
56
|
+
</CardHeader>
|
|
57
|
+
<CardContent className="ui-grid ui-grid--two">
|
|
58
|
+
<Field label="Email" required>
|
|
59
|
+
<Input type="email" placeholder="you@example.com" />
|
|
60
|
+
</Field>
|
|
61
|
+
<Field label="Gói sử dụng">
|
|
62
|
+
<DataTable
|
|
63
|
+
data={[{ name: 'Starter', price: '$19' }]}
|
|
64
|
+
rowKey="name"
|
|
65
|
+
columns={[
|
|
66
|
+
{ key: 'name', title: 'Tên', sortable: true },
|
|
67
|
+
{ key: 'price', title: 'Giá' },
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
</Field>
|
|
71
|
+
</CardContent>
|
|
72
|
+
<Button block>Tiếp tục</Button>
|
|
73
|
+
</Card>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Thành phần có sẵn
|
|
79
|
+
|
|
80
|
+
- Nút: `Button` (variant `primary | secondary | ghost | outline | destructive | link | accent`, size `sm | md | lg | icon`, hỗ trợ `block`)
|
|
81
|
+
- Badge: `Badge` (solid, outline, success, warning)
|
|
82
|
+
- Thẻ: `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`
|
|
83
|
+
- Form: `Field`, `Label`, `Input`, `Select`, `Textarea`, `Combobox` (searchable input + dropdown giống shadcn)
|
|
84
|
+
- Numeric: `NumericInput` (hỗ trợ integer/decimal, parse an toàn, format/clamp khi blur, dùng cho price/amount)
|
|
85
|
+
- Tabs: `Tabs` với mảng `items` `{ value, label, content, badge? }`
|
|
86
|
+
- Thông báo: `Alert` (tone info/success/warning/danger)
|
|
87
|
+
- Số liệu: `Stat` (label, value, delta, trend up/down/flat)
|
|
88
|
+
- Bảng: `DataTable<T>` với `columns` và `render` tuỳ biến
|
|
89
|
+
- Tooltip: `Tooltip`, `OverflowTooltip`, `LineClampTooltip` (hiển thị tooltip khi bị cắt theo dòng)
|
|
90
|
+
- Lưới tiện dụng: class `ui-grid`, `ui-grid--two`, `ui-grid--three` để xếp block nhanh
|
|
91
|
+
- `Showcase`: một view mẫu kết hợp stats + tabs + form + bảng (import từ `@base/ui` để demo)
|
|
92
|
+
|
|
93
|
+
## NumericInput
|
|
94
|
+
|
|
95
|
+
`NumericInput` dùng cho các ô số cần giữ trải nghiệm nhập liệu mượt (không nhảy caret, chấp nhận trạng thái tạm như `.` hoặc `12.`), đồng thời parse/format có kiểm soát.
|
|
96
|
+
|
|
97
|
+
### Public API
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
type NumericInputProps = {
|
|
101
|
+
value?: string | number;
|
|
102
|
+
defaultValue?: string | number;
|
|
103
|
+
onValueChange?: (raw: string) => void;
|
|
104
|
+
onNumberChange?: (num: number | null) => void;
|
|
105
|
+
mode?: 'integer' | 'decimal'; // default: 'decimal'
|
|
106
|
+
decimalScale?: number;
|
|
107
|
+
maxDecimalScale?: number;
|
|
108
|
+
min?: number;
|
|
109
|
+
max?: number;
|
|
110
|
+
allowNegative?: boolean; // default: false
|
|
111
|
+
decimalSeparator?: '.' | ',' | 'auto'; // default: 'auto'
|
|
112
|
+
formatOnBlur?: 'none' | 'trim' | 'fixed'; // default: 'trim'
|
|
113
|
+
clampOnBlur?: boolean; // default: false
|
|
114
|
+
disabled?: boolean;
|
|
115
|
+
readOnly?: boolean;
|
|
116
|
+
name?: string;
|
|
117
|
+
onBlur?: (e) => void;
|
|
118
|
+
onFocus?: (e) => void;
|
|
119
|
+
// + các input props khác: className, placeholder, ...
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Rule chính
|
|
124
|
+
|
|
125
|
+
- Luôn giữ `rawValue` theo đúng ký tự user nhập.
|
|
126
|
+
- `onValueChange` luôn bắn khi user gõ.
|
|
127
|
+
- `onNumberChange` trả `number` khi parse được, ngược lại trả `null`.
|
|
128
|
+
- Không tự chèn dấu phân tách hàng nghìn trong lúc gõ.
|
|
129
|
+
- Có hỗ trợ paste chuỗi như `1,234.50` hoặc `1.234,50` (`decimalSeparator="auto"`).
|
|
130
|
+
- Với `maxDecimalScale`, input sẽ chặn vượt số chữ số phần thập phân.
|
|
131
|
+
|
|
132
|
+
### Ví dụ dùng
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { NumericInput } from '@base/ui';
|
|
136
|
+
|
|
137
|
+
// 1) Integer qty
|
|
138
|
+
<NumericInput
|
|
139
|
+
mode="integer"
|
|
140
|
+
min={0}
|
|
141
|
+
value={qty}
|
|
142
|
+
onValueChange={setQty}
|
|
143
|
+
/>;
|
|
144
|
+
|
|
145
|
+
// 2) Money
|
|
146
|
+
<NumericInput
|
|
147
|
+
mode="decimal"
|
|
148
|
+
decimalScale={2}
|
|
149
|
+
maxDecimalScale={2}
|
|
150
|
+
formatOnBlur="fixed"
|
|
151
|
+
value={price}
|
|
152
|
+
onValueChange={setPrice}
|
|
153
|
+
/>;
|
|
154
|
+
|
|
155
|
+
// 3) Percent
|
|
156
|
+
<NumericInput
|
|
157
|
+
mode="decimal"
|
|
158
|
+
decimalScale={2}
|
|
159
|
+
maxDecimalScale={2}
|
|
160
|
+
min={0}
|
|
161
|
+
max={100}
|
|
162
|
+
clampOnBlur
|
|
163
|
+
value={percent}
|
|
164
|
+
onValueChange={setPercent}
|
|
165
|
+
/>;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## DataTable
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import type { DataTableColumn, DataTableSortState } from '@base/ui'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Tối thiểu
|
|
175
|
+
|
|
176
|
+
- `data`: mảng record
|
|
177
|
+
- `columns`: cấu hình cột
|
|
178
|
+
- `rowKey`: key của record (string/number) hoặc hàm `(record) => key`
|
|
179
|
+
|
|
180
|
+
### Props chính
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
type DataTableProps<T> = {
|
|
184
|
+
columns: DataTableColumn<T>[];
|
|
185
|
+
data: T[];
|
|
186
|
+
rowKey: keyof T | ((record: T) => string | number);
|
|
187
|
+
loading?: boolean;
|
|
188
|
+
emptyText?: ReactNode;
|
|
189
|
+
pagination?: {
|
|
190
|
+
page: number;
|
|
191
|
+
pageSize: number;
|
|
192
|
+
total: number;
|
|
193
|
+
onChange: (page: number) => void;
|
|
194
|
+
pageSizeOptions?: number[];
|
|
195
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
196
|
+
};
|
|
197
|
+
sort?: DataTableSortState | null;
|
|
198
|
+
onSortChange?: (sort: DataTableSortState | null) => void;
|
|
199
|
+
sortMode?: 'client' | 'server';
|
|
200
|
+
onRowClick?: (record: T) => void;
|
|
201
|
+
rowSelection?: {
|
|
202
|
+
selectedRowKeys: Array<string | number>;
|
|
203
|
+
onChange: (keys: Array<string | number>) => void;
|
|
204
|
+
};
|
|
205
|
+
renderActions?: (record: T) => ReactNode;
|
|
206
|
+
className?: string;
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Cấu hình cột
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
type DataTableColumn<T> = {
|
|
214
|
+
key: string; // khóa duy nhất của cột
|
|
215
|
+
title: ReactNode; // tiêu đề hiển thị ở header
|
|
216
|
+
dataIndex?: keyof T; // map dữ liệu (mặc định lấy theo key)
|
|
217
|
+
width?: number | string; // độ rộng ưu tiên (vd: 180, '180px', '20%')
|
|
218
|
+
render?: (value, record, index) => ReactNode; // custom cell
|
|
219
|
+
sortable?: boolean; // bật/tắt sort cho cột (ưu tiên cao nhất)
|
|
220
|
+
sorter?: (a, b) => number; // so sánh asc/desc
|
|
221
|
+
sortValue?: (record) => string | number | Date | null;
|
|
222
|
+
};
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Độ rộng cột (width)
|
|
226
|
+
|
|
227
|
+
- Nếu cần giữ độ rộng cố định cho một cột, đặt `width` trong cấu hình cột.
|
|
228
|
+
- Cột có `width` sẽ ưu tiên theo giá trị này; các cột còn lại vẫn tự đo theo nội dung.
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
const columns: DataTableColumn<User>[] = [
|
|
232
|
+
{ key: 'name', title: 'Name', dataIndex: 'name', width: 180 },
|
|
233
|
+
{ key: 'email', title: 'Email', dataIndex: 'email' },
|
|
234
|
+
];
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Bật/tắt nhanh
|
|
238
|
+
|
|
239
|
+
- Sort: set `sortable: true` (hoặc `sorter`/`sortValue`); muốn tắt hẳn thì `sortable: false`.
|
|
240
|
+
- Checkbox: truyền `rowSelection` để hiện; bỏ `rowSelection` để ẩn.
|
|
241
|
+
- Actions column: truyền `renderActions` để hiện; bỏ `renderActions` để ẩn.
|
|
242
|
+
- Pagination: truyền `pagination` để hiện; bỏ `pagination` để ẩn.
|
|
243
|
+
- Page size dropdown: truyền `pagination.onPageSizeChange` (kèm `pageSizeOptions` nếu cần).
|
|
244
|
+
|
|
245
|
+
### Sort (client)
|
|
246
|
+
|
|
247
|
+
Chỉ cột có `sortable`, `sorter`, hoặc `sortValue` mới hiện icon sort. Nếu muốn tắt hẳn, đặt `sortable: false`. Thứ tự click: `asc` -> `desc` -> bỏ sort.
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
const columns: DataTableColumn<User>[] = [
|
|
251
|
+
{ key: 'name', title: 'Name', dataIndex: 'name', sortable: true },
|
|
252
|
+
{
|
|
253
|
+
key: 'createdAt',
|
|
254
|
+
title: 'Created',
|
|
255
|
+
dataIndex: 'createdAt',
|
|
256
|
+
sortValue: (row) => new Date(row.createdAt),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
key: 'status',
|
|
260
|
+
title: 'Status',
|
|
261
|
+
sorter: (a, b) => a.status.localeCompare(b.status),
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Sort (server)
|
|
267
|
+
|
|
268
|
+
Khi gọi API để sort/paginate, dùng `sortMode="server"` để DataTable không tự sort dữ liệu. `onSortChange` trả về `{ key, direction } | null`, bạn tự gọi API và set data mới.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
const [sort, setSort] = useState<DataTableSortState | null>(null);
|
|
272
|
+
|
|
273
|
+
<DataTable
|
|
274
|
+
data={rows}
|
|
275
|
+
columns={columns}
|
|
276
|
+
rowKey="id"
|
|
277
|
+
sort={sort}
|
|
278
|
+
sortMode="server"
|
|
279
|
+
onSortChange={setSort}
|
|
280
|
+
/>;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Checkbox chọn dòng
|
|
284
|
+
|
|
285
|
+
Checkbox chỉ hiện khi truyền `rowSelection`. Không truyền thì sẽ ẩn.
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
<DataTable
|
|
289
|
+
data={rows}
|
|
290
|
+
columns={columns}
|
|
291
|
+
rowKey="id"
|
|
292
|
+
rowSelection={{
|
|
293
|
+
selectedRowKeys,
|
|
294
|
+
onChange: setSelectedRowKeys,
|
|
295
|
+
}}
|
|
296
|
+
/>;
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Pagination + chọn page size
|
|
300
|
+
|
|
301
|
+
Nếu truyền `onPageSizeChange`, DataTable sẽ hiện dropdown chọn số dòng mỗi trang.
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
const [page, setPage] = useState(1);
|
|
305
|
+
const [pageSize, setPageSize] = useState(20);
|
|
306
|
+
|
|
307
|
+
<DataTable
|
|
308
|
+
data={rows}
|
|
309
|
+
columns={columns}
|
|
310
|
+
rowKey="id"
|
|
311
|
+
pagination={{
|
|
312
|
+
page,
|
|
313
|
+
pageSize,
|
|
314
|
+
total,
|
|
315
|
+
onChange: setPage,
|
|
316
|
+
pageSizeOptions: [10, 20, 50, 100],
|
|
317
|
+
onPageSizeChange: (size) => {
|
|
318
|
+
setPageSize(size);
|
|
319
|
+
setPage(1);
|
|
320
|
+
},
|
|
321
|
+
}}
|
|
322
|
+
/>;
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Actions + click row
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
<DataTable
|
|
329
|
+
data={rows}
|
|
330
|
+
columns={columns}
|
|
331
|
+
rowKey="id"
|
|
332
|
+
onRowClick={(row) => console.log(row)}
|
|
333
|
+
renderActions={(row) => <ActionMenu row={row} />}
|
|
334
|
+
/>;
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Loading + empty
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
<DataTable
|
|
341
|
+
data={rows}
|
|
342
|
+
columns={columns}
|
|
343
|
+
rowKey="id"
|
|
344
|
+
loading={isLoading}
|
|
345
|
+
emptyText="No data"
|
|
346
|
+
/>;
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Sticky header khi scroll
|
|
350
|
+
|
|
351
|
+
Nếu muốn header dính khi scroll trang, không đặt `overflow` ở container cha. Nếu muốn scroll trong bảng, đặt `max-height` + `overflow-auto` cho DataTable.
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
<DataTable className="max-h-[60vh] overflow-auto" ... />
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Scroll ngang trên màn nhỏ
|
|
358
|
+
|
|
359
|
+
DataTable tự hỗ trợ scroll ngang khi nội dung vượt chiều rộng (đặc biệt trên màn nhỏ). Không cần cấu hình thêm.
|
|
360
|
+
|
|
361
|
+
### Lưu ý
|
|
362
|
+
|
|
363
|
+
- `rowKey` phải ổn định và unique, đặc biệt khi dùng `rowSelection`.
|
|
364
|
+
- Nếu truyền `sort` (controlled), bạn cần cập nhật lại state trong `onSortChange`.
|
|
365
|
+
- `sortMode="server"` chỉ đổi trạng thái UI, dữ liệu phải tự fetch/sort từ API.
|
|
366
|
+
- `pagination.onChange` dùng page bắt đầu từ 1.
|
|
367
|
+
- Dropdown page size chỉ hiện khi có `pagination.onPageSizeChange`.
|
|
368
|
+
|
|
369
|
+
## LineClampTooltip
|
|
370
|
+
|
|
371
|
+
Dùng khi cần hiển thị tối đa N dòng, nếu text bị cắt sẽ hiện tooltip đầy đủ khi hover.
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
import { LineClampTooltip } from '@base/ui'
|
|
375
|
+
|
|
376
|
+
<LineClampTooltip
|
|
377
|
+
text={record.apiEndpoint}
|
|
378
|
+
lineClamp={3}
|
|
379
|
+
className="w-[320px]"
|
|
380
|
+
side="top"
|
|
381
|
+
wrap
|
|
382
|
+
portal
|
|
383
|
+
/>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Tuỳ chỉnh theme
|
|
387
|
+
|
|
388
|
+
- Sửa tông màu, radius, shadow trong `packages/ui/src/styles.css` (biến `--primary`, `--radius`, `--shadow-*`).
|
|
389
|
+
- Các lớp `ui-*` đã được namespaced để tránh va chạm với CSS hiện tại.
|
|
390
|
+
|
|
391
|
+
## Gợi ý tích hợp
|
|
392
|
+
|
|
393
|
+
- Import `@base/ui/styles.css` ở host để các remote có thể tái sử dụng cùng theme.
|
|
394
|
+
- Nếu muốn kết hợp với Tailwind/shadcn gốc, giữ nguyên class `ui-*` và thêm `content` trỏ tới `packages/ui/src/**/*` trong `tailwind.config`.
|