@chatui/core
Version:
The React library for Chatbot UI
266 lines (238 loc) • 6.75 kB
text/less
.Composer {
display: flex;
align-items: flex-end;
padding: @composer-padding;
--action-size: @composer-input-min-height;
--action-font-size: 20Px;
> div + div {
margin-left: 9Px;
}
&[data-has-value="false"] .Composer-actions[data-action="send"],
&[data-has-value="true"][data-new-voice-input="true"]
.Composer-actions[data-action-icon]:not([data-action-icon="mic"]):not([data-action-icon="keyboard"]),
&[data-has-value="true"]:not([data-new-voice-input="true"]) .Composer-actions[data-action-icon] {
width: 0;
margin: 0;
opacity: 0;
}
&[data-has-value="true"] {
&:not([data-new-voice-input="true"]) {
.Composer-inputWrap {
margin-left: 0;
}
}
.Composer-sendBtn {
animation: 0.3s sendIn;
}
}
}
.Composer-actions {
display: flex;
align-items: center;
overflow: hidden;
width: var(--action-size);
height: var(--action-size);
transition: width 0.1s;
&[data-action='send'] {
width: var(--send-width, 63Px);
}
.IconBtn {
padding: 8Px;
background: var(--color-fill-1);
font-size: var(--action-font-size);
color: var(--color-text-1);
}
}
.Composer-toggleBtn {
.Icon {
transition: transform 0.3s;
}
&.active .Icon {
transform: rotate(45deg);
}
}
.Composer-inputWrap {
flex: 1;
position: relative;
}
.Composer-input {
overflow-x: hidden;
min-height: var(--action-size);
max-height: @composer-input-max-height;
padding: 8Px 12Px;
border: 0;
border-radius: var(--radius-md);
// background: @composer-input-bg;
line-height: 20Px;
font-size: 15Px;
caret-color: @composer-input-caret-color;
transition: @composer-input-transition;
}
.Composer-sendBtn {
flex: 0 0 auto;
min-width: 0;
padding: 8Px 16Px;
font-size: 14Px;
line-height: 18Px;
}
@keyframes sendIn {
0% {
transform: scale(0.2);
}
100% {
transform: scale(1);
}
}
/* 语音输入状态联动样式(仅新版语音输入启用时生效) */
.Composer[data-new-voice-input="true"] {
// 创建独立堆叠上下文,使子元素 ::before 的 z-index: -1 不会穿透到祖先背景
.Composer-inputWrap {
isolation: isolate;
}
// 渐变覆盖层:默认隐藏,仅在 recording 状态由动画驱动显隐
// - 径向渐变:自左上角 brand-3 实色向外淡化至透明(起点 alpha = 1)
// - z-index: -1 配合 inputWrap 的 isolation,将渐变压到 textarea 背后,文字不被覆盖
// - pointer-events: none 避免遮挡输入;will-change: opacity 提示合成层
.Composer-inputWrap::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 55%;
max-width: 80Px;
height: 80%;
max-height: 36Px;
border-radius: var(--radius-md) 0 0 var(--radius-md);
background: radial-gradient(
ellipse at top left,
var(--brand-3) 0%,
transparent 60%
);
pointer-events: none;
opacity: 0;
z-index: -1;
will-change: opacity;
}
// recording 状态联动:渐变层呼吸 + 跑马灯点亮 + 输入框底色让位
&[data-voice-status='recording'] {
.Composer-input {
// 让背后的渐变层透出来(原填充色上提到 inputWrap)
background-color: transparent;
}
// 由 inputWrap 接管输入框底色,渐变层落在 inputWrap 背景与 textarea 之间
// 边框不再用 box-shadow,改由 ::after 实现可动效的 1Px 边框(同时规避 iOS WebKit overflow-x + box-shadow 裁剪 bug)
.Composer-inputWrap {
background-color: var(--color-fill-1);
border-radius: var(--radius-md);
}
// 输入框左上角径向渐变呼吸覆盖层
// 通过 ::before 伪元素叠加在 inputWrap 上,不影响输入交互
.Composer-inputWrap::before {
animation: voiceInputPulse 2s ease-in-out infinite;
}
// 边框跑马灯动效:由 SVG <MarqueeBorder /> 接管,这里仅负责在 recording 状态下点亮
.Composer-marquee {
opacity: 1;
}
}
}
@keyframes voiceInputPulse {
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
// 边框跑马灯层(SVG 实现):默认隐藏,仅在 recording 状态点亮
// - 两个 <rect> 重叠:底层实色铺整圈,上层 stroke-dasharray + dashoffset 动画
// - rx / ry 跟随 var(--radius-md),与 inputWrap 圆角保持同步
// - pathLength=100 将周长归一化,dasharray="25 75" 即亮段占 1/4 周长
// - animation: 3s linear infinite,stroke-dashoffset 从 0 到 -100 实现顺时针匀速绕圈
// - vector-effect: non-scaling-stroke 保证描边始终 1Px,不被容器拉伸影响
.Composer-marquee {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0;
overflow: visible;
will-change: opacity;
rect {
rx: var(--radius-md);
ry: var(--radius-md);
fill: none;
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
&-base {
// 使用 stroke + stroke-opacity 拆分代替 color-mix,避免 Android 老版 WebView
// 不支持 color-mix() 时整个 stroke 声明被丢弃导致底色不渲染。
// stroke-opacity 是 SVG 1.1 属性,全环境兼容。
stroke: var(--brand-1);
stroke-opacity: 0.65;
}
// 三层 sweep 叠加营造亮段两端羽化渐变:
// - 中心同步原理:每层 dashoffset 起点 = L/2、终点 = L/2 - 100(变化量恒为 -100),
// 三层在 1.5s 内同步走完一圈,亮段中心任意时刻重合
// - 亮段长度从外到内递减,透明度从外到内递增,叠加后亮段中间最亮、两端逐步淡出
&-sweep {
stroke: var(--brand-1);
&--outer {
stroke-dasharray: 50 50;
stroke-opacity: 0.2;
animation: marqueeRotateOuter 1.5s linear infinite;
}
&--mid {
stroke-dasharray: 32 68;
stroke-opacity: 0.5;
animation: marqueeRotateMid 1.5s linear infinite;
}
&--core {
stroke-dasharray: 15 85;
stroke-opacity: 1;
animation: marqueeRotateCore 1.5s linear infinite;
}
}
}
@keyframes marqueeRotateOuter {
from {
stroke-dashoffset: 25;
}
to {
stroke-dashoffset: -75;
}
}
@keyframes marqueeRotateMid {
from {
stroke-dashoffset: 16;
}
to {
stroke-dashoffset: -84;
}
}
@keyframes marqueeRotateCore {
from {
stroke-dashoffset: 7.5;
}
to {
stroke-dashoffset: -92.5;
}
}
.ChatApp[data-elder-mode="true"] {
.Composer-input {
padding: 4Px 12Px;
font-size: 20Px;
line-height: 1.4;
}
}
html[data-color-scheme='dark'] {
// 不展示语音输入时输入框左上角的呼吸渐变
.Composer[data-new-voice-input='true'] {
.Composer-inputWrap::before {
content: none;
}
}
}