myoperator-mcp 0.2.375 → 0.2.376

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 +95 -48
  2. 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
  */
@@ -7091,11 +7099,32 @@ const MultiSelect = React.forwardRef(
7091
7099
  const listRef = React.useRef<HTMLDivElement | null>(null);
7092
7100
  /**
7093
7101
  * True once \`onScrollEnd\` has fired for the current visit to the bottom.
7094
- * Cleared only when the user scrolls back out of the threshold zone, so
7095
- * trackpad inertia \u2014 which keeps firing \`scroll\` for hundreds of ms after
7096
- * the finger lifts \u2014 cannot re-trigger the callback.
7102
+ * Cleared when the user scrolls back out of the threshold zone, or when a
7103
+ * landed page pushes the bottom back out of reach, so trackpad inertia \u2014
7104
+ * which keeps firing \`scroll\` for hundreds of ms after the finger lifts \u2014
7105
+ * cannot re-trigger the callback.
7097
7106
  */
7098
7107
  const isLatchedRef = React.useRef(false);
7108
+ /** Previous \`loadingMore\`, so the re-measure knows a page just landed. */
7109
+ const wasLoadingMoreRef = React.useRef(false);
7110
+ /** Pending \`requestAnimationFrame\` id for the coalesced scroll handler. */
7111
+ const scrollFrameRef = React.useRef<number | null>(null);
7112
+ /**
7113
+ * \`onScrollEnd\` read from a timer rather than from the closure, so an
7114
+ * inline arrow from the consumer cannot restart the post-fetch timer on
7115
+ * every render.
7116
+ */
7117
+ const onScrollEndRef = React.useRef(onScrollEnd);
7118
+ React.useEffect(() => {
7119
+ onScrollEndRef.current = onScrollEnd;
7120
+ }, [onScrollEnd]);
7121
+
7122
+ /** Distance in px from the current scroll position to the list bottom. */
7123
+ const distanceToBottom = () => {
7124
+ const node = listRef.current;
7125
+ if (!node) return null;
7126
+ return node.scrollHeight - node.scrollTop - node.clientHeight;
7127
+ };
7099
7128
 
7100
7129
  /**
7101
7130
  * Deliberately a plain function, not a \`useCallback\` \u2014 it must read the
@@ -7103,14 +7132,10 @@ const MultiSelect = React.forwardRef(
7103
7132
  * handler would need a props ref (writing refs during render is banned).
7104
7133
  */
7105
7134
  const maybeLoadMore = () => {
7106
- const node = listRef.current;
7107
- if (!node) return;
7135
+ const distance = distanceToBottom();
7136
+ if (distance === null) return;
7108
7137
 
7109
- const isNearBottom =
7110
- node.scrollHeight - node.scrollTop - node.clientHeight <
7111
- SCROLL_END_THRESHOLD_PX;
7112
-
7113
- if (!isNearBottom) {
7138
+ if (distance >= SCROLL_END_THRESHOLD_PX) {
7114
7139
  // Only an explicit scroll away from the boundary re-arms the latch.
7115
7140
  isLatchedRef.current = false;
7116
7141
  return;
@@ -7122,25 +7147,71 @@ const MultiSelect = React.forwardRef(
7122
7147
  onScrollEnd();
7123
7148
  };
7124
7149
 
7150
+ /**
7151
+ * \`scroll\` fires far more often than the browser paints, so a fast flick
7152
+ * delivers a burst of events whose geometry is mid-flight. Coalescing to
7153
+ * one \`requestAnimationFrame\` per burst measures once per frame, after
7154
+ * layout has settled, and still sees the final resting position.
7155
+ */
7156
+ const handleListScroll = () => {
7157
+ if (scrollFrameRef.current !== null) return;
7158
+ scrollFrameRef.current = requestAnimationFrame(() => {
7159
+ scrollFrameRef.current = null;
7160
+ maybeLoadMore();
7161
+ });
7162
+ };
7163
+
7125
7164
  // A closed menu or a new search starts from a clean latch.
7126
7165
  React.useEffect(() => {
7127
7166
  isLatchedRef.current = false;
7128
7167
  }, [isOpen, searchQuery]);
7129
7168
 
7169
+ React.useEffect(
7170
+ () => () => {
7171
+ if (scrollFrameRef.current !== null) {
7172
+ cancelAnimationFrame(scrollFrameRef.current);
7173
+ }
7174
+ },
7175
+ []
7176
+ );
7177
+
7130
7178
  /**
7131
- * A page can arrive without making the list taller than its max-height
7132
- * (few results, or a short viewport). No further \`scroll\` event would ever
7133
- * fire, so pagination would stall silently \u2014 re-check whenever the rendered
7134
- * options or the fetch state change.
7179
+ * Re-measure once a page has landed. Content growth emits no \`scroll\`
7180
+ * event, so without this the latch set on the way down would never clear
7181
+ * and pagination would stall permanently after a fast flick to the bottom.
7182
+ * It also covers the case where the page does not make the list taller
7183
+ * than its max-height (few results, or a short viewport), where no further
7184
+ * \`scroll\` event would ever fire either.
7185
+ *
7186
+ * The latch is cleared only once the bottom is genuinely out of reach \u2014
7187
+ * while the user is still pinned at distance ~0, it stays armed and the
7188
+ * next page is chained explicitly, so inertia cannot fan out into
7189
+ * duplicate requests.
7135
7190
  */
7136
7191
  React.useEffect(() => {
7137
- if (!isOpen || !hasMore || loadingMore) return;
7138
- const node = listRef.current;
7139
- if (!node || node.scrollHeight > node.clientHeight) return;
7140
- if (isLatchedRef.current) return;
7141
- isLatchedRef.current = true;
7142
- onScrollEnd?.();
7143
- }, [isOpen, portalTarget, hasMore, loadingMore, options, onScrollEnd]);
7192
+ const pageJustLanded = wasLoadingMoreRef.current && !loadingMore;
7193
+ wasLoadingMoreRef.current = loadingMore;
7194
+ if (!isOpen || loadingMore) return;
7195
+
7196
+ const timeoutId = window.setTimeout(() => {
7197
+ const distance = distanceToBottom();
7198
+ if (distance === null) return;
7199
+
7200
+ if (distance >= SCROLL_END_THRESHOLD_PX) {
7201
+ isLatchedRef.current = false;
7202
+ return;
7203
+ }
7204
+ if (!hasMore || !onScrollEndRef.current) return;
7205
+ // Still latched and nothing landed: the scroll handler's request is
7206
+ // in flight (or the consumer never set \`loadingMore\`) \u2014 don't refire.
7207
+ if (isLatchedRef.current && !pageJustLanded) return;
7208
+
7209
+ isLatchedRef.current = true;
7210
+ onScrollEndRef.current();
7211
+ }, POST_FETCH_MEASURE_DELAY_MS);
7212
+
7213
+ return () => window.clearTimeout(timeoutId);
7214
+ }, [isOpen, portalTarget, hasMore, loadingMore, options.length]);
7144
7215
 
7145
7216
  const flatOptions = React.useMemo(
7146
7217
  () => flattenMultiSelectOptions(options),
@@ -7572,7 +7643,7 @@ const MultiSelect = React.forwardRef(
7572
7643
  {/* Options */}
7573
7644
  <div
7574
7645
  ref={listRef}
7575
- onScroll={maybeLoadMore}
7646
+ onScroll={handleListScroll}
7576
7647
  className="overflow-auto overscroll-contain p-1"
7577
7648
  style={{
7578
7649
  maxHeight:
@@ -9509,32 +9580,11 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
9509
9580
  [disabled]
9510
9581
  );
9511
9582
 
9512
- /**
9513
- * The pending focus frame has to be cancellable. Opening the menu schedules
9514
- * a frame that refocuses the input, and the input's \`onFocus\` reopens the
9515
- * menu. If that frame lands AFTER an option has been picked, it reopens the
9516
- * dropdown that \`selectOption\` just closed \u2014 so selecting an option looks
9517
- * like it does nothing. Whether the frame lands before or after the click is
9518
- * pure timing, which is why the bug comes and goes.
9519
- */
9520
- const focusFrameRef = React.useRef<number | null>(null);
9521
-
9522
- const cancelPendingFocus = React.useCallback(() => {
9523
- if (focusFrameRef.current === null) return;
9524
- window.cancelAnimationFrame(focusFrameRef.current);
9525
- focusFrameRef.current = null;
9526
- }, []);
9527
-
9528
9583
  const focusSearchInput = React.useCallback(() => {
9529
- cancelPendingFocus();
9530
- focusFrameRef.current = window.requestAnimationFrame(() => {
9531
- focusFrameRef.current = null;
9584
+ window.requestAnimationFrame(() => {
9532
9585
  searchInputRef.current?.focus();
9533
9586
  });
9534
- }, [cancelPendingFocus]);
9535
-
9536
- // Never let a queued frame fire into an unmounted component.
9537
- React.useEffect(() => cancelPendingFocus, [cancelPendingFocus]);
9587
+ }, []);
9538
9588
 
9539
9589
  const setRootRef = React.useCallback(
9540
9590
  (node: HTMLDivElement | null) => {
@@ -9613,12 +9663,9 @@ const SearchFilter = React.forwardRef<HTMLDivElement, SearchFilterProps>(
9613
9663
  onValueChange?.(option.value);
9614
9664
  onOptionSelect?.(option);
9615
9665
  onSearchChange?.(option.label);
9616
- // Drop any queued refocus first, or it reopens what we are closing.
9617
- cancelPendingFocus();
9618
9666
  setOpen(false);
9619
9667
  },
9620
9668
  [
9621
- cancelPendingFocus,
9622
9669
  disabled,
9623
9670
  onSearchChange,
9624
9671
  onOptionSelect,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.375",
3
+ "version": "0.2.376",
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",