myoperator-mcp 0.2.372 → 0.2.373

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 +151 -15
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5987,6 +5987,11 @@ export function flattenMultiSelectOptions(
5987
5987
  return (input as MultiSelectOption[]).map(normalizeMultiSelectOption);
5988
5988
  }
5989
5989
 
5990
+ /** Menu never renders narrower than this, even off a tiny trigger. */
5991
+ const MENU_MIN_WIDTH = 180;
5992
+ /** Floor for the flip/shrink height so the list is never collapsed to nothing. */
5993
+ const MENU_MIN_HEIGHT = 120;
5994
+
5990
5995
  /**
5991
5996
  * MultiSelect trigger variants matching TextField styling
5992
5997
  */
@@ -6068,6 +6073,19 @@ export interface MultiSelectProps extends VariantProps<
6068
6073
  showClearAll?: boolean;
6069
6074
  /** Vertical rule before the chevron (Figma-style trigger) */
6070
6075
  showSeparatorBeforeChevron?: boolean;
6076
+ /**
6077
+ * Element the dropdown is portaled into. Defaults to \`document.body\` so the
6078
+ * menu escapes \`overflow\` and \`transform\` ancestors (a Radix DialogContent is
6079
+ * both, and a transformed ancestor would clip a \`position: fixed\` menu).
6080
+ */
6081
+ menuContainer?: HTMLElement | null;
6082
+ /**
6083
+ * Truncate long labels in the dropdown list to a single line with an ellipsis
6084
+ * instead of wrapping them. Selected chips in the trigger always truncate \u2014
6085
+ * this only controls the option rows.
6086
+ * @default false for \`simple\`, true for \`detailed\` (single-line row design)
6087
+ */
6088
+ truncateOptionText?: boolean;
6071
6089
  }
6072
6090
 
6073
6091
  /**
@@ -6114,6 +6132,8 @@ const MultiSelect = React.forwardRef(
6114
6132
  optionVariant = "simple",
6115
6133
  separateSelectedWithDivider = false,
6116
6134
  showClearAll = true,
6135
+ menuContainer,
6136
+ truncateOptionText,
6117
6137
  showSeparatorBeforeChevron = false,
6118
6138
  closeOnEscape = false,
6119
6139
  }: MultiSelectProps,
@@ -6127,9 +6147,25 @@ const MultiSelect = React.forwardRef(
6127
6147
  // Search query
6128
6148
  const [searchQuery, setSearchQuery] = React.useState("");
6129
6149
 
6150
+ // \`detailed\` rows are a single-line design, so they truncate unless the
6151
+ // caller opts out; \`simple\` rows wrap the full label unless asked not to.
6152
+ const truncateOptions =
6153
+ truncateOptionText ?? optionVariant === "detailed";
6154
+
6130
6155
  // Container ref for click outside detection
6131
6156
  const containerRef = React.useRef<HTMLDivElement | null>(null);
6132
6157
 
6158
+ /** Where the dropdown gets portaled; body unless the caller overrides. */
6159
+ const [portalTarget, setPortalTarget] = React.useState<HTMLElement | null>(
6160
+ null
6161
+ );
6162
+
6163
+ React.useEffect(() => {
6164
+ if (!isOpen || typeof document === "undefined") return;
6165
+ setPortalTarget(menuContainer ?? document.body);
6166
+ }, [isOpen, menuContainer]);
6167
+
6168
+
6133
6169
  const { refs, floatingStyles, isPositioned } = useFloating({
6134
6170
  open: isOpen,
6135
6171
  placement: "bottom-start",
@@ -6140,8 +6176,19 @@ const MultiSelect = React.forwardRef(
6140
6176
  shift({ padding: 8 }),
6141
6177
  size({
6142
6178
  padding: 8,
6143
- apply({ rects, elements }) {
6144
- elements.floating.style.width = \`\${rects.reference.width}px\`;
6179
+ apply({ rects, elements, availableHeight, availableWidth }) {
6180
+ // Match the trigger, but never render an unreadably narrow menu on a
6181
+ // small trigger, and never spill past the viewport on a small screen.
6182
+ const width = Math.min(
6183
+ Math.max(rects.reference.width, MENU_MIN_WIDTH),
6184
+ availableWidth
6185
+ );
6186
+ elements.floating.style.width = \`\${width}px\`;
6187
+ // Let the option list shrink instead of overflowing a short viewport.
6188
+ elements.floating.style.setProperty(
6189
+ "--multi-select-available-height",
6190
+ \`\${Math.max(availableHeight, MENU_MIN_HEIGHT)}px\`
6191
+ );
6145
6192
  },
6146
6193
  }),
6147
6194
  ],
@@ -6163,6 +6210,57 @@ const MultiSelect = React.forwardRef(
6163
6210
  [refs]
6164
6211
  );
6165
6212
 
6213
+ /**
6214
+ * Radix Dialog / Drawer wrap their content in \`react-remove-scroll\`, which
6215
+ * listens for \`wheel\` / \`touchmove\` on \`document\` (bubble phase) and calls
6216
+ * \`preventDefault()\` for anything outside the locked subtree \u2014 our
6217
+ * body-portaled menu counts as outside, so its list would not scroll.
6218
+ * Stopping propagation at the menu keeps that document listener from ever
6219
+ * seeing the event, leaving the browser's native scrolling intact.
6220
+ * Clicks are handled separately: the dialog sets \`pointer-events: none\` on
6221
+ * \`<body>\`, which the menu overrides with \`pointer-events: auto\`.
6222
+ */
6223
+ React.useEffect(() => {
6224
+ const node = refs.floating.current;
6225
+ if (!isOpen || !node) return;
6226
+
6227
+ const stop = (event: Event) => event.stopPropagation();
6228
+ node.addEventListener("wheel", stop, { passive: false });
6229
+ node.addEventListener("touchmove", stop, { passive: false });
6230
+
6231
+ /**
6232
+ * The dialog also traps focus. Radix \`FocusScope\` registers TWO bubble
6233
+ * listeners on \`document\` and either one is enough to make the search
6234
+ * input untypable:
6235
+ * - \`focusin\` \u2014 target outside the dialog, so focus is pulled back.
6236
+ * - \`focusout\` \u2014 fired on the element focus is LEAVING (inside the
6237
+ * dialog) with \`relatedTarget\` = our input; since that is outside, it
6238
+ * restores the previously focused element.
6239
+ * The \`focusout\` one never travels through the menu, so a listener on the
6240
+ * menu node cannot see it. Intercepting in the CAPTURE phase on
6241
+ * \`document\` does: capture at \`document\` runs before the bubble listeners
6242
+ * on the same node, so stopping propagation there means FocusScope never
6243
+ * runs \u2014 but only for focus events that involve the menu. Every other
6244
+ * focus event in the app is untouched.
6245
+ */
6246
+ const stopMenuFocusEvent = (event: Event) => {
6247
+ const { target, relatedTarget } = event as FocusEvent;
6248
+ const touchesMenu =
6249
+ (target instanceof Node && node.contains(target)) ||
6250
+ (relatedTarget instanceof Node && node.contains(relatedTarget));
6251
+ if (touchesMenu) event.stopPropagation();
6252
+ };
6253
+ document.addEventListener("focusin", stopMenuFocusEvent, true);
6254
+ document.addEventListener("focusout", stopMenuFocusEvent, true);
6255
+
6256
+ return () => {
6257
+ node.removeEventListener("wheel", stop);
6258
+ node.removeEventListener("touchmove", stop);
6259
+ document.removeEventListener("focusin", stopMenuFocusEvent, true);
6260
+ document.removeEventListener("focusout", stopMenuFocusEvent, true);
6261
+ };
6262
+ }, [isOpen, portalTarget, refs.floating]);
6263
+
6166
6264
  const flatOptions = React.useMemo(
6167
6265
  () => flattenMultiSelectOptions(options),
6168
6266
  [options]
@@ -6344,14 +6442,14 @@ const MultiSelect = React.forwardRef(
6344
6442
  return (
6345
6443
  <div
6346
6444
  ref={containerRef}
6347
- className={cn("flex flex-col gap-1", wrapperClassName)}
6445
+ className={cn("flex min-w-0 flex-col gap-1", wrapperClassName)}
6348
6446
  >
6349
6447
  {/* Label */}
6350
6448
  {label && (
6351
6449
  <label
6352
6450
  htmlFor={selectId}
6353
6451
  className={cn(
6354
- "text-sm font-semibold text-semantic-text-secondary",
6452
+ "break-words text-sm font-semibold text-semantic-text-secondary",
6355
6453
  labelClassName
6356
6454
  )}
6357
6455
  >
@@ -6387,7 +6485,7 @@ const MultiSelect = React.forwardRef(
6387
6485
  triggerClassName
6388
6486
  )}
6389
6487
  >
6390
- <div className="flex-1 flex flex-wrap gap-1">
6488
+ <div className="min-w-0 flex-1 flex flex-wrap gap-1">
6391
6489
  {selectedValues.length === 0 ? (
6392
6490
  <span className="text-base text-semantic-text-placeholder">
6393
6491
  {placeholder}
@@ -6396,9 +6494,14 @@ const MultiSelect = React.forwardRef(
6396
6494
  selectedLabels.map((label, index) => (
6397
6495
  <span
6398
6496
  key={selectedValues[index]}
6399
- className="inline-flex items-center gap-1 bg-semantic-bg-ui text-semantic-text-primary text-sm px-2 py-0.5 rounded"
6497
+ className="inline-flex min-w-0 max-w-full items-center gap-1 bg-semantic-bg-ui text-semantic-text-primary text-sm px-2 py-0.5 rounded"
6400
6498
  >
6401
- {label}
6499
+ <span
6500
+ className="min-w-0 truncate"
6501
+ title={typeof label === "string" ? label : undefined}
6502
+ >
6503
+ {label}
6504
+ </span>
6402
6505
  <span
6403
6506
  role="button"
6404
6507
  tabIndex={0}
@@ -6412,7 +6515,7 @@ const MultiSelect = React.forwardRef(
6412
6515
  );
6413
6516
  }
6414
6517
  }}
6415
- className="cursor-pointer hover:text-semantic-error-primary focus:outline-none"
6518
+ className="shrink-0 cursor-pointer hover:text-semantic-error-primary focus:outline-none"
6416
6519
  aria-label={\`Remove \${label}\`}
6417
6520
  >
6418
6521
  <X className="size-3" />
@@ -6471,14 +6574,14 @@ const MultiSelect = React.forwardRef(
6471
6574
  className="size-3.5 shrink-0 text-semantic-error-primary"
6472
6575
  aria-hidden
6473
6576
  />
6474
- <span className="text-sm text-semantic-error-primary">
6577
+ <span className="min-w-0 break-words text-sm text-semantic-error-primary">
6475
6578
  {error}
6476
6579
  </span>
6477
6580
  </div>
6478
6581
  ) : helperText ? (
6479
6582
  <span
6480
6583
  id={helperId}
6481
- className="text-sm text-semantic-text-muted"
6584
+ className="min-w-0 break-words text-sm text-semantic-text-muted"
6482
6585
  >
6483
6586
  {helperText}
6484
6587
  </span>
@@ -6487,7 +6590,7 @@ const MultiSelect = React.forwardRef(
6487
6590
  )}
6488
6591
 
6489
6592
  {isOpen &&
6490
- typeof document !== "undefined" &&
6593
+ portalTarget &&
6491
6594
  createPortal(
6492
6595
  <TooltipProvider delayDuration={200}>
6493
6596
  <div
@@ -6499,6 +6602,9 @@ const MultiSelect = React.forwardRef(
6499
6602
  style={{
6500
6603
  ...floatingStyles,
6501
6604
  zIndex: 10050,
6605
+ // Radix Dialog sets \`pointer-events: none\` on <body> while
6606
+ // open; without this the menu swallows nothing and clicks die.
6607
+ pointerEvents: "auto",
6502
6608
  visibility: isPositioned ? undefined : "hidden",
6503
6609
  }}
6504
6610
  onMouseDown={(e) => e.stopPropagation()}
@@ -6518,7 +6624,13 @@ const MultiSelect = React.forwardRef(
6518
6624
  )}
6519
6625
 
6520
6626
  {/* Options */}
6521
- <div className="max-h-60 overflow-auto p-1">
6627
+ <div
6628
+ className="overflow-auto overscroll-contain p-1"
6629
+ style={{
6630
+ maxHeight:
6631
+ "min(15rem, var(--multi-select-available-height, 15rem))",
6632
+ }}
6633
+ >
6522
6634
  {filteredOptions.length === 0 ? (
6523
6635
  <div className="py-6 text-center text-sm text-semantic-text-muted">
6524
6636
  No results found
@@ -6583,7 +6695,19 @@ const MultiSelect = React.forwardRef(
6583
6695
  <Check className="size-4 text-semantic-primary" />
6584
6696
  )}
6585
6697
  </span>
6586
- <span className="min-w-0 flex-1 whitespace-normal break-words text-left">
6698
+ <span
6699
+ title={
6700
+ truncateOptions && typeof option.label === "string"
6701
+ ? option.label
6702
+ : undefined
6703
+ }
6704
+ className={cn(
6705
+ "min-w-0 flex-1 text-left",
6706
+ truncateOptions
6707
+ ? "truncate"
6708
+ : "whitespace-normal break-words"
6709
+ )}
6710
+ >
6587
6711
  {option.label}
6588
6712
  </span>
6589
6713
  </button>
@@ -6615,7 +6739,19 @@ const MultiSelect = React.forwardRef(
6615
6739
  aria-hidden
6616
6740
  tabIndex={-1}
6617
6741
  />
6618
- <span className="min-w-0 flex-1 truncate text-left">
6742
+ <span
6743
+ title={
6744
+ typeof option.label === "string"
6745
+ ? option.label
6746
+ : undefined
6747
+ }
6748
+ className={cn(
6749
+ "min-w-0 flex-1 text-left",
6750
+ truncateOptions
6751
+ ? "truncate"
6752
+ : "whitespace-normal break-words"
6753
+ )}
6754
+ >
6619
6755
  {option.label}
6620
6756
  </span>
6621
6757
  {secondaryLine ? (
@@ -6667,7 +6803,7 @@ const MultiSelect = React.forwardRef(
6667
6803
  ) : null}
6668
6804
  </div>
6669
6805
  </TooltipProvider>,
6670
- document.body
6806
+ portalTarget
6671
6807
  )}
6672
6808
  </div>
6673
6809
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myoperator-mcp",
3
- "version": "0.2.372",
3
+ "version": "0.2.373",
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",