myoperator-mcp 0.2.372 → 0.2.374
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 +1148 -26
- 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
|
}}
|
|
@@ -5987,6 +6768,18 @@ export function flattenMultiSelectOptions(
|
|
|
5987
6768
|
return (input as MultiSelectOption[]).map(normalizeMultiSelectOption);
|
|
5988
6769
|
}
|
|
5989
6770
|
|
|
6771
|
+
/** Menu never renders narrower than this, even off a tiny trigger. */
|
|
6772
|
+
const MENU_MIN_WIDTH = 180;
|
|
6773
|
+
/** Floor for the flip/shrink height so the list is never collapsed to nothing. */
|
|
6774
|
+
const MENU_MIN_HEIGHT = 120;
|
|
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
|
+
|
|
5990
6783
|
/**
|
|
5991
6784
|
* MultiSelect trigger variants matching TextField styling
|
|
5992
6785
|
*/
|
|
@@ -6020,6 +6813,27 @@ export interface MultiSelectProps extends VariantProps<
|
|
|
6020
6813
|
disabled?: boolean;
|
|
6021
6814
|
/** Loading state with spinner */
|
|
6022
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;
|
|
6023
6837
|
/** Placeholder text when no value selected */
|
|
6024
6838
|
placeholder?: string;
|
|
6025
6839
|
/** Currently selected values (controlled) */
|
|
@@ -6039,6 +6853,18 @@ export interface MultiSelectProps extends VariantProps<
|
|
|
6039
6853
|
searchable?: boolean;
|
|
6040
6854
|
/** Search placeholder text */
|
|
6041
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;
|
|
6042
6868
|
/** Maximum selections allowed */
|
|
6043
6869
|
maxSelections?: number;
|
|
6044
6870
|
/**
|
|
@@ -6068,6 +6894,19 @@ export interface MultiSelectProps extends VariantProps<
|
|
|
6068
6894
|
showClearAll?: boolean;
|
|
6069
6895
|
/** Vertical rule before the chevron (Figma-style trigger) */
|
|
6070
6896
|
showSeparatorBeforeChevron?: boolean;
|
|
6897
|
+
/**
|
|
6898
|
+
* Element the dropdown is portaled into. Defaults to \`document.body\` so the
|
|
6899
|
+
* menu escapes \`overflow\` and \`transform\` ancestors (a Radix DialogContent is
|
|
6900
|
+
* both, and a transformed ancestor would clip a \`position: fixed\` menu).
|
|
6901
|
+
*/
|
|
6902
|
+
menuContainer?: HTMLElement | null;
|
|
6903
|
+
/**
|
|
6904
|
+
* Truncate long labels in the dropdown list to a single line with an ellipsis
|
|
6905
|
+
* instead of wrapping them. Selected chips in the trigger always truncate \u2014
|
|
6906
|
+
* this only controls the option rows.
|
|
6907
|
+
* @default false for \`simple\`, true for \`detailed\` (single-line row design)
|
|
6908
|
+
*/
|
|
6909
|
+
truncateOptionText?: boolean;
|
|
6071
6910
|
}
|
|
6072
6911
|
|
|
6073
6912
|
/**
|
|
@@ -6096,6 +6935,9 @@ const MultiSelect = React.forwardRef(
|
|
|
6096
6935
|
error,
|
|
6097
6936
|
disabled,
|
|
6098
6937
|
loading,
|
|
6938
|
+
onScrollEnd,
|
|
6939
|
+
hasMore = false,
|
|
6940
|
+
loadingMore = false,
|
|
6099
6941
|
placeholder = "Select options",
|
|
6100
6942
|
value,
|
|
6101
6943
|
defaultValue = [],
|
|
@@ -6103,6 +6945,8 @@ const MultiSelect = React.forwardRef(
|
|
|
6103
6945
|
options,
|
|
6104
6946
|
searchable,
|
|
6105
6947
|
searchPlaceholder = "Search...",
|
|
6948
|
+
selectAllLabel,
|
|
6949
|
+
summaryLabel,
|
|
6106
6950
|
maxSelections,
|
|
6107
6951
|
showSelectionFooter = true,
|
|
6108
6952
|
wrapperClassName,
|
|
@@ -6114,6 +6958,8 @@ const MultiSelect = React.forwardRef(
|
|
|
6114
6958
|
optionVariant = "simple",
|
|
6115
6959
|
separateSelectedWithDivider = false,
|
|
6116
6960
|
showClearAll = true,
|
|
6961
|
+
menuContainer,
|
|
6962
|
+
truncateOptionText,
|
|
6117
6963
|
showSeparatorBeforeChevron = false,
|
|
6118
6964
|
closeOnEscape = false,
|
|
6119
6965
|
}: MultiSelectProps,
|
|
@@ -6127,9 +6973,25 @@ const MultiSelect = React.forwardRef(
|
|
|
6127
6973
|
// Search query
|
|
6128
6974
|
const [searchQuery, setSearchQuery] = React.useState("");
|
|
6129
6975
|
|
|
6976
|
+
// \`detailed\` rows are a single-line design, so they truncate unless the
|
|
6977
|
+
// caller opts out; \`simple\` rows wrap the full label unless asked not to.
|
|
6978
|
+
const truncateOptions =
|
|
6979
|
+
truncateOptionText ?? optionVariant === "detailed";
|
|
6980
|
+
|
|
6130
6981
|
// Container ref for click outside detection
|
|
6131
6982
|
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
|
6132
6983
|
|
|
6984
|
+
/** Where the dropdown gets portaled; body unless the caller overrides. */
|
|
6985
|
+
const [portalTarget, setPortalTarget] = React.useState<HTMLElement | null>(
|
|
6986
|
+
null
|
|
6987
|
+
);
|
|
6988
|
+
|
|
6989
|
+
React.useEffect(() => {
|
|
6990
|
+
if (!isOpen || typeof document === "undefined") return;
|
|
6991
|
+
setPortalTarget(menuContainer ?? document.body);
|
|
6992
|
+
}, [isOpen, menuContainer]);
|
|
6993
|
+
|
|
6994
|
+
|
|
6133
6995
|
const { refs, floatingStyles, isPositioned } = useFloating({
|
|
6134
6996
|
open: isOpen,
|
|
6135
6997
|
placement: "bottom-start",
|
|
@@ -6140,8 +7002,19 @@ const MultiSelect = React.forwardRef(
|
|
|
6140
7002
|
shift({ padding: 8 }),
|
|
6141
7003
|
size({
|
|
6142
7004
|
padding: 8,
|
|
6143
|
-
apply({ rects, elements }) {
|
|
6144
|
-
|
|
7005
|
+
apply({ rects, elements, availableHeight, availableWidth }) {
|
|
7006
|
+
// Match the trigger, but never render an unreadably narrow menu on a
|
|
7007
|
+
// small trigger, and never spill past the viewport on a small screen.
|
|
7008
|
+
const width = Math.min(
|
|
7009
|
+
Math.max(rects.reference.width, MENU_MIN_WIDTH),
|
|
7010
|
+
availableWidth
|
|
7011
|
+
);
|
|
7012
|
+
elements.floating.style.width = \`\${width}px\`;
|
|
7013
|
+
// Let the option list shrink instead of overflowing a short viewport.
|
|
7014
|
+
elements.floating.style.setProperty(
|
|
7015
|
+
"--multi-select-available-height",
|
|
7016
|
+
\`\${Math.max(availableHeight, MENU_MIN_HEIGHT)}px\`
|
|
7017
|
+
);
|
|
6145
7018
|
},
|
|
6146
7019
|
}),
|
|
6147
7020
|
],
|
|
@@ -6163,6 +7036,112 @@ const MultiSelect = React.forwardRef(
|
|
|
6163
7036
|
[refs]
|
|
6164
7037
|
);
|
|
6165
7038
|
|
|
7039
|
+
/**
|
|
7040
|
+
* Radix Dialog / Drawer wrap their content in \`react-remove-scroll\`, which
|
|
7041
|
+
* listens for \`wheel\` / \`touchmove\` on \`document\` (bubble phase) and calls
|
|
7042
|
+
* \`preventDefault()\` for anything outside the locked subtree \u2014 our
|
|
7043
|
+
* body-portaled menu counts as outside, so its list would not scroll.
|
|
7044
|
+
* Stopping propagation at the menu keeps that document listener from ever
|
|
7045
|
+
* seeing the event, leaving the browser's native scrolling intact.
|
|
7046
|
+
* Clicks are handled separately: the dialog sets \`pointer-events: none\` on
|
|
7047
|
+
* \`<body>\`, which the menu overrides with \`pointer-events: auto\`.
|
|
7048
|
+
*/
|
|
7049
|
+
React.useEffect(() => {
|
|
7050
|
+
const node = refs.floating.current;
|
|
7051
|
+
if (!isOpen || !node) return;
|
|
7052
|
+
|
|
7053
|
+
const stop = (event: Event) => event.stopPropagation();
|
|
7054
|
+
node.addEventListener("wheel", stop, { passive: false });
|
|
7055
|
+
node.addEventListener("touchmove", stop, { passive: false });
|
|
7056
|
+
|
|
7057
|
+
/**
|
|
7058
|
+
* The dialog also traps focus. Radix \`FocusScope\` registers TWO bubble
|
|
7059
|
+
* listeners on \`document\` and either one is enough to make the search
|
|
7060
|
+
* input untypable:
|
|
7061
|
+
* - \`focusin\` \u2014 target outside the dialog, so focus is pulled back.
|
|
7062
|
+
* - \`focusout\` \u2014 fired on the element focus is LEAVING (inside the
|
|
7063
|
+
* dialog) with \`relatedTarget\` = our input; since that is outside, it
|
|
7064
|
+
* restores the previously focused element.
|
|
7065
|
+
* The \`focusout\` one never travels through the menu, so a listener on the
|
|
7066
|
+
* menu node cannot see it. Intercepting in the CAPTURE phase on
|
|
7067
|
+
* \`document\` does: capture at \`document\` runs before the bubble listeners
|
|
7068
|
+
* on the same node, so stopping propagation there means FocusScope never
|
|
7069
|
+
* runs \u2014 but only for focus events that involve the menu. Every other
|
|
7070
|
+
* focus event in the app is untouched.
|
|
7071
|
+
*/
|
|
7072
|
+
const stopMenuFocusEvent = (event: Event) => {
|
|
7073
|
+
const { target, relatedTarget } = event as FocusEvent;
|
|
7074
|
+
const touchesMenu =
|
|
7075
|
+
(target instanceof Node && node.contains(target)) ||
|
|
7076
|
+
(relatedTarget instanceof Node && node.contains(relatedTarget));
|
|
7077
|
+
if (touchesMenu) event.stopPropagation();
|
|
7078
|
+
};
|
|
7079
|
+
document.addEventListener("focusin", stopMenuFocusEvent, true);
|
|
7080
|
+
document.addEventListener("focusout", stopMenuFocusEvent, true);
|
|
7081
|
+
|
|
7082
|
+
return () => {
|
|
7083
|
+
node.removeEventListener("wheel", stop);
|
|
7084
|
+
node.removeEventListener("touchmove", stop);
|
|
7085
|
+
document.removeEventListener("focusin", stopMenuFocusEvent, true);
|
|
7086
|
+
document.removeEventListener("focusout", stopMenuFocusEvent, true);
|
|
7087
|
+
};
|
|
7088
|
+
}, [isOpen, portalTarget, refs.floating]);
|
|
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
|
+
|
|
6166
7145
|
const flatOptions = React.useMemo(
|
|
6167
7146
|
() => flattenMultiSelectOptions(options),
|
|
6168
7147
|
[options]
|
|
@@ -6172,6 +7151,28 @@ const MultiSelect = React.forwardRef(
|
|
|
6172
7151
|
const isControlled = value !== undefined;
|
|
6173
7152
|
const selectedValues = isControlled ? value : internalValue;
|
|
6174
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
|
+
|
|
6175
7176
|
// Derive state from props
|
|
6176
7177
|
const derivedState = error ? "error" : (state ?? "default");
|
|
6177
7178
|
|
|
@@ -6344,14 +7345,14 @@ const MultiSelect = React.forwardRef(
|
|
|
6344
7345
|
return (
|
|
6345
7346
|
<div
|
|
6346
7347
|
ref={containerRef}
|
|
6347
|
-
className={cn("flex flex-col gap-1", wrapperClassName)}
|
|
7348
|
+
className={cn("flex min-w-0 flex-col gap-1", wrapperClassName)}
|
|
6348
7349
|
>
|
|
6349
7350
|
{/* Label */}
|
|
6350
7351
|
{label && (
|
|
6351
7352
|
<label
|
|
6352
7353
|
htmlFor={selectId}
|
|
6353
7354
|
className={cn(
|
|
6354
|
-
"text-sm font-semibold text-semantic-text-secondary",
|
|
7355
|
+
"break-words text-sm font-semibold text-semantic-text-secondary",
|
|
6355
7356
|
labelClassName
|
|
6356
7357
|
)}
|
|
6357
7358
|
>
|
|
@@ -6387,18 +7388,40 @@ const MultiSelect = React.forwardRef(
|
|
|
6387
7388
|
triggerClassName
|
|
6388
7389
|
)}
|
|
6389
7390
|
>
|
|
6390
|
-
<div className="flex-1 flex flex-wrap gap-1">
|
|
7391
|
+
<div className="min-w-0 flex-1 flex flex-wrap gap-1">
|
|
6391
7392
|
{selectedValues.length === 0 ? (
|
|
6392
7393
|
<span className="text-base text-semantic-text-placeholder">
|
|
6393
7394
|
{placeholder}
|
|
6394
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>
|
|
6395
7413
|
) : (
|
|
6396
7414
|
selectedLabels.map((label, index) => (
|
|
6397
7415
|
<span
|
|
6398
7416
|
key={selectedValues[index]}
|
|
6399
|
-
className="inline-flex items-center gap-1 bg-semantic-bg-ui text-semantic-text-primary text-sm px-2 py-0.5 rounded"
|
|
7417
|
+
className="inline-flex min-w-0 max-w-full items-center gap-1 bg-semantic-bg-ui text-semantic-text-primary text-sm px-2 py-0.5 rounded"
|
|
6400
7418
|
>
|
|
6401
|
-
|
|
7419
|
+
<span
|
|
7420
|
+
className="min-w-0 truncate"
|
|
7421
|
+
title={typeof label === "string" ? label : undefined}
|
|
7422
|
+
>
|
|
7423
|
+
{label}
|
|
7424
|
+
</span>
|
|
6402
7425
|
<span
|
|
6403
7426
|
role="button"
|
|
6404
7427
|
tabIndex={0}
|
|
@@ -6412,7 +7435,7 @@ const MultiSelect = React.forwardRef(
|
|
|
6412
7435
|
);
|
|
6413
7436
|
}
|
|
6414
7437
|
}}
|
|
6415
|
-
className="cursor-pointer hover:text-semantic-error-primary focus:outline-none"
|
|
7438
|
+
className="shrink-0 cursor-pointer hover:text-semantic-error-primary focus:outline-none"
|
|
6416
7439
|
aria-label={\`Remove \${label}\`}
|
|
6417
7440
|
>
|
|
6418
7441
|
<X className="size-3" />
|
|
@@ -6471,14 +7494,14 @@ const MultiSelect = React.forwardRef(
|
|
|
6471
7494
|
className="size-3.5 shrink-0 text-semantic-error-primary"
|
|
6472
7495
|
aria-hidden
|
|
6473
7496
|
/>
|
|
6474
|
-
<span className="text-sm text-semantic-error-primary">
|
|
7497
|
+
<span className="min-w-0 break-words text-sm text-semantic-error-primary">
|
|
6475
7498
|
{error}
|
|
6476
7499
|
</span>
|
|
6477
7500
|
</div>
|
|
6478
7501
|
) : helperText ? (
|
|
6479
7502
|
<span
|
|
6480
7503
|
id={helperId}
|
|
6481
|
-
className="text-sm text-semantic-text-muted"
|
|
7504
|
+
className="min-w-0 break-words text-sm text-semantic-text-muted"
|
|
6482
7505
|
>
|
|
6483
7506
|
{helperText}
|
|
6484
7507
|
</span>
|
|
@@ -6487,7 +7510,7 @@ const MultiSelect = React.forwardRef(
|
|
|
6487
7510
|
)}
|
|
6488
7511
|
|
|
6489
7512
|
{isOpen &&
|
|
6490
|
-
|
|
7513
|
+
portalTarget &&
|
|
6491
7514
|
createPortal(
|
|
6492
7515
|
<TooltipProvider delayDuration={200}>
|
|
6493
7516
|
<div
|
|
@@ -6499,6 +7522,9 @@ const MultiSelect = React.forwardRef(
|
|
|
6499
7522
|
style={{
|
|
6500
7523
|
...floatingStyles,
|
|
6501
7524
|
zIndex: 10050,
|
|
7525
|
+
// Radix Dialog sets \`pointer-events: none\` on <body> while
|
|
7526
|
+
// open; without this the menu swallows nothing and clicks die.
|
|
7527
|
+
pointerEvents: "auto",
|
|
6502
7528
|
visibility: isPositioned ? undefined : "hidden",
|
|
6503
7529
|
}}
|
|
6504
7530
|
onMouseDown={(e) => e.stopPropagation()}
|
|
@@ -6517,9 +7543,43 @@ const MultiSelect = React.forwardRef(
|
|
|
6517
7543
|
</div>
|
|
6518
7544
|
)}
|
|
6519
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
|
+
|
|
6520
7572
|
{/* Options */}
|
|
6521
|
-
<div
|
|
6522
|
-
{
|
|
7573
|
+
<div
|
|
7574
|
+
ref={listRef}
|
|
7575
|
+
onScroll={maybeLoadMore}
|
|
7576
|
+
className="overflow-auto overscroll-contain p-1"
|
|
7577
|
+
style={{
|
|
7578
|
+
maxHeight:
|
|
7579
|
+
"min(15rem, var(--multi-select-available-height, 15rem))",
|
|
7580
|
+
}}
|
|
7581
|
+
>
|
|
7582
|
+
{filteredOptions.length === 0 && !loadingMore ? (
|
|
6523
7583
|
<div className="py-6 text-center text-sm text-semantic-text-muted">
|
|
6524
7584
|
No results found
|
|
6525
7585
|
</div>
|
|
@@ -6583,7 +7643,19 @@ const MultiSelect = React.forwardRef(
|
|
|
6583
7643
|
<Check className="size-4 text-semantic-primary" />
|
|
6584
7644
|
)}
|
|
6585
7645
|
</span>
|
|
6586
|
-
<span
|
|
7646
|
+
<span
|
|
7647
|
+
title={
|
|
7648
|
+
truncateOptions && typeof option.label === "string"
|
|
7649
|
+
? option.label
|
|
7650
|
+
: undefined
|
|
7651
|
+
}
|
|
7652
|
+
className={cn(
|
|
7653
|
+
"min-w-0 flex-1 text-left",
|
|
7654
|
+
truncateOptions
|
|
7655
|
+
? "truncate"
|
|
7656
|
+
: "whitespace-normal break-words"
|
|
7657
|
+
)}
|
|
7658
|
+
>
|
|
6587
7659
|
{option.label}
|
|
6588
7660
|
</span>
|
|
6589
7661
|
</button>
|
|
@@ -6615,7 +7687,19 @@ const MultiSelect = React.forwardRef(
|
|
|
6615
7687
|
aria-hidden
|
|
6616
7688
|
tabIndex={-1}
|
|
6617
7689
|
/>
|
|
6618
|
-
<span
|
|
7690
|
+
<span
|
|
7691
|
+
title={
|
|
7692
|
+
typeof option.label === "string"
|
|
7693
|
+
? option.label
|
|
7694
|
+
: undefined
|
|
7695
|
+
}
|
|
7696
|
+
className={cn(
|
|
7697
|
+
"min-w-0 flex-1 text-left",
|
|
7698
|
+
truncateOptions
|
|
7699
|
+
? "truncate"
|
|
7700
|
+
: "whitespace-normal break-words"
|
|
7701
|
+
)}
|
|
7702
|
+
>
|
|
6619
7703
|
{option.label}
|
|
6620
7704
|
</span>
|
|
6621
7705
|
{secondaryLine ? (
|
|
@@ -6657,6 +7741,17 @@ const MultiSelect = React.forwardRef(
|
|
|
6657
7741
|
return withDisabledTooltip(simpleRow);
|
|
6658
7742
|
})
|
|
6659
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}
|
|
6660
7755
|
</div>
|
|
6661
7756
|
|
|
6662
7757
|
{/* Footer with count */}
|
|
@@ -6667,7 +7762,7 @@ const MultiSelect = React.forwardRef(
|
|
|
6667
7762
|
) : null}
|
|
6668
7763
|
</div>
|
|
6669
7764
|
</TooltipProvider>,
|
|
6670
|
-
|
|
7765
|
+
portalTarget
|
|
6671
7766
|
)}
|
|
6672
7767
|
</div>
|
|
6673
7768
|
|
|
@@ -10680,7 +11775,15 @@ const TextField = React.forwardRef(
|
|
|
10680
11775
|
);
|
|
10681
11776
|
|
|
10682
11777
|
input.value = collapsedValue;
|
|
11778
|
+
// Restore the caret synchronously: assigning \`value\` above parks it at
|
|
11779
|
+
// 0, and the next keystroke can land before any animation frame runs,
|
|
11780
|
+
// which reorders what the user typed. The deferred pass is kept for
|
|
11781
|
+
// browsers that reset the caret again after React re-renders, but it
|
|
11782
|
+
// must no-op once the value has moved on \u2014 a frame that fires mid-typing
|
|
11783
|
+
// would otherwise drag the caret back to a stale position.
|
|
11784
|
+
input.setSelectionRange(nextCursor, nextCursor);
|
|
10683
11785
|
window.requestAnimationFrame(() => {
|
|
11786
|
+
if (input.value !== collapsedValue) return;
|
|
10684
11787
|
input.setSelectionRange(nextCursor, nextCursor);
|
|
10685
11788
|
});
|
|
10686
11789
|
}
|
|
@@ -10764,11 +11867,11 @@ const TextField = React.forwardRef(
|
|
|
10764
11867
|
input.value.slice(selectionEnd);
|
|
10765
11868
|
|
|
10766
11869
|
if (nextValue.includes(" ")) {
|
|
11870
|
+
// Blocking the insertion is enough \u2014 nothing moved, so the caret is
|
|
11871
|
+
// already where it belongs. Re-setting it from a later animation
|
|
11872
|
+
// frame, with an offset captured before this keystroke, drags the
|
|
11873
|
+
// caret backwards and reorders what follows.
|
|
10767
11874
|
e.preventDefault();
|
|
10768
|
-
input.setSelectionRange(selectionStart, selectionStart);
|
|
10769
|
-
window.requestAnimationFrame(() => {
|
|
10770
|
-
input.setSelectionRange(selectionStart, selectionStart);
|
|
10771
|
-
});
|
|
10772
11875
|
}
|
|
10773
11876
|
}}
|
|
10774
11877
|
onChange={handleChange}
|
|
@@ -12632,6 +13735,25 @@ var componentMetadata = {
|
|
|
12632
13735
|
}
|
|
12633
13736
|
]
|
|
12634
13737
|
},
|
|
13738
|
+
"date-range-picker": {
|
|
13739
|
+
"name": "DateRangePicker",
|
|
13740
|
+
"description": "A date range picker component.",
|
|
13741
|
+
"dependencies": [
|
|
13742
|
+
"class-variance-authority",
|
|
13743
|
+
"clsx",
|
|
13744
|
+
"tailwind-merge",
|
|
13745
|
+
"lucide-react"
|
|
13746
|
+
],
|
|
13747
|
+
"props": [],
|
|
13748
|
+
"variants": [],
|
|
13749
|
+
"examples": [
|
|
13750
|
+
{
|
|
13751
|
+
"title": "Basic DateRangePicker",
|
|
13752
|
+
"code": "<DateRangePicker>Content</DateRangePicker>",
|
|
13753
|
+
"description": "Simple date range picker usage"
|
|
13754
|
+
}
|
|
13755
|
+
]
|
|
13756
|
+
},
|
|
12635
13757
|
"date-time-picker": {
|
|
12636
13758
|
"name": "DateTimePicker",
|
|
12637
13759
|
"description": "A date time picker component.",
|