@baishuyun/ai-design 1.0.1 → 1.0.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.
Files changed (111) hide show
  1. package/.turbo/turbo-build.log +16 -14
  2. package/CHANGELOG.md +15 -0
  3. package/dist/ai-design.js +6255 -5314
  4. package/dist/ai-design.js.map +1 -1
  5. package/dist/ai-design.umd.cjs +67 -67
  6. package/dist/ai-design.umd.cjs.map +1 -1
  7. package/dist/src/components/agent-scroll-selector.d.ts +2 -1
  8. package/dist/src/components/agent-scroll-selector.d.ts.map +1 -1
  9. package/dist/src/components/bs-entry-select/app-group-folder.d.ts +6 -0
  10. package/dist/src/components/bs-entry-select/app-group-folder.d.ts.map +1 -0
  11. package/dist/src/components/bs-entry-select/bs-entry-select.d.ts +4 -0
  12. package/dist/src/components/bs-entry-select/bs-entry-select.d.ts.map +1 -0
  13. package/dist/src/components/bs-entry-select/build-tree.d.ts +9 -0
  14. package/dist/src/components/bs-entry-select/build-tree.d.ts.map +1 -0
  15. package/dist/src/components/bs-entry-select/entry-item.d.ts +8 -0
  16. package/dist/src/components/bs-entry-select/entry-item.d.ts.map +1 -0
  17. package/dist/src/components/bs-entry-select/entry-popover.d.ts +17 -0
  18. package/dist/src/components/bs-entry-select/entry-popover.d.ts.map +1 -0
  19. package/dist/src/components/bs-entry-select/index.d.ts +3 -0
  20. package/dist/src/components/bs-entry-select/index.d.ts.map +1 -0
  21. package/dist/src/components/bs-entry-select/other-app-section.d.ts +11 -0
  22. package/dist/src/components/bs-entry-select/other-app-section.d.ts.map +1 -0
  23. package/dist/src/components/bs-entry-select/scroll-slider.d.ts +10 -0
  24. package/dist/src/components/bs-entry-select/scroll-slider.d.ts.map +1 -0
  25. package/dist/src/components/bs-entry-select/search-input.d.ts +5 -0
  26. package/dist/src/components/bs-entry-select/search-input.d.ts.map +1 -0
  27. package/dist/src/components/bs-entry-select/tree-item-node.d.ts +9 -0
  28. package/dist/src/components/bs-entry-select/tree-item-node.d.ts.map +1 -0
  29. package/dist/src/components/{bs-entry-select.d.ts → bs-entry-select/types.d.ts} +12 -2
  30. package/dist/src/components/bs-entry-select/types.d.ts.map +1 -0
  31. package/dist/src/components/bs-icons.d.ts +2 -0
  32. package/dist/src/components/bs-icons.d.ts.map +1 -1
  33. package/dist/src/components/collapsible.d.ts +6 -0
  34. package/dist/src/components/collapsible.d.ts.map +1 -0
  35. package/dist/src/components/field-checker-list-msg.d.ts +19 -0
  36. package/dist/src/components/field-checker-list-msg.d.ts.map +1 -0
  37. package/dist/src/components/field-checker.d.ts +19 -0
  38. package/dist/src/components/field-checker.d.ts.map +1 -0
  39. package/dist/src/components/field-icon.d.ts +5 -0
  40. package/dist/src/components/field-icon.d.ts.map +1 -0
  41. package/dist/src/components/field-value-checker.d.ts +16 -0
  42. package/dist/src/components/field-value-checker.d.ts.map +1 -0
  43. package/dist/src/components/legacy-slot.d.ts.map +1 -1
  44. package/dist/src/components/office-container.d.ts +5 -0
  45. package/dist/src/components/office-container.d.ts.map +1 -1
  46. package/dist/src/components/subform-fields-value-checker.d.ts +22 -0
  47. package/dist/src/components/subform-fields-value-checker.d.ts.map +1 -0
  48. package/dist/src/const/ui.d.ts +3 -0
  49. package/dist/src/const/ui.d.ts.map +1 -1
  50. package/dist/src/hooks/use-column-appear.d.ts +22 -0
  51. package/dist/src/hooks/use-column-appear.d.ts.map +1 -0
  52. package/dist/src/hooks/use-resizable-width.d.ts +3 -1
  53. package/dist/src/hooks/use-resizable-width.d.ts.map +1 -1
  54. package/dist/src/index.d.ts +11 -2
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/lib/utils.d.ts +3 -0
  57. package/dist/src/lib/utils.d.ts.map +1 -1
  58. package/dist/src/stories/BsEntrySelect.stories.d.ts +4 -0
  59. package/dist/src/stories/BsEntrySelect.stories.d.ts.map +1 -1
  60. package/dist/src/stories/OfficeContainer.stories.d.ts +5 -0
  61. package/dist/src/stories/OfficeContainer.stories.d.ts.map +1 -1
  62. package/dist/src/stories/collapsible.stories.d.ts +8 -0
  63. package/dist/src/stories/collapsible.stories.d.ts.map +1 -0
  64. package/dist/src/stories/field-checker-list-msg.stories.d.ts +11 -0
  65. package/dist/src/stories/field-checker-list-msg.stories.d.ts.map +1 -0
  66. package/dist/src/stories/field-checker.stories.d.ts +13 -0
  67. package/dist/src/stories/field-checker.stories.d.ts.map +1 -0
  68. package/dist/src/stories/field-icon.stories.d.ts +15 -0
  69. package/dist/src/stories/field-icon.stories.d.ts.map +1 -0
  70. package/dist/src/stories/field-value-checker.stories.d.ts +13 -0
  71. package/dist/src/stories/field-value-checker.stories.d.ts.map +1 -0
  72. package/dist/src/stories/subform-fields-value-checker.stories.d.ts +9 -0
  73. package/dist/src/stories/subform-fields-value-checker.stories.d.ts.map +1 -0
  74. package/package.json +7 -5
  75. package/src/components/agent-scroll-selector.tsx +10 -3
  76. package/src/components/bs-entry-select/app-group-folder.tsx +61 -0
  77. package/src/components/bs-entry-select/bs-entry-select.tsx +271 -0
  78. package/src/components/bs-entry-select/build-tree.ts +144 -0
  79. package/src/components/bs-entry-select/entry-item.tsx +43 -0
  80. package/src/components/bs-entry-select/entry-popover.tsx +97 -0
  81. package/src/components/bs-entry-select/index.ts +2 -0
  82. package/src/components/bs-entry-select/other-app-section.tsx +96 -0
  83. package/src/components/bs-entry-select/scroll-slider.tsx +455 -0
  84. package/src/components/bs-entry-select/search-input.tsx +32 -0
  85. package/src/components/bs-entry-select/tree-item-node.tsx +84 -0
  86. package/src/components/bs-entry-select/types.ts +30 -0
  87. package/src/components/bs-icons.tsx +36 -1
  88. package/src/components/card.tsx +1 -1
  89. package/src/components/collapsible.tsx +9 -0
  90. package/src/components/field-checker-list-msg.tsx +146 -0
  91. package/src/components/field-checker.tsx +182 -0
  92. package/src/components/field-icon.tsx +13 -0
  93. package/src/components/field-value-checker.tsx +118 -0
  94. package/src/components/legacy-slot.tsx +7 -0
  95. package/src/components/office-container.tsx +111 -28
  96. package/src/components/subform-fields-value-checker.tsx +185 -0
  97. package/src/const/ui.ts +67 -0
  98. package/src/hooks/use-column-appear.ts +78 -0
  99. package/src/hooks/use-resizable-width.ts +10 -2
  100. package/src/index.ts +11 -2
  101. package/src/lib/utils.ts +67 -0
  102. package/src/stories/BsEntrySelect.stories.tsx +45 -2
  103. package/src/stories/OfficeContainer.stories.tsx +150 -1
  104. package/src/stories/collapsible.stories.tsx +47 -0
  105. package/src/stories/field-checker-list-msg.stories.tsx +95 -0
  106. package/src/stories/field-checker.stories.tsx +117 -0
  107. package/src/stories/field-icon.stories.tsx +70 -0
  108. package/src/stories/field-value-checker.stories.tsx +113 -0
  109. package/src/stories/subform-fields-value-checker.stories.tsx +98 -0
  110. package/dist/src/components/bs-entry-select.d.ts.map +0 -1
  111. package/src/components/bs-entry-select.tsx +0 -724
@@ -0,0 +1,11 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { FieldCheckerListMsg } from '../components/field-checker-list-msg';
3
+ declare const meta: Meta<typeof FieldCheckerListMsg>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof FieldCheckerListMsg>;
6
+ export declare const Default: Story;
7
+ export declare const StreamingState: Story;
8
+ export declare const ConfirmedState: Story;
9
+ export declare const FieldValueCheckerListMsgDefault: Story;
10
+ export declare const FieldValueCheckerListMsgStreaming: Story;
11
+ //# sourceMappingURL=field-checker-list-msg.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-checker-list-msg.stories.d.ts","sourceRoot":"","sources":["../../../src/stories/field-checker-list-msg.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,mBAAmB,EAA4B,MAAM,sCAAsC,CAAC;AAuBrG,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,mBAAmB,CAc1C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAElD,eAAO,MAAM,OAAO,EAAE,KAKrB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAK5B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAM5B,CAAC;AAEF,eAAO,MAAM,+BAA+B,EAAE,KAa7C,CAAC;AAEF,eAAO,MAAM,iCAAiC,EAAE,KAa/C,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { FieldChecker } from '../components/field-checker';
3
+ declare const meta: Meta<typeof FieldChecker>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof FieldChecker>;
6
+ export declare const Default: Story;
7
+ export declare const WithOptions: Story;
8
+ export declare const WithValue: Story;
9
+ export declare const Subform: Story;
10
+ export declare const Disabled: Story;
11
+ export declare const Confirmed: Story;
12
+ export declare const Streaming: Story;
13
+ //# sourceMappingURL=field-checker.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-checker.stories.d.ts","sourceRoot":"","sources":["../../../src/stories/field-checker.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAc3D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,YAAY,CAcnC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,YAAY,CAAC,CAAC;AAE3C,eAAO,MAAM,OAAO,EAAE,KAKrB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAgBzB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAMvB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAwBrB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAMtB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAMvB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAMvB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { FieldIcon } from '../components/field-icon';
3
+ declare const meta: Meta<typeof FieldIcon>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof FieldIcon>;
6
+ export declare const Text: Story;
7
+ export declare const Number: Story;
8
+ export declare const Datetime: Story;
9
+ export declare const Subform: Story;
10
+ export declare const Formula: Story;
11
+ export declare const Location: Story;
12
+ export declare const Upload: Story;
13
+ export declare const Unknown: Story;
14
+ export declare const AllIcons: Story;
15
+ //# sourceMappingURL=field-icon.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-icon.stories.d.ts","sourceRoot":"","sources":["../../../src/stories/field-icon.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAErD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,SAAS,CAOhC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,SAAS,CAAC,CAAC;AAExC,eAAO,MAAM,IAAI,EAAE,KAElB,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,KAEpB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAEtB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAEtB,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,KAEpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAqBtB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { FieldValueChecker } from '../components/field-value-checker';
3
+ declare const meta: Meta<typeof FieldValueChecker>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof FieldValueChecker>;
6
+ export declare const Default: Story;
7
+ export declare const NumberField: Story;
8
+ export declare const Address: Story;
9
+ export declare const LocationField: Story;
10
+ export declare const SubForm: Story;
11
+ export declare const ReadOnly: Story;
12
+ export declare const Streaming: Story;
13
+ //# sourceMappingURL=field-value-checker.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-value-checker.stories.d.ts","sourceRoot":"","sources":["../../../src/stories/field-value-checker.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AActE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,iBAAiB,CAcxC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEhD,eAAO,MAAM,OAAO,EAAE,KAIrB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAMzB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAWrB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAW3B,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAuBrB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAKtB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAKvB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { SubformFieldsValueChecker } from '../components/subform-fields-value-checker';
3
+ declare const meta: Meta<typeof SubformFieldsValueChecker>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof SubformFieldsValueChecker>;
6
+ export declare const SingleRow: Story;
7
+ export declare const MultipleRows: Story;
8
+ export declare const Readonly: Story;
9
+ //# sourceMappingURL=subform-fields-value-checker.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subform-fields-value-checker.stories.d.ts","sourceRoot":"","sources":["../../../src/stories/subform-fields-value-checker.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,4CAA4C,CAAC;AAEvF,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,yBAAyB,CAchD,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAExD,eAAO,MAAM,SAAS,EAAE,KAoBvB,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KA8B1B,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAoBtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/ai-design",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "AI 功能 UI 组件库 - 所有 AI 功能的 UI 表现唯一真相来源",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@radix-ui/react-checkbox": "^1.3.3",
35
+ "@radix-ui/react-collapsible": "^1.1.12",
35
36
  "@radix-ui/react-dialog": "^1.1.15",
36
37
  "@radix-ui/react-popover": "^1.1.15",
37
38
  "@radix-ui/react-radio-group": "^1.3.8",
@@ -49,7 +50,8 @@
49
50
  "tailwind-merge": "^3.4.0",
50
51
  "tailwindcss-animate": "^1.0.7",
51
52
  "tw-animate-css": "^1.4.0",
52
- "@baishuyun/types": "2.0.1"
53
+ "@baishuyun/agents": "1.0.2",
54
+ "@baishuyun/types": "2.0.2"
53
55
  },
54
56
  "devDependencies": {
55
57
  "@storybook/addon-docs": "^10.1.11",
@@ -64,9 +66,9 @@
64
66
  "typescript": "^5.5.0",
65
67
  "vite": "^5.1.4",
66
68
  "vite-plugin-dts": "^4.5.4",
67
- "@baishuyun/typescript-config": "1.0.1",
68
- "@baishuyun/tailwind-config": "2.0.1",
69
- "@baishuyun/types": "2.0.1"
69
+ "@baishuyun/tailwind-config": "2.0.2",
70
+ "@baishuyun/types": "2.0.2",
71
+ "@baishuyun/typescript-config": "1.0.2"
70
72
  },
71
73
  "keywords": [],
72
74
  "author": "",
@@ -11,6 +11,7 @@ export interface AgentScrollSelectorProps {
11
11
  agents: Array<AgentSelectAgent>;
12
12
  activeAgent?: { id: string } | null;
13
13
  onSelect: (agent: AgentSelectAgent) => void | Promise<void>;
14
+ showSlider?: boolean;
14
15
  className?: string;
15
16
  }
16
17
 
@@ -19,6 +20,7 @@ export const AgentScrollSelector = ({
19
20
  activeAgent,
20
21
  onSelect,
21
22
  className,
23
+ showSlider,
22
24
  }: AgentScrollSelectorProps) => {
23
25
  const scrollRef = useRef<HTMLDivElement>(null);
24
26
  const [canScrollLeft, setCanScrollLeft] = useState(false);
@@ -88,8 +90,8 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
88
90
  [onSelect]
89
91
  );
90
92
 
91
- return (
92
- <div className={cn('flex items-center gap-2.5 w-fit max-w-full min-w-0', className)}>
93
+ const slider = (
94
+ <>
93
95
  <IconBtn
94
96
  rounded
95
97
  disabled={!canScrollLeft}
@@ -140,7 +142,12 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
140
142
  }
141
143
  onClick={() => scroll('right')}
142
144
  />
145
+ </>
146
+ );
143
147
 
148
+ return (
149
+ <div className={cn('flex items-center gap-2.5 w-fit max-w-full min-w-0', className)}>
150
+ {showSlider ? slider : null}
144
151
  {agents.length > 0 && (
145
152
  <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
146
153
  <PopoverTrigger asChild>
@@ -159,7 +166,7 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
159
166
  )}
160
167
  >
161
168
  <MenuLinesIcon />
162
- <span>全部</span>
169
+ <span>技能</span>
163
170
  </button>
164
171
  </PopoverTrigger>
165
172
  <PopoverContent
@@ -0,0 +1,61 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { cn } from '../../lib/utils';
3
+ import { transCls } from '../../const/ui';
4
+ import { SolidArrowDownIcon } from '../bs-icons';
5
+
6
+ const ChevronRightIcon = ({ size }: { size?: number }) => (
7
+ <SolidArrowDownIcon className="-rotate-90" size={size} />
8
+ );
9
+
10
+ export function AppGroupFolder({
11
+ label,
12
+ searchQuery,
13
+ children,
14
+ }: {
15
+ label: string;
16
+ searchQuery: string;
17
+ children: React.ReactNode;
18
+ }) {
19
+ const [expanded, setExpanded] = useState(true);
20
+ const hasSearch = searchQuery.trim().length > 0;
21
+
22
+ useEffect(() => {
23
+ if (hasSearch) {
24
+ setExpanded(true);
25
+ }
26
+ }, [hasSearch]);
27
+
28
+ return (
29
+ <div>
30
+ <div
31
+ className={cn(
32
+ 'flex items-center gap-[6px] h-[32px] cursor-pointer rounded-[4px] pr-[10px]',
33
+ 'hover:bg-[#EFF0F6]',
34
+ 'border-b border-[#E0E0E0]',
35
+ 'rounded-b-none',
36
+ transCls,
37
+ )}
38
+ style={{ paddingLeft: 10, paddingTop: 9 }}
39
+ onClick={() => setExpanded(!expanded)}
40
+ >
41
+ <span
42
+ className={cn(
43
+ 'shrink-0 flex items-center justify-center w-[16px] h-[16px]',
44
+ transCls,
45
+ expanded && 'rotate-90',
46
+ )}
47
+ >
48
+ <ChevronRightIcon size={16} />
49
+ </span>
50
+ <i
51
+ className="icon-bsy-folder-close"
52
+ style={{ fontSize: 16, lineHeight: 1 }}
53
+ />
54
+ <span className="text-[12px] text-[#666666]">{label}</span>
55
+ </div>
56
+ {expanded && (
57
+ <div className="flex flex-col">{children}</div>
58
+ )}
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,271 @@
1
+ import {
2
+ forwardRef,
3
+ useState,
4
+ useRef,
5
+ useEffect,
6
+ useMemo,
7
+ useCallback,
8
+ } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+ import { cn } from '../../lib/utils';
11
+ import { transCls } from '../../const/ui';
12
+ import { ScrollSlider } from './scroll-slider';
13
+ import { EntryPopover } from './entry-popover';
14
+ import { buildEntryTreeFromAliasMap } from './build-tree';
15
+ import type { BsEntrySelectProps, BsEntrySelectChangeResult } from './types';
16
+ import { SolidArrowDownIcon as ChevronDownIcon, MenuLinesIcon } from '../bs-icons';
17
+
18
+ export type { BsEntrySelectProps, BsEntrySelectChangeResult };
19
+
20
+ export const BsEntrySelect = forwardRef<HTMLDivElement, BsEntrySelectProps>(
21
+ (
22
+ {
23
+ appDetails,
24
+ currentAppId,
25
+ value: controlledValue,
26
+ defaultValue,
27
+ onChange,
28
+ placeholder: _placeholder = '请选择',
29
+ disabled = false,
30
+ className,
31
+ showSlider = true,
32
+ },
33
+ ref,
34
+ ) => {
35
+ const [isOpen, setIsOpen] = useState(false);
36
+ const [searchQuery, setSearchQuery] = useState('');
37
+ const [internalValue, setInternalValue] = useState<string | undefined>(
38
+ defaultValue,
39
+ );
40
+ const triggerRef = useRef<HTMLDivElement>(null);
41
+ const popoverRef = useRef<HTMLDivElement>(null);
42
+ const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
43
+ const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
44
+ const [sliderHasOverflow, setSliderHasOverflow] = useState(false);
45
+
46
+ const handleOverflowChange = useCallback((hasOverflow: boolean) => {
47
+ setSliderHasOverflow(hasOverflow);
48
+ }, []);
49
+
50
+ const selectedEntryId = controlledValue ?? internalValue;
51
+
52
+ const otherAppIds = useMemo(() => {
53
+ return Object.keys(appDetails).filter((id) => id !== currentAppId);
54
+ }, [appDetails, currentAppId]);
55
+
56
+ const updatePopoverPosition = useCallback(() => {
57
+ if (!triggerRef.current) return;
58
+ const rect = triggerRef.current.getBoundingClientRect();
59
+ setPopoverStyle({
60
+ position: 'fixed',
61
+ top: rect.bottom + 4,
62
+ left: rect.left,
63
+ minWidth: Math.max(rect.width, 240),
64
+ zIndex: 9999,
65
+ });
66
+ }, []);
67
+
68
+ const setTriggerRef = useCallback((node: HTMLDivElement | null) => {
69
+ (triggerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
70
+ if (!node) {
71
+ setPortalContainer(null);
72
+ return;
73
+ }
74
+ const rootNode = node.getRootNode();
75
+ if (rootNode instanceof ShadowRoot) {
76
+ const existing = rootNode.querySelector<HTMLElement>(
77
+ '[data-bs-entry-select-portal]',
78
+ );
79
+ if (existing) {
80
+ setPortalContainer(existing);
81
+ return;
82
+ }
83
+ const container = document.createElement('div');
84
+ container.setAttribute('data-bs-entry-select-portal', 'true');
85
+ rootNode.appendChild(container);
86
+ setPortalContainer(container);
87
+ return;
88
+ }
89
+ setPortalContainer(null);
90
+ }, []);
91
+
92
+ const handleClose = useCallback(() => {
93
+ setIsOpen(false);
94
+ setSearchQuery('');
95
+ if (triggerRef.current) {
96
+ triggerRef.current.style.backgroundColor = '';
97
+ }
98
+ }, []);
99
+
100
+ const handleToggle = useCallback(() => {
101
+ if (disabled) return;
102
+ if (isOpen) {
103
+ handleClose();
104
+ } else {
105
+ setIsOpen(true);
106
+ updatePopoverPosition();
107
+ }
108
+ }, [disabled, isOpen, handleClose, updatePopoverPosition]);
109
+
110
+ const handleSelect = useCallback(
111
+ (result: BsEntrySelectChangeResult) => {
112
+ setInternalValue(result.entry.entryId);
113
+ onChange?.(result);
114
+ handleClose();
115
+ },
116
+ [onChange, handleClose],
117
+ );
118
+
119
+ // Event listeners for popover (click outside, scroll, resize)
120
+ useEffect(() => {
121
+ if (!isOpen) return;
122
+
123
+ const handleClickOutside = (e: MouseEvent) => {
124
+ const path = e.composedPath();
125
+ if (
126
+ (popoverRef.current && path.includes(popoverRef.current)) ||
127
+ (triggerRef.current && path.includes(triggerRef.current))
128
+ ) {
129
+ return;
130
+ }
131
+ handleClose();
132
+ };
133
+
134
+ const handleScroll = () => {
135
+ updatePopoverPosition();
136
+ };
137
+
138
+ const handleResize = () => {
139
+ updatePopoverPosition();
140
+ };
141
+
142
+ document.addEventListener('mousedown', handleClickOutside);
143
+ window.addEventListener('scroll', handleScroll, true);
144
+ window.addEventListener('resize', handleResize);
145
+
146
+ return () => {
147
+ document.removeEventListener('mousedown', handleClickOutside);
148
+ window.removeEventListener('scroll', handleScroll, true);
149
+ window.removeEventListener('resize', handleResize);
150
+ };
151
+ }, [isOpen, handleClose, updatePopoverPosition]);
152
+
153
+ // Keyboard escape listener
154
+ useEffect(() => {
155
+ if (!isOpen) return;
156
+
157
+ const handleEscape = (e: KeyboardEvent) => {
158
+ if (e.key === 'Escape') {
159
+ handleClose();
160
+ }
161
+ };
162
+
163
+ document.addEventListener('keydown', handleEscape);
164
+ return () => document.removeEventListener('keydown', handleEscape);
165
+ }, [isOpen, handleClose]);
166
+
167
+ const hasCurrentApp = Boolean(currentAppId && appDetails[currentAppId]);
168
+ const hasOtherApps = otherAppIds.length > 0;
169
+ const hasAnyContent = hasCurrentApp || hasOtherApps;
170
+
171
+ const currentAppTree = useMemo(() => {
172
+ if (!currentAppId || !appDetails[currentAppId]) return [];
173
+ return buildEntryTreeFromAliasMap(appDetails[currentAppId], searchQuery);
174
+ }, [appDetails, currentAppId, searchQuery]);
175
+
176
+ const hasSearch = searchQuery.trim().length > 0;
177
+
178
+ return (
179
+ <div ref={ref} className={cn('relative inline-flex items-center gap-[10px] min-w-0', className)}>
180
+ {/* Scroll slider — greedy width */}
181
+ {showSlider && (
182
+ <ScrollSlider
183
+ appDetails={appDetails}
184
+ currentAppId={currentAppId}
185
+ selectedEntryId={selectedEntryId}
186
+ onSelect={handleSelect}
187
+ onOverflowChange={handleOverflowChange}
188
+ />
189
+ )}
190
+
191
+ {/* Trigger — hidden when slider shows all nodes without overflow */}
192
+ {(!showSlider || sliderHasOverflow) && (
193
+ <div
194
+ ref={setTriggerRef}
195
+ className={cn(
196
+ 'flex items-center gap-[10px] px-[10px] py-[7px] rounded-[4px] cursor-pointer select-none shrink-0',
197
+ transCls,
198
+ isOpen && 'bg-[#EFF0F6]',
199
+ disabled && 'opacity-50 cursor-not-allowed',
200
+ )}
201
+ onClick={handleToggle}
202
+ onMouseEnter={(e) => {
203
+ if (!isOpen && !disabled) {
204
+ (e.currentTarget as HTMLElement).style.backgroundColor =
205
+ '#EFF0F6';
206
+ }
207
+ }}
208
+ onMouseLeave={(e) => {
209
+ if (!isOpen) {
210
+ (e.currentTarget as HTMLElement).style.backgroundColor = '';
211
+ }
212
+ }}
213
+ >
214
+ <span className="flex items-center gap-[10px] min-w-0 flex-1">
215
+ <MenuLinesIcon />
216
+ <span>全部</span>
217
+ {/* {selectedResult ? (
218
+ <>
219
+ <i
220
+ className={getEntryIconClass(selectedResult.entry)}
221
+ style={{ fontSize: 16, lineHeight: 1 }}
222
+ />
223
+ <span className="text-[14px] text-[#030303] truncate">
224
+ {selectedResult.entry.name}
225
+ </span>
226
+ </>
227
+ ) : (
228
+ <span className="text-[14px] text-[#999999] truncate">
229
+ {placeholder}
230
+ </span>
231
+ )} */}
232
+ </span>
233
+ <span
234
+ className={cn(
235
+ 'shrink-0 flex items-center justify-center w-[16px] h-[16px] text-[#666666]',
236
+ transCls,
237
+ isOpen && 'rotate-180',
238
+ )}
239
+ >
240
+ <ChevronDownIcon size={16} />
241
+ </span>
242
+ </div>
243
+ )}
244
+
245
+ {/* Popover */}
246
+ {isOpen &&
247
+ createPortal(
248
+ <div ref={popoverRef} style={popoverStyle}>
249
+ <EntryPopover
250
+ searchQuery={searchQuery}
251
+ onSearchChange={setSearchQuery}
252
+ currentAppTree={currentAppTree}
253
+ currentAppId={currentAppId}
254
+ otherAppIds={otherAppIds}
255
+ appDetails={appDetails}
256
+ selectedEntryId={selectedEntryId}
257
+ hasCurrentApp={hasCurrentApp}
258
+ hasOtherApps={hasOtherApps}
259
+ hasAnyContent={hasAnyContent}
260
+ hasSearch={hasSearch}
261
+ onSelect={handleSelect}
262
+ />
263
+ </div>,
264
+ portalContainer || document.body,
265
+ )}
266
+ </div>
267
+ );
268
+ },
269
+ );
270
+
271
+ BsEntrySelect.displayName = 'BsEntrySelect';
@@ -0,0 +1,144 @@
1
+ import type { IAppDetail, IEntryDetail } from '@baishuyun/types';
2
+ import type { EntryTreeItem, BsEntrySelectChangeResult } from './types';
3
+
4
+ export const DEPTH_PADDING: Record<number, string> = {
5
+ 0: 'pl-[10px]',
6
+ 1: 'pl-[32px]',
7
+ 2: 'pl-[32px]',
8
+ 3: 'pl-[54px]',
9
+ 4: 'pl-[78px]',
10
+ 5: 'pl-[102px]',
11
+ };
12
+
13
+ export function getDepthPadding(depth: number): string {
14
+ return DEPTH_PADDING[Math.min(depth, 5)] ?? DEPTH_PADDING[5];
15
+ }
16
+
17
+ export function getEntryIconClass(entry: IEntryDetail): string {
18
+ const isNew = entry.icon >= 25;
19
+ const newStr = isNew ? '-new' : '';
20
+ return `node-icon icon-bsy-form${newStr} icon${newStr}-bsy-form-${entry.icon}`;
21
+ }
22
+
23
+ export function buildEntryTreeFromAliasMap(
24
+ appDetail: IAppDetail,
25
+ searchQuery: string,
26
+ ): EntryTreeItem[] {
27
+ const { entryIdAliasMap, entryMap } = appDetail;
28
+ const aliasKeys = Object.keys(entryIdAliasMap);
29
+ if (aliasKeys.length === 0) return [];
30
+
31
+ const childrenByParent = new Map<string, string[]>();
32
+ for (const id of aliasKeys) {
33
+ const aliasItem = entryIdAliasMap[id];
34
+ const parentKey = aliasItem.parent || '0';
35
+ if (!childrenByParent.has(parentKey)) {
36
+ childrenByParent.set(parentKey, []);
37
+ }
38
+ childrenByParent.get(parentKey)!.push(id);
39
+ }
40
+
41
+ // Root parents: any parent value that is not itself a key in entryIdAliasMap.
42
+ // Also include the sentinel '0' for entries with empty/falsy parent.
43
+ const allParentValues = new Set(childrenByParent.keys());
44
+ const rootParents = Array.from(allParentValues).filter(
45
+ (p) => p === '0' || !entryIdAliasMap[p],
46
+ );
47
+
48
+ const lowerQuery = searchQuery.toLowerCase().trim();
49
+
50
+ function buildRecursive(parentId: string, depth: number): EntryTreeItem[] {
51
+ const childIds = childrenByParent.get(parentId) || [];
52
+ const nodes: EntryTreeItem[] = [];
53
+
54
+ for (const id of childIds) {
55
+ const aliasItem = entryIdAliasMap[id];
56
+ const entryDetail = entryMap[id] || null;
57
+ const name = entryDetail?.name || id;
58
+ const icon = entryDetail?.icon || 0;
59
+ const isGroup = aliasItem.type === 'group';
60
+
61
+ const children = buildRecursive(id, depth + 1);
62
+ const selfMatch = !lowerQuery || name.toLowerCase().includes(lowerQuery);
63
+
64
+ if (isGroup) {
65
+ if (selfMatch || children.length > 0) {
66
+ nodes.push({
67
+ id,
68
+ name,
69
+ icon,
70
+ type: 'group',
71
+ children,
72
+ depth,
73
+ isSelectable: false,
74
+ entry: entryDetail,
75
+ });
76
+ }
77
+ } else {
78
+ if (selfMatch) {
79
+ nodes.push({
80
+ id,
81
+ name,
82
+ icon,
83
+ type: aliasItem.type,
84
+ children: [],
85
+ depth,
86
+ isSelectable: true,
87
+ entry: entryDetail,
88
+ });
89
+ }
90
+ }
91
+ }
92
+
93
+ return nodes;
94
+ }
95
+
96
+ // Build from each root parent, all at depth 0
97
+ const result: EntryTreeItem[] = [];
98
+ for (const rootId of rootParents) {
99
+ result.push(...buildRecursive(rootId, 0));
100
+ }
101
+ return result;
102
+ }
103
+
104
+ export function filterTreeBySearch(
105
+ nodes: EntryTreeItem[],
106
+ searchQuery: string,
107
+ ): EntryTreeItem[] {
108
+ const lowerQuery = searchQuery.toLowerCase().trim();
109
+ if (!lowerQuery) return nodes;
110
+
111
+ const result: EntryTreeItem[] = [];
112
+ for (const node of nodes) {
113
+ const nameMatch = node.name.toLowerCase().includes(lowerQuery);
114
+ const filteredChildren = filterTreeBySearch(node.children, searchQuery);
115
+
116
+ if (!node.isSelectable) {
117
+ if (nameMatch || filteredChildren.length > 0) {
118
+ result.push({ ...node, children: filteredChildren });
119
+ }
120
+ } else {
121
+ if (nameMatch) {
122
+ result.push(node);
123
+ }
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+
129
+ export function findSelected(
130
+ appDetails: Record<string, IAppDetail>,
131
+ entryId: string,
132
+ currentAppId?: string,
133
+ ): BsEntrySelectChangeResult | null {
134
+ for (const [appId, detail] of Object.entries(appDetails)) {
135
+ if (detail.entryMap[entryId]) {
136
+ return {
137
+ entry: detail.entryMap[entryId],
138
+ appId,
139
+ section: appId === currentAppId ? 'current' : 'other',
140
+ };
141
+ }
142
+ }
143
+ return null;
144
+ }