feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @community-components/editor
> Project template for react component with storybook and supports publish independently.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,14 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
overrides: [
{
files: ['**/*.test.ts', '**/*.test.tsx', '**/__tests__/**/*'],
rules: {
'@typescript-eslint/no-empty-function': 'off',
},
},
],
});

View File

@@ -0,0 +1,103 @@
{
"name": "@coze-community/components",
"version": "0.0.1",
"description": "business components for community",
"license": "Apache-2.0",
"author": "gaoding.devingao@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.ts"
},
"main": "src/index.ts",
"unpkg": "./dist/umd/index.js",
"module": "./dist/esm/index.js",
"types": "./src/index.ts",
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-hooks": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-arch/responsive-kit": "workspace:*",
"@coze-common/coze-mitt": "workspace:*",
"@coze-foundation/space-store": "workspace:*",
"@coze-studio/api-schema": "workspace:*",
"@coze-studio/components": "workspace:*",
"@douyinfe/semi-illustrations": "^2.36.0",
"ahooks": "^3.7.8",
"classnames": "^2.3.2"
},
"devDependencies": {
"@coze-arch/bot-env": "workspace:*",
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/tea": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@emotion/react": "11.11.1",
"@emotion/styled": "11.11.0",
"@mui/material": "^5",
"@rollup/plugin-commonjs": "^24.0.0",
"@rollup/plugin-json": "~6.0.0",
"@rollup/plugin-node-resolve": "~15.0.1",
"@rollup/plugin-replace": "^4.0.0",
"@rsbuild/core": "1.1.13",
"@swc/core": "^1.3.35",
"@swc/helpers": "^0.4.12",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/dompurify": "3.0.5",
"@types/lodash-es": "^4.17.10",
"@types/prismjs": "1.26.3",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@types/react-helmet": "^6.1.11",
"@vitest/coverage-v8": "~3.0.5",
"autoprefixer": "^10.4.16",
"core-js": "^3.37.1",
"danmu.js": "^0.5.0",
"debug": "^4.3.4",
"less": "^3.13.1",
"less-loader": "~11.1.3",
"postcss": "^8.4.32",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-helmet": "^6.1.0",
"react-is": ">= 16.8.0",
"react-router-dom": "^6.22.0",
"rollup": "^4.9.0",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-node-externals": "^6.1.2",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-ts": "^3.1.1",
"scheduler": ">=0.19.0",
"styled-components": ">= 2",
"tailwindcss": "~3.3.3",
"typescript": "~5.8.2",
"vite": "^4.3.9",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5",
"webpack": "~5.91.0",
"xgplayer-subtitles": "^1.0.21"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
},
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
}

View File

@@ -0,0 +1,18 @@
.card-button {
cursor: pointer;
height: 32px;
padding: 6px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-align: center;
border: 1px solid;
border-radius: 8px;
@apply coz-stroke-primary;
@apply coz-bg-primary;
@apply coz-fg-primary;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type PropsWithChildren } from 'react';
import cls from 'classnames';
import styles from './index.module.less';
export const CardButton: FC<
PropsWithChildren<{
className?: string;
onClick?: () => void;
}>
> = ({ className, onClick, children }) => (
<button
className={cls(styles['card-button'], className)}
color="primary"
onClick={onClick}
>
{children}
</button>
);

View File

@@ -0,0 +1,42 @@
.container {
cursor: pointer;
position: relative;
margin-bottom: 20px;
padding: 16px;
border: 1px solid;
border-radius: 8px;
@apply coz-stroke-primary;
&:not(.skeleton):hover {
@apply coz-bg-max;
@apply coz-stroke-primary;
box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 6%);
&.shadow-primary {
box-shadow: 0 6px 8px 0 rgba(0, 8, 16, 12%);
}
}
&.skeleton {
cursor: default;
border-color: transparent;
}
.check {
position: absolute;
z-index: 1;
top: 16px;
right: 16px;
}
}
.width100 {
overflow: hidden;
width: 100%;
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import s from './index.module.less';
const Container = (props: {
className?: string;
children?: React.ReactNode;
shadowMode?: 'default' | 'primary';
onClick?: () => void;
}) => {
const { className, children, onClick, shadowMode } = props;
return (
<div
className={classNames(
'coz-bg-max',
s.container,
s.width100,
className,
s[`shadow-${shadowMode}`],
)}
onClick={onClick}
>
{children}
</div>
);
};
const SkeletonContainer = (props: {
children?: React.ReactNode;
className?: string;
}) => (
<div
className={classNames(
'coz-mg-primary',
s.container,
s.width100,
s.skeleton,
props.className,
)}
>
{props?.children}
</div>
);
export const CardContainer = Container;
export const CardSkeletonContainer = SkeletonContainer;

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import cls from 'classnames';
import { AvatarName } from '@coze-studio/components';
import { type explore } from '@coze-studio/api-schema';
import { type UserInfo as ProductUserInfo } from '@coze-arch/bot-api/product_api';
import { Typography } from '@coze-arch/coze-design';
type UserInfo = explore.product_common.UserInfo | ProductUserInfo;
interface TemplateCardBodyProps {
title?: string;
description?: string;
userInfo?: UserInfo;
descClassName?: string;
renderCardTag?: () => React.ReactNode;
renderDescBottomSlot?: () => React.ReactNode;
}
export const CardInfo: FC<TemplateCardBodyProps> = ({
title,
description,
userInfo,
renderCardTag,
descClassName,
renderDescBottomSlot,
}) => (
<div className={cls('mt-[8px] px-[4px] grow', 'flex flex-col')}>
<div className="flex items-center gap-[8px] overflow-hidden">
<Typography.Text
className="!font-medium text-[16px] leading-[22px] coz-fg-primary !max-w-[180px]"
ellipsis={{ showTooltip: true, rows: 1 }}
>
{title}
</Typography.Text>
{renderCardTag?.()}
</div>
<AvatarName
className="mt-[4px]"
avatar={userInfo?.avatar_url}
name={userInfo?.name}
username={userInfo?.user_name}
label={{
name: userInfo?.user_label?.label_name,
icon: userInfo?.user_label?.icon_url,
href: userInfo?.user_label?.jump_link,
}}
/>
<div
className={cls(
'mt-[8px] flex flex-col justify-between grow',
descClassName,
)}
>
<Typography.Text
className="min-h-[40px] leading-[20px] coz-fg-secondary"
ellipsis={{ showTooltip: true, rows: 2 }}
>
{description}
</Typography.Text>
{renderDescBottomSlot?.()}
</div>
</div>
);

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import {
IconCozBot,
IconCozWorkflow,
IconCozWorkspace,
} from '@coze-arch/coze-design/icons';
import { Tag, type TagProps } from '@coze-arch/coze-design';
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
interface IProps {
type: ProductEntityType;
}
interface TagConfig {
icon: ReactNode;
i18nKey: I18nKeysNoOptionsType;
}
const TYPE_ICON_MAP: Partial<Record<ProductEntityType, TagConfig>> = {
[ProductEntityType.BotTemplate]: {
icon: <IconCozBot />,
i18nKey: 'template_agent',
},
[ProductEntityType.WorkflowTemplateV2]: {
icon: <IconCozWorkflow />,
i18nKey: 'template_workflow',
},
[ProductEntityType.ImageflowTemplateV2]: {
icon: <IconCozWorkflow />,
i18nKey: 'template_workflow',
},
[ProductEntityType.ProjectTemplate]: {
icon: <IconCozWorkspace />,
i18nKey: 'project_store_search',
},
};
const TYPE_COLOR_MAP: Partial<Record<ProductEntityType, TagProps['color']>> = {
[ProductEntityType.BotTemplate]: 'primary',
[ProductEntityType.WorkflowTemplateV2]: 'primary',
[ProductEntityType.ImageflowTemplateV2]: 'primary',
[ProductEntityType.ProjectTemplate]: 'brand',
};
export const CardTag = ({ type }: IProps) => {
const config = TYPE_ICON_MAP[type];
if (!config) {
return null;
}
return (
<Tag
color={TYPE_COLOR_MAP[type] ?? 'primary'}
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
>
{config.icon}
<span className="ml-[2px]">{I18n.t(config.i18nKey)}</span>
</Tag>
);
};

View File

@@ -0,0 +1,79 @@
.plugin {
margin-bottom: 0;
.plugin-wrapper {
position: relative;
}
.btn-container {
position: absolute;
bottom: 0;
left: 0;
display: none;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
&.one-column-grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
&:hover {
.btn-container {
display: grid;
}
.description {
visibility: hidden;
}
}
.card-avatar {
width: 48px;
height: 48px;
border-radius: 6px;
/*
* 如此写边框和设置背景色的原因
* 1、边框是内边框处于图片的边缘上因此需要用after来写
* 2、背景色用before实体来写是由于 border和背景色都是透明色重叠会导致颜色加重出现边框
*/
&::after {
content: '';
position: absolute;
z-index: 2;
width: calc(100% - 2px);
height: calc(100% - 2px);
border-style: solid;
border-width: 1px;
border-radius: 6px;
@apply coz-stroke-primary;
}
&::before {
content: '';
position: absolute;
z-index: 1;
top: 1px;
left: 1px;
width: calc(100% - 2px);
height: calc(100% - 2px);
@apply bg-stroke-5;
border-radius: 5px;
}
& > img {
z-index: 2;
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import cls from 'classnames';
import { type explore } from '@coze-studio/api-schema';
import { I18n } from '@coze-arch/i18n';
import { Avatar, Space, Tag, Toast, Tooltip } from '@coze-arch/coze-design';
import { cozeBaseUrl } from '@/const/url';
import { PluginAuthMode, type AuthMode } from '../type';
import { CardInfo } from '../components/info';
import { CardContainer, CardSkeletonContainer } from '../components/container';
import { CardButton } from '../components/button';
import styles from './index.module.less';
interface ProductInfo extends explore.ProductInfo {
plugin_extra: explore.ProductInfo['plugin_extra'] & AuthMode;
}
export type PluginCardProps = ProductInfo & {
isInstalled?: boolean;
isShowInstallButton?: boolean;
};
export const PluginCard: FC<PluginCardProps> = props => (
<CardContainer
className={styles.plugin}
shadowMode="default"
onClick={() => {
console.log('CardContainer...');
}}
>
<div className={styles['plugin-wrapper']}>
<PluginCardBody {...props} />
<Space
className={cls(styles['btn-container'], {
[styles['one-column-grid']]:
props.isInstalled || !props.isShowInstallButton,
})}
>
{!props.isInstalled && props.isShowInstallButton ? (
<CardButton
onClick={() => {
Toast.success(I18n.t('plugin_install_success'));
}}
>
{I18n.t('plugin_store_install')}
</CardButton>
) : null}
<CardButton
onClick={() => {
window.open(
`${cozeBaseUrl}/store/plugin/${props.meta_info?.id}?from=plugin_card`,
);
}}
>
{I18n.t('plugin_usage_limits_modal_view_details')}
</CardButton>
</Space>
</div>
</CardContainer>
);
export const PluginCardSkeleton = () => (
<CardSkeletonContainer className={cls('h-[186px]', styles.plugin)} />
);
const PluginCardBody: FC<PluginCardProps> = props => {
const renderCardTag = () => {
if (
props.plugin_extra.auth_mode === PluginAuthMode.Required ||
props.plugin_extra.auth_mode === PluginAuthMode.Supported
) {
return (
<Tag
color={'yellow'}
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
>
<span className="ml-[2px]">
{I18n.t('plugin_store_unauthorized')}
</span>
</Tag>
);
} else if (props.plugin_extra.auth_mode === PluginAuthMode.Configured) {
return (
<Tooltip content={I18n.t('plugin_store_contact_deployer')}>
<Tag
color={'brand'}
className="h-[20px] !px-[4px] !py-[2px] coz-fg-primary font-medium shrink-0"
>
<span className="ml-[2px]">
{I18n.t('plugin_store_authorized')}
</span>
</Tag>
</Tooltip>
);
}
return null;
};
return (
<div>
<Avatar
className={styles['card-avatar']}
src={props.meta_info?.icon_url}
shape="square"
/>
<CardInfo
{...{
title: props.meta_info?.name,
description: props.meta_info?.description,
userInfo: props.meta_info?.user_info,
authMode: props.plugin_extra.auth_mode,
renderCardTag,
descClassName: styles.description,
}}
/>
</div>
);
};

View File

@@ -0,0 +1,28 @@
.template {
margin-bottom: 0;
.template-wrapper {
position: relative;
}
.btn-container {
position: absolute;
bottom: 0;
left: 0;
display: none;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
}
&:hover {
.btn-container {
display: grid;
}
.description {
visibility: hidden;
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, useState } from 'react';
import cls from 'classnames';
import { explore } from '@coze-studio/api-schema';
import { useSpaceList } from '@coze-foundation/space-store';
import { I18n } from '@coze-arch/i18n';
import { Image, Input, Modal, Space, Toast } from '@coze-arch/coze-design';
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
import { cozeBaseUrl } from '@/const/url';
import { type CardInfoProps } from '../type';
import { CardTag } from '../components/tag';
import { CardInfo } from '../components/info';
import { CardContainer, CardSkeletonContainer } from '../components/container';
import { CardButton } from '../components/button';
type ProductInfo = explore.ProductInfo;
import styles from './index.module.less';
export type TemplateCardProps = ProductInfo;
const PATH_MAP: Partial<
Record<explore.product_common.ProductEntityType, string>
> = {
[ProductEntityType.BotTemplate]: 'agent',
[ProductEntityType.WorkflowTemplateV2]: 'workflow',
[ProductEntityType.ImageflowTemplateV2]: 'workflow',
[ProductEntityType.ProjectTemplate]: 'project',
};
export const TemplateCard: FC<TemplateCardProps> = props => {
const [visible, setVisible] = useState(false);
return (
<CardContainer
className={styles.template}
shadowMode="default"
onClick={() => {
console.log('Template Click Card');
}}
>
<div className={styles['template-wrapper']}>
<TempCardBody
{...{
title: props.meta_info?.name,
description: props.meta_info?.description,
userInfo: props.meta_info?.user_info,
entityType: props.meta_info.entity_type,
imgSrc: props.meta_info.covers?.[0].url,
}}
/>
<Space className={styles['btn-container']}>
<CardButton
onClick={() => {
setVisible(true);
}}
>
{I18n.t('copy')}
</CardButton>
<CardButton
onClick={() => {
const pathPrefix = PATH_MAP[props.meta_info.entity_type] || '';
const pathSuffix = [
ProductEntityType.WorkflowTemplateV2,
ProductEntityType.ImageflowTemplateV2,
].includes(props.meta_info.entity_type)
? `?entity_type=${props.meta_info.entity_type}`
: '';
window.open(
`${cozeBaseUrl}/template/${pathPrefix}/${props.meta_info.id}${pathSuffix}`,
);
}}
>
{I18n.t('plugin_usage_limits_modal_view_details')}
</CardButton>
</Space>
</div>
{visible ? (
<DuplicateModal
productId={props.meta_info.id}
entityType={props.meta_info.entity_type}
defaultTitle={`${props.meta_info?.name}(${I18n.t('duplicate_rename_copy')})`}
hide={() => setVisible(false)}
/>
) : null}
</CardContainer>
);
};
const DuplicateModal: FC<{
defaultTitle: string;
productId: string;
entityType: explore.product_common.ProductEntityType;
hide: () => void;
}> = ({ defaultTitle, hide, productId, entityType }) => {
const [title, setTitle] = useState(defaultTitle);
const { spaces } = useSpaceList();
const spaceId = spaces?.[0]?.id;
console.log('title', title, spaces);
return (
<Modal
type="modal"
title={I18n.t('creat_project_use_template')}
visible={true}
onOk={async () => {
try {
await explore.PublicDuplicateProduct({
product_id: productId,
entity_type: entityType,
space_id: spaceId,
name: title,
});
Toast.success(I18n.t('copy_success'));
hide();
} catch (err) {
console.error('PublicDuplicateProduct', err);
Toast.error(I18n.t('copy_failed'));
}
}}
onCancel={hide}
cancelText={I18n.t('Cancel')}
okText={I18n.t('Confirm')}
>
<Space vertical spacing={4} className="w-full">
<Space className="w-full">
<span className="coz-fg-primary font-medium leading-[20px]">
{I18n.t('creat_project_project_name')}
</span>
<span className="coz-fg-hglt-red">*</span>
</Space>
<Input
className="w-full"
placeholder=""
defaultValue={defaultTitle}
onChange={value => {
setTitle(value);
}}
/>
</Space>
</Modal>
);
};
export const TemplateCardSkeleton = () => (
<CardSkeletonContainer className={cls('h-[278px]', styles.template)} />
);
export const TempCardBody: FC<
CardInfoProps & {
entityType?: explore.product_common.ProductEntityType | ProductEntityType;
renderImageBottomSlot?: () => React.ReactNode;
renderDescBottomSlot?: () => React.ReactNode;
}
> = ({
title,
imgSrc,
description,
entityType,
userInfo,
renderImageBottomSlot,
renderDescBottomSlot,
}) => (
<div>
<div className="relative w-full h-[140px] rounded-[8px] overflow-hidden">
<Image
preview={false}
src={imgSrc}
className="w-full h-full"
imgCls="w-full h-full object-cover object-center"
/>
{renderImageBottomSlot?.()}
</div>
<CardInfo
{...{
title,
description,
userInfo,
renderCardTag: () =>
entityType ? <CardTag type={entityType} /> : null,
descClassName: styles.description,
renderDescBottomSlot,
}}
/>
</div>
);

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type explore } from '@coze-studio/api-schema';
import { type UserInfo as ProductUserInfo } from '@coze-arch/bot-api/product_api';
type UserInfo = explore.product_common.UserInfo;
export interface CardInfoProps {
title?: string;
imgSrc?: string;
description?: string;
userInfo?: UserInfo | ProductUserInfo;
}
/** for open coze */
export enum PluginAuthMode {
/** No authorization required */
NoAuth = 0,
/** Authorization is required, but not configured */
Required = 1,
/** Authorization is required and has been configured */
Configured = 2,
/** Authorization is required, but the configuration can be empty */
Supported = 3,
}
export interface AuthMode {
/** for open coze */
auth_mode?: PluginAuthMode;
}

View File

@@ -0,0 +1,5 @@
.more {
&:global(.coz-tag-mini) {
padding: 0 4px;
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { CozAvatar, Tag, Tooltip, Typography } from '@coze-arch/coze-design';
import { type PluginConnectorInfo } from '@coze-arch/bot-api/product_api';
import styles from './index.module.less';
interface ConnectorListProps {
connectors: PluginConnectorInfo[];
className?: string;
visibleNum?: number;
}
const DEFAULT_VISIBLE_NUM = 3;
export const ConnectorList: React.FC<ConnectorListProps> = ({
connectors,
className,
visibleNum = DEFAULT_VISIBLE_NUM,
}) => {
const moreNum = connectors.length - visibleNum;
return (
<div className={classNames('ml-auto flex gap-4px', className)}>
{connectors.slice(0, visibleNum).map(item => (
<Tooltip key={item.id} content={item.name} theme="dark">
<CozAvatar
className="border coz-stroke-primary border-solid"
size="micro"
src={item.icon}
type="platform"
/>
</Tooltip>
))}
{moreNum > 0 ? (
<Tooltip
position="right"
content={
<div className="flex flex-col gap-8px max-w-[200px] max-h-[188px] overflow-y-auto overflow-x-hidden">
{connectors.slice(visibleNum).map(item => (
<div
key={item.id}
className="flex gap-8px items-center max-w-full"
>
<CozAvatar
className="border coz-stroke-primary border-solid"
size="micro"
src={item.icon}
type="platform"
/>
<Typography.Text
ellipsis={true}
className="flex-1 overflow-hidden"
>
{item.name}
</Typography.Text>
</div>
))}
</div>
}
>
<Tag className={styles.more} size="mini" color="primary">
+{moreNum}
</Tag>
</Tooltip>
) : null}
</div>
);
};

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const cozeBaseUrl = IS_OVERSEA
? // cp-disable-next-line
'https://www.coze.com'
: // cp-disable-next-line
'https://www.coze.cn';

View File

@@ -0,0 +1,18 @@
.favorite-btn {
color: #1D1C23;
:global(.semi-button-content-right) {
margin-left: 4px;
color: #1D1C23;
}
&.dark {
.un-collected {
path {
fill: #1D1C23
}
}
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {
useRef,
useState,
forwardRef,
useImperativeHandle,
type MouseEvent,
} from 'react';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { UIButton } from '@coze-arch/bot-semi';
import { type FavoriteIconBtnRef, FavoriteIconBtn } from '../favorite-icon-btn';
import styles from './index.module.less';
interface HeaderProps {
favoriteCount?: number;
productId?: string;
entityType?: number;
isFavorite?: boolean;
svgColor?: 'default' | 'dark';
onReportFavorite: (action) => void;
disabled?: boolean;
isMobile?: boolean;
unCollectedIconCls?: string;
collectedIconCls?: string;
onClickBefore?: (
action: 'cancel' | 'add',
event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>,
) => boolean | Promise<boolean>;
/**兼容UI1.0&2.0 全部替换后去除 */
isNewStyle?: boolean;
isForbiddenIconClick?: boolean;
}
/* Plugin header */
export const FavoriteBtn = forwardRef((props: HeaderProps, ref) => {
const {
favoriteCount,
onReportFavorite,
productId,
entityType,
isFavorite,
svgColor,
disabled,
isMobile,
isNewStyle,
collectedIconCls,
unCollectedIconCls,
onClickBefore,
isForbiddenIconClick,
} = props;
const refFavoriteBtn = useRef<FavoriteIconBtnRef>(null);
const [favoriteNumberAdd, setFavoriteNumberAdd] = useState(0);
// 该数字不能小于0 防止出现异常数字
const favoriteNum = Math.max(
0,
(Number(favoriteCount) || 0) + (Number(favoriteNumberAdd) || 0),
);
useImperativeHandle(
ref,
() => ({
favorite: (event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) =>
refFavoriteBtn.current?.favorite(event),
}),
[],
);
const favoriteIconButton = (
<FavoriteIconBtn
ref={refFavoriteBtn}
productId={productId}
entityType={entityType}
isFavorite={isFavorite}
onClickBefore={onClickBefore}
isVisible={true}
onReportTea={onReportFavorite}
className={collectedIconCls}
unCollectedIconCls={cls(styles['un-collected'], unCollectedIconCls)}
isForbiddenClick={isForbiddenIconClick}
onChange={value => {
setFavoriteNumberAdd(prevNumber =>
//该值再 1和 -1 之间。
Math.min(Math.max(prevNumber + (Number(value) || 0), -1), 1),
);
}}
isMobile={isMobile}
/>
);
return isMobile ? (
<div
onClick={event => {
if (!isForbiddenIconClick) {
refFavoriteBtn.current?.favorite(event);
}
}}
>
{favoriteIconButton}
</div>
) : isNewStyle ? (
<Button
size="large"
color="primary"
icon={favoriteIconButton}
onClick={event => {
refFavoriteBtn.current?.favorite(event);
}}
disabled={disabled}
>
{favoriteNum > 0
? `${I18n.t('mkpl_num_favorites')}(${favoriteNum})`
: I18n.t('mkpl_num_favorites')}
</Button>
) : (
<UIButton
theme={'light'}
// @ts-expect-error -- linter-disable-autofix
className={cls(styles['favorite-btn'], styles[svgColor])}
icon={favoriteIconButton}
onClick={event => {
refFavoriteBtn.current?.favorite(event);
}}
disabled={disabled}
>
{favoriteNum > 0 ? favoriteNum : I18n.t('mkpl_favorite')}
</UIButton>
);
});

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
IconMobileCollect,
IconMobileCollectFill,
} from '@coze-arch/bot-icons';
export const FavoriteIconMobile = (props: { isFavorite?: boolean }) => {
const { isFavorite } = props;
return <>{isFavorite ? <IconMobileCollectFill /> : <IconMobileCollect />}</>;
};

View File

@@ -0,0 +1,41 @@
.icon-filled {
width: 100%;
color: rgba(255, 204, 18, 100%);
}
.icon-stroked {
width: 100%;
color: rgba(29, 28, 35, 35%);
}
.show-ani {
animation-name: ani;
animation-duration: 0.6s;
}
.show-btn {
&.icon-stroked {
width: 100%;
color: var(--coz-fg-primary);
}
}
@keyframes ani {
0% {
transform: scale(0);
}
38% {
transform: scale(1.11);
}
64% {
transform: scale(0.99);
}
74%,
100% {
transform: scale(1);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import cls from 'classnames';
import { IconButton } from '@coze-arch/coze-design';
import { IconCollectFilled, IconCollectStroked } from '@coze-arch/bot-icons';
import styles from './index.module.less';
export const FavoriteIcon = (props: {
isFavorite?: boolean;
isShowAni: boolean;
unCollectedIconCls?: string;
isMobile?: boolean;
useButton?: boolean;
className?: string;
}) => {
const { isFavorite, isShowAni, className, unCollectedIconCls, useButton } =
props;
const iconProps = {
className: cls(
isFavorite ? styles['icon-filled'] : styles['icon-stroked'],
isFavorite ? className : unCollectedIconCls,
{
[styles['show-ani']]: isFavorite && isShowAni,
[styles['show-btn']]: useButton,
},
),
};
const icon = isFavorite ? (
<IconCollectFilled {...iconProps} />
) : (
<IconCollectStroked {...iconProps} />
);
if (useButton) {
return <IconButton size="default" color="primary" icon={icon} />;
}
return icon;
};

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { useMemoizedFn } from 'ahooks';
export const useAnimationChange = ({ isVisible }: { isVisible?: boolean }) => {
const [isShowAni, setIsShowAni] = useState(false);
useEffect(() => {
if (!isVisible) {
setIsShowAni(false);
}
}, [isVisible]);
const changeAnimationStatus = useMemoizedFn((isCurFavorite: boolean) => {
if (!isCurFavorite) {
setIsShowAni(true);
} else {
setIsShowAni(false);
}
});
return { isShowAni, changeAnimationStatus };
};

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemoizedFn } from 'ahooks';
import { type ProductEntityType } from '@coze-arch/bot-api/product_api';
import { ProductApi } from '@coze-arch/bot-api';
import { cozeMitt } from '@coze-common/coze-mitt';
import { type FavoriteCommParams } from '../type';
export const useFavoriteStatusRequest = ({
productId,
entityType,
entityId,
topicId,
onChange,
setIsFavorite,
}: FavoriteCommParams & {
setIsFavorite: (isFavorite: boolean) => void;
}) => {
const changeFavoriteStatus = useMemoizedFn(
async (isCurFavorite: boolean, action: string) => {
setIsFavorite(!isCurFavorite);
try {
await ProductApi.PublicFavoriteProduct({
// 后端不能处理空字符串
product_id: productId || undefined,
entity_type: entityType as ProductEntityType,
is_cancel: isCurFavorite,
entity_id: entityId,
topic_id: topicId,
});
onChange?.(isCurFavorite ? -1 : 1);
cozeMitt.emit('refreshFavList', {
id: entityId,
numDelta: action === 'add' ? 1 : -1,
emitPosition: 'favorite-icon-btn',
});
} catch (_err) {
setIsFavorite(isCurFavorite);
}
},
);
return { changeFavoriteStatus };
};

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useCallback, useRef, type MouseEvent } from 'react';
import { useMemoizedFn, useUpdateEffect } from 'ahooks';
import { type FavoriteCommParams } from '../type';
import { useFavoriteStatusRequest } from './use-farvorite-request';
import { useAnimationChange } from './use-animation-change';
type ClickAction = 'cancel' | 'add';
const getClickAction = (isCurFavoriteStatus: boolean): ClickAction =>
isCurFavoriteStatus ? 'cancel' : 'add';
export const useFavoriteChange = ({
isFavoriteDefault,
onReportTea,
productId,
entityId,
entityType,
onChange,
onClickBefore,
topicId,
isVisible,
onFavoriteStateChange,
}: FavoriteCommParams & {
onReportTea?: (action: 'cancel' | 'add') => void;
isFavoriteDefault?: boolean;
isVisible?: boolean;
onFavoriteStateChange?: (isFavorite: boolean) => void;
}) => {
const [isFavorite, setIsFavorite] = useState<boolean>(
isFavoriteDefault ?? false,
);
const { isShowAni, changeAnimationStatus } = useAnimationChange({
isVisible,
});
const refIsChange = useRef(false);
// 改变状态前,先做前置请求,判断是否需要放弃本次状态变更,如果 onClickBefore 返回 false则不进行变更。
const onClickBeforeHandle = useMemoizedFn(
async (
action: ClickAction,
event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>,
) => (await onClickBefore?.(action, event)) !== false,
);
const { changeFavoriteStatus } = useFavoriteStatusRequest({
productId,
entityType,
entityId,
topicId,
onChange,
setIsFavorite,
});
const onClick = useCallback(
async (event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
if (refIsChange.current) {
// 进行中,直接返回,不做处理
event?.stopPropagation?.();
event?.preventDefault?.();
return;
}
const action = getClickAction(isFavorite);
refIsChange.current = true;
try {
if ((await onClickBeforeHandle(action, event)) !== false) {
event?.stopPropagation?.();
onReportTea?.(action);
changeAnimationStatus(isFavorite);
await changeFavoriteStatus(isFavorite, action);
}
} catch (_err) {
console.error('useFavoriteChange:', _err);
}
refIsChange.current = false;
},
[isFavorite, onReportTea],
);
useUpdateEffect(() => {
onFavoriteStateChange?.(isFavorite);
}, [isFavorite]);
return { isFavorite, onClick, isShowAni };
};

View File

@@ -0,0 +1,10 @@
.favorite-icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { forwardRef, useImperativeHandle } from 'react';
import { type FavoriteIconBtnProps } from './type';
import { useFavoriteChange } from './hooks/use-favorite-change';
import { FavoriteIconMobile } from './components/favorite-icon-mobile';
import { FavoriteIcon } from './components/favorite-icon';
import styles from './index.module.less';
export interface FavoriteIconBtnRef {
favorite: (event) => void;
}
export const FavoriteIconBtn = forwardRef(
(props: FavoriteIconBtnProps, ref) => {
const {
topicId,
productId,
entityType,
entityId,
isFavorite: isFavoriteDefault,
onChange,
isVisible,
onReportTea,
unCollectedIconCls,
onClickBefore,
onFavoriteStateChange,
isMobile,
className,
useButton = false,
isForbiddenClick = false,
} = props;
const { isFavorite, onClick, isShowAni } = useFavoriteChange({
isFavoriteDefault,
onReportTea,
productId,
entityId,
entityType,
onChange,
onClickBefore,
topicId,
isVisible,
onFavoriteStateChange,
});
useImperativeHandle(
ref,
() => ({
favorite: onClick,
}),
[onClick],
);
if (!isVisible) {
return null;
}
return (
<div
onClick={isForbiddenClick ? undefined : onClick}
className={styles['favorite-icon-btn']}
data-testid="bot-card-favorite-icon"
>
{isMobile ? (
<FavoriteIconMobile isFavorite={isFavorite} />
) : (
<FavoriteIcon
useButton={useButton}
isFavorite={isFavorite}
isShowAni={isShowAni}
unCollectedIconCls={unCollectedIconCls}
className={className}
/>
)}
</div>
);
},
);

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type MouseEvent } from 'react';
export interface FavoriteCommParams {
topicId?: string;
productId?: string;
entityType?: number;
isFavorite?: boolean;
useButton?: boolean;
entityId?: string;
onClickBefore?: (
action: 'cancel' | 'add',
event?: MouseEvent<HTMLDivElement, globalThis.MouseEvent>,
) => boolean | Promise<boolean>;
onChange?: (num) => void; // 当收藏状态真正变化的时候,回调
}
export interface FavoriteIconBtnProps extends FavoriteCommParams {
onFavoriteStateChange?: (isFavorite: boolean) => void; // 当收藏icon的显示状态变化的时候回调
isVisible: boolean;
onReportTea?: (action: 'cancel' | 'add') => void;
unCollectedIconCls?: string;
isMobile?: boolean;
isForbiddenClick?: boolean;
className?: string;
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { OfficialLabel } from './official-label';
export { InfiniteList } from './infinite-list';
export {
type InfiniteListDataProps,
type InfiniteListRef,
type EmptyProps,
} from './infinite-list/type';
export { ConnectorList } from './connector-list';
export { FavoriteBtn } from './favorite-button';
export { FavoriteIconBtn, type FavoriteIconBtnRef } from './favorite-icon-btn';
export { SubMenuItem } from './sub-menu-item';
export {
TemplateCard,
TemplateCardSkeleton,
TempCardBody,
type TemplateCardProps,
} from './card/template';
export { CardTag } from './card/components/tag';
export {
PluginCard,
PluginCardSkeleton,
type PluginCardProps,
} from './card/plugin';

View File

@@ -0,0 +1,44 @@
.empty {
overflow: visible;
height: 100%;
.spin {
display: block;
width: 100%;
height: 100%;
:global {
.semi-spin-wrapper {
position: absolute;
display: flex;
justify-content: center;
svg {
width: 24px;
height: 24px;
}
}
.semi-tabs-content {
padding: 0;
}
.semi-spin-children {
height: 100%;
}
}
.loading-text {
margin-left: 8px;
font-size: 16px;
font-weight: 400;
line-height: 22px;
color: var(--semi-color-text-3, rgba(29, 28, 35, 35%));
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { UIEmpty, Spin } from '@coze-arch/bot-semi';
import { IllustrationFailure } from '@douyinfe/semi-illustrations';
import { type EmptyProps } from '../../type';
import s from './index.module.less';
/* Plugin header */
function Index(props: EmptyProps) {
const {
isLoading,
isSearching,
loadRetry,
isError,
renderEmpty,
text,
btn,
icon,
} = props;
return (
<div className={s.empty}>
{renderEmpty?.(props) ||
(!isError ? (
isLoading ? (
<Spin
tip={
<span className={s['loading-text']}>{I18n.t('Loading')}</span>
}
wrapperClassName={s.spin}
size="middle"
/>
) : (
<UIEmpty
isNotFound={!!isSearching}
empty={{
title: text?.emptyTitle || I18n.t('inifinit_list_empty_title'),
description: text?.emptyTitle ? text?.emptyDesc : '',
btnText: btn?.emptyText,
btnOnClick: btn?.emptyClick,
icon,
}}
notFound={{
title:
text?.searchEmptyTitle || I18n.t('inifinit_search_not_found'),
}}
/>
)
) : (
<UIEmpty
empty={{
title: I18n.t('inifinit_list_load_fail'),
icon: <IllustrationFailure />,
btnText: loadRetry && I18n.t('inifinit_list_retry'),
btnOnClick: () => {
loadRetry?.();
},
}}
/>
))}
</div>
);
}
export default Index;

View File

@@ -0,0 +1,55 @@
.footer-container {
padding: 12px 0 28px;
text-align: center;
* {
vertical-align: middle;
}
.loading,
.error-retry {
margin-left: 10px;
line-height: 20px;
color: var(--semi-color-text-3, rgba(29, 28, 35, 35%));
}
.error-retry {
cursor: pointer;
color: var(--semi-color-focus-border, #4d53e8);
}
:global {
.semi-spin-middle > .semi-spin-wrapper {
height: 16px;
svg {
width: 16px;
height: 16px;
}
}
}
.load-more-btn {
font-weight: 600;
background: #fff;
border-radius: 40px;
span {
color: #1d1c23;
}
&:hover {
background: #fff;
border: none;
}
}
&.responsive-foot-container {
padding: 0 0 16px;
.load-more-btn {
height: 40px;
padding: 16px 24px;
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Spin, UIButton } from '@coze-arch/bot-semi';
import { useIsResponsive } from '@coze-arch/bot-hooks';
import { type FooterProps } from '../../type';
import s from './index.module.less';
/* Plugin header */
function Index(props: FooterProps) {
const {
isLoading,
loadRetry,
isError,
renderFooter,
isNeedBtnLoadMore,
noMore,
} = props;
const isResponsive = useIsResponsive();
return (
<div
className={classNames(s['footer-container'], {
[s['responsive-foot-container']]: isResponsive,
})}
>
{renderFooter?.(props) ||
(isLoading ? (
<>
<Spin />
<span className={s.loading}>{I18n.t('Loading')}</span>
</>
) : isError ? (
<>
<Spin />
<span className={s['error-retry']} onClick={loadRetry}>
{I18n.t('inifinit_list_retry')}
</span>
</>
) : isNeedBtnLoadMore && !noMore ? (
<UIButton
onClick={loadRetry}
className={s['load-more-btn']}
theme="borderless"
>
{I18n.t('mkpl_load_btn')}
</UIButton>
) : null)}
</div>
);
}
export default Index;

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useState,
useRef,
useEffect,
type Dispatch,
type SetStateAction,
} from 'react';
import {
useInfiniteScroll,
useUpdateEffect,
useMemoizedFn,
useDebounceFn,
} from 'ahooks';
import { type ScrollProps, type InfiniteListDataProps } from '../type';
/* 滚动Hooks */
function useForwardFunc<T>(
dataInfo: InfiniteListDataProps<T>,
mutate: Dispatch<SetStateAction<InfiniteListDataProps<T>>>,
) {
// 手动插入数据,不通过接口
const insertData = (item, index) => {
dataInfo.list.splice(index, 0, item);
mutate({
...dataInfo,
list: [...(dataInfo?.list || [])],
});
};
// 手动删除数据,不通过接口
const removeData = index => {
dataInfo.list.splice(index, 1);
mutate({
...dataInfo,
list: [...(dataInfo?.list || [])],
});
};
const getDataList = () => dataInfo?.list;
return { insertData, removeData, getDataList };
}
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- 看了下代码行数不太好优化
function useScroll<T>(props: ScrollProps<T>) {
const {
targetRef,
loadData,
threshold,
reloadDeps,
isNeedBtnLoadMore,
resetDataIfReload = true,
} = props;
const [isLoadingError, setIsLoadingError] = useState<boolean>(false);
const refFetchNo = useRef<number>(0);
const refResolve = useRef<(value) => void>();
const {
loading,
data: dataInfo,
loadingMore,
loadMore,
noMore,
cancel,
mutate,
reload,
} = useInfiniteScroll<InfiniteListDataProps<T>>(
async current => {
// 此处逻辑如此复杂是解决Scroll中的bug。
// useInfiniteScroll中的cancel只是取消了一次请求但是数据会根据current重新设置一遍。
const fetchNo = refFetchNo.current;
if (refResolve.current) {
// 保证顺序执行,如果有当前方法,就取消上一次的请求,防止出现由于网络原因导致数据覆盖问题
// 同时发出A1,A2,三次请求但是A1先到达然后请求了B1, 但是A1过慢导致了A1覆盖了B1的请求。
refResolve.current({
...(current || {}),
list: [],
});
}
const result = await new Promise((resolve, reject) => {
refResolve.current = resolve;
loadData(current)
.then(value => resolve(value))
.catch(err => reject(err));
});
// @ts-expect-error -- linter-disable-autofix
refResolve.current = null;
// 切换Tab的时候如果此时正在请求防止数据的残留界面显示
if (refFetchNo.current !== fetchNo) {
if (current) {
current.list = [];
}
return {
list: [],
nextPage: 1,
};
}
return result as InfiniteListDataProps<T>;
},
{
target: isLoadingError || isNeedBtnLoadMore ? null : targetRef, //失败的时候通过去掉target的事件绑定禁止滚动加载。
threshold,
onBefore: () => {
//setIsLoadingError(false);
},
isNoMore: data => data?.hasMore !== undefined && !data?.hasMore,
onSuccess: () => {
if (isLoadingError) {
setIsLoadingError(false);
}
},
onError: e => {
// 如果在请求第一页数据时发生错误并且当前列表不为空则reset数据
// 这个case只有当resetDataIfReload设置为false时才会发生
// @ts-expect-error -- linter-disable-autofix
if (dataInfo.nextPage === 1 && (dataInfo?.list?.length ?? 0) > 0) {
// @ts-expect-error -- linter-disable-autofix
mutate({
...dataInfo,
list: [],
});
}
setIsLoadingError(true);
},
},
);
const { insertData, removeData, getDataList } = useForwardFunc(
// @ts-expect-error -- linter-disable-autofix
dataInfo,
mutate,
);
useEffect(() => {
if (isNeedBtnLoadMore && !(loading || loadingMore)) {
reload();
}
}, []);
const reloadData = useMemoizedFn(() => {
mutate({
list: resetDataIfReload ? [] : (dataInfo?.list ?? []),
hasMore: undefined,
nextPage: 1,
});
cancel();
setIsLoadingError(false);
reload();
});
useUpdateEffect(() => {
refFetchNo.current++;
reloadData();
}, [...(reloadDeps || [])]);
const isLoading = loading || loadingMore || props.isLoading;
const { run: loadMoreDebounce } = useDebounceFn(
() => {
if (isLoading) {
return;
}
if (!isNeedBtnLoadMore) {
loadMore();
}
},
{ wait: 500 },
);
useEffect(() => {
const resize = () => {
loadMoreDebounce();
};
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
}, []);
const { list } = dataInfo || {};
return {
dataList: list,
isLoading,
loadMore: () => {
if (!isLoading) {
//如果已经有数据加载中了,需要禁止重复加载。
loadMore();
}
},
reload: reloadData,
noMore,
cancel,
isLoadingError,
mutate,
insertData,
removeData,
getDataList,
};
}
export default useScroll;

View File

@@ -0,0 +1,5 @@
.height-whole-100 {
overflow: visible;
height: 100%;
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
forwardRef,
useImperativeHandle,
type RefObject,
useEffect,
} from 'react';
import cls from 'classnames';
import { ResponsiveList } from '@coze-arch/responsive-kit';
import { List } from '@coze-arch/bot-semi';
import { type InfiniteListProps, type InfiniteListRef } from './type';
import useScroll from './hooks/use-scroll';
import Footer from './components/footer';
import Empty from './components/empty';
import s from './index.module.less';
/* Plugin header */
function Index<T extends object>(props: InfiniteListProps<T>, ref) {
const {
isSearching,
className,
emptyContent,
grid,
renderItem,
itemClassName,
renderFooter,
scrollConf,
emptyConf,
onChangeState,
canShowData = true,
isNeedBtnLoadMore = false,
isResponsive,
retryFunc,
responsiveConf,
containerClassName,
} = props;
const {
dataList,
isLoading,
loadMore,
noMore,
isLoadingError,
mutate,
reload,
insertData,
removeData,
getDataList,
} = useScroll<T>({ ...scrollConf, isNeedBtnLoadMore });
useImperativeHandle(
ref,
() => ({ mutate, reload, insertData, removeData, getDataList }),
[mutate, reload, insertData, removeData, getDataList],
);
useEffect(() => {
onChangeState?.(isLoading, dataList);
}, [dataList, isLoading]);
// 根据白名单对列表移动端进行移动端适配
return (
<div className={cls(s['height-whole-100'], containerClassName)}>
{!dataList?.length || !canShowData ? (
/** 数据为空的时候,操作如何显示空页面 */
<Empty
isError={canShowData ? isLoadingError : false}
isSearching={isSearching}
isLoading={canShowData ? isLoading : true}
loadRetry={retryFunc || loadMore}
{...emptyConf}
/>
) : isResponsive ? (
<ResponsiveList<T>
className={className}
emptyContent={isLoading ? <></> : emptyContent}
dataSource={dataList}
renderItem={(item, number) => renderItem?.(item, number)}
gridCols={responsiveConf?.gridCols}
gridGapXs={{
basic: 4,
}}
footer={
<div className="text-sm px-6 py-3">
<Footer
isError={isLoadingError}
noMore={noMore}
isLoading={isLoading}
loadRetry={retryFunc || loadMore}
renderFooter={renderFooter}
isNeedBtnLoadMore={isNeedBtnLoadMore}
/>
</div>
}
/>
) : (
<List
{...{ className, emptyContent, grid }}
emptyContent={isLoading ? <></> : emptyContent}
dataSource={dataList}
split={false}
renderItem={(item, number) => (
<List.Item
className={
typeof itemClassName === 'string'
? itemClassName
: itemClassName?.(item) // 支持动态行className
}
>
{renderItem?.(item, number)}
</List.Item>
)}
footer={
<Footer
isError={isLoadingError}
noMore={noMore}
isLoading={isLoading}
loadRetry={retryFunc || loadMore}
renderFooter={renderFooter}
isNeedBtnLoadMore={isNeedBtnLoadMore}
dataNum={dataList?.length}
/>
}
/>
)}
</div>
);
}
export const InfiniteList = forwardRef(Index) as <T>(
props: InfiniteListProps<T> & { ref?: RefObject<InfiniteListRef> },
) => JSX.Element;

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactElement, type RefObject } from 'react';
import {
type ResponsiveTokenMap,
type ScreenRange,
} from '@coze-arch/responsive-kit';
import { type ListProps } from '@coze-arch/bot-semi/List';
export interface EmptyProps {
isError?: boolean;
isLoading?: boolean;
isSearching?: boolean;
loadRetry?: () => void; //重试加载
text?: {
emptyTitle?: string;
emptyDesc?: string;
searchEmptyTitle?: string;
};
btn?: {
emptyClick?: () => void; //
emptyText?: string;
};
icon?: ReactElement;
renderEmpty?: (
emptyProps: Omit<EmptyProps, 'renderEmpty'>,
) => React.ReactNode | null;
}
export interface FooterProps {
isError?: boolean; // 是否加载出错
isLoading?: boolean; // 是否加载中
noMore?: boolean; //没有更多数据
isNeedBtnLoadMore?: boolean;
dataNum?: number;
loadRetry?: () => void; //重试加载
renderFooter?: (
footerProps: Omit<FooterProps, 'renderFooter'>,
) => React.ReactNode | null;
}
export interface InfiniteListDataProps<T> {
list: T[];
hasMore?: boolean;
nextPage: number;
[key: string]: unknown;
}
export interface ScrollProps<T> {
threshold?: number; //距离下方多长距离,开始加载数据
targetRef?: RefObject<HTMLDivElement>; // 监听滚动的Dom 引用
loadData: (current) => Promise<InfiniteListDataProps<T>>; // 加载更多数据
reloadDeps?: unknown[]; // 重新加载数据依赖
isNeedBtnLoadMore?: boolean;
isLoading?: boolean; // 是否加载中
resetDataIfReload?: boolean; // 当reload时是否先reset列表已存在数据默认为true
}
export interface InfiniteListProps<T>
extends Pick<
ListProps<T>,
'className' | 'emptyContent' | 'grid' | 'renderItem'
> {
containerClassName?: string;
canShowData?: boolean; //是否能够显示数据了
isSearching?: boolean; // 是否搜索中,主要是用于错误显示的时候,选择文案使用
itemClassName?: string | ((item: T) => string);
isNeedBtnLoadMore?: boolean;
isResponsive?: boolean;
emptyConf: {
renderEmpty?: EmptyProps['renderEmpty'];
text?: EmptyProps['text'];
btn?: EmptyProps['btn'];
icon?: EmptyProps['icon'];
};
renderFooter?: FooterProps['renderFooter'];
scrollConf: ScrollProps<T>;
rowKey?: string;
retryFunc?: () => void;
onChangeState?: (loading, data) => void;
responsiveConf?: {
gridCols?: ResponsiveTokenMap<ScreenRange>;
};
}
export interface InfiniteListRef {
mutate: (data) => void;
reload: () => void;
insertData: (item, index) => void;
removeData: (index) => void;
getDataList: () => unknown[]; // 获取当前列表数据
}

View File

@@ -0,0 +1,32 @@
.official-label {
position: absolute;
z-index: 5;
right: 0;
bottom: 0;
transform: translate(25%, 25%);
display: flex;
align-items: center;
justify-content: center;
&.default {
svg {
width: 20px;
height: 20px;
}
}
&.small {
svg {
width: 16px;
height: 16px;
}
}
&.large {
svg {
width: 32px;
height: 32px;
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Tooltip } from '@coze-arch/coze-design';
import { IconOfficialLabel } from '@coze-arch/bot-icons';
import styles from './index.module.less';
/**
* small 16px
* default 20px
* large 32px
*/
export type OfficialLabelSize = 'small' | 'default' | 'large';
export interface OfficialLabelProps {
size?: OfficialLabelSize;
visible: boolean;
children?: React.ReactNode;
className?: string;
}
export const OfficialLabelSizeMap = {
small: styles.small,
default: styles.default,
large: styles.large,
};
export const OfficialLabel: React.FC<OfficialLabelProps> = ({
size = 'default',
children,
visible,
className,
}) => (
<div className="relative w-fit h-fit">
<Tooltip
spacing={12}
trigger={visible ? 'hover' : 'custom'}
content={I18n.t('mkpl_plugin_tooltip_official')}
>
{visible ? (
<IconOfficialLabel
className={classNames(
styles['official-label'],
OfficialLabelSizeMap[size],
className,
)}
/>
) : null}
</Tooltip>
{children}
</div>
);

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type ReactNode } from 'react';
import classNames from 'classnames';
import { Typography } from '@coze-arch/coze-design';
export interface SubMenuItemProps {
icon?: ReactNode;
title?: string;
activeIcon?: ReactNode;
isActive: boolean;
suffix?: ReactNode;
onClick: () => void;
}
export const SubMenuItem: FC<SubMenuItemProps> = ({
icon = null,
title,
activeIcon = null,
isActive,
suffix,
onClick,
}) => (
<div
onClick={onClick}
className={classNames(
'flex items-center gap-[8px]',
'transition-colors',
'rounded-[8px]',
'h-[32px] w-full',
'px-[8px]',
'cursor-pointer',
'hover:coz-mg-primary-hovered',
isActive ? 'coz-bg-primary coz-fg-plus' : 'coz-fg-primary coz-bg-max',
)}
>
<div className="text-[16px] leading-none leading-none w-[16px] h-[16px]">
{isActive ? activeIcon : icon}
</div>
<Typography.Text
ellipsis={{ showTooltip: true, rows: 1 }}
fontSize="14px"
weight={500}
className="flex-1 text-[14px] leading-[20px] font-[500]"
>
{title}
</Typography.Text>
{suffix}
</div>
);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{ts,tsx}', '../../packages/**/src/**/*.{ts,tsx}'],
} satisfies Config;

View File

@@ -0,0 +1,71 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"rootDirs": ["./"],
"types": [],
"paths": {
"@/*": ["./src/*"]
},
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src", "src/**/*.json"],
"references": [
{
"path": "../../arch/api-schema/tsconfig.build.json"
},
{
"path": "../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-env/tsconfig.build.json"
},
{
"path": "../../arch/bot-hooks/tsconfig.build.json"
},
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../arch/logger/tsconfig.build.json"
},
{
"path": "../../arch/responsive-kit/tsconfig.build.json"
},
{
"path": "../../arch/tea/tsconfig.build.json"
},
{
"path": "../../common/coze-mitt/tsconfig.build.json"
},
{
"path": "../../components/bot-icons/tsconfig.build.json"
},
{
"path": "../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../../foundation/space-store/tsconfig.build.json"
},
{
"path": "../../studio/components/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,27 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": [
"__tests__",
"stories",
"vitest.config.ts",
"vitest.setup.ts",
"tailwind.config.ts",
"../pages/src/flow-trial/store-preview"
],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDirs": ["./"],
"outDir": "./dist",
"types": ["vitest/globals", "@testing-library/jest-dom"],
"paths": {
"@/*": ["./src/*"]
},
"rootDir": "./"
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import svgr from 'vite-plugin-svgr';
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig(
{
dirname: __dirname,
preset: 'web',
plugins: [
// @ts-expect-error Incompatible svgr types
svgr({
include: ['**/*.svg'],
}),
],
test: {
setupFiles: ['./vitest.setup.ts'],
reporters: ['verbose'],
},
},
{
fixSemi: true,
},
);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom/vitest';