@meta-1/design 0.0.159

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 (99) hide show
  1. package/README.md +412 -0
  2. package/package.json +138 -0
  3. package/src/assets/icons/empty.svg +1 -0
  4. package/src/assets/icons/spin.svg +1 -0
  5. package/src/assets/locales/en-us.ts +74 -0
  6. package/src/assets/locales/zh-cn.ts +74 -0
  7. package/src/assets/locales/zh-tw.ts +74 -0
  8. package/src/assets/style/theme.css +173 -0
  9. package/src/components/icons/Empty.tsx +18 -0
  10. package/src/components/icons/Spin.tsx +16 -0
  11. package/src/components/icons/index.ts +2 -0
  12. package/src/components/ui/alert-dialog.tsx +111 -0
  13. package/src/components/ui/alert.tsx +49 -0
  14. package/src/components/ui/avatar.tsx +32 -0
  15. package/src/components/ui/badge.tsx +36 -0
  16. package/src/components/ui/breadcrumb.tsx +92 -0
  17. package/src/components/ui/button.tsx +52 -0
  18. package/src/components/ui/calendar.tsx +56 -0
  19. package/src/components/ui/card.tsx +56 -0
  20. package/src/components/ui/checkbox.tsx +28 -0
  21. package/src/components/ui/command.tsx +137 -0
  22. package/src/components/ui/dialog.tsx +127 -0
  23. package/src/components/ui/dropdown-menu.tsx +217 -0
  24. package/src/components/ui/form.tsx +138 -0
  25. package/src/components/ui/hover-card.tsx +36 -0
  26. package/src/components/ui/input-otp.tsx +66 -0
  27. package/src/components/ui/input.tsx +21 -0
  28. package/src/components/ui/label.tsx +21 -0
  29. package/src/components/ui/navigation-menu.tsx +142 -0
  30. package/src/components/ui/pagination.tsx +118 -0
  31. package/src/components/ui/popover.tsx +40 -0
  32. package/src/components/ui/progress.tsx +22 -0
  33. package/src/components/ui/radio-group.tsx +31 -0
  34. package/src/components/ui/resizable.tsx +46 -0
  35. package/src/components/ui/scroll-area.tsx +46 -0
  36. package/src/components/ui/select.tsx +158 -0
  37. package/src/components/ui/separator.tsx +26 -0
  38. package/src/components/ui/sheet.tsx +101 -0
  39. package/src/components/ui/skeleton.tsx +7 -0
  40. package/src/components/ui/sonner.tsx +23 -0
  41. package/src/components/ui/switch.tsx +26 -0
  42. package/src/components/ui/table.tsx +73 -0
  43. package/src/components/ui/tabs.tsx +40 -0
  44. package/src/components/ui/textarea.tsx +18 -0
  45. package/src/components/ui/tooltip.tsx +46 -0
  46. package/src/components/uix/action/index.tsx +37 -0
  47. package/src/components/uix/alert/index.tsx +43 -0
  48. package/src/components/uix/alert-dialog/index.tsx +109 -0
  49. package/src/components/uix/avatar/index.tsx +25 -0
  50. package/src/components/uix/breadcrumbs/index.tsx +38 -0
  51. package/src/components/uix/broadcast-channel-context/index.tsx +28 -0
  52. package/src/components/uix/button/index.tsx +29 -0
  53. package/src/components/uix/card/index.tsx +32 -0
  54. package/src/components/uix/checkbox/index.tsx +79 -0
  55. package/src/components/uix/checkbox-group/index.tsx +60 -0
  56. package/src/components/uix/combo-select/index.tsx +364 -0
  57. package/src/components/uix/config-provider/index.tsx +31 -0
  58. package/src/components/uix/data-table/index.tsx +491 -0
  59. package/src/components/uix/data-table/style.css +40 -0
  60. package/src/components/uix/date-picker/index.tsx +88 -0
  61. package/src/components/uix/date-range-picker/index.tsx +71 -0
  62. package/src/components/uix/dialog/index.tsx +70 -0
  63. package/src/components/uix/divider/index.tsx +23 -0
  64. package/src/components/uix/dropdown/index.tsx +117 -0
  65. package/src/components/uix/empty/index.tsx +29 -0
  66. package/src/components/uix/filters/index.tsx +105 -0
  67. package/src/components/uix/form/index.tsx +274 -0
  68. package/src/components/uix/image/index.tsx +13 -0
  69. package/src/components/uix/loading/index.tsx +24 -0
  70. package/src/components/uix/message/index.tsx +21 -0
  71. package/src/components/uix/pagination/index.tsx +180 -0
  72. package/src/components/uix/radio-group/index.tsx +35 -0
  73. package/src/components/uix/result/index.tsx +45 -0
  74. package/src/components/uix/select/index.tsx +93 -0
  75. package/src/components/uix/space/index.tsx +24 -0
  76. package/src/components/uix/spin/index.tsx +12 -0
  77. package/src/components/uix/steps/index.tsx +67 -0
  78. package/src/components/uix/switch/index.tsx +33 -0
  79. package/src/components/uix/tooltip/index.tsx +29 -0
  80. package/src/components/uix/tree/index.tsx +39 -0
  81. package/src/components/uix/tree/style.css +75 -0
  82. package/src/components/uix/tree-select/index.tsx +137 -0
  83. package/src/components/uix/tree-table/action.tsx +24 -0
  84. package/src/components/uix/tree-table/config.ts +2 -0
  85. package/src/components/uix/tree-table/index.tsx +86 -0
  86. package/src/components/uix/tree-table/utils.tsx +63 -0
  87. package/src/components/uix/uploader/index.tsx +237 -0
  88. package/src/components/uix/uploader/type.ts +20 -0
  89. package/src/components/uix/uploader/utils.ts +41 -0
  90. package/src/components/uix/value-formatter/index.tsx +59 -0
  91. package/src/hooks/index.ts +2 -0
  92. package/src/hooks/resize.ts +29 -0
  93. package/src/hooks/use.outside.ts +30 -0
  94. package/src/index.ts +159 -0
  95. package/src/lib/formatters.ts +13 -0
  96. package/src/lib/index.ts +4 -0
  97. package/src/lib/is.ts +6 -0
  98. package/src/lib/react-dom.ts +98 -0
  99. package/src/lib/utils.ts +39 -0
@@ -0,0 +1,491 @@
1
+ import { useContext, useEffect, useMemo, useState } from "react";
2
+ import { DotsHorizontalIcon } from "@radix-ui/react-icons";
3
+ import {
4
+ type ColumnDef,
5
+ flexRender,
6
+ getCoreRowModel,
7
+ getSortedRowModel,
8
+ type Row,
9
+ type SortingState,
10
+ useReactTable,
11
+ type VisibilityState,
12
+ } from "@tanstack/react-table";
13
+ import isArray from "lodash/isArray";
14
+ import isFunction from "lodash/isFunction";
15
+ import isString from "lodash/isString";
16
+ import isUndefined from "lodash/isUndefined";
17
+
18
+ import {
19
+ Action,
20
+ Checkbox,
21
+ cn,
22
+ Dropdown,
23
+ type DropdownMenuItemProps,
24
+ Filters,
25
+ type FiltersProps,
26
+ generateColumnStorageKey,
27
+ Pagination,
28
+ type PaginationProps,
29
+ Spin,
30
+ } from "@meta-1/design";
31
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../ui/table";
32
+ import "./style.css";
33
+
34
+ import { LayoutIcon } from "@radix-ui/react-icons";
35
+ import classNames from "classnames";
36
+ import get from "lodash/get";
37
+
38
+ import { UIXContext } from "@meta-1/design/components/uix/config-provider";
39
+ import { type Formatters, type FunctionMap, formatValue } from "@meta-1/design/components/uix/value-formatter";
40
+
41
+ export interface StickyColumnProps {
42
+ key: string;
43
+ position: "left" | "right";
44
+ size: number;
45
+ }
46
+
47
+ export type DataTableColumn<TData> = ColumnDef<TData, unknown> & {
48
+ className?: string;
49
+ formatters?: Formatters;
50
+ };
51
+
52
+ export interface DataTableProps<TData> {
53
+ autoHidePagination?: boolean;
54
+ inCard?: boolean;
55
+ columns: DataTableColumn<TData>[];
56
+ data: TData[];
57
+ showColumnVisibility?: boolean;
58
+ stickyColumns?: StickyColumnProps[];
59
+ checkbox?: boolean;
60
+ rowActions?: DropdownMenuItemProps[] | ((cell: TData) => DropdownMenuItemProps[]);
61
+ onRowActionClick?: (item: DropdownMenuItemProps, row: Row<TData>) => void;
62
+ loading?: boolean;
63
+ load?: (params?: unknown) => Promise<unknown> | unknown;
64
+ filter?: FiltersProps;
65
+ pagination?: PaginationProps | boolean;
66
+ cellHandles?: FunctionMap;
67
+ empty?: string;
68
+ showHeader?: boolean;
69
+ onRowClick?: (row: Row<TData>) => void;
70
+ }
71
+
72
+ // 本地存储相关函数
73
+ const LOCAL_STORAGE_PREFIX = "datatable_columns_";
74
+
75
+ const saveColumnVisibility = (storageKey: string, visibility: VisibilityState) => {
76
+ try {
77
+ localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${storageKey}`, JSON.stringify(visibility));
78
+ } catch (_error) {
79
+ // console.warn('Failed to save column visibility to localStorage:', error)
80
+ }
81
+ };
82
+
83
+ const loadColumnVisibility = (storageKey: string): VisibilityState => {
84
+ try {
85
+ const stored = localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${storageKey}`);
86
+ return stored ? JSON.parse(stored) : {};
87
+ } catch (_error) {
88
+ // console.warn('Failed to load column visibility from localStorage:', error)
89
+ return {};
90
+ }
91
+ };
92
+
93
+ export const getSticky = (
94
+ id: string,
95
+ leftStickyColumns: StickyColumnProps[],
96
+ rightStickyColumns: StickyColumnProps[],
97
+ ) => {
98
+ const inLeft = !!leftStickyColumns.find(({ key }) => key === id);
99
+ if (inLeft) {
100
+ let offset = 0;
101
+ let width = 0;
102
+ let index = 0;
103
+ for (const col of leftStickyColumns) {
104
+ index++;
105
+ if (col.key === id) {
106
+ width = col.size;
107
+ break;
108
+ }
109
+ offset += col.size;
110
+ }
111
+ return {
112
+ width,
113
+ offset,
114
+ enable: true,
115
+ position: "left",
116
+ last: index === leftStickyColumns.length,
117
+ first: false,
118
+ };
119
+ }
120
+ const inRight = !!rightStickyColumns.find(({ key }) => key === id);
121
+ if (inRight) {
122
+ let offset = 0;
123
+ let width = 0;
124
+ let i = 0;
125
+ for (let index = rightStickyColumns.length - 1; index >= 0; index--) {
126
+ i++;
127
+ const col = rightStickyColumns[index];
128
+ if (col.key === id) {
129
+ width = col.size;
130
+ break;
131
+ }
132
+ offset += col.size;
133
+ }
134
+ return {
135
+ width,
136
+ offset,
137
+ enable: true,
138
+ position: "right",
139
+ last: false,
140
+ first: i === rightStickyColumns.length,
141
+ };
142
+ }
143
+ return { enable: false };
144
+ };
145
+
146
+ // biome-ignore lint/suspicious/noExplicitAny: <hasActions>
147
+ const hasActions = (rowActions: DropdownMenuItemProps[] | ((cell: any) => DropdownMenuItemProps[])) => {
148
+ if (isFunction(rowActions)) {
149
+ return true;
150
+ }
151
+ if (isArray(rowActions)) {
152
+ return !!rowActions.length;
153
+ }
154
+ return false;
155
+ };
156
+
157
+ export function DataTable<TData>(props: DataTableProps<TData>) {
158
+ const {
159
+ data,
160
+ columns,
161
+ showColumnVisibility = true,
162
+ rowActions,
163
+ checkbox = false,
164
+ stickyColumns = [],
165
+ onRowActionClick,
166
+ filter,
167
+ loading,
168
+ pagination,
169
+ load,
170
+ cellHandles,
171
+ showHeader = true,
172
+ autoHidePagination = true,
173
+ inCard = false,
174
+ } = props;
175
+
176
+ const config = useContext(UIXContext);
177
+ const empty = props.empty || get(config.locale, "DataTable.empty");
178
+ // 生成存储key
179
+ const storageKey = useMemo(() => {
180
+ return generateColumnStorageKey(columns);
181
+ }, [columns]);
182
+ const [sorting, setSorting] = useState<SortingState>([]);
183
+ // 初始化时不从本地存储读取,避免 SSR 水合错误
184
+ const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
185
+ const [rowSelection, setRowSelection] = useState({});
186
+ const [mounted, setMounted] = useState(false);
187
+
188
+ useEffect(() => {
189
+ // 只在客户端挂载后才读取本地存储的列可见性
190
+ setColumnVisibility(loadColumnVisibility(storageKey));
191
+ setMounted(true);
192
+ }, [storageKey]);
193
+
194
+ // 当列可见性改变时,保存到本地存储(但跳过初始的空状态)
195
+ useEffect(() => {
196
+ if (mounted) {
197
+ saveColumnVisibility(storageKey, columnVisibility);
198
+ }
199
+ }, [columnVisibility, storageKey, mounted]);
200
+
201
+ // 当columns改变时,重新加载存储的状态
202
+ useEffect(() => {
203
+ if (mounted) {
204
+ const newStorageKey = generateColumnStorageKey(columns);
205
+ const storedVisibility = loadColumnVisibility(newStorageKey);
206
+ setColumnVisibility(storedVisibility);
207
+ }
208
+ }, [columns, mounted]);
209
+
210
+ const tableColumns = useMemo<ColumnDef<TData, unknown>[]>(() => {
211
+ const newColumns: ColumnDef<TData, unknown>[] = [];
212
+ if (checkbox) {
213
+ newColumns.push({
214
+ id: "select",
215
+ enableSorting: false,
216
+ enableHiding: false,
217
+ // biome-ignore lint/suspicious/noExplicitAny: <header>
218
+ header: ({ table }: { table: any }) => (
219
+ <div className="flex items-center justify-center pr-2">
220
+ <Checkbox
221
+ aria-label="Select all"
222
+ checked={table.getIsAllPageRowsSelected()}
223
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
224
+ />
225
+ </div>
226
+ ),
227
+ // biome-ignore lint/suspicious/noExplicitAny: <cell>
228
+ cell: ({ row }: { row: any }) => (
229
+ <div className="flex items-center justify-center pr-2">
230
+ <Checkbox
231
+ aria-label="Select row"
232
+ checked={row.getIsSelected()}
233
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
234
+ />
235
+ </div>
236
+ ),
237
+ });
238
+ }
239
+ newColumns.push(...columns);
240
+ if (hasActions(rowActions!)) {
241
+ newColumns.push({
242
+ id: "actions",
243
+ enableHiding: false,
244
+ size: 50,
245
+ enableResizing: false,
246
+ // biome-ignore lint/suspicious/noExplicitAny: <cell>
247
+ cell: ({ row }: { row: any }) => {
248
+ let items = rowActions;
249
+ if (isFunction(rowActions)) {
250
+ items = rowActions(row.original);
251
+ }
252
+ return (
253
+ <div className="flex w-full justify-end" onClick={(e) => e.stopPropagation()}>
254
+ <Dropdown
255
+ align="end"
256
+ asChild={true}
257
+ items={items as DropdownMenuItemProps[]}
258
+ onItemClick={(item) => onRowActionClick?.(item, row)}
259
+ >
260
+ <Action className="!p-0 h-6 w-6">
261
+ <DotsHorizontalIcon />
262
+ </Action>
263
+ </Dropdown>
264
+ </div>
265
+ );
266
+ },
267
+ });
268
+ }
269
+ return newColumns;
270
+ }, [columns, checkbox, rowActions, onRowActionClick]);
271
+
272
+ const table = useReactTable({
273
+ data,
274
+ columns: tableColumns,
275
+ getCoreRowModel: getCoreRowModel(),
276
+ onSortingChange: setSorting,
277
+ getSortedRowModel: getSortedRowModel(),
278
+ onColumnVisibilityChange: setColumnVisibility,
279
+ onRowSelectionChange: setRowSelection,
280
+ state: {
281
+ sorting,
282
+ columnVisibility,
283
+ rowSelection,
284
+ },
285
+ });
286
+
287
+ const leftStickyColumns = useMemo<StickyColumnProps[]>(() => {
288
+ const columns: StickyColumnProps[] = [];
289
+ if (checkbox) {
290
+ columns.push({
291
+ key: "select",
292
+ position: "left",
293
+ size: 40,
294
+ });
295
+ }
296
+ columns.push(...stickyColumns.filter(({ position }) => position === "left"));
297
+ return columns;
298
+ }, [stickyColumns, checkbox]);
299
+
300
+ const rightStickyColumns = useMemo<StickyColumnProps[]>(() => {
301
+ const columns: StickyColumnProps[] = [];
302
+ columns.push(...stickyColumns.filter(({ position }) => position === "right"));
303
+ if (hasActions(rowActions!)) {
304
+ columns.push({
305
+ key: "actions",
306
+ position: "right",
307
+ size: 40,
308
+ });
309
+ }
310
+ return columns;
311
+ }, [stickyColumns, rowActions]);
312
+
313
+ const columnSettings = useMemo<DropdownMenuItemProps[]>(() => {
314
+ return table
315
+ .getAllColumns()
316
+ .filter((column) => column.getCanHide())
317
+ .map((column) => {
318
+ // biome-ignore lint/suspicious/noExplicitAny: <column>
319
+ const item = columns.find((col: any) => {
320
+ const key = col.id || col.accessorKey;
321
+ return key.replace(/\./g, "_") === column.id;
322
+ });
323
+ let label = column.id;
324
+ if (item) {
325
+ if (isFunction(item.header)) {
326
+ label = item.header({ table, column, header: table.getFlatHeaders().find((col) => col.id === column.id)! });
327
+ } else if (isString(item.header)) {
328
+ label = item.header;
329
+ }
330
+ }
331
+ const checked = columnVisibility[column.id];
332
+ return {
333
+ id: column.id,
334
+ label,
335
+ type: "checkbox",
336
+ checked: isUndefined(checked) ? column.getIsVisible() : checked,
337
+ onCheckedChange: (_item, value) => column.toggleVisibility(value),
338
+ };
339
+ });
340
+ }, [table, columns, columnVisibility]);
341
+
342
+ const showVisibilityControl = useMemo(
343
+ () => showColumnVisibility && columnSettings.length,
344
+ [showColumnVisibility, columnSettings],
345
+ );
346
+
347
+ const showToolbar = useMemo(() => filter || showVisibilityControl, [filter, showVisibilityControl]);
348
+
349
+ const showPagination = useMemo(() => {
350
+ if (autoHidePagination) {
351
+ return pagination && (pagination as PaginationProps).total > (pagination as PaginationProps).size;
352
+ }
353
+ return pagination;
354
+ }, [pagination, autoHidePagination]);
355
+
356
+ return (
357
+ <>
358
+ {showToolbar ? (
359
+ <div
360
+ className={cn(
361
+ "border-0 border-secondary border-b border-solid pb-2",
362
+ "flex items-end",
363
+ filter ? "justify-between" : "justify-end",
364
+ )}
365
+ >
366
+ {filter ? <Filters {...filter} load={load} loading={loading} /> : null}
367
+ {showVisibilityControl ? (
368
+ <Dropdown align="end" asChild={true} items={columnSettings}>
369
+ <Action className="!p-0 h-9 w-9">
370
+ <LayoutIcon className="h-[18px] w-[18px]" />
371
+ </Action>
372
+ </Dropdown>
373
+ ) : null}
374
+ </div>
375
+ ) : null}
376
+ <div className={cn("relative")}>
377
+ <Table className={classNames("data-table", inCard ? "in-card" : null, !mounted && "invisible")}>
378
+ <TableHeader className={cn(!showHeader && "hidden")}>
379
+ {table.getHeaderGroups().map((headerGroup) => (
380
+ <TableRow key={headerGroup.id}>
381
+ {headerGroup.headers.map((header) => {
382
+ // 在未挂载时禁用粘性列功能,避免 SSR 水合错误
383
+ const sticky = mounted
384
+ ? getSticky(header.column.id, leftStickyColumns, rightStickyColumns)
385
+ : { enable: false };
386
+ const content = header.isPlaceholder
387
+ ? null
388
+ : flexRender(header.column.columnDef.header, header.getContext());
389
+ return (
390
+ <TableHead
391
+ className={cn(
392
+ sticky.enable ? "table-sticky-col sticky" : null,
393
+ sticky.last ? "table-sticky-col-last" : null,
394
+ sticky.first ? "table-sticky-col-first" : null,
395
+ // biome-ignore lint/suspicious/noExplicitAny: <className>
396
+ (header.column.columnDef as any).className,
397
+ )}
398
+ key={header.id}
399
+ style={
400
+ sticky.enable
401
+ ? {
402
+ zIndex: 10,
403
+ minWidth: sticky.width,
404
+ [sticky.position as string]: sticky.offset,
405
+ }
406
+ : undefined
407
+ }
408
+ >
409
+ {sticky.enable ? <div className="inner flex h-10 items-center px-2">{content}</div> : content}
410
+ </TableHead>
411
+ );
412
+ })}
413
+ </TableRow>
414
+ ))}
415
+ </TableHeader>
416
+ <TableBody>
417
+ {table.getRowModel().rows?.length ? (
418
+ table.getRowModel().rows.map((row) => (
419
+ <TableRow
420
+ data-state={row.getIsSelected() && "selected"}
421
+ key={row.id}
422
+ onClick={() => props.onRowClick?.(row)}
423
+ >
424
+ {row.getVisibleCells().map((cell) => {
425
+ // 在未挂载时禁用粘性列功能,避免 SSR 水合错误
426
+ const sticky = mounted
427
+ ? getSticky(cell.column.id, leftStickyColumns, rightStickyColumns)
428
+ : { enable: false };
429
+ const ctx = cell.getContext();
430
+ const render = ctx.renderValue;
431
+ // biome-ignore lint/suspicious/noExplicitAny: <formatters>
432
+ const formatters = (cell.column.columnDef as any).formatters || [];
433
+ ctx.renderValue = () => {
434
+ return formatValue(render(), formatters, cellHandles);
435
+ };
436
+ const content = flexRender(cell.column.columnDef.cell, ctx);
437
+ return (
438
+ <TableCell
439
+ className={cn(
440
+ sticky.enable ? "table-sticky-col sticky" : null,
441
+ sticky.last ? "table-sticky-col-last" : null,
442
+ sticky.first ? "table-sticky-col-first" : null,
443
+ )}
444
+ key={cell.id}
445
+ style={
446
+ sticky.enable
447
+ ? {
448
+ zIndex: 10,
449
+ minWidth: sticky.width,
450
+ [sticky.position as string]: sticky.offset,
451
+ }
452
+ : undefined
453
+ }
454
+ >
455
+ {sticky.enable ? <div className="inner flex h-10 items-center px-2">{content}</div> : content}
456
+ </TableCell>
457
+ );
458
+ })}
459
+ </TableRow>
460
+ ))
461
+ ) : (
462
+ <TableRow>
463
+ <TableCell className="h-24 text-center" colSpan={tableColumns.length}>
464
+ {empty}
465
+ </TableCell>
466
+ </TableRow>
467
+ )}
468
+ </TableBody>
469
+ </Table>
470
+ <div className={cn("py-4", !mounted && "invisible")}>
471
+ {showPagination && (
472
+ <Pagination
473
+ {...(pagination as PaginationProps)}
474
+ onChange={(page: number) => {
475
+ load?.({ page });
476
+ }}
477
+ onSizeChange={(size: number) => {
478
+ load?.({ size, page: 1 });
479
+ }}
480
+ />
481
+ )}
482
+ </div>
483
+ {loading || !mounted ? (
484
+ <div className="dark:!bg-black/5 absolute top-0 right-0 bottom-0 left-0 z-50 flex items-center justify-center bg-white/50">
485
+ <Spin />
486
+ </div>
487
+ ) : null}
488
+ </div>
489
+ </>
490
+ );
491
+ }
@@ -0,0 +1,40 @@
1
+ @import "../../../assets/style/theme.css";
2
+
3
+ .data-table {
4
+ --sticky-row-hover-color: rgba(250, 250, 250, 1);
5
+ }
6
+
7
+ .dark .data-table {
8
+ --sticky-row-hover-color: #1c1c1c;
9
+ }
10
+ .dark .data-table.in-card {
11
+ --sticky-row-hover-color: #181818;
12
+ }
13
+
14
+ .data-table tr {
15
+ @apply transition-none;
16
+ }
17
+
18
+ .data-table tr th.table-sticky-col,
19
+ .data-table tr td.table-sticky-col {
20
+ @apply p-0;
21
+ }
22
+
23
+ .data-table tr th.table-sticky-col .inner,
24
+ .data-table tr td.table-sticky-col .inner {
25
+ @apply bg-background;
26
+ }
27
+
28
+ .data-table.in-card tr th.table-sticky-col .inner,
29
+ .data-table.in-card tr td.table-sticky-col .inner {
30
+ @apply bg-card;
31
+ }
32
+
33
+ .data-table tr:hover th.table-sticky-col .inner,
34
+ .data-table tr:hover td.table-sticky-col .inner {
35
+ @apply bg-[var(--sticky-row-hover-color)];
36
+ }
37
+
38
+ .data-table tr[data-state="selected"] td.table-sticky-col .inner {
39
+ @apply bg-[hsl(var(--muted))];
40
+ }
@@ -0,0 +1,88 @@
1
+ import { forwardRef, useContext, useState } from "react";
2
+ import { Cross2Icon } from "@radix-ui/react-icons";
3
+ import { addDays, format } from "date-fns";
4
+ import get from "lodash/get";
5
+ import { Calendar as CalendarIcon } from "lucide-react";
6
+
7
+ import { Button, Calendar, cn, Popover, PopoverContent, PopoverTrigger, Select } from "@meta-1/design";
8
+ import { UIXContext } from "@meta-1/design/components/uix/config-provider";
9
+
10
+ export type DatePickerProps = {
11
+ placeholder?: string;
12
+ format?: string;
13
+ preset?: boolean;
14
+ className?: string;
15
+ allowClear?: boolean;
16
+ };
17
+
18
+ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>((props, _ref) => {
19
+ const { preset = false, allowClear = false } = props;
20
+ const [date, setDate] = useState<Date>();
21
+ const [presetValue, setPresetValue] = useState<string>("");
22
+
23
+ const config = useContext(UIXContext);
24
+ const locale = get(config.locale, "DatePicker.locale");
25
+ const formatConfig = props.format || get(config.locale, "DatePicker.format") || "yyyy-MM-dd";
26
+ const options = get(config.locale, "DatePicker.options");
27
+
28
+ const calendar = (
29
+ <Calendar
30
+ locale={locale}
31
+ mode="single"
32
+ onSelect={(v) => {
33
+ setDate(v);
34
+ setPresetValue("");
35
+ }}
36
+ selected={date}
37
+ />
38
+ );
39
+
40
+ return (
41
+ <Popover>
42
+ <PopoverTrigger asChild>
43
+ <Button
44
+ className={cn(
45
+ "group w-full justify-start space-x-1 text-left font-normal",
46
+ !date && "text-muted-foreground",
47
+ props.className,
48
+ )}
49
+ variant="outline"
50
+ >
51
+ <CalendarIcon className="mr-2 h-4 w-4" />
52
+ <span className="flex-1">{date ? format(date, formatConfig) : props.placeholder}</span>
53
+ {allowClear && date ? (
54
+ <Cross2Icon
55
+ className="hidden group-hover:block"
56
+ onClick={(e) => {
57
+ setDate(undefined);
58
+ setPresetValue("");
59
+ e.stopPropagation();
60
+ }}
61
+ />
62
+ ) : null}
63
+ </Button>
64
+ </PopoverTrigger>
65
+ <PopoverContent align="start" className={cn("flex w-fit", preset ? "flex-col space-y-2 p-2" : "p-0")}>
66
+ {preset ? (
67
+ <>
68
+ <Select
69
+ className="w-full"
70
+ onChange={(value) => {
71
+ setDate(addDays(new Date(), Number.parseInt(value, 10)));
72
+ setPresetValue(value);
73
+ }}
74
+ options={options || []}
75
+ placeholder="请选择"
76
+ value={presetValue}
77
+ />
78
+ <div className="rounded-md border">{calendar}</div>
79
+ </>
80
+ ) : (
81
+ calendar
82
+ )}
83
+ </PopoverContent>
84
+ </Popover>
85
+ );
86
+ });
87
+
88
+ DatePicker.displayName = "DatePicker";
@@ -0,0 +1,71 @@
1
+ import { forwardRef, type HTMLAttributes, useContext, useState } from "react";
2
+ import { format } from "date-fns";
3
+ import get from "lodash/get";
4
+ import { Calendar as CalendarIcon } from "lucide-react";
5
+ import type { DateRange } from "react-day-picker";
6
+
7
+ import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "@meta-1/design";
8
+ import { UIXContext } from "@meta-1/design/components/uix/config-provider";
9
+ import { cn } from "@meta-1/design/lib";
10
+
11
+ export type DateRangePickerProps = {
12
+ value?: DateRange;
13
+ onChange?: (value: DateRange) => void;
14
+ placeholder?: string;
15
+ format?: string;
16
+ } & HTMLAttributes<HTMLDivElement>;
17
+
18
+ export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((props, forwardedRef) => {
19
+ const { className, value, onChange } = props;
20
+ const elementRef = forwardedRef;
21
+
22
+ const config = useContext(UIXContext);
23
+ const locale = get(config.locale, "DateRangePicker.locale");
24
+ const formatConfig = props.format || get(config.locale, "DateRangePicker.format") || "yyyy-MM-dd";
25
+
26
+ const [date, setDate] = useState<DateRange | undefined>(value);
27
+
28
+ return (
29
+ <div className={cn("grid gap-2", className)} ref={elementRef}>
30
+ <Popover>
31
+ <PopoverTrigger asChild>
32
+ <Button
33
+ className={cn("justify-start text-left font-normal", !date && "text-muted-foreground")}
34
+ id="date"
35
+ type="button"
36
+ variant="outline"
37
+ >
38
+ <CalendarIcon className="mr-2 h-4 w-4" />
39
+ {date?.from ? (
40
+ date.to ? (
41
+ <>
42
+ {format(date.from, formatConfig)} - {format(date.to, formatConfig)}
43
+ </>
44
+ ) : (
45
+ format(date.from, formatConfig)
46
+ )
47
+ ) : (
48
+ <span>{props.placeholder}</span>
49
+ )}
50
+ </Button>
51
+ </PopoverTrigger>
52
+ <PopoverContent align="start" className="w-auto p-0">
53
+ <Calendar
54
+ defaultMonth={date?.from}
55
+ initialFocus
56
+ locale={locale}
57
+ mode="range"
58
+ numberOfMonths={2}
59
+ onSelect={(v) => {
60
+ setDate(v);
61
+ onChange?.(v!);
62
+ }}
63
+ selected={date}
64
+ />
65
+ </PopoverContent>
66
+ </Popover>
67
+ </div>
68
+ );
69
+ });
70
+
71
+ DateRangePicker.displayName = "DateRangePicker";