myoperator-mcp 0.2.295 → 0.2.297

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 +111 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5593,6 +5593,22 @@ export interface SelectFieldProps {
5593
5593
  searchable?: boolean;
5594
5594
  /** Search placeholder text */
5595
5595
  searchPlaceholder?: string;
5596
+ /**
5597
+ * Controlled search value. When provided, internal search state is
5598
+ * ignored and the consumer owns the value \u2014 typically used to drive
5599
+ * server-side filtering against a paginated API. The client-side
5600
+ * \`option.label\` filter is also skipped, since the consumer is expected
5601
+ * to have already filtered the options upstream.
5602
+ *
5603
+ * Pair with \`onSearchChange\`. Leave both undefined for the default
5604
+ * uncontrolled, client-side filtering behavior.
5605
+ */
5606
+ searchValue?: string;
5607
+ /**
5608
+ * Fires on every keystroke in the search input. Also fires with \`""\`
5609
+ * when the dropdown closes (so consumers can reset their query state).
5610
+ */
5611
+ onSearchChange?: (value: string) => void;
5596
5612
  /** Additional class for wrapper */
5597
5613
  wrapperClassName?: string;
5598
5614
  /** Additional class for trigger */
@@ -5691,6 +5707,8 @@ const SelectField = React.forwardRef(
5691
5707
  options,
5692
5708
  searchable,
5693
5709
  searchPlaceholder = "Search...",
5710
+ searchValue,
5711
+ onSearchChange,
5694
5712
  wrapperClassName,
5695
5713
  triggerClassName,
5696
5714
  labelClassName,
@@ -5702,8 +5720,11 @@ const SelectField = React.forwardRef(
5702
5720
  }: SelectFieldProps,
5703
5721
  ref: React.Ref<HTMLButtonElement>
5704
5722
  ) => {
5705
- // Internal state for search
5723
+ // Internal state for uncontrolled mode. When \`searchValue\` is provided,
5724
+ // the consumer owns the value and this state is unused.
5706
5725
  const [searchQuery, setSearchQuery] = React.useState("");
5726
+ const isSearchControlled = searchValue !== undefined;
5727
+ const effectiveSearchQuery = isSearchControlled ? searchValue : searchQuery;
5707
5728
 
5708
5729
  // Combined value change handler that also fires onSelect with full option object.
5709
5730
  // When interceptValue returns false, onValueChange is skipped (only onSelect fires).
@@ -5756,11 +5777,16 @@ const SelectField = React.forwardRef(
5756
5777
  const ungrouped: SelectOption[] = [];
5757
5778
 
5758
5779
  options.forEach((option) => {
5759
- // Filter by search query if searchable
5760
- if (searchable && searchQuery) {
5761
- if (!option.label.toLowerCase().includes(searchQuery.toLowerCase())) {
5762
- return;
5763
- }
5780
+ // Client-side filter only in uncontrolled mode. In controlled mode
5781
+ // the consumer has already filtered server-side; re-filtering here
5782
+ // would mask items their API returned.
5783
+ if (
5784
+ searchable &&
5785
+ !isSearchControlled &&
5786
+ searchQuery &&
5787
+ !option.label.toLowerCase().includes(searchQuery.toLowerCase())
5788
+ ) {
5789
+ return;
5764
5790
  }
5765
5791
 
5766
5792
  if (option.group) {
@@ -5774,7 +5800,7 @@ const SelectField = React.forwardRef(
5774
5800
  });
5775
5801
 
5776
5802
  return { groups, ungrouped };
5777
- }, [options, searchable, searchQuery]);
5803
+ }, [options, searchable, isSearchControlled, searchQuery]);
5778
5804
 
5779
5805
  const hasGroups = Object.keys(groupedOptions.groups).length > 0;
5780
5806
 
@@ -5789,13 +5815,23 @@ const SelectField = React.forwardRef(
5789
5815
 
5790
5816
  // Handle search input change
5791
5817
  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
5792
- setSearchQuery(e.target.value);
5818
+ const next = e.target.value;
5819
+ if (!isSearchControlled) {
5820
+ setSearchQuery(next);
5821
+ }
5822
+ onSearchChange?.(next);
5793
5823
  };
5794
5824
 
5795
- // Reset search when dropdown closes
5825
+ // Reset search when dropdown closes. In controlled mode we notify the
5826
+ // consumer so they can reset their state; in uncontrolled mode we clear
5827
+ // our own state directly.
5796
5828
  const handleOpenChange = (open: boolean) => {
5797
5829
  if (!open) {
5798
- setSearchQuery("");
5830
+ if (isSearchControlled) {
5831
+ onSearchChange?.("");
5832
+ } else {
5833
+ setSearchQuery("");
5834
+ }
5799
5835
  }
5800
5836
  };
5801
5837
 
@@ -5849,7 +5885,7 @@ const SelectField = React.forwardRef(
5849
5885
  <input
5850
5886
  type="text"
5851
5887
  placeholder={searchPlaceholder}
5852
- value={searchQuery}
5888
+ value={effectiveSearchQuery}
5853
5889
  onChange={handleSearchChange}
5854
5890
  className="w-full h-8 text-sm bg-transparent placeholder:text-semantic-text-muted focus:outline-none"
5855
5891
  // Prevent closing dropdown when clicking input
@@ -5893,7 +5929,7 @@ const SelectField = React.forwardRef(
5893
5929
 
5894
5930
  {/* No results message */}
5895
5931
  {searchable &&
5896
- searchQuery &&
5932
+ effectiveSearchQuery &&
5897
5933
  groupedOptions.ungrouped.length === 0 &&
5898
5934
  Object.keys(groupedOptions.groups).length === 0 && (
5899
5935
  <div className="py-6 text-center text-sm text-semantic-text-muted">
@@ -6085,12 +6121,23 @@ export type SelectContentProps = React.ComponentPropsWithoutRef<
6085
6121
  typeof SelectPrimitive.Content
6086
6122
  > & {
6087
6123
  /**
6088
- * Fires on the scrollable list viewport when scrolling completes (\`scrollend\`).
6089
- * Use with paginated option lists (e.g. load the next page when the user reaches the bottom).
6124
+ * Fires on the scrollable list viewport when the user reaches the bottom.
6125
+ * React 18 has no synthetic event for \`scrollend\`, so the listener is
6126
+ * attached imperatively to the viewport DOM node. On browsers that
6127
+ * support the native \`scrollend\` event (Chrome/Edge 114+, Firefox 109+,
6128
+ * Safari 17.4+) we use it directly; on older Safari we fall back to a
6129
+ * debounced \`scroll\` listener with a 24px bottom threshold.
6130
+ *
6131
+ * The handler receives a native Event typed as React.UIEvent for
6132
+ * back-compat \u2014 \`event.currentTarget\` is the viewport div, so consumers
6133
+ * can still read scrollTop/scrollHeight/clientHeight from it.
6090
6134
  */
6091
6135
  onViewportScrollEnd?: (event: React.UIEvent<HTMLDivElement>) => void;
6092
6136
  };
6093
6137
 
6138
+ const BOTTOM_THRESHOLD_PX = 24;
6139
+ const SCROLL_DEBOUNCE_MS = 150;
6140
+
6094
6141
  const SelectContent = React.forwardRef(
6095
6142
  (
6096
6143
  {
@@ -6104,6 +6151,55 @@ const SelectContent = React.forwardRef(
6104
6151
  ) => {
6105
6152
  useUnlockBodyScroll();
6106
6153
 
6154
+ // Use a state-backed ref so the effect re-runs when the viewport mounts.
6155
+ // The viewport lives inside Radix's Portal and only attaches when the
6156
+ // Select opens \u2014 a plain useRef wouldn't trigger the effect.
6157
+ const [viewport, setViewport] = React.useState<HTMLDivElement | null>(null);
6158
+
6159
+ React.useEffect(() => {
6160
+ if (!viewport || !onViewportScrollEnd) return;
6161
+
6162
+
6163
+ const isAtBottom = () => {
6164
+ const { scrollTop, scrollHeight, clientHeight } = viewport;
6165
+ return (
6166
+ scrollTop + clientHeight >= scrollHeight - BOTTOM_THRESHOLD_PX
6167
+ );
6168
+ };
6169
+
6170
+ const supportsScrollEnd =
6171
+ typeof window !== "undefined" && "onscrollend" in window;
6172
+
6173
+ if (supportsScrollEnd) {
6174
+ const handler = (event: Event) => {
6175
+ if (isAtBottom()) {
6176
+ onViewportScrollEnd(
6177
+ event as unknown as React.UIEvent<HTMLDivElement>
6178
+ );
6179
+ }
6180
+ };
6181
+ viewport.addEventListener("scrollend", handler);
6182
+ return () => viewport.removeEventListener("scrollend", handler);
6183
+ }
6184
+
6185
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
6186
+ const handler = (event: Event) => {
6187
+ if (timeoutId) clearTimeout(timeoutId);
6188
+ timeoutId = setTimeout(() => {
6189
+ if (isAtBottom()) {
6190
+ onViewportScrollEnd(
6191
+ event as unknown as React.UIEvent<HTMLDivElement>
6192
+ );
6193
+ }
6194
+ }, SCROLL_DEBOUNCE_MS);
6195
+ };
6196
+ viewport.addEventListener("scroll", handler, { passive: true });
6197
+ return () => {
6198
+ viewport.removeEventListener("scroll", handler);
6199
+ if (timeoutId) clearTimeout(timeoutId);
6200
+ };
6201
+ }, [viewport, onViewportScrollEnd]);
6202
+
6107
6203
  return (
6108
6204
  <SelectPrimitive.Portal>
6109
6205
  <SelectPrimitive.Content
@@ -6124,17 +6220,13 @@ const SelectContent = React.forwardRef(
6124
6220
  >
6125
6221
  <SelectScrollUpButton />
6126
6222
  <SelectPrimitive.Viewport
6223
+ ref={setViewport}
6127
6224
  data-select-viewport=""
6128
6225
  className={cn(
6129
6226
  "p-1",
6130
6227
  position === "popper" &&
6131
6228
  "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
6132
6229
  )}
6133
- {...((onViewportScrollEnd
6134
- ? { onScrollEnd: onViewportScrollEnd }
6135
- : {}) as React.ComponentPropsWithoutRef<
6136
- typeof SelectPrimitive.Viewport
6137
- >)}
6138
6230
  >
6139
6231
  {children}
6140
6232
  </SelectPrimitive.Viewport>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.295",
3
+ "version": "0.2.297",
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",