@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 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`.