myoperator-mcp 0.2.357 → 0.2.358

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +253 -72
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2812,6 +2812,129 @@ function isPointerInsideElement(
2812
2812
  return false;
2813
2813
  }
2814
2814
 
2815
+ // Floating sub-dropdowns (Month / Year / Time) are portaled to document.body
2816
+ // so they escape the popover's overflow clipping. Because they live outside the
2817
+ // popover in the DOM, the picker's own outside-click handler can't recognize
2818
+ // them via \`contains\`. Tagging each portaled panel with this attribute lets the
2819
+ // handler detect clicks that land inside an open sub-dropdown.
2820
+ const DROPDOWN_MARKER_ATTR = "data-dtp-dropdown";
2821
+ const DROPDOWN_GAP = 4;
2822
+ const DROPDOWN_MARGIN = 8;
2823
+ const DROPDOWN_MAX_HEIGHT = 256;
2824
+ const DROPDOWN_Z_INDEX = 10060;
2825
+ const DROPDOWN_PLACEMENT: Placement = "bottom-start";
2826
+ const DROPDOWN_WIDTH_VAR = "--date-time-picker-dropdown-width";
2827
+ const DROPDOWN_HEIGHT_VAR = "--date-time-picker-dropdown-height";
2828
+
2829
+ function isPointerInsideDropdown(event: MouseEvent) {
2830
+ const target = event.target;
2831
+
2832
+ return (
2833
+ target instanceof Element &&
2834
+ target.closest(\`[\${DROPDOWN_MARKER_ATTR}]\`) !== null
2835
+ );
2836
+ }
2837
+
2838
+ // Shared floating + dismissal behavior for the Month, Year, and Time dropdowns.
2839
+ // Each instance positions its panel against its trigger, mirrors the picker's
2840
+ // portal strategy, and closes itself on an outside click or Escape \u2014 without
2841
+ // collapsing the parent picker (Escape is captured so it doesn't bubble to the
2842
+ // picker's own Escape handler).
2843
+ function useFloatingDropdown({
2844
+ open,
2845
+ onOpenChange,
2846
+ strategy,
2847
+ }: {
2848
+ open: boolean;
2849
+ onOpenChange: (open: boolean) => void;
2850
+ strategy: Strategy;
2851
+ }) {
2852
+ const middleware = React.useMemo(
2853
+ () => [
2854
+ offset(DROPDOWN_GAP),
2855
+ flip({ padding: DROPDOWN_MARGIN }),
2856
+ shift({ padding: DROPDOWN_MARGIN }),
2857
+ floatingSize({
2858
+ padding: DROPDOWN_MARGIN,
2859
+ apply({ availableHeight, rects, elements }) {
2860
+ const maxHeight = Math.max(
2861
+ 1,
2862
+ Math.min(DROPDOWN_MAX_HEIGHT, availableHeight)
2863
+ );
2864
+ elements.floating.style.setProperty(
2865
+ DROPDOWN_HEIGHT_VAR,
2866
+ \`\${maxHeight}px\`
2867
+ );
2868
+ elements.floating.style.setProperty(
2869
+ DROPDOWN_WIDTH_VAR,
2870
+ \`\${rects.reference.width}px\`
2871
+ );
2872
+ },
2873
+ }),
2874
+ ],
2875
+ []
2876
+ );
2877
+
2878
+ const floating = useFloating<HTMLButtonElement>({
2879
+ open,
2880
+ placement: DROPDOWN_PLACEMENT,
2881
+ strategy,
2882
+ transform: false,
2883
+ middleware,
2884
+ whileElementsMounted: (reference, floatingEl, update) =>
2885
+ autoUpdate(reference, floatingEl, update, { animationFrame: true }),
2886
+ });
2887
+
2888
+ const { refs, floatingStyles, isPositioned } = floating;
2889
+
2890
+ React.useEffect(() => {
2891
+ if (!open) return;
2892
+
2893
+ const handlePointerDown = (event: MouseEvent) => {
2894
+ const reference = refs.reference.current as HTMLElement | null;
2895
+ if (
2896
+ !isPointerInsideElement(event, reference) &&
2897
+ !isPointerInsideElement(event, refs.floating.current)
2898
+ ) {
2899
+ onOpenChange(false);
2900
+ }
2901
+ };
2902
+
2903
+ const handleKeyDown = (event: KeyboardEvent) => {
2904
+ if (event.key === "Escape") {
2905
+ // Capture phase + stopPropagation keeps Escape from also reaching the
2906
+ // picker's handler, so it dismisses just this dropdown.
2907
+ event.stopPropagation();
2908
+ onOpenChange(false);
2909
+ }
2910
+ };
2911
+
2912
+ // Capture phase: the popover container calls stopPropagation on its own
2913
+ // mousedown, which would otherwise swallow clicks inside it before they
2914
+ // reach a bubble-phase document listener. Listening on capture runs this
2915
+ // handler top-down \u2014 before that stopPropagation \u2014 so clicks anywhere
2916
+ // outside the dropdown (including elsewhere in the popover) dismiss it.
2917
+ document.addEventListener("mousedown", handlePointerDown, true);
2918
+ document.addEventListener("keydown", handleKeyDown, true);
2919
+
2920
+ return () => {
2921
+ document.removeEventListener("mousedown", handlePointerDown, true);
2922
+ document.removeEventListener("keydown", handleKeyDown, true);
2923
+ };
2924
+ }, [open, onOpenChange, refs]);
2925
+
2926
+ const setReference = React.useCallback(
2927
+ (node: HTMLButtonElement | null) => refs.setReference(node),
2928
+ [refs]
2929
+ );
2930
+ const setFloating = React.useCallback(
2931
+ (node: HTMLDivElement | null) => refs.setFloating(node),
2932
+ [refs]
2933
+ );
2934
+
2935
+ return { setReference, setFloating, floatingStyles, isPositioned };
2936
+ }
2937
+
2815
2938
  function timeHasVisibleSeconds(time?: string) {
2816
2939
  const [, , second = "00"] = (time ?? "").split(":");
2817
2940
  return /^\\d{1,2}$/.test(second) && Number(second) !== 0;
@@ -3459,6 +3582,8 @@ function CalendarDropdown({
3459
3582
  open,
3460
3583
  onOpenChange,
3461
3584
  onValueChange,
3585
+ portalMount,
3586
+ strategy,
3462
3587
  }: {
3463
3588
  id: string;
3464
3589
  label: string;
@@ -3467,8 +3592,16 @@ function CalendarDropdown({
3467
3592
  open: boolean;
3468
3593
  onOpenChange: (open: boolean) => void;
3469
3594
  onValueChange: (value: string) => void;
3595
+ portalMount: HTMLElement | null;
3596
+ strategy: Strategy;
3470
3597
  }) {
3471
3598
  const selectedOption = options.find((option) => option.value === value);
3599
+ const { setReference, setFloating, floatingStyles, isPositioned } =
3600
+ useFloatingDropdown({
3601
+ open,
3602
+ onOpenChange,
3603
+ strategy,
3604
+ });
3472
3605
 
3473
3606
  return (
3474
3607
  <div className="relative">
@@ -3476,6 +3609,7 @@ function CalendarDropdown({
3476
3609
  {label}
3477
3610
  </label>
3478
3611
  <button
3612
+ ref={setReference}
3479
3613
  id={id}
3480
3614
  type="button"
3481
3615
  role="combobox"
@@ -3495,37 +3629,51 @@ function CalendarDropdown({
3495
3629
  aria-hidden="true"
3496
3630
  />
3497
3631
  </button>
3498
- {open && (
3499
- <div
3500
- id={\`\${id}-options\`}
3501
- role="listbox"
3502
- aria-label={\`\${label} options\`}
3503
- className="absolute left-0 top-full z-10 mt-1 max-h-52 min-w-full overflow-y-auto rounded-md border border-solid border-semantic-border-layout bg-semantic-bg-primary p-1 shadow-lg"
3504
- >
3505
- {options.map((option) => (
3506
- <button
3507
- key={option.value}
3508
- type="button"
3509
- role="option"
3510
- aria-label={option.label}
3511
- aria-selected={option.value === value}
3512
- disabled={option.disabled}
3513
- className={cn(
3514
- "flex w-full items-center rounded px-2 py-1.5 text-left text-sm text-semantic-text-primary transition-colors hover:bg-semantic-bg-hover disabled:cursor-not-allowed disabled:opacity-40",
3515
- option.value === value && "bg-semantic-primary text-semantic-text-inverted"
3516
- )}
3517
- onClick={() => {
3518
- if (option.disabled) return;
3632
+ {open &&
3633
+ portalMount &&
3634
+ createPortal(
3635
+ <div
3636
+ ref={setFloating}
3637
+ id={\`\${id}-options\`}
3638
+ role="listbox"
3639
+ aria-label={\`\${label} options\`}
3640
+ data-dtp-dropdown=""
3641
+ className="flex flex-col gap-0.5 overflow-y-auto rounded-md border border-solid border-semantic-border-layout bg-semantic-bg-primary p-1 shadow-lg [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-semantic-border-secondary [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-1.5"
3642
+ style={{
3643
+ ...floatingStyles,
3644
+ width: \`var(\${DROPDOWN_WIDTH_VAR}, auto)\`,
3645
+ maxHeight: \`var(\${DROPDOWN_HEIGHT_VAR}, \${DROPDOWN_MAX_HEIGHT}px)\`,
3646
+ zIndex: DROPDOWN_Z_INDEX,
3647
+ visibility: isPositioned ? undefined : "hidden",
3648
+ }}
3649
+ >
3650
+ {options.map((option) => (
3651
+ <button
3652
+ key={option.value}
3653
+ type="button"
3654
+ role="option"
3655
+ aria-label={option.label}
3656
+ aria-selected={option.value === value}
3657
+ disabled={option.disabled}
3658
+ className={cn(
3659
+ "flex w-full shrink-0 items-center rounded-md border border-solid px-2 py-1.5 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40",
3660
+ option.value === value
3661
+ ? "border-semantic-info-border bg-semantic-info-surface font-semibold text-semantic-text-primary"
3662
+ : "border-transparent text-semantic-text-secondary hover:bg-semantic-bg-hover"
3663
+ )}
3664
+ onClick={() => {
3665
+ if (option.disabled) return;
3519
3666
 
3520
- onValueChange(option.value);
3521
- onOpenChange(false);
3522
- }}
3523
- >
3524
- {option.label}
3525
- </button>
3526
- ))}
3527
- </div>
3528
- )}
3667
+ onValueChange(option.value);
3668
+ onOpenChange(false);
3669
+ }}
3670
+ >
3671
+ {option.label}
3672
+ </button>
3673
+ ))}
3674
+ </div>,
3675
+ portalMount
3676
+ )}
3529
3677
  </div>
3530
3678
  );
3531
3679
  }
@@ -3604,6 +3752,8 @@ function TimeField({
3604
3752
  open,
3605
3753
  onOpenChange,
3606
3754
  onChange,
3755
+ portalMount,
3756
+ strategy,
3607
3757
  }: {
3608
3758
  id: string;
3609
3759
  label: string;
@@ -3614,8 +3764,16 @@ function TimeField({
3614
3764
  open: boolean;
3615
3765
  onOpenChange: (open: boolean) => void;
3616
3766
  onChange: (time: string) => void;
3767
+ portalMount: HTMLElement | null;
3768
+ strategy: Strategy;
3617
3769
  }) {
3618
3770
  const { hour12, minute, second, meridiem } = decomposeTime(value);
3771
+ const { setReference, setFloating, floatingStyles, isPositioned } =
3772
+ useFloatingDropdown({
3773
+ open,
3774
+ onOpenChange,
3775
+ strategy,
3776
+ });
3619
3777
  // When seconds are hidden they are not editable, so normalize to :00 on any
3620
3778
  // change rather than carrying a stale seconds value forward.
3621
3779
  const effectiveSecond = showSeconds ? second : 0;
@@ -3660,6 +3818,7 @@ function TimeField({
3660
3818
  {label}
3661
3819
  </span>
3662
3820
  <button
3821
+ ref={setReference}
3663
3822
  id={id}
3664
3823
  type="button"
3665
3824
  aria-label={label}
@@ -3687,51 +3846,64 @@ function TimeField({
3687
3846
  aria-hidden="true"
3688
3847
  />
3689
3848
  </button>
3690
- {open && (
3691
- <div
3692
- className={cn(
3693
- "grid overflow-hidden rounded-lg border border-solid border-semantic-border-layout bg-semantic-bg-primary shadow-sm",
3694
- showSeconds ? "grid-cols-4" : "grid-cols-3"
3695
- )}
3696
- >
3697
- <TimeColumn
3698
- header="Hours"
3699
- ariaLabel={\`\${label} hours\`}
3700
- options={hourOptions}
3701
- onSelect={(key) =>
3702
- onChange(composeTime(Number(key), minute, effectiveSecond, meridiem))
3703
- }
3704
- />
3705
- <TimeColumn
3706
- header="Minutes"
3707
- ariaLabel={\`\${label} minutes\`}
3708
- options={minuteOptions}
3709
- onSelect={(key) =>
3710
- onChange(composeTime(hour12, Number(key), effectiveSecond, meridiem))
3711
- }
3712
- />
3713
- {showSeconds && (
3849
+ {open &&
3850
+ portalMount &&
3851
+ createPortal(
3852
+ <div
3853
+ ref={setFloating}
3854
+ role="listbox"
3855
+ aria-label={\`\${label} options\`}
3856
+ data-dtp-dropdown=""
3857
+ className={cn(
3858
+ "grid overflow-hidden rounded-lg border border-solid border-semantic-border-layout bg-semantic-bg-primary shadow-lg",
3859
+ showSeconds ? "grid-cols-4" : "grid-cols-3"
3860
+ )}
3861
+ style={{
3862
+ ...floatingStyles,
3863
+ width: \`var(\${DROPDOWN_WIDTH_VAR}, auto)\`,
3864
+ zIndex: DROPDOWN_Z_INDEX,
3865
+ visibility: isPositioned ? undefined : "hidden",
3866
+ }}
3867
+ >
3714
3868
  <TimeColumn
3715
- header="Seconds"
3716
- ariaLabel={\`\${label} seconds\`}
3717
- options={secondOptions}
3869
+ header="Hours"
3870
+ ariaLabel={\`\${label} hours\`}
3871
+ options={hourOptions}
3718
3872
  onSelect={(key) =>
3719
- onChange(composeTime(hour12, minute, Number(key), meridiem))
3873
+ onChange(composeTime(Number(key), minute, effectiveSecond, meridiem))
3720
3874
  }
3721
3875
  />
3722
- )}
3723
- <TimeColumn
3724
- header="AM/PM"
3725
- ariaLabel={\`\${label} meridiem\`}
3726
- options={meridiemOptions}
3727
- onSelect={(key) =>
3728
- onChange(
3729
- composeTime(hour12, minute, effectiveSecond, key as Meridiem)
3730
- )
3731
- }
3732
- />
3733
- </div>
3734
- )}
3876
+ <TimeColumn
3877
+ header="Minutes"
3878
+ ariaLabel={\`\${label} minutes\`}
3879
+ options={minuteOptions}
3880
+ onSelect={(key) =>
3881
+ onChange(composeTime(hour12, Number(key), effectiveSecond, meridiem))
3882
+ }
3883
+ />
3884
+ {showSeconds && (
3885
+ <TimeColumn
3886
+ header="Seconds"
3887
+ ariaLabel={\`\${label} seconds\`}
3888
+ options={secondOptions}
3889
+ onSelect={(key) =>
3890
+ onChange(composeTime(hour12, minute, Number(key), meridiem))
3891
+ }
3892
+ />
3893
+ )}
3894
+ <TimeColumn
3895
+ header="AM/PM"
3896
+ ariaLabel={\`\${label} meridiem\`}
3897
+ options={meridiemOptions}
3898
+ onSelect={(key) =>
3899
+ onChange(
3900
+ composeTime(hour12, minute, effectiveSecond, key as Meridiem)
3901
+ )
3902
+ }
3903
+ />
3904
+ </div>,
3905
+ portalMount
3906
+ )}
3735
3907
  </div>
3736
3908
  );
3737
3909
  }
@@ -3938,7 +4110,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3938
4110
  const handlePointerDown = (event: MouseEvent) => {
3939
4111
  if (
3940
4112
  !isPointerInsideElement(event, rootRef.current) &&
3941
- !isPointerInsideElement(event, popoverRef.current)
4113
+ !isPointerInsideElement(event, popoverRef.current) &&
4114
+ !isPointerInsideDropdown(event)
3942
4115
  ) {
3943
4116
  setOpen(false);
3944
4117
  }
@@ -4284,6 +4457,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
4284
4457
  id={\`\${triggerId}-month\`}
4285
4458
  label="Month"
4286
4459
  value={visibleMonth.getMonth().toString()}
4460
+ portalMount={portalMount}
4461
+ strategy={floatingStrategy}
4287
4462
  open={openCalendarDropdown === "month"}
4288
4463
  onOpenChange={(nextOpen) =>
4289
4464
  setOpenCalendarDropdown(nextOpen ? "month" : null)
@@ -4319,6 +4494,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
4319
4494
  id={\`\${triggerId}-year\`}
4320
4495
  label="Year"
4321
4496
  value={visibleMonth.getFullYear().toString()}
4497
+ portalMount={portalMount}
4498
+ strategy={floatingStrategy}
4322
4499
  open={openCalendarDropdown === "year"}
4323
4500
  onOpenChange={(nextOpen) =>
4324
4501
  setOpenCalendarDropdown(nextOpen ? "year" : null)
@@ -4429,6 +4606,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
4429
4606
  showSeconds={resolvedShowSeconds}
4430
4607
  minuteStep={minuteStep}
4431
4608
  secondStep={secondStep}
4609
+ portalMount={portalMount}
4610
+ strategy={floatingStrategy}
4432
4611
  open={openTimeField === "start"}
4433
4612
  onOpenChange={(nextOpen) =>
4434
4613
  setOpenTimeField(nextOpen ? "start" : null)
@@ -4449,6 +4628,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
4449
4628
  showSeconds={resolvedShowSeconds}
4450
4629
  minuteStep={minuteStep}
4451
4630
  secondStep={secondStep}
4631
+ portalMount={portalMount}
4632
+ strategy={floatingStrategy}
4452
4633
  open={openTimeField === "end"}
4453
4634
  onOpenChange={(nextOpen) =>
4454
4635
  setOpenTimeField(nextOpen ? "end" : null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.357",
3
+ "version": "0.2.358",
4
4
  "description": "MCP server for myOperator UI components - enables AI assistants to access component metadata, examples, and design tokens",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.js",