myoperator-ui 0.0.170 → 0.0.171
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/index.js +615 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5220,6 +5220,76 @@ const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
|
|
|
5220
5220
|
Skeleton.displayName = "Skeleton";
|
|
5221
5221
|
|
|
5222
5222
|
export { Skeleton, skeletonVariants };
|
|
5223
|
+
`, prefix)
|
|
5224
|
+
}
|
|
5225
|
+
]
|
|
5226
|
+
},
|
|
5227
|
+
"empty-state": {
|
|
5228
|
+
name: "empty-state",
|
|
5229
|
+
description: "Centered empty state with icon, title, description, and optional action buttons",
|
|
5230
|
+
category: "feedback",
|
|
5231
|
+
dependencies: [
|
|
5232
|
+
"clsx",
|
|
5233
|
+
"tailwind-merge"
|
|
5234
|
+
],
|
|
5235
|
+
files: [
|
|
5236
|
+
{
|
|
5237
|
+
name: "empty-state.tsx",
|
|
5238
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
5239
|
+
import { cn } from "../../lib/utils";
|
|
5240
|
+
|
|
5241
|
+
export interface EmptyStateProps {
|
|
5242
|
+
/** Icon element rendered inside the icon circle */
|
|
5243
|
+
icon?: React.ReactNode;
|
|
5244
|
+
/** Bold heading text */
|
|
5245
|
+
title: React.ReactNode;
|
|
5246
|
+
/** Optional subtitle / description text */
|
|
5247
|
+
description?: React.ReactNode;
|
|
5248
|
+
/** Optional action buttons rendered below the description */
|
|
5249
|
+
actions?: React.ReactNode;
|
|
5250
|
+
/** Additional CSS classes for the root container */
|
|
5251
|
+
className?: string;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5254
|
+
function EmptyState({
|
|
5255
|
+
icon,
|
|
5256
|
+
title,
|
|
5257
|
+
description,
|
|
5258
|
+
actions,
|
|
5259
|
+
className,
|
|
5260
|
+
}: EmptyStateProps) {
|
|
5261
|
+
return (
|
|
5262
|
+
<div
|
|
5263
|
+
data-slot="empty-state"
|
|
5264
|
+
className={cn(
|
|
5265
|
+
"flex flex-col items-center justify-center gap-5 py-16 px-4",
|
|
5266
|
+
className
|
|
5267
|
+
)}
|
|
5268
|
+
>
|
|
5269
|
+
{icon && (
|
|
5270
|
+
<div className="bg-semantic-primary-surface rounded-[40px] size-[90px] flex items-center justify-center text-semantic-text-secondary">
|
|
5271
|
+
{icon}
|
|
5272
|
+
</div>
|
|
5273
|
+
)}
|
|
5274
|
+
<div className="flex flex-col items-center gap-1.5 text-center">
|
|
5275
|
+
<p className="m-0 text-base font-semibold text-semantic-text-primary">
|
|
5276
|
+
{title}
|
|
5277
|
+
</p>
|
|
5278
|
+
{description && (
|
|
5279
|
+
<p className="m-0 text-sm text-semantic-text-muted max-w-xs">
|
|
5280
|
+
{description}
|
|
5281
|
+
</p>
|
|
5282
|
+
)}
|
|
5283
|
+
</div>
|
|
5284
|
+
{actions && (
|
|
5285
|
+
<div className="flex items-center gap-4">{actions}</div>
|
|
5286
|
+
)}
|
|
5287
|
+
</div>
|
|
5288
|
+
);
|
|
5289
|
+
}
|
|
5290
|
+
EmptyState.displayName = "EmptyState";
|
|
5291
|
+
|
|
5292
|
+
export { EmptyState };
|
|
5223
5293
|
`, prefix)
|
|
5224
5294
|
}
|
|
5225
5295
|
]
|
|
@@ -6007,17 +6077,25 @@ PaginationLink.displayName = "PaginationLink";
|
|
|
6007
6077
|
export interface PaginationPreviousProps extends PaginationLinkProps {
|
|
6008
6078
|
/** Additional CSS classes */
|
|
6009
6079
|
className?: string;
|
|
6080
|
+
/** Disables the previous button */
|
|
6081
|
+
disabled?: boolean;
|
|
6010
6082
|
}
|
|
6011
6083
|
|
|
6012
6084
|
function PaginationPrevious({
|
|
6013
6085
|
className,
|
|
6086
|
+
disabled,
|
|
6014
6087
|
...props
|
|
6015
6088
|
}: PaginationPreviousProps) {
|
|
6016
6089
|
return (
|
|
6017
6090
|
<PaginationLink
|
|
6018
6091
|
aria-label="Go to previous page"
|
|
6092
|
+
aria-disabled={disabled}
|
|
6019
6093
|
size="default"
|
|
6020
|
-
className={cn(
|
|
6094
|
+
className={cn(
|
|
6095
|
+
"gap-1 px-2.5 sm:pl-2.5",
|
|
6096
|
+
disabled && "pointer-events-none opacity-50",
|
|
6097
|
+
className
|
|
6098
|
+
)}
|
|
6021
6099
|
{...props}
|
|
6022
6100
|
>
|
|
6023
6101
|
<ChevronLeftIcon />
|
|
@@ -6030,17 +6108,25 @@ PaginationPrevious.displayName = "PaginationPrevious";
|
|
|
6030
6108
|
export interface PaginationNextProps extends PaginationLinkProps {
|
|
6031
6109
|
/** Additional CSS classes */
|
|
6032
6110
|
className?: string;
|
|
6111
|
+
/** Disables the next button */
|
|
6112
|
+
disabled?: boolean;
|
|
6033
6113
|
}
|
|
6034
6114
|
|
|
6035
6115
|
function PaginationNext({
|
|
6036
6116
|
className,
|
|
6117
|
+
disabled,
|
|
6037
6118
|
...props
|
|
6038
6119
|
}: PaginationNextProps) {
|
|
6039
6120
|
return (
|
|
6040
6121
|
<PaginationLink
|
|
6041
6122
|
aria-label="Go to next page"
|
|
6123
|
+
aria-disabled={disabled}
|
|
6042
6124
|
size="default"
|
|
6043
|
-
className={cn(
|
|
6125
|
+
className={cn(
|
|
6126
|
+
"gap-1 px-2.5 sm:pr-2.5",
|
|
6127
|
+
disabled && "pointer-events-none opacity-50",
|
|
6128
|
+
className
|
|
6129
|
+
)}
|
|
6044
6130
|
{...props}
|
|
6045
6131
|
>
|
|
6046
6132
|
<span className="hidden sm:block">Next</span>
|
|
@@ -6073,6 +6159,115 @@ function PaginationEllipsis({
|
|
|
6073
6159
|
}
|
|
6074
6160
|
PaginationEllipsis.displayName = "PaginationEllipsis";
|
|
6075
6161
|
|
|
6162
|
+
export interface PaginationWidgetProps {
|
|
6163
|
+
/** Current page (1-based) */
|
|
6164
|
+
currentPage: number;
|
|
6165
|
+
/** Total number of pages */
|
|
6166
|
+
totalPages: number;
|
|
6167
|
+
/** Called when the user navigates to a new page */
|
|
6168
|
+
onPageChange: (page: number) => void;
|
|
6169
|
+
/** Number of pages shown on each side of current page (default: 1) */
|
|
6170
|
+
siblingCount?: number;
|
|
6171
|
+
/** Additional CSS classes */
|
|
6172
|
+
className?: string;
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6175
|
+
function usePaginationRange(
|
|
6176
|
+
currentPage: number,
|
|
6177
|
+
totalPages: number,
|
|
6178
|
+
siblingCount: number
|
|
6179
|
+
): (number | "ellipsis")[] {
|
|
6180
|
+
if (totalPages <= 1) return [1];
|
|
6181
|
+
|
|
6182
|
+
const range = (start: number, end: number): number[] =>
|
|
6183
|
+
Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
6184
|
+
|
|
6185
|
+
const leftSibling = Math.max(currentPage - siblingCount, 2);
|
|
6186
|
+
const rightSibling = Math.min(currentPage + siblingCount, totalPages - 1);
|
|
6187
|
+
|
|
6188
|
+
const showLeftEllipsis = leftSibling > 2;
|
|
6189
|
+
const showRightEllipsis = rightSibling < totalPages - 1;
|
|
6190
|
+
|
|
6191
|
+
const pages: (number | "ellipsis")[] = [1];
|
|
6192
|
+
|
|
6193
|
+
if (showLeftEllipsis) {
|
|
6194
|
+
pages.push("ellipsis");
|
|
6195
|
+
} else {
|
|
6196
|
+
// fill in pages between 1 and leftSibling if no ellipsis
|
|
6197
|
+
for (let p = 2; p < leftSibling; p++) pages.push(p);
|
|
6198
|
+
}
|
|
6199
|
+
|
|
6200
|
+
pages.push(...range(leftSibling, rightSibling));
|
|
6201
|
+
|
|
6202
|
+
if (showRightEllipsis) {
|
|
6203
|
+
pages.push("ellipsis");
|
|
6204
|
+
} else {
|
|
6205
|
+
for (let p = rightSibling + 1; p < totalPages; p++) pages.push(p);
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
if (totalPages > 1) pages.push(totalPages);
|
|
6209
|
+
|
|
6210
|
+
return pages;
|
|
6211
|
+
}
|
|
6212
|
+
|
|
6213
|
+
function PaginationWidget({
|
|
6214
|
+
currentPage,
|
|
6215
|
+
totalPages,
|
|
6216
|
+
onPageChange,
|
|
6217
|
+
siblingCount = 1,
|
|
6218
|
+
className,
|
|
6219
|
+
}: PaginationWidgetProps) {
|
|
6220
|
+
const pages = usePaginationRange(currentPage, totalPages, siblingCount);
|
|
6221
|
+
|
|
6222
|
+
return (
|
|
6223
|
+
<Pagination className={className}>
|
|
6224
|
+
<PaginationContent>
|
|
6225
|
+
<PaginationItem>
|
|
6226
|
+
<PaginationPrevious
|
|
6227
|
+
href="#"
|
|
6228
|
+
disabled={currentPage === 1}
|
|
6229
|
+
onClick={(e) => {
|
|
6230
|
+
e.preventDefault();
|
|
6231
|
+
if (currentPage > 1) onPageChange(currentPage - 1);
|
|
6232
|
+
}}
|
|
6233
|
+
/>
|
|
6234
|
+
</PaginationItem>
|
|
6235
|
+
{pages.map((page, idx) =>
|
|
6236
|
+
page === "ellipsis" ? (
|
|
6237
|
+
<PaginationItem key={\`ellipsis-\${idx}\`}>
|
|
6238
|
+
<PaginationEllipsis />
|
|
6239
|
+
</PaginationItem>
|
|
6240
|
+
) : (
|
|
6241
|
+
<PaginationItem key={page}>
|
|
6242
|
+
<PaginationLink
|
|
6243
|
+
href="#"
|
|
6244
|
+
isActive={page === currentPage}
|
|
6245
|
+
onClick={(e) => {
|
|
6246
|
+
e.preventDefault();
|
|
6247
|
+
onPageChange(page);
|
|
6248
|
+
}}
|
|
6249
|
+
>
|
|
6250
|
+
{page}
|
|
6251
|
+
</PaginationLink>
|
|
6252
|
+
</PaginationItem>
|
|
6253
|
+
)
|
|
6254
|
+
)}
|
|
6255
|
+
<PaginationItem>
|
|
6256
|
+
<PaginationNext
|
|
6257
|
+
href="#"
|
|
6258
|
+
disabled={currentPage === totalPages}
|
|
6259
|
+
onClick={(e) => {
|
|
6260
|
+
e.preventDefault();
|
|
6261
|
+
if (currentPage < totalPages) onPageChange(currentPage + 1);
|
|
6262
|
+
}}
|
|
6263
|
+
/>
|
|
6264
|
+
</PaginationItem>
|
|
6265
|
+
</PaginationContent>
|
|
6266
|
+
</Pagination>
|
|
6267
|
+
);
|
|
6268
|
+
}
|
|
6269
|
+
PaginationWidget.displayName = "PaginationWidget";
|
|
6270
|
+
|
|
6076
6271
|
export {
|
|
6077
6272
|
Pagination,
|
|
6078
6273
|
PaginationContent,
|
|
@@ -6081,6 +6276,7 @@ export {
|
|
|
6081
6276
|
PaginationPrevious,
|
|
6082
6277
|
PaginationNext,
|
|
6083
6278
|
PaginationEllipsis,
|
|
6279
|
+
PaginationWidget,
|
|
6084
6280
|
};
|
|
6085
6281
|
`, prefix)
|
|
6086
6282
|
}
|
|
@@ -8213,6 +8409,423 @@ export interface BankDetailsProps {
|
|
|
8213
8409
|
name: "index.ts",
|
|
8214
8410
|
content: prefixTailwindClasses(`export { BankDetails } from "./bank-details";
|
|
8215
8411
|
export type { BankDetailsProps, BankDetailItem } from "./types";
|
|
8412
|
+
`, prefix)
|
|
8413
|
+
}
|
|
8414
|
+
]
|
|
8415
|
+
},
|
|
8416
|
+
"date-range-modal": {
|
|
8417
|
+
name: "date-range-modal",
|
|
8418
|
+
description: "A modal for selecting a date range with start and end date pickers",
|
|
8419
|
+
category: "custom",
|
|
8420
|
+
dependencies: [
|
|
8421
|
+
"clsx",
|
|
8422
|
+
"tailwind-merge",
|
|
8423
|
+
"lucide-react"
|
|
8424
|
+
],
|
|
8425
|
+
internalDependencies: [
|
|
8426
|
+
"dialog",
|
|
8427
|
+
"button",
|
|
8428
|
+
"input"
|
|
8429
|
+
],
|
|
8430
|
+
isMultiFile: true,
|
|
8431
|
+
directory: "date-range-modal",
|
|
8432
|
+
mainFile: "index.tsx",
|
|
8433
|
+
files: [
|
|
8434
|
+
{
|
|
8435
|
+
name: "index.tsx",
|
|
8436
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8437
|
+
import {
|
|
8438
|
+
Dialog,
|
|
8439
|
+
DialogContent,
|
|
8440
|
+
DialogHeader,
|
|
8441
|
+
DialogTitle,
|
|
8442
|
+
} from "../dialog";
|
|
8443
|
+
import { Button } from "../button";
|
|
8444
|
+
import { DateInput } from "./date-input";
|
|
8445
|
+
|
|
8446
|
+
export interface DateRangeModalProps {
|
|
8447
|
+
open: boolean;
|
|
8448
|
+
onOpenChange: (open: boolean) => void;
|
|
8449
|
+
/** Modal title. Defaults to "Select custom date" */
|
|
8450
|
+
title?: string;
|
|
8451
|
+
/** Called when the user confirms with both dates selected */
|
|
8452
|
+
onConfirm: (start: Date, end: Date) => void;
|
|
8453
|
+
/** Called when the user cancels */
|
|
8454
|
+
onCancel?: () => void;
|
|
8455
|
+
/** Confirm button label. Defaults to "Select custom date range" */
|
|
8456
|
+
confirmButtonText?: string;
|
|
8457
|
+
/** Cancel button label. Defaults to "Cancel" */
|
|
8458
|
+
cancelButtonText?: string;
|
|
8459
|
+
/** Disables confirm button and shows loading state */
|
|
8460
|
+
loading?: boolean;
|
|
8461
|
+
minDate?: Date;
|
|
8462
|
+
maxDate?: Date;
|
|
8463
|
+
}
|
|
8464
|
+
|
|
8465
|
+
function DateRangeModal({
|
|
8466
|
+
open,
|
|
8467
|
+
onOpenChange,
|
|
8468
|
+
title = "Select custom date",
|
|
8469
|
+
onConfirm,
|
|
8470
|
+
onCancel,
|
|
8471
|
+
confirmButtonText = "Select custom date range",
|
|
8472
|
+
cancelButtonText = "Cancel",
|
|
8473
|
+
loading = false,
|
|
8474
|
+
minDate,
|
|
8475
|
+
maxDate,
|
|
8476
|
+
}: DateRangeModalProps) {
|
|
8477
|
+
const [startDate, setStartDate] = React.useState<Date | undefined>(undefined);
|
|
8478
|
+
const [endDate, setEndDate] = React.useState<Date | undefined>(undefined);
|
|
8479
|
+
|
|
8480
|
+
const canConfirm = !!startDate && !!endDate;
|
|
8481
|
+
|
|
8482
|
+
function handleConfirm() {
|
|
8483
|
+
if (startDate && endDate) {
|
|
8484
|
+
onConfirm(startDate, endDate);
|
|
8485
|
+
}
|
|
8486
|
+
}
|
|
8487
|
+
|
|
8488
|
+
function handleCancel() {
|
|
8489
|
+
onCancel?.();
|
|
8490
|
+
onOpenChange(false);
|
|
8491
|
+
}
|
|
8492
|
+
|
|
8493
|
+
// Reset state when modal closes
|
|
8494
|
+
React.useEffect(() => {
|
|
8495
|
+
if (!open) {
|
|
8496
|
+
setStartDate(undefined);
|
|
8497
|
+
setEndDate(undefined);
|
|
8498
|
+
}
|
|
8499
|
+
}, [open]);
|
|
8500
|
+
|
|
8501
|
+
return (
|
|
8502
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
8503
|
+
<DialogContent className="sm:max-w-md">
|
|
8504
|
+
<DialogHeader>
|
|
8505
|
+
<DialogTitle>{title}</DialogTitle>
|
|
8506
|
+
</DialogHeader>
|
|
8507
|
+
|
|
8508
|
+
<hr className="border-semantic-border-layout -mx-6" />
|
|
8509
|
+
|
|
8510
|
+
<div className="flex flex-col gap-4 py-2">
|
|
8511
|
+
<DateInput
|
|
8512
|
+
label="Start date"
|
|
8513
|
+
value={startDate}
|
|
8514
|
+
onChange={setStartDate}
|
|
8515
|
+
placeholder="MM/DD/YYYY"
|
|
8516
|
+
minDate={minDate}
|
|
8517
|
+
maxDate={maxDate}
|
|
8518
|
+
/>
|
|
8519
|
+
<DateInput
|
|
8520
|
+
label="End date"
|
|
8521
|
+
value={endDate}
|
|
8522
|
+
onChange={setEndDate}
|
|
8523
|
+
placeholder="MM/DD/YYYY"
|
|
8524
|
+
minDate={startDate ?? minDate}
|
|
8525
|
+
maxDate={maxDate}
|
|
8526
|
+
/>
|
|
8527
|
+
</div>
|
|
8528
|
+
|
|
8529
|
+
<hr className="border-semantic-border-layout -mx-6" />
|
|
8530
|
+
|
|
8531
|
+
<div className="flex items-center justify-end gap-3 pt-1">
|
|
8532
|
+
<Button variant="outline" onClick={handleCancel} disabled={loading}>
|
|
8533
|
+
{cancelButtonText}
|
|
8534
|
+
</Button>
|
|
8535
|
+
<Button
|
|
8536
|
+
onClick={handleConfirm}
|
|
8537
|
+
disabled={!canConfirm || loading}
|
|
8538
|
+
>
|
|
8539
|
+
{loading ? "Loading..." : confirmButtonText}
|
|
8540
|
+
</Button>
|
|
8541
|
+
</div>
|
|
8542
|
+
</DialogContent>
|
|
8543
|
+
</Dialog>
|
|
8544
|
+
);
|
|
8545
|
+
}
|
|
8546
|
+
DateRangeModal.displayName = "DateRangeModal";
|
|
8547
|
+
|
|
8548
|
+
export { DateRangeModal };
|
|
8549
|
+
`, prefix)
|
|
8550
|
+
},
|
|
8551
|
+
{
|
|
8552
|
+
name: "calendar.tsx",
|
|
8553
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8554
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
8555
|
+
import { cn } from "../../../lib/utils";
|
|
8556
|
+
|
|
8557
|
+
const DAYS_OF_WEEK = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
|
8558
|
+
|
|
8559
|
+
const MONTHS = [
|
|
8560
|
+
"January", "February", "March", "April", "May", "June",
|
|
8561
|
+
"July", "August", "September", "October", "November", "December",
|
|
8562
|
+
];
|
|
8563
|
+
|
|
8564
|
+
export interface CalendarProps {
|
|
8565
|
+
value?: Date;
|
|
8566
|
+
onChange: (date: Date) => void;
|
|
8567
|
+
minDate?: Date;
|
|
8568
|
+
maxDate?: Date;
|
|
8569
|
+
}
|
|
8570
|
+
|
|
8571
|
+
function isSameDay(a: Date, b: Date): boolean {
|
|
8572
|
+
return (
|
|
8573
|
+
a.getFullYear() === b.getFullYear() &&
|
|
8574
|
+
a.getMonth() === b.getMonth() &&
|
|
8575
|
+
a.getDate() === b.getDate()
|
|
8576
|
+
);
|
|
8577
|
+
}
|
|
8578
|
+
|
|
8579
|
+
function isBeforeDay(a: Date, b: Date): boolean {
|
|
8580
|
+
const aD = new Date(a.getFullYear(), a.getMonth(), a.getDate());
|
|
8581
|
+
const bD = new Date(b.getFullYear(), b.getMonth(), b.getDate());
|
|
8582
|
+
return aD < bD;
|
|
8583
|
+
}
|
|
8584
|
+
|
|
8585
|
+
function isAfterDay(a: Date, b: Date): boolean {
|
|
8586
|
+
const aD = new Date(a.getFullYear(), a.getMonth(), a.getDate());
|
|
8587
|
+
const bD = new Date(b.getFullYear(), b.getMonth(), b.getDate());
|
|
8588
|
+
return aD > bD;
|
|
8589
|
+
}
|
|
8590
|
+
|
|
8591
|
+
function getDaysInMonth(year: number, month: number): number {
|
|
8592
|
+
return new Date(year, month + 1, 0).getDate();
|
|
8593
|
+
}
|
|
8594
|
+
|
|
8595
|
+
function getFirstDayOfWeek(year: number, month: number): number {
|
|
8596
|
+
return new Date(year, month, 1).getDay();
|
|
8597
|
+
}
|
|
8598
|
+
|
|
8599
|
+
function Calendar({ value, onChange, minDate, maxDate }: CalendarProps) {
|
|
8600
|
+
const today = new Date();
|
|
8601
|
+
const initial = value ?? today;
|
|
8602
|
+
|
|
8603
|
+
const [viewYear, setViewYear] = React.useState(initial.getFullYear());
|
|
8604
|
+
const [viewMonth, setViewMonth] = React.useState(initial.getMonth());
|
|
8605
|
+
|
|
8606
|
+
const daysInMonth = getDaysInMonth(viewYear, viewMonth);
|
|
8607
|
+
const firstDayOfWeek = getFirstDayOfWeek(viewYear, viewMonth);
|
|
8608
|
+
|
|
8609
|
+
// Previous month fill days
|
|
8610
|
+
const prevMonthDays = getDaysInMonth(
|
|
8611
|
+
viewMonth === 0 ? viewYear - 1 : viewYear,
|
|
8612
|
+
viewMonth === 0 ? 11 : viewMonth - 1
|
|
8613
|
+
);
|
|
8614
|
+
|
|
8615
|
+
const handlePrevMonth = () => {
|
|
8616
|
+
if (viewMonth === 0) {
|
|
8617
|
+
setViewMonth(11);
|
|
8618
|
+
setViewYear((y) => y - 1);
|
|
8619
|
+
} else {
|
|
8620
|
+
setViewMonth((m) => m - 1);
|
|
8621
|
+
}
|
|
8622
|
+
};
|
|
8623
|
+
|
|
8624
|
+
const handleNextMonth = () => {
|
|
8625
|
+
if (viewMonth === 11) {
|
|
8626
|
+
setViewMonth(0);
|
|
8627
|
+
setViewYear((y) => y + 1);
|
|
8628
|
+
} else {
|
|
8629
|
+
setViewMonth((m) => m + 1);
|
|
8630
|
+
}
|
|
8631
|
+
};
|
|
8632
|
+
|
|
8633
|
+
const cells: { day: number; currentMonth: boolean; date: Date }[] = [];
|
|
8634
|
+
|
|
8635
|
+
// Fill leading days from previous month
|
|
8636
|
+
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
|
|
8637
|
+
const d = prevMonthDays - i;
|
|
8638
|
+
const prevMonth = viewMonth === 0 ? 11 : viewMonth - 1;
|
|
8639
|
+
const prevYear = viewMonth === 0 ? viewYear - 1 : viewYear;
|
|
8640
|
+
cells.push({ day: d, currentMonth: false, date: new Date(prevYear, prevMonth, d) });
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8643
|
+
// Current month days
|
|
8644
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
8645
|
+
cells.push({ day: d, currentMonth: true, date: new Date(viewYear, viewMonth, d) });
|
|
8646
|
+
}
|
|
8647
|
+
|
|
8648
|
+
// Fill trailing days from next month
|
|
8649
|
+
const remainder = cells.length % 7;
|
|
8650
|
+
if (remainder !== 0) {
|
|
8651
|
+
const nextMonth = viewMonth === 11 ? 0 : viewMonth + 1;
|
|
8652
|
+
const nextYear = viewMonth === 11 ? viewYear + 1 : viewYear;
|
|
8653
|
+
for (let d = 1; d <= 7 - remainder; d++) {
|
|
8654
|
+
cells.push({ day: d, currentMonth: false, date: new Date(nextYear, nextMonth, d) });
|
|
8655
|
+
}
|
|
8656
|
+
}
|
|
8657
|
+
|
|
8658
|
+
return (
|
|
8659
|
+
<div className="select-none w-full">
|
|
8660
|
+
{/* Header */}
|
|
8661
|
+
<div className="flex items-center justify-between mb-3">
|
|
8662
|
+
<button
|
|
8663
|
+
type="button"
|
|
8664
|
+
onClick={handlePrevMonth}
|
|
8665
|
+
className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
|
|
8666
|
+
aria-label="Previous month"
|
|
8667
|
+
>
|
|
8668
|
+
<ChevronLeftIcon className="size-4" />
|
|
8669
|
+
</button>
|
|
8670
|
+
<span className="text-sm font-semibold text-semantic-text-primary">
|
|
8671
|
+
{MONTHS[viewMonth]} {viewYear}
|
|
8672
|
+
</span>
|
|
8673
|
+
<button
|
|
8674
|
+
type="button"
|
|
8675
|
+
onClick={handleNextMonth}
|
|
8676
|
+
className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
|
|
8677
|
+
aria-label="Next month"
|
|
8678
|
+
>
|
|
8679
|
+
<ChevronRightIcon className="size-4" />
|
|
8680
|
+
</button>
|
|
8681
|
+
</div>
|
|
8682
|
+
|
|
8683
|
+
{/* Days of week */}
|
|
8684
|
+
<div className="grid grid-cols-7 mb-1">
|
|
8685
|
+
{DAYS_OF_WEEK.map((d) => (
|
|
8686
|
+
<div
|
|
8687
|
+
key={d}
|
|
8688
|
+
className="text-center text-xs font-medium text-semantic-text-muted py-1"
|
|
8689
|
+
>
|
|
8690
|
+
{d}
|
|
8691
|
+
</div>
|
|
8692
|
+
))}
|
|
8693
|
+
</div>
|
|
8694
|
+
|
|
8695
|
+
{/* Day grid */}
|
|
8696
|
+
<div className="grid grid-cols-7">
|
|
8697
|
+
{cells.map(({ day, currentMonth, date }, idx) => {
|
|
8698
|
+
const isToday = currentMonth && isSameDay(date, today);
|
|
8699
|
+
const isSelected = value ? isSameDay(date, value) : false;
|
|
8700
|
+
const isDisabled =
|
|
8701
|
+
(minDate && isBeforeDay(date, minDate)) ||
|
|
8702
|
+
(maxDate && isAfterDay(date, maxDate));
|
|
8703
|
+
|
|
8704
|
+
return (
|
|
8705
|
+
<button
|
|
8706
|
+
key={idx}
|
|
8707
|
+
type="button"
|
|
8708
|
+
disabled={!!isDisabled}
|
|
8709
|
+
onClick={() => {
|
|
8710
|
+
if (!isDisabled) onChange(date);
|
|
8711
|
+
}}
|
|
8712
|
+
className={cn(
|
|
8713
|
+
"relative flex items-center justify-center size-8 mx-auto rounded-full text-xs transition-colors",
|
|
8714
|
+
isSelected
|
|
8715
|
+
? "bg-semantic-primary text-semantic-text-inverted font-semibold"
|
|
8716
|
+
: currentMonth
|
|
8717
|
+
? "text-semantic-text-primary hover:bg-semantic-bg-hover"
|
|
8718
|
+
: "text-semantic-text-muted hover:bg-semantic-bg-hover",
|
|
8719
|
+
isDisabled && "opacity-40 cursor-not-allowed pointer-events-none"
|
|
8720
|
+
)}
|
|
8721
|
+
aria-label={date.toDateString()}
|
|
8722
|
+
aria-pressed={isSelected}
|
|
8723
|
+
aria-current={isToday ? "date" : undefined}
|
|
8724
|
+
>
|
|
8725
|
+
{day}
|
|
8726
|
+
{isToday && !isSelected && (
|
|
8727
|
+
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-semantic-primary" />
|
|
8728
|
+
)}
|
|
8729
|
+
</button>
|
|
8730
|
+
);
|
|
8731
|
+
})}
|
|
8732
|
+
</div>
|
|
8733
|
+
</div>
|
|
8734
|
+
);
|
|
8735
|
+
}
|
|
8736
|
+
Calendar.displayName = "Calendar";
|
|
8737
|
+
|
|
8738
|
+
export { Calendar };
|
|
8739
|
+
`, prefix)
|
|
8740
|
+
},
|
|
8741
|
+
{
|
|
8742
|
+
name: "date-input.tsx",
|
|
8743
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8744
|
+
import { CalendarIcon } from "lucide-react";
|
|
8745
|
+
import { cn } from "../../../lib/utils";
|
|
8746
|
+
import { Calendar } from "./calendar";
|
|
8747
|
+
|
|
8748
|
+
export interface DateInputProps {
|
|
8749
|
+
label: string;
|
|
8750
|
+
value?: Date;
|
|
8751
|
+
onChange: (date: Date) => void;
|
|
8752
|
+
placeholder?: string;
|
|
8753
|
+
minDate?: Date;
|
|
8754
|
+
maxDate?: Date;
|
|
8755
|
+
}
|
|
8756
|
+
|
|
8757
|
+
function formatDate(date: Date): string {
|
|
8758
|
+
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
8759
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
8760
|
+
const yyyy = date.getFullYear();
|
|
8761
|
+
return \`\${mm}/\${dd}/\${yyyy}\`;
|
|
8762
|
+
}
|
|
8763
|
+
|
|
8764
|
+
function DateInput({
|
|
8765
|
+
label,
|
|
8766
|
+
value,
|
|
8767
|
+
onChange,
|
|
8768
|
+
placeholder = "MM/DD/YYYY",
|
|
8769
|
+
minDate,
|
|
8770
|
+
maxDate,
|
|
8771
|
+
}: DateInputProps) {
|
|
8772
|
+
const [open, setOpen] = React.useState(false);
|
|
8773
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
8774
|
+
|
|
8775
|
+
// Close on outside click
|
|
8776
|
+
React.useEffect(() => {
|
|
8777
|
+
function handleClick(e: MouseEvent) {
|
|
8778
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
8779
|
+
setOpen(false);
|
|
8780
|
+
}
|
|
8781
|
+
}
|
|
8782
|
+
if (open) {
|
|
8783
|
+
document.addEventListener("mousedown", handleClick);
|
|
8784
|
+
}
|
|
8785
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
8786
|
+
}, [open]);
|
|
8787
|
+
|
|
8788
|
+
return (
|
|
8789
|
+
<div ref={containerRef} className="relative flex flex-col gap-1.5">
|
|
8790
|
+
<label className="text-sm font-medium text-semantic-text-primary">
|
|
8791
|
+
{label}
|
|
8792
|
+
</label>
|
|
8793
|
+
<button
|
|
8794
|
+
type="button"
|
|
8795
|
+
onClick={() => setOpen((o) => !o)}
|
|
8796
|
+
className={cn(
|
|
8797
|
+
"flex items-center justify-between gap-2 w-full px-3 py-2 rounded-md border text-sm transition-colors outline-none",
|
|
8798
|
+
"border-semantic-border-input bg-semantic-bg-primary text-semantic-text-primary",
|
|
8799
|
+
"hover:border-semantic-border-input-focus/50",
|
|
8800
|
+
open && "border-semantic-border-input-focus/50 shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
|
|
8801
|
+
!value && "text-semantic-text-muted"
|
|
8802
|
+
)}
|
|
8803
|
+
aria-haspopup="dialog"
|
|
8804
|
+
aria-expanded={open}
|
|
8805
|
+
>
|
|
8806
|
+
<span>{value ? formatDate(value) : placeholder}</span>
|
|
8807
|
+
<CalendarIcon className="size-4 text-semantic-text-muted shrink-0" />
|
|
8808
|
+
</button>
|
|
8809
|
+
|
|
8810
|
+
{open && (
|
|
8811
|
+
<div className="absolute top-full left-0 z-50 mt-1 w-72 rounded-lg border border-semantic-border-layout bg-semantic-bg-primary shadow-lg p-3">
|
|
8812
|
+
<Calendar
|
|
8813
|
+
value={value}
|
|
8814
|
+
onChange={(date) => {
|
|
8815
|
+
onChange(date);
|
|
8816
|
+
setOpen(false);
|
|
8817
|
+
}}
|
|
8818
|
+
minDate={minDate}
|
|
8819
|
+
maxDate={maxDate}
|
|
8820
|
+
/>
|
|
8821
|
+
</div>
|
|
8822
|
+
)}
|
|
8823
|
+
</div>
|
|
8824
|
+
);
|
|
8825
|
+
}
|
|
8826
|
+
DateInput.displayName = "DateInput";
|
|
8827
|
+
|
|
8828
|
+
export { DateInput };
|
|
8216
8829
|
`, prefix)
|
|
8217
8830
|
}
|
|
8218
8831
|
]
|