myoperator-mcp 0.2.376 → 0.2.378

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 +153 -13
  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
  />
@@ -8489,6 +8518,49 @@ function PaginationEllipsis({
8489
8518
  }
8490
8519
  PaginationEllipsis.displayName = "PaginationEllipsis";
8491
8520
 
8521
+ export interface PaginationInfoProps extends React.ComponentProps<"p"> {
8522
+ /** Current page (1-based) */
8523
+ currentPage: number;
8524
+ /** Number of items shown per page */
8525
+ pageSize: number;
8526
+ /** Total number of items across all pages */
8527
+ totalItems: number;
8528
+ /** Leading label before the range (default: "Showing") */
8529
+ label?: string;
8530
+ /** Additional CSS classes */
8531
+ className?: string;
8532
+ }
8533
+
8534
+ function PaginationInfo({
8535
+ currentPage,
8536
+ pageSize,
8537
+ totalItems,
8538
+ label = "Showing",
8539
+ className,
8540
+ ...props
8541
+ }: PaginationInfoProps) {
8542
+ const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1;
8543
+ const endItem = Math.min(currentPage * pageSize, totalItems);
8544
+
8545
+ return (
8546
+ <p
8547
+ data-slot="pagination-info"
8548
+ aria-live="polite"
8549
+ className={cn(
8550
+ "m-0 text-sm text-semantic-text-muted whitespace-nowrap",
8551
+ className
8552
+ )}
8553
+ {...props}
8554
+ >
8555
+ {label}{" "}
8556
+ <span className="font-medium text-semantic-text-primary">
8557
+ {startItem}\u2013{endItem} of {totalItems}
8558
+ </span>
8559
+ </p>
8560
+ );
8561
+ }
8562
+ PaginationInfo.displayName = "PaginationInfo";
8563
+
8492
8564
  export interface PaginationWidgetProps {
8493
8565
  /** Current page (1-based) */
8494
8566
  currentPage: number;
@@ -8498,7 +8570,13 @@ export interface PaginationWidgetProps {
8498
8570
  onPageChange: (page: number) => void;
8499
8571
  /** Number of pages shown on each side of current page (default: 1) */
8500
8572
  siblingCount?: number;
8501
- /** Additional CSS classes */
8573
+ /** Total number of items \u2014 required to render the "Showing X-Y of Z" summary */
8574
+ totalItems?: number;
8575
+ /** Items per page \u2014 required to render the "Showing X-Y of Z" summary */
8576
+ pageSize?: number;
8577
+ /** Horizontal placement of the page controls (default: "center", or "end" when the summary is shown) */
8578
+ align?: "start" | "center" | "end";
8579
+ /** Additional CSS classes for the outer wrapper */
8502
8580
  className?: string;
8503
8581
  }
8504
8582
 
@@ -8540,17 +8618,35 @@ function usePaginationRange(
8540
8618
  return pages;
8541
8619
  }
8542
8620
 
8621
+ const paginationAlignClasses = {
8622
+ start: "justify-start",
8623
+ center: "justify-center",
8624
+ end: "justify-end",
8625
+ } as const;
8626
+
8543
8627
  function PaginationWidget({
8544
8628
  currentPage,
8545
8629
  totalPages,
8546
8630
  onPageChange,
8547
8631
  siblingCount = 1,
8632
+ totalItems,
8633
+ pageSize,
8634
+ align,
8548
8635
  className,
8549
8636
  }: PaginationWidgetProps) {
8550
8637
  const pages = usePaginationRange(currentPage, totalPages, siblingCount);
8638
+ const showInfo = totalItems !== undefined && pageSize !== undefined;
8639
+ const resolvedAlign = align ?? (showInfo ? "end" : "center");
8551
8640
 
8552
- return (
8553
- <Pagination className={className}>
8641
+ const controls = (
8642
+ <Pagination
8643
+ className={cn(
8644
+ "mx-0 w-auto",
8645
+ paginationAlignClasses[resolvedAlign],
8646
+ !showInfo && "w-full",
8647
+ !showInfo && className
8648
+ )}
8649
+ >
8554
8650
  <PaginationContent>
8555
8651
  <PaginationItem>
8556
8652
  <PaginationPrevious
@@ -8595,6 +8691,25 @@ function PaginationWidget({
8595
8691
  </PaginationContent>
8596
8692
  </Pagination>
8597
8693
  );
8694
+
8695
+ if (!showInfo) return controls;
8696
+
8697
+ return (
8698
+ <div
8699
+ data-slot="pagination-widget"
8700
+ className={cn(
8701
+ "flex w-full flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between",
8702
+ className
8703
+ )}
8704
+ >
8705
+ <PaginationInfo
8706
+ currentPage={currentPage}
8707
+ pageSize={pageSize}
8708
+ totalItems={totalItems}
8709
+ />
8710
+ {controls}
8711
+ </div>
8712
+ );
8598
8713
  }
8599
8714
  PaginationWidget.displayName = "PaginationWidget";
8600
8715
 
@@ -8606,6 +8721,7 @@ export {
8606
8721
  PaginationPrevious,
8607
8722
  PaginationNext,
8608
8723
  PaginationEllipsis,
8724
+ PaginationInfo,
8609
8725
  PaginationWidget,
8610
8726
  };
8611
8727
  `,
@@ -9580,11 +9696,32 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
9580
9696
  [disabled]
9581
9697
  );
9582
9698
 
9699
+ /**
9700
+ * The pending focus frame has to be cancellable. Opening the menu schedules
9701
+ * a frame that refocuses the input, and the input's \`onFocus\` reopens the
9702
+ * menu. If that frame lands AFTER an option has been picked, it reopens the
9703
+ * dropdown that \`selectOption\` just closed \u2014 so selecting an option looks
9704
+ * like it does nothing. Whether the frame lands before or after the click is
9705
+ * pure timing, which is why the bug comes and goes.
9706
+ */
9707
+ const focusFrameRef = React.useRef<number | null>(null);
9708
+
9709
+ const cancelPendingFocus = React.useCallback(() => {
9710
+ if (focusFrameRef.current === null) return;
9711
+ window.cancelAnimationFrame(focusFrameRef.current);
9712
+ focusFrameRef.current = null;
9713
+ }, []);
9714
+
9583
9715
  const focusSearchInput = React.useCallback(() => {
9584
- window.requestAnimationFrame(() => {
9716
+ cancelPendingFocus();
9717
+ focusFrameRef.current = window.requestAnimationFrame(() => {
9718
+ focusFrameRef.current = null;
9585
9719
  searchInputRef.current?.focus();
9586
9720
  });
9587
- }, []);
9721
+ }, [cancelPendingFocus]);
9722
+
9723
+ // Never let a queued frame fire into an unmounted component.
9724
+ React.useEffect(() => cancelPendingFocus, [cancelPendingFocus]);
9588
9725
 
9589
9726
  const setRootRef = React.useCallback(
9590
9727
  (node: HTMLDivElement | null) => {
@@ -9663,9 +9800,12 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
9663
9800
  onValueChange?.(option.value);
9664
9801
  onOptionSelect?.(option);
9665
9802
  onSearchChange?.(option.label);
9803
+ // Drop any queued refocus first, or it reopens what we are closing.
9804
+ cancelPendingFocus();
9666
9805
  setOpen(false);
9667
9806
  },
9668
9807
  [
9808
+ cancelPendingFocus,
9669
9809
  disabled,
9670
9810
  onSearchChange,
9671
9811
  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.378",
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",