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,19 @@
.container {
.icon {
color: #4d53e8;
&>svg {
color: #4d53e8;
}
}
}
.disabled {
.icon {
color: var(--light-usage-disabled-color-disabled-text, rgb(28 31 35 / 35%));
&>svg {
color: var(--light-usage-disabled-color-disabled-text, rgb(28 31 35 / 35%));
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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 { IconCozAddNode } from '@coze-arch/coze-design/icons';
import { IconButton, type ButtonProps } from '@coze-arch/coze-design';
import { IconAdd } from '@coze-arch/bot-icons';
import styles from './index.module.less';
type AddOperationProps = React.PropsWithChildren<{
readonly?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
subitem?: boolean;
size?: ButtonProps['size'];
color?: ButtonProps['color'];
}>;
export default function AddOperation({
readonly,
onClick,
className,
style,
disabled,
subitem = false,
size,
color,
...restProps
}: AddOperationProps) {
if (readonly) {
return null;
}
return (
<IconButton
data-testid={restProps['data-testid']}
onClick={onClick}
className={classNames(
styles.container,
disabled ? styles.disabled : null,
className,
)}
style={style}
icon={subitem ? <IconCozAddNode /> : <IconAdd className={styles.icon} />}
disabled={disabled}
size={size}
color={color}
/>
);
}

View File

@@ -0,0 +1,39 @@
/* stylelint-disable declaration-no-important */
.popup_container {
position: relative;
display: flex;
flex-direction: column;
.nano {
pointer-events: none;
position: absolute;
top: 0;
right: 0;
align-self: stretch;
width: 100%;
height: 100%;
:global {
.semi-portal-inner {
left: 50% !important;
}
}
}
}
.tooltip {
&.top-level {
max-width: 400px;
font-size: 14px;
font-weight: 400;
font-style: normal;
line-height: 22px;
word-break: break-word;
border-radius: 6px;
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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, { type PropsWithChildren, useRef } from 'react';
import classNames from 'classnames';
import { Tooltip, type TooltipProps } from '@coze-arch/coze-design';
import styles from './index.module.less';
type AutoSizeTooltipProps = PropsWithChildren<
{
className?: string;
style?: React.CSSProperties;
containerClassName?: string;
containerStyle?: React.CSSProperties;
tooltipClassName?: string;
tooltipStyle?: React.CSSProperties;
} & Omit<TooltipProps, 'className' | 'style'>
>;
export default function AutoSizeTooltip({
children,
className,
style,
tooltipClassName,
tooltipStyle,
containerClassName,
containerStyle,
...rest
}: AutoSizeTooltipProps) {
const nanoRef = useRef<HTMLDivElement | null>(null);
const renderContent = () => (
<>
<div
ref={nanoRef}
className={classNames(styles.nano, containerClassName)}
style={containerStyle}
/>
<Tooltip
{...rest}
className={classNames(
styles.tooltip,
styles['top-level'],
tooltipClassName,
)}
style={{ left: 0, ...tooltipStyle }}
>
{children}
</Tooltip>
</>
);
return (
<div
className={classNames(styles.popup_container, className)}
style={style}
>
{renderContent()}
</div>
);
}

View File

@@ -0,0 +1,23 @@
.uiBanner {
margin: 8px 16px;
padding: 12px 24px;
:global {
.semi-banner-extra {
position: absolute;
top: 22px;
right: 56px;
margin-top: 0;
z-index: 10;
}
.semi-banner-content {
display: flex;
align-items: center;
}
.semi-banner-icon {
margin-right: 16px;
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.
*/
/**
* 对 semi-ui 的 banner 做一个简单的样式封装,符合 UX 设计稿规范
*/
import { type FC } from 'react';
import classnames from 'classnames';
import { Banner, type BannerProps } from '@coze-arch/coze-design';
import { IconClose } from '@douyinfe/semi-icons';
import styles from './index.module.less';
export const UIBanner: FC<BannerProps> = props => (
<Banner
bordered
closeIcon={<IconClose />}
fullMode={false}
{...props}
className={classnames(styles.uiBanner, props.className)}
/>
);

View File

@@ -0,0 +1,36 @@
.input-wrapper {
width: 100%;
span {
width: 100%;
}
}
.error-content {
height: 20px;
}
.error-float {
width: 100%;
position: absolute;
}
.error-text {
font-size: 12px;
padding-top: 2px;
padding-left: 12px;
color: @error-red;
position: absolute;
}
.input {
input {
text-overflow: ellipsis;
overflow: hidden;
}
}
.limit-count {
padding-left: 8px;
padding-right: 12px;
overflow: hidden;
color: var(--light-usage-text-color-text-3, rgba(28, 31, 35, 0.35));
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}

View File

@@ -0,0 +1,194 @@
/*
* 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, {
type ComponentProps,
useRef,
useImperativeHandle,
useMemo,
useEffect,
type ForwardedRef,
} from 'react';
import isNumber from 'lodash-es/isNumber';
import cs from 'classnames';
import { useReactive } from 'ahooks';
import {
Input as UIInput,
type InputProps,
type TooltipProps,
} from '@coze-arch/coze-design';
import AutoSizeTooltip from '../auto-size-tooltip';
import s from './index.module.less';
export interface WorkflowSLInputRefType {
triggerFocus?: () => void;
}
export type WorkflowSLInputProps = ComponentProps<typeof UIInput> & {
value: string | undefined;
onRef?: ForwardedRef<WorkflowSLInputRefType>;
ellipsis?: boolean;
handleChange?: (v: string) => void;
handleBlur?: (v: string) => void;
handleFocus?: (v: string) => void;
ellipsisTooltipProps?: TooltipProps;
onFocusTooltipProps?: TooltipProps;
tooltipProps?: TooltipProps;
inputProps?: InputProps;
errorMsg?: string;
errorMsgFloat?: boolean;
maxCount?: number;
className?: string;
style?: React.CSSProperties;
};
const SL_INPUT_TIMEOUT = 10;
export default function WorkflowSLInput(props: WorkflowSLInputProps) {
const { ellipsis = true, maxCount } = props;
const showCount = isNumber(maxCount) && maxCount > 0;
useImperativeHandle(props.onRef, () => ({
triggerFocus,
}));
const $state = useReactive({
value: props.value,
inputOnFocus: false,
inputEle: false,
});
const inputRef = useRef<HTMLInputElement>(null);
const triggerFocus = () => {
$state.inputEle = true;
inputRef?.current?.focus();
};
const onFocus = () => {
$state.inputOnFocus = true;
$state.inputEle = true;
props?.handleFocus?.($state.value || '');
};
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
$state.inputOnFocus = false;
props?.handleBlur?.($state.value || '');
props?.onBlur?.(e);
$state.inputEle = false;
};
const onChange = (v: string) => {
$state.value = v;
props?.handleChange?.(v);
};
const onclick = () => {
if (!$state.inputEle) {
setTimeout(() => {
inputRef?.current?.focus();
}, SL_INPUT_TIMEOUT);
}
$state.inputEle = true;
};
const hasEllipsis = useMemo(() => {
const clientWidth = inputRef.current?.clientWidth || 0;
const scrollWidth = inputRef.current?.scrollWidth || 0;
return clientWidth < scrollWidth - 1;
}, [
ellipsis,
$state.inputOnFocus,
$state.value,
inputRef.current?.clientWidth,
inputRef.current?.scrollWidth,
$state.inputEle,
]);
useEffect(() => {
$state.value = props.value;
}, [props.value]);
const LimitCountNode = (
<span className={s['limit-count']}>
{$state.value?.length || 0}/{maxCount}
</span>
);
return (
<div
className={cs(s['input-wrapper'], props.className)}
style={props.style}
>
{!$state.inputEle && hasEllipsis ? (
<AutoSizeTooltip
content={$state.value}
position={'top'}
showArrow
mouseEnterDelay={300}
{...props.tooltipProps}
>
<div
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
onClick={onclick}
>
<UIInput
{...props.inputProps}
validateStatus={props.validateStatus}
ref={inputRef}
value={$state.value}
className={ellipsis ? s.input : ''}
suffix={showCount ? LimitCountNode : undefined}
></UIInput>
</div>
</AutoSizeTooltip>
) : (
<div className={cs(props?.errorMsg ? s['error-wrapper'] : null)}>
<AutoSizeTooltip
{...props.onFocusTooltipProps}
trigger="custom"
visible={
Boolean(props.onFocusTooltipProps?.content) && $state.inputOnFocus
}
showArrow
>
<UIInput
{...props.inputProps}
validateStatus={props.validateStatus}
ref={inputRef}
value={$state.value}
className={ellipsis ? s.input : ''}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
suffix={showCount ? LimitCountNode : undefined}
></UIInput>
</AutoSizeTooltip>
</div>
)}
{props?.errorMsg ? (
<div
className={cs(
s['error-content'],
props?.errorMsgFloat ? s['error-float'] : null,
)}
>
<div className={s['error-text']}>{props?.errorMsg}</div>
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,68 @@
/* stylelint-disable declaration-no-important */
.input-wrapper {
width: 100%;
span {
width: 100%;
}
}
.error-wrapper {
:global {
.semi-input-wrapper {
border: 1px solid @error-red;
}
}
}
.error-content {
height: 20px;
}
.error-float {
position: absolute;
width: 100%;
}
.error-text {
position: absolute;
padding-top: 2px;
padding-left: 12px;
font-size: 12px;
color: @error-red;
}
.textarea-pd {
padding-bottom: 0;
}
.inputting {
textarea {
.textarea-pd;
}
}
.input-blur {
textarea {
.textarea-pd;
overflow: hidden;
display: -webkit-box;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
.text-input-placeholder {
padding-top: 0;
padding-bottom: 0;
textarea {
padding-top: 4px !important;
}
}

View File

@@ -0,0 +1,179 @@
/*
* 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, {
type ComponentProps,
useState,
useRef,
type ForwardedRef,
} from 'react';
import cs from 'classnames';
import { TextArea } from '@coze-arch/coze-design';
import { sleep } from '@coze-arch/bot-utils';
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
import { type TextAreaProps } from '@coze-arch/bot-semi/Input';
import AutoSizeTooltip from '../auto-size-tooltip';
import s from './index.module.less';
export interface WorkflowSLTextAreaRefType {
triggerFocus?: () => void;
}
export type WorkflowSLTextAreaProps = ComponentProps<typeof TextArea> & {
value: string | undefined;
onRef?: ForwardedRef<WorkflowSLTextAreaRefType>;
handleChange?: (v: string) => void;
handleBlur?: () => void;
handleFocus?: () => void;
ellipsisTooltipProps?: TooltipProps;
onFocusTooltipProps?: TooltipProps;
inputFocusProps: TextAreaProps;
inputBlurProps?: TextAreaProps;
textAreaProps?: TextAreaProps;
errorMsg?: string;
errorMsgFloat?: boolean;
disabled?: boolean;
};
/**
* @component TextArea 在 Workflow 场景下的二次封装;
* focus(inputting) 的时候提供多行滚动输入能力blur 的时候提供 ellipsis 和 tooltip 提示能力
*/
export default function WorkflowSLTextArea(props: WorkflowSLTextAreaProps) {
const { value, handleChange, inputFocusProps, inputBlurProps, disabled } =
props;
const [focus, setFocus] = useState<boolean>(false);
const selectionCacheRef = useRef<number>();
const focusInputRef = useRef<HTMLTextAreaElement>(null);
const blurInputRef = useRef<HTMLTextAreaElement>(null);
const hasEllipsis = (() => {
const clientHeight =
blurInputRef.current?.clientHeight ||
focusInputRef.current?.clientHeight ||
0;
const scrollHeight =
blurInputRef.current?.scrollHeight ||
focusInputRef.current?.scrollHeight ||
0;
return clientHeight < scrollHeight - 1 && (value as string).length > 0;
})();
/* 非focus状态下仅用于展示溢出时提供tooltip */
const InputDisplay = () => {
const handleFocus = async () => {
// 1. 获取光标位置focus后定位到相同位置
await sleep(50);
selectionCacheRef.current = blurInputRef.current?.selectionStart;
setFocus(true);
// 2. 触发真正输入组件的focus
await sleep(50);
focusInputRef.current?.focus();
};
if (!hasEllipsis) {
return (
<TextArea
className={s['text-input-placeholder']}
{...inputBlurProps}
ref={blurInputRef}
value={value}
onFocus={handleFocus}
disabled={disabled}
/>
);
}
return (
<AutoSizeTooltip
content={
<article
style={{
wordWrap: 'break-word',
wordBreak: 'normal',
textAlign: 'left',
}}
>
{value}
</article>
}
position={'top'}
showArrow
mouseEnterDelay={300}
trigger="hover"
>
<TextArea
className={s['.text-input-placeholder']}
{...inputBlurProps}
ref={blurInputRef}
value={value}
onFocus={handleFocus}
disabled={disabled}
/>
</AutoSizeTooltip>
);
};
return (
<div className={cs(s['input-wrapper'], props.className)}>
<div className={cs(props?.errorMsg ? s['error-wrapper'] : null)}>
{!focus ? (
<InputDisplay />
) : (
<TextArea
{...inputFocusProps}
ref={focusInputRef}
value={value}
onChange={handleChange}
onFocus={() => {
if (selectionCacheRef.current !== undefined) {
focusInputRef.current?.setSelectionRange(
selectionCacheRef.current,
selectionCacheRef.current,
);
selectionCacheRef.current = undefined;
}
props.handleFocus?.();
}}
onBlur={() => {
setFocus(false);
props.handleBlur?.();
}}
disabled={disabled}
/>
)}
</div>
{props?.errorMsg ? (
<div
className={cs(
s['error-content'],
props?.errorMsgFloat ? s['error-float'] : null,
)}
>
<div className={s['error-text']}>{props?.errorMsg}</div>
</div>
) : null}
</div>
);
}