myoperator-mcp 0.2.373 → 0.2.375
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 +1023 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2562,6 +2562,777 @@ const DateDivider = React.forwardRef(
|
|
|
2562
2562
|
DateDivider.displayName = "DateDivider";
|
|
2563
2563
|
|
|
2564
2564
|
export { DateDivider };
|
|
2565
|
+
`,
|
|
2566
|
+
"date-range-picker": `import * as React from "react";
|
|
2567
|
+
import { createPortal } from "react-dom";
|
|
2568
|
+
import {
|
|
2569
|
+
autoUpdate,
|
|
2570
|
+
flip,
|
|
2571
|
+
offset,
|
|
2572
|
+
shift,
|
|
2573
|
+
size as floatingSize,
|
|
2574
|
+
useFloating,
|
|
2575
|
+
type Placement,
|
|
2576
|
+
type Strategy,
|
|
2577
|
+
} from "@floating-ui/react-dom";
|
|
2578
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2579
|
+
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
|
2580
|
+
|
|
2581
|
+
import { cn } from "@/lib/utils";
|
|
2582
|
+
import {
|
|
2583
|
+
DropdownMenu,
|
|
2584
|
+
DropdownMenuContent,
|
|
2585
|
+
DropdownMenuItem,
|
|
2586
|
+
DropdownMenuTrigger,
|
|
2587
|
+
} from "./dropdown-menu";
|
|
2588
|
+
|
|
2589
|
+
const DEFAULT_PLACEHOLDER = "Date Range";
|
|
2590
|
+
const POPOVER_MARGIN = 8;
|
|
2591
|
+
const POPOVER_GAP = 4;
|
|
2592
|
+
const MAX_POPOVER_HEIGHT = 420;
|
|
2593
|
+
const POPOVER_SCROLL_HEIGHT_VAR = "--date-range-picker-scroll-height";
|
|
2594
|
+
const CALENDAR_PLACEMENT: Placement = "bottom-start";
|
|
2595
|
+
// Above the calendar popover's own z-index (10050) so the month/year
|
|
2596
|
+
// dropdowns render on top of it rather than behind.
|
|
2597
|
+
const CALENDAR_DROPDOWN_Z_INDEX = "z-[10060]";
|
|
2598
|
+
const YEAR_OPTIONS_SPAN = 10;
|
|
2599
|
+
|
|
2600
|
+
const weekDays = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
|
2601
|
+
const MONTH_SHORT_NAMES = [
|
|
2602
|
+
"Jan",
|
|
2603
|
+
"Feb",
|
|
2604
|
+
"Mar",
|
|
2605
|
+
"Apr",
|
|
2606
|
+
"May",
|
|
2607
|
+
"Jun",
|
|
2608
|
+
"Jul",
|
|
2609
|
+
"Aug",
|
|
2610
|
+
"Sep",
|
|
2611
|
+
"Oct",
|
|
2612
|
+
"Nov",
|
|
2613
|
+
"Dec",
|
|
2614
|
+
];
|
|
2615
|
+
const MONTH_LONG_NAMES = [
|
|
2616
|
+
"January",
|
|
2617
|
+
"February",
|
|
2618
|
+
"March",
|
|
2619
|
+
"April",
|
|
2620
|
+
"May",
|
|
2621
|
+
"June",
|
|
2622
|
+
"July",
|
|
2623
|
+
"August",
|
|
2624
|
+
"September",
|
|
2625
|
+
"October",
|
|
2626
|
+
"November",
|
|
2627
|
+
"December",
|
|
2628
|
+
];
|
|
2629
|
+
|
|
2630
|
+
const dateRangePickerTriggerVariants = cva(
|
|
2631
|
+
"flex h-10 w-full items-center gap-2 rounded-lg border border-solid border-semantic-border-input bg-semantic-bg-primary px-4 py-2.5 text-left text-sm text-semantic-text-primary outline-none transition-colors hover:border-semantic-border-input-focus/50 disabled:cursor-not-allowed disabled:opacity-50",
|
|
2632
|
+
{
|
|
2633
|
+
variants: {
|
|
2634
|
+
state: {
|
|
2635
|
+
default: "",
|
|
2636
|
+
error:
|
|
2637
|
+
"border-semantic-error-primary hover:border-semantic-error-primary",
|
|
2638
|
+
},
|
|
2639
|
+
},
|
|
2640
|
+
defaultVariants: {
|
|
2641
|
+
state: "default",
|
|
2642
|
+
},
|
|
2643
|
+
}
|
|
2644
|
+
);
|
|
2645
|
+
|
|
2646
|
+
export interface DateRangeValue {
|
|
2647
|
+
start?: Date;
|
|
2648
|
+
end?: Date;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
export interface DateRangePreset {
|
|
2652
|
+
label: string;
|
|
2653
|
+
getRange: () => DateRangeValue;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
export interface DateRangePickerProps
|
|
2657
|
+
extends
|
|
2658
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange">,
|
|
2659
|
+
Pick<VariantProps<typeof dateRangePickerTriggerVariants>, "state"> {
|
|
2660
|
+
value?: DateRangeValue;
|
|
2661
|
+
defaultValue?: DateRangeValue;
|
|
2662
|
+
onValueChange?: (value: DateRangeValue) => void;
|
|
2663
|
+
/** Trigger placeholder text when no range is selected. Defaults to "Date Range". */
|
|
2664
|
+
placeholder?: string;
|
|
2665
|
+
/**
|
|
2666
|
+
* Presets shown in the left column. Defaults to
|
|
2667
|
+
* Today/Yesterday/Last 7 days/Last 30 days/This month/Last month.
|
|
2668
|
+
* Pass an empty array to hide the presets column entirely.
|
|
2669
|
+
*/
|
|
2670
|
+
presets?: DateRangePreset[];
|
|
2671
|
+
minDate?: Date;
|
|
2672
|
+
maxDate?: Date;
|
|
2673
|
+
disablePastDates?: boolean;
|
|
2674
|
+
disabled?: boolean;
|
|
2675
|
+
open?: boolean;
|
|
2676
|
+
defaultOpen?: boolean;
|
|
2677
|
+
onOpenChange?: (open: boolean) => void;
|
|
2678
|
+
portalContainer?: HTMLElement | null;
|
|
2679
|
+
/** Custom display formatter for the trigger's filled-state text. Defaults to "D MMM YYYY - D MMM YYYY". */
|
|
2680
|
+
formatRange?: (value: DateRangeValue) => string;
|
|
2681
|
+
/** Additional className merged onto the trigger button (overrides base trigger styling) */
|
|
2682
|
+
triggerClassName?: string;
|
|
2683
|
+
/** Additional className merged onto the trigger's label span \u2014 e.g. \`"hidden sm:inline"\` to collapse to an icon-only button below a breakpoint */
|
|
2684
|
+
triggerLabelClassName?: string;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
function normalizeValue(value?: DateRangeValue): DateRangeValue {
|
|
2688
|
+
return { start: value?.start, end: value?.end };
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
function isSameDay(a?: Date, b?: Date) {
|
|
2692
|
+
if (!a || !b) return false;
|
|
2693
|
+
|
|
2694
|
+
return (
|
|
2695
|
+
a.getFullYear() === b.getFullYear() &&
|
|
2696
|
+
a.getMonth() === b.getMonth() &&
|
|
2697
|
+
a.getDate() === b.getDate()
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
function startOfDay(date: Date) {
|
|
2702
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function isBeforeDay(date: Date, minDate: Date) {
|
|
2706
|
+
return startOfDay(date).getTime() < startOfDay(minDate).getTime();
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
function isAfterDay(date: Date, maxDate: Date) {
|
|
2710
|
+
return startOfDay(date).getTime() > startOfDay(maxDate).getTime();
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
function startOfMonth(date: Date) {
|
|
2714
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function addMonths(date: Date, months: number) {
|
|
2718
|
+
return new Date(date.getFullYear(), date.getMonth() + months, 1);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
function addDays(date: Date, days: number) {
|
|
2722
|
+
const result = new Date(date);
|
|
2723
|
+
result.setDate(result.getDate() + days);
|
|
2724
|
+
return result;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function getCalendarDays(month: Date) {
|
|
2728
|
+
const firstDay = startOfMonth(month);
|
|
2729
|
+
const gridStart = new Date(firstDay);
|
|
2730
|
+
gridStart.setDate(firstDay.getDate() - firstDay.getDay());
|
|
2731
|
+
|
|
2732
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
2733
|
+
const day = new Date(gridStart);
|
|
2734
|
+
day.setDate(gridStart.getDate() + index);
|
|
2735
|
+
return day;
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function isPointerInsideElement(
|
|
2740
|
+
event: MouseEvent,
|
|
2741
|
+
element: HTMLElement | null
|
|
2742
|
+
) {
|
|
2743
|
+
if (!element) return false;
|
|
2744
|
+
|
|
2745
|
+
const target = event.target;
|
|
2746
|
+
if (target instanceof Node && element.contains(target)) return true;
|
|
2747
|
+
|
|
2748
|
+
if (typeof event.composedPath === "function") {
|
|
2749
|
+
return event
|
|
2750
|
+
.composedPath()
|
|
2751
|
+
.some((node) => node instanceof Node && element.contains(node));
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
return false;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
function formatDateShort(date: Date) {
|
|
2758
|
+
return \`\${date.getDate()} \${MONTH_SHORT_NAMES[date.getMonth()]} \${date.getFullYear()}\`;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
function defaultFormatRange(value: DateRangeValue) {
|
|
2762
|
+
if (value.start && value.end) {
|
|
2763
|
+
return \`\${formatDateShort(value.start)} - \${formatDateShort(value.end)}\`;
|
|
2764
|
+
}
|
|
2765
|
+
if (value.start) return formatDateShort(value.start);
|
|
2766
|
+
if (value.end) return formatDateShort(value.end);
|
|
2767
|
+
|
|
2768
|
+
return "";
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
/**
|
|
2772
|
+
* Default presets shown in the DateRangePicker's left column. Exported so
|
|
2773
|
+
* consumers can reference, filter, or extend the list.
|
|
2774
|
+
*/
|
|
2775
|
+
export const DEFAULT_DATE_RANGE_PRESETS: DateRangePreset[] = [
|
|
2776
|
+
{
|
|
2777
|
+
label: "Today",
|
|
2778
|
+
getRange: () => {
|
|
2779
|
+
const today = startOfDay(new Date());
|
|
2780
|
+
return { start: today, end: today };
|
|
2781
|
+
},
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
label: "Yesterday",
|
|
2785
|
+
getRange: () => {
|
|
2786
|
+
const yesterday = startOfDay(addDays(new Date(), -1));
|
|
2787
|
+
return { start: yesterday, end: yesterday };
|
|
2788
|
+
},
|
|
2789
|
+
},
|
|
2790
|
+
{
|
|
2791
|
+
label: "Last 7 days",
|
|
2792
|
+
getRange: () => {
|
|
2793
|
+
const today = startOfDay(new Date());
|
|
2794
|
+
return { start: addDays(today, -6), end: today };
|
|
2795
|
+
},
|
|
2796
|
+
},
|
|
2797
|
+
{
|
|
2798
|
+
label: "Last 30 days",
|
|
2799
|
+
getRange: () => {
|
|
2800
|
+
const today = startOfDay(new Date());
|
|
2801
|
+
return { start: addDays(today, -29), end: today };
|
|
2802
|
+
},
|
|
2803
|
+
},
|
|
2804
|
+
{
|
|
2805
|
+
label: "This month",
|
|
2806
|
+
getRange: () => {
|
|
2807
|
+
const today = startOfDay(new Date());
|
|
2808
|
+
return { start: startOfMonth(today), end: today };
|
|
2809
|
+
},
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
label: "Last month",
|
|
2813
|
+
getRange: () => {
|
|
2814
|
+
const start = addMonths(startOfMonth(new Date()), -1);
|
|
2815
|
+
const end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
|
|
2816
|
+
return { start, end };
|
|
2817
|
+
},
|
|
2818
|
+
},
|
|
2819
|
+
];
|
|
2820
|
+
|
|
2821
|
+
function FigmaCalendarIcon({ className }: { className?: string }) {
|
|
2822
|
+
return (
|
|
2823
|
+
<svg
|
|
2824
|
+
viewBox="0 0 18 18"
|
|
2825
|
+
fill="none"
|
|
2826
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2827
|
+
className={className}
|
|
2828
|
+
aria-hidden="true"
|
|
2829
|
+
>
|
|
2830
|
+
<path
|
|
2831
|
+
d="M6 1.5V4.5M12 1.5V4.5M2.25 7.5H15.75M3.75 3H14.25C15.0784 3 15.75 3.67157 15.75 4.5V15C15.75 15.8284 15.0784 16.5 14.25 16.5H3.75C2.92157 16.5 2.25 15.8284 2.25 15V4.5C2.25 3.67157 2.92157 3 3.75 3Z"
|
|
2832
|
+
stroke="currentColor"
|
|
2833
|
+
strokeWidth="0.8"
|
|
2834
|
+
strokeLinecap="round"
|
|
2835
|
+
strokeLinejoin="round"
|
|
2836
|
+
/>
|
|
2837
|
+
</svg>
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
const DateRangePicker = React.forwardRef<HTMLDivElement, DateRangePickerProps>(
|
|
2842
|
+
(
|
|
2843
|
+
{
|
|
2844
|
+
className,
|
|
2845
|
+
state,
|
|
2846
|
+
value,
|
|
2847
|
+
defaultValue,
|
|
2848
|
+
onValueChange,
|
|
2849
|
+
placeholder = DEFAULT_PLACEHOLDER,
|
|
2850
|
+
presets = DEFAULT_DATE_RANGE_PRESETS,
|
|
2851
|
+
minDate,
|
|
2852
|
+
maxDate,
|
|
2853
|
+
disablePastDates = false,
|
|
2854
|
+
disabled = false,
|
|
2855
|
+
open: controlledOpen,
|
|
2856
|
+
defaultOpen = false,
|
|
2857
|
+
onOpenChange,
|
|
2858
|
+
portalContainer,
|
|
2859
|
+
formatRange,
|
|
2860
|
+
triggerClassName,
|
|
2861
|
+
triggerLabelClassName,
|
|
2862
|
+
id,
|
|
2863
|
+
...props
|
|
2864
|
+
},
|
|
2865
|
+
ref
|
|
2866
|
+
) => {
|
|
2867
|
+
const generatedId = React.useId();
|
|
2868
|
+
const triggerId = id ?? generatedId;
|
|
2869
|
+
const isValueControlled = value !== undefined;
|
|
2870
|
+
const isOpenControlled = controlledOpen !== undefined;
|
|
2871
|
+
const [internalValue, setInternalValue] = React.useState(() =>
|
|
2872
|
+
normalizeValue(defaultValue)
|
|
2873
|
+
);
|
|
2874
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
2875
|
+
const currentValue = normalizeValue(
|
|
2876
|
+
isValueControlled ? value : internalValue
|
|
2877
|
+
);
|
|
2878
|
+
const open = isOpenControlled ? controlledOpen : internalOpen;
|
|
2879
|
+
|
|
2880
|
+
// Draft state \u2014 the calendar operates on a working copy of the range so
|
|
2881
|
+
// that Cancel can discard in-progress selections and Apply commits them.
|
|
2882
|
+
const [draftValue, setDraftValue] = React.useState<DateRangeValue>(
|
|
2883
|
+
() => currentValue
|
|
2884
|
+
);
|
|
2885
|
+
// Tracks whether the next day click should complete the range (true) or
|
|
2886
|
+
// start a new one (false). Drives the "click 1 = start, click 2 = end,
|
|
2887
|
+
// click 3 = restart" cycle.
|
|
2888
|
+
const [pendingEnd, setPendingEnd] = React.useState(false);
|
|
2889
|
+
const [visibleMonth, setVisibleMonth] = React.useState(() =>
|
|
2890
|
+
startOfMonth(currentValue.start ?? currentValue.end ?? new Date())
|
|
2891
|
+
);
|
|
2892
|
+
|
|
2893
|
+
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
|
2894
|
+
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
|
|
2895
|
+
const popoverRef = React.useRef<HTMLDivElement | null>(null);
|
|
2896
|
+
const usesContainerPortal = portalContainer !== undefined;
|
|
2897
|
+
const floatingStrategy: Strategy = usesContainerPortal
|
|
2898
|
+
? "absolute"
|
|
2899
|
+
: "fixed";
|
|
2900
|
+
const floatingMiddleware = React.useMemo(
|
|
2901
|
+
() => [
|
|
2902
|
+
offset(POPOVER_GAP),
|
|
2903
|
+
flip({ padding: POPOVER_MARGIN }),
|
|
2904
|
+
shift({ padding: POPOVER_MARGIN }),
|
|
2905
|
+
floatingSize({
|
|
2906
|
+
padding: POPOVER_MARGIN,
|
|
2907
|
+
apply({ availableHeight, elements }) {
|
|
2908
|
+
const maxHeight = Math.max(
|
|
2909
|
+
1,
|
|
2910
|
+
Math.min(MAX_POPOVER_HEIGHT, availableHeight)
|
|
2911
|
+
);
|
|
2912
|
+
elements.floating.style.setProperty(
|
|
2913
|
+
POPOVER_SCROLL_HEIGHT_VAR,
|
|
2914
|
+
\`\${maxHeight}px\`
|
|
2915
|
+
);
|
|
2916
|
+
},
|
|
2917
|
+
}),
|
|
2918
|
+
],
|
|
2919
|
+
[]
|
|
2920
|
+
);
|
|
2921
|
+
const { refs, floatingStyles, isPositioned } =
|
|
2922
|
+
useFloating<HTMLButtonElement>({
|
|
2923
|
+
open,
|
|
2924
|
+
placement: CALENDAR_PLACEMENT,
|
|
2925
|
+
strategy: floatingStrategy,
|
|
2926
|
+
transform: false,
|
|
2927
|
+
middleware: floatingMiddleware,
|
|
2928
|
+
whileElementsMounted: (reference, floating, update) =>
|
|
2929
|
+
autoUpdate(reference, floating, update, { animationFrame: true }),
|
|
2930
|
+
});
|
|
2931
|
+
const calendarDays = React.useMemo(
|
|
2932
|
+
() => getCalendarDays(visibleMonth),
|
|
2933
|
+
[visibleMonth]
|
|
2934
|
+
);
|
|
2935
|
+
const displayValue = (formatRange ?? defaultFormatRange)(currentValue);
|
|
2936
|
+
const effectiveMinDate = React.useMemo(() => {
|
|
2937
|
+
if (!disablePastDates) return minDate;
|
|
2938
|
+
|
|
2939
|
+
const today = startOfDay(new Date());
|
|
2940
|
+
if (!minDate) return today;
|
|
2941
|
+
|
|
2942
|
+
return isBeforeDay(minDate, today) ? today : minDate;
|
|
2943
|
+
}, [disablePastDates, minDate]);
|
|
2944
|
+
const portalMount =
|
|
2945
|
+
typeof document !== "undefined"
|
|
2946
|
+
? usesContainerPortal
|
|
2947
|
+
? portalContainer
|
|
2948
|
+
: document.body
|
|
2949
|
+
: null;
|
|
2950
|
+
|
|
2951
|
+
const setOpen = React.useCallback(
|
|
2952
|
+
(nextOpen: boolean) => {
|
|
2953
|
+
if (!isOpenControlled) {
|
|
2954
|
+
setInternalOpen(nextOpen);
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
onOpenChange?.(nextOpen);
|
|
2958
|
+
},
|
|
2959
|
+
[isOpenControlled, onOpenChange]
|
|
2960
|
+
);
|
|
2961
|
+
|
|
2962
|
+
const setTriggerRef = React.useCallback(
|
|
2963
|
+
(node: HTMLButtonElement | null) => {
|
|
2964
|
+
triggerRef.current = node;
|
|
2965
|
+
refs.setReference(node);
|
|
2966
|
+
},
|
|
2967
|
+
[refs]
|
|
2968
|
+
);
|
|
2969
|
+
|
|
2970
|
+
const setPopoverRef = React.useCallback(
|
|
2971
|
+
(node: HTMLDivElement | null) => {
|
|
2972
|
+
popoverRef.current = node;
|
|
2973
|
+
refs.setFloating(node);
|
|
2974
|
+
},
|
|
2975
|
+
[refs]
|
|
2976
|
+
);
|
|
2977
|
+
|
|
2978
|
+
// Reset the draft to the last committed value (and jump the calendar to
|
|
2979
|
+
// that month) each time the popover opens, so a Cancel from a previous
|
|
2980
|
+
// session never leaks into the next one.
|
|
2981
|
+
const currentValueRef = React.useRef(currentValue);
|
|
2982
|
+
React.useEffect(() => {
|
|
2983
|
+
currentValueRef.current = currentValue;
|
|
2984
|
+
});
|
|
2985
|
+
|
|
2986
|
+
React.useEffect(() => {
|
|
2987
|
+
if (!open) return;
|
|
2988
|
+
|
|
2989
|
+
const latestValue = currentValueRef.current;
|
|
2990
|
+
setDraftValue(latestValue);
|
|
2991
|
+
setPendingEnd(false);
|
|
2992
|
+
setVisibleMonth(
|
|
2993
|
+
startOfMonth(latestValue.start ?? latestValue.end ?? new Date())
|
|
2994
|
+
);
|
|
2995
|
+
}, [open]);
|
|
2996
|
+
|
|
2997
|
+
React.useEffect(() => {
|
|
2998
|
+
if (!open) return;
|
|
2999
|
+
|
|
3000
|
+
const handlePointerDown = (event: MouseEvent) => {
|
|
3001
|
+
if (
|
|
3002
|
+
!isPointerInsideElement(event, rootRef.current) &&
|
|
3003
|
+
!isPointerInsideElement(event, popoverRef.current)
|
|
3004
|
+
) {
|
|
3005
|
+
setOpen(false);
|
|
3006
|
+
}
|
|
3007
|
+
};
|
|
3008
|
+
|
|
3009
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
3010
|
+
if (event.key === "Escape") {
|
|
3011
|
+
setOpen(false);
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
|
|
3015
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
3016
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
3017
|
+
|
|
3018
|
+
return () => {
|
|
3019
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
3020
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
3021
|
+
};
|
|
3022
|
+
}, [open, setOpen]);
|
|
3023
|
+
|
|
3024
|
+
const commitValue = React.useCallback(
|
|
3025
|
+
(nextValue: DateRangeValue) => {
|
|
3026
|
+
if (!isValueControlled) {
|
|
3027
|
+
setInternalValue(nextValue);
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
onValueChange?.(nextValue);
|
|
3031
|
+
},
|
|
3032
|
+
[isValueControlled, onValueChange]
|
|
3033
|
+
);
|
|
3034
|
+
|
|
3035
|
+
const handlePresetClick = (preset: DateRangePreset) => {
|
|
3036
|
+
const range = preset.getRange();
|
|
3037
|
+
commitValue(range);
|
|
3038
|
+
setOpen(false);
|
|
3039
|
+
};
|
|
3040
|
+
|
|
3041
|
+
const handleDayClick = (day: Date) => {
|
|
3042
|
+
if (!pendingEnd) {
|
|
3043
|
+
setDraftValue({ start: day, end: day });
|
|
3044
|
+
setPendingEnd(true);
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// Second click completes the range \u2014 commit and close immediately,
|
|
3049
|
+
// the same way a preset does, since there's no Apply step anymore.
|
|
3050
|
+
const start = draftValue.start ?? day;
|
|
3051
|
+
const nextValue = isBeforeDay(day, start)
|
|
3052
|
+
? { start: day, end: start }
|
|
3053
|
+
: { start, end: day };
|
|
3054
|
+
|
|
3055
|
+
setDraftValue(nextValue);
|
|
3056
|
+
setPendingEnd(false);
|
|
3057
|
+
commitValue(nextValue);
|
|
3058
|
+
setOpen(false);
|
|
3059
|
+
};
|
|
3060
|
+
|
|
3061
|
+
const popover =
|
|
3062
|
+
open &&
|
|
3063
|
+
!disabled &&
|
|
3064
|
+
portalMount &&
|
|
3065
|
+
createPortal(
|
|
3066
|
+
<div
|
|
3067
|
+
ref={setPopoverRef}
|
|
3068
|
+
role="dialog"
|
|
3069
|
+
aria-modal="false"
|
|
3070
|
+
aria-labelledby={\`\${triggerId}-calendar-heading\`}
|
|
3071
|
+
className={cn(
|
|
3072
|
+
"flex flex-col rounded-lg border border-solid border-semantic-border-layout bg-semantic-bg-primary shadow-lg overflow-y-auto overflow-x-hidden overscroll-contain pointer-events-auto",
|
|
3073
|
+
"[scrollbar-gutter:stable] [scrollbar-width:thin] [scrollbar-color:var(--semantic-border-secondary)_transparent]",
|
|
3074
|
+
"[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-semantic-border-secondary"
|
|
3075
|
+
)}
|
|
3076
|
+
style={{
|
|
3077
|
+
...floatingStyles,
|
|
3078
|
+
maxWidth: \`calc(100vw - \${POPOVER_MARGIN * 2}px)\`,
|
|
3079
|
+
maxHeight: \`var(\${POPOVER_SCROLL_HEIGHT_VAR}, min(\${MAX_POPOVER_HEIGHT}px, calc(100dvh - \${
|
|
3080
|
+
POPOVER_MARGIN * 2
|
|
3081
|
+
}px)))\`,
|
|
3082
|
+
zIndex: 10050,
|
|
3083
|
+
visibility: isPositioned ? undefined : "hidden",
|
|
3084
|
+
}}
|
|
3085
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
3086
|
+
onMouseDown={(event) => event.stopPropagation()}
|
|
3087
|
+
onWheel={(event) => event.stopPropagation()}
|
|
3088
|
+
onTouchMove={(event) => event.stopPropagation()}
|
|
3089
|
+
>
|
|
3090
|
+
<div className="flex flex-row">
|
|
3091
|
+
{presets.length > 0 && (
|
|
3092
|
+
<div className="flex w-36 shrink-0 flex-col gap-0.5 border-r border-solid border-semantic-border-layout p-3">
|
|
3093
|
+
{presets.map((preset) => (
|
|
3094
|
+
<button
|
|
3095
|
+
key={preset.label}
|
|
3096
|
+
type="button"
|
|
3097
|
+
className="w-full rounded px-2 py-1.5 text-left text-sm text-semantic-text-primary transition-colors hover:bg-semantic-bg-hover"
|
|
3098
|
+
onClick={() => handlePresetClick(preset)}
|
|
3099
|
+
>
|
|
3100
|
+
{preset.label}
|
|
3101
|
+
</button>
|
|
3102
|
+
))}
|
|
3103
|
+
</div>
|
|
3104
|
+
)}
|
|
3105
|
+
|
|
3106
|
+
<div className="min-w-[272px] flex-1 p-3 touch-pan-y">
|
|
3107
|
+
<div className="mb-3 flex items-center justify-between gap-2">
|
|
3108
|
+
<button
|
|
3109
|
+
type="button"
|
|
3110
|
+
aria-label="Previous month"
|
|
3111
|
+
className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
|
|
3112
|
+
onClick={() =>
|
|
3113
|
+
setVisibleMonth((month) => addMonths(month, -1))
|
|
3114
|
+
}
|
|
3115
|
+
>
|
|
3116
|
+
<ChevronLeft className="size-4" aria-hidden="true" />
|
|
3117
|
+
</button>
|
|
3118
|
+
|
|
3119
|
+
<div
|
|
3120
|
+
id={\`\${triggerId}-calendar-heading\`}
|
|
3121
|
+
className="flex items-center gap-2"
|
|
3122
|
+
>
|
|
3123
|
+
<DropdownMenu>
|
|
3124
|
+
<DropdownMenuTrigger asChild>
|
|
3125
|
+
<button
|
|
3126
|
+
type="button"
|
|
3127
|
+
className="rounded border border-solid border-semantic-border-layout px-3 py-1.5 text-sm font-semibold text-semantic-text-primary transition-colors hover:bg-semantic-bg-hover"
|
|
3128
|
+
>
|
|
3129
|
+
{MONTH_LONG_NAMES[visibleMonth.getMonth()]}
|
|
3130
|
+
</button>
|
|
3131
|
+
</DropdownMenuTrigger>
|
|
3132
|
+
<DropdownMenuContent
|
|
3133
|
+
align="start"
|
|
3134
|
+
className={cn(CALENDAR_DROPDOWN_Z_INDEX, "max-h-[240px] overflow-y-auto")}
|
|
3135
|
+
>
|
|
3136
|
+
{MONTH_LONG_NAMES.map((label, monthIndex) => (
|
|
3137
|
+
<DropdownMenuItem
|
|
3138
|
+
key={label}
|
|
3139
|
+
onSelect={() =>
|
|
3140
|
+
setVisibleMonth(
|
|
3141
|
+
startOfMonth(
|
|
3142
|
+
new Date(visibleMonth.getFullYear(), monthIndex, 1)
|
|
3143
|
+
)
|
|
3144
|
+
)
|
|
3145
|
+
}
|
|
3146
|
+
>
|
|
3147
|
+
{label}
|
|
3148
|
+
</DropdownMenuItem>
|
|
3149
|
+
))}
|
|
3150
|
+
</DropdownMenuContent>
|
|
3151
|
+
</DropdownMenu>
|
|
3152
|
+
|
|
3153
|
+
<DropdownMenu>
|
|
3154
|
+
<DropdownMenuTrigger asChild>
|
|
3155
|
+
<button
|
|
3156
|
+
type="button"
|
|
3157
|
+
className="flex items-center gap-1 rounded border border-solid border-semantic-border-layout px-3 py-1.5 text-sm font-semibold text-semantic-text-primary transition-colors hover:bg-semantic-bg-hover"
|
|
3158
|
+
>
|
|
3159
|
+
{visibleMonth.getFullYear()}
|
|
3160
|
+
<ChevronDown
|
|
3161
|
+
className="size-3.5 text-semantic-text-muted"
|
|
3162
|
+
aria-hidden="true"
|
|
3163
|
+
/>
|
|
3164
|
+
</button>
|
|
3165
|
+
</DropdownMenuTrigger>
|
|
3166
|
+
<DropdownMenuContent
|
|
3167
|
+
align="start"
|
|
3168
|
+
className={cn(CALENDAR_DROPDOWN_Z_INDEX, "max-h-[240px] overflow-y-auto")}
|
|
3169
|
+
>
|
|
3170
|
+
{Array.from(
|
|
3171
|
+
{ length: YEAR_OPTIONS_SPAN * 2 + 1 },
|
|
3172
|
+
(_, index) => visibleMonth.getFullYear() - YEAR_OPTIONS_SPAN + index
|
|
3173
|
+
).map((year) => (
|
|
3174
|
+
<DropdownMenuItem
|
|
3175
|
+
key={year}
|
|
3176
|
+
onSelect={() =>
|
|
3177
|
+
setVisibleMonth(
|
|
3178
|
+
startOfMonth(new Date(year, visibleMonth.getMonth(), 1))
|
|
3179
|
+
)
|
|
3180
|
+
}
|
|
3181
|
+
>
|
|
3182
|
+
{year}
|
|
3183
|
+
</DropdownMenuItem>
|
|
3184
|
+
))}
|
|
3185
|
+
</DropdownMenuContent>
|
|
3186
|
+
</DropdownMenu>
|
|
3187
|
+
</div>
|
|
3188
|
+
|
|
3189
|
+
<button
|
|
3190
|
+
type="button"
|
|
3191
|
+
aria-label="Next month"
|
|
3192
|
+
className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
|
|
3193
|
+
onClick={() =>
|
|
3194
|
+
setVisibleMonth((month) => addMonths(month, 1))
|
|
3195
|
+
}
|
|
3196
|
+
>
|
|
3197
|
+
<ChevronRight className="size-4" aria-hidden="true" />
|
|
3198
|
+
</button>
|
|
3199
|
+
</div>
|
|
3200
|
+
|
|
3201
|
+
<div className="grid grid-cols-7">
|
|
3202
|
+
{weekDays.map((day) => (
|
|
3203
|
+
<div
|
|
3204
|
+
key={day}
|
|
3205
|
+
className="flex h-8 items-center justify-center text-xs font-medium text-semantic-text-muted"
|
|
3206
|
+
>
|
|
3207
|
+
{day}
|
|
3208
|
+
</div>
|
|
3209
|
+
))}
|
|
3210
|
+
{calendarDays.map((day) => {
|
|
3211
|
+
const isCurrentMonth =
|
|
3212
|
+
day.getMonth() === visibleMonth.getMonth();
|
|
3213
|
+
const isToday = isSameDay(day, new Date());
|
|
3214
|
+
const { start, end } = draftValue;
|
|
3215
|
+
const hasRange = !!start && !!end && !isSameDay(start, end);
|
|
3216
|
+
const isRangeStart = isSameDay(day, start);
|
|
3217
|
+
const isRangeEnd = isSameDay(day, end);
|
|
3218
|
+
const isBetween =
|
|
3219
|
+
hasRange &&
|
|
3220
|
+
!!start &&
|
|
3221
|
+
!!end &&
|
|
3222
|
+
isAfterDay(day, start) &&
|
|
3223
|
+
isBeforeDay(day, end);
|
|
3224
|
+
const isInBand =
|
|
3225
|
+
hasRange && (isRangeStart || isRangeEnd || isBetween);
|
|
3226
|
+
const isSelectedEdge = isRangeStart || isRangeEnd;
|
|
3227
|
+
const isDisabled =
|
|
3228
|
+
(effectiveMinDate &&
|
|
3229
|
+
isBeforeDay(day, effectiveMinDate)) ||
|
|
3230
|
+
(maxDate && isAfterDay(day, maxDate));
|
|
3231
|
+
const dayLabel = day.toLocaleDateString("en-US", {
|
|
3232
|
+
month: "long",
|
|
3233
|
+
day: "numeric",
|
|
3234
|
+
year: "numeric",
|
|
3235
|
+
});
|
|
3236
|
+
|
|
3237
|
+
return (
|
|
3238
|
+
<div
|
|
3239
|
+
key={day.toISOString()}
|
|
3240
|
+
className={cn(
|
|
3241
|
+
"relative flex h-8 items-center justify-center",
|
|
3242
|
+
isInBand && "bg-semantic-info-surface",
|
|
3243
|
+
isRangeStart && hasRange && "rounded-l-full",
|
|
3244
|
+
isRangeEnd && hasRange && "rounded-r-full"
|
|
3245
|
+
)}
|
|
3246
|
+
>
|
|
3247
|
+
<button
|
|
3248
|
+
type="button"
|
|
3249
|
+
aria-label={dayLabel}
|
|
3250
|
+
aria-pressed={isSelectedEdge}
|
|
3251
|
+
aria-current={isToday ? "date" : undefined}
|
|
3252
|
+
disabled={!!isDisabled}
|
|
3253
|
+
className={cn(
|
|
3254
|
+
"relative flex size-8 items-center justify-center rounded-full text-xs transition-colors",
|
|
3255
|
+
isSelectedEdge
|
|
3256
|
+
? "bg-semantic-primary text-semantic-text-inverted font-semibold"
|
|
3257
|
+
: isCurrentMonth
|
|
3258
|
+
? "text-semantic-text-primary hover:bg-semantic-bg-hover"
|
|
3259
|
+
: "text-semantic-text-muted hover:bg-semantic-bg-hover",
|
|
3260
|
+
isDisabled &&
|
|
3261
|
+
"opacity-40 cursor-not-allowed pointer-events-none"
|
|
3262
|
+
)}
|
|
3263
|
+
onClick={() => {
|
|
3264
|
+
if (isDisabled) return;
|
|
3265
|
+
|
|
3266
|
+
handleDayClick(day);
|
|
3267
|
+
}}
|
|
3268
|
+
>
|
|
3269
|
+
{day.getDate()}
|
|
3270
|
+
{isToday && !isSelectedEdge && (
|
|
3271
|
+
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-semantic-primary" />
|
|
3272
|
+
)}
|
|
3273
|
+
</button>
|
|
3274
|
+
</div>
|
|
3275
|
+
);
|
|
3276
|
+
})}
|
|
3277
|
+
</div>
|
|
3278
|
+
</div>
|
|
3279
|
+
</div>
|
|
3280
|
+
</div>,
|
|
3281
|
+
portalMount
|
|
3282
|
+
);
|
|
3283
|
+
|
|
3284
|
+
return (
|
|
3285
|
+
<div
|
|
3286
|
+
ref={(node) => {
|
|
3287
|
+
rootRef.current = node;
|
|
3288
|
+
|
|
3289
|
+
if (typeof ref === "function") {
|
|
3290
|
+
ref(node);
|
|
3291
|
+
} else if (ref) {
|
|
3292
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current =
|
|
3293
|
+
node;
|
|
3294
|
+
}
|
|
3295
|
+
}}
|
|
3296
|
+
className={cn("relative inline-block w-full max-w-full", className)}
|
|
3297
|
+
{...props}
|
|
3298
|
+
>
|
|
3299
|
+
<button
|
|
3300
|
+
ref={setTriggerRef}
|
|
3301
|
+
id={triggerId}
|
|
3302
|
+
type="button"
|
|
3303
|
+
disabled={disabled}
|
|
3304
|
+
aria-haspopup="dialog"
|
|
3305
|
+
aria-expanded={open}
|
|
3306
|
+
className={cn(
|
|
3307
|
+
dateRangePickerTriggerVariants({ state }),
|
|
3308
|
+
open &&
|
|
3309
|
+
state !== "error" &&
|
|
3310
|
+
"border-semantic-border-input-focus/50 shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
|
|
3311
|
+
!displayValue && "text-semantic-text-placeholder",
|
|
3312
|
+
triggerClassName
|
|
3313
|
+
)}
|
|
3314
|
+
onClick={() => setOpen(!open)}
|
|
3315
|
+
>
|
|
3316
|
+
<FigmaCalendarIcon className="size-[18px] shrink-0 text-semantic-text-secondary" />
|
|
3317
|
+
<span
|
|
3318
|
+
className={cn(
|
|
3319
|
+
"min-w-0 flex-1 truncate",
|
|
3320
|
+
!displayValue && "font-normal",
|
|
3321
|
+
triggerLabelClassName
|
|
3322
|
+
)}
|
|
3323
|
+
>
|
|
3324
|
+
{displayValue || placeholder}
|
|
3325
|
+
</span>
|
|
3326
|
+
</button>
|
|
3327
|
+
|
|
3328
|
+
{popover}
|
|
3329
|
+
</div>
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
);
|
|
3333
|
+
DateRangePicker.displayName = "DateRangePicker";
|
|
3334
|
+
|
|
3335
|
+
export { DateRangePicker, dateRangePickerTriggerVariants };
|
|
2565
3336
|
`,
|
|
2566
3337
|
"date-time-picker": `import * as React from "react";
|
|
2567
3338
|
import { createPortal } from "react-dom";
|
|
@@ -5029,7 +5800,7 @@ const DialogContent = React.forwardRef(({ className, children, size, hideCloseBu
|
|
|
5029
5800
|
{children}
|
|
5030
5801
|
{/* Accessibility: Add hidden description if none provided */}
|
|
5031
5802
|
{!hasDescription && (
|
|
5032
|
-
<DialogPrimitive.Description className="sr-only">
|
|
5803
|
+
<DialogPrimitive.Description className="sr-only m-0">
|
|
5033
5804
|
Dialog content
|
|
5034
5805
|
</DialogPrimitive.Description>
|
|
5035
5806
|
)}
|
|
@@ -5088,7 +5859,7 @@ DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
|
5088
5859
|
const DialogDescription = React.forwardRef(({ className, ...props }: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>, ref: React.Ref<React.ElementRef<typeof DialogPrimitive.Description>>) => (
|
|
5089
5860
|
<DialogPrimitive.Description
|
|
5090
5861
|
ref={ref}
|
|
5091
|
-
className={cn("text-sm text-muted-foreground", className)}
|
|
5862
|
+
className={cn("m-0 text-sm text-muted-foreground", className)}
|
|
5092
5863
|
{...props}
|
|
5093
5864
|
/>
|
|
5094
5865
|
));
|
|
@@ -5816,11 +6587,11 @@ const Input = React.forwardRef(
|
|
|
5816
6587
|
input.value.slice(selectionEnd);
|
|
5817
6588
|
|
|
5818
6589
|
if (nextValue.includes(" ")) {
|
|
6590
|
+
// Blocking the insertion is enough \u2014 nothing moved, so the caret is
|
|
6591
|
+
// already where it belongs. Re-setting it (especially from a later
|
|
6592
|
+
// animation frame, with a selection offset captured before this
|
|
6593
|
+
// keystroke) drags the caret backwards and reorders what follows.
|
|
5819
6594
|
e.preventDefault();
|
|
5820
|
-
input.setSelectionRange(selectionStart, selectionStart);
|
|
5821
|
-
window.requestAnimationFrame(() => {
|
|
5822
|
-
input.setSelectionRange(selectionStart, selectionStart);
|
|
5823
|
-
});
|
|
5824
6595
|
}
|
|
5825
6596
|
}}
|
|
5826
6597
|
onChange={(e) => {
|
|
@@ -5841,9 +6612,19 @@ const Input = React.forwardRef(
|
|
|
5841
6612
|
);
|
|
5842
6613
|
|
|
5843
6614
|
input.value = collapsedValue;
|
|
6615
|
+
// Restore the caret synchronously: assigning \`value\` above parks it
|
|
6616
|
+
// at 0, and the next keystroke can land before any animation frame
|
|
6617
|
+
// runs, which reorders what the user typed. The deferred pass is
|
|
6618
|
+
// kept for browsers that reset the caret again after React
|
|
6619
|
+
// re-renders, but it must no-op once the value has moved on \u2014 a
|
|
6620
|
+
// frame that fires mid-typing would otherwise drag the caret back
|
|
6621
|
+
// to a position captured several keystrokes ago.
|
|
6622
|
+
input.setSelectionRange(nextCursor, nextCursor);
|
|
5844
6623
|
window.requestAnimationFrame(() => {
|
|
6624
|
+
if (input.value !== collapsedValue) return;
|
|
5845
6625
|
input.setSelectionRange(nextCursor, nextCursor);
|
|
5846
6626
|
});
|
|
6627
|
+
|
|
5847
6628
|
}
|
|
5848
6629
|
onChange?.(e);
|
|
5849
6630
|
}}
|
|
@@ -5992,6 +6773,13 @@ const MENU_MIN_WIDTH = 180;
|
|
|
5992
6773
|
/** Floor for the flip/shrink height so the list is never collapsed to nothing. */
|
|
5993
6774
|
const MENU_MIN_HEIGHT = 120;
|
|
5994
6775
|
|
|
6776
|
+
/**
|
|
6777
|
+
* Distance from the bottom of the option list, in px, at which \`onScrollEnd\`
|
|
6778
|
+
* fires. Roughly one option row, so the next page starts loading just before
|
|
6779
|
+
* the user actually hits the end.
|
|
6780
|
+
*/
|
|
6781
|
+
const SCROLL_END_THRESHOLD_PX = 48;
|
|
6782
|
+
|
|
5995
6783
|
/**
|
|
5996
6784
|
* MultiSelect trigger variants matching TextField styling
|
|
5997
6785
|
*/
|
|
@@ -6025,6 +6813,27 @@ export interface MultiSelectProps extends VariantProps<
|
|
|
6025
6813
|
disabled?: boolean;
|
|
6026
6814
|
/** Loading state with spinner */
|
|
6027
6815
|
loading?: boolean;
|
|
6816
|
+
/**
|
|
6817
|
+
* Called once when the option list is scrolled to within
|
|
6818
|
+
* \`SCROLL_END_THRESHOLD_PX\` of its bottom. Use it to fetch the next page of
|
|
6819
|
+
* server-side options. Fires at most once per scroll gesture \u2014 it re-arms
|
|
6820
|
+
* only after the user scrolls back away from the bottom, so a single
|
|
6821
|
+
* trackpad flick (which keeps emitting scroll events while it decelerates)
|
|
6822
|
+
* cannot fan out into several requests.
|
|
6823
|
+
*/
|
|
6824
|
+
onScrollEnd?: () => void;
|
|
6825
|
+
/**
|
|
6826
|
+
* Whether the server has more pages. When false, \`onScrollEnd\` is never
|
|
6827
|
+
* called.
|
|
6828
|
+
* @default false
|
|
6829
|
+
*/
|
|
6830
|
+
hasMore?: boolean;
|
|
6831
|
+
/**
|
|
6832
|
+
* Whether a page fetch is in flight. Renders a loading row at the bottom of
|
|
6833
|
+
* the list and suppresses further \`onScrollEnd\` calls.
|
|
6834
|
+
* @default false
|
|
6835
|
+
*/
|
|
6836
|
+
loadingMore?: boolean;
|
|
6028
6837
|
/** Placeholder text when no value selected */
|
|
6029
6838
|
placeholder?: string;
|
|
6030
6839
|
/** Currently selected values (controlled) */
|
|
@@ -6044,6 +6853,18 @@ export interface MultiSelectProps extends VariantProps<
|
|
|
6044
6853
|
searchable?: boolean;
|
|
6045
6854
|
/** Search placeholder text */
|
|
6046
6855
|
searchPlaceholder?: string;
|
|
6856
|
+
/**
|
|
6857
|
+
* When set, the trigger shows a single compact summary (e.g. "3 lines
|
|
6858
|
+
* selected") instead of one chip per selection; hovering it reveals the full
|
|
6859
|
+
* list of selected labels in a tooltip. Receives the selected count.
|
|
6860
|
+
*/
|
|
6861
|
+
summaryLabel?: (count: number) => string;
|
|
6862
|
+
/**
|
|
6863
|
+
* When set, pins a select-all row at the top of the list with this label
|
|
6864
|
+
* (e.g. "All lines"). Toggles every non-disabled option; respects
|
|
6865
|
+
* \`maxSelections\`. Omit to hide the row entirely.
|
|
6866
|
+
*/
|
|
6867
|
+
selectAllLabel?: string;
|
|
6047
6868
|
/** Maximum selections allowed */
|
|
6048
6869
|
maxSelections?: number;
|
|
6049
6870
|
/**
|
|
@@ -6114,6 +6935,9 @@ const MultiSelect = React.forwardRef(
|
|
|
6114
6935
|
error,
|
|
6115
6936
|
disabled,
|
|
6116
6937
|
loading,
|
|
6938
|
+
onScrollEnd,
|
|
6939
|
+
hasMore = false,
|
|
6940
|
+
loadingMore = false,
|
|
6117
6941
|
placeholder = "Select options",
|
|
6118
6942
|
value,
|
|
6119
6943
|
defaultValue = [],
|
|
@@ -6121,6 +6945,8 @@ const MultiSelect = React.forwardRef(
|
|
|
6121
6945
|
options,
|
|
6122
6946
|
searchable,
|
|
6123
6947
|
searchPlaceholder = "Search...",
|
|
6948
|
+
selectAllLabel,
|
|
6949
|
+
summaryLabel,
|
|
6124
6950
|
maxSelections,
|
|
6125
6951
|
showSelectionFooter = true,
|
|
6126
6952
|
wrapperClassName,
|
|
@@ -6261,6 +7087,61 @@ const MultiSelect = React.forwardRef(
|
|
|
6261
7087
|
};
|
|
6262
7088
|
}, [isOpen, portalTarget, refs.floating]);
|
|
6263
7089
|
|
|
7090
|
+
/** The scrollable option list; also read by the no-scrollbar fallback. */
|
|
7091
|
+
const listRef = React.useRef<HTMLDivElement | null>(null);
|
|
7092
|
+
/**
|
|
7093
|
+
* True once \`onScrollEnd\` has fired for the current visit to the bottom.
|
|
7094
|
+
* Cleared only when the user scrolls back out of the threshold zone, so
|
|
7095
|
+
* trackpad inertia \u2014 which keeps firing \`scroll\` for hundreds of ms after
|
|
7096
|
+
* the finger lifts \u2014 cannot re-trigger the callback.
|
|
7097
|
+
*/
|
|
7098
|
+
const isLatchedRef = React.useRef(false);
|
|
7099
|
+
|
|
7100
|
+
/**
|
|
7101
|
+
* Deliberately a plain function, not a \`useCallback\` \u2014 it must read the
|
|
7102
|
+
* current \`hasMore\` / \`loadingMore\` on every scroll event, and a memoised
|
|
7103
|
+
* handler would need a props ref (writing refs during render is banned).
|
|
7104
|
+
*/
|
|
7105
|
+
const maybeLoadMore = () => {
|
|
7106
|
+
const node = listRef.current;
|
|
7107
|
+
if (!node) return;
|
|
7108
|
+
|
|
7109
|
+
const isNearBottom =
|
|
7110
|
+
node.scrollHeight - node.scrollTop - node.clientHeight <
|
|
7111
|
+
SCROLL_END_THRESHOLD_PX;
|
|
7112
|
+
|
|
7113
|
+
if (!isNearBottom) {
|
|
7114
|
+
// Only an explicit scroll away from the boundary re-arms the latch.
|
|
7115
|
+
isLatchedRef.current = false;
|
|
7116
|
+
return;
|
|
7117
|
+
}
|
|
7118
|
+
if (isLatchedRef.current || !hasMore || loadingMore || !onScrollEnd)
|
|
7119
|
+
return;
|
|
7120
|
+
|
|
7121
|
+
isLatchedRef.current = true;
|
|
7122
|
+
onScrollEnd();
|
|
7123
|
+
};
|
|
7124
|
+
|
|
7125
|
+
// A closed menu or a new search starts from a clean latch.
|
|
7126
|
+
React.useEffect(() => {
|
|
7127
|
+
isLatchedRef.current = false;
|
|
7128
|
+
}, [isOpen, searchQuery]);
|
|
7129
|
+
|
|
7130
|
+
/**
|
|
7131
|
+
* A page can arrive without making the list taller than its max-height
|
|
7132
|
+
* (few results, or a short viewport). No further \`scroll\` event would ever
|
|
7133
|
+
* fire, so pagination would stall silently \u2014 re-check whenever the rendered
|
|
7134
|
+
* options or the fetch state change.
|
|
7135
|
+
*/
|
|
7136
|
+
React.useEffect(() => {
|
|
7137
|
+
if (!isOpen || !hasMore || loadingMore) return;
|
|
7138
|
+
const node = listRef.current;
|
|
7139
|
+
if (!node || node.scrollHeight > node.clientHeight) return;
|
|
7140
|
+
if (isLatchedRef.current) return;
|
|
7141
|
+
isLatchedRef.current = true;
|
|
7142
|
+
onScrollEnd?.();
|
|
7143
|
+
}, [isOpen, portalTarget, hasMore, loadingMore, options, onScrollEnd]);
|
|
7144
|
+
|
|
6264
7145
|
const flatOptions = React.useMemo(
|
|
6265
7146
|
() => flattenMultiSelectOptions(options),
|
|
6266
7147
|
[options]
|
|
@@ -6270,6 +7151,28 @@ const MultiSelect = React.forwardRef(
|
|
|
6270
7151
|
const isControlled = value !== undefined;
|
|
6271
7152
|
const selectedValues = isControlled ? value : internalValue;
|
|
6272
7153
|
|
|
7154
|
+
// Select-all: values eligible for select-all exclude disabled options
|
|
7155
|
+
const selectableValues = React.useMemo(
|
|
7156
|
+
() => flatOptions.filter((o) => !o.disabled).map((o) => o.value),
|
|
7157
|
+
[flatOptions]
|
|
7158
|
+
);
|
|
7159
|
+
const allSelected =
|
|
7160
|
+
selectableValues.length > 0 &&
|
|
7161
|
+
selectableValues.every((v) => selectedValues.includes(v));
|
|
7162
|
+
const someSelected = selectedValues.length > 0 && !allSelected;
|
|
7163
|
+
|
|
7164
|
+
const toggleSelectAll = () => {
|
|
7165
|
+
const newValues = allSelected
|
|
7166
|
+
? []
|
|
7167
|
+
: maxSelections
|
|
7168
|
+
? selectableValues.slice(0, maxSelections)
|
|
7169
|
+
: selectableValues;
|
|
7170
|
+
if (!isControlled) {
|
|
7171
|
+
setInternalValue(newValues);
|
|
7172
|
+
}
|
|
7173
|
+
onValueChange?.(newValues);
|
|
7174
|
+
};
|
|
7175
|
+
|
|
6273
7176
|
// Derive state from props
|
|
6274
7177
|
const derivedState = error ? "error" : (state ?? "default");
|
|
6275
7178
|
|
|
@@ -6490,6 +7393,23 @@ const MultiSelect = React.forwardRef(
|
|
|
6490
7393
|
<span className="text-base text-semantic-text-placeholder">
|
|
6491
7394
|
{placeholder}
|
|
6492
7395
|
</span>
|
|
7396
|
+
) : summaryLabel ? (
|
|
7397
|
+
<TooltipProvider delayDuration={200}>
|
|
7398
|
+
<Tooltip>
|
|
7399
|
+
<TooltipTrigger asChild>
|
|
7400
|
+
<span className="min-w-0 truncate text-sm text-semantic-text-primary">
|
|
7401
|
+
{summaryLabel(selectedValues.length)}
|
|
7402
|
+
</span>
|
|
7403
|
+
</TooltipTrigger>
|
|
7404
|
+
<TooltipContent>
|
|
7405
|
+
<div className="flex flex-col gap-0.5">
|
|
7406
|
+
{selectedLabels.map((label, index) => (
|
|
7407
|
+
<span key={selectedValues[index]}>{label}</span>
|
|
7408
|
+
))}
|
|
7409
|
+
</div>
|
|
7410
|
+
</TooltipContent>
|
|
7411
|
+
</Tooltip>
|
|
7412
|
+
</TooltipProvider>
|
|
6493
7413
|
) : (
|
|
6494
7414
|
selectedLabels.map((label, index) => (
|
|
6495
7415
|
<span
|
|
@@ -6623,15 +7543,43 @@ const MultiSelect = React.forwardRef(
|
|
|
6623
7543
|
</div>
|
|
6624
7544
|
)}
|
|
6625
7545
|
|
|
7546
|
+
{/* Select all */}
|
|
7547
|
+
{selectAllLabel && (
|
|
7548
|
+
<div
|
|
7549
|
+
role="option"
|
|
7550
|
+
aria-selected={allSelected}
|
|
7551
|
+
tabIndex={0}
|
|
7552
|
+
onClick={toggleSelectAll}
|
|
7553
|
+
onKeyDown={(e) => {
|
|
7554
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
7555
|
+
e.preventDefault();
|
|
7556
|
+
toggleSelectAll();
|
|
7557
|
+
}
|
|
7558
|
+
}}
|
|
7559
|
+
className="flex w-full cursor-pointer select-none items-center gap-2 border-b border-solid border-semantic-border-layout px-3 py-2 text-sm text-semantic-text-primary outline-none hover:bg-semantic-bg-ui"
|
|
7560
|
+
>
|
|
7561
|
+
<Checkbox
|
|
7562
|
+
checked={allSelected ? true : someSelected ? "indeterminate" : false}
|
|
7563
|
+
size="sm"
|
|
7564
|
+
className="pointer-events-none shrink-0"
|
|
7565
|
+
aria-hidden
|
|
7566
|
+
tabIndex={-1}
|
|
7567
|
+
/>
|
|
7568
|
+
<span className="min-w-0 flex-1 truncate text-left">{selectAllLabel}</span>
|
|
7569
|
+
</div>
|
|
7570
|
+
)}
|
|
7571
|
+
|
|
6626
7572
|
{/* Options */}
|
|
6627
7573
|
<div
|
|
7574
|
+
ref={listRef}
|
|
7575
|
+
onScroll={maybeLoadMore}
|
|
6628
7576
|
className="overflow-auto overscroll-contain p-1"
|
|
6629
7577
|
style={{
|
|
6630
7578
|
maxHeight:
|
|
6631
7579
|
"min(15rem, var(--multi-select-available-height, 15rem))",
|
|
6632
7580
|
}}
|
|
6633
7581
|
>
|
|
6634
|
-
{filteredOptions.length === 0 ? (
|
|
7582
|
+
{filteredOptions.length === 0 && !loadingMore ? (
|
|
6635
7583
|
<div className="py-6 text-center text-sm text-semantic-text-muted">
|
|
6636
7584
|
No results found
|
|
6637
7585
|
</div>
|
|
@@ -6793,6 +7741,17 @@ const MultiSelect = React.forwardRef(
|
|
|
6793
7741
|
return withDisabledTooltip(simpleRow);
|
|
6794
7742
|
})
|
|
6795
7743
|
)}
|
|
7744
|
+
|
|
7745
|
+
{loadingMore ? (
|
|
7746
|
+
<div
|
|
7747
|
+
role="status"
|
|
7748
|
+
aria-live="polite"
|
|
7749
|
+
className="flex items-center justify-center gap-2 py-3 text-sm text-semantic-text-muted"
|
|
7750
|
+
>
|
|
7751
|
+
<Loader2 className="size-4 animate-spin" />
|
|
7752
|
+
<span>Loading more...</span>
|
|
7753
|
+
</div>
|
|
7754
|
+
) : null}
|
|
6796
7755
|
</div>
|
|
6797
7756
|
|
|
6798
7757
|
{/* Footer with count */}
|
|
@@ -8550,11 +9509,32 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
|
|
|
8550
9509
|
[disabled]
|
|
8551
9510
|
);
|
|
8552
9511
|
|
|
9512
|
+
/**
|
|
9513
|
+
* The pending focus frame has to be cancellable. Opening the menu schedules
|
|
9514
|
+
* a frame that refocuses the input, and the input's \`onFocus\` reopens the
|
|
9515
|
+
* menu. If that frame lands AFTER an option has been picked, it reopens the
|
|
9516
|
+
* dropdown that \`selectOption\` just closed \u2014 so selecting an option looks
|
|
9517
|
+
* like it does nothing. Whether the frame lands before or after the click is
|
|
9518
|
+
* pure timing, which is why the bug comes and goes.
|
|
9519
|
+
*/
|
|
9520
|
+
const focusFrameRef = React.useRef<number | null>(null);
|
|
9521
|
+
|
|
9522
|
+
const cancelPendingFocus = React.useCallback(() => {
|
|
9523
|
+
if (focusFrameRef.current === null) return;
|
|
9524
|
+
window.cancelAnimationFrame(focusFrameRef.current);
|
|
9525
|
+
focusFrameRef.current = null;
|
|
9526
|
+
}, []);
|
|
9527
|
+
|
|
8553
9528
|
const focusSearchInput = React.useCallback(() => {
|
|
8554
|
-
|
|
9529
|
+
cancelPendingFocus();
|
|
9530
|
+
focusFrameRef.current = window.requestAnimationFrame(() => {
|
|
9531
|
+
focusFrameRef.current = null;
|
|
8555
9532
|
searchInputRef.current?.focus();
|
|
8556
9533
|
});
|
|
8557
|
-
}, []);
|
|
9534
|
+
}, [cancelPendingFocus]);
|
|
9535
|
+
|
|
9536
|
+
// Never let a queued frame fire into an unmounted component.
|
|
9537
|
+
React.useEffect(() => cancelPendingFocus, [cancelPendingFocus]);
|
|
8558
9538
|
|
|
8559
9539
|
const setRootRef = React.useCallback(
|
|
8560
9540
|
(node: HTMLDivElement | null) => {
|
|
@@ -8633,9 +9613,12 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
|
|
|
8633
9613
|
onValueChange?.(option.value);
|
|
8634
9614
|
onOptionSelect?.(option);
|
|
8635
9615
|
onSearchChange?.(option.label);
|
|
9616
|
+
// Drop any queued refocus first, or it reopens what we are closing.
|
|
9617
|
+
cancelPendingFocus();
|
|
8636
9618
|
setOpen(false);
|
|
8637
9619
|
},
|
|
8638
9620
|
[
|
|
9621
|
+
cancelPendingFocus,
|
|
8639
9622
|
disabled,
|
|
8640
9623
|
onSearchChange,
|
|
8641
9624
|
onOptionSelect,
|
|
@@ -10816,7 +11799,15 @@ const TextField = React.forwardRef(
|
|
|
10816
11799
|
);
|
|
10817
11800
|
|
|
10818
11801
|
input.value = collapsedValue;
|
|
11802
|
+
// Restore the caret synchronously: assigning \`value\` above parks it at
|
|
11803
|
+
// 0, and the next keystroke can land before any animation frame runs,
|
|
11804
|
+
// which reorders what the user typed. The deferred pass is kept for
|
|
11805
|
+
// browsers that reset the caret again after React re-renders, but it
|
|
11806
|
+
// must no-op once the value has moved on \u2014 a frame that fires mid-typing
|
|
11807
|
+
// would otherwise drag the caret back to a stale position.
|
|
11808
|
+
input.setSelectionRange(nextCursor, nextCursor);
|
|
10819
11809
|
window.requestAnimationFrame(() => {
|
|
11810
|
+
if (input.value !== collapsedValue) return;
|
|
10820
11811
|
input.setSelectionRange(nextCursor, nextCursor);
|
|
10821
11812
|
});
|
|
10822
11813
|
}
|
|
@@ -10900,11 +11891,11 @@ const TextField = React.forwardRef(
|
|
|
10900
11891
|
input.value.slice(selectionEnd);
|
|
10901
11892
|
|
|
10902
11893
|
if (nextValue.includes(" ")) {
|
|
11894
|
+
// Blocking the insertion is enough \u2014 nothing moved, so the caret is
|
|
11895
|
+
// already where it belongs. Re-setting it from a later animation
|
|
11896
|
+
// frame, with an offset captured before this keystroke, drags the
|
|
11897
|
+
// caret backwards and reorders what follows.
|
|
10903
11898
|
e.preventDefault();
|
|
10904
|
-
input.setSelectionRange(selectionStart, selectionStart);
|
|
10905
|
-
window.requestAnimationFrame(() => {
|
|
10906
|
-
input.setSelectionRange(selectionStart, selectionStart);
|
|
10907
|
-
});
|
|
10908
11899
|
}
|
|
10909
11900
|
}}
|
|
10910
11901
|
onChange={handleChange}
|
|
@@ -12768,6 +13759,25 @@ var componentMetadata = {
|
|
|
12768
13759
|
}
|
|
12769
13760
|
]
|
|
12770
13761
|
},
|
|
13762
|
+
"date-range-picker": {
|
|
13763
|
+
"name": "DateRangePicker",
|
|
13764
|
+
"description": "A date range picker component.",
|
|
13765
|
+
"dependencies": [
|
|
13766
|
+
"class-variance-authority",
|
|
13767
|
+
"clsx",
|
|
13768
|
+
"tailwind-merge",
|
|
13769
|
+
"lucide-react"
|
|
13770
|
+
],
|
|
13771
|
+
"props": [],
|
|
13772
|
+
"variants": [],
|
|
13773
|
+
"examples": [
|
|
13774
|
+
{
|
|
13775
|
+
"title": "Basic DateRangePicker",
|
|
13776
|
+
"code": "<DateRangePicker>Content</DateRangePicker>",
|
|
13777
|
+
"description": "Simple date range picker usage"
|
|
13778
|
+
}
|
|
13779
|
+
]
|
|
13780
|
+
},
|
|
12771
13781
|
"date-time-picker": {
|
|
12772
13782
|
"name": "DateTimePicker",
|
|
12773
13783
|
"description": "A date time picker component.",
|
package/package.json
CHANGED