@baishuyun/ai-design 2.0.45 → 2.0.48

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @baishuyun/ai-design
2
2
 
3
+ ## 2.0.48
4
+
5
+ ### Patch Changes
6
+
7
+ - fix style and other bugs
8
+ - Updated dependencies
9
+ - @baishuyun/ai-feat-utils@2.0.43
10
+ - @baishuyun/types@3.0.40
11
+
12
+ ## 2.0.47
13
+
14
+ ### Patch Changes
15
+
16
+ - fix agent helper style
17
+ - Updated dependencies
18
+ - @baishuyun/ai-feat-utils@2.0.42
19
+ - @baishuyun/types@3.0.39
20
+
21
+ ## 2.0.46
22
+
23
+ ### Patch Changes
24
+
25
+ - fix host style overwrite
26
+ - Updated dependencies
27
+ - @baishuyun/ai-feat-utils@2.0.41
28
+ - @baishuyun/types@3.0.38
29
+
3
30
  ## 2.0.45
4
31
 
5
32
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/ai-design",
3
- "version": "2.0.45",
3
+ "version": "2.0.48",
4
4
  "description": "AI 功能 UI 组件库 - 所有 AI 功能的 UI 表现唯一真相来源",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -60,8 +60,8 @@
60
60
  "tw-animate-css": "^1.4.0",
61
61
  "vaul": "^1.1.2",
62
62
  "zod": "^4.1.13",
63
- "@baishuyun/ai-feat-utils": "2.0.40",
64
- "@baishuyun/types": "3.0.37"
63
+ "@baishuyun/ai-feat-utils": "2.0.43",
64
+ "@baishuyun/types": "3.0.40"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "react": "^19.0.0",
@@ -84,7 +84,7 @@
84
84
  "vite": "^5.1.4",
85
85
  "vite-plugin-dts": "^4.5.4",
86
86
  "@baishuyun/tailwind-config": "3.0.9",
87
- "@baishuyun/types": "3.0.37",
87
+ "@baishuyun/types": "3.0.40",
88
88
  "@baishuyun/typescript-config": "2.0.9"
89
89
  },
90
90
  "keywords": [],
@@ -118,11 +118,11 @@ export const AgentHelperCard = ({
118
118
  )}
119
119
  >
120
120
  {/* Header */}
121
- <div className="grid-rows-[max-content] inline-grid leading-[0] place-items-start shrink-0 w-full">
121
+ <div className="grid-cols-1 grid-rows-[max-content] inline-grid leading-[0] place-items-start shrink-0 w-full">
122
122
  <div className="col-1 content-stretch flex h-[42px] items-center justify-between ml-0 mt-0 px-[12px] row-1 w-full">
123
- <div className="content-stretch flex gap-[6px] items-center shrink-0 min-w-0">
123
+ <div className="content-stretch flex gap-[6px] items-center min-w-0">
124
124
  <TriggerTypeIcon triggerType={helper.triggerType} size={18} />
125
- <p className="leading-[normal] shrink-0 text-[#030303] text-[14px] truncate">
125
+ <p className="leading-[normal] text-[#030303] text-[14px] truncate">
126
126
  {helper.helperName}
127
127
  </p>
128
128
  </div>
@@ -6,6 +6,7 @@ import { SquareChecker } from '../../square-checker';
6
6
  import type { IAppGroup, IAppGroupItem } from '@baishuyun/types';
7
7
  import { AppIcon } from '../../font-icon';
8
8
  import { SearchIcon } from '../../bs-icons';
9
+ import classNames from 'classnames';
9
10
 
10
11
  // ── Tree Node Type ────────────────────────────────────────────────
11
12
 
@@ -41,7 +42,10 @@ export function groupsToAppTreeNodes(groups: IAppGroup[]): AppTreeNode[] {
41
42
  /** Folder/group icon (16x16) */
42
43
  const FolderIcon = () => (
43
44
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
44
- <path d="M13.9004 15H2.65039C2.25257 15 1.87104 14.842 1.58973 14.5607C1.30843 14.2794 1.15039 13.8978 1.15039 13.5V4.5C1.15039 4.10218 1.30843 3.72064 1.58973 3.43934C1.87104 3.15804 2.25257 3 2.65039 3H7.15039L8.65039 4.5H13.9004C14.0974 4.5 14.2924 4.5388 14.4744 4.61418C14.6564 4.68956 14.8218 4.80005 14.9611 4.93934C15.1003 5.07863 15.2108 5.24399 15.2862 5.42597C15.3616 5.60796 15.4004 5.80302 15.4004 6H2.65039V13.5L4.25539 7.5H16.0754C16.1927 7.49595 16.3093 7.51947 16.4158 7.56867C16.5223 7.61788 16.6158 7.69139 16.6888 7.78331C16.7617 7.87523 16.8121 7.98298 16.8358 8.0979C16.8595 8.21282 16.856 8.33171 16.8254 8.445L15.3704 13.875C15.2865 14.1999 15.0961 14.4873 14.8296 14.6913C14.5631 14.8952 14.2359 15.0039 13.9004 15Z" fill="#6F81BD"/>
45
+ <path
46
+ d="M13.9004 15H2.65039C2.25257 15 1.87104 14.842 1.58973 14.5607C1.30843 14.2794 1.15039 13.8978 1.15039 13.5V4.5C1.15039 4.10218 1.30843 3.72064 1.58973 3.43934C1.87104 3.15804 2.25257 3 2.65039 3H7.15039L8.65039 4.5H13.9004C14.0974 4.5 14.2924 4.5388 14.4744 4.61418C14.6564 4.68956 14.8218 4.80005 14.9611 4.93934C15.1003 5.07863 15.2108 5.24399 15.2862 5.42597C15.3616 5.60796 15.4004 5.80302 15.4004 6H2.65039V13.5L4.25539 7.5H16.0754C16.1927 7.49595 16.3093 7.51947 16.4158 7.56867C16.5223 7.61788 16.6158 7.69139 16.6888 7.78331C16.7617 7.87523 16.8121 7.98298 16.8358 8.0979C16.8595 8.21282 16.856 8.33171 16.8254 8.445L15.3704 13.875C15.2865 14.1999 15.0961 14.4873 14.8296 14.6913C14.5631 14.8952 14.2359 15.0039 13.9004 15Z"
47
+ fill="#6F81BD"
48
+ />
45
49
  </svg>
46
50
  );
47
51
 
@@ -70,11 +74,20 @@ const FolderIcon = () => (
70
74
  /** Collapse/expand toggle (18x18) — shown on expandable parent nodes */
71
75
  const ToggleIcon = ({ expanded }: { expanded: boolean }) => (
72
76
  <svg
73
- width="18" height="18" viewBox="0 0 18 18" fill="none"
77
+ width="18"
78
+ height="18"
79
+ viewBox="0 0 18 18"
80
+ fill="none"
74
81
  className={cn('shrink-0', transCls)}
75
82
  style={{ transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
76
83
  >
77
- <path d="M6 8L9 11L12 8" stroke="#666666" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
84
+ <path
85
+ d="M6 8L9 11L12 8"
86
+ stroke="#666666"
87
+ strokeWidth="1.2"
88
+ strokeLinecap="round"
89
+ strokeLinejoin="round"
90
+ />
78
91
  </svg>
79
92
  );
80
93
 
@@ -97,11 +110,20 @@ const MoreIcon = () => (
97
110
  /** Dropdown chevron in trigger — matches Figma trigger icon */
98
111
  const DropdownChevron = ({ open }: { open: boolean }) => (
99
112
  <svg
100
- width="16" height="16" viewBox="0 0 16 16" fill="none"
113
+ width="16"
114
+ height="16"
115
+ viewBox="0 0 16 16"
116
+ fill="none"
101
117
  className={cn('shrink-0', transCls)}
102
118
  style={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }}
103
119
  >
104
- <path d="M4 6L8 10L12 6" stroke="#666666" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
120
+ <path
121
+ d="M4 6L8 10L12 6"
122
+ stroke="#666666"
123
+ strokeWidth="1.2"
124
+ strokeLinecap="round"
125
+ strokeLinejoin="round"
126
+ />
105
127
  </svg>
106
128
  );
107
129
 
@@ -130,6 +152,7 @@ export interface AppSelectProps {
130
152
  maxVisibleChips?: number;
131
153
  /** Additional className */
132
154
  className?: string;
155
+ contentCls?: string;
133
156
  /** Show a "全部应用" (All Apps) item at the top. Only effective in single mode. */
134
157
  showAll?: boolean;
135
158
  /** Allow selecting an empty value as the first option. Only effective in single mode. */
@@ -137,6 +160,8 @@ export interface AppSelectProps {
137
160
  /** Label for the empty option. Default "无". */
138
161
  emptyLabel?: string;
139
162
  popoverContentOnly?: boolean; // for testing: render only the popover content without the trigger
163
+
164
+ disableMaxHeight?: boolean;
140
165
  }
141
166
 
142
167
  // ── Helpers ───────────────────────────────────────────────────────
@@ -171,6 +196,8 @@ export const AppSelect = ({
171
196
  disabled = false,
172
197
  maxVisibleChips = 4,
173
198
  className,
199
+ contentCls,
200
+ disableMaxHeight,
174
201
  }: AppSelectProps) => {
175
202
  const [open, setOpen] = useState(false);
176
203
  const [searchQuery, setSearchQuery] = useState('');
@@ -181,6 +208,11 @@ export const AppSelect = ({
181
208
  const [triggerWidth, setTriggerWidth] = useState<number>(0);
182
209
  const triggerBtnRef = useRef<HTMLButtonElement | null>(null);
183
210
 
211
+ const maxTreeHeightCls = classNames({
212
+ 'max-h-[400px]': !disableMaxHeight,
213
+ 'max-h-full': disableMaxHeight,
214
+ });
215
+
184
216
  // Expanded node IDs (only nodes with children can be expanded)
185
217
  const [expandedIds, setExpandedIds] = useState<Set<string>>(() => {
186
218
  // Start with root nodes expanded
@@ -213,7 +245,7 @@ export const AppSelect = ({
213
245
 
214
246
  // Selected labels for trigger
215
247
  const selectedLabels = useMemo(() => {
216
- const ids = Array.isArray(resolvedValue) ? resolvedValue : (resolvedValue ? [resolvedValue] : []);
248
+ const ids = Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : [];
217
249
  return ids.map((id) => nodeMap.get(id)?.label ?? id);
218
250
  }, [resolvedValue, nodeMap]);
219
251
 
@@ -236,7 +268,10 @@ export const AppSelect = ({
236
268
  const labelMatch = node?.label?.toLowerCase().includes(q);
237
269
  const filteredChildren = node.children ? filterTree(node.children) : [];
238
270
  if (labelMatch || filteredChildren.length > 0) {
239
- result.push({ ...node, children: filteredChildren.length > 0 ? filteredChildren : node.children });
271
+ result.push({
272
+ ...node,
273
+ children: filteredChildren.length > 0 ? filteredChildren : node.children,
274
+ });
240
275
  }
241
276
  }
242
277
  return result;
@@ -249,11 +284,17 @@ export const AppSelect = ({
249
284
  const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
250
285
  triggerBtnRef.current = node;
251
286
  if (node) setTriggerWidth(node.offsetWidth);
252
- if (!node) { setPortalContainer(null); return; }
287
+ if (!node) {
288
+ setPortalContainer(null);
289
+ return;
290
+ }
253
291
  const rootNode = node.getRootNode();
254
292
  if (rootNode instanceof ShadowRoot) {
255
293
  const existing = rootNode.querySelector<HTMLElement>('[data-bs-app-select-portal]');
256
- if (existing) { setPortalContainer(existing); return; }
294
+ if (existing) {
295
+ setPortalContainer(existing);
296
+ return;
297
+ }
257
298
  const container = document.createElement('div');
258
299
  container.setAttribute('data-bs-app-select-portal', 'true');
259
300
  rootNode.appendChild(container);
@@ -354,9 +395,7 @@ export const AppSelect = ({
354
395
  'flex items-center gap-[6px] w-full h-[34px] rounded-[4px] text-left',
355
396
  'pr-[10px]',
356
397
  transCls,
357
- isSelected
358
- ? 'bg-[#F0F6FF] text-[#0265FF]'
359
- : 'text-[#030303] hover:bg-[#EFF0F6]'
398
+ isSelected ? 'bg-[#F0F6FF] text-[#0265FF]' : 'text-[#030303] hover:bg-[#EFF0F6]'
360
399
  )}
361
400
  style={{ paddingLeft: `${leftPad}px`, paddingTop: '10px', paddingBottom: '10px' }}
362
401
  >
@@ -376,7 +415,7 @@ export const AppSelect = ({
376
415
 
377
416
  {/* SquareChecker for multi-select on leaf nodes — always visible */}
378
417
  {mode === 'multiple' && !hasChildren && node.type !== 'group' && (
379
- <span onClick={(e) => e.stopPropagation()} className='grid place-items-center'>
418
+ <span onClick={(e) => e.stopPropagation()} className="grid place-items-center">
380
419
  <SquareChecker
381
420
  checked={isSelected}
382
421
  onCheckedChange={() => handleSelect(node.id, node)}
@@ -388,9 +427,7 @@ export const AppSelect = ({
388
427
 
389
428
  {/* Children (when expanded) */}
390
429
  {hasChildren && isExpanded && (
391
- <div>
392
- {node.children!.map((child) => renderNodeRow(child, depth + 1))}
393
- </div>
430
+ <div>{node.children!.map((child) => renderNodeRow(child, depth + 1))}</div>
394
431
  )}
395
432
  </div>
396
433
  );
@@ -403,70 +440,70 @@ export const AppSelect = ({
403
440
  const hasMultiSelection = mode === 'multiple' && multiSelectedLabels.length > 0;
404
441
 
405
442
  const popoverContent = (
406
- <div className="flex flex-col gap-[4px]">
407
- {/* Search */}
408
- <div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
409
- <SearchIcon />
410
- <input
411
- type="text"
412
- value={searchQuery}
413
- onChange={(e) => setSearchQuery(e.target.value)}
414
- placeholder={searchPlaceholder}
415
- className="flex-1 outline-none text-[14px] placeholder:text-[#999999] bg-transparent min-w-0 text-[#030303]"
416
- />
417
- </div>
418
-
419
- {/* Empty option */}
420
- {allowEmpty && mode === 'single' && !searchQuery.trim() && (
421
- <button
422
- type="button"
423
- onClick={() => {
424
- updateValue('');
425
- setOpen(false);
426
- }}
427
- className={cn(
428
- 'flex items-center w-full h-[34px] rounded-[4px] text-left',
429
- 'px-[10px]',
430
- transCls,
431
- 'text-[#030303] hover:bg-[#EFF0F6]'
432
- )}
433
- >
434
- <span className="text-[14px] leading-[normal]">{emptyLabel}</span>
435
- </button>
436
- )}
437
-
438
- {/* "全部应用" item */}
439
- {showAll && mode === 'single' && !searchQuery.trim() && (
440
- <button
441
- type="button"
442
- onClick={() => {
443
- updateValue(ALL_APPS_VALUE);
444
- setOpen(false);
445
- }}
446
- className={cn(
447
- 'flex items-center w-full h-[34px] rounded-[4px] text-left',
448
- 'px-[10px]',
449
- transCls,
450
- selectedIds.has(ALL_APPS_VALUE)
451
- ? 'bg-[#F0F6FF] text-[#0265FF]'
452
- : 'text-[#030303] hover:bg-[#EFF0F6]'
453
- )}
454
- >
455
- <span className="text-[14px] leading-[normal]">全部应用</span>
456
- </button>
457
- )}
458
-
459
- {/* Tree */}
460
- <div className="max-h-[400px] overflow-y-auto">
461
- {filteredNodes.length > 0 ? (
462
- filteredNodes.map((node) => renderNodeRow(node, 0))
463
- ) : (
464
- <div className="flex items-center justify-center h-[60px] text-[12px] text-[#999999]">
465
- {searchQuery.trim() ? '未找到相关应用' : '暂无应用'}
466
- </div>
467
- )}
468
- </div>
443
+ <div className={classNames('flex min-h-0 flex-col gap-[4px]', contentCls)}>
444
+ {/* Search */}
445
+ <div className="flex shrink-0 items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
446
+ <SearchIcon />
447
+ <input
448
+ type="text"
449
+ value={searchQuery}
450
+ onChange={(e) => setSearchQuery(e.target.value)}
451
+ placeholder={searchPlaceholder}
452
+ className="flex-1 outline-none text-[14px] placeholder:text-[#999999] bg-transparent min-w-0 text-[#030303]"
453
+ />
454
+ </div>
455
+
456
+ {/* Empty option */}
457
+ {allowEmpty && mode === 'single' && !searchQuery.trim() && (
458
+ <button
459
+ type="button"
460
+ onClick={() => {
461
+ updateValue('');
462
+ setOpen(false);
463
+ }}
464
+ className={cn(
465
+ 'flex shrink-0 items-center w-full h-[34px] rounded-[4px] text-left',
466
+ 'px-[10px]',
467
+ transCls,
468
+ 'text-[#030303] hover:bg-[#EFF0F6]'
469
+ )}
470
+ >
471
+ <span className="text-[14px] leading-[normal]">{emptyLabel}</span>
472
+ </button>
473
+ )}
474
+
475
+ {/* "全部应用" item */}
476
+ {showAll && mode === 'single' && !searchQuery.trim() && (
477
+ <button
478
+ type="button"
479
+ onClick={() => {
480
+ updateValue(ALL_APPS_VALUE);
481
+ setOpen(false);
482
+ }}
483
+ className={cn(
484
+ 'flex shrink-0 items-center w-full h-[34px] rounded-[4px] text-left',
485
+ 'px-[10px]',
486
+ transCls,
487
+ selectedIds.has(ALL_APPS_VALUE)
488
+ ? 'bg-[#F0F6FF] text-[#0265FF]'
489
+ : 'text-[#030303] hover:bg-[#EFF0F6]'
490
+ )}
491
+ >
492
+ <span className="text-[14px] leading-[normal]">全部应用</span>
493
+ </button>
494
+ )}
495
+
496
+ {/* Tree */}
497
+ <div className={classNames('min-h-0 overflow-y-auto', maxTreeHeightCls)}>
498
+ {filteredNodes.length > 0 ? (
499
+ filteredNodes.map((node) => renderNodeRow(node, 0))
500
+ ) : (
501
+ <div className="flex items-center justify-center h-[60px] text-[12px] text-[#999999]">
502
+ {searchQuery.trim() ? '未找到相关应用' : '暂无应用'}
469
503
  </div>
504
+ )}
505
+ </div>
506
+ </div>
470
507
  );
471
508
 
472
509
  if (popoverContentOnly) {
@@ -511,9 +548,7 @@ export const AppSelect = ({
511
548
  <span className="text-[14px] text-[#030303] truncate leading-[normal]">
512
549
  {label}
513
550
  </span>
514
- <span
515
- className="cursor-pointer flex items-center shrink-0 hover:bg-[#EFF0F6]"
516
- >
551
+ <span className="cursor-pointer flex items-center shrink-0 hover:bg-[#EFF0F6]">
517
552
  <CloseIcon />
518
553
  </span>
519
554
  </span>
@@ -548,8 +583,8 @@ export const AppSelect = ({
548
583
  align="start"
549
584
  sideOffset={4}
550
585
  portalContainer={portalContainer}
551
- className="!border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]"
552
- style={{ minWidth: triggerWidth || undefined }}
586
+ className="flex flex-col overflow-hidden !border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]"
587
+ style={{ minWidth: triggerWidth || undefined, maxHeight: 300 }}
553
588
  >
554
589
  {popoverContent}
555
590
  </PopoverContent>
@@ -13,6 +13,8 @@ import { transCls } from '../../../const/ui';
13
13
  import { SolidArrowDownIcon, SearchIcon } from '../../bs-icons';
14
14
  import { useSelectPopoverPosition } from '../../../hooks/use-select-popover-position';
15
15
 
16
+ const POPOVER_MAX_HEIGHT = 300;
17
+
16
18
  const FolderIcon = () => (
17
19
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
18
20
  <path d="M13.9004 15H2.65039C2.25257 15 1.87104 14.842 1.58973 14.5607C1.30843 14.2794 1.15039 13.8978 1.15039 13.5V4.5C1.15039 4.10218 1.30843 3.72064 1.58973 3.43934C1.87104 3.15804 2.25257 3 2.65039 3H7.15039L8.65039 4.5H13.9004C14.0974 4.5 14.2924 4.5388 14.4744 4.61418C14.6564 4.68956 14.8218 4.80005 14.9611 4.93934C15.1003 5.07863 15.2108 5.24399 15.2862 5.42597C15.3616 5.60796 15.4004 5.80302 15.4004 6H2.65039V13.5L4.25539 7.5H16.0754C16.1927 7.49595 16.3093 7.51947 16.4158 7.56867C16.5223 7.61788 16.6158 7.69139 16.6888 7.78331C16.7617 7.87523 16.8121 7.98298 16.8358 8.0979C16.8595 8.21282 16.856 8.33171 16.8254 8.445L15.3704 13.875C15.2865 14.1999 15.0961 14.4873 14.8296 14.6913C14.5631 14.8952 14.2359 15.0039 13.9004 15Z" fill="#6F81BD"/>
@@ -307,11 +309,12 @@ function FormPopover({
307
309
  <div
308
310
  className={cn(
309
311
  'bg-white rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]',
310
- 'flex flex-col gap-[4px]',
312
+ 'flex min-h-0 flex-col gap-[4px] overflow-hidden',
311
313
  )}
314
+ style={{ maxHeight: POPOVER_MAX_HEIGHT }}
312
315
  >
313
316
  {/* Search input */}
314
- <div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
317
+ <div className="flex shrink-0 items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
315
318
  <SearchIcon />
316
319
  <input
317
320
  type="text"
@@ -337,7 +340,7 @@ function FormPopover({
337
340
  )}
338
341
 
339
342
  {/* Form list */}
340
- <div className="flex flex-col gap-0 max-h-[400px] overflow-y-auto">
343
+ <div className="flex min-h-0 flex-col gap-0 overflow-y-auto">
341
344
  {tree.length > 0 ? (
342
345
  tree.map((node) => (
343
346
  <FormTreeItemNode
@@ -391,7 +394,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
391
394
  const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
392
395
 
393
396
  const { popoverStyle, updatePopoverPosition } =
394
- useSelectPopoverPosition(triggerRef);
397
+ useSelectPopoverPosition(triggerRef, { estimatedHeight: POPOVER_MAX_HEIGHT });
395
398
 
396
399
  const resolvedValue = controlledValue ?? internalValue;
397
400
 
@@ -709,7 +712,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
709
712
  {/* Popover */}
710
713
  {isOpen &&
711
714
  createPortal(
712
- <div ref={popoverRef} style={popoverStyle}>
715
+ <div ref={popoverRef} className="flex flex-col" style={popoverStyle}>
713
716
  <FormPopover
714
717
  searchQuery={searchQuery}
715
718
  onSearchChange={setSearchQuery}
@@ -8,11 +8,11 @@ export interface FieldsPortalProps extends FieldsPreviewerProps {
8
8
  children?: React.ReactNode;
9
9
  }
10
10
 
11
- export const FieldsPortal = ({ portalEl, children, ...rest }: FieldsPortalProps): ReactPortal => {
11
+ export const FieldsPortal = ({ portalEl, children, ...rest }: FieldsPortalProps): ReactPortal | null => {
12
12
  const { shadowContainer: shadowEntry } = useShadow({
13
13
  rootEl: portalEl,
14
14
  id: 'chat-sdk-fields-portal',
15
15
  });
16
16
 
17
- return createPortal(<FieldsPreviewer {...rest}>{children}</FieldsPreviewer>, shadowEntry!);
17
+ return shadowEntry ? createPortal(<FieldsPreviewer {...rest}>{children}</FieldsPreviewer>, shadowEntry) : null;
18
18
  };
@@ -117,6 +117,7 @@ export const FieldsPreviewer = forwardRef<HTMLDivElement, FieldsPreviewerProps>(
117
117
 
118
118
  const { shadowContainer } = useShadow({
119
119
  id: 'dialog-container',
120
+ enabled: !disableShadowContainer,
120
121
  rootEl: dialogContainer,
121
122
  style: containerStyle,
122
123
  wrapperStyle: {
@@ -1,5 +1,7 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { LegacySlotRenderer } from '../types';
3
+ import { syncHostStylesIntoShadow } from '@baishuyun/ai-feat-utils';
4
+ import { useInjectedShadowStyle } from '../hooks/use-shadow-boundary';
3
5
 
4
6
  export interface LegacySlotProps {
5
7
  renderLegacy: LegacySlotRenderer;
@@ -10,8 +12,6 @@ export interface LegacySlotProps {
10
12
  onSetRef?: (ref: HTMLDivElement) => void;
11
13
  }
12
14
 
13
- const STYLE_INJECTED_ATTR = 'data-style-injected';
14
-
15
15
  const injectStylesIntoShadow = (currentElement: Node) => {
16
16
  if (!currentElement) return;
17
17
 
@@ -31,20 +31,8 @@ const injectStylesIntoShadow = (currentElement: Node) => {
31
31
  }
32
32
 
33
33
  if (!shadowRoot) return;
34
- if (shadowRoot.querySelector(`style[${STYLE_INJECTED_ATTR}]`)) return;
35
-
36
- document.head.querySelectorAll('style').forEach((styleEl) => {
37
- const cloned = styleEl.cloneNode(true) as HTMLElement;
38
- cloned.setAttribute(STYLE_INJECTED_ATTR, '');
39
- shadowRoot!.appendChild(cloned);
40
- });
41
-
42
34
  // if style is loaded by link tag, we also need to copy it into shadow DOM
43
- document.head.querySelectorAll('link[rel="stylesheet"]').forEach((linkEl) => {
44
- const cloned = linkEl.cloneNode(true) as HTMLElement;
45
- cloned.setAttribute(STYLE_INJECTED_ATTR, '');
46
- shadowRoot!.appendChild(cloned);
47
- });
35
+ return syncHostStylesIntoShadow(shadowRoot, shadowRoot.host as HTMLElement);
48
36
  };
49
37
 
50
38
  export function LegacySlot({
@@ -66,13 +54,14 @@ export function LegacySlot({
66
54
  const host = hostRef.current;
67
55
  onSetRef?.(host);
68
56
  let target: HTMLElement | ShadowRoot = host;
57
+ let disposeStyles: (() => void) | undefined;
69
58
 
70
59
  if (shadow) {
71
60
  const shadowRoot = host.shadowRoot || host.attachShadow({ mode: 'open' });
72
61
  target = shadowRoot;
73
62
 
74
63
  if (withHostStyles) {
75
- injectStylesIntoShadow(host);
64
+ host.setAttribute('data-bs-style-mode', 'host');
76
65
  }
77
66
  if (typeof injectedStyles === 'string') {
78
67
  const styleEl = document.createElement('style');
@@ -86,12 +75,16 @@ export function LegacySlot({
86
75
 
87
76
  const mountPoint = document.createElement('div');
88
77
  mountPoint.style.display = 'contents';
78
+ mountPoint.setAttribute('data-bs-shadow-content', '');
89
79
  target.appendChild(mountPoint);
90
80
  mountPointRef.current = mountPoint;
81
+ if (shadow && withHostStyles) disposeStyles = injectStylesIntoShadow(host);
91
82
 
92
83
  cleanupRef.current = renderLegacy(mountPoint, props as Record<string, any>);
93
84
 
94
85
  return () => {
86
+ disposeStyles?.();
87
+ host.shadowRoot?.querySelector('style[data-legacy-injected-raw]')?.remove();
95
88
  cleanupRef.current?.();
96
89
  if (mountPointRef.current) {
97
90
  mountPointRef.current.remove();
@@ -101,6 +94,8 @@ export function LegacySlot({
101
94
  };
102
95
  }, []);
103
96
 
97
+ useInjectedShadowStyle(hostRef, injectedStyles, 'data-legacy-injected-raw');
98
+
104
99
  useEffect(() => {
105
100
  if (!mountPointRef.current) return;
106
101
  cleanupRef.current = renderLegacy(mountPointRef.current, props as Record<string, any>);
@@ -210,6 +210,10 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
210
210
  (f) => (matchSearch(f.fieldDesc) || matchSearch(f.fieldName)) && f.type !== 'subform'
211
211
  );
212
212
 
213
+ const usedSourceFieldNames = new Set(
214
+ value.map(item => item.sourceField.fieldName).filter(Boolean)
215
+ );
216
+
213
217
  const searchedTargetFields = filterFields(targetOptions);
214
218
 
215
219
  const fieldRowCls = cn(
@@ -254,6 +258,9 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
254
258
  const selectedSourceType = item.sourceField.fieldName
255
259
  ? flatSourceFields.find(f => f.fieldName === item.sourceField.fieldName)?.type
256
260
  : undefined;
261
+ const rowSourceFields = filteredSourceFields.filter(
262
+ f => !usedSourceFieldNames.has(f.fieldName) || f.fieldName === item.sourceField.fieldName
263
+ );
257
264
  const rowTargetFields = selectedSourceType
258
265
  ? filterFieldsByType(searchedTargetFields, selectedSourceType)
259
266
  : searchedTargetFields;
@@ -304,7 +311,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
304
311
  />
305
312
  </div>
306
313
  <div className="max-h-[210px] overflow-auto">
307
- {filteredSourceFields.map((field) => (
314
+ {rowSourceFields.map((field) => (
308
315
  <button
309
316
  key={field.fieldName}
310
317
  type="button"
@@ -320,7 +327,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
320
327
  <span className="truncate">{field.fieldDesc}</span>
321
328
  </button>
322
329
  ))}
323
- {filteredSourceFields.length === 0 && (
330
+ {rowSourceFields.length === 0 && (
324
331
  <div className="px-[10px] py-[7px] text-[14px] text-[#999] leading-normal">
325
332
  无匹配结果
326
333
  </div>
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useShadowBoundary } from '../hooks/use-shadow-boundary';
4
+ import type { ShadowStyle, ShadowStyleMode } from '../hooks/use-shadow-boundary';
5
+
6
+ export interface ShadowContentProps {
7
+ children: ReactNode;
8
+ mode?: ShadowStyleMode;
9
+ styles?: ShadowStyle;
10
+ }
11
+
12
+ export const ShadowContent = ({ children, mode = 'sdk', styles }: ShadowContentProps) => {
13
+ const { setHostRef, container } = useShadowBoundary(mode, styles);
14
+ return (
15
+ <div ref={setHostRef} data-bs-style-mode={mode} style={{ display: 'contents' }}>
16
+ {container && createPortal(children, container)}
17
+ </div>
18
+ );
19
+ };
@@ -0,0 +1,23 @@
1
+ import { useLayoutEffect, type ElementType, type ReactNode } from 'react';
2
+ import { usePortalContainer } from '../hooks/use-portal-container';
3
+
4
+ export const ShadowPortal = ({ component: Portal, container, ...props }: {
5
+ component: ElementType;
6
+ container?: Element | DocumentFragment | null;
7
+ children?: ReactNode;
8
+ forceMount?: true;
9
+ 'data-slot'?: string;
10
+ }) => {
11
+ const { portalContainer, setTriggerRef, portalReady } = usePortalContainer('data-bs-portal-container');
12
+ useLayoutEffect(() => {
13
+ if (container instanceof Element && container.parentNode instanceof ShadowRoot) {
14
+ container.setAttribute('data-bs-portal-container', '');
15
+ }
16
+ }, [container]);
17
+ return (
18
+ <>
19
+ <span ref={setTriggerRef} style={{ display: 'none' }} aria-hidden />
20
+ {portalReady && <Portal {...props} container={container === undefined ? portalContainer ?? undefined : container} />}
21
+ </>
22
+ );
23
+ };
@@ -2,12 +2,15 @@ import * as React from 'react';
2
2
  import * as DialogPrimitive from '@radix-ui/react-dialog';
3
3
 
4
4
  import { cn } from '../../lib/utils';
5
+ import { ShadowPortal } from '../shadow-portal';
5
6
 
6
7
  const Dialog = DialogPrimitive.Root;
7
8
 
8
9
  const DialogTrigger = DialogPrimitive.Trigger;
9
10
 
10
- const DialogPortal = DialogPrimitive.Portal;
11
+ const DialogPortal = (props: React.ComponentProps<typeof DialogPrimitive.Portal>) => (
12
+ <ShadowPortal component={DialogPrimitive.Portal} {...props} />
13
+ );
11
14
 
12
15
  const DialogClose = DialogPrimitive.Close;
13
16
 
@@ -4,6 +4,7 @@ import { Drawer as DrawerPrimitive } from "vaul"
4
4
  import { cn } from "../../lib/utils"
5
5
  import { IconBtn } from "../icon-btn"
6
6
  import { CloseIcon } from "../bs-icons"
7
+ import { ShadowPortal } from '../shadow-portal';
7
8
 
8
9
  function Drawer({
9
10
  ...props
@@ -24,7 +25,8 @@ function DrawerPortal({
24
25
  container?: HTMLElement | null;
25
26
  }) {
26
27
  return (
27
- <DrawerPrimitive.Portal
28
+ <ShadowPortal
29
+ component={DrawerPrimitive.Portal}
28
30
  data-slot="drawer-portal"
29
31
  container={container ?? undefined}
30
32
  {...props}
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
  import * as PopoverPrimitive from '@radix-ui/react-popover';
3
3
 
4
4
  import { cn } from '../../lib/utils';
5
+ import { ShadowPortal } from '../shadow-portal';
5
6
 
6
7
  const Popover = PopoverPrimitive.Root;
7
8
 
@@ -52,7 +53,7 @@ const PopoverContent = React.forwardRef<
52
53
  });
53
54
 
54
55
  return (
55
- <PopoverPrimitive.Portal container={portalContainer ?? undefined}>
56
+ <ShadowPortal component={PopoverPrimitive.Portal} container={portalContainer ?? undefined}>
56
57
  <PopoverPrimitive.Content
57
58
  ref={ref}
58
59
  align={align}
@@ -76,7 +77,7 @@ const PopoverContent = React.forwardRef<
76
77
  }}
77
78
  {...props}
78
79
  />
79
- </PopoverPrimitive.Portal>
80
+ </ShadowPortal>
80
81
  );
81
82
  }
82
83
  );
@@ -3,6 +3,7 @@ import * as SelectPrimitive from '@radix-ui/react-select';
3
3
  import { ChevronDown, ChevronUp } from 'lucide-react';
4
4
 
5
5
  import { cn } from '../../lib/utils';
6
+ import { ShadowPortal } from '../shadow-portal';
6
7
 
7
8
  const Select = SelectPrimitive.Root;
8
9
 
@@ -112,7 +113,7 @@ const SelectContent = React.forwardRef<
112
113
  {containerProp === undefined && (
113
114
  <span ref={setDetectorRef} style={{ display: 'none' }} aria-hidden />
114
115
  )}
115
- <SelectPrimitive.Portal container={effectiveContainer ?? undefined}>
116
+ <ShadowPortal component={SelectPrimitive.Portal} container={effectiveContainer ?? undefined}>
116
117
  <SelectPrimitive.Content
117
118
  ref={ref}
118
119
  className={cn(
@@ -161,7 +162,7 @@ const SelectContent = React.forwardRef<
161
162
  </SelectPrimitive.Viewport>
162
163
  <SelectScrollDownButton />
163
164
  </SelectPrimitive.Content>
164
- </SelectPrimitive.Portal>
165
+ </ShadowPortal>
165
166
  </>
166
167
  );
167
168
  }
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3
3
 
4
4
  import { cn } from '../../lib/utils';
5
+ import { ShadowPortal } from '../shadow-portal';
5
6
 
6
7
  const TooltipZIndexContext = React.createContext<number | undefined>(undefined);
7
8
 
@@ -30,7 +31,7 @@ const TooltipContent = React.forwardRef<
30
31
  const zIndexFromProvider = React.useContext(TooltipZIndexContext);
31
32
 
32
33
  return (
33
- <TooltipPrimitive.Portal container={portalContainer ?? undefined}>
34
+ <ShadowPortal component={TooltipPrimitive.Portal} container={portalContainer ?? undefined}>
34
35
  <TooltipPrimitive.Content
35
36
  ref={ref}
36
37
  sideOffset={sideOffset}
@@ -44,7 +45,7 @@ const TooltipContent = React.forwardRef<
44
45
  {props.children}
45
46
  <TooltipPrimitive.Arrow className="fill-[#525867] stroke-[#525867] stroke-[2px] stroke-linejoin-round [paint-order:stroke_fill]" />
46
47
  </TooltipPrimitive.Content>
47
- </TooltipPrimitive.Portal>
48
+ </ShadowPortal>
48
49
  );
49
50
  });
50
51
 
@@ -0,0 +1,12 @@
1
+ export const scopeSdkStyles = (css: string) => css.replace(/:host(?![-(])/g, ':is(:host, :host > div, [data-bs-shadow-content], [data-bs-portal-container])');
2
+
3
+ export const SDK_SHADOW_BASELINE = `
4
+ :host, :host > div {
5
+ font: 14px/1.5 ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
6
+ color: #121111;
7
+ letter-spacing: normal;
8
+ text-align: start;
9
+ text-transform: none;
10
+ color-scheme: light;
11
+ }
12
+ `;
@@ -2,7 +2,9 @@ import { useCallback, useState } from 'react';
2
2
 
3
3
  export const usePortalContainer = (portalName: string) => {
4
4
  const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
5
+ const [portalReady, setPortalReady] = useState(false);
5
6
  const setTriggerRef = useCallback((node: HTMLElement | null) => {
7
+ setPortalReady(!!node);
6
8
  if (!node) {
7
9
  setPortalContainer(null);
8
10
  return;
@@ -14,14 +16,15 @@ export const usePortalContainer = (portalName: string) => {
14
16
  setPortalContainer(existing);
15
17
  return;
16
18
  }
17
- const container = document.createElement('div');
19
+ const container = node.ownerDocument.createElement('div');
18
20
  container.setAttribute(portalName, 'true');
21
+ container.setAttribute('data-bs-portal-container', '');
19
22
  rootNode.appendChild(container);
20
23
  setPortalContainer(container);
21
24
  } else {
22
25
  setPortalContainer(null);
23
26
  }
24
- }, []);
27
+ }, [portalName]);
25
28
 
26
- return { portalContainer, setTriggerRef };
29
+ return { portalContainer, setTriggerRef, portalReady };
27
30
  };
@@ -0,0 +1,64 @@
1
+ import { useEffect, useLayoutEffect, useState, type RefObject } from 'react';
2
+ import { syncHostStylesIntoShadow } from '@baishuyun/ai-feat-utils';
3
+ import { SDK_SHADOW_BASELINE, scopeSdkStyles } from '../const/shadow';
4
+
5
+ export type ShadowStyleMode = 'sdk' | 'host';
6
+ export type ShadowStyle = string | (() => string);
7
+
8
+ export const useShadowBoundary = (mode: ShadowStyleMode, styles?: ShadowStyle) => {
9
+ const [host, setHostRef] = useState<HTMLDivElement | null>(null);
10
+ const [boundary, setBoundary] = useState<{
11
+ root: ShadowRoot;
12
+ container: HTMLDivElement;
13
+ style: HTMLStyleElement;
14
+ } | null>(null);
15
+
16
+ useLayoutEffect(() => {
17
+ if (!host) return;
18
+ const root = host.shadowRoot || host.attachShadow({ mode: 'open' });
19
+ const style = host.ownerDocument.createElement('style');
20
+ const container = host.ownerDocument.createElement('div');
21
+ container.setAttribute('data-bs-shadow-content', '');
22
+ container.style.display = 'contents';
23
+ root.append(style, container);
24
+ setBoundary({ root, container, style });
25
+ return () => {
26
+ style.remove();
27
+ container.remove();
28
+ setBoundary(null);
29
+ };
30
+ }, [host]);
31
+
32
+ useLayoutEffect(() => {
33
+ if (!boundary || !host) return;
34
+ host.setAttribute('data-bs-style-mode', mode);
35
+ if (mode === 'sdk') boundary.container.className = '';
36
+ return mode === 'host' ? syncHostStylesIntoShadow(boundary.root, host) : undefined;
37
+ }, [boundary, host, mode]);
38
+
39
+ useLayoutEffect(() => {
40
+ if (!boundary) return;
41
+ boundary.style.toggleAttribute('data-bs-sdk-styles', mode === 'sdk');
42
+ boundary.style.toggleAttribute('data-bs-custom-styles', mode === 'host');
43
+ const css = typeof styles === 'function' ? styles() : styles || '';
44
+ boundary.style.textContent = mode === 'sdk' ? SDK_SHADOW_BASELINE + scopeSdkStyles(css) : css;
45
+ }, [boundary, mode, styles]);
46
+
47
+ return { setHostRef, container: boundary?.container ?? null };
48
+ };
49
+
50
+ export const useInjectedShadowStyle = (host: RefObject<HTMLElement | null>, css: string | undefined, attribute: string) => {
51
+ useEffect(() => {
52
+ const root = host.current?.shadowRoot;
53
+ if (!root) return;
54
+ let style = root.querySelector<HTMLStyleElement>(`style[${attribute}]`);
55
+ if (!css) { style?.remove(); return; }
56
+ if (!style) {
57
+ style = root.ownerDocument.createElement('style');
58
+ style.setAttribute(attribute, '');
59
+ root.appendChild(style);
60
+ }
61
+ style.textContent = css;
62
+ return () => style?.remove();
63
+ }, [host, css, attribute]);
64
+ };
@@ -1,4 +1,6 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
+ import { SDK_SHADOW_BASELINE, scopeSdkStyles } from '../const/shadow';
3
+ import sdkStyles from '../style.css?inline';
2
4
 
3
5
  interface UseShadowOptions {
4
6
  /** Shadow DOM挂载的根元素(默认挂到body) */
@@ -15,6 +17,7 @@ interface UseShadowOptions {
15
17
  extraHtml?: string;
16
18
 
17
19
  id: string;
20
+ enabled?: boolean;
18
21
  }
19
22
 
20
23
  /**
@@ -24,6 +27,7 @@ interface UseShadowOptions {
24
27
  */
25
28
  export const useShadow = ({
26
29
  id,
30
+ enabled = true,
27
31
  style = '',
28
32
  rootEl,
29
33
  containerClasses = [],
@@ -57,10 +61,13 @@ export const useShadow = ({
57
61
  wrapperRef.current = null;
58
62
  }
59
63
 
64
+ if (!enabled) return;
65
+
60
66
  try {
61
67
  // 1. 创建外层包裹元素(挂载Shadow Root的载体)
62
68
  const wrapperEl = document.createElement('div');
63
- wrapperEl.classList.add(...wrapperClasses, 'wrapper');
69
+ wrapperEl.classList.add(...wrapperClasses, 'bs-sdk-shadow-wrapper');
70
+ wrapperEl.setAttribute('data-bs-style-mode', 'sdk');
64
71
  // wrapperEl.style.height = '100%';
65
72
  wrapperRef.current = wrapperEl;
66
73
 
@@ -81,15 +88,15 @@ export const useShadow = ({
81
88
  }
82
89
 
83
90
  // 3. 注入样式到Shadow DOM
84
- if (style) {
85
- const styleEl = document.createElement('style');
86
- styleEl.textContent = style;
87
- shadowRoot.appendChild(styleEl);
88
- }
91
+ const styleEl = document.createElement('style');
92
+ styleEl.setAttribute('data-bs-sdk-styles', '');
93
+ styleEl.textContent = SDK_SHADOW_BASELINE + scopeSdkStyles(sdkStyles + style);
94
+ shadowRoot.appendChild(styleEl);
89
95
 
90
96
  // 4. 创建Shadow内部的容器(最终挂载React Portal的元素)
91
97
  const containerEl = document.createElement('div');
92
98
  containerEl.classList.add(...containerClasses, 'shadow-container');
99
+ containerEl.setAttribute('data-bs-shadow-content', '');
93
100
  shadowRoot.appendChild(containerEl);
94
101
 
95
102
  // 5. 将外层元素挂载到指定根节点
@@ -109,7 +116,17 @@ export const useShadow = ({
109
116
  // wrapperRef.current = null;
110
117
  // }
111
118
  // };
112
- }, [rootEl]); // Recreate when rootEl changes
119
+ return () => {
120
+ wrapperRef.current?.remove();
121
+ wrapperRef.current = null;
122
+ setShadowContainer(null);
123
+ };
124
+ }, [rootEl, enabled]); // Recreate when rootEl changes
125
+
126
+ useEffect(() => {
127
+ const styleEl = wrapperRef.current?.shadowRoot?.querySelector('style[data-bs-sdk-styles]');
128
+ if (styleEl) styleEl.textContent = SDK_SHADOW_BASELINE + scopeSdkStyles(sdkStyles + style);
129
+ }, [style, shadowContainer]);
113
130
 
114
131
  if (shadowContainer) {
115
132
  shadowContainer.id = id;
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ export { cn, copyTextToClipboard, toHumanReadable, isSubformFieldEqual } from '.
3
3
 
4
4
  // Hooks
5
5
  export { useShadow } from './hooks/use-shadow';
6
+ export { ShadowContent } from './components/shadow-content';
7
+ export type { ShadowContentProps } from './components/shadow-content';
6
8
 
7
9
  // Constants
8
10
  export {
@@ -1,5 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
3
4
  import { AppSelect, groupsToAppTreeNodes } from '../components/agent-helper/app-select';
4
5
  import type { AppTreeNode } from '../components/agent-helper/app-select';
5
6
  import type { IAppGroup } from '@baishuyun/types';
@@ -125,6 +126,49 @@ type Story = StoryObj<typeof meta>;
125
126
 
126
127
  export const Default: Story = {};
127
128
 
129
+ export const ConstrainedHeight: Story = {
130
+ args: { allowEmpty: true, showAll: true },
131
+ play: async ({ canvasElement }) => {
132
+ await userEvent.click(within(canvasElement).getByRole('button'));
133
+ const popover = await within(canvasElement.ownerDocument.body).findByRole('dialog');
134
+ await userEvent.clear(within(popover).getByRole('textbox'));
135
+ const content = popover.firstElementChild as HTMLElement;
136
+ const tree = content.lastElementChild as HTMLElement;
137
+ await waitFor(() => {
138
+ expect(popover.getBoundingClientRect().height).toBeLessThanOrEqual(parseFloat(getComputedStyle(popover).maxHeight));
139
+ expect(content.getBoundingClientRect().bottom).toBeLessThanOrEqual(popover.getBoundingClientRect().bottom);
140
+ expect(popover.scrollHeight).toBeLessThanOrEqual(popover.clientHeight);
141
+ expect(tree.scrollHeight).toBeGreaterThan(tree.clientHeight);
142
+ expect(content.firstElementChild!.getBoundingClientRect().height).toBe(30);
143
+ expect(within(popover).getByRole('button', { name: '无' }).getBoundingClientRect().height).toBe(34);
144
+ expect(within(popover).getByRole('button', { name: '全部应用' }).getBoundingClientRect().height).toBe(34);
145
+ });
146
+ tree.scrollTop = tree.scrollHeight;
147
+ expect(tree.scrollTop).toBeGreaterThan(0);
148
+ await userEvent.type(within(popover).getByRole('textbox'), 'API接口管理');
149
+ await userEvent.click(within(popover).getByRole('button', { name: 'API接口管理' }));
150
+ expect(within(canvasElement).getByRole('button')).toHaveTextContent('API接口管理');
151
+ },
152
+ };
153
+
154
+ export const EmbeddedConstrainedHeight: Story = {
155
+ args: { popoverContentOnly: true, disableMaxHeight: true, showAll: true, contentCls: 'h-full' },
156
+ render: (args) => <div style={{ height: 200 }}><AppSelect {...args} /></div>,
157
+ play: async ({ canvasElement }) => {
158
+ const content = within(canvasElement).getByRole('textbox').parentElement!.parentElement!;
159
+ const tree = content.lastElementChild as HTMLElement;
160
+ await waitFor(() => {
161
+ expect(content.getBoundingClientRect().height).toBe(200);
162
+ expect(tree.getBoundingClientRect().bottom).toBeLessThanOrEqual(content.getBoundingClientRect().bottom);
163
+ expect(tree.scrollHeight).toBeGreaterThan(tree.clientHeight);
164
+ expect(within(content).getByRole('button', { name: '全部应用' }).getBoundingClientRect().height).toBe(34);
165
+ });
166
+ expect(content.firstElementChild!.getBoundingClientRect().height).toBe(30);
167
+ tree.scrollTop = tree.scrollHeight;
168
+ expect(tree.scrollTop).toBeGreaterThan(0);
169
+ },
170
+ };
171
+
128
172
  export const SingleSelected: Story = {
129
173
  args: { defaultValue: 'app2' },
130
174
  };
@@ -1,5 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
3
4
  import { FormSelect } from '../components/agent-helper/form-select';
4
5
  import type { FormSelectItem, FormSelectChangeResult } from '../components/agent-helper/form-select';
5
6
 
@@ -91,6 +92,45 @@ export const ManyItems: Story = {
91
92
  },
92
93
  };
93
94
 
95
+ export const ConstrainedHeight: Story = {
96
+ args: { items: manyFormItems, allowEmpty: true },
97
+ play: async ({ canvasElement, args }) => {
98
+ if (!args.popoverOnly) {
99
+ await userEvent.click(within(canvasElement).getByText(/^(无|测试表单 30)$/));
100
+ }
101
+ const search = await within(canvasElement.ownerDocument.body).findByRole('textbox');
102
+ await userEvent.clear(search);
103
+ const content = search.parentElement!.parentElement!;
104
+ const list = content.lastElementChild as HTMLElement;
105
+ await waitFor(() => {
106
+ expect(content.getBoundingClientRect().height).toBeLessThanOrEqual(300);
107
+ expect(content.scrollHeight).toBeLessThanOrEqual(content.clientHeight);
108
+ expect(search.parentElement!.getBoundingClientRect().height).toBe(30);
109
+ expect(within(content).getByText('无').parentElement!.getBoundingClientRect().height).toBe(34);
110
+ expect(list.scrollHeight).toBeGreaterThan(list.clientHeight);
111
+ if (!args.popoverOnly) {
112
+ const portal = content.parentElement!;
113
+ expect(parseFloat(getComputedStyle(portal).maxHeight)).toBeLessThanOrEqual(300);
114
+ expect(content.getBoundingClientRect().bottom).toBeLessThanOrEqual(portal.getBoundingClientRect().bottom);
115
+ }
116
+ });
117
+ const searchTop = search.getBoundingClientRect().top;
118
+ list.scrollTop = list.scrollHeight;
119
+ expect(list.scrollTop).toBeGreaterThan(0);
120
+ expect(search.getBoundingClientRect().top).toBe(searchTop);
121
+ await userEvent.type(search, '测试表单 30');
122
+ await userEvent.click(within(content).getByText('测试表单 30'));
123
+ if (!args.popoverOnly) {
124
+ expect(within(canvasElement).getByText('测试表单 30')).toBeVisible();
125
+ }
126
+ },
127
+ };
128
+
129
+ export const PopoverOnlyConstrainedHeight: Story = {
130
+ ...ConstrainedHeight,
131
+ args: { ...ConstrainedHeight.args, popoverOnly: true },
132
+ };
133
+
94
134
  export const Empty: Story = {
95
135
  args: {
96
136
  items: [],
package/src/style.css CHANGED
@@ -82,7 +82,34 @@
82
82
  --color-sidebar-border: var(--sidebar-border);
83
83
  --color-sidebar-ring: var(--sidebar-ring);
84
84
 
85
- --radius: 0.625rem;
85
+ --spacing: 3.5px;
86
+ --radius: 8.75px;
87
+ --text-xs: 10.5px;
88
+ --text-xs--line-height: 14px;
89
+ --text-sm: 12.25px;
90
+ --text-sm--line-height: 17.5px;
91
+ --text-base: 14px;
92
+ --text-base--line-height: 21px;
93
+ --text-lg: 15.75px;
94
+ --text-lg--line-height: 24.5px;
95
+ --text-xl: 17.5px;
96
+ --text-xl--line-height: 24.5px;
97
+ --text-2xl: 21px;
98
+ --text-2xl--line-height: 28px;
99
+ --text-3xl: 26.25px;
100
+ --text-3xl--line-height: 31.5px;
101
+ --text-4xl: 31.5px;
102
+ --text-4xl--line-height: 35px;
103
+ --text-5xl: 42px;
104
+ --text-5xl--line-height: 1;
105
+ --text-6xl: 52.5px;
106
+ --text-6xl--line-height: 1;
107
+ --text-7xl: 63px;
108
+ --text-7xl--line-height: 1;
109
+ --text-8xl: 84px;
110
+ --text-8xl--line-height: 1;
111
+ --text-9xl: 112px;
112
+ --text-9xl--line-height: 1;
86
113
  --background: oklch(1 0 0);
87
114
  --foreground: oklch(0.129 0.042 264.695);
88
115
  --card: oklch(1 0 0);
package/style.css CHANGED
@@ -812,6 +812,9 @@
812
812
  .max-h-\[600px\] {
813
813
  max-height: 600px;
814
814
  }
815
+ .max-h-full {
816
+ max-height: 100%;
817
+ }
815
818
  .min-h-0 {
816
819
  min-height: 0px;
817
820
  }
@@ -1149,6 +1152,9 @@
1149
1152
  .\[appearance\:textfield\] {
1150
1153
  appearance: textfield;
1151
1154
  }
1155
+ .grid-cols-1 {
1156
+ grid-template-columns: repeat(1, minmax(0, 1fr));
1157
+ }
1152
1158
  .grid-cols-3 {
1153
1159
  grid-template-columns: repeat(3, minmax(0, 1fr));
1154
1160
  }