@baishuyun/ai-design 2.1.0-alpha.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.storybook/preview.ts +2 -0
  2. package/CHANGELOG.md +189 -20
  3. package/package.json +8 -9
  4. package/scripts/post-build.cjs +32 -0
  5. package/src/components/agent-drawer.tsx +119 -0
  6. package/src/components/agent-helper/agent-entry-card/index.tsx +3 -8
  7. package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
  8. package/src/components/agent-helper/app-select/index.tsx +119 -84
  9. package/src/components/agent-helper/condition-input/index.tsx +18 -21
  10. package/src/components/agent-helper/condition-item/index.tsx +1 -0
  11. package/src/components/agent-helper/field-edit-preference-select/index.tsx +31 -4
  12. package/src/components/agent-helper/field-value-input/dept-select.tsx +447 -0
  13. package/src/components/agent-helper/field-value-input/index.tsx +3 -2
  14. package/src/components/agent-helper/field-value-input/picker-inputs.tsx +20 -18
  15. package/src/components/agent-helper/form-select/index.tsx +12 -25
  16. package/src/components/agent-helper/relational-operator-select/index.tsx +37 -23
  17. package/src/components/agent-helper/table/index.tsx +6 -2
  18. package/src/components/agent-helper/team-wide-form-select/data-adapter.ts +109 -0
  19. package/src/components/agent-helper/team-wide-form-select/index.tsx +289 -0
  20. package/src/components/agent-helper/team-wide-form-select/tree-panel.tsx +180 -0
  21. package/src/components/agent-helper/team-wide-form-select/types.ts +36 -0
  22. package/src/components/agent-helper/team-wide-form-select/use-form-tree.ts +108 -0
  23. package/src/components/agent-helper/trigger-select/index.tsx +1 -1
  24. package/src/components/agent-select.tsx +115 -35
  25. package/src/components/bot-avatar-name.tsx +1 -1
  26. package/src/components/bs-entry-select/build-tree.ts +0 -6
  27. package/src/components/bs-entry-select/entry-item.tsx +7 -11
  28. package/src/components/bs-entry-select/other-app-section.tsx +2 -4
  29. package/src/components/bs-entry-select/scroll-slider.tsx +6 -11
  30. package/src/components/bs-icons.tsx +752 -206
  31. package/src/components/card.tsx +55 -10
  32. package/src/components/circle-checker.tsx +18 -3
  33. package/src/components/collapsible-txt-msg.tsx +18 -13
  34. package/src/components/fake-bot-msg.tsx +18 -7
  35. package/src/components/field-checker-list-msg.tsx +4 -3
  36. package/src/components/fields-design-info-table.tsx +9 -26
  37. package/src/components/fields-portal.tsx +2 -2
  38. package/src/components/fields-previewer.tsx +1 -0
  39. package/src/components/font-icon.tsx +20 -9
  40. package/src/components/form-info-editor.tsx +21 -6
  41. package/src/components/legacy-slot.tsx +11 -16
  42. package/src/components/memoized-markdown.tsx +32 -19
  43. package/src/components/message-content.tsx +7 -3
  44. package/src/components/output-config-editor.tsx +39 -25
  45. package/src/components/primary-confirm-btn.tsx +6 -1
  46. package/src/components/scroll-to-bottom-btn.tsx +3 -2
  47. package/src/components/shadow-content.tsx +19 -0
  48. package/src/components/shadow-portal.tsx +23 -0
  49. package/src/components/ui/dialog.tsx +4 -1
  50. package/src/components/ui/drawer.tsx +3 -1
  51. package/src/components/ui/enhanced-drawer.tsx +134 -0
  52. package/src/components/ui/popover.tsx +3 -2
  53. package/src/components/ui/select.tsx +3 -2
  54. package/src/components/ui/tooltip.tsx +3 -2
  55. package/src/components/user-txt-msg-bubble.tsx +41 -19
  56. package/src/components/warning-msg.tsx +29 -12
  57. package/src/const/shadow.ts +17 -0
  58. package/src/const/ui.ts +2 -2
  59. package/src/hooks/use-column-appear.ts +1 -1
  60. package/src/hooks/use-portal-container.ts +6 -3
  61. package/src/hooks/use-shadow-boundary.ts +67 -0
  62. package/src/hooks/use-shadow.ts +24 -7
  63. package/src/index.ts +21 -0
  64. package/src/stories/AppSelect.stories.tsx +44 -0
  65. package/src/stories/FieldValueInput.stories.tsx +6 -3
  66. package/src/stories/FormSelect.stories.tsx +40 -0
  67. package/src/stories/IconfontPicker.stories.tsx +106 -0
  68. package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
  69. package/src/stories/TeamWideFormSelect.stories.tsx +362 -0
  70. package/src/stories/UserTextMsgBubble.stories.ts +28 -0
  71. package/src/style.css +28 -1
  72. package/style.css +172 -130
@@ -1,3 +1,4 @@
1
+ import classNames from 'classnames';
1
2
  import { transCls } from '../const/ui';
2
3
  import { cn } from '../lib/utils';
3
4
  import { ReactNode } from 'react';
@@ -9,28 +10,72 @@ export interface CardProps {
9
10
  icon?: ReactNode;
10
11
  iconCode?: string;
11
12
  onClick?: (title: string, iconCode?: string) => void;
13
+ /** 追加到根节点的类名(如移动端满宽 max-w-none) */
14
+ className?: string;
15
+ /**
16
+ * 紧凑模式(移动端推荐卡片,对齐 Figma 40040:30035):
17
+ * 标题行去内边距、高 26,标题与描述间距 6,卡片总高 92。
18
+ * 默认 false,不影响现有 PC 样式
19
+ */
20
+ compact?: boolean;
12
21
  }
13
22
 
14
- export const Card = ({ title, desc, index, icon, onClick, iconCode }: CardProps) => {
23
+ export const Card = ({
24
+ title,
25
+ desc,
26
+ index,
27
+ icon,
28
+ onClick,
29
+ iconCode,
30
+ className,
31
+ compact,
32
+ }: CardProps) => {
33
+ const iconFontCls = classNames(
34
+ {
35
+ 'text-[18px]': compact,
36
+ },
37
+ 'text-[#0265FF]'
38
+ );
39
+
40
+ const titleCls = classNames(
41
+ {
42
+ 'text-[16px]': compact,
43
+ 'font-[500]': compact,
44
+ },
45
+ 'truncate !text-[#12111] group-hover:text-[#0265FF]'
46
+ );
47
+
48
+ const contentCls = classNames(
49
+ {
50
+ 'text-[12px]': !compact,
51
+ 'text-[14px]': compact,
52
+ },
53
+ 'text-[#666] leading-5'
54
+ );
55
+
15
56
  return (
16
57
  <div
17
58
  className={cn(
18
- 'bs-card group flex flex-col cursor-pointer p-2.5 gap-2.5 rounded-lg max-w-[350px] border border-[#e0e0e0] bg-white',
19
- {
20
- transCls,
21
- }
59
+ 'bs-card group flex flex-col cursor-pointer p-2.5 rounded-lg max-w-[350px] border border-[#e0e0e0] bg-white',
60
+ compact ? 'gap-1.5' : 'gap-2.5',
61
+ transCls,
62
+ className
22
63
  )}
23
64
  onClick={() => {
24
65
  onClick?.(title, iconCode);
25
66
  }}
26
67
  >
27
- <div className="px-2.5 flex items-center gap-1.5 rounded-[4px] h-[34px] bg-white">
28
- {index === undefined ? null : <div>{index}</div>}
29
- {icon === undefined ? null : <div className="text-[#0265FF]">{icon}</div>}
68
+ <div
69
+ className={cn('flex items-center gap-1.5 rounded-[4px] bg-white', {
70
+ 'h-[26px]': compact,
71
+ 'px-2.5 h-[34px]': !compact,
72
+ })}
73
+ >
74
+ {icon === undefined ? null : <div className={iconFontCls}>{icon}</div>}
30
75
  {/* make title text ellipsis */}
31
- <div className="truncate !text-[#12111] group-hover:text-[#0265FF]">{title}</div>
76
+ <div className={titleCls}>{title}</div>
32
77
  </div>
33
- <div className="text-[#666] leading-5 text-[12px]">{desc}</div>
78
+ <div className={contentCls}>{desc}</div>
34
79
  </div>
35
80
  );
36
81
  };
@@ -1,6 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { cn } from '../lib/utils';
3
3
  import { CheckerInBoxIcon } from './bs-icons';
4
+ import classNames from 'classnames';
4
5
 
5
6
  export interface CircleCheckerProps {
6
7
  defaultChecked?: boolean;
@@ -9,6 +10,7 @@ export interface CircleCheckerProps {
9
10
  children?: React.ReactNode;
10
11
  disabled?: boolean;
11
12
  className?: string;
13
+ isMobile?: boolean;
12
14
  }
13
15
 
14
16
  export const CircleChecker = ({
@@ -17,6 +19,7 @@ export const CircleChecker = ({
17
19
  onCheckedChange,
18
20
  children,
19
21
  disabled = false,
22
+ isMobile,
20
23
  className,
21
24
  }: CircleCheckerProps) => {
22
25
  const [internalChecked, setInternalChecked] = useState(defaultChecked);
@@ -34,6 +37,16 @@ export const CircleChecker = ({
34
37
  onCheckedChange?.(newChecked);
35
38
  };
36
39
 
40
+ const sizeCls = classNames({
41
+ 'w-4 h-4': !isMobile,
42
+ 'w-[20px] h-[20px]': isMobile
43
+ })
44
+
45
+ const fontCls = classNames({
46
+ 'text-[14px]': !isMobile,
47
+ 'text-[16px]': isMobile,
48
+ })
49
+
37
50
  return (
38
51
  <button
39
52
  type="button"
@@ -43,14 +56,16 @@ export const CircleChecker = ({
43
56
  onClick={handleClick}
44
57
  className={cn(
45
58
  'inline-flex items-center gap-[6px] cursor-pointer select-none',
46
- 'text-[#121111] text-[14px]',
59
+ 'text-[#121111]',
60
+ fontCls,
47
61
  disabled && 'cursor-not-allowed opacity-50',
48
62
  className
49
63
  )}
50
64
  >
51
65
  <span
52
66
  className={cn(
53
- 'inline-flex items-center justify-center w-4 h-4 rounded-full transition-colors duration-200 box-border border',
67
+ 'inline-flex items-center justify-center rounded-full transition-colors duration-200 box-border border',
68
+ sizeCls,
54
69
  isChecked ? 'bg-[#2E60FF] border-transparent' : 'bg-white border-[#e0e0e0]'
55
70
  )}
56
71
  >
@@ -60,7 +75,7 @@ export const CircleChecker = ({
60
75
  </span>
61
76
  )}
62
77
  </span>
63
- {children && <span className="text-sm">{children}</span>}
78
+ {children && <span className="">{children}</span>}
64
79
  </button>
65
80
  );
66
81
  };
@@ -7,7 +7,8 @@ import classNames from 'classnames';
7
7
  export interface CollapsibleTxtMsgProps {
8
8
  title: string;
9
9
  content?: string;
10
- icon?: ReactNode;
10
+ /** 不传时用 AnalysisIcon。null 不显示图标 */
11
+ icon?: ReactNode | null;
11
12
  defaultOpen?: boolean;
12
13
  children?: ReactNode;
13
14
 
@@ -15,7 +16,8 @@ export interface CollapsibleTxtMsgProps {
15
16
 
16
17
  disableExpand?: boolean;
17
18
 
18
- withOpenShadow?: boolean;
19
+ /** 移动端消息内容文字 16px(PC 端 14px) */
20
+ isMobile?: boolean;
19
21
  }
20
22
 
21
23
  export const CollapsibleTxtMsg = ({
@@ -26,24 +28,25 @@ export const CollapsibleTxtMsg = ({
26
28
  disableExpand,
27
29
  children,
28
30
  defaultOpen,
29
- withOpenShadow,
31
+ isMobile,
30
32
  }: CollapsibleTxtMsgProps) => {
31
33
  const [isOpen, setIsOpen] = useState(defaultOpen);
32
-
33
-
34
- const isGhostOpen = isOpen && ghost;
34
+ const sizeCls = isMobile ? 'text-[16px]' : 'text-[14px]';
35
+ const iconSizeCls = 'w-[16px] h-[16px]';
35
36
 
36
37
  const cls = classNames({
37
- 'bg-[#fafafa] border border-[#e0e0e0] border-solid p-2.5': !ghost,
38
- 'shadow-lg p-2.5': withOpenShadow && isGhostOpen,
38
+ 'bg-[#fafafa] border border-[#e0e0e0] border-solid p-2.5': !ghost
39
39
  })
40
40
 
41
- const t = (isGhostOpen && !withOpenShadow) ? '' : title;
41
+ const isGhostOpen = isOpen && ghost;
42
+
43
+ const t = isGhostOpen ? '' : title;
44
+ const shownIcon = icon === undefined ? <AnalysisIcon /> : icon;
42
45
 
43
46
  return (
44
47
  <div className={classNames("flex flex-col gap-2.5 rounded-lg w-full", cls)}>
45
48
  <div
46
- className={cn('flex items-center justify-between w-full gap-2.5', {
49
+ className={cn('flex items-center justify-between w-full', {
47
50
  'cursor-pointer': !disableExpand,
48
51
  })}
49
52
  onClick={() => {
@@ -53,8 +56,10 @@ export const CollapsibleTxtMsg = ({
53
56
  }}
54
57
  >
55
58
  <div className="flex gap-2.5 items-center">
56
- <div className="w-4 h-4 shrink-0 flex items-center">{icon || <AnalysisIcon />}</div>
57
- <p className="text-[#666] text-[14px] leading-normal">{t}</p>
59
+ {shownIcon ? (
60
+ <div className={classNames("shrink-0 flex items-center", iconSizeCls)}>{shownIcon}</div>
61
+ ) : null}
62
+ <p className={`text-[#666] ${sizeCls} leading-normal`}>{t}</p>
58
63
  </div>
59
64
  <button
60
65
  onClick={() => {
@@ -73,7 +78,7 @@ export const CollapsibleTxtMsg = ({
73
78
  </button>
74
79
  </div>
75
80
  {isOpen && !disableExpand && (
76
- <div className="text-[#121111] text-[14px] word-wrap break-all max-w-full">{content || children}</div>
81
+ <div className={`text-[#121111] ${sizeCls} word-wrap`}>{content || children}</div>
77
82
  )}
78
83
  </div>
79
84
  );
@@ -6,20 +6,23 @@ export const FakeBotMessage = ({
6
6
  children,
7
7
  className,
8
8
  avatarJsx,
9
- headless = false,
9
+ isMobile,
10
10
  }: {
11
11
  children: ReactNode;
12
12
  offset?: boolean;
13
13
  className?: string;
14
14
  avatarJsx?: ReactNode;
15
- headless?: boolean;
15
+ /** 移动端消息内容文字 16px(PC 端 14px) */
16
+ isMobile?: boolean;
16
17
  }) => {
17
- if (headless || !avatarJsx) {
18
+ if (!avatarJsx) {
18
19
  return (
19
20
  <div>
20
21
  <MessageContent
22
+ isMobile={isMobile}
21
23
  className={cn(
22
24
  'wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left w-full',
25
+ isMobile && 'gap-[10px]',
23
26
  className
24
27
  )}
25
28
  data-testid="message-content"
@@ -30,18 +33,26 @@ export const FakeBotMessage = ({
30
33
  );
31
34
  }
32
35
 
33
-
34
36
  return (
35
37
  <div
36
- className={cn('group/message w-full')}
38
+ className={cn('group/message w-full', className)}
37
39
  data-role="assistant"
38
40
  data-testid={`message-assistant`}
39
41
  >
40
- <div className="flex w-full flex-col items-start justify-start gap-2.5">
42
+ <div
43
+ className={cn(
44
+ 'flex w-full flex-col items-start justify-start',
45
+ isMobile ? 'gap-[10px]' : 'gap-2.5'
46
+ )}
47
+ >
41
48
  {avatarJsx}
42
49
  <div className="flex flex-col min-w-0 grow gap-2.5 w-full">
43
50
  <MessageContent
44
- className="w-full wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left"
51
+ isMobile={isMobile}
52
+ className={cn(
53
+ 'w-full wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left',
54
+ isMobile && 'gap-[10px]'
55
+ )}
45
56
  data-testid="message-content"
46
57
  >
47
58
  {children}
@@ -26,8 +26,9 @@ export const FieldCheckerListMsg = memo(
26
26
  children?: ReactNode;
27
27
  streaming: boolean;
28
28
  confirmed?: boolean;
29
+ isMobile?: boolean;
29
30
  }
30
- >(({ fields, children, streaming, confirmed }, ref) => {
31
+ >(({ fields, children, streaming, confirmed, isMobile }, ref) => {
31
32
  // Track individual field checked states (all default to true)
32
33
  const [checkedMap, setCheckedMap] = useState<Record<string, boolean>>(() => {
33
34
  const map: Record<string, boolean> = {};
@@ -72,11 +73,11 @@ export const FieldCheckerListMsg = memo(
72
73
  );
73
74
 
74
75
  return (
75
- <CollapsibleTxtMsg title="字段生成" icon={<LetterAIcon />} defaultOpen>
76
+ <CollapsibleTxtMsg title="字段生成" icon={<LetterAIcon />} defaultOpen isMobile={isMobile}>
76
77
  {
77
78
  <div
78
79
  className={cn(
79
- 'flex gap-2.5 px-4 py-2.5 rounded-[4px] w-full transition-colors cursor-pointer hover:bg-[#EFF0F6] items-center',
80
+ 'flex gap-[6px] px-4 py-2.5 rounded-[4px] w-full transition-colors cursor-pointer hover:bg-[#EFF0F6] items-center',
80
81
  {
81
82
  'pointer-events-none': confirmed || streaming,
82
83
  }
@@ -38,7 +38,7 @@ function groupFields(fields: IFieldDesignInfo[]) {
38
38
  return groups;
39
39
  }
40
40
 
41
- const cellCls = 'flex items-center px-1.5 py-2.5 text-xs leading-normal';
41
+ const cellCls = 'flex items-center px-1.5 py-2.5 text-[12px] leading-normal';
42
42
 
43
43
  function FieldRow({
44
44
  field,
@@ -59,11 +59,9 @@ function FieldRow({
59
59
  <span>{field.fieldName}</span>
60
60
  </div>
61
61
  {/* 类型 */}
62
- <div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px]')}>
63
- {field.type}
64
- </div>
62
+ <div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px]')}>{field.type}</div>
65
63
  {/* 必填 */}
66
- <div className={cn(cellCls, 'w-9 shrink-0 min-h-[34px]')}>
64
+ <div className={cn(cellCls, 'w-10 shrink-0 min-h-[34px]')}>
67
65
  {field.required ? '是' : '否'}
68
66
  </div>
69
67
  {/* 说明 */}
@@ -100,11 +98,9 @@ function SubFormGroup({
100
98
  <span>{parent.fieldName}</span>
101
99
  </div>
102
100
  {/* 类型 */}
103
- <div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px] ')}>
104
- {parent.type}
105
- </div>
101
+ <div className={cn(cellCls, 'w-[72px] shrink-0 min-h-[34px] ')}>{parent.type}</div>
106
102
  {/* 必填 */}
107
- <div className={cn(cellCls, 'w-9 shrink-0 min-h-[34px]')}>
103
+ <div className={cn(cellCls, 'w-10 shrink-0 min-h-[34px]')}>
108
104
  {parent.required ? '是' : '否'}
109
105
  </div>
110
106
  {/* 说明 */}
@@ -126,23 +122,10 @@ export function FieldsDesignInfoTable({ fields }: FieldsDesignInfoTableProps) {
126
122
  <div className="flex flex-col items-start w-full bg-white rounded-[10px] overflow-hidden text-black my-[10px] border border-[#E0E0E0]">
127
123
  {/* 表头 */}
128
124
  <div className="flex items-center w-full shrink-0 bg-[#E5E6EB] rounded-t-[10px] border-b border-[#E0E0E0]">
129
- <div className={cn(cellCls, 'w-20 shrink-0 h-[34px] font-bold')}>
130
- 名称
131
- </div>
132
- <div className={cn(cellCls, 'w-[72px] shrink-0 h-[34px] font-bold')}>
133
- 类型
134
- </div>
135
- <div
136
- className={cn(
137
- cellCls,
138
- 'w-9 shrink-0 h-[34px] font-bold justify-center'
139
- )}
140
- >
141
- 必填
142
- </div>
143
- <div className={cn(cellCls, 'flex-1 min-w-0 h-[34px] font-bold')}>
144
- 说明
145
- </div>
125
+ <div className={cn(cellCls, 'w-20 shrink-0 h-[34px] font-bold')}>名称</div>
126
+ <div className={cn(cellCls, 'w-[72px] shrink-0 h-[34px] font-bold')}>类型</div>
127
+ <div className={cn(cellCls, 'w-10 shrink-0 h-[34px] font-bold justify-center')}>必填</div>
128
+ <div className={cn(cellCls, 'flex-1 min-w-0 h-[34px] font-bold')}>说明</div>
146
129
  </div>
147
130
 
148
131
  {/* 表体 */}
@@ -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,10 +1,11 @@
1
- import { getIconUnicode } from "@baishuyun/ai-feat-utils";
1
+ import { getIconUnicode, FORM_ICON_UNICODE_MAP } from "@baishuyun/ai-feat-utils";
2
2
 
3
- function getFormIconClass(icon: number): string {
4
- const isNew = icon >= 25;
5
- const newStr = isNew ? '-new' : '';
6
- return `node-icon icon-bsy-form${newStr} icon${newStr}-bsy-form-${icon}`;
7
- }
3
+ // 数字表单图标个别色值:对齐宿主 FONT_ICON_STYLE 里的特殊色
4
+ // (PC 的 .icon-bsy-form-1 / .icon-bsy-form-2),未命中回落到默认蓝 #0265ff。
5
+ const FORM_ICON_COLOR: Record<number, string> = {
6
+ 1: '#ff6600',
7
+ 2: '#52c41a',
8
+ };
8
9
 
9
10
  export const AppIcon = ({
10
11
  name
@@ -31,7 +32,10 @@ export const FontIcon = ({
31
32
  style?: React.CSSProperties;
32
33
  color?: string;
33
34
  }) => {
34
- if (!code && !name) {
35
+ // 判空:只有 null/undefined 才算「无图标」。
36
+ // 注意不能用 !name——icon 为 0 是合法图标(表单默认 icon 0),
37
+ // !0 为真会把它误判成无图标,导致绝大多数表单图标不渲染。
38
+ if (!code && (name === undefined || name === null)) {
35
39
  return null;
36
40
  }
37
41
 
@@ -47,10 +51,17 @@ export const FontIcon = ({
47
51
 
48
52
  const iconCode = code || getIconUnicode(name?.toString() || '');
49
53
  if (!iconCode) {
54
+ // 数字表单图标:不用 .icon-bsy-form-N 的 :before 内码(部分环境
55
+ // shadow DOM / WebView 中 :before content 无法生成),改为把 unicode
56
+ // 字符直接放进文本内容(与 sdk-icon 同法),font-family 走 finex/finex-new
57
+ const unicode = FORM_ICON_UNICODE_MAP[name!];
58
+ const fontFamily = name! >= 25 ? 'finex-new' : 'finex';
59
+ const iconColor = FORM_ICON_COLOR[name!] ?? '#0265ff';
50
60
  return (
51
61
  <i
52
- className={getFormIconClass(name!)}
53
- style={{ fontSize: 16, lineHeight: 1 }}
62
+ className={className}
63
+ style={{ fontSize: 16, lineHeight: 1, fontStyle: 'normal', fontFamily, color: iconColor, ...style }}
64
+ dangerouslySetInnerHTML={{ __html: unicode ? `&#x${unicode};` : '' }}
54
65
  />
55
66
  )
56
67
  }
@@ -66,6 +66,7 @@ export interface FormInfoEditorProps {
66
66
  onMount?: () => void;
67
67
  readonly?: boolean;
68
68
  iconRenderer?: (icon: FormIconOption & { size: number}) => React.ReactNode; // Optional custom renderer for icons
69
+ mobile?: boolean;
69
70
  }
70
71
 
71
72
  /**
@@ -85,6 +86,7 @@ export const FormInfoEditor = memo(
85
86
  expanded: controlledExpanded,
86
87
  onExpandedChange,
87
88
  iconRenderer,
89
+ mobile,
88
90
  className,
89
91
  }: FormInfoEditorProps) => {
90
92
  const [internalExpanded, setInternalExpanded] = useState(false);
@@ -103,6 +105,16 @@ export const FormInfoEditor = memo(
103
105
  onMount?.();
104
106
  }, []);
105
107
 
108
+ const fontCls = cn({
109
+ 'text-[16px]': mobile,
110
+ 'text-[14px]': !mobile,
111
+ });
112
+
113
+ const sizeCls = cn({
114
+ 'w-[16px] h-[16px]': !mobile,
115
+ 'w-[20px] h-[20px]': !mobile
116
+ })
117
+
106
118
  // Default Status (Collapsed)
107
119
  if (!expanded || readonly) {
108
120
  return (
@@ -121,7 +133,7 @@ export const FormInfoEditor = memo(
121
133
  {/* Left side: Form icon and name */}
122
134
  <div className="flex items-center gap-[10px] min-w-0 flex-1 mr-[10px]">
123
135
  <FormIcon size={36} code={iconOpt?.code} render={iconRenderer} value={iconOpt?.value} />
124
- <p className="text-[14px] text-black leading-normal truncate">{nameVal}</p>
136
+ <p className={cn("text-black leading-normal truncate", fontCls)}>{nameVal}</p>
125
137
  </div>
126
138
 
127
139
  <button
@@ -130,19 +142,21 @@ export const FormInfoEditor = memo(
130
142
  setExpanded(true);
131
143
  }}
132
144
  className={cn(
133
- 'w-[16px] h-[16px] flex items-center justify-center cursor-pointer hover:text-[#0265ff]',
145
+ 'flex items-center justify-center cursor-pointer hover:text-[#0265ff]',
134
146
  transCls,
147
+ sizeCls,
135
148
  {
136
149
  'pointer-events-none opacity-0': readonly,
137
150
  }
138
151
  )}
139
152
  >
140
- <EditIcon />
153
+ <EditIcon size={mobile ? 20 : 16} />
141
154
  </button>
142
155
  </div>
143
156
  );
144
157
  }
145
158
 
159
+
146
160
  // Expanded Status (Edit mode)
147
161
  return (
148
162
  <div
@@ -155,7 +169,7 @@ export const FormInfoEditor = memo(
155
169
  >
156
170
  {/* Name field */}
157
171
  <div className="flex flex-col gap-[10px] w-full">
158
- <p className="text-[14px] text-[#030303] font-bold leading-normal">名称</p>
172
+ <p className={cn("text-[#030303] font-bold leading-normal", fontCls)}>名称</p>
159
173
  <input
160
174
  type="text"
161
175
  value={nameVal}
@@ -167,9 +181,10 @@ export const FormInfoEditor = memo(
167
181
  }}
168
182
  className={cn(
169
183
  'bg-white border border-[#e0e0e0] rounded-[4px] h-[34px] px-[10px] py-[7px] w-full',
170
- 'text-[14px] text-[#030303] leading-normal outline-none',
184
+ 'text-[#030303] leading-normal outline-none',
171
185
  'focus:border-[#0265ff]',
172
186
  'hover:border-[#0265ff]',
187
+ fontCls,
173
188
  transCls
174
189
  )}
175
190
  />
@@ -177,7 +192,7 @@ export const FormInfoEditor = memo(
177
192
 
178
193
  {/* Icon field */}
179
194
  <div className="flex flex-col gap-[10px] w-full">
180
- <p className="text-[14px] text-[#030303] font-bold leading-normal">图标</p>
195
+ <p className={cn("text-[#030303] font-bold leading-normal", fontCls)}>图标</p>
181
196
  <div className="relative" style={{ zIndex: 10 }}>
182
197
  <button
183
198
  onClick={() => setIsDropdownOpen(!isDropdownOpen)}
@@ -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>);