@momentumworld/ui-react 0.17.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/chunk-2HQPCV2L.js +895 -0
- package/dist/chunk-2HQPCV2L.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +356 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2162 -0
- package/dist/index.js +11640 -0
- package/dist/index.js.map +1 -0
- package/dist/input-JCZs61MO.d.ts +24 -0
- package/package.json +90 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/layout/dashboard-topbar.tsx +221 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-counter.tsx +109 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/aurora-orb.tsx +163 -0
- package/src/components/ui/aurora-text.tsx +76 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +150 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/chat-divider.tsx +67 -0
- package/src/components/ui/chat.tsx +494 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/color-picker.tsx +389 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/confetti.tsx +196 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/context-meter.tsx +134 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/data-table-paged.tsx +163 -0
- package/src/components/ui/date-picker.tsx +154 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/fab.tsx +45 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/help-tip.tsx +94 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/markdown.tsx +188 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/pipeline.tsx +102 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/segmented-control.tsx +83 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +774 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +206 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +242 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/thinking-orb.tsx +6 -0
- package/src/components/ui/time-picker.tsx +308 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +143 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +329 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { NfcIcon } from "lucide-react";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils/css";
|
|
5
|
+
|
|
6
|
+
// The card is laid out once at this design size, then transform-scaled to the
|
|
7
|
+
// requested width — so every variant stays pixel-identical at any size.
|
|
8
|
+
const BASE_WIDTH = 316;
|
|
9
|
+
const BASE_HEIGHT = 190;
|
|
10
|
+
|
|
11
|
+
type CreditCardType =
|
|
12
|
+
| "transparent"
|
|
13
|
+
| "transparent-gradient"
|
|
14
|
+
| "brand-dark"
|
|
15
|
+
| "brand-light"
|
|
16
|
+
| "gray-dark"
|
|
17
|
+
| "gray-light"
|
|
18
|
+
| "transparent-strip"
|
|
19
|
+
| "gray-strip"
|
|
20
|
+
| "gradient-strip"
|
|
21
|
+
| "salmon-strip"
|
|
22
|
+
| "gray-strip-vertical"
|
|
23
|
+
| "gradient-strip-vertical"
|
|
24
|
+
| "salmon-strip-vertical";
|
|
25
|
+
|
|
26
|
+
interface CreditCardVariantStyle {
|
|
27
|
+
card: string;
|
|
28
|
+
/** Decorative band rendered behind the content. */
|
|
29
|
+
strip?: string;
|
|
30
|
+
/** Light content on a dark or colorful face — switches the network logo. */
|
|
31
|
+
onDark: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const GLASS_FACE =
|
|
35
|
+
"border border-white/30 bg-white/10 text-white backdrop-blur-md";
|
|
36
|
+
const LIGHT_FACE =
|
|
37
|
+
"bg-[#fffefc] text-[#1c1917] ring-1 ring-[#0c0a09]/10 ring-inset";
|
|
38
|
+
const HORIZONTAL_STRIP = "inset-x-0 top-16 h-9";
|
|
39
|
+
const VERTICAL_STRIP = "inset-y-0 right-12 w-9";
|
|
40
|
+
|
|
41
|
+
const creditCardVariants: Record<CreditCardType, CreditCardVariantStyle> = {
|
|
42
|
+
"brand-dark": {
|
|
43
|
+
card: "bg-gradient-to-br from-[#a8761c] via-[#92600a] to-[#5e3f08] text-[#faf3df]",
|
|
44
|
+
onDark: true,
|
|
45
|
+
},
|
|
46
|
+
"brand-light": {
|
|
47
|
+
card: "bg-gradient-to-br from-[#ecd9a8] via-[#dcb35e] to-[#c0922e] text-[#41290a]",
|
|
48
|
+
onDark: false,
|
|
49
|
+
},
|
|
50
|
+
"gradient-strip": {
|
|
51
|
+
card: LIGHT_FACE,
|
|
52
|
+
onDark: false,
|
|
53
|
+
strip: cn(
|
|
54
|
+
HORIZONTAL_STRIP,
|
|
55
|
+
"bg-gradient-to-r from-[#d4a84b] via-[#c48a1a] to-[#92600a]",
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
"gradient-strip-vertical": {
|
|
59
|
+
card: LIGHT_FACE,
|
|
60
|
+
onDark: false,
|
|
61
|
+
strip: cn(
|
|
62
|
+
VERTICAL_STRIP,
|
|
63
|
+
"bg-gradient-to-b from-[#d4a84b] via-[#c48a1a] to-[#92600a]",
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
"gray-dark": {
|
|
67
|
+
card: "bg-gradient-to-br from-[#3b3633] via-[#1c1917] to-[#0c0a09] text-[#faf9f6]",
|
|
68
|
+
onDark: true,
|
|
69
|
+
},
|
|
70
|
+
"gray-light": {
|
|
71
|
+
card: "bg-gradient-to-br from-[#faf9f6] to-[#e7e5e4] text-[#1c1917] ring-1 ring-[#0c0a09]/10 ring-inset",
|
|
72
|
+
onDark: false,
|
|
73
|
+
},
|
|
74
|
+
"gray-strip": {
|
|
75
|
+
card: LIGHT_FACE,
|
|
76
|
+
onDark: false,
|
|
77
|
+
strip: cn(HORIZONTAL_STRIP, "bg-[#292524]"),
|
|
78
|
+
},
|
|
79
|
+
"gray-strip-vertical": {
|
|
80
|
+
card: LIGHT_FACE,
|
|
81
|
+
onDark: false,
|
|
82
|
+
strip: cn(VERTICAL_STRIP, "bg-[#292524]"),
|
|
83
|
+
},
|
|
84
|
+
"salmon-strip": {
|
|
85
|
+
card: LIGHT_FACE,
|
|
86
|
+
onDark: false,
|
|
87
|
+
strip: cn(HORIZONTAL_STRIP, "bg-gradient-to-r from-[#e8a18f] to-[#d4644f]"),
|
|
88
|
+
},
|
|
89
|
+
"salmon-strip-vertical": {
|
|
90
|
+
card: LIGHT_FACE,
|
|
91
|
+
onDark: false,
|
|
92
|
+
strip: cn(VERTICAL_STRIP, "bg-gradient-to-b from-[#e8a18f] to-[#d4644f]"),
|
|
93
|
+
},
|
|
94
|
+
transparent: {
|
|
95
|
+
card: GLASS_FACE,
|
|
96
|
+
onDark: true,
|
|
97
|
+
},
|
|
98
|
+
"transparent-gradient": {
|
|
99
|
+
card: "border border-white/25 bg-gradient-to-br from-[#d4a84b]/60 via-white/10 to-white/5 text-white backdrop-blur-md",
|
|
100
|
+
onDark: true,
|
|
101
|
+
},
|
|
102
|
+
"transparent-strip": {
|
|
103
|
+
card: GLASS_FACE,
|
|
104
|
+
onDark: true,
|
|
105
|
+
strip: cn(HORIZONTAL_STRIP, "bg-white/25"),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The card-network mark — two overlapping circles in the kit's palette. */
|
|
110
|
+
function CreditCardLogo({ onDark }: { onDark: boolean }) {
|
|
111
|
+
return (
|
|
112
|
+
<span aria-hidden className="flex items-center" data-slot="credit-card-logo">
|
|
113
|
+
<span
|
|
114
|
+
className={cn(
|
|
115
|
+
"size-6 rounded-full",
|
|
116
|
+
onDark ? "bg-white/60" : "bg-[#d4a84b]",
|
|
117
|
+
)}
|
|
118
|
+
/>
|
|
119
|
+
<span
|
|
120
|
+
className={cn(
|
|
121
|
+
"-ml-2.5 size-6 rounded-full",
|
|
122
|
+
onDark ? "bg-white/30" : "bg-[#92600a]/85",
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
</span>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface CreditCardProps extends React.ComponentProps<"div"> {
|
|
130
|
+
type?: CreditCardType;
|
|
131
|
+
company?: React.ReactNode;
|
|
132
|
+
cardNumber?: string;
|
|
133
|
+
cardHolder?: string;
|
|
134
|
+
cardExpiration?: string;
|
|
135
|
+
/** Rendered width in px — the height follows the 316 × 190 aspect ratio. */
|
|
136
|
+
width?: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function CreditCard({
|
|
140
|
+
className,
|
|
141
|
+
style,
|
|
142
|
+
type = "brand-dark",
|
|
143
|
+
company = "Country Club",
|
|
144
|
+
cardNumber = "1234 5678 9012 3456",
|
|
145
|
+
cardHolder = "OLIVIA RHYE",
|
|
146
|
+
cardExpiration = "06/28",
|
|
147
|
+
width = BASE_WIDTH,
|
|
148
|
+
...props
|
|
149
|
+
}: CreditCardProps) {
|
|
150
|
+
const scale = width / BASE_WIDTH;
|
|
151
|
+
const variant = creditCardVariants[type];
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div
|
|
155
|
+
className={cn("relative shrink-0 select-none font-sans", className)}
|
|
156
|
+
data-slot="credit-card"
|
|
157
|
+
data-type={type}
|
|
158
|
+
style={{ height: Math.round(BASE_HEIGHT * scale), width, ...style }}
|
|
159
|
+
{...props}
|
|
160
|
+
>
|
|
161
|
+
<div
|
|
162
|
+
className={cn(
|
|
163
|
+
"absolute top-0 left-0 flex origin-top-left flex-col justify-between overflow-hidden rounded-[20px] p-5",
|
|
164
|
+
variant.card,
|
|
165
|
+
)}
|
|
166
|
+
style={{
|
|
167
|
+
height: BASE_HEIGHT,
|
|
168
|
+
transform: `scale(${scale})`,
|
|
169
|
+
width: BASE_WIDTH,
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{variant.strip ? (
|
|
173
|
+
<span aria-hidden className={cn("absolute", variant.strip)} />
|
|
174
|
+
) : null}
|
|
175
|
+
<div className="relative flex items-start justify-between">
|
|
176
|
+
<span
|
|
177
|
+
className="font-semibold text-base tracking-tight"
|
|
178
|
+
data-slot="credit-card-company"
|
|
179
|
+
>
|
|
180
|
+
{company}
|
|
181
|
+
</span>
|
|
182
|
+
<NfcIcon aria-hidden className="size-5 opacity-70" />
|
|
183
|
+
</div>
|
|
184
|
+
<div className="relative flex flex-col gap-2.5">
|
|
185
|
+
<span
|
|
186
|
+
className="font-medium text-[0.9375rem] tabular-nums tracking-[0.12em]"
|
|
187
|
+
data-slot="credit-card-number"
|
|
188
|
+
>
|
|
189
|
+
{cardNumber}
|
|
190
|
+
</span>
|
|
191
|
+
<div className="flex items-end justify-between">
|
|
192
|
+
<div className="flex items-baseline gap-4">
|
|
193
|
+
<span
|
|
194
|
+
className="font-medium text-[0.6875rem] uppercase tracking-[0.14em]"
|
|
195
|
+
data-slot="credit-card-holder"
|
|
196
|
+
>
|
|
197
|
+
{cardHolder}
|
|
198
|
+
</span>
|
|
199
|
+
<span
|
|
200
|
+
className="font-medium text-[0.6875rem] tabular-nums tracking-[0.14em] opacity-80"
|
|
201
|
+
data-slot="credit-card-expiration"
|
|
202
|
+
>
|
|
203
|
+
{cardExpiration}
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
<CreditCardLogo onDark={variant.onDark} />
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { CreditCard, type CreditCardProps, type CreditCardType };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { SearchIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
import { Input } from "./input";
|
|
8
|
+
import { PaginationControls } from "./pagination-controls";
|
|
9
|
+
import {
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
} from "./table";
|
|
17
|
+
|
|
18
|
+
export interface DataTablePagedColumn {
|
|
19
|
+
key: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DataTablePagedProps {
|
|
24
|
+
/** Columns to render. Pass `label` to override the header text. */
|
|
25
|
+
columns: DataTablePagedColumn[];
|
|
26
|
+
/** Row objects; values are rendered with `String(...)` unless a ReactNode. */
|
|
27
|
+
rows: Record<string, unknown>[];
|
|
28
|
+
pageSize?: number;
|
|
29
|
+
/** Show the client-side search box (filters across all columns). */
|
|
30
|
+
searchable?: boolean;
|
|
31
|
+
searchPlaceholder?: string;
|
|
32
|
+
emptyLabel?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Total row count when `rows` is only one page of a larger server-side set —
|
|
35
|
+
* shown as "n of total" in the footer.
|
|
36
|
+
*/
|
|
37
|
+
totalCount?: number;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatCell(value: unknown): React.ReactNode {
|
|
42
|
+
if (value === null || value === undefined || value === "") return "—";
|
|
43
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
44
|
+
if (React.isValidElement(value)) return value;
|
|
45
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
46
|
+
return String(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Client-side paginated (and optionally searchable) data table. Designed for
|
|
51
|
+
* result sets a chat agent or API returns: pass plain row objects and column
|
|
52
|
+
* keys, get a themed table with pagination that never renders huge lists.
|
|
53
|
+
*/
|
|
54
|
+
function DataTablePaged({
|
|
55
|
+
className,
|
|
56
|
+
columns,
|
|
57
|
+
emptyLabel = "No results",
|
|
58
|
+
pageSize = 10,
|
|
59
|
+
rows,
|
|
60
|
+
searchable = true,
|
|
61
|
+
searchPlaceholder = "Filter results…",
|
|
62
|
+
totalCount,
|
|
63
|
+
}: DataTablePagedProps) {
|
|
64
|
+
const [page, setPage] = React.useState(1);
|
|
65
|
+
const [query, setQuery] = React.useState("");
|
|
66
|
+
|
|
67
|
+
const filteredRows = React.useMemo(() => {
|
|
68
|
+
const trimmed = query.trim().toLowerCase();
|
|
69
|
+
if (!trimmed) return rows;
|
|
70
|
+
return rows.filter((row) =>
|
|
71
|
+
columns.some((column) =>
|
|
72
|
+
String(row[column.key] ?? "")
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.includes(trimmed),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}, [columns, query, rows]);
|
|
78
|
+
|
|
79
|
+
const pageCount = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
|
80
|
+
const safePage = Math.min(page, pageCount);
|
|
81
|
+
const pageRows = filteredRows.slice((safePage - 1) * pageSize, safePage * pageSize);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={cn("flex flex-col gap-2", className)} data-slot="data-table-paged">
|
|
85
|
+
{searchable && rows.length > pageSize && (
|
|
86
|
+
<div className="relative">
|
|
87
|
+
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-2.5 size-4 text-muted-foreground" />
|
|
88
|
+
<Input
|
|
89
|
+
className="h-8 pl-8 text-sm"
|
|
90
|
+
onChange={(event) => {
|
|
91
|
+
setQuery(event.target.value);
|
|
92
|
+
setPage(1);
|
|
93
|
+
}}
|
|
94
|
+
placeholder={searchPlaceholder}
|
|
95
|
+
value={query}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
101
|
+
<Table>
|
|
102
|
+
<TableHeader>
|
|
103
|
+
<TableRow>
|
|
104
|
+
{columns.map((column) => (
|
|
105
|
+
<TableHead className="whitespace-nowrap" key={column.key}>
|
|
106
|
+
{column.label ?? column.key.replaceAll("_", " ")}
|
|
107
|
+
</TableHead>
|
|
108
|
+
))}
|
|
109
|
+
</TableRow>
|
|
110
|
+
</TableHeader>
|
|
111
|
+
<TableBody>
|
|
112
|
+
{pageRows.length === 0 ? (
|
|
113
|
+
<TableRow>
|
|
114
|
+
<TableCell
|
|
115
|
+
className="py-6 text-center text-muted-foreground"
|
|
116
|
+
colSpan={columns.length}
|
|
117
|
+
>
|
|
118
|
+
{emptyLabel}
|
|
119
|
+
</TableCell>
|
|
120
|
+
</TableRow>
|
|
121
|
+
) : (
|
|
122
|
+
pageRows.map((row, rowIndex) => (
|
|
123
|
+
<TableRow key={`${safePage}-${rowIndex}`}>
|
|
124
|
+
{columns.map((column) => (
|
|
125
|
+
<TableCell className="max-w-56 truncate" key={column.key}>
|
|
126
|
+
{formatCell(row[column.key])}
|
|
127
|
+
</TableCell>
|
|
128
|
+
))}
|
|
129
|
+
</TableRow>
|
|
130
|
+
))
|
|
131
|
+
)}
|
|
132
|
+
</TableBody>
|
|
133
|
+
</Table>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
137
|
+
<span className="text-muted-foreground text-xs">
|
|
138
|
+
{filteredRows.length}
|
|
139
|
+
{typeof totalCount === "number" && totalCount > rows.length
|
|
140
|
+
? ` of ${totalCount}`
|
|
141
|
+
: ""}{" "}
|
|
142
|
+
rows
|
|
143
|
+
</span>
|
|
144
|
+
<PaginationControls
|
|
145
|
+
page={safePage}
|
|
146
|
+
pageCount={pageCount}
|
|
147
|
+
renderPageLink={(nextPage) => (
|
|
148
|
+
// biome-ignore lint/a11y/useValidAnchor: client-side pagination
|
|
149
|
+
<a
|
|
150
|
+
href="#"
|
|
151
|
+
onClick={(event) => {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
setPage(nextPage);
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { DataTablePaged };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { format } from "date-fns";
|
|
4
|
+
import { CalendarIcon } from "lucide-react";
|
|
5
|
+
import type { DateRange, Matcher } from "react-day-picker";
|
|
6
|
+
|
|
7
|
+
import { Button } from "./button";
|
|
8
|
+
import { Calendar } from "./calendar";
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
10
|
+
import { cn } from "../../lib/utils/css";
|
|
11
|
+
|
|
12
|
+
/** Build react-day-picker `disabled` matchers from optional min/max bounds. */
|
|
13
|
+
function boundsMatcher(fromDate?: Date, toDate?: Date): Matcher[] | undefined {
|
|
14
|
+
const out: Matcher[] = [];
|
|
15
|
+
if (fromDate) out.push({ before: fromDate });
|
|
16
|
+
if (toDate) out.push({ after: toDate });
|
|
17
|
+
return out.length ? out : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DatePickerProps {
|
|
21
|
+
/** The selected date (controlled). */
|
|
22
|
+
value?: Date;
|
|
23
|
+
onValueChange?: (date: Date | undefined) => void;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
/** Trigger button `disabled`. */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Restrict selectable days (passed straight to react-day-picker). */
|
|
28
|
+
fromDate?: Date;
|
|
29
|
+
toDate?: Date;
|
|
30
|
+
/** date-fns format for the trigger label (default "LLL dd, y"). */
|
|
31
|
+
displayFormat?: string;
|
|
32
|
+
align?: "start" | "center" | "end";
|
|
33
|
+
className?: string;
|
|
34
|
+
id?: string;
|
|
35
|
+
"aria-label"?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A single-date picker: a button that opens a calendar in a popover. The
|
|
40
|
+
* canonical way to take a date in this system — never a native `<input
|
|
41
|
+
* type="date">`. Controlled via `value` / `onValueChange`.
|
|
42
|
+
*/
|
|
43
|
+
function DatePicker({
|
|
44
|
+
value,
|
|
45
|
+
onValueChange,
|
|
46
|
+
placeholder = "Pick a date",
|
|
47
|
+
disabled,
|
|
48
|
+
fromDate,
|
|
49
|
+
toDate,
|
|
50
|
+
displayFormat = "LLL dd, y",
|
|
51
|
+
align = "start",
|
|
52
|
+
className,
|
|
53
|
+
id,
|
|
54
|
+
...props
|
|
55
|
+
}: DatePickerProps) {
|
|
56
|
+
return (
|
|
57
|
+
<Popover>
|
|
58
|
+
<PopoverTrigger
|
|
59
|
+
render={
|
|
60
|
+
<Button
|
|
61
|
+
className={cn("w-full justify-start font-normal", !value && "text-muted-foreground", className)}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
id={id}
|
|
64
|
+
variant="outline"
|
|
65
|
+
aria-label={props["aria-label"]}
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<CalendarIcon aria-hidden />
|
|
70
|
+
{value ? format(value, displayFormat) : <span>{placeholder}</span>}
|
|
71
|
+
</PopoverTrigger>
|
|
72
|
+
<PopoverContent align={align} className="w-auto p-0">
|
|
73
|
+
<Calendar
|
|
74
|
+
autoFocus
|
|
75
|
+
disabled={boundsMatcher(fromDate, toDate)}
|
|
76
|
+
mode="single"
|
|
77
|
+
onSelect={(d) => onValueChange?.(d)}
|
|
78
|
+
selected={value}
|
|
79
|
+
/>
|
|
80
|
+
</PopoverContent>
|
|
81
|
+
</Popover>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface DateRangePickerProps {
|
|
86
|
+
value?: DateRange;
|
|
87
|
+
onValueChange?: (range: DateRange | undefined) => void;
|
|
88
|
+
placeholder?: string;
|
|
89
|
+
disabled?: boolean;
|
|
90
|
+
fromDate?: Date;
|
|
91
|
+
toDate?: Date;
|
|
92
|
+
numberOfMonths?: number;
|
|
93
|
+
displayFormat?: string;
|
|
94
|
+
align?: "start" | "center" | "end";
|
|
95
|
+
className?: string;
|
|
96
|
+
id?: string;
|
|
97
|
+
"aria-label"?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A date-range picker: pick a start and (optionally) end date. Controlled via
|
|
102
|
+
* `value` / `onValueChange` with react-day-picker's `{from, to}` shape.
|
|
103
|
+
*/
|
|
104
|
+
function DateRangePicker({
|
|
105
|
+
value,
|
|
106
|
+
onValueChange,
|
|
107
|
+
placeholder = "Pick a date range",
|
|
108
|
+
disabled,
|
|
109
|
+
fromDate,
|
|
110
|
+
toDate,
|
|
111
|
+
numberOfMonths = 2,
|
|
112
|
+
displayFormat = "LLL dd, y",
|
|
113
|
+
align = "start",
|
|
114
|
+
className,
|
|
115
|
+
id,
|
|
116
|
+
...props
|
|
117
|
+
}: DateRangePickerProps) {
|
|
118
|
+
const label = value?.from
|
|
119
|
+
? value.to
|
|
120
|
+
? `${format(value.from, displayFormat)} – ${format(value.to, displayFormat)}`
|
|
121
|
+
: format(value.from, displayFormat)
|
|
122
|
+
: null;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Popover>
|
|
126
|
+
<PopoverTrigger
|
|
127
|
+
render={
|
|
128
|
+
<Button
|
|
129
|
+
className={cn("w-full justify-start font-normal", !label && "text-muted-foreground", className)}
|
|
130
|
+
disabled={disabled}
|
|
131
|
+
id={id}
|
|
132
|
+
variant="outline"
|
|
133
|
+
aria-label={props["aria-label"]}
|
|
134
|
+
/>
|
|
135
|
+
}
|
|
136
|
+
>
|
|
137
|
+
<CalendarIcon aria-hidden />
|
|
138
|
+
{label ?? <span>{placeholder}</span>}
|
|
139
|
+
</PopoverTrigger>
|
|
140
|
+
<PopoverContent align={align} className="w-auto p-0">
|
|
141
|
+
<Calendar
|
|
142
|
+
autoFocus
|
|
143
|
+
disabled={boundsMatcher(fromDate, toDate)}
|
|
144
|
+
mode="range"
|
|
145
|
+
numberOfMonths={numberOfMonths}
|
|
146
|
+
onSelect={(r) => onValueChange?.(r)}
|
|
147
|
+
selected={value}
|
|
148
|
+
/>
|
|
149
|
+
</PopoverContent>
|
|
150
|
+
</Popover>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { DatePicker, DateRangePicker, type DatePickerProps, type DateRangePickerProps };
|