myoperator-mcp 0.2.364 → 0.2.366

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 +141 -68
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -710,6 +710,7 @@ const badgeVariants = cva(
710
710
  variant: {
711
711
  // Status-based variants (existing)
712
712
  active: "bg-semantic-success-surface text-semantic-success-primary",
713
+ information: "bg-semantic-info-surface text-semantic-info-text",
713
714
  warning: "bg-semantic-warning-surface text-semantic-warning-primary",
714
715
  failed: "bg-semantic-error-surface text-semantic-error-primary",
715
716
  disabled: "bg-semantic-bg-ui text-semantic-text-muted",
@@ -740,6 +741,7 @@ const badgeVariants = cva(
740
741
  * @example
741
742
  * \`\`\`tsx
742
743
  * <Badge variant="active">Active</Badge>
744
+ * <Badge variant="information">Coming Soon</Badge>
743
745
  * <Badge variant="warning">Warning</Badge>
744
746
  * <Badge variant="failed">Failed</Badge>
745
747
  * <Badge variant="disabled">Disabled</Badge>
@@ -8292,6 +8294,8 @@ export interface SearchFilterProps
8292
8294
  searchPlaceholder?: string;
8293
8295
  /** Search input behavior. Defaults to "numeric" for phone-number filtering. */
8294
8296
  searchMode?: SearchFilterSearchMode;
8297
+ /** Minimum query length before internal option filtering runs. Below it, all options are shown. Defaults to 0. */
8298
+ minSearchLength?: number;
8295
8299
  /** Message shown when filtering returns no options */
8296
8300
  emptyMessage?: string;
8297
8301
  /** Disables the whole filter */
@@ -8319,6 +8323,7 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
8319
8323
  onSearchChange,
8320
8324
  searchPlaceholder = "Search...",
8321
8325
  searchMode = "numeric",
8326
+ minSearchLength = 0,
8322
8327
  emptyMessage = "No options found",
8323
8328
  disabled = false,
8324
8329
  searchDisabled = false,
@@ -8455,6 +8460,7 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
8455
8460
  }, [isOpen, refs.floating, setOpen]);
8456
8461
 
8457
8462
  const filteredOptions = React.useMemo(() => {
8463
+ if (normalizedSearchQuery.length < minSearchLength) return options;
8458
8464
  if (!normalizedSearchQuery) return options;
8459
8465
 
8460
8466
  return options.filter((option) =>
@@ -8462,7 +8468,7 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
8462
8468
  normalizedSearchQuery
8463
8469
  )
8464
8470
  );
8465
- }, [normalizedSearchQuery, options, searchMode]);
8471
+ }, [normalizedSearchQuery, options, searchMode, minSearchLength]);
8466
8472
 
8467
8473
  const selectOption = React.useCallback(
8468
8474
  (option: SearchFilterOption) => {
@@ -8670,6 +8676,7 @@ export { SearchFilter, searchFilterVariants };
8670
8676
  import { Loader2, Search } from "lucide-react";
8671
8677
 
8672
8678
  import { cn } from "@/lib/utils";
8679
+ import { Input } from "./input";
8673
8680
  import {
8674
8681
  Select,
8675
8682
  SelectContent,
@@ -8728,6 +8735,12 @@ export interface SelectFieldProps {
8728
8735
  searchable?: boolean;
8729
8736
  /** Search placeholder text */
8730
8737
  searchPlaceholder?: string;
8738
+ /**
8739
+ * Render a divider (top border) above each option group and display the
8740
+ * group labels in uppercase with letter-spacing \u2014 matches the Figma
8741
+ * "routing" dropdown style. Only affects grouped options.
8742
+ */
8743
+ separateGroups?: boolean;
8731
8744
  /**
8732
8745
  * Controlled search value. When provided, internal search state is
8733
8746
  * ignored and the consumer owns the value \u2014 typically used to drive
@@ -8849,6 +8862,7 @@ const SelectField = React.forwardRef(
8849
8862
  options,
8850
8863
  searchable,
8851
8864
  searchPlaceholder = "Search...",
8865
+ separateGroups,
8852
8866
  searchValue,
8853
8867
  onSearchChange,
8854
8868
  wrapperClassName,
@@ -8869,6 +8883,17 @@ const SelectField = React.forwardRef(
8869
8883
  const isSearchControlled = searchValue !== undefined;
8870
8884
  const effectiveSearchQuery = isSearchControlled ? searchValue : searchQuery;
8871
8885
 
8886
+ // Track the current selection ourselves so we always know which option is
8887
+ // selected \u2014 even in uncontrolled mode. This lets us keep the selected
8888
+ // option mounted while the search filter hides everything else (see the
8889
+ // filter below), which prevents Radix from blanking the trigger value and
8890
+ // stealing focus off the search input when the selected item unmounts.
8891
+ const isValueControlled = value !== undefined;
8892
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(
8893
+ defaultValue ?? ""
8894
+ );
8895
+ const selectedValue = isValueControlled ? value : uncontrolledValue;
8896
+
8872
8897
  // Combined value change handler that also fires onSelect with full option object.
8873
8898
  // When interceptValue returns false, onValueChange is skipped (only onSelect fires).
8874
8899
  const handleValueChange = React.useCallback(
@@ -8877,6 +8902,9 @@ const SelectField = React.forwardRef(
8877
8902
 
8878
8903
  if (!intercepted) {
8879
8904
  onValueChange?.(newValue);
8905
+ if (!isValueControlled) {
8906
+ setUncontrolledValue(newValue);
8907
+ }
8880
8908
  }
8881
8909
 
8882
8910
  if (onSelect) {
@@ -8886,7 +8914,7 @@ const SelectField = React.forwardRef(
8886
8914
  }
8887
8915
  }
8888
8916
  },
8889
- [onValueChange, onSelect, interceptValue, options]
8917
+ [onValueChange, onSelect, interceptValue, options, isValueControlled]
8890
8918
  );
8891
8919
 
8892
8920
  // Support re-selection: fire onSelect when clicking the already-selected
@@ -8895,11 +8923,11 @@ const SelectField = React.forwardRef(
8895
8923
  // no-op.
8896
8924
  const handleItemClick = React.useCallback(
8897
8925
  (option: SelectOption) => {
8898
- if (option.value === value) {
8926
+ if (option.value === selectedValue) {
8899
8927
  handleValueChange(option.value);
8900
8928
  }
8901
8929
  },
8902
- [value, handleValueChange]
8930
+ [selectedValue, handleValueChange]
8903
8931
  );
8904
8932
 
8905
8933
  // Derive state from props
@@ -8914,46 +8942,55 @@ const SelectField = React.forwardRef(
8914
8942
  // Determine aria-describedby
8915
8943
  const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined;
8916
8944
 
8917
- // Group options by group property
8945
+ // Group options by group property.
8946
+ //
8947
+ // When client-side filtering is active, non-matching options are hidden
8948
+ // (rendered with \`display:none\`) rather than removed from the tree. The
8949
+ // one exception we *must* keep mounted is the currently-selected option:
8950
+ // if Radix's selected item unmounts, the trigger loses its displayed value
8951
+ // and focus is pulled off the search input. \`matchCount\` tracks how many
8952
+ // options are actually visible so the "no results" / footer states stay
8953
+ // accurate.
8918
8954
  const groupedOptions = React.useMemo(() => {
8919
- const groups: Record<string, SelectOption[]> = {};
8920
- const ungrouped: SelectOption[] = [];
8955
+ const groups: Record<string, (SelectOption & { hidden?: boolean })[]> =
8956
+ {};
8957
+ const ungrouped: (SelectOption & { hidden?: boolean })[] = [];
8958
+ let matchCount = 0;
8959
+
8960
+ const filtering =
8961
+ !!searchable && !isSearchControlled && !!searchQuery;
8962
+ const query = searchQuery.toLowerCase();
8921
8963
 
8922
8964
  options.forEach((option) => {
8923
- // Client-side filter only in uncontrolled mode. In controlled mode
8924
- // the consumer has already filtered server-side; re-filtering here
8925
- // would mask items their API returned.
8926
- if (
8927
- searchable &&
8928
- !isSearchControlled &&
8929
- searchQuery &&
8930
- !option.label.toLowerCase().includes(searchQuery.toLowerCase())
8931
- ) {
8965
+ const isMatch =
8966
+ !filtering || option.label.toLowerCase().includes(query);
8967
+ const isSelected = option.value === selectedValue && !!selectedValue;
8968
+
8969
+ // Drop only options that neither match the query nor are selected.
8970
+ if (!isMatch && !isSelected) {
8932
8971
  return;
8933
8972
  }
8934
8973
 
8974
+ if (isMatch) matchCount++;
8975
+ const entry = { ...option, hidden: !isMatch };
8976
+
8935
8977
  if (option.group) {
8936
8978
  if (!groups[option.group]) {
8937
8979
  groups[option.group] = [];
8938
8980
  }
8939
- groups[option.group].push(option);
8981
+ groups[option.group].push(entry);
8940
8982
  } else {
8941
- ungrouped.push(option);
8983
+ ungrouped.push(entry);
8942
8984
  }
8943
8985
  });
8944
8986
 
8945
- return { groups, ungrouped };
8946
- }, [options, searchable, isSearchControlled, searchQuery]);
8987
+ return { groups, ungrouped, matchCount };
8988
+ }, [options, searchable, isSearchControlled, searchQuery, selectedValue]);
8947
8989
 
8948
8990
  const hasGroups = Object.keys(groupedOptions.groups).length > 0;
8949
8991
 
8950
- // Count rendered options for the "End of list" footer (only show when at least one is visible).
8951
- const totalRendered =
8952
- groupedOptions.ungrouped.length +
8953
- Object.values(groupedOptions.groups).reduce(
8954
- (sum, items) => sum + items.length,
8955
- 0
8956
- );
8992
+ // Number of *visible* options \u2014 drives the empty-state and footer rows.
8993
+ const totalRendered = groupedOptions.matchCount;
8957
8994
  const showEndOfList = hasMore === false && totalRendered > 0 && !loadingMore;
8958
8995
 
8959
8996
  // Handle search input change
@@ -9009,7 +9046,14 @@ const SelectField = React.forwardRef(
9009
9046
  ref={ref}
9010
9047
  id={selectId}
9011
9048
  state={derivedState}
9012
- className={cn(loading && "pr-10", triggerClassName)}
9049
+ className={cn(
9050
+ loading && "pr-10",
9051
+ // Figma "routing" style uses a darker (text-muted) placeholder
9052
+ // instead of the default lighter placeholder token.
9053
+ separateGroups &&
9054
+ "[&>span[data-placeholder]]:text-semantic-text-muted",
9055
+ triggerClassName
9056
+ )}
9013
9057
  aria-invalid={!!error}
9014
9058
  aria-describedby={ariaDescribedBy}
9015
9059
  >
@@ -9024,28 +9068,45 @@ const SelectField = React.forwardRef(
9024
9068
  className={contentClassName}
9025
9069
  >
9026
9070
  {/* Search input */}
9027
- {searchable && (
9028
- <div className="flex items-center gap-2 px-3 pb-1.5 border-b border-solid border-semantic-border-layout">
9029
- <Search className="size-4 text-semantic-text-muted shrink-0" />
9030
- <input
9031
- type="text"
9032
- placeholder={searchPlaceholder}
9033
- value={effectiveSearchQuery}
9034
- onChange={handleSearchChange}
9035
- className="w-full h-[42px] text-base text-semantic-text-primary bg-transparent placeholder:text-semantic-text-placeholder focus:outline-none"
9036
- // Prevent closing dropdown when clicking input
9037
- onClick={(e) => e.stopPropagation()}
9038
- onKeyDown={(e) => e.stopPropagation()}
9039
- />
9040
- </div>
9041
- )}
9071
+ {searchable &&
9072
+ (separateGroups ? (
9073
+ // Figma "routing" style \u2014 a bordered, rounded search box that
9074
+ // reuses the shared Input component (no leading icon).
9075
+ <div className="px-1 pb-1.5">
9076
+ <Input
9077
+ type="text"
9078
+ placeholder={searchPlaceholder}
9079
+ value={effectiveSearchQuery}
9080
+ onChange={handleSearchChange}
9081
+ // Prevent closing dropdown when clicking input
9082
+ onClick={(e) => e.stopPropagation()}
9083
+ onKeyDown={(e) => e.stopPropagation()}
9084
+ />
9085
+ </div>
9086
+ ) : (
9087
+ <div className="flex items-center gap-2 px-3 pb-1.5 border-b border-solid border-semantic-border-layout">
9088
+ <Search className="size-4 text-semantic-text-muted shrink-0" />
9089
+ <input
9090
+ type="text"
9091
+ placeholder={searchPlaceholder}
9092
+ value={effectiveSearchQuery}
9093
+ onChange={handleSearchChange}
9094
+ className="w-full h-[42px] text-base text-semantic-text-primary bg-transparent placeholder:text-semantic-text-placeholder focus:outline-none"
9095
+ // Prevent closing dropdown when clicking input
9096
+ onClick={(e) => e.stopPropagation()}
9097
+ onKeyDown={(e) => e.stopPropagation()}
9098
+ />
9099
+ </div>
9100
+ ))}
9042
9101
 
9043
- {/* Ungrouped options */}
9102
+ {/* Ungrouped options. Non-matching options stay mounted but hidden
9103
+ (only the selected option is ever kept while filtering). */}
9044
9104
  {groupedOptions.ungrouped.map((option) => (
9045
9105
  <SelectItem
9046
9106
  key={option.value}
9047
9107
  value={option.value}
9048
9108
  disabled={option.disabled}
9109
+ className={cn(option.hidden && "hidden")}
9049
9110
  onPointerUp={() => handleItemClick(option)}
9050
9111
  >
9051
9112
  {option.label}
@@ -9055,32 +9116,44 @@ const SelectField = React.forwardRef(
9055
9116
  {/* Grouped options */}
9056
9117
  {hasGroups &&
9057
9118
  Object.entries(groupedOptions.groups).map(
9058
- ([groupName, groupOptions]) => (
9059
- <SelectGroup key={groupName}>
9060
- <SelectLabel>{groupName}</SelectLabel>
9061
- {groupOptions.map((option) => (
9062
- <SelectItem
9063
- key={option.value}
9064
- value={option.value}
9065
- disabled={option.disabled}
9066
- onPointerUp={() => handleItemClick(option)}
9067
- >
9068
- {option.label}
9069
- </SelectItem>
9070
- ))}
9071
- </SelectGroup>
9072
- )
9119
+ ([groupName, groupOptions]) => {
9120
+ // Hide the group label when every option in it is filtered
9121
+ // out (i.e. only a hidden selected item remains).
9122
+ const hasVisible = groupOptions.some((o) => !o.hidden);
9123
+ return (
9124
+ <SelectGroup key={groupName}>
9125
+ {hasVisible && (
9126
+ <SelectLabel
9127
+ className={cn(
9128
+ separateGroups &&
9129
+ "mt-1 border-t border-solid border-semantic-border-layout pt-2.5 uppercase tracking-[0.5px]"
9130
+ )}
9131
+ >
9132
+ {groupName}
9133
+ </SelectLabel>
9134
+ )}
9135
+ {groupOptions.map((option) => (
9136
+ <SelectItem
9137
+ key={option.value}
9138
+ value={option.value}
9139
+ disabled={option.disabled}
9140
+ className={cn(option.hidden && "hidden")}
9141
+ onPointerUp={() => handleItemClick(option)}
9142
+ >
9143
+ {option.label}
9144
+ </SelectItem>
9145
+ ))}
9146
+ </SelectGroup>
9147
+ );
9148
+ }
9073
9149
  )}
9074
9150
 
9075
- {/* No results message */}
9076
- {searchable &&
9077
- effectiveSearchQuery &&
9078
- groupedOptions.ungrouped.length === 0 &&
9079
- Object.keys(groupedOptions.groups).length === 0 && (
9080
- <div className="py-6 text-center text-sm text-semantic-text-muted">
9081
- No results found
9082
- </div>
9083
- )}
9151
+ {/* No results message \u2014 based on the count of *visible* options. */}
9152
+ {searchable && effectiveSearchQuery && totalRendered === 0 && (
9153
+ <div className="py-6 text-center text-sm text-semantic-text-muted">
9154
+ No results found
9155
+ </div>
9156
+ )}
9084
9157
 
9085
9158
  {/* Loading-more row (lazy-load) */}
9086
9159
  {loadingMore && (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.364",
3
+ "version": "0.2.366",
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",