myoperator-mcp 0.2.290 → 0.2.292

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 +355 -130
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1450,88 +1450,154 @@ ContactListItem.displayName = "ContactListItem";
1450
1450
 
1451
1451
  export { ContactListItem };
1452
1452
  `,
1453
- "creatable-multi-select": `import * as React from "react"
1454
- import { ChevronDown, Plus, Info, X } from "lucide-react"
1453
+ "creatable-multi-select": `import * as React from "react";
1454
+ import { ChevronDown, Plus, Info, X } from "lucide-react";
1455
1455
 
1456
- import { cn } from "@/lib/utils"
1456
+ import { cn } from "@/lib/utils";
1457
1457
  import {
1458
1458
  creatableSelectTriggerVariants,
1459
1459
  creatableEnterHintKbdClassName,
1460
1460
  creatablePrimaryRoleHintRowClassName,
1461
- } from "./creatable-select"
1461
+ } from "./creatable-select";
1462
1462
 
1463
1463
  /** @deprecated Use \`creatableSelectTriggerVariants\` from \`./creatable-select\` \u2014 aliases the same trigger styles as Primary Role. */
1464
- const creatableMultiSelectTriggerVariants = creatableSelectTriggerVariants
1464
+ const creatableMultiSelectTriggerVariants = creatableSelectTriggerVariants;
1465
1465
 
1466
1466
  export interface CreatableMultiSelectOption {
1467
- value: string
1468
- label: string
1469
- disabled?: boolean
1467
+ value: string;
1468
+ label: string;
1469
+ disabled?: boolean;
1470
1470
  }
1471
1471
 
1472
- export interface CreatableMultiSelectProps
1473
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
1472
+ export interface CreatableMultiSelectProps extends Omit<
1473
+ React.HTMLAttributes<HTMLDivElement>,
1474
+ "onChange"
1475
+ > {
1474
1476
  /** Currently selected values */
1475
- value?: string[]
1477
+ value?: string[];
1476
1478
  /** Callback when values change */
1477
- onValueChange?: (values: string[]) => void
1479
+ onValueChange?: (values: string[]) => void;
1478
1480
  /** Available preset options */
1479
- options?: CreatableMultiSelectOption[]
1481
+ options?: CreatableMultiSelectOption[];
1480
1482
  /** Placeholder when no values selected */
1481
- placeholder?: string
1483
+ placeholder?: string;
1482
1484
  /** Whether the component is disabled */
1483
- disabled?: boolean
1485
+ disabled?: boolean;
1484
1486
  /** Error state */
1485
- state?: "default" | "error"
1487
+ state?: "default" | "error";
1486
1488
  /** Helper text shown below the trigger */
1487
- helperText?: string
1489
+ helperText?: string;
1488
1490
  /**
1489
1491
  * Shown inside the open dropdown (e.g. "Type to create a custom tone").
1490
1492
  * Pair with {@link maxItems} so users see guidance when no preset matches their typing.
1491
1493
  */
1492
- createHintText?: string
1494
+ createHintText?: string;
1493
1495
  /** Max number of items that can be selected (default: unlimited) */
1494
- maxItems?: number
1496
+ maxItems?: number;
1495
1497
  /** Max character length per item when typing/creating (default: unlimited) */
1496
- maxLengthPerItem?: number
1498
+ maxLengthPerItem?: number;
1497
1499
  /**
1498
1500
  * When true (default), shows \`current/max\` under the trigger while typing when \`maxLengthPerItem\` is set.
1499
1501
  * Set to false to match Figma (counter lives only in the field flow / not under the control).
1500
1502
  */
1501
- showPerItemCharacterCounter?: boolean
1503
+ showPerItemCharacterCounter?: boolean;
1502
1504
  /**
1503
1505
  * Closed trigger: show removable chips (default) or a single comma-separated summary line (Figma Tone).
1504
1506
  * While open, the trigger always shows the summary line; selected chips with remove controls appear in the panel.
1505
1507
  */
1506
- triggerDisplay?: "chips" | "summary"
1508
+ triggerDisplay?: "chips" | "summary";
1507
1509
  /**
1508
1510
  * When set, the text input is transformed (e.g. strip invalid characters).
1509
1511
  * If the raw value differs from the sanitized value, \`onInvalidCharacters\` is called.
1510
1512
  */
1511
- sanitizeInput?: (raw: string) => string
1513
+ sanitizeInput?: (raw: string) => string;
1512
1514
  /** Fired when \`sanitizeInput\` removed one or more characters from the raw input. */
1513
- onInvalidCharacters?: () => void
1515
+ onInvalidCharacters?: () => void;
1514
1516
  /**
1515
1517
  * When \`sanitizeInput\` is set, fired on input change if the raw value is already valid.
1516
1518
  * Use to clear validation errors when the user corrects input.
1517
1519
  */
1518
- onValidInput?: () => void
1520
+ onValidInput?: () => void;
1521
+ /** Fired with the current open-dropdown draft text so parents can validate while typing. */
1522
+ onInputValueChange?: (value: string) => void;
1519
1523
  }
1520
1524
 
1521
1525
  function joinSelectedLabels(
1522
1526
  values: string[],
1523
- options: CreatableMultiSelectOption[]
1527
+ options: CreatableMultiSelectOption[],
1528
+ sanitizeInput?: (raw: string) => string,
1529
+ maxLengthPerItem?: number
1524
1530
  ): string {
1525
1531
  return values
1526
- .map((val) => options.find((o) => o.value === val)?.label ?? val)
1527
- .join(", ")
1532
+ .map((val) => labelForValue(val, options, sanitizeInput, maxLengthPerItem))
1533
+ .join(", ");
1534
+ }
1535
+
1536
+ function storedValueCandidates(
1537
+ option: CreatableMultiSelectOption,
1538
+ sanitizeInput?: (raw: string) => string,
1539
+ maxLengthPerItem?: number
1540
+ ): string[] {
1541
+ const values = [option.value];
1542
+ if (sanitizeInput) values.push(sanitizeInput(option.value).trim());
1543
+ if (maxLengthPerItem != null) {
1544
+ values.push(option.value.slice(0, maxLengthPerItem));
1545
+ if (sanitizeInput) {
1546
+ values.push(
1547
+ sanitizeInput(option.value).trim().slice(0, maxLengthPerItem)
1548
+ );
1549
+ }
1550
+ }
1551
+ return Array.from(new Set(values.filter(Boolean)));
1528
1552
  }
1529
1553
 
1530
1554
  function labelForValue(
1531
1555
  val: string,
1532
- options: CreatableMultiSelectOption[]
1556
+ options: CreatableMultiSelectOption[],
1557
+ sanitizeInput?: (raw: string) => string,
1558
+ maxLengthPerItem?: number
1533
1559
  ): string {
1534
- return options.find((o) => o.value === val)?.label ?? val
1560
+ const direct = options.find((o) => o.value === val);
1561
+ if (direct) return direct.label;
1562
+ const byStoredForm = options.find((o) =>
1563
+ storedValueCandidates(o, sanitizeInput, maxLengthPerItem).includes(val)
1564
+ );
1565
+ if (byStoredForm) return byStoredForm.label;
1566
+ return val;
1567
+ }
1568
+
1569
+ /** Whether a preset option is already in the selection (including legacy stored forms). */
1570
+ function isOptionSelected(
1571
+ option: CreatableMultiSelectOption,
1572
+ selected: string[],
1573
+ sanitizeInput?: (raw: string) => string,
1574
+ maxLengthPerItem?: number
1575
+ ): boolean {
1576
+ return storedValueCandidates(option, sanitizeInput, maxLengthPerItem).some(
1577
+ (candidate) => selected.includes(candidate)
1578
+ );
1579
+ }
1580
+
1581
+ /** Whether a candidate value is already selected (matches raw or legacy preset forms). */
1582
+ function isValueAlreadySelected(
1583
+ candidate: string,
1584
+ selected: string[],
1585
+ options: CreatableMultiSelectOption[],
1586
+ sanitizeInput?: (raw: string) => string,
1587
+ maxLengthPerItem?: number
1588
+ ): boolean {
1589
+ if (selected.includes(candidate)) return true;
1590
+ return options.some((o) => {
1591
+ const candidates = storedValueCandidates(
1592
+ o,
1593
+ sanitizeInput,
1594
+ maxLengthPerItem
1595
+ );
1596
+ return (
1597
+ candidates.includes(candidate) &&
1598
+ candidates.some((stored) => selected.includes(stored))
1599
+ );
1600
+ });
1535
1601
  }
1536
1602
 
1537
1603
  const CreatableMultiSelect = React.forwardRef(
@@ -1553,63 +1619,96 @@ const CreatableMultiSelect = React.forwardRef(
1553
1619
  sanitizeInput,
1554
1620
  onInvalidCharacters,
1555
1621
  onValidInput,
1622
+ onInputValueChange,
1556
1623
  ...props
1557
1624
  }: CreatableMultiSelectProps,
1558
1625
  ref: React.Ref<HTMLDivElement>
1559
1626
  ) => {
1560
- const [isOpen, setIsOpen] = React.useState(false)
1561
- const [inputValue, setInputValue] = React.useState("")
1562
- const containerRef = React.useRef<HTMLDivElement>(null)
1563
- const inputRef = React.useRef<HTMLInputElement>(null)
1564
- const listboxId = React.useId()
1627
+ const [isOpen, setIsOpen] = React.useState(false);
1628
+ const [inputValue, setInputValue] = React.useState("");
1629
+ const containerRef = React.useRef<HTMLDivElement>(null);
1630
+ const inputRef = React.useRef<HTMLInputElement>(null);
1631
+ const listboxId = React.useId();
1565
1632
 
1566
- React.useImperativeHandle(ref, () => containerRef.current!)
1633
+ React.useImperativeHandle(ref, () => containerRef.current!);
1567
1634
 
1568
- const derivedState = state === "error" ? "error" : "default"
1635
+ const derivedState = state === "error" ? "error" : "default";
1569
1636
 
1570
- const selectedSummary = joinSelectedLabels(value, options)
1637
+ const selectedSummary = joinSelectedLabels(
1638
+ value,
1639
+ options,
1640
+ sanitizeInput,
1641
+ maxLengthPerItem
1642
+ );
1571
1643
 
1572
1644
  const addValue = (val: string) => {
1573
- const afterSanitize = sanitizeInput ? sanitizeInput(val) : val
1574
- const trimmed = afterSanitize.trim()
1575
- if (!trimmed || value.includes(trimmed)) return
1576
- if (maxItems != null && value.length >= maxItems) return
1645
+ const isPreset = options.some((o) => o.value === val);
1646
+ const afterSanitize = isPreset
1647
+ ? val
1648
+ : sanitizeInput
1649
+ ? sanitizeInput(val)
1650
+ : val;
1651
+ const trimmed = afterSanitize.trim();
1652
+ if (
1653
+ !trimmed ||
1654
+ isValueAlreadySelected(
1655
+ trimmed,
1656
+ value,
1657
+ options,
1658
+ sanitizeInput,
1659
+ maxLengthPerItem
1660
+ )
1661
+ ) {
1662
+ return;
1663
+ }
1664
+ if (maxItems != null && value.length >= maxItems) return;
1577
1665
  const toAdd =
1578
- maxLengthPerItem != null
1666
+ !isPreset && maxLengthPerItem != null
1579
1667
  ? trimmed.slice(0, maxLengthPerItem)
1580
- : trimmed
1668
+ : trimmed;
1581
1669
  if (toAdd) {
1582
- onValueChange?.([...value, toAdd])
1583
- setInputValue("")
1670
+ const nextValue = [...value, toAdd];
1671
+ onValueChange?.(nextValue);
1672
+ setInputValue("");
1673
+ onInputValueChange?.("");
1674
+ const reachedMax =
1675
+ maxItems != null && nextValue.length >= maxItems;
1676
+ if (reachedMax) {
1677
+ setIsOpen(false);
1678
+ } else {
1679
+ requestAnimationFrame(() => inputRef.current?.focus());
1680
+ }
1584
1681
  }
1585
- }
1682
+ };
1586
1683
 
1587
1684
  const removeValue = (val: string) => {
1588
- onValueChange?.(value.filter((v) => v !== val))
1589
- }
1685
+ onValueChange?.(value.filter((v) => v !== val));
1686
+ };
1590
1687
 
1591
1688
  const handleOpen = React.useCallback(() => {
1592
- if (disabled) return
1593
- setIsOpen(true)
1594
- setInputValue("")
1595
- }, [disabled])
1689
+ if (disabled) return;
1690
+ setIsOpen(true);
1691
+ setInputValue("");
1692
+ onInputValueChange?.("");
1693
+ }, [disabled, onInputValueChange]);
1596
1694
 
1597
1695
  React.useEffect(() => {
1598
- if (!isOpen) return
1599
- requestAnimationFrame(() => inputRef.current?.focus())
1600
- }, [isOpen])
1696
+ if (!isOpen) return;
1697
+ requestAnimationFrame(() => inputRef.current?.focus());
1698
+ }, [isOpen]);
1601
1699
 
1602
1700
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
1603
1701
  if (e.key === "Enter") {
1604
- e.preventDefault()
1605
- if (inputValue.trim()) addValue(inputValue)
1702
+ e.preventDefault();
1703
+ if (inputValue.trim()) addValue(inputValue);
1606
1704
  } else if (e.key === "Backspace" && !inputValue && value.length > 0) {
1607
- removeValue(value[value.length - 1])
1705
+ removeValue(value[value.length - 1]);
1608
1706
  } else if (e.key === "Escape") {
1609
- setIsOpen(false)
1610
- setInputValue("")
1707
+ setIsOpen(false);
1708
+ setInputValue("");
1709
+ onInputValueChange?.("");
1611
1710
  }
1612
- }
1711
+ };
1613
1712
 
1614
1713
  // Close on outside click
1615
1714
  React.useEffect(() => {
@@ -1618,34 +1717,45 @@ const CreatableMultiSelect = React.forwardRef(
1618
1717
  containerRef.current &&
1619
1718
  !containerRef.current.contains(e.target as Node)
1620
1719
  ) {
1621
- setIsOpen(false)
1622
- setInputValue("")
1720
+ setIsOpen(false);
1721
+ setInputValue("");
1722
+ onInputValueChange?.("");
1623
1723
  }
1624
- }
1625
- document.addEventListener("mousedown", handler)
1626
- return () => document.removeEventListener("mousedown", handler)
1627
- }, [])
1724
+ };
1725
+ document.addEventListener("mousedown", handler);
1726
+ return () => document.removeEventListener("mousedown", handler);
1727
+ }, [onInputValueChange]);
1628
1728
 
1629
1729
  const availablePresets = options.filter(
1630
- (o) => !value.includes(o.value) && !o.disabled
1631
- )
1632
- const trimmedInput = inputValue.trim()
1730
+ (o) =>
1731
+ !isOptionSelected(o, value, sanitizeInput, maxLengthPerItem) &&
1732
+ !o.disabled
1733
+ );
1734
+ const trimmedInput = inputValue.trim();
1633
1735
  const filteredPresets = trimmedInput
1634
1736
  ? availablePresets.filter((o) =>
1635
1737
  o.label.toLowerCase().includes(trimmedInput.toLowerCase())
1636
1738
  )
1637
- : availablePresets
1739
+ : availablePresets;
1638
1740
 
1639
1741
  const isCustomDraft =
1640
1742
  trimmedInput.length > 0 &&
1641
1743
  !options.some(
1642
1744
  (o) => o.label.toLowerCase() === trimmedInput.toLowerCase()
1643
1745
  ) &&
1644
- !value.includes(trimmedInput) &&
1645
- (maxItems == null || value.length < maxItems)
1746
+ !isValueAlreadySelected(
1747
+ trimmedInput,
1748
+ value,
1749
+ options,
1750
+ sanitizeInput,
1751
+ maxLengthPerItem
1752
+ ) &&
1753
+ (maxItems == null || value.length < maxItems);
1646
1754
 
1647
1755
  const summaryTriggerLabel =
1648
- value.length === 0 ? placeholder : selectedSummary
1756
+ value.length === 0 ? placeholder : selectedSummary;
1757
+
1758
+ const canAddMore = maxItems == null || value.length < maxItems;
1649
1759
 
1650
1760
  return (
1651
1761
  <div
@@ -1661,11 +1771,11 @@ const CreatableMultiSelect = React.forwardRef(
1661
1771
  "flex h-auto min-h-[42px] cursor-text items-start gap-2 py-2 text-left"
1662
1772
  )}
1663
1773
  onClick={(e) => {
1664
- if (disabled) return
1774
+ if (disabled) return;
1665
1775
  if ((e.target as HTMLElement).closest("[data-chip-remove]")) {
1666
- return
1776
+ return;
1667
1777
  }
1668
- inputRef.current?.focus()
1778
+ inputRef.current?.focus();
1669
1779
  }}
1670
1780
  >
1671
1781
  <div className="flex min-h-0 min-w-0 flex-1 flex-wrap content-start items-center gap-1.5">
@@ -1676,25 +1786,35 @@ const CreatableMultiSelect = React.forwardRef(
1676
1786
  className="inline-flex max-w-full items-center gap-0.5 rounded bg-semantic-bg-ui py-1 pl-2 pr-0.5 text-sm text-semantic-text-primary"
1677
1787
  >
1678
1788
  <span className="min-w-0 truncate">
1679
- {labelForValue(val, options)}
1789
+ {labelForValue(
1790
+ val,
1791
+ options,
1792
+ sanitizeInput,
1793
+ maxLengthPerItem
1794
+ )}
1680
1795
  </span>
1681
1796
  <button
1682
1797
  type="button"
1683
1798
  data-chip-remove
1684
1799
  disabled={disabled}
1685
- aria-label={\`Remove \${labelForValue(val, options)}\`}
1800
+ aria-label={\`Remove \${labelForValue(
1801
+ val,
1802
+ options,
1803
+ sanitizeInput,
1804
+ maxLengthPerItem
1805
+ )}\`}
1686
1806
  className={cn(
1687
1807
  "inline-flex size-6 shrink-0 items-center justify-center rounded text-semantic-text-muted transition-colors",
1688
1808
  !disabled &&
1689
1809
  "hover:bg-semantic-bg-hover hover:text-semantic-text-primary"
1690
1810
  )}
1691
1811
  onMouseDown={(e) => {
1692
- e.preventDefault()
1693
- e.stopPropagation()
1812
+ e.preventDefault();
1813
+ e.stopPropagation();
1694
1814
  }}
1695
1815
  onClick={(e) => {
1696
- e.stopPropagation()
1697
- if (!disabled) removeValue(val)
1816
+ e.stopPropagation();
1817
+ if (!disabled) removeValue(val);
1698
1818
  }}
1699
1819
  >
1700
1820
  <X className="size-3.5" strokeWidth={2} aria-hidden />
@@ -1711,17 +1831,18 @@ const CreatableMultiSelect = React.forwardRef(
1711
1831
  type="text"
1712
1832
  value={inputValue}
1713
1833
  onChange={(e) => {
1714
- const raw = e.target.value
1715
- const sanitized = sanitizeInput ? sanitizeInput(raw) : raw
1834
+ const raw = e.target.value;
1835
+ const sanitized = sanitizeInput ? sanitizeInput(raw) : raw;
1716
1836
  if (sanitizeInput) {
1717
- if (raw !== sanitized) onInvalidCharacters?.()
1718
- else onValidInput?.()
1837
+ if (raw !== sanitized) onInvalidCharacters?.();
1838
+ else onValidInput?.();
1719
1839
  }
1720
- setInputValue(
1840
+ const nextInput =
1721
1841
  maxLengthPerItem != null
1722
1842
  ? sanitized.slice(0, maxLengthPerItem)
1723
- : sanitized
1724
- )
1843
+ : sanitized;
1844
+ setInputValue(nextInput);
1845
+ onInputValueChange?.(nextInput);
1725
1846
  }}
1726
1847
  maxLength={maxLengthPerItem}
1727
1848
  onKeyDown={handleKeyDown}
@@ -1755,18 +1876,18 @@ const CreatableMultiSelect = React.forwardRef(
1755
1876
  aria-controls={listboxId}
1756
1877
  aria-disabled={disabled || undefined}
1757
1878
  onKeyDown={(e) => {
1758
- if (disabled) return
1879
+ if (disabled) return;
1759
1880
  if (e.key === "Enter" || e.key === " ") {
1760
- e.preventDefault()
1761
- handleOpen()
1881
+ e.preventDefault();
1882
+ handleOpen();
1762
1883
  }
1763
1884
  }}
1764
1885
  onClick={(e) => {
1765
- if (disabled) return
1886
+ if (disabled) return;
1766
1887
  if ((e.target as HTMLElement).closest("[data-chip-remove]")) {
1767
- return
1888
+ return;
1768
1889
  }
1769
- handleOpen()
1890
+ handleOpen();
1770
1891
  }}
1771
1892
  className={cn(
1772
1893
  creatableSelectTriggerVariants({ state: derivedState }),
@@ -1802,25 +1923,35 @@ const CreatableMultiSelect = React.forwardRef(
1802
1923
  className="inline-flex max-w-full items-center gap-0.5 rounded bg-semantic-bg-ui py-1 pl-2 pr-0.5 text-sm text-semantic-text-primary"
1803
1924
  >
1804
1925
  <span className="min-w-0 truncate">
1805
- {labelForValue(val, options)}
1926
+ {labelForValue(
1927
+ val,
1928
+ options,
1929
+ sanitizeInput,
1930
+ maxLengthPerItem
1931
+ )}
1806
1932
  </span>
1807
1933
  <button
1808
1934
  type="button"
1809
1935
  data-chip-remove
1810
1936
  disabled={disabled}
1811
- aria-label={\`Remove \${labelForValue(val, options)}\`}
1937
+ aria-label={\`Remove \${labelForValue(
1938
+ val,
1939
+ options,
1940
+ sanitizeInput,
1941
+ maxLengthPerItem
1942
+ )}\`}
1812
1943
  className={cn(
1813
1944
  "inline-flex size-6 shrink-0 items-center justify-center rounded text-semantic-text-muted transition-colors",
1814
1945
  !disabled &&
1815
1946
  "hover:bg-semantic-bg-hover hover:text-semantic-text-primary"
1816
1947
  )}
1817
1948
  onMouseDown={(e) => {
1818
- e.preventDefault()
1819
- e.stopPropagation()
1949
+ e.preventDefault();
1950
+ e.stopPropagation();
1820
1951
  }}
1821
1952
  onClick={(e) => {
1822
- e.stopPropagation()
1823
- if (!disabled) removeValue(val)
1953
+ e.stopPropagation();
1954
+ if (!disabled) removeValue(val);
1824
1955
  }}
1825
1956
  >
1826
1957
  <X className="size-3.5" strokeWidth={2} aria-hidden />
@@ -1838,30 +1969,28 @@ const CreatableMultiSelect = React.forwardRef(
1838
1969
 
1839
1970
  {/* Dropdown panel: decorative hint row, counter, max-selections, presets. Input lives in the trigger above. */}
1840
1971
  {isOpen && (
1841
- <div
1842
- className="absolute left-0 top-full z-[9999] mt-1 flex w-full flex-col overflow-hidden rounded border border-solid border-semantic-border-layout bg-semantic-bg-primary shadow-sm animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-200"
1843
- >
1972
+ <div className="absolute left-0 top-full z-[9999] mt-1 flex w-full flex-col overflow-hidden rounded border border-solid border-semantic-border-layout bg-semantic-bg-primary shadow-sm animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-200">
1844
1973
  {createHintText ? (
1845
1974
  <div className={creatablePrimaryRoleHintRowClassName}>
1846
1975
  <span className="text-sm text-semantic-text-muted">
1847
1976
  {createHintText}
1848
1977
  </span>
1849
- <kbd className={creatableEnterHintKbdClassName}>
1850
- Enter \u21B5
1851
- </kbd>
1978
+ <kbd className={creatableEnterHintKbdClassName}>Enter \u21B5</kbd>
1852
1979
  </div>
1853
1980
  ) : null}
1854
1981
 
1855
- {(filteredPresets.length > 0 || isCustomDraft) && (
1982
+ {(canAddMore ||
1983
+ filteredPresets.length > 0 ||
1984
+ isCustomDraft) && (
1856
1985
  <div
1857
1986
  className={cn(
1858
1987
  "flex flex-col px-4",
1859
- filteredPresets.length > 0
1988
+ filteredPresets.length > 0 || (canAddMore && maxItems != null)
1860
1989
  ? "gap-2.5 pb-4 pt-2.5"
1861
1990
  : "py-1"
1862
1991
  )}
1863
1992
  >
1864
- {maxItems != null && filteredPresets.length > 0 ? (
1993
+ {maxItems != null && canAddMore ? (
1865
1994
  <p className="m-0 text-sm text-semantic-text-muted">
1866
1995
  Max selections allowed: {maxItems}
1867
1996
  </p>
@@ -1880,8 +2009,8 @@ const CreatableMultiSelect = React.forwardRef(
1880
2009
  role="option"
1881
2010
  aria-selected={false}
1882
2011
  onMouseDown={(e) => {
1883
- e.preventDefault()
1884
- addValue(option.value)
2012
+ e.preventDefault();
2013
+ addValue(option.value);
1885
2014
  }}
1886
2015
  className="inline-flex items-center gap-2.5 whitespace-nowrap rounded border-0 bg-semantic-bg-ui px-2 py-1 text-left text-sm text-semantic-text-primary transition-colors hover:bg-semantic-bg-hover"
1887
2016
  >
@@ -1902,8 +2031,8 @@ const CreatableMultiSelect = React.forwardRef(
1902
2031
  role="option"
1903
2032
  aria-selected={false}
1904
2033
  onMouseDown={(e) => {
1905
- e.preventDefault()
1906
- addValue(inputValue)
2034
+ e.preventDefault();
2035
+ addValue(inputValue);
1907
2036
  }}
1908
2037
  className="-mx-4 flex w-[calc(100%+2rem)] items-center gap-2 rounded-none px-4 py-2 text-left text-base text-semantic-text-link outline-none transition-colors cursor-pointer select-none hover:bg-semantic-bg-ui"
1909
2038
  >
@@ -1919,18 +2048,16 @@ const CreatableMultiSelect = React.forwardRef(
1919
2048
  {helperText && !isOpen ? (
1920
2049
  <div className="mt-1.5 flex items-center gap-1.5">
1921
2050
  <Info className="size-[18px] shrink-0 text-semantic-text-muted" />
1922
- <p className="m-0 text-sm text-semantic-text-muted">
1923
- {helperText}
1924
- </p>
2051
+ <p className="m-0 text-sm text-semantic-text-muted">{helperText}</p>
1925
2052
  </div>
1926
2053
  ) : null}
1927
2054
  </div>
1928
- )
2055
+ );
1929
2056
  }
1930
- )
1931
- CreatableMultiSelect.displayName = "CreatableMultiSelect"
2057
+ );
2058
+ CreatableMultiSelect.displayName = "CreatableMultiSelect";
1932
2059
 
1933
- export { CreatableMultiSelect, creatableMultiSelectTriggerVariants }
2060
+ export { CreatableMultiSelect, creatableMultiSelectTriggerVariants };
1934
2061
  `,
1935
2062
  "creatable-select": `import * as React from "react"
1936
2063
  import { cva, type VariantProps } from "class-variance-authority"
@@ -1946,7 +2073,7 @@ const creatableSelectTriggerVariants = cva(
1946
2073
  default:
1947
2074
  "border border-solid border-semantic-border-input focus-within:border-semantic-border-input-focus/50 focus-within:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
1948
2075
  error:
1949
- "border border-solid border-semantic-error-primary/40 focus-within:border-semantic-error-primary/60 focus-within:shadow-[0_0_0_1px_rgba(240,68,56,0.1)]",
2076
+ "border border-solid border-semantic-error-primary focus-within:border-semantic-error-primary focus-within:shadow-[0_0_0_1px_rgba(240,68,56,0.12)]",
1950
2077
  },
1951
2078
  },
1952
2079
  defaultVariants: {
@@ -2020,6 +2147,8 @@ export interface CreatableSelectProps
2020
2147
  * (nothing was stripped). Use to clear validation errors when the user corrects input.
2021
2148
  */
2022
2149
  onValidInput?: () => void
2150
+ /** Fired with the current open-dropdown draft text so parents can validate while typing. */
2151
+ onInputValueChange?: (value: string) => void
2023
2152
  }
2024
2153
 
2025
2154
  const CreatableSelect = React.forwardRef(
@@ -2038,6 +2167,7 @@ const CreatableSelect = React.forwardRef(
2038
2167
  normalizeComboboxInput,
2039
2168
  onInvalidCharacters,
2040
2169
  onValidInput,
2170
+ onInputValueChange,
2041
2171
  ...props
2042
2172
  }: CreatableSelectProps,
2043
2173
  ref: React.Ref<HTMLDivElement>
@@ -2072,6 +2202,7 @@ const CreatableSelect = React.forwardRef(
2072
2202
  if (disabled) return
2073
2203
  setOpen(true)
2074
2204
  setSearch("")
2205
+ onInputValueChange?.("")
2075
2206
  setHighlightIndex(-1)
2076
2207
  requestAnimationFrame(() => inputRef.current?.focus())
2077
2208
  }
@@ -2081,8 +2212,9 @@ const CreatableSelect = React.forwardRef(
2081
2212
  onValueChange?.(val)
2082
2213
  setOpen(false)
2083
2214
  setSearch("")
2215
+ onInputValueChange?.("")
2084
2216
  },
2085
- [onValueChange]
2217
+ [onInputValueChange, onValueChange]
2086
2218
  )
2087
2219
 
2088
2220
  const handleCreate = React.useCallback(() => {
@@ -2096,10 +2228,12 @@ const CreatableSelect = React.forwardRef(
2096
2228
  onValueChange?.(value)
2097
2229
  setOpen(false)
2098
2230
  setSearch("")
2231
+ onInputValueChange?.("")
2099
2232
  }
2100
2233
  }, [
2101
2234
  search,
2102
2235
  onValueChange,
2236
+ onInputValueChange,
2103
2237
  maxLength,
2104
2238
  sanitizeInput,
2105
2239
  normalizeComboboxInput,
@@ -2109,6 +2243,8 @@ const CreatableSelect = React.forwardRef(
2109
2243
  if (e.key === "Escape") {
2110
2244
  e.preventDefault()
2111
2245
  setOpen(false)
2246
+ setSearch("")
2247
+ onInputValueChange?.("")
2112
2248
  return
2113
2249
  }
2114
2250
 
@@ -2161,11 +2297,12 @@ const CreatableSelect = React.forwardRef(
2161
2297
  !containerRef.current.contains(e.target as Node)
2162
2298
  ) {
2163
2299
  setOpen(false)
2300
+ onInputValueChange?.("")
2164
2301
  }
2165
2302
  }
2166
2303
  document.addEventListener("mousedown", handler)
2167
2304
  return () => document.removeEventListener("mousedown", handler)
2168
- }, [open])
2305
+ }, [onInputValueChange, open])
2169
2306
 
2170
2307
  // Reset highlight when filter changes
2171
2308
  React.useEffect(() => {
@@ -2201,9 +2338,10 @@ const CreatableSelect = React.forwardRef(
2201
2338
  const next = normalizeComboboxInput
2202
2339
  ? normalizeComboboxInput(sanitized)
2203
2340
  : sanitized
2204
- setSearch(
2341
+ const nextSearch =
2205
2342
  maxLength != null ? next.slice(0, maxLength) : next
2206
- )
2343
+ setSearch(nextSearch)
2344
+ onInputValueChange?.(nextSearch)
2207
2345
  }}
2208
2346
  maxLength={maxLength}
2209
2347
  onKeyDown={handleKeyDown}
@@ -5465,6 +5603,29 @@ export interface SelectFieldProps {
5465
5603
  id?: string;
5466
5604
  /** Name attribute for form submission */
5467
5605
  name?: string;
5606
+ /**
5607
+ * Fires when the user scrolls to the bottom of the open dropdown.
5608
+ * Use this to load the next page from the server. The callback is
5609
+ * forwarded to SelectContent's \`onViewportScrollEnd\` (debounced by
5610
+ * the native \`scrollend\` event), so it won't fire while a scroll is
5611
+ * still in progress.
5612
+ *
5613
+ * No virtualization is applied \u2014 all loaded items render to the DOM.
5614
+ * For >2k items consumers may notice some lag; virtualization is a
5615
+ * separate ticket if it becomes a real-world problem.
5616
+ */
5617
+ onScrollEnd?: () => void;
5618
+ /**
5619
+ * When true, renders a small "Loading more\u2026" row at the bottom of
5620
+ * the options list. Set this to true while your API call is in flight
5621
+ * so the user knows more items are on the way.
5622
+ */
5623
+ loadingMore?: boolean;
5624
+ /**
5625
+ * When false, prevents \`onScrollEnd\` from firing further and renders
5626
+ * an "End of list" footer row. Default true (keep firing).
5627
+ */
5628
+ hasMore?: boolean;
5468
5629
  }
5469
5630
 
5470
5631
  /**
@@ -5483,6 +5644,34 @@ export interface SelectFieldProps {
5483
5644
  * required
5484
5645
  * />
5485
5646
  * \`\`\`
5647
+ *
5648
+ * @example Lazy-load on scroll
5649
+ * \`\`\`tsx
5650
+ * const [items, setItems] = useState<SelectOption[]>([]);
5651
+ * const [loadingMore, setLoadingMore] = useState(false);
5652
+ * const [hasMore, setHasMore] = useState(true);
5653
+ * const page = useRef(0);
5654
+ *
5655
+ * const loadNext = async () => {
5656
+ * if (loadingMore || !hasMore) return;
5657
+ * setLoadingMore(true);
5658
+ * const { results, isLast } = await api.fetchTemplates(page.current);
5659
+ * setItems(prev => [...prev, ...results]);
5660
+ * setHasMore(!isLast);
5661
+ * page.current += 1;
5662
+ * setLoadingMore(false);
5663
+ * };
5664
+ *
5665
+ * useEffect(() => { loadNext(); }, []);
5666
+ *
5667
+ * <SelectField
5668
+ * label="Template"
5669
+ * options={items}
5670
+ * onScrollEnd={loadNext}
5671
+ * loadingMore={loadingMore}
5672
+ * hasMore={hasMore}
5673
+ * />
5674
+ * \`\`\`
5486
5675
  */
5487
5676
  const SelectField = React.forwardRef(
5488
5677
  (
@@ -5507,6 +5696,9 @@ const SelectField = React.forwardRef(
5507
5696
  labelClassName,
5508
5697
  id,
5509
5698
  name,
5699
+ onScrollEnd,
5700
+ loadingMore,
5701
+ hasMore,
5510
5702
  }: SelectFieldProps,
5511
5703
  ref: React.Ref<HTMLButtonElement>
5512
5704
  ) => {
@@ -5586,6 +5778,15 @@ const SelectField = React.forwardRef(
5586
5778
 
5587
5779
  const hasGroups = Object.keys(groupedOptions.groups).length > 0;
5588
5780
 
5781
+ // Count rendered options for the "End of list" footer (only show when at least one is visible).
5782
+ const totalRendered =
5783
+ groupedOptions.ungrouped.length +
5784
+ Object.values(groupedOptions.groups).reduce(
5785
+ (sum, items) => sum + items.length,
5786
+ 0
5787
+ );
5788
+ const showEndOfList = hasMore === false && totalRendered > 0 && !loadingMore;
5789
+
5589
5790
  // Handle search input change
5590
5791
  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
5591
5792
  setSearchQuery(e.target.value);
@@ -5638,7 +5839,9 @@ const SelectField = React.forwardRef(
5638
5839
  <Loader2 className="absolute right-8 size-4 animate-spin text-semantic-text-muted" />
5639
5840
  )}
5640
5841
  </SelectTrigger>
5641
- <SelectContent>
5842
+ <SelectContent
5843
+ onViewportScrollEnd={hasMore !== false ? onScrollEnd : undefined}
5844
+ >
5642
5845
  {/* Search input */}
5643
5846
  {searchable && (
5644
5847
  <div className="flex items-center gap-2 px-3 pb-1.5 border-b border-solid border-semantic-border-layout">
@@ -5697,6 +5900,28 @@ const SelectField = React.forwardRef(
5697
5900
  No results found
5698
5901
  </div>
5699
5902
  )}
5903
+
5904
+ {/* Loading-more row (lazy-load) */}
5905
+ {loadingMore && (
5906
+ <div
5907
+ role="status"
5908
+ aria-live="polite"
5909
+ className="flex items-center justify-center gap-2 py-2 text-sm text-semantic-text-muted"
5910
+ >
5911
+ <Loader2 className="size-3.5 animate-spin" aria-hidden="true" />
5912
+ <span>Loading more\u2026</span>
5913
+ </div>
5914
+ )}
5915
+
5916
+ {/* End-of-list footer (lazy-load) */}
5917
+ {showEndOfList && (
5918
+ <div
5919
+ role="status"
5920
+ className="py-2 text-center text-xs text-semantic-text-muted"
5921
+ >
5922
+ End of list
5923
+ </div>
5924
+ )}
5700
5925
  </SelectContent>
5701
5926
  </Select>
5702
5927
 
@@ -5743,7 +5968,7 @@ const selectTriggerVariants = cva(
5743
5968
  default:
5744
5969
  "border border-solid border-semantic-border-input focus:outline-none focus:border-semantic-border-input-focus focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]",
5745
5970
  error:
5746
- "border border-solid border-semantic-error-primary/40 focus:outline-none focus:border-semantic-error-primary/60 focus:shadow-[0_0_0_1px_rgba(240,68,56,0.1)]",
5971
+ "border border-solid border-semantic-error-primary focus:outline-none focus:border-semantic-error-primary focus:shadow-[0_0_0_1px_rgba(240,68,56,0.12)]",
5747
5972
  },
5748
5973
  },
5749
5974
  defaultVariants: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.290",
3
+ "version": "0.2.292",
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",