@baishuyun/ai-design 2.1.0-alpha.4 → 2.1.3
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/.storybook/preview.ts +2 -0
- package/CHANGELOG.md +197 -28
- package/package.json +11 -11
- package/scripts/post-build.cjs +32 -0
- package/src/components/agent-drawer.tsx +119 -0
- package/src/components/agent-helper/agent-entry-card/index.tsx +3 -8
- package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
- package/src/components/agent-helper/app-select/index.tsx +119 -84
- package/src/components/agent-helper/condition-input/index.tsx +18 -21
- package/src/components/agent-helper/condition-item/index.tsx +1 -0
- package/src/components/agent-helper/field-edit-preference-select/index.tsx +31 -4
- package/src/components/agent-helper/field-value-input/dept-select.tsx +447 -0
- package/src/components/agent-helper/field-value-input/index.tsx +3 -2
- package/src/components/agent-helper/field-value-input/picker-inputs.tsx +20 -18
- package/src/components/agent-helper/form-select/index.tsx +12 -25
- package/src/components/agent-helper/relational-operator-select/index.tsx +37 -23
- package/src/components/agent-helper/table/index.tsx +6 -2
- package/src/components/agent-helper/team-wide-form-select/data-adapter.ts +109 -0
- package/src/components/agent-helper/team-wide-form-select/index.tsx +289 -0
- package/src/components/agent-helper/team-wide-form-select/tree-panel.tsx +180 -0
- package/src/components/agent-helper/team-wide-form-select/types.ts +36 -0
- package/src/components/agent-helper/team-wide-form-select/use-form-tree.ts +108 -0
- package/src/components/agent-helper/trigger-select/index.tsx +1 -1
- package/src/components/agent-select.tsx +115 -35
- package/src/components/bot-avatar-name.tsx +1 -1
- package/src/components/bs-entry-select/build-tree.ts +0 -6
- package/src/components/bs-entry-select/entry-item.tsx +7 -11
- package/src/components/bs-entry-select/other-app-section.tsx +2 -4
- package/src/components/bs-entry-select/scroll-slider.tsx +6 -11
- package/src/components/bs-icons.tsx +752 -206
- package/src/components/card.tsx +55 -10
- package/src/components/circle-checker.tsx +18 -3
- package/src/components/collapsible-txt-msg.tsx +18 -13
- package/src/components/fake-bot-msg.tsx +18 -7
- package/src/components/field-checker-list-msg.tsx +4 -3
- package/src/components/fields-design-info-table.tsx +9 -26
- package/src/components/fields-portal.tsx +2 -2
- package/src/components/fields-previewer.tsx +1 -0
- package/src/components/font-icon.tsx +20 -9
- package/src/components/form-info-editor.tsx +21 -6
- package/src/components/legacy-slot.tsx +11 -16
- package/src/components/memoized-markdown.tsx +32 -19
- package/src/components/message-content.tsx +7 -3
- package/src/components/output-config-editor.tsx +39 -25
- package/src/components/primary-confirm-btn.tsx +6 -1
- package/src/components/scroll-to-bottom-btn.tsx +3 -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/enhanced-drawer.tsx +134 -0
- 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/components/user-txt-msg-bubble.tsx +41 -19
- package/src/components/warning-msg.tsx +29 -12
- package/src/const/shadow.ts +17 -0
- package/src/const/ui.ts +2 -2
- package/src/hooks/use-column-appear.ts +1 -1
- package/src/hooks/use-portal-container.ts +6 -3
- package/src/hooks/use-shadow-boundary.ts +67 -0
- package/src/hooks/use-shadow.ts +24 -7
- package/src/index.ts +21 -0
- package/src/stories/AppSelect.stories.tsx +44 -0
- package/src/stories/FieldValueInput.stories.tsx +6 -3
- package/src/stories/FormSelect.stories.tsx +40 -0
- package/src/stories/IconfontPicker.stories.tsx +106 -0
- package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
- package/src/stories/TeamWideFormSelect.stories.tsx +362 -0
- package/src/stories/UserTextMsgBubble.stories.ts +28 -0
- package/src/style.css +30 -1
- package/style.css +172 -130
|
@@ -1,32 +1,45 @@
|
|
|
1
|
-
import { marked } from
|
|
2
|
-
import { memo, useMemo } from
|
|
3
|
-
import ReactMarkdown from
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import { CSSProperties, memo, useMemo } from 'react';
|
|
3
|
+
import ReactMarkdown from 'react-markdown';
|
|
4
|
+
import remarkBreaks from 'remark-breaks';
|
|
4
5
|
|
|
5
6
|
function parseMarkdownIntoBlocks(markdown: string): string[] {
|
|
6
7
|
const tokens = marked.lexer(markdown);
|
|
7
8
|
return tokens.map((token) => token.raw);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
const MemoizedMarkdownBlock = memo(
|
|
11
|
-
(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
const MemoizedMarkdownBlock = memo(({ content, breaks }: { content: string; breaks?: boolean }) => {
|
|
12
|
+
return (
|
|
13
|
+
<ReactMarkdown remarkPlugins={breaks ? [remarkBreaks] : undefined}>{content}</ReactMarkdown>
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
MemoizedMarkdownBlock.displayName = 'MemoizedMarkdownBlock';
|
|
18
|
+
|
|
19
|
+
type MarkdownProps = { content: string; id: string; breaks?: boolean };
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
const Prose = memo(function Prose({
|
|
22
|
+
content,
|
|
23
|
+
id,
|
|
24
|
+
breaks,
|
|
25
|
+
style,
|
|
26
|
+
}: MarkdownProps & { style?: CSSProperties }) {
|
|
27
|
+
const blocks = useMemo(() => parseMarkdownIntoBlocks(content), [content]);
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
return blocks.map((block, index) => (
|
|
30
|
+
<article className="prose" key={`${id}-block_${index}`} style={style}>
|
|
31
|
+
<MemoizedMarkdownBlock breaks={breaks} content={block} />
|
|
32
|
+
</article>
|
|
33
|
+
));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const MemoizedMarkdown = Object.assign(
|
|
37
|
+
memo(function MemoizedMarkdown({ content, id, breaks }: MarkdownProps) {
|
|
24
38
|
const blocks = useMemo(() => parseMarkdownIntoBlocks(content), [content]);
|
|
25
39
|
|
|
26
40
|
return blocks.map((block, index) => (
|
|
27
|
-
<MemoizedMarkdownBlock content={block} key={`${id}-block_${index}`} />
|
|
41
|
+
<MemoizedMarkdownBlock breaks={breaks} content={block} key={`${id}-block_${index}`} />
|
|
28
42
|
));
|
|
29
|
-
},
|
|
43
|
+
}),
|
|
44
|
+
{ Prose },
|
|
30
45
|
);
|
|
31
|
-
|
|
32
|
-
MemoizedMarkdown.displayName = "MemoizedMarkdown";
|
|
@@ -3,16 +3,20 @@ import { cn } from '../lib/utils';
|
|
|
3
3
|
|
|
4
4
|
export type MessageContentProps = HTMLAttributes<HTMLDivElement> & {
|
|
5
5
|
compact?: boolean;
|
|
6
|
+
/** 移动端消息内容文字 16px(PC 端 14px) */
|
|
7
|
+
isMobile?: boolean;
|
|
6
8
|
};
|
|
7
9
|
|
|
8
|
-
export const MessageContent = ({ children, className, ...props }: MessageContentProps) => (
|
|
10
|
+
export const MessageContent = ({ children, className, isMobile, ...props }: MessageContentProps) => (
|
|
9
11
|
<div
|
|
10
12
|
className={cn(
|
|
11
|
-
'flex flex-col gap-2.5 overflow-hidden p-0 text-foreground
|
|
13
|
+
'flex flex-col gap-2.5 overflow-hidden p-0 text-foreground',
|
|
12
14
|
'group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground',
|
|
13
15
|
'group-[.is-assistant]:bg-secondary group-[.is-assistant]:text-foreground',
|
|
14
16
|
'is-user:dark overflow-visible',
|
|
15
|
-
className
|
|
17
|
+
className,
|
|
18
|
+
// 字号放在最后,保证 MessageContent 自身字号权威,外部 className 传入的 text-[*] 无法覆盖
|
|
19
|
+
isMobile ? 'text-[16px]' : 'text-[14px]'
|
|
16
20
|
)}
|
|
17
21
|
{...props}
|
|
18
22
|
>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
2
|
import type { FieldType, IAgentHelperOutputConfig, OutputParam, IField, ISubformField } from '@baishuyun/types';
|
|
3
3
|
import { cn } from '../lib/utils';
|
|
4
4
|
import { transCls } from '../const/ui';
|
|
5
5
|
import { TrashIcon, SearchIcon } from '../components/bs-icons';
|
|
6
6
|
import { Popover, PopoverTrigger, PopoverContent } from './ui/popover';
|
|
7
7
|
import { usePortalContainer } from '../hooks/use-portal-container';
|
|
8
|
+
import { BsTooltip } from './tooltip';
|
|
8
9
|
|
|
9
10
|
const PlusIcon = () => (
|
|
10
11
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -110,17 +111,16 @@ const VALID_TYPE_MAP: Partial<Record<FieldType, Set<FieldType>>> = {
|
|
|
110
111
|
'image': new Set(['signature', 'image', 'upload']),
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
function isCompatibleType(sourceType?: FieldType | '', targetType?: FieldType | ''): boolean {
|
|
115
|
+
return !!sourceType && !!targetType && sourceType !== 'subform' && targetType !== 'subform' &&
|
|
116
|
+
(sourceType === targetType || !!VALID_TYPE_MAP[sourceType]?.has(targetType));
|
|
117
|
+
}
|
|
114
118
|
|
|
115
|
-
function filterFieldsByType(fields: OutputFieldOption[],
|
|
119
|
+
function filterFieldsByType(fields: OutputFieldOption[], sourceType?: FieldType | ''): OutputFieldOption[] {
|
|
116
120
|
return fields.reduce<OutputFieldOption[]>((acc, f) => {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const filteredChildren = f.children ? filterFieldsByType(f.children, targetType) : [];
|
|
122
|
-
if (selfMatches || generalMatches || filteredChildren.length > 0) {
|
|
123
|
-
acc.push({ ...f, children: filteredChildren.length > 0 ? filteredChildren : f.children });
|
|
121
|
+
const filteredChildren = f.children ? filterFieldsByType(f.children, sourceType) : [];
|
|
122
|
+
if (filteredChildren.length > 0 || (!f.children?.length && isCompatibleType(sourceType, f.type))) {
|
|
123
|
+
acc.push({ ...f, children: filteredChildren.length > 0 ? filteredChildren : undefined });
|
|
124
124
|
}
|
|
125
125
|
return acc;
|
|
126
126
|
}, []);
|
|
@@ -133,8 +133,8 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
133
133
|
|
|
134
134
|
const { portalContainer, setTriggerRef } = usePortalContainer('data-bs-output-config-portal');
|
|
135
135
|
|
|
136
|
-
const sourceOptions =
|
|
137
|
-
const targetOptions =
|
|
136
|
+
const sourceOptions = useMemo(() => outputParamsToOptions(sourceFields ?? []), [sourceFields]);
|
|
137
|
+
const targetOptions = useMemo(() => fieldsToOptions(targetFields ?? []), [targetFields]);
|
|
138
138
|
|
|
139
139
|
const handleDropdownOpenChange = useCallback((index: number, field: 'source' | 'target', open: boolean) => {
|
|
140
140
|
if (open) {
|
|
@@ -161,7 +161,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
161
161
|
const newItem = { ...newValue[index], sourceField: { fieldName: field.fieldName, fieldDesc: field.fieldDesc } };
|
|
162
162
|
if (newItem.targetField.fieldName) {
|
|
163
163
|
const targetOption = findFieldByName(targetOptions, newItem.targetField.fieldName);
|
|
164
|
-
if (
|
|
164
|
+
if (!isCompatibleType(field.type, targetOption?.type)) {
|
|
165
165
|
newItem.targetField = { fieldName: '', fieldDesc: '' };
|
|
166
166
|
}
|
|
167
167
|
}
|
|
@@ -172,12 +172,21 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
172
172
|
}, [value, onChange, targetOptions]);
|
|
173
173
|
|
|
174
174
|
const handleSelectTargetField = useCallback((index: number, field: OutputFieldOption) => {
|
|
175
|
+
const sourceOption = flattenSourceFields(sourceOptions).find(
|
|
176
|
+
option => option.fieldName === value[index].sourceField.fieldName
|
|
177
|
+
);
|
|
178
|
+
if (!isCompatibleType(sourceOption?.type, field.type)) {
|
|
179
|
+
if (import.meta.env.DEV) {
|
|
180
|
+
console.warn('[OutputConfigEditor] 不兼容的输出映射', { sourceType: sourceOption?.type, targetType: field.type });
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
175
184
|
const newValue = [...value];
|
|
176
185
|
newValue[index] = { ...newValue[index], targetField: { fieldName: field.fieldName, fieldDesc: field.fieldDesc } };
|
|
177
186
|
onChange?.(newValue);
|
|
178
187
|
setDropdownTarget(null);
|
|
179
188
|
setSearchText('');
|
|
180
|
-
}, [value, onChange]);
|
|
189
|
+
}, [value, onChange, sourceOptions]);
|
|
181
190
|
|
|
182
191
|
const toggleParent = useCallback((fieldName: string) => {
|
|
183
192
|
setCollapsedParents(prev => {
|
|
@@ -209,6 +218,10 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
209
218
|
(f) => (matchSearch(f.fieldDesc) || matchSearch(f.fieldName)) && f.type !== 'subform'
|
|
210
219
|
);
|
|
211
220
|
|
|
221
|
+
const usedSourceFieldNames = new Set(
|
|
222
|
+
value.map(item => item.sourceField.fieldName).filter(Boolean)
|
|
223
|
+
);
|
|
224
|
+
|
|
212
225
|
const searchedTargetFields = filterFields(targetOptions);
|
|
213
226
|
|
|
214
227
|
const fieldRowCls = cn(
|
|
@@ -253,9 +266,10 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
253
266
|
const selectedSourceType = item.sourceField.fieldName
|
|
254
267
|
? flatSourceFields.find(f => f.fieldName === item.sourceField.fieldName)?.type
|
|
255
268
|
: undefined;
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
269
|
+
const rowSourceFields = filteredSourceFields.filter(
|
|
270
|
+
f => !usedSourceFieldNames.has(f.fieldName) || f.fieldName === item.sourceField.fieldName
|
|
271
|
+
);
|
|
272
|
+
const rowTargetFields = filterFieldsByType(searchedTargetFields, selectedSourceType);
|
|
259
273
|
return (
|
|
260
274
|
<div key={index} className="flex gap-[10px] items-center">
|
|
261
275
|
{/* Source field selector (JSON output field) */}
|
|
@@ -269,9 +283,9 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
269
283
|
isDropdownOpen(index, 'source') ? 'border-[#0265ff]' : 'border-[#e0e0e0]',
|
|
270
284
|
)}
|
|
271
285
|
>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
<span className="text-[14px] text-[#030303] leading-normal truncate" title={item.sourceField.fieldDesc ?? "" }>
|
|
287
|
+
{item.sourceField.fieldDesc || <span className="text-[#999]">请选择字段</span>}
|
|
288
|
+
</span>
|
|
275
289
|
<CaretIcon className={cn(
|
|
276
290
|
'shrink-0 text-[#666]',
|
|
277
291
|
isDropdownOpen(index, 'source') && 'rotate-180',
|
|
@@ -303,7 +317,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
303
317
|
/>
|
|
304
318
|
</div>
|
|
305
319
|
<div className="max-h-[210px] overflow-auto">
|
|
306
|
-
{
|
|
320
|
+
{rowSourceFields.map((field) => (
|
|
307
321
|
<button
|
|
308
322
|
key={field.fieldName}
|
|
309
323
|
type="button"
|
|
@@ -319,7 +333,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
319
333
|
<span className="truncate">{field.fieldDesc}</span>
|
|
320
334
|
</button>
|
|
321
335
|
))}
|
|
322
|
-
{
|
|
336
|
+
{rowSourceFields.length === 0 && (
|
|
323
337
|
<div className="px-[10px] py-[7px] text-[14px] text-[#999] leading-normal">
|
|
324
338
|
无匹配结果
|
|
325
339
|
</div>
|
|
@@ -344,9 +358,9 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
|
|
|
344
358
|
isDropdownOpen(index, 'target') ? 'border-[#0265ff]' : 'border-[#e0e0e0]',
|
|
345
359
|
)}
|
|
346
360
|
>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
361
|
+
<span className="text-[14px] text-[#030303] leading-normal truncate" title={item.targetField.fieldDesc ?? ""}>
|
|
362
|
+
{item.targetField.fieldDesc || <span className="text-[#999]">请选择表单字段</span>}
|
|
363
|
+
</span>
|
|
350
364
|
<CaretIcon className={cn(
|
|
351
365
|
'shrink-0 text-[#666]',
|
|
352
366
|
isDropdownOpen(index, 'target') && 'rotate-180',
|
|
@@ -8,6 +8,8 @@ export interface PrimaryConfirmBtnProps extends ButtonProps {
|
|
|
8
8
|
icon?: React.ReactNode;
|
|
9
9
|
disabledWithInfo?: boolean;
|
|
10
10
|
light?: boolean;
|
|
11
|
+
/** 移动端按钮高度 40px、文本 16px(PC 端 34px / 14px) */
|
|
12
|
+
isMobile?: boolean;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export const PrimaryConfirmBtn = ({
|
|
@@ -18,15 +20,18 @@ export const PrimaryConfirmBtn = ({
|
|
|
18
20
|
light,
|
|
19
21
|
icon,
|
|
20
22
|
children,
|
|
23
|
+
isMobile,
|
|
21
24
|
...rest
|
|
22
25
|
}: PrimaryConfirmBtnProps) => {
|
|
26
|
+
const sizeCls = isMobile ? 'h-[40px] text-[16px]' : 'h-[34px] text-[14px]';
|
|
23
27
|
const btn = (
|
|
24
28
|
<Button
|
|
25
29
|
{...rest}
|
|
26
30
|
variant="default"
|
|
27
31
|
disabled={disabled}
|
|
28
32
|
className={cn(
|
|
29
|
-
'px-[10px] py-0 rounded-[100px]
|
|
33
|
+
'px-[10px] py-0 rounded-[100px] leading-normal min-w-[100px] cursor-pointer w-full disabled:opacity-100 gap-[6px] shadow-none',
|
|
34
|
+
sizeCls,
|
|
30
35
|
disabled
|
|
31
36
|
? 'bg-[#f0f0f0] text-[#999] cursor-not-allowed shadow-none'
|
|
32
37
|
: light
|
|
@@ -5,9 +5,10 @@ import { ScrollArrowDownIcon } from './bs-icons';
|
|
|
5
5
|
export interface ScrollToBottomProps {
|
|
6
6
|
onClick?: () => void;
|
|
7
7
|
className?: string;
|
|
8
|
+
isMobile?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export const ScrollToBottomBtn = ({ onClick, className }: ScrollToBottomProps) => {
|
|
11
|
+
export const ScrollToBottomBtn = ({ onClick, className, isMobile }: ScrollToBottomProps) => {
|
|
11
12
|
return (
|
|
12
13
|
<button
|
|
13
14
|
type="button"
|
|
@@ -22,7 +23,7 @@ export const ScrollToBottomBtn = ({ onClick, className }: ScrollToBottomProps) =
|
|
|
22
23
|
className
|
|
23
24
|
)}
|
|
24
25
|
>
|
|
25
|
-
<ScrollArrowDownIcon />
|
|
26
|
+
<ScrollArrowDownIcon size={isMobile ? 20 : 24 } />
|
|
26
27
|
</button>
|
|
27
28
|
);
|
|
28
29
|
};
|
|
@@ -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}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Drawer, DrawerContent, DrawerTitle } from './drawer';
|
|
5
|
+
|
|
6
|
+
export type EnhancedDrawerDirection = 'left' | 'right' | 'top' | 'bottom';
|
|
7
|
+
|
|
8
|
+
export interface EnhancedDrawerProps {
|
|
9
|
+
/** 受控开关 */
|
|
10
|
+
open?: boolean;
|
|
11
|
+
/** 非受控默认开关 */
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
onOpenChange?: (open: boolean) => void;
|
|
14
|
+
/** 滑入方向,默认 right */
|
|
15
|
+
direction?: EnhancedDrawerDirection;
|
|
16
|
+
/** 侧边抽屉宽度(left/right 生效),默认 300 */
|
|
17
|
+
width?: number;
|
|
18
|
+
/** 无障碍标题(sr-only 展示) */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** 是否显示遮罩,默认 true */
|
|
21
|
+
showOverlay?: boolean;
|
|
22
|
+
/** 点击遮罩是否关闭,默认 true */
|
|
23
|
+
overlayClosable?: boolean;
|
|
24
|
+
/** 遮罩点击回调(额外行为,如打点) */
|
|
25
|
+
onOverlayClick?: () => void;
|
|
26
|
+
/** 遮罩附加样式 */
|
|
27
|
+
overlayStyle?: React.CSSProperties;
|
|
28
|
+
/** 抽屉面板附加 className */
|
|
29
|
+
className?: string;
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 增强版抽屉(基于 vaul,面向 shadow DOM / WebView 环境的封装)。
|
|
35
|
+
*
|
|
36
|
+
* 统一处理了以下兼容问题,业务组件无需再各自处理:
|
|
37
|
+
* 1. `modal={false}` —— 关闭 Radix Dialog 的 body 滚动锁(react-remove-scroll 的
|
|
38
|
+
* touchmove preventDefault 在 shadow DOM 里因事件目标 retarget 无法识别抽屉内
|
|
39
|
+
* 滚动容器,会锁死滚动);同时 vaul 的 Overlay 在 modal=false 时返回 null。
|
|
40
|
+
* 2. `dismissible={false}` —— 关闭 vaul 拖拽关闭,避免 onPress 每次 pointerdown 都
|
|
41
|
+
* setPointerCapture 禁掉原生触摸滚动。
|
|
42
|
+
* 3. 手动渲染遮罩(vaul 遮罩因 modal=false 消失),点遮罩关闭走受控路径
|
|
43
|
+
* (绕过被 dismissible 拦掉的 vaul 关闭路径)。
|
|
44
|
+
*
|
|
45
|
+
* 内容区若需滚动,业务组件在自身滚动容器上加 `data-vaul-no-drag`(保险,
|
|
46
|
+
* dismissible=false 已阻止拖拽,此标记可防未来语义变化)。
|
|
47
|
+
*/
|
|
48
|
+
export const EnhancedDrawer = ({
|
|
49
|
+
open: controlledOpen,
|
|
50
|
+
defaultOpen,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
direction = 'right',
|
|
53
|
+
width = 300,
|
|
54
|
+
title,
|
|
55
|
+
showOverlay = true,
|
|
56
|
+
overlayClosable = true,
|
|
57
|
+
onOverlayClick,
|
|
58
|
+
overlayStyle,
|
|
59
|
+
className,
|
|
60
|
+
children,
|
|
61
|
+
}: EnhancedDrawerProps) => {
|
|
62
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
|
|
63
|
+
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
|
64
|
+
|
|
65
|
+
const setOpen = useCallback(
|
|
66
|
+
(next: boolean) => {
|
|
67
|
+
if (controlledOpen === undefined) setInternalOpen(next);
|
|
68
|
+
onOpenChange?.(next);
|
|
69
|
+
},
|
|
70
|
+
[controlledOpen, onOpenChange]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 遮罩 portal 容器:抽屉内容经 vaul portal 进所在 shadow root,遮罩需与之同层
|
|
74
|
+
// 才能正确叠在抽屉下方(z 更低)且覆盖全屏
|
|
75
|
+
const [maskContainer, setMaskContainer] = useState<Element | null>(null);
|
|
76
|
+
const maskHostRef = useCallback((node: HTMLSpanElement | null) => {
|
|
77
|
+
if (!node) {
|
|
78
|
+
setMaskContainer(null);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const root = node.getRootNode();
|
|
82
|
+
setMaskContainer(
|
|
83
|
+
root instanceof ShadowRoot ? (root as unknown as Element) : document.body
|
|
84
|
+
);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const handleOverlayClick = useCallback(() => {
|
|
88
|
+
onOverlayClick?.();
|
|
89
|
+
if (overlayClosable) setOpen(false);
|
|
90
|
+
}, [onOverlayClick, overlayClosable, setOpen]);
|
|
91
|
+
|
|
92
|
+
const isHorizontal = direction === 'left' || direction === 'right';
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<span ref={maskHostRef} style={{ display: 'none' }} />
|
|
97
|
+
|
|
98
|
+
{showOverlay &&
|
|
99
|
+
open &&
|
|
100
|
+
maskContainer &&
|
|
101
|
+
createPortal(
|
|
102
|
+
<div
|
|
103
|
+
onClick={handleOverlayClick}
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
style={{
|
|
106
|
+
position: 'fixed',
|
|
107
|
+
inset: 0,
|
|
108
|
+
background: 'rgba(0,0,0,0.3)',
|
|
109
|
+
zIndex: 40,
|
|
110
|
+
...overlayStyle,
|
|
111
|
+
}}
|
|
112
|
+
/>,
|
|
113
|
+
maskContainer
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<Drawer
|
|
117
|
+
open={open}
|
|
118
|
+
onOpenChange={setOpen}
|
|
119
|
+
direction={direction}
|
|
120
|
+
modal={false}
|
|
121
|
+
dismissible={false}
|
|
122
|
+
>
|
|
123
|
+
<DrawerContent
|
|
124
|
+
className={cn(className)}
|
|
125
|
+
hideOverlay
|
|
126
|
+
style={isHorizontal ? { width: `${width}px`, maxWidth: `${width}px` } : undefined}
|
|
127
|
+
>
|
|
128
|
+
{title != null && <DrawerTitle className="sr-only">{title}</DrawerTitle>}
|
|
129
|
+
{children}
|
|
130
|
+
</DrawerContent>
|
|
131
|
+
</Drawer>
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
@@ -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
|
|