@baishuyun/ai-design 2.0.26 → 2.0.27
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 -14
- package/src/components/agent-helper/agent-helper-card-list/index.tsx +4 -5
- package/src/components/agent-scroll-selector.tsx +8 -3
- package/src/components/agent-select.tsx +3 -1
- package/src/components/bs-entry-select/build-tree.ts +49 -0
- package/src/components/bs-entry-select/entry-item.tsx +5 -3
- package/src/components/bs-entry-select/scroll-slider.tsx +443 -71
- package/src/components/bs-entry-select/types.ts +12 -0
- package/src/components/fake-bot-msg.tsx +1 -1
- package/src/components/fields-previewer.tsx +2 -2
- package/src/components/icons/empty-agent-icon.tsx +94 -0
- package/src/components/output-config-editor.tsx +1 -0
- package/src/index.ts +1 -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.27",
|
|
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/
|
|
64
|
-
"@baishuyun/
|
|
63
|
+
"@baishuyun/ai-feat-utils": "2.0.25",
|
|
64
|
+
"@baishuyun/types": "3.0.23"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"react": "^19.0.0",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"typescript": "^5.5.0",
|
|
84
84
|
"vite": "^5.1.4",
|
|
85
85
|
"vite-plugin-dts": "^4.5.4",
|
|
86
|
-
"@baishuyun/types": "3.0.
|
|
86
|
+
"@baishuyun/types": "3.0.23",
|
|
87
87
|
"@baishuyun/tailwind-config": "3.0.9",
|
|
88
88
|
"@baishuyun/typescript-config": "2.0.9"
|
|
89
89
|
},
|
|
@@ -81,21 +81,11 @@ function useTriggerDescription(helper: IAgentHelper, formService?: IBsFormServic
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function buildAgentInfo(agentName?: string, agentIcon?: string): IAgentInfo | undefined {
|
|
85
|
-
if (!agentName && !agentIcon) {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
id: '',
|
|
90
|
-
name: agentName || '',
|
|
91
|
-
icon: agentIcon || '',
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
84
|
export interface AgentHelperCardProps {
|
|
96
85
|
helper: IAgentHelper;
|
|
97
86
|
agentName?: string;
|
|
98
87
|
agentIcon?: string;
|
|
88
|
+
getAgent: (agentId: string) => IAgentInfo | undefined;
|
|
99
89
|
className?: string;
|
|
100
90
|
onEnabledChange?: (enabled: boolean) => void;
|
|
101
91
|
onViewLogs?: () => void;
|
|
@@ -108,18 +98,17 @@ export interface AgentHelperCardProps {
|
|
|
108
98
|
|
|
109
99
|
export const AgentHelperCard = ({
|
|
110
100
|
helper,
|
|
111
|
-
agentName,
|
|
112
|
-
agentIcon,
|
|
113
101
|
className,
|
|
114
102
|
onEnabledChange,
|
|
115
103
|
onViewLogs,
|
|
116
104
|
onEdit,
|
|
105
|
+
getAgent,
|
|
117
106
|
onCopy,
|
|
118
107
|
onDelete,
|
|
119
108
|
formService,
|
|
120
109
|
}: AgentHelperCardProps) => {
|
|
121
110
|
const description = useTriggerDescription(helper, formService);
|
|
122
|
-
const agent =
|
|
111
|
+
const agent = getAgent(helper.agentId);
|
|
123
112
|
|
|
124
113
|
return (
|
|
125
114
|
<div
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { cn } from '../../../lib/utils';
|
|
2
2
|
import { AgentHelperCard } from '../agent-helper-card';
|
|
3
|
-
import type { IAgentHelper, IBsFormService } from '@baishuyun/types';
|
|
3
|
+
import type { IAgentHelper, IAgentInfo, IBsFormService } from '@baishuyun/types';
|
|
4
4
|
|
|
5
5
|
export interface AgentHelperCardListProps {
|
|
6
6
|
items: Array<IAgentHelper>;
|
|
7
7
|
className?: string;
|
|
8
8
|
getAgentName?: (agentId: string) => string | undefined;
|
|
9
9
|
getAgentIcon?: (agentId: string) => string | undefined;
|
|
10
|
+
getAgent: (agentId: string) => (IAgentInfo | undefined);
|
|
10
11
|
onEnabledChange?: (helperId: string, enabled: boolean) => void;
|
|
11
12
|
onViewLogs?: (helperId: string) => void;
|
|
12
13
|
onEdit?: (helperId: string) => void;
|
|
@@ -18,8 +19,7 @@ export interface AgentHelperCardListProps {
|
|
|
18
19
|
export const AgentHelperCardList = ({
|
|
19
20
|
items,
|
|
20
21
|
className,
|
|
21
|
-
|
|
22
|
-
getAgentIcon,
|
|
22
|
+
getAgent,
|
|
23
23
|
onEnabledChange,
|
|
24
24
|
onViewLogs,
|
|
25
25
|
onEdit,
|
|
@@ -39,8 +39,7 @@ export const AgentHelperCardList = ({
|
|
|
39
39
|
formService={formService}
|
|
40
40
|
key={helper.id}
|
|
41
41
|
helper={helper}
|
|
42
|
-
|
|
43
|
-
agentIcon={getAgentIcon?.(helper.agentId)}
|
|
42
|
+
getAgent={getAgent}
|
|
44
43
|
className="w-[335px] shrink-0"
|
|
45
44
|
onEnabledChange={
|
|
46
45
|
onEnabledChange
|
|
@@ -145,9 +145,14 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
|
|
|
145
145
|
</>
|
|
146
146
|
);
|
|
147
147
|
|
|
148
|
+
const s = showSlider ? slider : null;
|
|
149
|
+
if (!s && !agents.length) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
148
153
|
return (
|
|
149
154
|
<div className={cn('flex items-center gap-2.5 w-fit max-w-full min-w-0', className)}>
|
|
150
|
-
{
|
|
155
|
+
{s}
|
|
151
156
|
{agents.length > 0 && (
|
|
152
157
|
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
|
153
158
|
<PopoverTrigger asChild>
|
|
@@ -155,7 +160,7 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
|
|
|
155
160
|
ref={setTriggerRef}
|
|
156
161
|
type="button"
|
|
157
162
|
className={cn(
|
|
158
|
-
'flex items-center gap-[6px] shrink-0 h-[34px]',
|
|
163
|
+
'flex items-center gap-[6px] shrink-0 h-[34px] w-[34px]',
|
|
159
164
|
'px-[10px] py-[5px] rounded-[4px]',
|
|
160
165
|
'text-[14px] text-[#121111] leading-[normal]',
|
|
161
166
|
'hover:bg-[#EFF0F6]',
|
|
@@ -166,7 +171,7 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
|
|
|
166
171
|
)}
|
|
167
172
|
>
|
|
168
173
|
<MenuLinesIcon />
|
|
169
|
-
<span>技能</span>
|
|
174
|
+
{/* <span>技能</span> */}
|
|
170
175
|
</button>
|
|
171
176
|
</PopoverTrigger>
|
|
172
177
|
<PopoverContent
|
|
@@ -10,6 +10,7 @@ export interface AgentSelectProps {
|
|
|
10
10
|
agents: Array<AgentSelectAgent>;
|
|
11
11
|
showAllItems?: boolean;
|
|
12
12
|
showSearch?: boolean;
|
|
13
|
+
className?: string;
|
|
13
14
|
activeAgent?: { id: string } | null;
|
|
14
15
|
onSelect: (agent: AgentSelectAgent) => void | Promise<void>;
|
|
15
16
|
}
|
|
@@ -25,6 +26,7 @@ export const AgentSelect = ({
|
|
|
25
26
|
activeAgent,
|
|
26
27
|
onSelect,
|
|
27
28
|
showAllItems,
|
|
29
|
+
className,
|
|
28
30
|
}: AgentSelectProps) => {
|
|
29
31
|
const [searchQuery, setSearchQuery] = useState('');
|
|
30
32
|
|
|
@@ -56,7 +58,7 @@ export const AgentSelect = ({
|
|
|
56
58
|
})
|
|
57
59
|
|
|
58
60
|
return (
|
|
59
|
-
<div className=
|
|
61
|
+
<div className={classNames('flex flex-col', 'rounded-[4px]', className)}>
|
|
60
62
|
{showSearch && (
|
|
61
63
|
<div className="px-1.5 pt-1 pb-0.5">
|
|
62
64
|
<div className="flex h-7.5 items-center gap-1.5 rounded-[4px] bg-white px-2.5 py-1.25 border border-[#E0E0E0]">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IAppDetail, IEntryAliasItem, IEntryDetail, IEntryRef } from '@baishuyun/types';
|
|
2
2
|
import type { EntryTreeItem, BsEntrySelectChangeResult } from './types';
|
|
3
|
+
import { FLOW_CENTER_GROUP_ID, FLOW_CENTER_ENTRIES } from './types';
|
|
3
4
|
import { FormSelectItem } from '../agent-helper/form-select';
|
|
4
5
|
|
|
5
6
|
export const DEPTH_PADDING: Record<number, string> = {
|
|
@@ -213,3 +214,51 @@ export function findSelected(
|
|
|
213
214
|
}
|
|
214
215
|
return null;
|
|
215
216
|
}
|
|
217
|
+
|
|
218
|
+
/** Create a virtual IEntryDetail for a flow center entry */
|
|
219
|
+
export function createFlowEntryDetail(entryId: string, name: string): IEntryDetail {
|
|
220
|
+
return {
|
|
221
|
+
_id: `${FLOW_CENTER_GROUP_ID}_${entryId}`,
|
|
222
|
+
entryId,
|
|
223
|
+
hasCoop: false,
|
|
224
|
+
hasFlow: true,
|
|
225
|
+
icon: 0,
|
|
226
|
+
isEvent: false,
|
|
227
|
+
isFavorite: false,
|
|
228
|
+
name,
|
|
229
|
+
parent: FLOW_CENTER_GROUP_ID,
|
|
230
|
+
seq: 0,
|
|
231
|
+
type: 'flow',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Build the synthetic "流程中心" group tree with 4 flow entries */
|
|
236
|
+
export function buildFlowCenterTree(): EntryTreeItem[] {
|
|
237
|
+
const children: EntryTreeItem[] = FLOW_CENTER_ENTRIES.map((item) => ({
|
|
238
|
+
// Use entryId directly as the node id so selected-state matching
|
|
239
|
+
// (selectedEntryId === node.id) works like real entries
|
|
240
|
+
id: item.entryId,
|
|
241
|
+
name: item.name,
|
|
242
|
+
icon: 0,
|
|
243
|
+
iconClass: `sdk-icon ${item.iconClass} text-[#0265ff]`,
|
|
244
|
+
type: 'flow',
|
|
245
|
+
children: [],
|
|
246
|
+
// depth 0: the group popover renders these as a flat list — no tree indent
|
|
247
|
+
depth: 0,
|
|
248
|
+
isSelectable: true,
|
|
249
|
+
entry: createFlowEntryDetail(item.entryId, item.name),
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
const groupNode: EntryTreeItem = {
|
|
253
|
+
id: FLOW_CENTER_GROUP_ID,
|
|
254
|
+
name: '流程中心',
|
|
255
|
+
icon: 0,
|
|
256
|
+
type: 'group',
|
|
257
|
+
depth: 0,
|
|
258
|
+
isSelectable: false,
|
|
259
|
+
entry: null,
|
|
260
|
+
children,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return [groupNode];
|
|
264
|
+
}
|
|
@@ -28,9 +28,11 @@ export function EntryItem({
|
|
|
28
28
|
<span className="flex items-center gap-[6px] min-w-0 flex-1">
|
|
29
29
|
<i
|
|
30
30
|
className={
|
|
31
|
-
item.
|
|
32
|
-
?
|
|
33
|
-
:
|
|
31
|
+
item.iconClass
|
|
32
|
+
? item.iconClass
|
|
33
|
+
: item.entry
|
|
34
|
+
? getEntryIconClass(item.entry)
|
|
35
|
+
: 'node-icon icon-bsy-form icon-new-bsy-form-25'
|
|
34
36
|
}
|
|
35
37
|
style={{ fontSize: 16, lineHeight: 1 }}
|
|
36
38
|
/>
|
|
@@ -4,16 +4,18 @@ import {
|
|
|
4
4
|
useEffect,
|
|
5
5
|
useCallback,
|
|
6
6
|
useMemo,
|
|
7
|
+
Fragment,
|
|
7
8
|
} from 'react';
|
|
8
9
|
import { createPortal } from 'react-dom';
|
|
9
10
|
import { cn } from '../../lib/utils';
|
|
10
11
|
import { transCls } from '../../const/ui';
|
|
11
12
|
import { IconBtn } from '../icon-btn';
|
|
12
|
-
import { LeftArrowIcon, SolidArrowDownIcon } from '../bs-icons';
|
|
13
|
+
import { LeftArrowIcon, SolidArrowDownIcon, DividerIcon } from '../bs-icons';
|
|
13
14
|
import {
|
|
14
15
|
buildEntryTreeFromAliasMap,
|
|
15
16
|
getEntryIconClass,
|
|
16
17
|
filterTreeBySearch,
|
|
18
|
+
buildFlowCenterTree,
|
|
17
19
|
} from './build-tree';
|
|
18
20
|
import { SearchInput } from './search-input';
|
|
19
21
|
import { TreeItemNode } from './tree-item-node';
|
|
@@ -73,10 +75,27 @@ export function ScrollSlider({
|
|
|
73
75
|
{ estimatedHeight: 340 },
|
|
74
76
|
);
|
|
75
77
|
|
|
78
|
+
const hoverCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
79
|
+
|
|
76
80
|
const isTabs = variant === 'tabs';
|
|
81
|
+
const showFlowCenter = isTabs && (appDetails[currentAppId ?? '']?.isFlow ?? false);
|
|
77
82
|
|
|
78
83
|
// Build first-level nodes from all apps (no search filter)
|
|
79
84
|
const flatNodes = useMemo<FlatNode[]>(() => {
|
|
85
|
+
const flowCenterNodes: FlatNode[] = [];
|
|
86
|
+
|
|
87
|
+
// Flow center — first tab in tabs mode for flow-type apps
|
|
88
|
+
if (showFlowCenter) {
|
|
89
|
+
const flowTree = buildFlowCenterTree();
|
|
90
|
+
flowCenterNodes.push(
|
|
91
|
+
...flowTree.map((node) => ({
|
|
92
|
+
node,
|
|
93
|
+
appId: currentAppId ?? '',
|
|
94
|
+
section: 'current' as const,
|
|
95
|
+
})),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
const appIds = Object.keys(appDetails);
|
|
81
100
|
const currentFirst: FlatNode[] = [];
|
|
82
101
|
const otherNodes: FlatNode[] = [];
|
|
@@ -93,12 +112,13 @@ export function ScrollSlider({
|
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
114
|
|
|
96
|
-
const merged = [...currentFirst, ...otherNodes];
|
|
97
|
-
return showCurrentAppEntriesOnly ? currentFirst : merged;
|
|
115
|
+
const merged = [...flowCenterNodes, ...currentFirst, ...otherNodes];
|
|
116
|
+
return showCurrentAppEntriesOnly ? [...flowCenterNodes, ...currentFirst] : merged;
|
|
98
117
|
}, [
|
|
99
118
|
appDetails,
|
|
100
119
|
currentAppId,
|
|
101
120
|
showCurrentAppEntriesOnly,
|
|
121
|
+
showFlowCenter,
|
|
102
122
|
Object.keys(appDetails)
|
|
103
123
|
.map((id) => (appDetails[id]?.entryList?.length ?? 0))
|
|
104
124
|
.join(','),
|
|
@@ -110,6 +130,17 @@ export function ScrollSlider({
|
|
|
110
130
|
return filterTreeBySearch(activeGroup.node.children, groupSearchQuery);
|
|
111
131
|
}, [activeGroup, groupSearchQuery]);
|
|
112
132
|
|
|
133
|
+
// Map from group pill node ID → FlatNode, for native hover delegation
|
|
134
|
+
const groupFlatNodeById = useMemo(() => {
|
|
135
|
+
const map = new Map<string, FlatNode>();
|
|
136
|
+
for (const fn of flatNodes) {
|
|
137
|
+
if (!fn.node.isSelectable) {
|
|
138
|
+
map.set(fn.node.id, fn);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return map;
|
|
142
|
+
}, [flatNodes]);
|
|
143
|
+
|
|
113
144
|
const checkScroll = useCallback(() => {
|
|
114
145
|
const el = scrollRef.current;
|
|
115
146
|
if (!el) return;
|
|
@@ -163,9 +194,52 @@ export function ScrollSlider({
|
|
|
163
194
|
) as HTMLElement | null;
|
|
164
195
|
}
|
|
165
196
|
}
|
|
166
|
-
if (pill)
|
|
167
|
-
|
|
168
|
-
|
|
197
|
+
if (!pill) return;
|
|
198
|
+
|
|
199
|
+
const container = scrollRef.current;
|
|
200
|
+
const PAD = 12; // matches scrollPaddingInline, covers corner flares
|
|
201
|
+
const start = performance.now();
|
|
202
|
+
let rafId = 0;
|
|
203
|
+
|
|
204
|
+
// Ease toward the pill with the target recomputed EVERY frame. A one-shot
|
|
205
|
+
// scrollIntoView({behavior: 'smooth'}) computes its destination upfront —
|
|
206
|
+
// the selection triggers a host navigation whose follow-up layout changes
|
|
207
|
+
// (content reload, arrow show/hide) shift the container mid-animation and
|
|
208
|
+
// the pill lands clipped. Live recomputation keeps this one continuous
|
|
209
|
+
// ease-out motion that always converges, with no visible second jump.
|
|
210
|
+
const step = () => {
|
|
211
|
+
const containerRect = container.getBoundingClientRect();
|
|
212
|
+
const pillRect = pill.getBoundingClientRect();
|
|
213
|
+
let delta = 0;
|
|
214
|
+
if (pillRect.left < containerRect.left + PAD) {
|
|
215
|
+
delta = pillRect.left - containerRect.left - PAD;
|
|
216
|
+
} else if (pillRect.right > containerRect.right - PAD) {
|
|
217
|
+
delta = pillRect.right - containerRect.right + PAD;
|
|
218
|
+
}
|
|
219
|
+
if (Math.abs(delta) < 1) return; // arrived
|
|
220
|
+
container.scrollLeft += delta * 0.3;
|
|
221
|
+
if (performance.now() - start < 600) {
|
|
222
|
+
rafId = requestAnimationFrame(step);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
rafId = requestAnimationFrame(step);
|
|
226
|
+
|
|
227
|
+
// Stop animating if the user takes over scrolling
|
|
228
|
+
const cancelOnUserScroll = () => cancelAnimationFrame(rafId);
|
|
229
|
+
container.addEventListener('wheel', cancelOnUserScroll, {
|
|
230
|
+
once: true,
|
|
231
|
+
passive: true,
|
|
232
|
+
});
|
|
233
|
+
container.addEventListener('touchstart', cancelOnUserScroll, {
|
|
234
|
+
once: true,
|
|
235
|
+
passive: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
cancelAnimationFrame(rafId);
|
|
240
|
+
container.removeEventListener('wheel', cancelOnUserScroll);
|
|
241
|
+
container.removeEventListener('touchstart', cancelOnUserScroll);
|
|
242
|
+
};
|
|
169
243
|
}, [selectedEntryId, descendantToGroupId]);
|
|
170
244
|
|
|
171
245
|
useEffect(() => {
|
|
@@ -195,46 +269,168 @@ export function ScrollSlider({
|
|
|
195
269
|
activeGroupIdRef.current = null;
|
|
196
270
|
setActiveGroup(null);
|
|
197
271
|
setGroupSearchQuery('');
|
|
272
|
+
if (hoverCloseTimerRef.current) {
|
|
273
|
+
clearTimeout(hoverCloseTimerRef.current);
|
|
274
|
+
hoverCloseTimerRef.current = null;
|
|
275
|
+
}
|
|
198
276
|
}, []);
|
|
199
277
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
setActiveGroup(null);
|
|
205
|
-
setGroupSearchQuery('');
|
|
206
|
-
return;
|
|
278
|
+
const cancelCloseTimer = useCallback(() => {
|
|
279
|
+
if (hoverCloseTimerRef.current) {
|
|
280
|
+
clearTimeout(hoverCloseTimerRef.current);
|
|
281
|
+
hoverCloseTimerRef.current = null;
|
|
207
282
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
// Delayed close — cancelled when the mouse re-enters the pill or popover.
|
|
286
|
+
// The :hover guard prevents closing when a transient leave fired but the
|
|
287
|
+
// cursor is still over the pill or popover (e.g. layout-shift artifacts).
|
|
288
|
+
const scheduleClosePopover = useCallback(() => {
|
|
289
|
+
cancelCloseTimer();
|
|
290
|
+
hoverCloseTimerRef.current = setTimeout(() => {
|
|
291
|
+
const pillEl = activeGroupIdRef.current
|
|
292
|
+
? groupPillRefs.current.get(activeGroupIdRef.current)
|
|
293
|
+
: null;
|
|
294
|
+
if (pillEl?.matches(':hover') || popoverRef.current?.matches(':hover')) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
closeGroupPopover();
|
|
298
|
+
}, 200);
|
|
299
|
+
}, [cancelCloseTimer, closeGroupPopover]);
|
|
300
|
+
|
|
301
|
+
// Core open logic shared by click and hover paths
|
|
302
|
+
const openGroupPopoverCore = useCallback(
|
|
303
|
+
(flatNode: FlatNode) => {
|
|
304
|
+
// Clear any pending close timer
|
|
305
|
+
cancelCloseTimer();
|
|
306
|
+
|
|
307
|
+
// Skip if already open for this group (hover keeps it open)
|
|
308
|
+
if (activeGroup?.node.id === flatNode.node.id) return;
|
|
309
|
+
|
|
310
|
+
setGroupSearchQuery('');
|
|
311
|
+
activeGroupIdRef.current = flatNode.node.id;
|
|
312
|
+
setActiveGroup(flatNode);
|
|
313
|
+
|
|
314
|
+
// Check Shadow DOM portal
|
|
315
|
+
const triggerEl = groupPillRefs.current.get(flatNode.node.id);
|
|
316
|
+
if (triggerEl) {
|
|
317
|
+
const rootNode = triggerEl.getRootNode();
|
|
318
|
+
if (rootNode instanceof ShadowRoot) {
|
|
319
|
+
const existing = rootNode.querySelector<HTMLElement>(
|
|
320
|
+
'[data-bs-entry-select-group-portal]',
|
|
321
|
+
);
|
|
322
|
+
if (existing) {
|
|
323
|
+
setGroupPortalContainer(existing);
|
|
324
|
+
} else {
|
|
325
|
+
const container = document.createElement('div');
|
|
326
|
+
container.setAttribute('data-bs-entry-select-group-portal', 'true');
|
|
327
|
+
rootNode.appendChild(container);
|
|
328
|
+
setGroupPortalContainer(container);
|
|
329
|
+
}
|
|
222
330
|
} else {
|
|
223
|
-
|
|
224
|
-
container.setAttribute('data-bs-entry-select-group-portal', 'true');
|
|
225
|
-
rootNode.appendChild(container);
|
|
226
|
-
setGroupPortalContainer(container);
|
|
331
|
+
setGroupPortalContainer(null);
|
|
227
332
|
}
|
|
228
|
-
} else {
|
|
229
|
-
setGroupPortalContainer(null);
|
|
230
333
|
}
|
|
231
|
-
}
|
|
232
334
|
|
|
233
|
-
|
|
234
|
-
|
|
335
|
+
// Position synchronously — the pill is already mounted, so its rect is
|
|
336
|
+
// available immediately. Batching this with setActiveGroup ensures the
|
|
337
|
+
// popover mounts at the correct position in the same commit; a deferred
|
|
338
|
+
// (rAF) position would render one frame in-flow at the end of the body,
|
|
339
|
+
// shifting layout and firing a spurious mouseleave on the pill.
|
|
235
340
|
updatePopoverPosition();
|
|
236
|
-
}
|
|
237
|
-
|
|
341
|
+
},
|
|
342
|
+
[activeGroup, cancelCloseTimer, updatePopoverPosition],
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const openGroupPopover = useCallback(
|
|
346
|
+
(flatNode: FlatNode, event: React.MouseEvent) => {
|
|
347
|
+
event.stopPropagation();
|
|
348
|
+
if (activeGroup?.node.id === flatNode.node.id) {
|
|
349
|
+
activeGroupIdRef.current = null;
|
|
350
|
+
setActiveGroup(null);
|
|
351
|
+
setGroupSearchQuery('');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
openGroupPopoverCore(flatNode);
|
|
355
|
+
},
|
|
356
|
+
[activeGroup, openGroupPopoverCore],
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
// Hover handlers for tabs mode.
|
|
360
|
+
//
|
|
361
|
+
// NOTE: these are driven by NATIVE mouseover/mouseout listeners (attached in
|
|
362
|
+
// the effects below), not React's onMouseEnter/onMouseLeave. React's
|
|
363
|
+
// synthetic enter/leave events do not fire when the pointer crosses from
|
|
364
|
+
// light DOM into a shadow root — which is exactly what happens when the
|
|
365
|
+
// mouse enters a tab from the host content area below the tab bar.
|
|
366
|
+
const handleNativeOver = useCallback(
|
|
367
|
+
(e: MouseEvent) => {
|
|
368
|
+
const t = e.target as HTMLElement | null;
|
|
369
|
+
if (!t) return;
|
|
370
|
+
if (popoverRef.current?.contains(t)) {
|
|
371
|
+
cancelCloseTimer();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const pillEl = t.closest?.('[data-entry-id]') as HTMLElement | null;
|
|
375
|
+
const flatNode = pillEl
|
|
376
|
+
? groupFlatNodeById.get(pillEl.getAttribute('data-entry-id') ?? '')
|
|
377
|
+
: undefined;
|
|
378
|
+
if (flatNode) {
|
|
379
|
+
cancelCloseTimer();
|
|
380
|
+
openGroupPopoverCore(flatNode);
|
|
381
|
+
} else {
|
|
382
|
+
// Hovering a leaf pill or empty slider area — close any open group popover
|
|
383
|
+
scheduleClosePopover();
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
[cancelCloseTimer, groupFlatNodeById, openGroupPopoverCore, scheduleClosePopover],
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const handleNativeOut = useCallback(
|
|
390
|
+
(e: MouseEvent) => {
|
|
391
|
+
const to = e.relatedTarget as HTMLElement | null;
|
|
392
|
+
if (to && scrollRef.current?.contains(to)) return;
|
|
393
|
+
if (to && popoverRef.current?.contains(to)) return;
|
|
394
|
+
scheduleClosePopover();
|
|
395
|
+
},
|
|
396
|
+
[scheduleClosePopover],
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// Keep latest handlers in refs so listeners stay attached across re-renders
|
|
400
|
+
const handleNativeOverRef = useRef(handleNativeOver);
|
|
401
|
+
const handleNativeOutRef = useRef(handleNativeOut);
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
handleNativeOverRef.current = handleNativeOver;
|
|
404
|
+
handleNativeOutRef.current = handleNativeOut;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Native hover listeners on the scroll container (delegated to group pills)
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
const el = scrollRef.current;
|
|
410
|
+
if (!el || !isTabs) return;
|
|
411
|
+
const over = (e: MouseEvent) => handleNativeOverRef.current(e);
|
|
412
|
+
const out = (e: MouseEvent) => handleNativeOutRef.current(e);
|
|
413
|
+
el.addEventListener('mouseover', over);
|
|
414
|
+
el.addEventListener('mouseout', out);
|
|
415
|
+
return () => {
|
|
416
|
+
el.removeEventListener('mouseover', over);
|
|
417
|
+
el.removeEventListener('mouseout', out);
|
|
418
|
+
};
|
|
419
|
+
}, [isTabs]);
|
|
420
|
+
|
|
421
|
+
// Native hover listeners on the group popover (rendered in a portal subtree)
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
const pop = popoverRef.current;
|
|
424
|
+
if (!isTabs || !activeGroup || !pop) return;
|
|
425
|
+
const over = (e: MouseEvent) => handleNativeOverRef.current(e);
|
|
426
|
+
const out = (e: MouseEvent) => handleNativeOutRef.current(e);
|
|
427
|
+
pop.addEventListener('mouseover', over);
|
|
428
|
+
pop.addEventListener('mouseout', out);
|
|
429
|
+
return () => {
|
|
430
|
+
pop.removeEventListener('mouseover', over);
|
|
431
|
+
pop.removeEventListener('mouseout', out);
|
|
432
|
+
};
|
|
433
|
+
}, [isTabs, activeGroup]);
|
|
238
434
|
|
|
239
435
|
const handleGroupPopoverSelect = useCallback(
|
|
240
436
|
(result: BsEntrySelectChangeResult) => {
|
|
@@ -302,19 +498,53 @@ export function ScrollSlider({
|
|
|
302
498
|
|
|
303
499
|
<div
|
|
304
500
|
ref={scrollRef}
|
|
501
|
+
// scroll-padding: scrollIntoView aligns pills this far from the
|
|
502
|
+
// container edge, keeping the selected tab's outward corner flares
|
|
503
|
+
// (protruding 9px) fully visible instead of clipped
|
|
504
|
+
style={isTabs ? { scrollPaddingInline: 12 } : undefined}
|
|
305
505
|
className={cn(
|
|
306
506
|
'flex-1 overflow-x-auto scrollbar-hide flex min-w-0',
|
|
307
507
|
isTabs
|
|
308
|
-
|
|
508
|
+
// px compensates -mx: keeps pills at the same visual position
|
|
509
|
+
// while letting bottom corner flares paint without being clipped
|
|
510
|
+
? 'items-end gap-0 overflow-y-hidden px-[10px] -mx-[10px] border-b-[#e0e0e0] before:content-[""] before:absolute before:bottom-[0px] before:left-[-200px] before:right-0 before:h-[1px] before:w-[200vw] before:bg-[#e0e0e0] h-full'
|
|
309
511
|
: 'items-center gap-2.5',
|
|
310
512
|
)}
|
|
311
513
|
>
|
|
312
|
-
{flatNodes.map((flatNode) => {
|
|
514
|
+
{flatNodes.map((flatNode, index) => {
|
|
313
515
|
const { node, appId, section } = flatNode;
|
|
314
516
|
const isLeaf = node.isSelectable;
|
|
315
517
|
const isSelected = isLeaf && selectedEntryId === node.id;
|
|
316
|
-
|
|
317
|
-
|
|
518
|
+
// Active tab styling applies only when a descendant entry is
|
|
519
|
+
// selected — hovering/clicking to open the popover must NOT make
|
|
520
|
+
// the group look active (that would show two active tabs at once)
|
|
521
|
+
const isGroupActive = node?.children.findIndex(c => c.id === selectedEntryId) >= 0;
|
|
522
|
+
// Popover-open is a separate, transient state — drives the chevron
|
|
523
|
+
const isGroupPopoverOpen = activeGroup?.node.id === node.id;
|
|
524
|
+
|
|
525
|
+
// Divider between inactive tabs: always rendered in tabs mode so
|
|
526
|
+
// the strip width never shifts on selection change — only its
|
|
527
|
+
// visibility toggles (hidden next to the active tab, which has
|
|
528
|
+
// its own borders)
|
|
529
|
+
const isNodeActive = (fn: FlatNode | null): boolean => {
|
|
530
|
+
if (!fn) return false;
|
|
531
|
+
return fn.node.isSelectable
|
|
532
|
+
? selectedEntryId === fn.node.id
|
|
533
|
+
: fn.node.children.findIndex((c) => c.id === selectedEntryId) >= 0;
|
|
534
|
+
};
|
|
535
|
+
const dividerVisible =
|
|
536
|
+
!isNodeActive(flatNode) && !isNodeActive(flatNodes[index - 1]);
|
|
537
|
+
const divider = isTabs && index > 0 ? (
|
|
538
|
+
<span
|
|
539
|
+
aria-hidden
|
|
540
|
+
className={cn(
|
|
541
|
+
'self-center shrink-0 flex items-center justify-center w-[1px]',
|
|
542
|
+
!dividerVisible && 'invisible',
|
|
543
|
+
)}
|
|
544
|
+
>
|
|
545
|
+
<DividerIcon />
|
|
546
|
+
</span>
|
|
547
|
+
) : null;
|
|
318
548
|
|
|
319
549
|
const setPillRef = (el: HTMLButtonElement | null) => {
|
|
320
550
|
if (el) {
|
|
@@ -326,8 +556,9 @@ export function ScrollSlider({
|
|
|
326
556
|
|
|
327
557
|
if (isLeaf) {
|
|
328
558
|
return (
|
|
329
|
-
<
|
|
330
|
-
|
|
559
|
+
<Fragment key={node.id}>
|
|
560
|
+
{divider}
|
|
561
|
+
<button
|
|
331
562
|
data-entry-id={node.id}
|
|
332
563
|
type="button"
|
|
333
564
|
onClick={() => {
|
|
@@ -336,65 +567,197 @@ export function ScrollSlider({
|
|
|
336
567
|
}
|
|
337
568
|
}}
|
|
338
569
|
className={cn(
|
|
339
|
-
'flex items-center
|
|
570
|
+
'flex items-center shrink-0 max-w-[240px]',
|
|
340
571
|
transCls,
|
|
341
572
|
isTabs
|
|
342
573
|
? [
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
574
|
+
// Button stays h-full so text keeps its Y position;
|
|
575
|
+
// the visual tab box is the decorative span below
|
|
576
|
+
'group px-[10px] py-[8px] relative z-10 h-full',
|
|
577
|
+
isSelected ? 'text-[#0265FF]' : 'text-[#030303]',
|
|
347
578
|
]
|
|
348
579
|
: [
|
|
349
|
-
'px-[10px] py-[7px] rounded-[4px]',
|
|
580
|
+
'px-[10px] py-[7px] rounded-[4px] gap-[6px]',
|
|
350
581
|
isSelected
|
|
351
582
|
? 'border border-[#F0F6FF] bg-[#F0F6FF] text-[#0265FF]'
|
|
352
583
|
: 'border border-[#E0E0E0] text-[#030303] hover:bg-[#F0F6FF]',
|
|
353
584
|
],
|
|
354
585
|
)}
|
|
355
586
|
>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
587
|
+
{/* Visual tab box — side borders stop 10px above the bar so
|
|
588
|
+
the concave corner arcs can continue them; transparent
|
|
589
|
+
borders when unselected (decorative only, no layout shift).
|
|
590
|
+
No transCls here: the fill strip and corner flares below
|
|
591
|
+
appear/disappear instantly on selection change, so this
|
|
592
|
+
box must switch instantly too — otherwise its fading
|
|
593
|
+
background leaves a ghost image on the deselected tab */}
|
|
594
|
+
{isTabs && (
|
|
595
|
+
<span
|
|
596
|
+
aria-hidden
|
|
597
|
+
className={cn(
|
|
598
|
+
'pointer-events-none absolute inset-x-0 top-[6px] bottom-[10px] border-r border-t border-l border-b-none rounded-t-[10px] rounded-b-none',
|
|
599
|
+
isSelected
|
|
600
|
+
? 'border-[#e0e0e0] bg-[#f6f7fa]'
|
|
601
|
+
: 'border-transparent',
|
|
602
|
+
)}
|
|
603
|
+
/>
|
|
604
|
+
)}
|
|
605
|
+
{isTabs && isSelected && (
|
|
606
|
+
<>
|
|
607
|
+
{/* Fill below the tab body — covers the bar line under the tab */}
|
|
608
|
+
<span
|
|
609
|
+
aria-hidden
|
|
610
|
+
className="pointer-events-none absolute inset-x-0 bottom-0 h-[10px] bg-[#f6f7fa]"
|
|
611
|
+
/>
|
|
612
|
+
{/* Concave corner flares. The square is shifted 1px
|
|
613
|
+
inward (left/right-[-9px]) so the full border ring —
|
|
614
|
+
including its tangent junctions with the side border
|
|
615
|
+
(vertical tangent) and the bar line (horizontal
|
|
616
|
+
tangent) — fits inside the element and paints above
|
|
617
|
+
the bottom fill strip */}
|
|
618
|
+
<span
|
|
619
|
+
aria-hidden
|
|
620
|
+
className="pointer-events-none absolute left-[-9px] bottom-0 w-[10px] h-[10px]"
|
|
621
|
+
style={{
|
|
622
|
+
background:
|
|
623
|
+
'radial-gradient(circle at 0% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
|
|
624
|
+
}}
|
|
625
|
+
/>
|
|
626
|
+
<span
|
|
627
|
+
aria-hidden
|
|
628
|
+
className="pointer-events-none absolute right-[-9px] bottom-0 w-[10px] h-[10px]"
|
|
629
|
+
style={{
|
|
630
|
+
background:
|
|
631
|
+
'radial-gradient(circle at 100% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
|
|
632
|
+
}}
|
|
633
|
+
/>
|
|
634
|
+
</>
|
|
635
|
+
)}
|
|
636
|
+
{/* In tabs mode, hover highlights only this icon+text chip —
|
|
637
|
+
the tab background itself stays unchanged. No transCls:
|
|
638
|
+
on selection the hover bg must clear instantly, otherwise
|
|
639
|
+
it fades out slowly and leaves a chip-shaped ghost on the
|
|
640
|
+
newly selected tab */}
|
|
641
|
+
<span
|
|
642
|
+
className={cn(
|
|
643
|
+
'flex items-center gap-[6px] min-w-0',
|
|
644
|
+
isTabs &&
|
|
645
|
+
cn(
|
|
646
|
+
'relative rounded-[6px] px-[20px] py-[6px]',
|
|
647
|
+
!isSelected && 'group-hover:bg-[#F0F6FF]',
|
|
648
|
+
),
|
|
649
|
+
)}
|
|
650
|
+
>
|
|
651
|
+
<i
|
|
652
|
+
className={
|
|
653
|
+
node.iconClass
|
|
654
|
+
? node.iconClass
|
|
655
|
+
: node.entry
|
|
656
|
+
? getEntryIconClass(node.entry)
|
|
657
|
+
: 'node-icon icon-bsy-form icon-new-bsy-form-25'
|
|
658
|
+
}
|
|
659
|
+
style={{ fontSize: 16, lineHeight: 1 }}
|
|
660
|
+
/>
|
|
661
|
+
<span className="text-[14px] leading-[normal] truncate">
|
|
662
|
+
{node.name}
|
|
663
|
+
</span>
|
|
366
664
|
</span>
|
|
367
665
|
</button>
|
|
666
|
+
</Fragment>
|
|
368
667
|
);
|
|
369
668
|
}
|
|
370
669
|
|
|
371
670
|
// Group pill (has descendants)
|
|
372
671
|
return (
|
|
672
|
+
<Fragment key={node.id}>
|
|
673
|
+
{divider}
|
|
373
674
|
<button
|
|
374
|
-
key={node.id}
|
|
375
675
|
data-entry-id={node.id}
|
|
376
676
|
ref={setPillRef}
|
|
377
677
|
type="button"
|
|
378
|
-
|
|
678
|
+
{...(!isTabs
|
|
679
|
+
? {
|
|
680
|
+
onClick: (e: React.MouseEvent) => openGroupPopover(flatNode, e),
|
|
681
|
+
}
|
|
682
|
+
: {})}
|
|
379
683
|
className={cn(
|
|
380
|
-
'flex items-center
|
|
684
|
+
'flex items-center shrink-0 max-w-[240px]',
|
|
381
685
|
transCls,
|
|
382
686
|
isTabs
|
|
383
687
|
? [
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
688
|
+
// Button stays h-full so text keeps its Y position;
|
|
689
|
+
// the visual tab box is the decorative span below
|
|
690
|
+
'group gap-[4px] px-[10px] py-[8px] relative z-10 h-full',
|
|
691
|
+
isGroupActive ? 'text-[#0265FF]' : 'text-[#030303]',
|
|
388
692
|
]
|
|
389
693
|
: [
|
|
390
|
-
'px-[10px] py-[7px] rounded-[4px]',
|
|
694
|
+
'gap-[10px] px-[10px] py-[7px] rounded-[4px]',
|
|
391
695
|
isGroupActive
|
|
392
696
|
? 'border border-[#0265FF] text-[#030303]'
|
|
393
697
|
: 'border border-[#E0E0E0] text-[#030303] hover:bg-[#F0F6FF]',
|
|
394
698
|
],
|
|
395
699
|
)}
|
|
396
700
|
>
|
|
397
|
-
|
|
701
|
+
{/* Visual tab box — side borders stop 10px above the bar so
|
|
702
|
+
the concave corner arcs can continue them; transparent
|
|
703
|
+
borders when inactive (decorative only, no layout shift) */}
|
|
704
|
+
{isTabs && (
|
|
705
|
+
<span
|
|
706
|
+
aria-hidden
|
|
707
|
+
className={cn(
|
|
708
|
+
'pointer-events-none absolute inset-x-0 top-[6px] bottom-[10px] border-r border-t border-l border-b-none rounded-t-[10px] rounded-b-none',
|
|
709
|
+
isGroupActive
|
|
710
|
+
? 'border-[#e0e0e0] bg-[#f6f7fa]'
|
|
711
|
+
: 'border-transparent',
|
|
712
|
+
)}
|
|
713
|
+
/>
|
|
714
|
+
)}
|
|
715
|
+
{isTabs && isGroupActive && (
|
|
716
|
+
<>
|
|
717
|
+
{/* Fill below the tab body — covers the bar line under the tab */}
|
|
718
|
+
<span
|
|
719
|
+
aria-hidden
|
|
720
|
+
className="pointer-events-none absolute inset-x-0 bottom-0 h-[10px] bg-[#f6f7fa]"
|
|
721
|
+
/>
|
|
722
|
+
{/* Concave corner flares. The square is shifted 1px
|
|
723
|
+
inward (left/right-[-9px]) so the full border ring —
|
|
724
|
+
including its tangent junctions with the side border
|
|
725
|
+
(vertical tangent) and the bar line (horizontal
|
|
726
|
+
tangent) — fits inside the element and paints above
|
|
727
|
+
the bottom fill strip */}
|
|
728
|
+
<span
|
|
729
|
+
aria-hidden
|
|
730
|
+
className="pointer-events-none absolute left-[-9px] bottom-0 w-[10px] h-[10px]"
|
|
731
|
+
style={{
|
|
732
|
+
background:
|
|
733
|
+
'radial-gradient(circle at 0% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
|
|
734
|
+
}}
|
|
735
|
+
/>
|
|
736
|
+
<span
|
|
737
|
+
aria-hidden
|
|
738
|
+
className="pointer-events-none absolute right-[-9px] bottom-0 w-[10px] h-[10px]"
|
|
739
|
+
style={{
|
|
740
|
+
background:
|
|
741
|
+
'radial-gradient(circle at 100% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
|
|
742
|
+
}}
|
|
743
|
+
/>
|
|
744
|
+
</>
|
|
745
|
+
)}
|
|
746
|
+
{/* In tabs mode, hover highlights only this icon+text chip —
|
|
747
|
+
the tab background itself stays unchanged. No transCls:
|
|
748
|
+
on activation the hover bg must clear instantly, otherwise
|
|
749
|
+
it fades out slowly and leaves a chip-shaped ghost on the
|
|
750
|
+
newly activated tab */}
|
|
751
|
+
<span
|
|
752
|
+
className={cn(
|
|
753
|
+
'flex items-center gap-[6px] min-w-0 flex-1',
|
|
754
|
+
isTabs &&
|
|
755
|
+
cn(
|
|
756
|
+
'relative rounded-[6px] px-[20px] py-[6px]',
|
|
757
|
+
!isGroupActive && 'group-hover:bg-[#F0F6FF]',
|
|
758
|
+
),
|
|
759
|
+
)}
|
|
760
|
+
>
|
|
398
761
|
<i
|
|
399
762
|
className="icon-bsy-folder-close"
|
|
400
763
|
style={{ fontSize: 16, lineHeight: 1 }}
|
|
@@ -405,14 +768,15 @@ export function ScrollSlider({
|
|
|
405
768
|
</span>
|
|
406
769
|
<span
|
|
407
770
|
className={cn(
|
|
408
|
-
'shrink-0 flex items-center justify-center w-[16px] h-[16px] text-[#666666]',
|
|
771
|
+
'relative shrink-0 flex items-center justify-center w-[16px] h-[16px] text-[#666666]',
|
|
409
772
|
transCls,
|
|
410
|
-
|
|
773
|
+
isGroupPopoverOpen && 'rotate-90',
|
|
411
774
|
)}
|
|
412
775
|
>
|
|
413
776
|
<ChevronRightIcon size={16} />
|
|
414
777
|
</span>
|
|
415
778
|
</button>
|
|
779
|
+
</Fragment>
|
|
416
780
|
);
|
|
417
781
|
})}
|
|
418
782
|
</div>
|
|
@@ -463,6 +827,14 @@ export function ScrollSlider({
|
|
|
463
827
|
transCls,
|
|
464
828
|
)}
|
|
465
829
|
>
|
|
830
|
+
{/* Transparent hover bridges: keep the mouse "inside" the popover
|
|
831
|
+
while crossing the gap between pill and popover */}
|
|
832
|
+
{isTabs && (
|
|
833
|
+
<>
|
|
834
|
+
<div className="absolute left-0 right-0 -top-[6px] h-[6px]" />
|
|
835
|
+
<div className="absolute left-0 right-0 -bottom-[6px] h-[6px]" />
|
|
836
|
+
</>
|
|
837
|
+
)}
|
|
466
838
|
<SearchInput
|
|
467
839
|
placeholder='搜索表单名称'
|
|
468
840
|
value={groupSearchQuery}
|
|
@@ -6,6 +6,16 @@ export interface BsEntrySelectChangeResult {
|
|
|
6
6
|
section: 'current' | 'other';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/** Flow center — synthetic group tab for flow-type apps in tabs mode */
|
|
10
|
+
export const FLOW_CENTER_GROUP_ID = '__flow_center__';
|
|
11
|
+
|
|
12
|
+
export const FLOW_CENTER_ENTRIES = [
|
|
13
|
+
{ entryId: 'todo', name: '待办任务', iconClass: 'icon-new-bsy-todo' },
|
|
14
|
+
{ entryId: 'cc', name: '抄送我的', iconClass: 'icon-new-bsy-cc' },
|
|
15
|
+
{ entryId: 'transactors', name: '我处理的', iconClass: 'icon-new-bsy-transactors' },
|
|
16
|
+
{ entryId: 'create', name: '我发起的', iconClass: 'icon-new-bsy-create' },
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
9
19
|
export type BsEntrySelectSliderVariant = 'default' | 'tabs';
|
|
10
20
|
|
|
11
21
|
export interface BsEntrySelectProps {
|
|
@@ -31,4 +41,6 @@ export interface EntryTreeItem {
|
|
|
31
41
|
depth: number;
|
|
32
42
|
isSelectable: boolean;
|
|
33
43
|
entry: IEntryDetail | null;
|
|
44
|
+
/** Optional custom icon CSS class — used for synthetic entries like flow center */
|
|
45
|
+
iconClass?: string;
|
|
34
46
|
}
|
|
@@ -17,7 +17,7 @@ export const FakeBotMessage = ({
|
|
|
17
17
|
<div>
|
|
18
18
|
<MessageContent
|
|
19
19
|
className={cn(
|
|
20
|
-
'
|
|
20
|
+
'wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left w-full',
|
|
21
21
|
className
|
|
22
22
|
)}
|
|
23
23
|
data-testid="message-content"
|
|
@@ -203,8 +203,8 @@ export const FieldsPreviewer = forwardRef<HTMLDivElement, FieldsPreviewerProps>(
|
|
|
203
203
|
<AbsFullscreenGradientBg
|
|
204
204
|
zIndex={zIndex}
|
|
205
205
|
padding={isFields ? p : 0}
|
|
206
|
-
disableGradient
|
|
207
|
-
bgColor=
|
|
206
|
+
disableGradient
|
|
207
|
+
bgColor="#f6f7fa"
|
|
208
208
|
ref={setAbsContainer}
|
|
209
209
|
>
|
|
210
210
|
{fullscreen ? contentJsx : content}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export const EmptyAgentIcon = () => (
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="155" height="97" viewBox="0 0 155 97" fill="none">
|
|
3
|
+
<mask id="mask0_33687_60435" style={{maskType: 'alpha'}} maskUnits="userSpaceOnUse" x="0" y="71" width="155" height="19">
|
|
4
|
+
<rect x="0.6875" y="71.3262" width="153.628" height="17.8319" fill="white"/>
|
|
5
|
+
</mask>
|
|
6
|
+
<g mask="url(#mask0_33687_60435)">
|
|
7
|
+
<ellipse cx="77.5017" cy="85.9947" rx="76.8142" ry="14.6686" fill="url(#paint0_linear_33687_60435)"/>
|
|
8
|
+
</g>
|
|
9
|
+
<ellipse cx="123.117" cy="60.3534" rx="6.51549" ry="8.91593" fill="url(#paint1_linear_33687_60435)"/>
|
|
10
|
+
<path d="M122.891 79.2946V52.6367L123.672 79.2946H122.891Z" fill="#D5DDF4"/>
|
|
11
|
+
<path d="M123.003 67.5213L118.875 63.7448L123.003 66.2995V67.5213Z" fill="#D5DDF4"/>
|
|
12
|
+
<path d="M125.011 59.7461L123.003 61.9676V62.7451L125.011 59.7461Z" fill="#D5DDF4"/>
|
|
13
|
+
<path d="M123.016 78.6367L103.15 79.296H123.016V78.6367Z" fill="url(#paint2_linear_33687_60435)"/>
|
|
14
|
+
<ellipse cx="38.4748" cy="66.1104" rx="3.77212" ry="4.80088" transform="rotate(-4.32208 38.4748 66.1104)" fill="url(#paint3_linear_33687_60435)"/>
|
|
15
|
+
<path d="M39.065 75.831L38.1016 63.084L38.1402 75.7356L39.065 75.831Z" fill="#D5DDF4"/>
|
|
16
|
+
<path d="M38.5854 70.1526L40.4091 68.2122L38.5415 69.5727L38.5854 70.1526Z" fill="#D5DDF4"/>
|
|
17
|
+
<path d="M37.3534 66.5346L38.3862 67.5168L38.414 67.8858L37.3534 66.5346Z" fill="#D5DDF4"/>
|
|
18
|
+
<path d="M38.9297 75.1816L17.0783 77.329L38.9794 75.839L38.9297 75.1816Z" fill="url(#paint4_linear_33687_60435)"/>
|
|
19
|
+
<path d="M96.0119 69.9553L100.847 50.027C101.266 48.3002 99.9583 46.6367 98.1814 46.6367H55.7188L59.1062 69.9553H96.0119Z" fill="#90ACF8"/>
|
|
20
|
+
<path d="M96.0119 69.9553L100.847 50.027C101.266 48.3002 99.9583 46.6367 98.1814 46.6367H55.7188L59.1062 69.9553H96.0119Z" fill="#90ACF8" fill-opacity="0.2"/>
|
|
21
|
+
<rect x="76.3438" y="32.0664" width="24.0044" height="28.9571" rx="1.37168" transform="rotate(14.142 76.3438 32.0664)" fill="url(#paint5_linear_33687_60435)" fill-opacity="0.2"/>
|
|
22
|
+
<rect x="68.1953" y="38.3086" width="23.6252" height="23.6724" rx="1.37168" transform="rotate(14.142 68.1953 38.3086)" fill="#90ACF8" fill-opacity="0.2"/>
|
|
23
|
+
<g filter="url(#filter3_d_33687_60435)">
|
|
24
|
+
<path d="M56.6096 69.7845L53.8535 42.3508C53.7521 41.3412 54.5448 40.4648 55.5595 40.4648H62.7559C63.2633 40.4648 63.7446 40.6895 64.0703 41.0785L66.468 43.941C66.9893 44.5633 67.7593 44.9228 68.5711 44.9228H92.1169C93.0009 44.9228 93.7401 45.595 93.8238 46.4752L96.0078 69.4508C96.1035 70.4573 95.3119 71.3277 94.3009 71.3277H58.3156C57.435 71.3277 56.6976 70.6606 56.6096 69.7845Z" fill="#F0F4FE"/>
|
|
25
|
+
</g>
|
|
26
|
+
<path d="M56.6096 69.7845L53.8535 42.3508C53.7521 41.3412 54.5448 40.4648 55.5595 40.4648H62.7559C63.2633 40.4648 63.7446 40.6895 64.0703 41.0785L66.468 43.941C66.9893 44.5633 67.7593 44.9228 68.5711 44.9228H92.1169C93.0009 44.9228 93.7401 45.595 93.8238 46.4752L96.0078 69.4508C96.1035 70.4573 95.3119 71.3277 94.3009 71.3277H58.3156C57.435 71.3277 56.6976 70.6606 56.6096 69.7845Z" fill="#F0F4FE" fill-opacity="0.2"/>
|
|
27
|
+
<mask id="mask1_33687_60435" style={{maskType: 'alpha'}} maskUnits="userSpaceOnUse" x="53" y="40" width="44" height="32">
|
|
28
|
+
<path d="M56.6096 69.7845L53.8535 42.3508C53.7521 41.3412 54.5448 40.4648 55.5595 40.4648H62.7559C63.2633 40.4648 63.7446 40.6895 64.0703 41.0785L66.468 43.941C66.9893 44.5633 67.7593 44.9228 68.5711 44.9228H92.1169C93.0009 44.9228 93.7401 45.595 93.8238 46.4752L96.0078 69.4508C96.1035 70.4573 95.3119 71.3277 94.3009 71.3277H58.3156C57.435 71.3277 56.6976 70.6606 56.6096 69.7845Z" fill="#F0F4FE"/>
|
|
29
|
+
</mask>
|
|
30
|
+
<g mask="url(#mask1_33687_60435)">
|
|
31
|
+
<g filter="url(#filter5_f_33687_60435)">
|
|
32
|
+
<circle cx="59.322" cy="40.8084" r="15.4314" fill="#B9FAFF"/>
|
|
33
|
+
</g>
|
|
34
|
+
</g>
|
|
35
|
+
<rect x="61.7266" y="53.4961" width="18.5177" height="2.74336" rx="0.685841" fill="#CEDCF6"/>
|
|
36
|
+
<rect x="62.4062" y="60.3555" width="11.6593" height="2.74336" rx="0.685841" fill="#CEDCF6"/>
|
|
37
|
+
<path d="M59.0767 34.0097C58.2476 33.5999 56.7207 33.7515 56.7207 33.7515C56.7207 33.7515 57.5181 35.3518 58.4504 35.8866C59.2981 36.3729 60.9114 36.3533 60.9114 36.3533C60.9114 36.3533 60.1207 34.5257 59.0767 34.0097Z" fill="url(#paint6_linear_33687_60435)"/>
|
|
38
|
+
<path d="M63.2906 34.3064C63.3572 33.7574 62.9649 32.928 62.9649 32.928C62.9649 32.928 62.2156 33.698 62.0988 34.3302C61.9925 34.905 62.3271 35.8094 62.3271 35.8094C62.3271 35.8094 63.2067 34.9976 63.2906 34.3064Z" fill="url(#paint7_linear_33687_60435)"/>
|
|
39
|
+
<path d="M108.994 42.7749C110.54 41.9201 111.955 39.3643 111.955 39.3643C111.955 39.3643 108.562 38.8796 106.707 39.7606C105.02 40.5616 103.304 43.1097 103.304 43.1097C103.304 43.1097 107.048 43.851 108.994 42.7749Z" fill="url(#paint8_linear_33687_60435)"/>
|
|
40
|
+
<defs>
|
|
41
|
+
<filter id="filter3_d_33687_60435" x="53.8457" y="40.4648" width="46.285" height="34.9783" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
42
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
43
|
+
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
44
|
+
<feOffset dx="2.05752" dy="2.05752"/>
|
|
45
|
+
<feGaussianBlur stdDeviation="1.02876"/>
|
|
46
|
+
<feComposite in2="hardAlpha" operator="out"/>
|
|
47
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0.564706 0 0 0 0 0.67451 0 0 0 0 0.972549 0 0 0 0.2 0"/>
|
|
48
|
+
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_33687_60435"/>
|
|
49
|
+
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_33687_60435" result="shape"/>
|
|
50
|
+
</filter>
|
|
51
|
+
<filter id="filter5_f_33687_60435" x="-0.00318146" y="-18.5169" width="118.651" height="118.651" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
52
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
53
|
+
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
54
|
+
<feGaussianBlur stdDeviation="21.9469" result="effect1_foregroundBlur_33687_60435"/>
|
|
55
|
+
</filter>
|
|
56
|
+
<linearGradient id="paint0_linear_33687_60435" x1="77.5017" y1="71.3262" x2="77.5017" y2="86.1595" gradientUnits="userSpaceOnUse">
|
|
57
|
+
<stop stop-color="#E6EEFF"/>
|
|
58
|
+
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
|
59
|
+
</linearGradient>
|
|
60
|
+
<linearGradient id="paint1_linear_33687_60435" x1="123.117" y1="51.4375" x2="123.117" y2="69.2694" gradientUnits="userSpaceOnUse">
|
|
61
|
+
<stop stop-color="#E7EFFF"/>
|
|
62
|
+
<stop offset="1" stop-color="#F3F7FD"/>
|
|
63
|
+
</linearGradient>
|
|
64
|
+
<linearGradient id="paint2_linear_33687_60435" x1="123.016" y1="78.9663" x2="103.15" y2="78.9663" gradientUnits="userSpaceOnUse">
|
|
65
|
+
<stop stop-color="#D5DDF4"/>
|
|
66
|
+
<stop offset="1" stop-color="#EDF2FF"/>
|
|
67
|
+
</linearGradient>
|
|
68
|
+
<linearGradient id="paint3_linear_33687_60435" x1="38.4748" y1="61.3095" x2="38.4748" y2="70.9113" gradientUnits="userSpaceOnUse">
|
|
69
|
+
<stop stop-color="#E7EFFF"/>
|
|
70
|
+
<stop offset="1" stop-color="#F3F7FD"/>
|
|
71
|
+
</linearGradient>
|
|
72
|
+
<linearGradient id="paint4_linear_33687_60435" x1="38.8078" y1="75.3561" x2="17.0534" y2="77.0003" gradientUnits="userSpaceOnUse">
|
|
73
|
+
<stop stop-color="#D5DDF4"/>
|
|
74
|
+
<stop offset="1" stop-color="#EDF2FF"/>
|
|
75
|
+
</linearGradient>
|
|
76
|
+
<linearGradient id="paint5_linear_33687_60435" x1="88.346" y1="32.0664" x2="88.346" y2="61.0235" gradientUnits="userSpaceOnUse">
|
|
77
|
+
<stop stop-color="#E5FFFF"/>
|
|
78
|
+
<stop offset="1" stop-color="#90ACF8"/>
|
|
79
|
+
</linearGradient>
|
|
80
|
+
<linearGradient id="paint6_linear_33687_60435" x1="58.5152" y1="35.9539" x2="59.1557" y2="34.0174" gradientUnits="userSpaceOnUse">
|
|
81
|
+
<stop stop-color="#DBE6FF"/>
|
|
82
|
+
<stop offset="1" stop-color="#F1F5FF"/>
|
|
83
|
+
</linearGradient>
|
|
84
|
+
<linearGradient id="paint7_linear_33687_60435" x1="62.0735" y1="34.3802" x2="63.3021" y2="34.3556" gradientUnits="userSpaceOnUse">
|
|
85
|
+
<stop stop-color="#DBE6FF"/>
|
|
86
|
+
<stop offset="1" stop-color="#F1F5FF"/>
|
|
87
|
+
</linearGradient>
|
|
88
|
+
<linearGradient id="paint8_linear_33687_60435" x1="106.53" y1="39.7891" x2="108.875" y2="42.9064" gradientUnits="userSpaceOnUse">
|
|
89
|
+
<stop stop-color="#DBE6FF"/>
|
|
90
|
+
<stop offset="1" stop-color="#F1F5FF"/>
|
|
91
|
+
</linearGradient>
|
|
92
|
+
</defs>
|
|
93
|
+
</svg>
|
|
94
|
+
)
|
|
@@ -105,6 +105,7 @@ function findFieldByName(fields: OutputFieldOption[], fieldName: string): Output
|
|
|
105
105
|
const VALID_TYPE_MAP: Partial<Record<FieldType, Set<FieldType>>> = {
|
|
106
106
|
'text': new Set(['text', 'textarea']),
|
|
107
107
|
'number': new Set(['text', 'textarea']),
|
|
108
|
+
'radio': new Set(['radio', 'radiogroup']),
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
const EMPTY_SET = new Set();
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type { FormIconOption, LegacySlotRenderer } from './types';
|
|
|
20
20
|
|
|
21
21
|
// Icons
|
|
22
22
|
export { CalendarIcon } from './components/icons/calendar-icon';
|
|
23
|
+
export { EmptyAgentIcon } from './components/icons/empty-agent-icon';
|
|
23
24
|
|
|
24
25
|
// UI primitives (shadcn)
|
|
25
26
|
export * from './components/ui/button';
|