myoperator-mcp 0.2.363 → 0.2.365

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 +136 -67
  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>
@@ -8670,6 +8672,7 @@ export { SearchFilter, searchFilterVariants };
8670
8672
  import { Loader2, Search } from "lucide-react";
8671
8673
 
8672
8674
  import { cn } from "@/lib/utils";
8675
+ import { Input } from "./input";
8673
8676
  import {
8674
8677
  Select,
8675
8678
  SelectContent,
@@ -8728,6 +8731,12 @@ export interface SelectFieldProps {
8728
8731
  searchable?: boolean;
8729
8732
  /** Search placeholder text */
8730
8733
  searchPlaceholder?: string;
8734
+ /**
8735
+ * Render a divider (top border) above each option group and display the
8736
+ * group labels in uppercase with letter-spacing \u2014 matches the Figma
8737
+ * "routing" dropdown style. Only affects grouped options.
8738
+ */
8739
+ separateGroups?: boolean;
8731
8740
  /**
8732
8741
  * Controlled search value. When provided, internal search state is
8733
8742
  * ignored and the consumer owns the value \u2014 typically used to drive
@@ -8849,6 +8858,7 @@ const SelectField = React.forwardRef(
8849
8858
  options,
8850
8859
  searchable,
8851
8860
  searchPlaceholder = "Search...",
8861
+ separateGroups,
8852
8862
  searchValue,
8853
8863
  onSearchChange,
8854
8864
  wrapperClassName,
@@ -8869,6 +8879,17 @@ const SelectField = React.forwardRef(
8869
8879
  const isSearchControlled = searchValue !== undefined;
8870
8880
  const effectiveSearchQuery = isSearchControlled ? searchValue : searchQuery;
8871
8881
 
8882
+ // Track the current selection ourselves so we always know which option is
8883
+ // selected \u2014 even in uncontrolled mode. This lets us keep the selected
8884
+ // option mounted while the search filter hides everything else (see the
8885
+ // filter below), which prevents Radix from blanking the trigger value and
8886
+ // stealing focus off the search input when the selected item unmounts.
8887
+ const isValueControlled = value !== undefined;
8888
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(
8889
+ defaultValue ?? ""
8890
+ );
8891
+ const selectedValue = isValueControlled ? value : uncontrolledValue;
8892
+
8872
8893
  // Combined value change handler that also fires onSelect with full option object.
8873
8894
  // When interceptValue returns false, onValueChange is skipped (only onSelect fires).
8874
8895
  const handleValueChange = React.useCallback(
@@ -8877,6 +8898,9 @@ const SelectField = React.forwardRef(
8877
8898
 
8878
8899
  if (!intercepted) {
8879
8900
  onValueChange?.(newValue);
8901
+ if (!isValueControlled) {
8902
+ setUncontrolledValue(newValue);
8903
+ }
8880
8904
  }
8881
8905
 
8882
8906
  if (onSelect) {
@@ -8886,7 +8910,7 @@ const SelectField = React.forwardRef(
8886
8910
  }
8887
8911
  }
8888
8912
  },
8889
- [onValueChange, onSelect, interceptValue, options]
8913
+ [onValueChange, onSelect, interceptValue, options, isValueControlled]
8890
8914
  );
8891
8915
 
8892
8916
  // Support re-selection: fire onSelect when clicking the already-selected
@@ -8895,11 +8919,11 @@ const SelectField = React.forwardRef(
8895
8919
  // no-op.
8896
8920
  const handleItemClick = React.useCallback(
8897
8921
  (option: SelectOption) => {
8898
- if (option.value === value) {
8922
+ if (option.value === selectedValue) {
8899
8923
  handleValueChange(option.value);
8900
8924
  }
8901
8925
  },
8902
- [value, handleValueChange]
8926
+ [selectedValue, handleValueChange]
8903
8927
  );
8904
8928
 
8905
8929
  // Derive state from props
@@ -8914,46 +8938,55 @@ const SelectField = React.forwardRef(
8914
8938
  // Determine aria-describedby
8915
8939
  const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined;
8916
8940
 
8917
- // Group options by group property
8941
+ // Group options by group property.
8942
+ //
8943
+ // When client-side filtering is active, non-matching options are hidden
8944
+ // (rendered with \`display:none\`) rather than removed from the tree. The
8945
+ // one exception we *must* keep mounted is the currently-selected option:
8946
+ // if Radix's selected item unmounts, the trigger loses its displayed value
8947
+ // and focus is pulled off the search input. \`matchCount\` tracks how many
8948
+ // options are actually visible so the "no results" / footer states stay
8949
+ // accurate.
8918
8950
  const groupedOptions = React.useMemo(() => {
8919
- const groups: Record<string, SelectOption[]> = {};
8920
- const ungrouped: SelectOption[] = [];
8951
+ const groups: Record<string, (SelectOption & { hidden?: boolean })[]> =
8952
+ {};
8953
+ const ungrouped: (SelectOption & { hidden?: boolean })[] = [];
8954
+ let matchCount = 0;
8955
+
8956
+ const filtering =
8957
+ !!searchable && !isSearchControlled && !!searchQuery;
8958
+ const query = searchQuery.toLowerCase();
8921
8959
 
8922
8960
  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
- ) {
8961
+ const isMatch =
8962
+ !filtering || option.label.toLowerCase().includes(query);
8963
+ const isSelected = option.value === selectedValue && !!selectedValue;
8964
+
8965
+ // Drop only options that neither match the query nor are selected.
8966
+ if (!isMatch && !isSelected) {
8932
8967
  return;
8933
8968
  }
8934
8969
 
8970
+ if (isMatch) matchCount++;
8971
+ const entry = { ...option, hidden: !isMatch };
8972
+
8935
8973
  if (option.group) {
8936
8974
  if (!groups[option.group]) {
8937
8975
  groups[option.group] = [];
8938
8976
  }
8939
- groups[option.group].push(option);
8977
+ groups[option.group].push(entry);
8940
8978
  } else {
8941
- ungrouped.push(option);
8979
+ ungrouped.push(entry);
8942
8980
  }
8943
8981
  });
8944
8982
 
8945
- return { groups, ungrouped };
8946
- }, [options, searchable, isSearchControlled, searchQuery]);
8983
+ return { groups, ungrouped, matchCount };
8984
+ }, [options, searchable, isSearchControlled, searchQuery, selectedValue]);
8947
8985
 
8948
8986
  const hasGroups = Object.keys(groupedOptions.groups).length > 0;
8949
8987
 
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
- );
8988
+ // Number of *visible* options \u2014 drives the empty-state and footer rows.
8989
+ const totalRendered = groupedOptions.matchCount;
8957
8990
  const showEndOfList = hasMore === false && totalRendered > 0 && !loadingMore;
8958
8991
 
8959
8992
  // Handle search input change
@@ -9009,7 +9042,14 @@ const SelectField = React.forwardRef(
9009
9042
  ref={ref}
9010
9043
  id={selectId}
9011
9044
  state={derivedState}
9012
- className={cn(loading && "pr-10", triggerClassName)}
9045
+ className={cn(
9046
+ loading && "pr-10",
9047
+ // Figma "routing" style uses a darker (text-muted) placeholder
9048
+ // instead of the default lighter placeholder token.
9049
+ separateGroups &&
9050
+ "[&>span[data-placeholder]]:text-semantic-text-muted",
9051
+ triggerClassName
9052
+ )}
9013
9053
  aria-invalid={!!error}
9014
9054
  aria-describedby={ariaDescribedBy}
9015
9055
  >
@@ -9024,28 +9064,45 @@ const SelectField = React.forwardRef(
9024
9064
  className={contentClassName}
9025
9065
  >
9026
9066
  {/* 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
- )}
9067
+ {searchable &&
9068
+ (separateGroups ? (
9069
+ // Figma "routing" style \u2014 a bordered, rounded search box that
9070
+ // reuses the shared Input component (no leading icon).
9071
+ <div className="px-1 pb-1.5">
9072
+ <Input
9073
+ type="text"
9074
+ placeholder={searchPlaceholder}
9075
+ value={effectiveSearchQuery}
9076
+ onChange={handleSearchChange}
9077
+ // Prevent closing dropdown when clicking input
9078
+ onClick={(e) => e.stopPropagation()}
9079
+ onKeyDown={(e) => e.stopPropagation()}
9080
+ />
9081
+ </div>
9082
+ ) : (
9083
+ <div className="flex items-center gap-2 px-3 pb-1.5 border-b border-solid border-semantic-border-layout">
9084
+ <Search className="size-4 text-semantic-text-muted shrink-0" />
9085
+ <input
9086
+ type="text"
9087
+ placeholder={searchPlaceholder}
9088
+ value={effectiveSearchQuery}
9089
+ onChange={handleSearchChange}
9090
+ className="w-full h-[42px] text-base text-semantic-text-primary bg-transparent placeholder:text-semantic-text-placeholder focus:outline-none"
9091
+ // Prevent closing dropdown when clicking input
9092
+ onClick={(e) => e.stopPropagation()}
9093
+ onKeyDown={(e) => e.stopPropagation()}
9094
+ />
9095
+ </div>
9096
+ ))}
9042
9097
 
9043
- {/* Ungrouped options */}
9098
+ {/* Ungrouped options. Non-matching options stay mounted but hidden
9099
+ (only the selected option is ever kept while filtering). */}
9044
9100
  {groupedOptions.ungrouped.map((option) => (
9045
9101
  <SelectItem
9046
9102
  key={option.value}
9047
9103
  value={option.value}
9048
9104
  disabled={option.disabled}
9105
+ className={cn(option.hidden && "hidden")}
9049
9106
  onPointerUp={() => handleItemClick(option)}
9050
9107
  >
9051
9108
  {option.label}
@@ -9055,32 +9112,44 @@ const SelectField = React.forwardRef(
9055
9112
  {/* Grouped options */}
9056
9113
  {hasGroups &&
9057
9114
  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
- )
9115
+ ([groupName, groupOptions]) => {
9116
+ // Hide the group label when every option in it is filtered
9117
+ // out (i.e. only a hidden selected item remains).
9118
+ const hasVisible = groupOptions.some((o) => !o.hidden);
9119
+ return (
9120
+ <SelectGroup key={groupName}>
9121
+ {hasVisible && (
9122
+ <SelectLabel
9123
+ className={cn(
9124
+ separateGroups &&
9125
+ "mt-1 border-t border-solid border-semantic-border-layout pt-2.5 uppercase tracking-[0.5px]"
9126
+ )}
9127
+ >
9128
+ {groupName}
9129
+ </SelectLabel>
9130
+ )}
9131
+ {groupOptions.map((option) => (
9132
+ <SelectItem
9133
+ key={option.value}
9134
+ value={option.value}
9135
+ disabled={option.disabled}
9136
+ className={cn(option.hidden && "hidden")}
9137
+ onPointerUp={() => handleItemClick(option)}
9138
+ >
9139
+ {option.label}
9140
+ </SelectItem>
9141
+ ))}
9142
+ </SelectGroup>
9143
+ );
9144
+ }
9073
9145
  )}
9074
9146
 
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
- )}
9147
+ {/* No results message \u2014 based on the count of *visible* options. */}
9148
+ {searchable && effectiveSearchQuery && totalRendered === 0 && (
9149
+ <div className="py-6 text-center text-sm text-semantic-text-muted">
9150
+ No results found
9151
+ </div>
9152
+ )}
9084
9153
 
9085
9154
  {/* Loading-more row (lazy-load) */}
9086
9155
  {loadingMore && (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.363",
3
+ "version": "0.2.365",
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",