myoperator-mcp 0.2.375 → 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.
- package/dist/index.js +130 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6780,6 +6780,14 @@ const MENU_MIN_HEIGHT = 120;
|
|
|
6780
6780
|
*/
|
|
6781
6781
|
const SCROLL_END_THRESHOLD_PX = 48;
|
|
6782
6782
|
|
|
6783
|
+
/**
|
|
6784
|
+
* How long to wait after a page lands before re-measuring the list. React has
|
|
6785
|
+
* committed the new rows by the time the effect runs, but the browser has not
|
|
6786
|
+
* necessarily laid them out, so \`scrollHeight\` can still read the pre-growth
|
|
6787
|
+
* value. One frame is usually enough; 50ms is a cheap margin.
|
|
6788
|
+
*/
|
|
6789
|
+
const POST_FETCH_MEASURE_DELAY_MS = 50;
|
|
6790
|
+
|
|
6783
6791
|
/**
|
|
6784
6792
|
* MultiSelect trigger variants matching TextField styling
|
|
6785
6793
|
*/
|
|
@@ -6853,6 +6861,18 @@ export interface MultiSelectProps extends VariantProps<
|
|
|
6853
6861
|
searchable?: boolean;
|
|
6854
6862
|
/** Search placeholder text */
|
|
6855
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;
|
|
6856
6876
|
/**
|
|
6857
6877
|
* When set, the trigger shows a single compact summary (e.g. "3 lines
|
|
6858
6878
|
* selected") instead of one chip per selection; hovering it reveals the full
|
|
@@ -6945,6 +6965,8 @@ const MultiSelect = React.forwardRef(
|
|
|
6945
6965
|
options,
|
|
6946
6966
|
searchable,
|
|
6947
6967
|
searchPlaceholder = "Search...",
|
|
6968
|
+
searchQuery: searchQueryProp,
|
|
6969
|
+
onSearchQueryChange,
|
|
6948
6970
|
selectAllLabel,
|
|
6949
6971
|
summaryLabel,
|
|
6950
6972
|
maxSelections,
|
|
@@ -6970,8 +6992,20 @@ const MultiSelect = React.forwardRef(
|
|
|
6970
6992
|
React.useState<string[]>(defaultValue);
|
|
6971
6993
|
// Dropdown open state
|
|
6972
6994
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
6973
|
-
// Search query
|
|
6974
|
-
|
|
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
|
+
);
|
|
6975
7009
|
|
|
6976
7010
|
// \`detailed\` rows are a single-line design, so they truncate unless the
|
|
6977
7011
|
// caller opts out; \`simple\` rows wrap the full label unless asked not to.
|
|
@@ -7091,11 +7125,32 @@ const MultiSelect = React.forwardRef(
|
|
|
7091
7125
|
const listRef = React.useRef<HTMLDivElement | null>(null);
|
|
7092
7126
|
/**
|
|
7093
7127
|
* True once \`onScrollEnd\` has fired for the current visit to the bottom.
|
|
7094
|
-
* Cleared
|
|
7095
|
-
*
|
|
7096
|
-
* the finger lifts \u2014
|
|
7128
|
+
* Cleared when the user scrolls back out of the threshold zone, or when a
|
|
7129
|
+
* landed page pushes the bottom back out of reach, so trackpad inertia \u2014
|
|
7130
|
+
* which keeps firing \`scroll\` for hundreds of ms after the finger lifts \u2014
|
|
7131
|
+
* cannot re-trigger the callback.
|
|
7097
7132
|
*/
|
|
7098
7133
|
const isLatchedRef = React.useRef(false);
|
|
7134
|
+
/** Previous \`loadingMore\`, so the re-measure knows a page just landed. */
|
|
7135
|
+
const wasLoadingMoreRef = React.useRef(false);
|
|
7136
|
+
/** Pending \`requestAnimationFrame\` id for the coalesced scroll handler. */
|
|
7137
|
+
const scrollFrameRef = React.useRef<number | null>(null);
|
|
7138
|
+
/**
|
|
7139
|
+
* \`onScrollEnd\` read from a timer rather than from the closure, so an
|
|
7140
|
+
* inline arrow from the consumer cannot restart the post-fetch timer on
|
|
7141
|
+
* every render.
|
|
7142
|
+
*/
|
|
7143
|
+
const onScrollEndRef = React.useRef(onScrollEnd);
|
|
7144
|
+
React.useEffect(() => {
|
|
7145
|
+
onScrollEndRef.current = onScrollEnd;
|
|
7146
|
+
}, [onScrollEnd]);
|
|
7147
|
+
|
|
7148
|
+
/** Distance in px from the current scroll position to the list bottom. */
|
|
7149
|
+
const distanceToBottom = () => {
|
|
7150
|
+
const node = listRef.current;
|
|
7151
|
+
if (!node) return null;
|
|
7152
|
+
return node.scrollHeight - node.scrollTop - node.clientHeight;
|
|
7153
|
+
};
|
|
7099
7154
|
|
|
7100
7155
|
/**
|
|
7101
7156
|
* Deliberately a plain function, not a \`useCallback\` \u2014 it must read the
|
|
@@ -7103,14 +7158,10 @@ const MultiSelect = React.forwardRef(
|
|
|
7103
7158
|
* handler would need a props ref (writing refs during render is banned).
|
|
7104
7159
|
*/
|
|
7105
7160
|
const maybeLoadMore = () => {
|
|
7106
|
-
const
|
|
7107
|
-
if (
|
|
7161
|
+
const distance = distanceToBottom();
|
|
7162
|
+
if (distance === null) return;
|
|
7108
7163
|
|
|
7109
|
-
|
|
7110
|
-
node.scrollHeight - node.scrollTop - node.clientHeight <
|
|
7111
|
-
SCROLL_END_THRESHOLD_PX;
|
|
7112
|
-
|
|
7113
|
-
if (!isNearBottom) {
|
|
7164
|
+
if (distance >= SCROLL_END_THRESHOLD_PX) {
|
|
7114
7165
|
// Only an explicit scroll away from the boundary re-arms the latch.
|
|
7115
7166
|
isLatchedRef.current = false;
|
|
7116
7167
|
return;
|
|
@@ -7122,25 +7173,71 @@ const MultiSelect = React.forwardRef(
|
|
|
7122
7173
|
onScrollEnd();
|
|
7123
7174
|
};
|
|
7124
7175
|
|
|
7176
|
+
/**
|
|
7177
|
+
* \`scroll\` fires far more often than the browser paints, so a fast flick
|
|
7178
|
+
* delivers a burst of events whose geometry is mid-flight. Coalescing to
|
|
7179
|
+
* one \`requestAnimationFrame\` per burst measures once per frame, after
|
|
7180
|
+
* layout has settled, and still sees the final resting position.
|
|
7181
|
+
*/
|
|
7182
|
+
const handleListScroll = () => {
|
|
7183
|
+
if (scrollFrameRef.current !== null) return;
|
|
7184
|
+
scrollFrameRef.current = requestAnimationFrame(() => {
|
|
7185
|
+
scrollFrameRef.current = null;
|
|
7186
|
+
maybeLoadMore();
|
|
7187
|
+
});
|
|
7188
|
+
};
|
|
7189
|
+
|
|
7125
7190
|
// A closed menu or a new search starts from a clean latch.
|
|
7126
7191
|
React.useEffect(() => {
|
|
7127
7192
|
isLatchedRef.current = false;
|
|
7128
7193
|
}, [isOpen, searchQuery]);
|
|
7129
7194
|
|
|
7195
|
+
React.useEffect(
|
|
7196
|
+
() => () => {
|
|
7197
|
+
if (scrollFrameRef.current !== null) {
|
|
7198
|
+
cancelAnimationFrame(scrollFrameRef.current);
|
|
7199
|
+
}
|
|
7200
|
+
},
|
|
7201
|
+
[]
|
|
7202
|
+
);
|
|
7203
|
+
|
|
7130
7204
|
/**
|
|
7131
|
-
*
|
|
7132
|
-
*
|
|
7133
|
-
*
|
|
7134
|
-
*
|
|
7205
|
+
* Re-measure once a page has landed. Content growth emits no \`scroll\`
|
|
7206
|
+
* event, so without this the latch set on the way down would never clear
|
|
7207
|
+
* and pagination would stall permanently after a fast flick to the bottom.
|
|
7208
|
+
* It also covers the case where the page does not make the list taller
|
|
7209
|
+
* than its max-height (few results, or a short viewport), where no further
|
|
7210
|
+
* \`scroll\` event would ever fire either.
|
|
7211
|
+
*
|
|
7212
|
+
* The latch is cleared only once the bottom is genuinely out of reach \u2014
|
|
7213
|
+
* while the user is still pinned at distance ~0, it stays armed and the
|
|
7214
|
+
* next page is chained explicitly, so inertia cannot fan out into
|
|
7215
|
+
* duplicate requests.
|
|
7135
7216
|
*/
|
|
7136
7217
|
React.useEffect(() => {
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
if (!
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7218
|
+
const pageJustLanded = wasLoadingMoreRef.current && !loadingMore;
|
|
7219
|
+
wasLoadingMoreRef.current = loadingMore;
|
|
7220
|
+
if (!isOpen || loadingMore) return;
|
|
7221
|
+
|
|
7222
|
+
const timeoutId = window.setTimeout(() => {
|
|
7223
|
+
const distance = distanceToBottom();
|
|
7224
|
+
if (distance === null) return;
|
|
7225
|
+
|
|
7226
|
+
if (distance >= SCROLL_END_THRESHOLD_PX) {
|
|
7227
|
+
isLatchedRef.current = false;
|
|
7228
|
+
return;
|
|
7229
|
+
}
|
|
7230
|
+
if (!hasMore || !onScrollEndRef.current) return;
|
|
7231
|
+
// Still latched and nothing landed: the scroll handler's request is
|
|
7232
|
+
// in flight (or the consumer never set \`loadingMore\`) \u2014 don't refire.
|
|
7233
|
+
if (isLatchedRef.current && !pageJustLanded) return;
|
|
7234
|
+
|
|
7235
|
+
isLatchedRef.current = true;
|
|
7236
|
+
onScrollEndRef.current();
|
|
7237
|
+
}, POST_FETCH_MEASURE_DELAY_MS);
|
|
7238
|
+
|
|
7239
|
+
return () => window.clearTimeout(timeoutId);
|
|
7240
|
+
}, [isOpen, portalTarget, hasMore, loadingMore, options.length]);
|
|
7144
7241
|
|
|
7145
7242
|
const flatOptions = React.useMemo(
|
|
7146
7243
|
() => flattenMultiSelectOptions(options),
|
|
@@ -7186,8 +7283,11 @@ const MultiSelect = React.forwardRef(
|
|
|
7186
7283
|
// Determine aria-describedby
|
|
7187
7284
|
const ariaDescribedBy = error ? errorId : helperText ? helperId : undefined;
|
|
7188
7285
|
|
|
7189
|
-
// 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).
|
|
7190
7289
|
const filteredOptions = React.useMemo(() => {
|
|
7290
|
+
if (isSearchControlled) return flatOptions;
|
|
7191
7291
|
if (!searchable || !searchQuery.trim()) return flatOptions;
|
|
7192
7292
|
const q = searchQuery.toLowerCase();
|
|
7193
7293
|
return flatOptions.filter((option) => {
|
|
@@ -7198,7 +7298,7 @@ const MultiSelect = React.forwardRef(
|
|
|
7198
7298
|
(option.group?.toLowerCase().includes(q) ?? false)
|
|
7199
7299
|
);
|
|
7200
7300
|
});
|
|
7201
|
-
}, [flatOptions, searchable, searchQuery]);
|
|
7301
|
+
}, [flatOptions, searchable, searchQuery, isSearchControlled]);
|
|
7202
7302
|
|
|
7203
7303
|
type DisplayItem =
|
|
7204
7304
|
| { type: "option"; option: MultiSelectOption }
|
|
@@ -7316,7 +7416,7 @@ const MultiSelect = React.forwardRef(
|
|
|
7316
7416
|
return;
|
|
7317
7417
|
}
|
|
7318
7418
|
setIsOpen(false);
|
|
7319
|
-
|
|
7419
|
+
updateSearchQuery("");
|
|
7320
7420
|
};
|
|
7321
7421
|
|
|
7322
7422
|
const timeoutId = window.setTimeout(() => {
|
|
@@ -7327,13 +7427,13 @@ const MultiSelect = React.forwardRef(
|
|
|
7327
7427
|
window.clearTimeout(timeoutId);
|
|
7328
7428
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
7329
7429
|
};
|
|
7330
|
-
}, [isOpen, refs.floating]);
|
|
7430
|
+
}, [isOpen, refs.floating, updateSearchQuery]);
|
|
7331
7431
|
|
|
7332
7432
|
// Handle keyboard navigation
|
|
7333
7433
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
7334
7434
|
if (e.key === "Escape" && closeOnEscape) {
|
|
7335
7435
|
setIsOpen(false);
|
|
7336
|
-
|
|
7436
|
+
updateSearchQuery("");
|
|
7337
7437
|
} else if (e.key === "Enter" || e.key === " ") {
|
|
7338
7438
|
if (!isOpen) {
|
|
7339
7439
|
e.preventDefault();
|
|
@@ -7536,7 +7636,7 @@ const MultiSelect = React.forwardRef(
|
|
|
7536
7636
|
type="text"
|
|
7537
7637
|
placeholder={searchPlaceholder}
|
|
7538
7638
|
value={searchQuery}
|
|
7539
|
-
onChange={(e) =>
|
|
7639
|
+
onChange={(e) => updateSearchQuery(e.target.value)}
|
|
7540
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"
|
|
7541
7641
|
onClick={(e) => e.stopPropagation()}
|
|
7542
7642
|
/>
|
|
@@ -7572,7 +7672,7 @@ const MultiSelect = React.forwardRef(
|
|
|
7572
7672
|
{/* Options */}
|
|
7573
7673
|
<div
|
|
7574
7674
|
ref={listRef}
|
|
7575
|
-
onScroll={
|
|
7675
|
+
onScroll={handleListScroll}
|
|
7576
7676
|
className="overflow-auto overscroll-contain p-1"
|
|
7577
7677
|
style={{
|
|
7578
7678
|
maxHeight:
|
package/package.json
CHANGED