myoperator-mcp 0.2.376 → 0.2.377

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 +63 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6861,6 +6861,18 @@ export interface MultiSelectProps extends VariantProps<
6861
6861
  searchable?: boolean;
6862
6862
  /** Search placeholder text */
6863
6863
  searchPlaceholder?: string;
6864
+ /**
6865
+ * Controlled search value. Pair with \`onSearchQueryChange\` when filtering
6866
+ * happens server-side (e.g. alongside \`onScrollEnd\` pagination, where each
6867
+ * page only has a slice of the full result set \u2014 client-side filtering
6868
+ * would search just that slice and show false "No results found" states).
6869
+ * When provided, the component stops managing its own search state and
6870
+ * stops filtering \`options\` itself; the caller is expected to pass already
6871
+ * filtered \`options\` for the current \`searchQuery\`.
6872
+ */
6873
+ searchQuery?: string;
6874
+ /** Fires on every search input change. Required to pair with \`searchQuery\`. */
6875
+ onSearchQueryChange?: (query: string) => void;
6864
6876
  /**
6865
6877
  * When set, the trigger shows a single compact summary (e.g. "3 lines
6866
6878
  * selected") instead of one chip per selection; hovering it reveals the full
@@ -6953,6 +6965,8 @@ const MultiSelect = React.forwardRef(
6953
6965
  options,
6954
6966
  searchable,
6955
6967
  searchPlaceholder = "Search...",
6968
+ searchQuery: searchQueryProp,
6969
+ onSearchQueryChange,
6956
6970
  selectAllLabel,
6957
6971
  summaryLabel,
6958
6972
  maxSelections,
@@ -6978,8 +6992,20 @@ const MultiSelect = React.forwardRef(
6978
6992
  React.useState<string[]>(defaultValue);
6979
6993
  // Dropdown open state
6980
6994
  const [isOpen, setIsOpen] = React.useState(false);
6981
- // Search query
6982
- const [searchQuery, setSearchQuery] = React.useState("");
6995
+ // Search query \u2014 controlled when the caller passes \`searchQuery\` (server-side
6996
+ // filtering), uncontrolled otherwise.
6997
+ const [internalSearchQuery, setInternalSearchQuery] = React.useState("");
6998
+ const isSearchControlled = searchQueryProp !== undefined;
6999
+ const searchQuery = isSearchControlled ? searchQueryProp : internalSearchQuery;
7000
+ const updateSearchQuery = React.useCallback(
7001
+ (next: string) => {
7002
+ if (!isSearchControlled) {
7003
+ setInternalSearchQuery(next);
7004
+ }
7005
+ onSearchQueryChange?.(next);
7006
+ },
7007
+ [isSearchControlled, onSearchQueryChange]
7008
+ );
6983
7009
 
6984
7010
  // \`detailed\` rows are a single-line design, so they truncate unless the
6985
7011
  // caller opts out; \`simple\` rows wrap the full label unless asked not to.
@@ -7257,8 +7283,11 @@ const MultiSelect = React.forwardRef(
7257
7283
  // Determine aria-describedby
7258
7284
  const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined;
7259
7285
 
7260
- // Filter options by search query
7286
+ // Filter options by search query. Skipped when \`searchQuery\` is
7287
+ // controlled \u2014 the caller owns filtering then (typically server-side, so
7288
+ // \`options\` is already the filtered slice for the current query).
7261
7289
  const filteredOptions = React.useMemo(() => {
7290
+ if (isSearchControlled) return flatOptions;
7262
7291
  if (!searchable || !searchQuery.trim()) return flatOptions;
7263
7292
  const q = searchQuery.toLowerCase();
7264
7293
  return flatOptions.filter((option) => {
@@ -7269,7 +7298,7 @@ const MultiSelect = React.forwardRef(
7269
7298
  (option.group?.toLowerCase().includes(q) ?? false)
7270
7299
  );
7271
7300
  });
7272
- }, [flatOptions, searchable, searchQuery]);
7301
+ }, [flatOptions, searchable, searchQuery, isSearchControlled]);
7273
7302
 
7274
7303
  type DisplayItem =
7275
7304
  | { type: "option"; option: MultiSelectOption }
@@ -7387,7 +7416,7 @@ const MultiSelect = React.forwardRef(
7387
7416
  return;
7388
7417
  }
7389
7418
  setIsOpen(false);
7390
- setSearchQuery("");
7419
+ updateSearchQuery("");
7391
7420
  };
7392
7421
 
7393
7422
  const timeoutId = window.setTimeout(() => {
@@ -7398,13 +7427,13 @@ const MultiSelect = React.forwardRef(
7398
7427
  window.clearTimeout(timeoutId);
7399
7428
  document.removeEventListener("mousedown", handleClickOutside);
7400
7429
  };
7401
- }, [isOpen, refs.floating]);
7430
+ }, [isOpen, refs.floating, updateSearchQuery]);
7402
7431
 
7403
7432
  // Handle keyboard navigation
7404
7433
  const handleKeyDown = (e: React.KeyboardEvent) => {
7405
7434
  if (e.key === "Escape" && closeOnEscape) {
7406
7435
  setIsOpen(false);
7407
- setSearchQuery("");
7436
+ updateSearchQuery("");
7408
7437
  } else if (e.key === "Enter" || e.key === " ") {
7409
7438
  if (!isOpen) {
7410
7439
  e.preventDefault();
@@ -7607,7 +7636,7 @@ const MultiSelect = React.forwardRef(
7607
7636
  type="text"
7608
7637
  placeholder={searchPlaceholder}
7609
7638
  value={searchQuery}
7610
- onChange={(e) => setSearchQuery(e.target.value)}
7639
+ onChange={(e) => updateSearchQuery(e.target.value)}
7611
7640
  className="w-full h-[42px] px-3 text-base text-semantic-text-primary border border-solid border-semantic-border-input rounded bg-semantic-bg-primary placeholder:text-semantic-text-placeholder focus:outline-none focus:border-semantic-border-input-focus/50"
7612
7641
  onClick={(e) => e.stopPropagation()}
7613
7642
  />
@@ -9580,11 +9609,32 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
9580
9609
  [disabled]
9581
9610
  );
9582
9611
 
9612
+ /**
9613
+ * The pending focus frame has to be cancellable. Opening the menu schedules
9614
+ * a frame that refocuses the input, and the input's \`onFocus\` reopens the
9615
+ * menu. If that frame lands AFTER an option has been picked, it reopens the
9616
+ * dropdown that \`selectOption\` just closed \u2014 so selecting an option looks
9617
+ * like it does nothing. Whether the frame lands before or after the click is
9618
+ * pure timing, which is why the bug comes and goes.
9619
+ */
9620
+ const focusFrameRef = React.useRef<number | null>(null);
9621
+
9622
+ const cancelPendingFocus = React.useCallback(() => {
9623
+ if (focusFrameRef.current === null) return;
9624
+ window.cancelAnimationFrame(focusFrameRef.current);
9625
+ focusFrameRef.current = null;
9626
+ }, []);
9627
+
9583
9628
  const focusSearchInput = React.useCallback(() => {
9584
- window.requestAnimationFrame(() => {
9629
+ cancelPendingFocus();
9630
+ focusFrameRef.current = window.requestAnimationFrame(() => {
9631
+ focusFrameRef.current = null;
9585
9632
  searchInputRef.current?.focus();
9586
9633
  });
9587
- }, []);
9634
+ }, [cancelPendingFocus]);
9635
+
9636
+ // Never let a queued frame fire into an unmounted component.
9637
+ React.useEffect(() => cancelPendingFocus, [cancelPendingFocus]);
9588
9638
 
9589
9639
  const setRootRef = React.useCallback(
9590
9640
  (node: HTMLDivElement | null) => {
@@ -9663,9 +9713,12 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
9663
9713
  onValueChange?.(option.value);
9664
9714
  onOptionSelect?.(option);
9665
9715
  onSearchChange?.(option.label);
9716
+ // Drop any queued refocus first, or it reopens what we are closing.
9717
+ cancelPendingFocus();
9666
9718
  setOpen(false);
9667
9719
  },
9668
9720
  [
9721
+ cancelPendingFocus,
9669
9722
  disabled,
9670
9723
  onSearchChange,
9671
9724
  onOptionSelect,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.376",
3
+ "version": "0.2.377",
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",