@baishuyun/ai-design 2.0.47 → 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 +9 -0
- package/package.json +4 -4
- package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
- package/src/components/agent-helper/app-select/index.tsx +7 -7
- package/src/components/agent-helper/form-select/index.tsx +8 -5
- package/src/components/fields-portal.tsx +2 -2
- package/src/components/fields-previewer.tsx +1 -0
- package/src/components/legacy-slot.tsx +11 -16
- package/src/components/output-config-editor.tsx +9 -2
- package/src/components/shadow-content.tsx +19 -0
- package/src/components/shadow-portal.tsx +23 -0
- package/src/components/ui/dialog.tsx +4 -1
- package/src/components/ui/drawer.tsx +3 -1
- package/src/components/ui/popover.tsx +3 -2
- package/src/components/ui/select.tsx +3 -2
- package/src/components/ui/tooltip.tsx +3 -2
- package/src/const/shadow.ts +12 -0
- package/src/hooks/use-portal-container.ts +6 -3
- package/src/hooks/use-shadow-boundary.ts +64 -0
- package/src/hooks/use-shadow.ts +24 -7
- package/src/index.ts +2 -0
- package/src/stories/AppSelect.stories.tsx +44 -0
- package/src/stories/FormSelect.stories.tsx +40 -0
- package/src/style.css +28 -1
- package/style.css +6 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baishuyun/ai-design",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
64
|
-
"@baishuyun/types": "3.0.
|
|
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.
|
|
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
|
|
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]
|
|
125
|
+
<p className="leading-[normal] text-[#030303] text-[14px] truncate">
|
|
126
126
|
{helper.helperName}
|
|
127
127
|
</p>
|
|
128
128
|
</div>
|
|
@@ -440,9 +440,9 @@ export const AppSelect = ({
|
|
|
440
440
|
const hasMultiSelection = mode === 'multiple' && multiSelectedLabels.length > 0;
|
|
441
441
|
|
|
442
442
|
const popoverContent = (
|
|
443
|
-
<div className={classNames('flex flex-col gap-[4px]', contentCls)}>
|
|
443
|
+
<div className={classNames('flex min-h-0 flex-col gap-[4px]', contentCls)}>
|
|
444
444
|
{/* Search */}
|
|
445
|
-
<div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
|
|
445
|
+
<div className="flex shrink-0 items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
|
|
446
446
|
<SearchIcon />
|
|
447
447
|
<input
|
|
448
448
|
type="text"
|
|
@@ -462,7 +462,7 @@ export const AppSelect = ({
|
|
|
462
462
|
setOpen(false);
|
|
463
463
|
}}
|
|
464
464
|
className={cn(
|
|
465
|
-
'flex items-center w-full h-[34px] rounded-[4px] text-left',
|
|
465
|
+
'flex shrink-0 items-center w-full h-[34px] rounded-[4px] text-left',
|
|
466
466
|
'px-[10px]',
|
|
467
467
|
transCls,
|
|
468
468
|
'text-[#030303] hover:bg-[#EFF0F6]'
|
|
@@ -481,7 +481,7 @@ export const AppSelect = ({
|
|
|
481
481
|
setOpen(false);
|
|
482
482
|
}}
|
|
483
483
|
className={cn(
|
|
484
|
-
'flex items-center w-full h-[34px] rounded-[4px] text-left',
|
|
484
|
+
'flex shrink-0 items-center w-full h-[34px] rounded-[4px] text-left',
|
|
485
485
|
'px-[10px]',
|
|
486
486
|
transCls,
|
|
487
487
|
selectedIds.has(ALL_APPS_VALUE)
|
|
@@ -494,7 +494,7 @@ export const AppSelect = ({
|
|
|
494
494
|
)}
|
|
495
495
|
|
|
496
496
|
{/* Tree */}
|
|
497
|
-
<div className={classNames('overflow-y-auto', maxTreeHeightCls)}>
|
|
497
|
+
<div className={classNames('min-h-0 overflow-y-auto', maxTreeHeightCls)}>
|
|
498
498
|
{filteredNodes.length > 0 ? (
|
|
499
499
|
filteredNodes.map((node) => renderNodeRow(node, 0))
|
|
500
500
|
) : (
|
|
@@ -583,8 +583,8 @@ export const AppSelect = ({
|
|
|
583
583
|
align="start"
|
|
584
584
|
sideOffset={4}
|
|
585
585
|
portalContainer={portalContainer}
|
|
586
|
-
className="!border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]"
|
|
587
|
-
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 }}
|
|
588
588
|
>
|
|
589
589
|
{popoverContent}
|
|
590
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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 =
|
|
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
|
+
};
|
package/src/hooks/use-shadow.ts
CHANGED
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
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
|
}
|