periplo-ui 3.16.0 → 3.18.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.
- package/dist/components/Badge/Badge.d.ts +7 -2
- package/dist/components/Badge/Badge.js +48 -18
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/DataTable/DataTable.d.ts +6 -2
- package/dist/components/DataTable/DataTable.js +17 -3
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +66 -25
- package/dist/components/DatePicker/DatePicker.js +85 -37
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/lib/dateUtils.d.ts +41 -0
- package/dist/lib/dateUtils.js +50 -0
- package/dist/lib/dateUtils.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { VariantProps } from 'class-variance-authority';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
declare const badgeVariants: (props?: ({
|
|
4
|
-
|
|
4
|
+
color?: "neutral" | "primary" | "accent" | "success" | "warning" | "error" | null | undefined;
|
|
5
5
|
size?: "sm" | "md" | "lg" | null | undefined;
|
|
6
6
|
position?: "top-right" | "top-left" | "bottom-right" | "bottom-left" | null | undefined;
|
|
7
|
+
numeric?: boolean | null | undefined;
|
|
7
8
|
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
8
9
|
export interface BadgeProps extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof badgeVariants> {
|
|
9
10
|
/**
|
|
@@ -15,7 +16,7 @@ export interface BadgeProps extends React.HTMLAttributes<HTMLElement>, VariantPr
|
|
|
15
16
|
* The color variant of the badge
|
|
16
17
|
* @default 'accent'
|
|
17
18
|
*/
|
|
18
|
-
|
|
19
|
+
color?: 'neutral' | 'accent' | 'primary' | 'success' | 'warning' | 'error';
|
|
19
20
|
/**
|
|
20
21
|
* The size of the badge
|
|
21
22
|
* @default 'md'
|
|
@@ -26,6 +27,10 @@ export interface BadgeProps extends React.HTMLAttributes<HTMLElement>, VariantPr
|
|
|
26
27
|
* @default 'top-right'
|
|
27
28
|
*/
|
|
28
29
|
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
30
|
+
/**
|
|
31
|
+
* The number to display in the badge)
|
|
32
|
+
*/
|
|
33
|
+
count?: number;
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
31
36
|
* A status indicator component that can be used to show notifications,
|
|
@@ -9,17 +9,19 @@ const badgeVariants = cva("rounded-full", {
|
|
|
9
9
|
* Controls the color scheme of the badge
|
|
10
10
|
* @default 'accent'
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
color: {
|
|
13
13
|
/** Default gray appearance */
|
|
14
|
-
neutral: "bg-neutral",
|
|
14
|
+
neutral: "bg-neutral text-neutral-foreground",
|
|
15
|
+
/** Accent brand color */
|
|
16
|
+
accent: "bg-accent text-accent-foreground",
|
|
15
17
|
/** Primary brand color */
|
|
16
|
-
|
|
18
|
+
primary: "bg-primary text-primary-foreground",
|
|
17
19
|
/** Indicates successful or positive state */
|
|
18
|
-
success: "bg-success",
|
|
20
|
+
success: "bg-success text-success-foreground ",
|
|
19
21
|
/** Indicates warning or cautionary state */
|
|
20
|
-
warning: "bg-warning",
|
|
22
|
+
warning: "bg-warning text-warning-foreground",
|
|
21
23
|
/** Indicates error or critical state */
|
|
22
|
-
error: "bg-error"
|
|
24
|
+
error: "bg-error text-error-foreground"
|
|
23
25
|
},
|
|
24
26
|
/**
|
|
25
27
|
* Controls the size of the badge
|
|
@@ -46,30 +48,58 @@ const badgeVariants = cva("rounded-full", {
|
|
|
46
48
|
"bottom-right": "right-0 bottom-0",
|
|
47
49
|
/** Places the badge in the bottom-left corner */
|
|
48
50
|
"bottom-left": "left-0 bottom-0"
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Controls whether the badge displays a number
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
numeric: {
|
|
57
|
+
true: "min-w-[16px] h-4 px-1 flex items-center justify-center text-[10px] font-medium",
|
|
58
|
+
false: ""
|
|
49
59
|
}
|
|
50
60
|
},
|
|
51
61
|
defaultVariants: {
|
|
52
|
-
|
|
62
|
+
color: "primary",
|
|
53
63
|
size: "md",
|
|
54
|
-
position: "top-right"
|
|
64
|
+
position: "top-right",
|
|
65
|
+
numeric: false
|
|
55
66
|
}
|
|
56
67
|
});
|
|
57
68
|
const Badge = React.forwardRef(
|
|
58
|
-
({ className,
|
|
69
|
+
({ className, color = "accent", size = "md", position = "top-right", ping = true, count, ...props }, ref) => {
|
|
59
70
|
const sizeClasses = {
|
|
60
71
|
sm: "h-2 w-2",
|
|
61
72
|
md: "h-3 w-3",
|
|
62
73
|
lg: "h-4 w-4"
|
|
63
74
|
}[size || "md"];
|
|
64
|
-
return /* @__PURE__ */ jsxs(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
return /* @__PURE__ */ jsxs(
|
|
76
|
+
"span",
|
|
77
|
+
{
|
|
78
|
+
className: cn(
|
|
79
|
+
"absolute flex",
|
|
80
|
+
badgeVariants({ color, position, numeric: !!count }),
|
|
81
|
+
!count && sizeClasses,
|
|
82
|
+
className
|
|
83
|
+
),
|
|
84
|
+
ref,
|
|
85
|
+
...props,
|
|
86
|
+
children: [
|
|
87
|
+
ping && /* @__PURE__ */ jsx(
|
|
88
|
+
"span",
|
|
89
|
+
{
|
|
90
|
+
className: cn(badgeVariants({ color }), "absolute inline-flex h-full w-full animate-ping opacity-75")
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
/* @__PURE__ */ jsx(
|
|
94
|
+
"span",
|
|
95
|
+
{
|
|
96
|
+
className: cn(badgeVariants({ color }), "relative inline-flex h-full w-full items-center justify-center"),
|
|
97
|
+
children: count
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
);
|
|
73
103
|
}
|
|
74
104
|
);
|
|
75
105
|
Badge.displayName = "Badge";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.js","sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\nimport { cn } from '../../lib/utils'\n\nconst badgeVariants = cva('rounded-full', {\n variants: {\n /**\n * Controls the color scheme of the badge\n * @default 'accent'\n */\n
|
|
1
|
+
{"version":3,"file":"Badge.js","sources":["../../../src/components/Badge/Badge.tsx"],"sourcesContent":["import { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\nimport { cn } from '../../lib/utils'\n\nconst badgeVariants = cva('rounded-full', {\n variants: {\n /**\n * Controls the color scheme of the badge\n * @default 'accent'\n */\n color: {\n /** Default gray appearance */\n neutral: 'bg-neutral text-neutral-foreground',\n /** Accent brand color */\n accent: 'bg-accent text-accent-foreground',\n /** Primary brand color */\n primary: 'bg-primary text-primary-foreground',\n /** Indicates successful or positive state */\n success: 'bg-success text-success-foreground ',\n /** Indicates warning or cautionary state */\n warning: 'bg-warning text-warning-foreground',\n /** Indicates error or critical state */\n error: 'bg-error text-error-foreground',\n },\n /**\n * Controls the size of the badge\n * @default 'md'\n */\n size: {\n /** Small size - 8px */\n sm: 'h-2 w-2',\n /** Medium size - 12px */\n md: 'h-3 w-3',\n /** Large size - 16px */\n lg: 'h-4 w-4',\n },\n /**\n * Controls the position of the badge relative to its container\n * @default 'top-right'\n */\n position: {\n /** Places the badge in the top-right corner */\n 'top-right': 'right-0 top-0',\n /** Places the badge in the top-left corner */\n 'top-left': 'left-0 top-0',\n /** Places the badge in the bottom-right corner */\n 'bottom-right': 'right-0 bottom-0',\n /** Places the badge in the bottom-left corner */\n 'bottom-left': 'left-0 bottom-0',\n },\n /**\n * Controls whether the badge displays a number\n * @default false\n */\n numeric: {\n true: 'min-w-[16px] h-4 px-1 flex items-center justify-center text-[10px] font-medium',\n false: '',\n },\n },\n defaultVariants: {\n color: 'primary',\n size: 'md',\n position: 'top-right',\n numeric: false,\n },\n})\n\nexport interface BadgeProps extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof badgeVariants> {\n /**\n * Controls the visibility of the pulsing animation effect\n * @default true\n */\n ping?: boolean\n\n /**\n * The color variant of the badge\n * @default 'accent'\n */\n color?: 'neutral' | 'accent' | 'primary' | 'success' | 'warning' | 'error'\n\n /**\n * The size of the badge\n * @default 'md'\n */\n size?: 'sm' | 'md' | 'lg'\n\n /**\n * The position of the badge relative to its container\n * @default 'top-right'\n */\n position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'\n\n /**\n * The number to display in the badge)\n */\n count?: number\n}\n\n/**\n * A status indicator component that can be used to show notifications,\n * status, or draw attention to an element.\n *\n * @example\n * ```tsx\n * // Simple notification badge\n * <div className=\"relative\">\n * <Bell className=\"h-6 w-6\" />\n * <Badge intent=\"error\" />\n * </div>\n *\n * // Custom size and position\n * <div className=\"relative\">\n * <Avatar />\n * <Badge\n * intent=\"success\"\n * size=\"sm\"\n * position=\"bottom-right\"\n * ping={false}\n * />\n * </div>\n * ```\n */\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n ({ className, color = 'accent', size = 'md', position = 'top-right', ping = true, count, ...props }, ref) => {\n const sizeClasses = {\n sm: 'h-2 w-2',\n md: 'h-3 w-3',\n lg: 'h-4 w-4',\n }[size || 'md']\n\n return (\n <span\n className={cn(\n 'absolute flex',\n badgeVariants({ color, position, numeric: !!count }),\n !count && sizeClasses,\n className,\n )}\n ref={ref}\n {...props}\n >\n {ping && (\n <span\n className={cn(badgeVariants({ color }), 'absolute inline-flex h-full w-full animate-ping opacity-75')}\n />\n )}\n <span\n className={cn(badgeVariants({ color }), 'relative inline-flex h-full w-full items-center justify-center')}\n >\n {count}\n </span>\n </span>\n )\n },\n)\n\nBadge.displayName = 'Badge'\n\nexport { Badge, badgeVariants }\n"],"names":[],"mappings":";;;;;AAIM,MAAA,aAAA,GAAgB,IAAI,cAAgB,EAAA;AAAA,EACxC,QAAU,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKR,KAAO,EAAA;AAAA;AAAA,MAEL,OAAS,EAAA,oCAAA;AAAA;AAAA,MAET,MAAQ,EAAA,kCAAA;AAAA;AAAA,MAER,OAAS,EAAA,oCAAA;AAAA;AAAA,MAET,OAAS,EAAA,sCAAA;AAAA;AAAA,MAET,OAAS,EAAA,oCAAA;AAAA;AAAA,MAET,KAAO,EAAA;AAAA,KACT;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,IAAM,EAAA;AAAA;AAAA,MAEJ,EAAI,EAAA,SAAA;AAAA;AAAA,MAEJ,EAAI,EAAA,SAAA;AAAA;AAAA,MAEJ,EAAI,EAAA;AAAA,KACN;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,QAAU,EAAA;AAAA;AAAA,MAER,WAAa,EAAA,eAAA;AAAA;AAAA,MAEb,UAAY,EAAA,cAAA;AAAA;AAAA,MAEZ,cAAgB,EAAA,kBAAA;AAAA;AAAA,MAEhB,aAAe,EAAA;AAAA,KACjB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,OAAS,EAAA;AAAA,MACP,IAAM,EAAA,gFAAA;AAAA,MACN,KAAO,EAAA;AAAA;AACT,GACF;AAAA,EACA,eAAiB,EAAA;AAAA,IACf,KAAO,EAAA,SAAA;AAAA,IACP,IAAM,EAAA,IAAA;AAAA,IACN,QAAU,EAAA,WAAA;AAAA,IACV,OAAS,EAAA;AAAA;AAEb,CAAC;AAyDD,MAAM,QAAQ,KAAM,CAAA,UAAA;AAAA,EAClB,CAAC,EAAE,SAAW,EAAA,KAAA,GAAQ,UAAU,IAAO,GAAA,IAAA,EAAM,QAAW,GAAA,WAAA,EAAa,OAAO,IAAM,EAAA,KAAA,EAAO,GAAG,KAAA,IAAS,GAAQ,KAAA;AAC3G,IAAA,MAAM,WAAc,GAAA;AAAA,MAClB,EAAI,EAAA,SAAA;AAAA,MACJ,EAAI,EAAA,SAAA;AAAA,MACJ,EAAI,EAAA;AAAA,KACN,CAAE,QAAQ,IAAI,CAAA;AAEd,IACE,uBAAA,IAAA;AAAA,MAAC,MAAA;AAAA,MAAA;AAAA,QACC,SAAW,EAAA,EAAA;AAAA,UACT,eAAA;AAAA,UACA,aAAA,CAAc,EAAE,KAAO,EAAA,QAAA,EAAU,SAAS,CAAC,CAAC,OAAO,CAAA;AAAA,UACnD,CAAC,KAAS,IAAA,WAAA;AAAA,UACV;AAAA,SACF;AAAA,QACA,GAAA;AAAA,QACC,GAAG,KAAA;AAAA,QAEH,QAAA,EAAA;AAAA,UACC,IAAA,oBAAA,GAAA;AAAA,YAAC,MAAA;AAAA,YAAA;AAAA,cACC,WAAW,EAAG,CAAA,aAAA,CAAc,EAAE,KAAM,EAAC,GAAG,4DAA4D;AAAA;AAAA,WACtG;AAAA,0BAEF,GAAA;AAAA,YAAC,MAAA;AAAA,YAAA;AAAA,cACC,WAAW,EAAG,CAAA,aAAA,CAAc,EAAE,KAAM,EAAC,GAAG,gEAAgE,CAAA;AAAA,cAEvG,QAAA,EAAA;AAAA;AAAA;AACH;AAAA;AAAA,KACF;AAAA;AAGN;AAEA,KAAA,CAAM,WAAc,GAAA,OAAA;;;;"}
|
|
@@ -54,8 +54,12 @@ export type DataTableProps<TData> = {
|
|
|
54
54
|
readonly primaryFilters?: React.ReactNode;
|
|
55
55
|
/** Secondary filters that appear in the filters dropdown */
|
|
56
56
|
readonly secondaryFilters?: React.ReactNode;
|
|
57
|
+
/** Number of active primary filters */
|
|
58
|
+
readonly activePrimaryFiltersCount?: number;
|
|
59
|
+
/** Number of active secondary filters */
|
|
60
|
+
readonly activeSecondaryFiltersCount?: number;
|
|
57
61
|
/** Text customization for filters */
|
|
58
|
-
readonly
|
|
62
|
+
readonly labels?: {
|
|
59
63
|
/** Text for the column visibility button (default: "Hide columns") */
|
|
60
64
|
columnVisibilityButton?: string;
|
|
61
65
|
/** Text for the filters button when only secondary filters are present (default: "Filters") */
|
|
@@ -115,5 +119,5 @@ type RowIdentifierFn<T> = (row: T) => string;
|
|
|
115
119
|
* <DataTable columns={columns} data={data} />
|
|
116
120
|
* ```
|
|
117
121
|
*/
|
|
118
|
-
export declare function DataTable<TData extends object = any>({ columns: userColumns, data, getRowId, showColumnVisibilityControls, isLoading, pagination, primaryFilters, secondaryFilters,
|
|
122
|
+
export declare function DataTable<TData extends object = any>({ columns: userColumns, data, getRowId, showColumnVisibilityControls, isLoading, pagination, primaryFilters, secondaryFilters, activePrimaryFiltersCount, activeSecondaryFiltersCount, labels, onSelectAll, onSelect, className, tableClassName, }: DataTableProps<TData>): import("react/jsx-runtime").JSX.Element;
|
|
119
123
|
export {};
|
|
@@ -13,6 +13,7 @@ import { cn } from '../../lib/utils.js';
|
|
|
13
13
|
import { Typography } from '../Typography/Typography.js';
|
|
14
14
|
import { PopoverRoot, PopoverTrigger, PopoverContent } from '../Popover/Popover.js';
|
|
15
15
|
import { useIsMobile } from '../../lib/useMobile.js';
|
|
16
|
+
import { Badge } from '../Badge/Badge.js';
|
|
16
17
|
|
|
17
18
|
function DataTable({
|
|
18
19
|
columns: userColumns,
|
|
@@ -23,7 +24,9 @@ function DataTable({
|
|
|
23
24
|
pagination,
|
|
24
25
|
primaryFilters,
|
|
25
26
|
secondaryFilters,
|
|
26
|
-
|
|
27
|
+
activePrimaryFiltersCount = 0,
|
|
28
|
+
activeSecondaryFiltersCount = 0,
|
|
29
|
+
labels = {
|
|
27
30
|
columnVisibilityButton: "Hide columns",
|
|
28
31
|
filters: "Filters",
|
|
29
32
|
moreFilters: "More filters"
|
|
@@ -158,14 +161,25 @@ function DataTable({
|
|
|
158
161
|
!isMobile && (primaryFilters ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: primaryFilters }) : /* @__PURE__ */ jsx("div", {})),
|
|
159
162
|
/* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", isMobile ? "w-full justify-end" : ""), children: [
|
|
160
163
|
(isMobile ? primaryFilters || secondaryFilters : secondaryFilters) && /* @__PURE__ */ jsxs(PopoverRoot, { children: [
|
|
161
|
-
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "text", size: "sm", StartIcon: FunnelSimple, className: "relative whitespace-nowrap", children: [
|
|
165
|
+
(isMobile && activePrimaryFiltersCount + activeSecondaryFiltersCount > 0 || !isMobile && activeSecondaryFiltersCount > 0) && /* @__PURE__ */ jsx(
|
|
166
|
+
Badge,
|
|
167
|
+
{
|
|
168
|
+
count: isMobile ? activePrimaryFiltersCount + activeSecondaryFiltersCount : activeSecondaryFiltersCount,
|
|
169
|
+
color: "primary",
|
|
170
|
+
ping: false,
|
|
171
|
+
className: "absolute -right-1 -top-1"
|
|
172
|
+
}
|
|
173
|
+
),
|
|
174
|
+
!primaryFilters || isMobile ? labels.filters : labels.moreFilters
|
|
175
|
+
] }) }),
|
|
162
176
|
/* @__PURE__ */ jsx(PopoverContent, { align: "center", className: "w-fit p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
163
177
|
isMobile && primaryFilters && primaryFilters,
|
|
164
178
|
secondaryFilters
|
|
165
179
|
] }) })
|
|
166
180
|
] }),
|
|
167
181
|
showColumnVisibilityControls && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
168
|
-
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", className: cn("whitespace-nowrap"), StartIcon: TextColumns, children:
|
|
182
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", className: cn("whitespace-nowrap"), StartIcon: TextColumns, children: labels.columnVisibilityButton ?? "Hide columns" }) }),
|
|
169
183
|
/* @__PURE__ */ jsxs(DropdownMenuContent, { children: [
|
|
170
184
|
/* @__PURE__ */ jsx(
|
|
171
185
|
DropdownMenuCheckboxItem,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.js","sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["'use client'\n\nimport {\n ColumnDef,\n flexRender,\n getCoreRowModel,\n getPaginationRowModel,\n useReactTable,\n VisibilityState,\n} from '@tanstack/react-table'\nimport * as React from 'react'\n\nimport { Button } from '../Button'\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../DropdownMenu'\nimport { Skeleton } from '../Skeleton'\nimport { Table as TableComponent, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../Table'\nimport { DataTablePagination } from './DataTablePagination'\nimport { Checkbox } from '../Checkbox'\nimport { TextColumns, FunnelSimple } from '@phosphor-icons/react'\nimport { cn } from '@/lib/utils'\nimport { Typography } from '../Typography'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { useIsMobile } from '@/lib/useMobile'\n\ntype BasePaginationProps = {\n /** Number of rows per page */\n readonly pageSize: number\n /** Whether the pagination is in a loading state */\n readonly isLoading?: boolean\n /** Text customization for pagination */\n readonly labels?: {\n /** Text shown before the page size number (default: \"Showing\") */\n showing?: string\n /** Text shown before the total number (default: \"of\") */\n of?: string\n /** Text shown after the total number (default: \"results\") */\n results?: string\n /** Aria label for previous page button (default: \"Previous page\") */\n previousPage?: string\n /** Aria label for next page button (default: \"Next page\") */\n nextPage?: string\n /** Aria label for page number (default: \"Page {number}\") */\n pageLabel?: string\n }\n}\n\ntype BackendPaginationProps = BasePaginationProps & {\n /** Current page */\n readonly currentPage: number\n /** Total number of items */\n readonly total: number\n /** Callback when page changes */\n readonly onPageChange: (page: number) => void\n}\n\n/**\n * Type helper to check if a type has an 'id' property\n */\ntype HasId<T> = T extends { id: string | number } ? true : false\n\n/**\n * Props for the DataTable component\n * @template TData The type of data being displayed in the table\n */\nexport type DataTableProps<TData> = {\n /** Array of column definitions that describe the table structure */\n readonly columns: Array<ColumnDef<TData>>\n /** Array of data items to be displayed in the table */\n readonly data: Array<TData>\n /** Whether to show the column visibility toggle menu */\n readonly showColumnVisibilityControls?: boolean\n /** Whether the table is in a loading state */\n readonly isLoading?: boolean\n /** Pagination configuration. If not provided, pagination is disabled */\n readonly pagination?: BasePaginationProps | BackendPaginationProps\n /** Primary filters that appear directly above the table */\n readonly primaryFilters?: React.ReactNode\n /** Secondary filters that appear in the filters dropdown */\n readonly secondaryFilters?: React.ReactNode\n /** Text customization for filters */\n readonly customLabels?: {\n /** Text for the column visibility button (default: \"Hide columns\") */\n columnVisibilityButton?: string\n /** Text for the filters button when only secondary filters are present (default: \"Filters\") */\n filters?: string\n /** Text for the more filters button when both primary and secondary filters are present (default: \"More filters\") */\n moreFilters?: string\n }\n /** Callback when all rows are selected */\n readonly onSelectAll?: (selected: boolean) => void\n /** Callback when a row is selected */\n readonly onSelect?: (selected: boolean, row: TData) => void\n /** Optional className for the table container */\n readonly className?: string\n /** Optional className for the table */\n readonly tableClassName?: string\n} & (HasId<TData> extends true\n ? {\n /** Function to get unique identifier from a row. Not needed when data has 'id' property */\n readonly getRowId?: never\n }\n : {\n /** Function to get unique identifier from a row. Required when data doesn't have 'id' property */\n readonly getRowId: RowIdentifierFn<TData>\n })\n\n/**\n * Function to get a unique identifier from a row\n */\ntype RowIdentifierFn<T> = (row: T) => string\n\n/**\n * A feature-rich data table component built on top of TanStack Table.\n * Provides sorting, filtering, pagination, and column visibility controls.\n *\n * @template TData The type of data being displayed in the table\n *\n * @example\n * ```tsx\n * type User = {\n * id: string;\n * name: string;\n * email: string;\n * };\n *\n * const columns: ColumnDef<User>[] = [\n * {\n * accessorKey: 'name',\n * header: 'Name',\n * },\n * {\n * accessorKey: 'email',\n * header: 'Email',\n * },\n * ];\n *\n * const data: User[] = [\n * { id: '1', name: 'John', email: 'john@example.com' },\n * { id: '2', name: 'Jane', email: 'jane@example.com' },\n * ];\n *\n * <DataTable columns={columns} data={data} />\n * ```\n */\nexport function DataTable<TData extends object = any>({\n columns: userColumns,\n data,\n getRowId = (row: TData) => (row as { id: string })?.id,\n showColumnVisibilityControls = true,\n isLoading = false,\n pagination,\n primaryFilters,\n secondaryFilters,\n customLabels = {\n columnVisibilityButton: 'Hide columns',\n filters: 'Filters',\n moreFilters: 'More filters',\n },\n onSelectAll,\n onSelect,\n className,\n tableClassName,\n}: DataTableProps<TData>) {\n const isMobile = useIsMobile()\n const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})\n const [isAllRowsSelected, setIsAllRowsSelected] = React.useState(false)\n const [deselectedRows, setDeselectedRows] = React.useState<Record<string, boolean>>({})\n const [selectedRows, setSelectedRows] = React.useState<Record<string, boolean>>({})\n const [pageIndex, setPageIndex] = React.useState(0)\n\n const isBackendPagination = pagination && 'onPageChange' in pagination\n const total = isBackendPagination ? pagination.total : data.length\n const pageSize = pagination?.pageSize ?? data.length\n const totalPages = Math.ceil(total / pageSize)\n\n const isSelectable = typeof onSelectAll === 'function' || typeof onSelect === 'function'\n\n const handleSelectAll = (checked: boolean) => {\n setIsAllRowsSelected(checked)\n setDeselectedRows({})\n setSelectedRows({})\n onSelectAll?.(checked)\n }\n\n const handleRowSelect = (checked: boolean, rowData: TData) => {\n const rowId = (getRowId as (row: TData) => string)(rowData)\n\n if (isAllRowsSelected) {\n setDeselectedRows((prev) => {\n const newDeselections = { ...prev }\n if (!checked) {\n newDeselections[rowId] = true\n } else {\n delete newDeselections[rowId]\n }\n return newDeselections\n })\n } else {\n setSelectedRows((prev) => {\n const newSelection = { ...prev }\n if (checked) {\n newSelection[rowId] = true\n } else {\n delete newSelection[rowId]\n }\n return newSelection\n })\n }\n\n onSelect?.(checked, rowData)\n }\n\n const rowSelection = React.useMemo(() => {\n if (isAllRowsSelected) {\n return Object.fromEntries(\n data.map((row) => [\n (getRowId as (row: TData) => string)(row),\n !deselectedRows[(getRowId as (row: TData) => string)(row)],\n ]),\n )\n }\n return selectedRows\n }, [data, isAllRowsSelected, deselectedRows, selectedRows, getRowId])\n\n const table = useReactTable({\n data,\n columns: userColumns,\n getCoreRowModel: getCoreRowModel(),\n getPaginationRowModel: pagination && !isBackendPagination ? getPaginationRowModel() : undefined,\n onColumnVisibilityChange: setColumnVisibility,\n enableRowSelection: isSelectable,\n getRowId: getRowId,\n state: {\n columnVisibility,\n rowSelection,\n pagination: pagination\n ? {\n pageIndex: isBackendPagination ? pagination.currentPage - 1 : pageIndex,\n pageSize,\n }\n : undefined,\n },\n manualPagination: isBackendPagination,\n onPaginationChange: isBackendPagination\n ? undefined\n : (updater) => {\n if (typeof updater === 'function') {\n const newState = updater({ pageIndex, pageSize })\n setPageIndex(newState.pageIndex)\n }\n },\n })\n\n const renderTableBody = () => {\n if (isLoading) {\n return Array.from({ length: pageSize ?? 10 }).map((_, rowIndex) => (\n <TableRow key={`skeleton-row-${rowIndex.toString()}`}>\n {isSelectable && (\n <TableCell className=\"w-[50px]\">\n <Checkbox checked={false} disabled />\n </TableCell>\n )}\n {table\n .getAllColumns()\n .filter((column) => column.getIsVisible())\n .map((column) => (\n <TableCell\n key={`skeleton-cell-${rowIndex.toString()}-${column.id}`}\n style={{ width: column.columnDef.size }}\n >\n <div className={cn('flex items-center justify-center', column.id === 'actions' && 'justify-end')}>\n <Skeleton className={cn(column.id === 'actions' ? 'h-8 w-8' : 'h-[20px] w-full')} />\n </div>\n </TableCell>\n ))}\n </TableRow>\n ))\n }\n\n if (data.length === 0) {\n return (\n <TableRow>\n <TableCell\n colSpan={isSelectable ? table.getAllColumns().length + 1 : table.getAllColumns().length}\n className=\"h-[200px] text-center\"\n >\n <Typography color=\"neutral\">No data available</Typography>\n </TableCell>\n </TableRow>\n )\n }\n\n return table.getRowModel().rows.map((row) => {\n const rowId = (getRowId as (row: TData) => string)(row.original)\n const isSelected = rowSelection[rowId] ?? false\n\n return (\n <TableRow key={rowId} data-selected={isSelected}>\n {isSelectable && (\n <TableCell className=\"w-[50px]\">\n <Checkbox\n checked={isSelected}\n onCheckedChange={(checked) => {\n const isChecked = checked === true\n handleRowSelect(isChecked, row.original)\n }}\n aria-label={`Select row ${rowId}`}\n />\n </TableCell>\n )}\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id} style={{ width: cell.column.columnDef.size }}>\n <Typography>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Typography>\n </TableCell>\n ))}\n </TableRow>\n )\n })\n }\n\n return (\n <div className={cn('flex h-full min-h-0 w-full flex-1 flex-col gap-2 overflow-hidden', className)}>\n {(showColumnVisibilityControls || primaryFilters || secondaryFilters) && (\n <div className=\"flex flex-shrink-0 items-end justify-between p-1\">\n {!isMobile && (primaryFilters ? <div className=\"flex items-center gap-2\">{primaryFilters}</div> : <div />)}\n <div className={cn('flex items-center gap-2', isMobile ? 'w-full justify-end' : '')}>\n {(isMobile ? primaryFilters || secondaryFilters : secondaryFilters) && (\n <PopoverRoot>\n <PopoverTrigger asChild>\n <Button variant=\"text\" size=\"sm\" StartIcon={FunnelSimple}>\n {!primaryFilters || isMobile ? customLabels.filters : customLabels.moreFilters}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"center\" className=\"w-fit p-4\">\n <div className=\"flex flex-col gap-4\">\n {isMobile && primaryFilters && primaryFilters}\n {secondaryFilters}\n </div>\n </PopoverContent>\n </PopoverRoot>\n )}\n {showColumnVisibilityControls && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"text\" size=\"sm\" className={cn('whitespace-nowrap')} StartIcon={TextColumns}>\n {customLabels.columnVisibilityButton ?? 'Hide columns'}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent>\n <DropdownMenuCheckboxItem\n key={'all-columns'}\n className=\"capitalize\"\n checked={table.getAllColumns().every((column) => column.getIsVisible())}\n onSelect={(event) => event.preventDefault()}\n onCheckedChange={(value) =>\n table.getAllColumns().forEach((column) => column.toggleVisibility(!!value))\n }\n >\n Select all\n </DropdownMenuCheckboxItem>\n <DropdownMenuSeparator className=\"bg-neutral-100\" />\n {table\n .getAllColumns()\n .filter((column) => column.getCanHide())\n .map((column) => {\n return (\n <DropdownMenuCheckboxItem\n key={column.id}\n className=\"capitalize\"\n checked={column.getIsVisible()}\n onSelect={(event) => event.preventDefault()}\n onCheckedChange={(value) => column.toggleVisibility(!!value)}\n >\n {column.columnDef.header?.toString()}\n </DropdownMenuCheckboxItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )}\n\n <div className=\"flex min-h-0 flex-1 flex-col rounded-md border bg-white\">\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <div className=\"h-full overflow-auto\">\n <TableComponent className=\"w-full\" tableClassName={cn('table-fixed', tableClassName)}>\n <TableHeader className=\"sticky top-0 z-10 bg-neutral-50\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {isSelectable && (\n <TableHead className=\"w-[50px]\">\n <Checkbox\n checked={isAllRowsSelected}\n onCheckedChange={handleSelectAll}\n disabled={isLoading || !data.length}\n aria-label=\"Select all rows\"\n />\n </TableHead>\n )}\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className=\"whitespace-normal\"\n style={{ width: header.column.columnDef.size }}\n >\n <Typography weight=\"medium\">\n {header.isPlaceholder\n ? null\n : flexRender(header.column.columnDef.header, header.getContext())}\n </Typography>\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>{renderTableBody()}</TableBody>\n </TableComponent>\n </div>\n </div>\n {!!pagination && (\n <div className=\"border-t px-4 py-2\">\n <DataTablePagination\n table={table}\n total={total}\n pageSize={pageSize}\n currentPage={\n isBackendPagination ? pagination?.currentPage : table.getState().pagination?.pageIndex + 1 || 1\n }\n totalPages={totalPages}\n onPageChange={isBackendPagination ? pagination?.onPageChange : undefined}\n isLoading={pagination?.isLoading}\n labels={pagination?.labels}\n />\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAsJO;AAA+C;AAC3C;AACT;AACoD;AACrB;AACnB;AACZ;AACA;AACA;AACe;AACW;AACf;AACI;AACf;AACA;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AAEA;AACE;AACA;AACA;AACA;AAAqB;AAGvB;AACE;AAEA;AACE;AACE;AACA;AACE;AAAyB;AAEzB;AAA4B;AAE9B;AAAO;AACR;AAED;AACE;AACA;AACE;AAAsB;AAEtB;AAAyB;AAE3B;AAAO;AACR;AAGH;AAA2B;AAG7B;AACE;AACE;AAAc;AACM;AACwB;AACiB;AAC1D;AACH;AAEF;AAAO;AAGT;AAA4B;AAC1B;AACS;AACwB;AACqD;AAC5D;AACN;AACpB;AACO;AACL;AACA;AAEI;AACgE;AAC9D;AAEF;AACN;AACkB;AAIZ;AACE;AACA;AAA+B;AACjC;AACF;AAGN;AACE;AACE;AAEK;AAGC;AAME;AAAC;AAAA;AAEuC;AAItC;AAAA;AALsD;AAOzD;AAEN;AAGH;AACE;AAEI;AAAC;AAAA;AACkF;AACvE;AAEmC;AAAA;AAEjD;AAIJ;AACE;AACA;AAEA;AAEK;AAEG;AAAC;AAAA;AACU;AAEP;AACA;AAAuC;AACzC;AAC+B;AAAA;AAEnC;AAMD;AACH;AAEH;AAGH;AAEM;AAEG;AAAsG;AAEnG;AAEE;AAIA;AAGK;AAA8B;AAC9B;AAEL;AACF;AAIE;AAIA;AAEE;AAAA;AAAC;AAAA;AAEW;AAC4D;AAC5B;AAEkC;AAE7E;AAAA;AAPM;AASP;AACkD;AAK9C;AACE;AAAC;AAAA;AAEW;AACmB;AACa;AACiB;AAExB;AAAA;AANvB;AAOd;AAEH;AACL;AACF;AAEJ;AACF;AAIA;AAGM;AAGO;AAEG;AAAC;AAAA;AACU;AACQ;AACY;AAClB;AAAA;AAEf;AAGA;AAAC;AAAA;AAEW;AACmC;AAM7C;AAAA;AARY;AAUf;AAGP;AAC8B;AAGpC;AAGI;AAAC;AAAA;AACC;AACA;AACA;AAEgG;AAEhG;AAC+D;AACxC;AACH;AAAA;AAExB;AAEJ;AAGN;;"}
|
|
1
|
+
{"version":3,"file":"DataTable.js","sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["'use client'\n\nimport {\n ColumnDef,\n flexRender,\n getCoreRowModel,\n getPaginationRowModel,\n useReactTable,\n VisibilityState,\n} from '@tanstack/react-table'\nimport * as React from 'react'\n\nimport { Button } from '../Button'\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../DropdownMenu'\nimport { Skeleton } from '../Skeleton'\nimport { Table as TableComponent, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../Table'\nimport { DataTablePagination } from './DataTablePagination'\nimport { Checkbox } from '../Checkbox'\nimport { TextColumns, FunnelSimple } from '@phosphor-icons/react'\nimport { cn } from '@/lib/utils'\nimport { Typography } from '../Typography'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { useIsMobile } from '@/lib/useMobile'\nimport { Badge } from '../Badge'\n\ntype BasePaginationProps = {\n /** Number of rows per page */\n readonly pageSize: number\n /** Whether the pagination is in a loading state */\n readonly isLoading?: boolean\n /** Text customization for pagination */\n readonly labels?: {\n /** Text shown before the page size number (default: \"Showing\") */\n showing?: string\n /** Text shown before the total number (default: \"of\") */\n of?: string\n /** Text shown after the total number (default: \"results\") */\n results?: string\n /** Aria label for previous page button (default: \"Previous page\") */\n previousPage?: string\n /** Aria label for next page button (default: \"Next page\") */\n nextPage?: string\n /** Aria label for page number (default: \"Page {number}\") */\n pageLabel?: string\n }\n}\n\ntype BackendPaginationProps = BasePaginationProps & {\n /** Current page */\n readonly currentPage: number\n /** Total number of items */\n readonly total: number\n /** Callback when page changes */\n readonly onPageChange: (page: number) => void\n}\n\n/**\n * Type helper to check if a type has an 'id' property\n */\ntype HasId<T> = T extends { id: string | number } ? true : false\n\n/**\n * Props for the DataTable component\n * @template TData The type of data being displayed in the table\n */\nexport type DataTableProps<TData> = {\n /** Array of column definitions that describe the table structure */\n readonly columns: Array<ColumnDef<TData>>\n /** Array of data items to be displayed in the table */\n readonly data: Array<TData>\n /** Whether to show the column visibility toggle menu */\n readonly showColumnVisibilityControls?: boolean\n /** Whether the table is in a loading state */\n readonly isLoading?: boolean\n /** Pagination configuration. If not provided, pagination is disabled */\n readonly pagination?: BasePaginationProps | BackendPaginationProps\n /** Primary filters that appear directly above the table */\n readonly primaryFilters?: React.ReactNode\n /** Secondary filters that appear in the filters dropdown */\n readonly secondaryFilters?: React.ReactNode\n /** Number of active primary filters */\n readonly activePrimaryFiltersCount?: number\n /** Number of active secondary filters */\n readonly activeSecondaryFiltersCount?: number\n /** Text customization for filters */\n readonly labels?: {\n /** Text for the column visibility button (default: \"Hide columns\") */\n columnVisibilityButton?: string\n /** Text for the filters button when only secondary filters are present (default: \"Filters\") */\n filters?: string\n /** Text for the more filters button when both primary and secondary filters are present (default: \"More filters\") */\n moreFilters?: string\n }\n /** Callback when all rows are selected */\n readonly onSelectAll?: (selected: boolean) => void\n /** Callback when a row is selected */\n readonly onSelect?: (selected: boolean, row: TData) => void\n /** Optional className for the table container */\n readonly className?: string\n /** Optional className for the table */\n readonly tableClassName?: string\n} & (HasId<TData> extends true\n ? {\n /** Function to get unique identifier from a row. Not needed when data has 'id' property */\n readonly getRowId?: never\n }\n : {\n /** Function to get unique identifier from a row. Required when data doesn't have 'id' property */\n readonly getRowId: RowIdentifierFn<TData>\n })\n\n/**\n * Function to get a unique identifier from a row\n */\ntype RowIdentifierFn<T> = (row: T) => string\n\n/**\n * A feature-rich data table component built on top of TanStack Table.\n * Provides sorting, filtering, pagination, and column visibility controls.\n *\n * @template TData The type of data being displayed in the table\n *\n * @example\n * ```tsx\n * type User = {\n * id: string;\n * name: string;\n * email: string;\n * };\n *\n * const columns: ColumnDef<User>[] = [\n * {\n * accessorKey: 'name',\n * header: 'Name',\n * },\n * {\n * accessorKey: 'email',\n * header: 'Email',\n * },\n * ];\n *\n * const data: User[] = [\n * { id: '1', name: 'John', email: 'john@example.com' },\n * { id: '2', name: 'Jane', email: 'jane@example.com' },\n * ];\n *\n * <DataTable columns={columns} data={data} />\n * ```\n */\nexport function DataTable<TData extends object = any>({\n columns: userColumns,\n data,\n getRowId = (row: TData) => (row as { id: string })?.id,\n showColumnVisibilityControls = true,\n isLoading = false,\n pagination,\n primaryFilters,\n secondaryFilters,\n activePrimaryFiltersCount = 0,\n activeSecondaryFiltersCount = 0,\n labels = {\n columnVisibilityButton: 'Hide columns',\n filters: 'Filters',\n moreFilters: 'More filters',\n },\n onSelectAll,\n onSelect,\n className,\n tableClassName,\n}: DataTableProps<TData>) {\n const isMobile = useIsMobile()\n const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})\n const [isAllRowsSelected, setIsAllRowsSelected] = React.useState(false)\n const [deselectedRows, setDeselectedRows] = React.useState<Record<string, boolean>>({})\n const [selectedRows, setSelectedRows] = React.useState<Record<string, boolean>>({})\n const [pageIndex, setPageIndex] = React.useState(0)\n\n const isBackendPagination = pagination && 'onPageChange' in pagination\n const total = isBackendPagination ? pagination.total : data.length\n const pageSize = pagination?.pageSize ?? data.length\n const totalPages = Math.ceil(total / pageSize)\n\n const isSelectable = typeof onSelectAll === 'function' || typeof onSelect === 'function'\n\n const handleSelectAll = (checked: boolean) => {\n setIsAllRowsSelected(checked)\n setDeselectedRows({})\n setSelectedRows({})\n onSelectAll?.(checked)\n }\n\n const handleRowSelect = (checked: boolean, rowData: TData) => {\n const rowId = (getRowId as (row: TData) => string)(rowData)\n\n if (isAllRowsSelected) {\n setDeselectedRows((prev) => {\n const newDeselections = { ...prev }\n if (!checked) {\n newDeselections[rowId] = true\n } else {\n delete newDeselections[rowId]\n }\n return newDeselections\n })\n } else {\n setSelectedRows((prev) => {\n const newSelection = { ...prev }\n if (checked) {\n newSelection[rowId] = true\n } else {\n delete newSelection[rowId]\n }\n return newSelection\n })\n }\n\n onSelect?.(checked, rowData)\n }\n\n const rowSelection = React.useMemo(() => {\n if (isAllRowsSelected) {\n return Object.fromEntries(\n data.map((row) => [\n (getRowId as (row: TData) => string)(row),\n !deselectedRows[(getRowId as (row: TData) => string)(row)],\n ]),\n )\n }\n return selectedRows\n }, [data, isAllRowsSelected, deselectedRows, selectedRows, getRowId])\n\n const table = useReactTable({\n data,\n columns: userColumns,\n getCoreRowModel: getCoreRowModel(),\n getPaginationRowModel: pagination && !isBackendPagination ? getPaginationRowModel() : undefined,\n onColumnVisibilityChange: setColumnVisibility,\n enableRowSelection: isSelectable,\n getRowId: getRowId,\n state: {\n columnVisibility,\n rowSelection,\n pagination: pagination\n ? {\n pageIndex: isBackendPagination ? pagination.currentPage - 1 : pageIndex,\n pageSize,\n }\n : undefined,\n },\n manualPagination: isBackendPagination,\n onPaginationChange: isBackendPagination\n ? undefined\n : (updater) => {\n if (typeof updater === 'function') {\n const newState = updater({ pageIndex, pageSize })\n setPageIndex(newState.pageIndex)\n }\n },\n })\n\n const renderTableBody = () => {\n if (isLoading) {\n return Array.from({ length: pageSize ?? 10 }).map((_, rowIndex) => (\n <TableRow key={`skeleton-row-${rowIndex.toString()}`}>\n {isSelectable && (\n <TableCell className=\"w-[50px]\">\n <Checkbox checked={false} disabled />\n </TableCell>\n )}\n {table\n .getAllColumns()\n .filter((column) => column.getIsVisible())\n .map((column) => (\n <TableCell\n key={`skeleton-cell-${rowIndex.toString()}-${column.id}`}\n style={{ width: column.columnDef.size }}\n >\n <div className={cn('flex items-center justify-center', column.id === 'actions' && 'justify-end')}>\n <Skeleton className={cn(column.id === 'actions' ? 'h-8 w-8' : 'h-[20px] w-full')} />\n </div>\n </TableCell>\n ))}\n </TableRow>\n ))\n }\n\n if (data.length === 0) {\n return (\n <TableRow>\n <TableCell\n colSpan={isSelectable ? table.getAllColumns().length + 1 : table.getAllColumns().length}\n className=\"h-[200px] text-center\"\n >\n <Typography color=\"neutral\">No data available</Typography>\n </TableCell>\n </TableRow>\n )\n }\n\n return table.getRowModel().rows.map((row) => {\n const rowId = (getRowId as (row: TData) => string)(row.original)\n const isSelected = rowSelection[rowId] ?? false\n\n return (\n <TableRow key={rowId} data-selected={isSelected}>\n {isSelectable && (\n <TableCell className=\"w-[50px]\">\n <Checkbox\n checked={isSelected}\n onCheckedChange={(checked) => {\n const isChecked = checked === true\n handleRowSelect(isChecked, row.original)\n }}\n aria-label={`Select row ${rowId}`}\n />\n </TableCell>\n )}\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id} style={{ width: cell.column.columnDef.size }}>\n <Typography>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Typography>\n </TableCell>\n ))}\n </TableRow>\n )\n })\n }\n\n return (\n <div className={cn('flex h-full min-h-0 w-full flex-1 flex-col gap-2 overflow-hidden', className)}>\n {(showColumnVisibilityControls || primaryFilters || secondaryFilters) && (\n <div className=\"flex flex-shrink-0 items-end justify-between p-1\">\n {!isMobile && (primaryFilters ? <div className=\"flex items-center gap-2\">{primaryFilters}</div> : <div />)}\n <div className={cn('flex items-center gap-2', isMobile ? 'w-full justify-end' : '')}>\n {(isMobile ? primaryFilters || secondaryFilters : secondaryFilters) && (\n <PopoverRoot>\n <PopoverTrigger asChild>\n <Button variant=\"text\" size=\"sm\" StartIcon={FunnelSimple} className=\"relative whitespace-nowrap\">\n {((isMobile && activePrimaryFiltersCount + activeSecondaryFiltersCount > 0) ||\n (!isMobile && activeSecondaryFiltersCount > 0)) && (\n <Badge\n count={\n isMobile\n ? activePrimaryFiltersCount + activeSecondaryFiltersCount\n : activeSecondaryFiltersCount\n }\n color=\"primary\"\n ping={false}\n className=\"absolute -right-1 -top-1\"\n />\n )}\n {!primaryFilters || isMobile ? labels.filters : labels.moreFilters}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"center\" className=\"w-fit p-4\">\n <div className=\"flex flex-col gap-4\">\n {isMobile && primaryFilters && primaryFilters}\n {secondaryFilters}\n </div>\n </PopoverContent>\n </PopoverRoot>\n )}\n {showColumnVisibilityControls && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"text\" size=\"sm\" className={cn('whitespace-nowrap')} StartIcon={TextColumns}>\n {labels.columnVisibilityButton ?? 'Hide columns'}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent>\n <DropdownMenuCheckboxItem\n key={'all-columns'}\n className=\"capitalize\"\n checked={table.getAllColumns().every((column) => column.getIsVisible())}\n onSelect={(event) => event.preventDefault()}\n onCheckedChange={(value) =>\n table.getAllColumns().forEach((column) => column.toggleVisibility(!!value))\n }\n >\n Select all\n </DropdownMenuCheckboxItem>\n <DropdownMenuSeparator className=\"bg-neutral-100\" />\n {table\n .getAllColumns()\n .filter((column) => column.getCanHide())\n .map((column) => {\n return (\n <DropdownMenuCheckboxItem\n key={column.id}\n className=\"capitalize\"\n checked={column.getIsVisible()}\n onSelect={(event) => event.preventDefault()}\n onCheckedChange={(value) => column.toggleVisibility(!!value)}\n >\n {column.columnDef.header?.toString()}\n </DropdownMenuCheckboxItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )}\n\n <div className=\"flex min-h-0 flex-1 flex-col rounded-md border bg-white\">\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <div className=\"h-full overflow-auto\">\n <TableComponent className=\"w-full\" tableClassName={cn('table-fixed', tableClassName)}>\n <TableHeader className=\"sticky top-0 z-10 bg-neutral-50\">\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {isSelectable && (\n <TableHead className=\"w-[50px]\">\n <Checkbox\n checked={isAllRowsSelected}\n onCheckedChange={handleSelectAll}\n disabled={isLoading || !data.length}\n aria-label=\"Select all rows\"\n />\n </TableHead>\n )}\n {headerGroup.headers.map((header) => (\n <TableHead\n key={header.id}\n className=\"whitespace-normal\"\n style={{ width: header.column.columnDef.size }}\n >\n <Typography weight=\"medium\">\n {header.isPlaceholder\n ? null\n : flexRender(header.column.columnDef.header, header.getContext())}\n </Typography>\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>{renderTableBody()}</TableBody>\n </TableComponent>\n </div>\n </div>\n {!!pagination && (\n <div className=\"border-t px-4 py-2\">\n <DataTablePagination\n table={table}\n total={total}\n pageSize={pageSize}\n currentPage={\n isBackendPagination ? pagination?.currentPage : table.getState().pagination?.pageIndex + 1 || 1\n }\n totalPages={totalPages}\n onPageChange={isBackendPagination ? pagination?.onPageChange : undefined}\n isLoading={pagination?.isLoading}\n labels={pagination?.labels}\n />\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AA2JO;AAA+C;AAC3C;AACT;AACoD;AACrB;AACnB;AACZ;AACA;AACA;AAC4B;AACE;AACrB;AACiB;AACf;AACI;AACf;AACA;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AAEA;AACE;AACA;AACA;AACA;AAAqB;AAGvB;AACE;AAEA;AACE;AACE;AACA;AACE;AAAyB;AAEzB;AAA4B;AAE9B;AAAO;AACR;AAED;AACE;AACA;AACE;AAAsB;AAEtB;AAAyB;AAE3B;AAAO;AACR;AAGH;AAA2B;AAG7B;AACE;AACE;AAAc;AACM;AACwB;AACiB;AAC1D;AACH;AAEF;AAAO;AAGT;AAA4B;AAC1B;AACS;AACwB;AACqD;AAC5D;AACN;AACpB;AACO;AACL;AACA;AAEI;AACgE;AAC9D;AAEF;AACN;AACkB;AAIZ;AACE;AACA;AAA+B;AACjC;AACF;AAGN;AACE;AACE;AAEK;AAGC;AAME;AAAC;AAAA;AAEuC;AAItC;AAAA;AALsD;AAOzD;AAEN;AAGH;AACE;AAEI;AAAC;AAAA;AACkF;AACvE;AAEmC;AAAA;AAEjD;AAIJ;AACE;AACA;AAEA;AAEK;AAEG;AAAC;AAAA;AACU;AAEP;AACA;AAAuC;AACzC;AAC+B;AAAA;AAEnC;AAMD;AACH;AAEH;AAGH;AAEM;AAEG;AAAsG;AAEnG;AAEE;AAEO;AAED;AAAC;AAAA;AAIO;AAEA;AACA;AACI;AAAA;AACZ;AAEqD;AAE3D;AAGK;AAA8B;AAC9B;AAEL;AACF;AAIE;AAIA;AAEE;AAAA;AAAC;AAAA;AAEW;AAC4D;AAC5B;AAEkC;AAE7E;AAAA;AAPM;AASP;AACkD;AAK9C;AACE;AAAC;AAAA;AAEW;AACmB;AACa;AACiB;AAExB;AAAA;AANvB;AAOd;AAEH;AACL;AACF;AAEJ;AACF;AAIA;AAGM;AAGO;AAEG;AAAC;AAAA;AACU;AACQ;AACY;AAClB;AAAA;AAEf;AAGA;AAAC;AAAA;AAEW;AACmC;AAM7C;AAAA;AARY;AAUf;AAGP;AAC8B;AAGpC;AAGI;AAAC;AAAA;AACC;AACA;AACA;AAEgG;AAEhG;AAC+D;AACxC;AACH;AAAA;AAExB;AAEJ;AAGN;;"}
|
|
@@ -1,39 +1,80 @@
|
|
|
1
|
+
import { Locale } from 'date-fns';
|
|
1
2
|
import { DateRange, PropsBase } from 'react-day-picker';
|
|
2
|
-
|
|
3
|
+
import { ValueFormat } from '../../lib/dateUtils';
|
|
4
|
+
type SupportedLocaleString = 'enUS' | 'es' | 'pt' | 'enGB' | 'de' | 'it' | 'fr';
|
|
5
|
+
type DatePickerOutput<Variant extends 'single' | 'range', Format extends ValueFormat> = Variant extends 'single' ? Format extends 'date' ? Date | undefined : string | undefined : Format extends 'date' ? DateRange | undefined : {
|
|
6
|
+
from?: string;
|
|
7
|
+
to?: string;
|
|
8
|
+
} | undefined;
|
|
9
|
+
type DatePickerInput<V extends 'single' | 'range'> = V extends 'single' ? Date | string | undefined : {
|
|
10
|
+
from?: Date | string;
|
|
11
|
+
to?: Date | string;
|
|
12
|
+
} | undefined;
|
|
13
|
+
export interface DatePickerProps<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'> extends Omit<PropsBase, 'mode' | 'selected' | 'onSelect' | 'locale'> {
|
|
3
14
|
/**
|
|
4
15
|
* Placeholder text displayed when no date is selected
|
|
5
|
-
* @default 'Pick a date'
|
|
16
|
+
* @default 'Pick a date' for single mode, 'Pick a date range' for range mode
|
|
6
17
|
*/
|
|
7
|
-
placeholder?: string;
|
|
18
|
+
readonly placeholder?: string;
|
|
8
19
|
/**
|
|
9
|
-
* Format string to use when displaying the selected date
|
|
20
|
+
* Format string to use when displaying the selected date in the button
|
|
10
21
|
* @default 'MMM d, yyyy'
|
|
11
22
|
*/
|
|
12
|
-
|
|
23
|
+
readonly displayFormat?: string;
|
|
13
24
|
/**
|
|
14
25
|
* ClassName for the button
|
|
15
26
|
*/
|
|
16
|
-
buttonClassName?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
readonly buttonClassName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to allow the user to show the year switcher menu
|
|
30
|
+
* @default true for single mode, false for range mode
|
|
31
|
+
*/
|
|
32
|
+
readonly showYearSwitcher?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Initial value for the date picker
|
|
35
|
+
* Accepts both Date objects and ISO strings regardless of valueFormat setting
|
|
36
|
+
*/
|
|
37
|
+
readonly initialValue?: DatePickerInput<V>;
|
|
38
|
+
/**
|
|
39
|
+
* Current value for the date picker
|
|
40
|
+
* Accepts both Date objects and ISO strings regardless of valueFormat setting
|
|
41
|
+
*/
|
|
42
|
+
readonly value?: DatePickerInput<V>;
|
|
43
|
+
/**
|
|
44
|
+
* Determines if the picker should close after a selection
|
|
45
|
+
* @default true for single mode, false for range mode
|
|
46
|
+
*/
|
|
47
|
+
readonly closeOnSelect?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Determines the format of the value provided to the onChange callback
|
|
50
|
+
* - 'iso' (default): onChange receives ISO string(s) ('yyyy-MM-dd')
|
|
51
|
+
* - 'date': onChange receives JavaScript Date object(s)
|
|
52
|
+
*
|
|
53
|
+
* Note: The component accepts both Date objects and ISO strings for value/initialValue
|
|
54
|
+
* regardless of this setting.
|
|
55
|
+
* @default 'iso'
|
|
56
|
+
*/
|
|
57
|
+
readonly valueFormat?: F;
|
|
58
|
+
/**
|
|
59
|
+
* Callback when date or date range changes
|
|
60
|
+
*/
|
|
61
|
+
readonly onChange?: (value: DatePickerOutput<V, F>) => void;
|
|
62
|
+
/**
|
|
63
|
+
* DatePicker mode - single date or date range
|
|
64
|
+
* @default 'single'
|
|
65
|
+
*/
|
|
66
|
+
readonly variant?: V;
|
|
67
|
+
/**
|
|
68
|
+
* The locale to use for formatting dates and determining the start of the week.
|
|
69
|
+
* Can be a string identifier for supported locales ('enUS', 'es', 'pt', 'enGB', 'de', 'it', 'fr')
|
|
70
|
+
* or a Locale object from date-fns/locale for other languages.
|
|
71
|
+
* @default 'enUS'
|
|
72
|
+
*/
|
|
73
|
+
readonly locale?: SupportedLocaleString | Locale;
|
|
74
|
+
}
|
|
75
|
+
declare function DatePicker<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'>({ variant, placeholder, valueFormat, initialValue: initialValueProp, value: valueProp, onChange, buttonClassName, displayFormat, closeOnSelect, showYearSwitcher, locale: localeProp, ...rest }: DatePickerProps<V, F>): import("react/jsx-runtime").JSX.Element;
|
|
36
76
|
declare namespace DatePicker {
|
|
37
77
|
var displayName: string;
|
|
38
78
|
}
|
|
39
79
|
export { DatePicker };
|
|
80
|
+
export type { DateRange };
|
|
@@ -1,64 +1,112 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
-
import
|
|
3
|
+
import { CalendarBlank } from '@phosphor-icons/react';
|
|
4
4
|
import { format } from 'date-fns';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { enUS, es, pt, enGB, de, it, fr } from 'date-fns/locale';
|
|
6
|
+
import * as React from 'react';
|
|
7
7
|
import { Button, buttonVariants } from '../Button/Button.js';
|
|
8
8
|
import { Calendar } from '../Calendar/Calendar.js';
|
|
9
|
-
import {
|
|
9
|
+
import { PopoverRoot, PopoverTrigger, PopoverContent } from '../Popover/Popover.js';
|
|
10
|
+
import { parseInputDate, parseInputRange, formatOutputDate, formatOutputRange } from '../../lib/dateUtils.js';
|
|
11
|
+
import { cn } from '../../lib/utils.js';
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
const localeMap = {
|
|
14
|
+
enUS,
|
|
15
|
+
es,
|
|
16
|
+
pt,
|
|
17
|
+
enGB,
|
|
18
|
+
de,
|
|
19
|
+
it,
|
|
20
|
+
fr
|
|
21
|
+
};
|
|
22
|
+
function DatePicker({
|
|
23
|
+
variant = "single",
|
|
24
|
+
placeholder = variant === "single" ? "Pick a date" : "Pick a date range",
|
|
25
|
+
valueFormat = "iso",
|
|
26
|
+
initialValue: initialValueProp,
|
|
27
|
+
value: valueProp,
|
|
28
|
+
onChange,
|
|
29
|
+
buttonClassName,
|
|
30
|
+
displayFormat = "MMM d, yyyy",
|
|
31
|
+
closeOnSelect = variant === "single",
|
|
32
|
+
showYearSwitcher = variant === "single",
|
|
33
|
+
locale: localeProp = "enUS",
|
|
34
|
+
...rest
|
|
35
|
+
}) {
|
|
26
36
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
);
|
|
33
|
-
const
|
|
34
|
-
|
|
37
|
+
const resolvedLocale = React.useMemo(() => {
|
|
38
|
+
if (typeof localeProp === "string") {
|
|
39
|
+
return localeMap[localeProp];
|
|
40
|
+
}
|
|
41
|
+
return localeProp;
|
|
42
|
+
}, [localeProp]);
|
|
43
|
+
const [internalSingleDate, setInternalSingleDate] = React.useState(() => {
|
|
44
|
+
if (variant === "single") {
|
|
45
|
+
return parseInputDate(initialValueProp);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
});
|
|
49
|
+
const [internalDateRange, setInternalDateRange] = React.useState(() => {
|
|
50
|
+
if (variant === "range") {
|
|
51
|
+
return parseInputRange(initialValueProp);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
});
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (variant === "single") {
|
|
57
|
+
const parsedValue = parseInputDate(valueProp);
|
|
58
|
+
if (parsedValue?.getTime() !== internalSingleDate?.getTime()) {
|
|
59
|
+
setInternalSingleDate(parsedValue);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const parsedValue = parseInputRange(valueProp);
|
|
63
|
+
if (parsedValue?.from?.getTime() !== internalDateRange?.from?.getTime() || parsedValue?.to?.getTime() !== internalDateRange?.to?.getTime()) {
|
|
64
|
+
setInternalDateRange(parsedValue);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, [valueProp, variant]);
|
|
68
|
+
const singleDate = internalSingleDate;
|
|
69
|
+
const dateRange = internalDateRange;
|
|
35
70
|
const handleSelect = React.useCallback(
|
|
36
71
|
(selectedDate) => {
|
|
37
72
|
if (variant === "single") {
|
|
38
73
|
const date = selectedDate;
|
|
39
74
|
setInternalSingleDate(date);
|
|
40
|
-
if (onChange
|
|
75
|
+
if (onChange) {
|
|
76
|
+
const output = valueFormat === "date" ? date : formatOutputDate(date, "iso");
|
|
77
|
+
onChange(output);
|
|
78
|
+
}
|
|
41
79
|
if (closeOnSelect) {
|
|
42
80
|
setIsOpen(false);
|
|
43
81
|
}
|
|
44
82
|
} else {
|
|
45
83
|
const range = selectedDate;
|
|
46
84
|
setInternalDateRange(range);
|
|
47
|
-
if (onChange
|
|
85
|
+
if (onChange) {
|
|
86
|
+
const output = valueFormat === "date" ? range : formatOutputRange(range, "iso");
|
|
87
|
+
onChange(output);
|
|
88
|
+
}
|
|
48
89
|
}
|
|
49
90
|
},
|
|
50
|
-
[variant, onChange, closeOnSelect]
|
|
91
|
+
[variant, valueFormat, onChange, closeOnSelect]
|
|
51
92
|
);
|
|
52
|
-
const
|
|
93
|
+
const formatForDisplay = () => {
|
|
94
|
+
const formatOptions = { locale: resolvedLocale };
|
|
53
95
|
if (variant === "single") {
|
|
54
|
-
return singleDate ? format(singleDate,
|
|
96
|
+
return singleDate ? format(singleDate, displayFormat, formatOptions) : placeholder;
|
|
55
97
|
}
|
|
56
98
|
if (!dateRange) return placeholder;
|
|
57
|
-
|
|
99
|
+
const fromStr = dateRange.from ? format(dateRange.from, displayFormat, formatOptions) : "...";
|
|
100
|
+
const toStr = dateRange.to ? format(dateRange.to, displayFormat, formatOptions) : "...";
|
|
101
|
+
if (!dateRange.from && !dateRange.to) return placeholder;
|
|
102
|
+
if (!dateRange.from) return `... - ${toStr}`;
|
|
103
|
+
if (!dateRange.to) return `${fromStr} - ...`;
|
|
104
|
+
return `${fromStr} - ${toStr}`;
|
|
58
105
|
};
|
|
59
106
|
const calendarProps = React.useMemo(() => {
|
|
60
107
|
const baseProps = {
|
|
61
108
|
...rest,
|
|
109
|
+
locale: resolvedLocale,
|
|
62
110
|
initialFocus: true
|
|
63
111
|
};
|
|
64
112
|
if (variant === "single") {
|
|
@@ -78,7 +126,7 @@ function DatePicker(props) {
|
|
|
78
126
|
defaultMonth: dateRange?.from ?? /* @__PURE__ */ new Date(),
|
|
79
127
|
numberOfMonths: rest.numberOfMonths ?? 2
|
|
80
128
|
};
|
|
81
|
-
}, [variant, rest, singleDate, dateRange, handleSelect]);
|
|
129
|
+
}, [variant, rest, singleDate, dateRange, handleSelect, resolvedLocale]);
|
|
82
130
|
return /* @__PURE__ */ jsxs(PopoverRoot, { open: isOpen, onOpenChange: setIsOpen, children: [
|
|
83
131
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
84
132
|
Button,
|
|
@@ -86,15 +134,15 @@ function DatePicker(props) {
|
|
|
86
134
|
id: rest.id,
|
|
87
135
|
variant: "ghost",
|
|
88
136
|
className: cn(
|
|
89
|
-
"w-fit justify-start text-left font-normal",
|
|
137
|
+
"relative flex w-fit items-center justify-start text-left font-normal",
|
|
90
138
|
!(variant === "single" ? singleDate : dateRange) && "text-muted-foreground",
|
|
91
139
|
buttonVariants({ variant: "input" }),
|
|
92
140
|
buttonClassName
|
|
93
141
|
),
|
|
94
142
|
disabled: typeof rest.disabled === "boolean" ? rest.disabled : false,
|
|
95
143
|
children: [
|
|
96
|
-
/* @__PURE__ */ jsx(CalendarBlank, { className: "
|
|
97
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
144
|
+
/* @__PURE__ */ jsx(CalendarBlank, { className: "absolute left-4 h-4 w-4 shrink-0" }),
|
|
145
|
+
/* @__PURE__ */ jsx("span", { className: "w-full pl-7 text-center", children: formatForDisplay() })
|
|
98
146
|
]
|
|
99
147
|
}
|
|
100
148
|
) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatePicker.js","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport
|
|
1
|
+
{"version":3,"file":"DatePicker.js","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport { CalendarBlank } from '@phosphor-icons/react'\nimport { format as formatFn, Locale } from 'date-fns'\nimport { enUS, es, pt, enGB, de, it, fr } from 'date-fns/locale'\nimport * as React from 'react'\nimport { DateRange, PropsBase } from 'react-day-picker'\n\nimport { Button, buttonVariants } from '../Button'\nimport { Calendar } from '../Calendar'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\n\nimport {\n formatOutputDate,\n formatOutputRange,\n parseInputDate,\n parseInputRange,\n InputDate,\n InputRange,\n ValueFormat,\n} from '@/lib/dateUtils'\nimport { cn } from '@/lib/utils'\n\ntype SupportedLocaleString = 'enUS' | 'es' | 'pt' | 'enGB' | 'de' | 'it' | 'fr'\n\nconst localeMap: Record<SupportedLocaleString, Locale> = {\n enUS,\n es,\n pt,\n enGB,\n de,\n it,\n fr,\n}\n\ntype DatePickerOutput<Variant extends 'single' | 'range', Format extends ValueFormat> = Variant extends 'single'\n ? Format extends 'date'\n ? Date | undefined\n : string | undefined\n : Format extends 'date'\n ? DateRange | undefined\n : { from?: string; to?: string } | undefined\n\ntype DatePickerInput<V extends 'single' | 'range'> = V extends 'single'\n ? Date | string | undefined\n : { from?: Date | string; to?: Date | string } | undefined\n\nexport interface DatePickerProps<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'>\n extends Omit<PropsBase, 'mode' | 'selected' | 'onSelect' | 'locale'> {\n /**\n * Placeholder text displayed when no date is selected\n * @default 'Pick a date' for single mode, 'Pick a date range' for range mode\n */\n readonly placeholder?: string\n\n /**\n * Format string to use when displaying the selected date in the button\n * @default 'MMM d, yyyy'\n */\n readonly displayFormat?: string\n\n /**\n * ClassName for the button\n */\n readonly buttonClassName?: string\n\n /**\n * Whether to allow the user to show the year switcher menu\n * @default true for single mode, false for range mode\n */\n readonly showYearSwitcher?: boolean\n\n /**\n * Initial value for the date picker\n * Accepts both Date objects and ISO strings regardless of valueFormat setting\n */\n readonly initialValue?: DatePickerInput<V>\n\n /**\n * Current value for the date picker\n * Accepts both Date objects and ISO strings regardless of valueFormat setting\n */\n readonly value?: DatePickerInput<V>\n\n /**\n * Determines if the picker should close after a selection\n * @default true for single mode, false for range mode\n */\n readonly closeOnSelect?: boolean\n\n /**\n * Determines the format of the value provided to the onChange callback\n * - 'iso' (default): onChange receives ISO string(s) ('yyyy-MM-dd')\n * - 'date': onChange receives JavaScript Date object(s)\n *\n * Note: The component accepts both Date objects and ISO strings for value/initialValue\n * regardless of this setting.\n * @default 'iso'\n */\n readonly valueFormat?: F\n\n /**\n * Callback when date or date range changes\n */\n readonly onChange?: (value: DatePickerOutput<V, F>) => void\n\n /**\n * DatePicker mode - single date or date range\n * @default 'single'\n */\n readonly variant?: V\n\n /**\n * The locale to use for formatting dates and determining the start of the week.\n * Can be a string identifier for supported locales ('enUS', 'es', 'pt', 'enGB', 'de', 'it', 'fr')\n * or a Locale object from date-fns/locale for other languages.\n * @default 'enUS'\n */\n readonly locale?: SupportedLocaleString | Locale\n}\n\nfunction DatePicker<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'>({\n variant = 'single' as V,\n placeholder = variant === 'single' ? 'Pick a date' : 'Pick a date range',\n valueFormat = 'iso' as F,\n initialValue: initialValueProp,\n value: valueProp,\n onChange,\n buttonClassName,\n displayFormat = 'MMM d, yyyy',\n closeOnSelect = variant === 'single',\n showYearSwitcher = variant === 'single',\n locale: localeProp = 'enUS',\n ...rest\n}: DatePickerProps<V, F>) {\n const [isOpen, setIsOpen] = React.useState(false)\n\n const resolvedLocale = React.useMemo(() => {\n if (typeof localeProp === 'string') {\n return localeMap[localeProp]\n }\n return localeProp\n }, [localeProp])\n\n const [internalSingleDate, setInternalSingleDate] = React.useState<Date | undefined>(() => {\n if (variant === 'single') {\n return parseInputDate(initialValueProp as InputDate)\n }\n return undefined\n })\n\n const [internalDateRange, setInternalDateRange] = React.useState<DateRange | undefined>(() => {\n if (variant === 'range') {\n return parseInputRange(initialValueProp as InputRange)\n }\n return undefined\n })\n\n React.useEffect(() => {\n if (variant === 'single') {\n const parsedValue = parseInputDate(valueProp as InputDate)\n if (parsedValue?.getTime() !== internalSingleDate?.getTime()) {\n setInternalSingleDate(parsedValue)\n }\n } else {\n const parsedValue = parseInputRange(valueProp as InputRange)\n if (\n parsedValue?.from?.getTime() !== internalDateRange?.from?.getTime() ||\n parsedValue?.to?.getTime() !== internalDateRange?.to?.getTime()\n ) {\n setInternalDateRange(parsedValue)\n }\n }\n }, [valueProp, variant])\n\n const singleDate = internalSingleDate\n const dateRange = internalDateRange\n\n const handleSelect = React.useCallback(\n (selectedDate: Date | DateRange | undefined) => {\n if (variant === 'single') {\n const date = selectedDate as Date | undefined\n setInternalSingleDate(date)\n if (onChange) {\n const output = valueFormat === 'date' ? date : formatOutputDate(date, 'iso')\n onChange(output as DatePickerOutput<V, F>)\n }\n if (closeOnSelect) {\n setIsOpen(false)\n }\n } else {\n const range = selectedDate as DateRange | undefined\n setInternalDateRange(range)\n if (onChange) {\n const output = valueFormat === 'date' ? range : formatOutputRange(range, 'iso')\n onChange(output as DatePickerOutput<V, F>)\n }\n }\n },\n [variant, valueFormat, onChange, closeOnSelect],\n )\n\n const formatForDisplay = () => {\n const formatOptions = { locale: resolvedLocale }\n if (variant === 'single') {\n return singleDate ? formatFn(singleDate, displayFormat, formatOptions) : placeholder\n }\n\n if (!dateRange) return placeholder\n const fromStr = dateRange.from ? formatFn(dateRange.from, displayFormat, formatOptions) : '...'\n const toStr = dateRange.to ? formatFn(dateRange.to, displayFormat, formatOptions) : '...'\n if (!dateRange.from && !dateRange.to) return placeholder\n if (!dateRange.from) return `... - ${toStr}`\n if (!dateRange.to) return `${fromStr} - ...`\n return `${fromStr} - ${toStr}`\n }\n\n const calendarProps = React.useMemo(() => {\n const baseProps = {\n ...rest,\n locale: resolvedLocale,\n initialFocus: true,\n }\n\n if (variant === 'single') {\n return {\n ...baseProps,\n mode: 'single' as const,\n selected: singleDate,\n onSelect: (date: Date | undefined) => handleSelect(date),\n defaultMonth: singleDate ?? new Date(),\n }\n }\n\n return {\n ...baseProps,\n mode: 'range' as const,\n selected: dateRange,\n onSelect: (range: DateRange | undefined) => handleSelect(range),\n defaultMonth: dateRange?.from ?? new Date(),\n numberOfMonths: rest.numberOfMonths ?? 2,\n }\n }, [variant, rest, singleDate, dateRange, handleSelect, resolvedLocale])\n\n return (\n <PopoverRoot open={isOpen} onOpenChange={setIsOpen}>\n <PopoverTrigger asChild>\n <Button\n id={rest.id}\n variant=\"ghost\"\n className={cn(\n 'relative flex w-fit items-center justify-start text-left font-normal',\n !(variant === 'single' ? singleDate : dateRange) && 'text-muted-foreground',\n buttonVariants({ variant: 'input' }),\n buttonClassName,\n )}\n disabled={typeof rest.disabled === 'boolean' ? rest.disabled : false}\n >\n <CalendarBlank className=\"absolute left-4 h-4 w-4 shrink-0\" />\n <span className=\"w-full pl-7 text-center\">{formatForDisplay()}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-0\" align=\"center\">\n <Calendar\n {...calendarProps}\n className=\"border-0\"\n showYearSwitcher={variant === 'single' ? showYearSwitcher : false}\n />\n </PopoverContent>\n </PopoverRoot>\n )\n}\n\nDatePicker.displayName = 'DatePicker'\n\nexport { DatePicker }\nexport type { DateRange }\n"],"names":[],"mappings":";;;;;;;;;;;;AAyBA;AAAyD;AACvD;AACA;AACA;AACA;AACA;AACA;AAEF;AAwFA;AAA4F;AAChF;AAC2C;AACvC;AACA;AACP;AACP;AACA;AACgB;AACY;AACG;AACV;AAEvB;AACE;AAEA;AACE;AACE;AAA2B;AAE7B;AAAO;AAGT;AACE;AACE;AAAmD;AAErD;AAAO;AAGT;AACE;AACE;AAAqD;AAEvD;AAAO;AAGT;AACE;AACE;AACA;AACE;AAAiC;AACnC;AAEA;AACA;AAIE;AAAgC;AAClC;AACF;AAGF;AACA;AAEA;AAA2B;AAEvB;AACE;AACA;AACA;AACE;AACA;AAAyC;AAE3C;AACE;AAAe;AACjB;AAEA;AACA;AACA;AACE;AACA;AAAyC;AAC3C;AACF;AACF;AAC8C;AAGhD;AACE;AACA;AACE;AAAyE;AAG3E;AACA;AACA;AACA;AACA;AACA;AACA;AAA4B;AAG9B;AACE;AAAkB;AACb;AACK;AACM;AAGhB;AACE;AAAO;AACF;AACG;AACI;AAC6C;AAClB;AACvC;AAGF;AAAO;AACF;AACG;AACI;AACoD;AACpB;AACH;AACzC;AAGF;AAEI;AACE;AAAC;AAAA;AACU;AACD;AACG;AACT;AACoD;AACjB;AACnC;AACF;AAC+D;AAE/D;AAA4D;AACE;AAAA;AAAA;AAElE;AAEE;AAAC;AAAA;AACK;AACM;AACkD;AAAA;AAEhE;AAGN;AAEA;;"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DateRange } from 'react-day-picker';
|
|
2
|
+
/**
|
|
3
|
+
* Format options for date values returned by onChange callbacks
|
|
4
|
+
*/
|
|
5
|
+
export type ValueFormat = 'iso' | 'date';
|
|
6
|
+
/**
|
|
7
|
+
* Type for single date input that can be a Date object, ISO string, or undefined
|
|
8
|
+
*/
|
|
9
|
+
export type InputDate = Date | string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Type for date range input that can be a DateRange object, an object with from/to properties, or undefined
|
|
12
|
+
*/
|
|
13
|
+
export type InputRange = DateRange | {
|
|
14
|
+
from?: Date | string;
|
|
15
|
+
to?: Date | string;
|
|
16
|
+
} | undefined;
|
|
17
|
+
export type OutputDate<TFormat extends ValueFormat> = TFormat extends 'iso' ? string | undefined : Date | undefined;
|
|
18
|
+
export type OutputRange<TFormat extends ValueFormat> = TFormat extends 'iso' ? {
|
|
19
|
+
from?: string;
|
|
20
|
+
to?: string;
|
|
21
|
+
} | undefined : DateRange | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Parses an input value (Date or ISO string) into a valid Date object.
|
|
24
|
+
* Returns undefined if the input is invalid or cannot be parsed.
|
|
25
|
+
*/
|
|
26
|
+
export declare const parseInputDate: (input: InputDate) => Date | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Formats a Date object into the specified output format (Date or ISO string).
|
|
29
|
+
* Returns undefined if the input date is undefined or invalid.
|
|
30
|
+
*/
|
|
31
|
+
export declare const formatOutputDate: <TFormat extends "iso" | "date">(date: Date | undefined, format: TFormat) => OutputDate<TFormat>;
|
|
32
|
+
/**
|
|
33
|
+
* Parses an input range value (containing Dates or ISO strings) into a DateRange object.
|
|
34
|
+
* Returns undefined if the input is invalid.
|
|
35
|
+
*/
|
|
36
|
+
export declare const parseInputRange: (input: InputRange) => DateRange | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Formats a DateRange object into the specified output format (DateRange or object with ISO strings).
|
|
39
|
+
* Returns undefined if the input range is undefined.
|
|
40
|
+
*/
|
|
41
|
+
export declare const formatOutputRange: <TFormat extends "iso" | "date">(range: DateRange | undefined, format: TFormat) => OutputRange<TFormat>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isValid, parseISO, format } from 'date-fns';
|
|
2
|
+
|
|
3
|
+
const parseInputDate = (input) => {
|
|
4
|
+
if (input === undefined || input === null) return undefined;
|
|
5
|
+
if (input instanceof Date && isValid(input)) {
|
|
6
|
+
return input;
|
|
7
|
+
}
|
|
8
|
+
if (typeof input === "string") {
|
|
9
|
+
try {
|
|
10
|
+
const parsedDate = parseISO(input);
|
|
11
|
+
return isValid(parsedDate) ? parsedDate : void 0;
|
|
12
|
+
} catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
const formatOutputDate = (date, format$1) => {
|
|
19
|
+
if (date === undefined || !isValid(date)) return undefined;
|
|
20
|
+
if (format$1 === "date") {
|
|
21
|
+
return date;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return format(date, "yyyy-MM-dd");
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const parseInputRange = (input) => {
|
|
30
|
+
if (!input) return undefined;
|
|
31
|
+
const fromDate = parseInputDate(input.from);
|
|
32
|
+
const toDate = parseInputDate(input.to);
|
|
33
|
+
if (fromDate === undefined && toDate === undefined) return undefined;
|
|
34
|
+
return { from: fromDate, to: toDate };
|
|
35
|
+
};
|
|
36
|
+
const formatOutputRange = (range, format) => {
|
|
37
|
+
if (!range) return undefined;
|
|
38
|
+
if (format === "date") {
|
|
39
|
+
return range;
|
|
40
|
+
}
|
|
41
|
+
const fromString = formatOutputDate(range.from, "iso");
|
|
42
|
+
const toString = formatOutputDate(range.to, "iso");
|
|
43
|
+
return {
|
|
44
|
+
from: fromString,
|
|
45
|
+
to: toString
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export { formatOutputDate, formatOutputRange, parseInputDate, parseInputRange };
|
|
50
|
+
//# sourceMappingURL=dateUtils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dateUtils.js","sources":["../../src/lib/dateUtils.ts"],"sourcesContent":["import { format as formatFn, isValid, parseISO } from 'date-fns' // Used for parsing, formatting, and validating dates\nimport { DateRange } from 'react-day-picker' // Type used for date ranges\n\n/**\n * Format options for date values returned by onChange callbacks\n */\nexport type ValueFormat = 'iso' | 'date'\n\n/**\n * Type for single date input that can be a Date object, ISO string, or undefined\n */\nexport type InputDate = Date | string | undefined\n\n/**\n * Type for date range input that can be a DateRange object, an object with from/to properties, or undefined\n */\nexport type InputRange = DateRange | { from?: Date | string; to?: Date | string } | undefined\n\nexport type OutputDate<TFormat extends ValueFormat> = TFormat extends 'iso' ? string | undefined : Date | undefined\nexport type OutputRange<TFormat extends ValueFormat> = TFormat extends 'iso'\n ? { from?: string; to?: string } | undefined\n : DateRange | undefined\n\n/**\n * Parses an input value (Date or ISO string) into a valid Date object.\n * Returns undefined if the input is invalid or cannot be parsed.\n */\nexport const parseInputDate = (input: InputDate): Date | undefined => {\n if (input === undefined || input === null) return undefined\n\n if (input instanceof Date && isValid(input)) {\n return input\n }\n\n if (typeof input === 'string') {\n try {\n const parsedDate = parseISO(input)\n return isValid(parsedDate) ? parsedDate : undefined\n } catch {\n return undefined\n }\n }\n\n return undefined\n}\n\n/**\n * Formats a Date object into the specified output format (Date or ISO string).\n * Returns undefined if the input date is undefined or invalid.\n */\nexport const formatOutputDate = <TFormat extends 'iso' | 'date'>(\n date: Date | undefined,\n format: TFormat,\n): OutputDate<TFormat> => {\n if (date === undefined || !isValid(date)) return undefined\n\n if (format === 'date') {\n return date as OutputDate<TFormat>\n }\n\n try {\n return formatFn(date, 'yyyy-MM-dd') as OutputDate<TFormat>\n } catch {\n return undefined\n }\n}\n\n/**\n * Parses an input range value (containing Dates or ISO strings) into a DateRange object.\n * Returns undefined if the input is invalid.\n */\nexport const parseInputRange = (input: InputRange): DateRange | undefined => {\n if (!input) return undefined\n\n const fromDate = parseInputDate(input.from)\n const toDate = parseInputDate(input.to)\n\n if (fromDate === undefined && toDate === undefined) return undefined\n\n return { from: fromDate, to: toDate }\n}\n\n/**\n * Formats a DateRange object into the specified output format (DateRange or object with ISO strings).\n * Returns undefined if the input range is undefined.\n */\nexport const formatOutputRange = <TFormat extends 'iso' | 'date'>(\n range: DateRange | undefined,\n format: TFormat,\n): OutputRange<TFormat> => {\n if (!range) return undefined\n\n if (format === 'date') {\n return range as OutputRange<TFormat>\n }\n\n const fromString = formatOutputDate(range.from, 'iso')\n const toString = formatOutputDate(range.to, 'iso')\n\n return {\n from: fromString,\n to: toString,\n } as OutputRange<TFormat>\n}\n"],"names":["format","formatFn"],"mappings":";;AA2Ba,MAAA,cAAA,GAAiB,CAAC,KAAuC,KAAA;AACpE,EAAA,IAAI,KAAU,KAAA,SAAA,IAAa,KAAU,KAAA,IAAA,EAAa,OAAA,SAAA;AAElD,EAAA,IAAI,KAAiB,YAAA,IAAA,IAAQ,OAAQ,CAAA,KAAK,CAAG,EAAA;AAC3C,IAAO,OAAA,KAAA;AAAA;AAGT,EAAI,IAAA,OAAO,UAAU,QAAU,EAAA;AAC7B,IAAI,IAAA;AACF,MAAM,MAAA,UAAA,GAAa,SAAS,KAAK,CAAA;AACjC,MAAO,OAAA,OAAA,CAAQ,UAAU,CAAA,GAAI,UAAa,GAAA,KAAA,CAAA;AAAA,KACpC,CAAA,MAAA;AACN,MAAO,OAAA,SAAA;AAAA;AACT;AAGF,EAAO,OAAA,SAAA;AACT;AAMa,MAAA,gBAAA,GAAmB,CAC9B,IAAA,EACAA,QACwB,KAAA;AACxB,EAAA,IAAI,SAAS,SAAa,IAAA,CAAC,OAAQ,CAAA,IAAI,GAAU,OAAA,SAAA;AAEjD,EAAA,IAAIA,aAAW,MAAQ,EAAA;AACrB,IAAO,OAAA,IAAA;AAAA;AAGT,EAAI,IAAA;AACF,IAAO,OAAAC,MAAA,CAAS,MAAM,YAAY,CAAA;AAAA,GAC5B,CAAA,MAAA;AACN,IAAO,OAAA,SAAA;AAAA;AAEX;AAMa,MAAA,eAAA,GAAkB,CAAC,KAA6C,KAAA;AAC3E,EAAI,IAAA,CAAC,OAAc,OAAA,SAAA;AAEnB,EAAM,MAAA,QAAA,GAAW,cAAe,CAAA,KAAA,CAAM,IAAI,CAAA;AAC1C,EAAM,MAAA,MAAA,GAAS,cAAe,CAAA,KAAA,CAAM,EAAE,CAAA;AAEtC,EAAA,IAAI,QAAa,KAAA,SAAA,IAAa,MAAW,KAAA,SAAA,EAAkB,OAAA,SAAA;AAE3D,EAAA,OAAO,EAAE,IAAA,EAAM,QAAU,EAAA,EAAA,EAAI,MAAO,EAAA;AACtC;AAMa,MAAA,iBAAA,GAAoB,CAC/B,KAAA,EACA,MACyB,KAAA;AACzB,EAAI,IAAA,CAAC,OAAc,OAAA,SAAA;AAEnB,EAAA,IAAI,WAAW,MAAQ,EAAA;AACrB,IAAO,OAAA,KAAA;AAAA;AAGT,EAAA,MAAM,UAAa,GAAA,gBAAA,CAAiB,KAAM,CAAA,IAAA,EAAM,KAAK,CAAA;AACrD,EAAA,MAAM,QAAW,GAAA,gBAAA,CAAiB,KAAM,CAAA,EAAA,EAAI,KAAK,CAAA;AAEjD,EAAO,OAAA;AAAA,IACL,IAAM,EAAA,UAAA;AAAA,IACN,EAAI,EAAA;AAAA,GACN;AACF;;;;"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "periplo-ui",
|
|
3
3
|
"description": "IATI UI library",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.18.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"eslint-plugin-react-hooks": "4.6.2",
|
|
68
68
|
"eslint-plugin-storybook": "0.11.2",
|
|
69
69
|
"eslint-plugin-testing-library": "6.5.0",
|
|
70
|
-
"eslint-plugin-vitest": "0.
|
|
70
|
+
"eslint-plugin-vitest": "0.4.1",
|
|
71
71
|
"husky": "9.1.7",
|
|
72
72
|
"jsdom": "24.1.3",
|
|
73
73
|
"lint-staged": "15.3.0",
|