@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.
Files changed (138) hide show
  1. package/dist/chunk-2HQPCV2L.js +895 -0
  2. package/dist/chunk-2HQPCV2L.js.map +1 -0
  3. package/dist/form.d.ts +175 -0
  4. package/dist/form.js +356 -0
  5. package/dist/form.js.map +1 -0
  6. package/dist/index.d.ts +2162 -0
  7. package/dist/index.js +11640 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/input-JCZs61MO.d.ts +24 -0
  10. package/package.json +90 -0
  11. package/src/components/form/checkbox-group-field.tsx +101 -0
  12. package/src/components/form/date-field.tsx +79 -0
  13. package/src/components/form/date-range-field.tsx +106 -0
  14. package/src/components/form/form-context.ts +10 -0
  15. package/src/components/form/form.tsx +54 -0
  16. package/src/components/form/number-field.tsx +69 -0
  17. package/src/components/form/select-field.tsx +76 -0
  18. package/src/components/form/submit-button.tsx +28 -0
  19. package/src/components/form/text-field.tsx +107 -0
  20. package/src/components/layout/dashboard-header.tsx +54 -0
  21. package/src/components/layout/dashboard-panel.tsx +34 -0
  22. package/src/components/layout/dashboard-topbar.tsx +221 -0
  23. package/src/components/theme-provider.tsx +403 -0
  24. package/src/components/ui/accordion.tsx +69 -0
  25. package/src/components/ui/alert-dialog.tsx +169 -0
  26. package/src/components/ui/alert.tsx +80 -0
  27. package/src/components/ui/animated-counter.tsx +109 -0
  28. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  29. package/src/components/ui/app-store-buttons.tsx +182 -0
  30. package/src/components/ui/aspect-ratio.tsx +23 -0
  31. package/src/components/ui/aurora-orb.tsx +163 -0
  32. package/src/components/ui/aurora-text.tsx +76 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +150 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/chat-divider.tsx +67 -0
  45. package/src/components/ui/chat.tsx +494 -0
  46. package/src/components/ui/checkbox-group.tsx +16 -0
  47. package/src/components/ui/checkbox.tsx +82 -0
  48. package/src/components/ui/collapsible.tsx +45 -0
  49. package/src/components/ui/color-picker.tsx +389 -0
  50. package/src/components/ui/combobox.tsx +411 -0
  51. package/src/components/ui/command.tsx +264 -0
  52. package/src/components/ui/confetti.tsx +196 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/context-meter.tsx +134 -0
  55. package/src/components/ui/credit-card.tsx +214 -0
  56. package/src/components/ui/data-table-paged.tsx +163 -0
  57. package/src/components/ui/date-picker.tsx +154 -0
  58. package/src/components/ui/dialog.tsx +196 -0
  59. package/src/components/ui/drawer.tsx +135 -0
  60. package/src/components/ui/empty.tsx +127 -0
  61. package/src/components/ui/fab.tsx +45 -0
  62. package/src/components/ui/featured-icon.tsx +149 -0
  63. package/src/components/ui/field.tsx +88 -0
  64. package/src/components/ui/fieldset.tsx +29 -0
  65. package/src/components/ui/form.tsx +17 -0
  66. package/src/components/ui/frame.tsx +82 -0
  67. package/src/components/ui/generic-empty.tsx +142 -0
  68. package/src/components/ui/group.tsx +97 -0
  69. package/src/components/ui/help-tip.tsx +94 -0
  70. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  71. package/src/components/ui/input-group.tsx +102 -0
  72. package/src/components/ui/input-otp.tsx +96 -0
  73. package/src/components/ui/input.tsx +66 -0
  74. package/src/components/ui/item.tsx +198 -0
  75. package/src/components/ui/kbd.tsx +30 -0
  76. package/src/components/ui/label.tsx +28 -0
  77. package/src/components/ui/markdown.tsx +188 -0
  78. package/src/components/ui/menu.tsx +312 -0
  79. package/src/components/ui/menubar.tsx +93 -0
  80. package/src/components/ui/meter.tsx +67 -0
  81. package/src/components/ui/multi-select.tsx +308 -0
  82. package/src/components/ui/navigation-menu.tsx +143 -0
  83. package/src/components/ui/number-field.tsx +160 -0
  84. package/src/components/ui/pagination-controls.tsx +74 -0
  85. package/src/components/ui/pagination.tsx +149 -0
  86. package/src/components/ui/pipeline.tsx +102 -0
  87. package/src/components/ui/popover.tsx +119 -0
  88. package/src/components/ui/preview-card.tsx +55 -0
  89. package/src/components/ui/progress.tsx +289 -0
  90. package/src/components/ui/qr-code.tsx +150 -0
  91. package/src/components/ui/radio-group.tsx +103 -0
  92. package/src/components/ui/resizable.tsx +56 -0
  93. package/src/components/ui/scroll-area.tsx +90 -0
  94. package/src/components/ui/scroller.tsx +38 -0
  95. package/src/components/ui/section-header.tsx +118 -0
  96. package/src/components/ui/segmented-control.tsx +83 -0
  97. package/src/components/ui/select.tsx +181 -0
  98. package/src/components/ui/separator.tsx +23 -0
  99. package/src/components/ui/sheet.tsx +224 -0
  100. package/src/components/ui/sidebar.tsx +774 -0
  101. package/src/components/ui/skeleton.tsx +16 -0
  102. package/src/components/ui/slider.tsx +108 -0
  103. package/src/components/ui/smooth-scroll.tsx +206 -0
  104. package/src/components/ui/social-button.tsx +247 -0
  105. package/src/components/ui/spinner-on-demand.tsx +32 -0
  106. package/src/components/ui/spinner.tsx +18 -0
  107. package/src/components/ui/stat.tsx +187 -0
  108. package/src/components/ui/stepper.tsx +167 -0
  109. package/src/components/ui/switch.tsx +56 -0
  110. package/src/components/ui/table.tsx +126 -0
  111. package/src/components/ui/tabs.tsx +90 -0
  112. package/src/components/ui/tag.tsx +242 -0
  113. package/src/components/ui/target-countdown.tsx +46 -0
  114. package/src/components/ui/text-editor.tsx +313 -0
  115. package/src/components/ui/textarea.tsx +51 -0
  116. package/src/components/ui/thinking-orb.tsx +6 -0
  117. package/src/components/ui/time-picker.tsx +308 -0
  118. package/src/components/ui/timeline.tsx +116 -0
  119. package/src/components/ui/toast.tsx +268 -0
  120. package/src/components/ui/toggle-group.tsx +101 -0
  121. package/src/components/ui/toggle.tsx +45 -0
  122. package/src/components/ui/toolbar.tsx +89 -0
  123. package/src/components/ui/tooltip.tsx +102 -0
  124. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  125. package/src/components/ui/video-player.tsx +275 -0
  126. package/src/components/upload/avatar-upload-base.tsx +131 -0
  127. package/src/components/upload/image-upload-base.tsx +112 -0
  128. package/src/form.ts +17 -0
  129. package/src/index.ts +143 -0
  130. package/src/lib/hooks/use-callback-ref.ts +15 -0
  131. package/src/lib/hooks/use-first-render.ts +11 -0
  132. package/src/lib/hooks/use-hover.ts +53 -0
  133. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  134. package/src/lib/hooks/use-media-query.ts +164 -0
  135. package/src/lib/utils/css.ts +6 -0
  136. package/src/styles.css +329 -0
  137. package/src/types/helpers.ts +24 -0
  138. 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 };