kmod-cli 1.5.0 → 1.6.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.
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+
3
+ interface ListProps<T> {
4
+ items: T[];
5
+ keyExtractor: (item: T) => React.Key;
6
+ renderItem: (item: T, index: number) => React.ReactNode;
7
+ }
8
+
9
+ export function List<T>({ items, keyExtractor, renderItem }: ListProps<T>) {
10
+ return (
11
+ <>
12
+ {items.map((item, index) => (
13
+ <ListItem
14
+ key={keyExtractor(item)}
15
+ item={item}
16
+ index={index}
17
+ renderItem={renderItem}
18
+ />
19
+ ))}
20
+ </>
21
+ );
22
+ }
23
+
24
+ interface ListItemProps<T> {
25
+ item: T;
26
+ index: number;
27
+ renderItem: (item: T, index: number) => React.ReactNode;
28
+ }
29
+
30
+ // Generic memo component
31
+ function _ListItem<T>({ item, index, renderItem }: ListItemProps<T>) {
32
+ return <>{renderItem(item, index)}</>;
33
+ }
34
+
35
+ const ListItem = React.memo(_ListItem) as <T>(
36
+ props: ListItemProps<T>
37
+ ) => React.ReactNode;
38
+
39
+
40
+ // Example usage of the List component
41
+ // interface User {
42
+ // id: number;
43
+ // name: string;
44
+ // }
45
+
46
+ // const users: User[] = [
47
+ // { id: 1, name: 'Alice' },
48
+ // { id: 2, name: 'Bob' },
49
+ // { id: 3, name: 'Charlie' },
50
+ // ];
51
+
52
+ // export function UserList() {
53
+ // return (
54
+ // <List
55
+ // items={users}
56
+ // keyExtractor={(user) => user.id}
57
+ // renderItem={(user) => <div>{user.name}</div>}
58
+ // />
59
+ // );
60
+ // }
@@ -8,7 +8,6 @@ import {
8
8
  useState,
9
9
  } from 'react';
10
10
 
11
- // Nếu bạn cần alias cho ITable type, dùng:
12
11
  import type { Table as ITable } from '@tanstack/react-table';
13
12
  import {
14
13
  Cell,
@@ -55,7 +54,7 @@ export type TableClassNames = {
55
54
  };
56
55
  export type TableHeaderProps<TData> =
57
56
  HTMLAttributes<HTMLTableSectionElement> & {
58
- handleClick: ({
57
+ handleClick?: ({
59
58
  e,
60
59
  table,
61
60
  }: {
@@ -64,7 +63,7 @@ export type TableHeaderProps<TData> =
64
63
  }) => void;
65
64
  };
66
65
  export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
67
- handleClick: ({
66
+ handleClick?: ({
68
67
  e,
69
68
  table,
70
69
  }: {
@@ -73,7 +72,8 @@ export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
73
72
  }) => void;
74
73
  };
75
74
  export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
76
- handleClick: ({
75
+ classNameCondition?: | (({cell, table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) => string) | string;
76
+ handleClick?: ({
77
77
  e,
78
78
  table,
79
79
  cell,
@@ -85,7 +85,8 @@ export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
85
85
  };
86
86
  export type TableCellProps<TData, TValue> =
87
87
  HTMLAttributes<HTMLTableCellElement> & {
88
- handleClick: ({
88
+ classNameCondition?: | (({cell, table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) => string) | string;
89
+ handleClick?: ({
89
90
  e,
90
91
  table,
91
92
  cell,
@@ -96,7 +97,7 @@ export type TableCellProps<TData, TValue> =
96
97
  }) => void;
97
98
  };
98
99
  export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
99
- handleClick: ({
100
+ handleClick?: ({
100
101
  e,
101
102
  table,
102
103
  row,
@@ -107,7 +108,8 @@ export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
107
108
  }) => void;
108
109
  };
109
110
  export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
110
- handleClick: ({
111
+ classNameCondition?: | (({row, table}: {row?: Row<TData>; table?: ITable<TData>}) => string) | string;
112
+ handleClick?: ({
111
113
  e,
112
114
  table,
113
115
  row,
@@ -118,7 +120,7 @@ export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
118
120
  }) => void;
119
121
  };
120
122
  export type TableProps<TData> = HTMLAttributes<HTMLTableElement> & {
121
- handleClick: ({
123
+ handleClick?: ({
122
124
  e,
123
125
  table,
124
126
  }: {
@@ -173,6 +175,15 @@ export type DataTableProps<TData, TValue> = {
173
175
  // handles?: Handles
174
176
  };
175
177
 
178
+ export interface UseTablePropsFn<TData, TValue> {
179
+ table: ITable<TData>;
180
+ row?: Row<TData>;
181
+ cell?: Cell<TData, TValue>;
182
+ header?: Header<TData, TValue>;
183
+ headerGroup?: HeaderGroup<TData>;
184
+ }
185
+
186
+
176
187
  export type DataTableToolbarFns<TData> = {
177
188
  globalFilter: string;
178
189
  setGlobalFilter: (value: string) => void;
@@ -204,7 +215,7 @@ export function DataTable<TData, TValue>({
204
215
  useTableProps,
205
216
  initialState,
206
217
  alternate = "even",
207
- alternateColor = "#f5f5f5",
218
+ alternateColor = "#fbfbfb",
208
219
  // handles
209
220
  }: DataTableProps<TData, TValue>) {
210
221
  const table = useReactTable({
@@ -241,47 +252,74 @@ export function DataTable<TData, TValue>({
241
252
  };
242
253
 
243
254
  const {
244
- handleClick: tableHandleClick,
245
- onClick: tableOnClick,
246
- ...tableDomProps
247
- } = useTableProps?.tableProps || {};
255
+ handleClick: tableHandleClick,
256
+ onClick: tableOnClick,
257
+ ...tableDomProps
258
+ } = useTableProps?.tableProps || {};
248
259
 
249
- const {
250
- handleClick: headerHandleClick,
251
- onClick: headerOnClick,
252
- ...headerDomProps
253
- } = useTableProps?.headerProps || {};
254
- const {
255
- handleClick: rowHeadHandleClick,
256
- onClick: rowHeadOnClick,
257
- ...rowHeadDomProps
258
- } = useTableProps?.rowHeadProps || {};
259
- const {
260
- handleClick: bodyHandleClick,
261
- onClick: bodyOnClick,
262
- ...bodyDomProps
263
- } = useTableProps?.bodyProps || {};
264
- const {
265
- handleClick: rowBodyHandleClick,
266
- onClick: rowBodyOnClick,
267
- style: rowBodyStyle,
268
- ...rowBodyDomProps
269
- } = useTableProps?.rowBodyProps || {};
270
- const {
271
- handleClick: cellBodyHandleClick,
272
- onClick: cellBodyOnClick,
273
- ...cellBodyDomProps
274
- } = useTableProps?.cellBodyProps || {};
260
+ const {
261
+ handleClick: headerHandleClick,
262
+ onClick: headerOnClick,
263
+ ...headerDomProps
264
+ } = useTableProps?.headerProps || {};
265
+ const {
266
+ handleClick: rowHeadHandleClick,
267
+ onClick: rowHeadOnClick,
268
+ ...rowHeadDomProps
269
+ } = useTableProps?.rowHeadProps || {};
270
+ const {
271
+ handleClick: cellHeadHandleClick,
272
+ onClick: cellHeadOnClick,
273
+ classNameCondition: cellHeadClassNameCondition,
274
+ ...cellHeadDomProps
275
+ } = useTableProps?.cellHeadProps || {};
276
+ const {
277
+ handleClick: bodyHandleClick,
278
+ onClick: bodyOnClick,
279
+ ...bodyDomProps
280
+ } = useTableProps?.bodyProps || {};
281
+ const {
282
+ handleClick: rowBodyHandleClick,
283
+ onClick: rowBodyOnClick,
284
+ style: rowBodyStyle,
285
+ classNameCondition: rowBodyClassNameCondition,
286
+ ...rowBodyDomProps
287
+ } = useTableProps?.rowBodyProps || {};
288
+ const {
289
+ handleClick: cellBodyHandleClick,
290
+ onClick: cellBodyOnClick,
291
+ classNameCondition: cellBodyClassNameCondition,
292
+ ...cellBodyDomProps
293
+ } = useTableProps?.cellBodyProps || {};
275
294
 
276
- const {
277
- handleClick: skRowHandleClick,
278
- ...skRowDomProps
279
- } = useTableProps?.rowBodyProps || {};
295
+ const { handleClick: skRowHandleClick, classNameCondition: skRowClassNameCondition, ...skRowDomProps } =
296
+ useTableProps?.rowBodyProps || {};
280
297
 
281
- const {
282
- handleClick: skCellHandleClick,
283
- ...skCellDomProps
284
- } = useTableProps?.cellBodyProps || {};
298
+ const { handleClick: skCellHandleClick, classNameCondition: skCellClassNameCondition, ...skCellDomProps } =
299
+ useTableProps?.cellBodyProps || {};
300
+
301
+
302
+ function getCellHeadClassNameByCondition({cell,table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) {
303
+ if(!cell || !table) return "";
304
+ const classNameCondition = useTableProps?.cellHeadProps?.classNameCondition;
305
+ if(!classNameCondition) return "";
306
+ if(typeof classNameCondition === "string") return classNameCondition;
307
+ return classNameCondition({cell,table});
308
+ }
309
+ function getCellBodyClassNameByCondition({cell,table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) {
310
+ if(!cell || !table) return "";
311
+ const classNameCondition = useTableProps?.cellBodyProps?.classNameCondition;
312
+ if(!classNameCondition) return "";
313
+ if(typeof classNameCondition === "string") return classNameCondition;
314
+ return classNameCondition({cell,table});
315
+ }
316
+ function getRowBodyClassNameByCondition({row,table}: {row?: Row<TData>; table?: ITable<TData>}) {
317
+ if(!row || !table) return "";
318
+ const classNameCondition = useTableProps?.rowBodyProps?.classNameCondition;
319
+ if(!classNameCondition) return "";
320
+ if(typeof classNameCondition === "string") return classNameCondition;
321
+ return classNameCondition({row,table});
322
+ }
285
323
 
286
324
 
287
325
  return (
@@ -321,11 +359,7 @@ const {
321
359
  {headerGroup.headers.map((header) => (
322
360
  <TableHead
323
361
  key={header.id}
324
- {...(() => {
325
- const { handleClick, onClick, ...rest } =
326
- useTableProps?.cellHeadProps || {};
327
- return rest;
328
- })()}
362
+ {...cellHeadDomProps}
329
363
  className={cn(
330
364
  "cursor-pointer select-none",
331
365
  classNames?.header?.head
@@ -334,22 +368,11 @@ const {
334
368
  width: header.getSize() ? `${header.getSize()}px !important` : "auto",
335
369
  }}
336
370
  onClick={(e) => {
337
- // Just call the parent's onClick if provided
338
- if (useTableProps?.cellHeadProps?.onClick) {
339
- useTableProps.cellHeadProps.onClick(e);
340
- }
341
-
342
- // Just call the parent's handleClick if provided
343
- if (useTableProps?.cellHeadProps?.handleClick) {
344
- useTableProps.cellHeadProps.handleClick({
345
- e,
346
- cell: header,
347
- table,
348
- });
349
- }
371
+ cellHeadOnClick?.(e);
372
+ cellHeadHandleClick?.({ e, table, cell: header });
350
373
  }}
351
374
  >
352
- <div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content)}>
375
+ <div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content, getCellHeadClassNameByCondition({cell: header, table}))}>
353
376
  {flexRender(
354
377
  header.column.columnDef.header,
355
378
  header.getContext()
@@ -384,15 +407,13 @@ const {
384
407
  {!isLoading &&
385
408
  table.getRowModel().rows.length > 0 &&
386
409
  table.getRowModel().rows.map((row, index) => {
387
- const { handleClick, onClick, ...rest } =
388
- useTableProps?.rowBodyProps || {};
389
410
 
390
411
  return (
391
412
  <TableRow
392
413
  {...rowBodyDomProps}
393
414
  key={row.id}
394
415
  style={{...rowBodyStyle, backgroundColor: getAlternateColor(index)}}
395
- className={cn(classNames?.body?.row)}
416
+ className={cn(classNames?.body?.row, getRowBodyClassNameByCondition({row,table}))}
396
417
  data-state={row.getIsSelected() && "selected"}
397
418
  onClick={(e) => {
398
419
  rowBodyOnClick?.(e);
@@ -403,7 +424,7 @@ const {
403
424
  <TableCell
404
425
  {...cellBodyDomProps}
405
426
  key={cell.id}
406
- className={cn(classNames?.body?.cell)}
427
+ className={cn(classNames?.body?.cell, getCellBodyClassNameByCondition({cell, table}))}
407
428
  onClick={(e) => {
408
429
  cellBodyOnClick?.(e);
409
430
  cellBodyHandleClick?.({ e, cell, table });
@@ -480,26 +501,27 @@ export const TableSkeleton = <TData, TValue>({
480
501
  const {
481
502
  handleClick: _rowHandleClick,
482
503
  onClick: _rowOnClick,
504
+ classNameCondition: rowClassNameCondition,
483
505
  ...rowDomProps
484
506
  } = props?.rowBodyProps || {};
485
507
 
486
508
  const {
487
509
  handleClick: _cellHandleClick,
488
510
  onClick: _cellOnClick,
511
+ classNameCondition: cellClassNameCondition,
489
512
  ...cellDomProps
490
513
  } = props?.cellBodyProps || {};
491
514
 
492
-
493
515
  if (showNoData) {
494
516
  return (
495
517
  <TableRow
496
518
  key="no-data-skeleton"
497
- className={cn(classNames?.body?.row)}
519
+ className={cn(classNames?.body?.row, )}
498
520
  {...rowDomProps}
499
521
  >
500
522
  <TableCell
501
523
  colSpan={columns.length}
502
- className={cn("h-24 text-center", classNames?.body?.cell)}
524
+ className={cn("h-24 text-center")}
503
525
  {...cellDomProps}
504
526
  >
505
527
  {emptyLabel}
@@ -1,95 +1,200 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import React from 'react';
4
4
 
5
- import { ColumnDef } from "@tanstack/react-table";
5
+ import { ColumnDef } from '@tanstack/react-table';
6
6
 
7
- import { DataTable } from "./data-table";
7
+ import { cn } from '../../lib/utils';
8
+ import {
9
+ DataTable,
10
+ UseTableProps,
11
+ } from './data-table'; // chỉnh path cho đúng
8
12
 
13
+ /* ======================================================
14
+ * 1. Data type
15
+ * ====================================================== */
9
16
  type User = {
10
17
  id: string;
11
18
  name: string;
12
19
  email: string;
20
+ role: "admin" | "user";
21
+ status: "active" | "inactive";
13
22
  };
14
23
 
15
- // 1. Columns
24
+ /* ======================================================
25
+ * 2. Mock data
26
+ * ====================================================== */
27
+ const USERS: User[] = [
28
+ {
29
+ id: "1",
30
+ name: "John Doe",
31
+ email: "john@example.com",
32
+ role: "admin",
33
+ status: "active",
34
+ },
35
+ {
36
+ id: "2",
37
+ name: "Jane Smith",
38
+ email: "jane@example.com",
39
+ role: "user",
40
+ status: "inactive",
41
+ },
42
+ {
43
+ id: "3",
44
+ name: "Alex Johnson",
45
+ email: "alex@example.com",
46
+ role: "user",
47
+ status: "active",
48
+ },
49
+ ];
50
+
51
+ /* ======================================================
52
+ * 3. Columns
53
+ * ====================================================== */
16
54
  const columns: ColumnDef<User>[] = [
17
55
  {
18
56
  accessorKey: "name",
19
57
  header: "Name",
20
- cell: ({ row }) => <div className="font-medium">{row.getValue("name")}</div>,
21
- enableSorting: true,
58
+ cell: ({ row }) => (
59
+ <span className="font-medium">{row.original.name}</span>
60
+ ),
22
61
  },
23
62
  {
24
63
  accessorKey: "email",
25
64
  header: "Email",
26
- enableSorting: true,
65
+ cell: ({ getValue }) => (
66
+ <span className="text-blue-600">{getValue<string>()}</span>
67
+ ),
68
+ },
69
+ {
70
+ accessorKey: "role",
71
+ header: "Role",
72
+ cell: ({ getValue }) => (
73
+ <span className="capitalize">{getValue<string>()}</span>
74
+ ),
75
+ },
76
+ {
77
+ accessorKey: "status",
78
+ header: "Status",
79
+ cell: ({ getValue }) => {
80
+ const value = getValue<string>();
81
+ return (
82
+ <span
83
+ className={cn(
84
+ "px-2 py-0.5 rounded text-xs font-medium",
85
+ value === "active"
86
+ ? "bg-green-100 text-green-700"
87
+ : "bg-red-100 text-red-700"
88
+ )}
89
+ >
90
+ {value}
91
+ </span>
92
+ );
93
+ },
27
94
  },
28
95
  ];
29
96
 
30
- // 2. Dummy Data
31
- const users: User[] = [
32
- { id: "1", name: "Nguyễn Văn A", email: "a@example.com" },
33
- { id: "2", name: "Trần Thị B", email: "b@example.com" },
34
- { id: "3", name: "Lê Văn C", email: "c@example.com" },
35
- { id: "4", name: "Phạm Thị D", email: "d@example.com" },
36
- ];
97
+ /* ======================================================
98
+ * 4. Toolbar
99
+ * ====================================================== */
100
+ const Toolbar = ({ fns }: any) => {
101
+ return (
102
+ <div className="flex items-center gap-2">
103
+ <input
104
+ value={fns.globalFilter ?? ""}
105
+ onChange={(e) => fns.setGlobalFilter(e.target.value)}
106
+ placeholder="Search..."
107
+ className="border rounded px-2 py-1 text-sm"
108
+ />
109
+ </div>
110
+ );
111
+ };
37
112
 
38
- // 3. Component chính
39
- export default function UserTableExample() {
40
- const [data, setData] = useState<User[]>(users);
41
- const [isLoading, setIsLoading] = useState(false);
113
+ /* ======================================================
114
+ * 5. Pagination
115
+ * ====================================================== */
116
+ const Pagination = ({ fns }: any) => {
117
+ return (
118
+ <div className="flex items-center justify-end gap-2">
119
+ <button
120
+ onClick={fns.previousPage}
121
+ disabled={!fns.getCanPreviousPage()}
122
+ className="px-3 py-1 border rounded disabled:opacity-50"
123
+ >
124
+ Prev
125
+ </button>
126
+
127
+ <span className="text-sm">
128
+ Page {fns.pageIndex + 1}
129
+ </span>
130
+
131
+ <button
132
+ onClick={fns.nextPage}
133
+ disabled={!fns.getCanNextPage()}
134
+ className="px-3 py-1 border rounded disabled:opacity-50"
135
+ >
136
+ Next
137
+ </button>
138
+ </div>
139
+ );
140
+ };
42
141
 
142
+ /* ======================================================
143
+ * 6. useTableProps (row / cell behavior)
144
+ * ====================================================== */
145
+ const useTableProps: UseTableProps<User, unknown> = {
146
+ rowBodyProps: {
147
+ classNameCondition: ({ row }) =>
148
+ row?.original.status === "inactive"
149
+ ? "opacity-60"
150
+ : "",
151
+ handleClick: ({ row }) => {
152
+ console.log("Row clicked:", row.original);
153
+ },
154
+ },
155
+ cellBodyProps: {
156
+ classNameCondition: ({ cell }) =>
157
+ cell?.column.id === "email"
158
+ ? "underline"
159
+ : "",
160
+ },
161
+ };
162
+
163
+ /* ======================================================
164
+ * 7. Final Component
165
+ * ====================================================== */
166
+ export default function UserTableExample() {
43
167
  return (
44
- <DataTable
45
- data={data}
46
- columns={columns}
47
- isLoading={isLoading}
48
- emptyLabel="Không có dữ liệu nào"
49
- classNames={{
50
- wrapper: "p-4",
51
- table: "bg-transparent",
52
- header: {
53
- header: "bg-muted",
54
- row: "border-b",
55
- head: "text-left text-sm font-semibold",
56
- },
57
- body: {
58
- body: "bg-transparent",
59
- row: "hover:bg-muted/30",
60
- cell: "text-sm px-2 py-3",
61
- },
62
- }}
63
- toolbarTable={({ table, fns }) => (
64
- <div className="flex items-center gap-2">
65
- <input
66
- type="text"
67
- placeholder="Tìm kiếm..."
68
- value={fns.globalFilter ?? ""}
69
- onChange={(e) => fns.setGlobalFilter(e.target.value)}
70
- className="px-3 py-2 border rounded-md w-64"
71
- />
72
- </div>
73
- )}
74
- paginationTable={({ fns }) => (
75
- <div className="flex items-center justify-end gap-4">
76
- <button
77
- onClick={fns.previousPage}
78
- disabled={!fns.getCanPreviousPage()}
79
- className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
80
- >
81
- Trang trước
82
- </button>
83
- <span>Trang {fns.pageIndex + 1}</span>
84
- <button
85
- onClick={fns.nextPage}
86
- disabled={!fns.getCanNextPage()}
87
- className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
88
- >
89
- Trang sau
90
- </button>
91
- </div>
92
- )}
93
- />
168
+ <div className="p-4 space-y-4">
169
+ <h1 className="text-lg font-semibold">User Table</h1>
170
+
171
+ <DataTable<User, unknown>
172
+ data={USERS}
173
+ columns={columns}
174
+ enableSort
175
+ alternate="even"
176
+ alternateColor="#f9fafb"
177
+ emptyLabel="No users found"
178
+ initialState={{
179
+ pagination: {
180
+ pageSize: 5,
181
+ pageIndex: 0,
182
+ },
183
+ }}
184
+ toolbarTable={({ fns }) => <Toolbar fns={fns} />}
185
+ paginationTable={({ fns }) => <Pagination fns={fns} />}
186
+ useTableProps={useTableProps}
187
+ classNames={{
188
+ table: "border rounded-md",
189
+ header: {
190
+ head: "bg-gray-50 text-sm font-semibold",
191
+ },
192
+ body: {
193
+ row: "hover:bg-gray-50 cursor-pointer",
194
+ cell: "text-sm",
195
+ },
196
+ }}
197
+ />
198
+ </div>
94
199
  );
95
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kmod-cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Stack components utilities fast setup in projects",
5
5
  "author": "kumo_d",
6
6
  "license": "MIT",