Merge branch 'detail_v3' of http://gitlab.t-win.kr/ifheone/shoptime into detail_v3
This commit is contained in:
@@ -7,9 +7,11 @@ import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDeco
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import TInput, { ICONS, KINDS } from '../../../components/TInput/TInput';
|
||||
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
||||
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
|
||||
import css from './VoiceInputOverlay.module.less';
|
||||
import VoicePromptScreen from './modes/VoicePromptScreen';
|
||||
import VoiceListening from './modes/VoiceListening';
|
||||
|
||||
const OverlayContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
@@ -44,26 +46,8 @@ const VoiceInputOverlay = ({
|
||||
}) => {
|
||||
const lastFocusedElement = useRef(null);
|
||||
const [inputFocus, setInputFocus] = useState(false);
|
||||
|
||||
// ESC 키 핸들러
|
||||
const handleKeyDown = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
// 키보드 이벤트 리스너 등록
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}
|
||||
}, [isVisible, handleKeyDown]);
|
||||
// 내부 모드 상태 관리 (prompt -> listening -> close)
|
||||
const [currentMode, setCurrentMode] = useState(mode);
|
||||
|
||||
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
|
||||
useEffect(() => {
|
||||
@@ -71,6 +55,9 @@ const VoiceInputOverlay = ({
|
||||
// 현재 포커스된 요소 저장
|
||||
lastFocusedElement.current = Spotlight.getCurrent();
|
||||
|
||||
// 모드 초기화 (항상 prompt 모드로 시작)
|
||||
setCurrentMode(mode);
|
||||
|
||||
// Overlay 내부로 포커스 이동
|
||||
setTimeout(() => {
|
||||
Spotlight.focus(OVERLAY_SPOTLIGHT_ID);
|
||||
@@ -83,16 +70,15 @@ const VoiceInputOverlay = ({
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [isVisible]);
|
||||
}, [isVisible, mode]);
|
||||
|
||||
// 모드에 따른 컨텐츠 렌더링
|
||||
const renderModeContent = () => {
|
||||
switch (mode) {
|
||||
switch (currentMode) {
|
||||
case VOICE_MODES.PROMPT:
|
||||
return <VoicePromptScreen suggestions={suggestions} />;
|
||||
case VOICE_MODES.MODE_2:
|
||||
// 추후 MODE_2 컴포넌트 추가
|
||||
return <div>Mode 2 (Coming soon)</div>;
|
||||
case VOICE_MODES.LISTENING:
|
||||
return <VoiceListening />;
|
||||
case VOICE_MODES.MODE_3:
|
||||
// 추후 MODE_3 컴포넌트 추가
|
||||
return <div>Mode 3 (Coming soon)</div>;
|
||||
@@ -113,59 +99,113 @@ const VoiceInputOverlay = ({
|
||||
setInputFocus(false);
|
||||
}, []);
|
||||
|
||||
// 마이크 버튼 클릭 (overlay 닫기)
|
||||
const handleMicClick = useCallback(() => {
|
||||
// 마이크 버튼 클릭 (모드 전환: prompt -> listening -> close)
|
||||
const handleMicClick = useCallback((e) => {
|
||||
console.log('[VoiceInputOverlay] handleMicClick called, currentMode:', currentMode);
|
||||
|
||||
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
||||
if (e && e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (e && e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
if (currentMode === VOICE_MODES.PROMPT) {
|
||||
// prompt 모드에서 클릭 시 -> listening 모드로 전환
|
||||
console.log('[VoiceInputOverlay] Switching to LISTENING mode');
|
||||
setCurrentMode(VOICE_MODES.LISTENING);
|
||||
} else if (currentMode === VOICE_MODES.LISTENING) {
|
||||
// listening 모드에서 클릭 시 -> 종료
|
||||
console.log('[VoiceInputOverlay] Closing from LISTENING mode');
|
||||
onClose();
|
||||
} else {
|
||||
// 기타 모드에서는 바로 종료
|
||||
console.log('[VoiceInputOverlay] Closing from other mode');
|
||||
onClose();
|
||||
}
|
||||
}, [currentMode, onClose]);
|
||||
|
||||
// dim 레이어 클릭 핸들러 (마이크 버튼과 분리)
|
||||
const handleDimClick = useCallback((e) => {
|
||||
console.log('[VoiceInputOverlay] dimBackground clicked');
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className={css.voiceOverlayContainer}>
|
||||
{/* 배경 dim 레이어 - 클릭하면 닫힘 */}
|
||||
<div className={css.dimBackground} onClick={onClose} />
|
||||
<TFullPopup
|
||||
open={isVisible}
|
||||
onClose={onClose}
|
||||
noAutoDismiss={true}
|
||||
spotlightRestrict="self-only"
|
||||
spotlightId={OVERLAY_SPOTLIGHT_ID}
|
||||
noAnimation={false}
|
||||
scrimType="none"
|
||||
className={css.tFullPopupWrapper}
|
||||
>
|
||||
<div className={css.voiceOverlayContainer}>
|
||||
{/* 배경 dim 레이어 - 클릭하면 닫힘 */}
|
||||
<div className={css.dimBackground} onClick={handleDimClick} />
|
||||
|
||||
{/* 모드별 컨텐츠 영역 - Spotlight Container (self-only) */}
|
||||
<OverlayContainer
|
||||
className={css.contentArea}
|
||||
spotlightId={OVERLAY_SPOTLIGHT_ID}
|
||||
spotlightDisabled={!isVisible}
|
||||
>
|
||||
{/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */}
|
||||
<div className={css.inputWrapper}>
|
||||
<div className={css.searchInputWrapper}>
|
||||
<TInput
|
||||
className={css.inputBox}
|
||||
kind={KINDS.withIcon}
|
||||
icon={ICONS.search}
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
onIconClick={() => onSearchSubmit && onSearchSubmit(searchQuery)}
|
||||
spotlightId={INPUT_SPOTLIGHT_ID}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
<SpottableMicButton
|
||||
className={classNames(css.microphoneButton, css.active)}
|
||||
onClick={handleMicClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleMicClick();
|
||||
}
|
||||
}}
|
||||
spotlightId={MIC_SPOTLIGHT_ID}
|
||||
>
|
||||
<div className={css.microphoneCircle}>
|
||||
<img src={micIcon} alt="Microphone" className={css.microphoneIcon} />
|
||||
</div>
|
||||
</SpottableMicButton>
|
||||
{/* 모드별 컨텐츠 영역 - Spotlight Container (self-only) */}
|
||||
<OverlayContainer
|
||||
className={css.contentArea}
|
||||
spotlightId={OVERLAY_SPOTLIGHT_ID}
|
||||
spotlightDisabled={!isVisible}
|
||||
>
|
||||
{/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */}
|
||||
<div className={css.inputWrapper} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={css.searchInputWrapper}>
|
||||
<TInput
|
||||
className={css.inputBox}
|
||||
kind={KINDS.withIcon}
|
||||
icon={ICONS.search}
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
onIconClick={() => onSearchSubmit && onSearchSubmit(searchQuery)}
|
||||
spotlightId={INPUT_SPOTLIGHT_ID}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
<SpottableMicButton
|
||||
className={classNames(
|
||||
css.microphoneButton,
|
||||
css.active,
|
||||
currentMode === VOICE_MODES.LISTENING && css.listening
|
||||
)}
|
||||
onClick={handleMicClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleMicClick(e);
|
||||
}
|
||||
}}
|
||||
spotlightId={MIC_SPOTLIGHT_ID}
|
||||
>
|
||||
<div className={css.microphoneCircle}>
|
||||
<img src={micIcon} alt="Microphone" className={css.microphoneIcon} />
|
||||
</div>
|
||||
{currentMode === VOICE_MODES.LISTENING && (
|
||||
<svg className={css.rippleSvg} width="100" height="100">
|
||||
<circle
|
||||
className={css.rippleCircle}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="47"
|
||||
fill="none"
|
||||
stroke="#C70850"
|
||||
strokeWidth="6"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</SpottableMicButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 모드별 컨텐츠 */}
|
||||
<div className={css.modeContent}>{renderModeContent()}</div>
|
||||
</OverlayContainer>
|
||||
</div>
|
||||
{/* 모드별 컨텐츠 */}
|
||||
{renderModeContent()}
|
||||
</OverlayContainer>
|
||||
</div>
|
||||
</TFullPopup>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.module.less
|
||||
@import "../../../style/CommonStyle.module.less";
|
||||
|
||||
// TFullPopup wrapper - TFullPopup의 기본 스타일을 override하지 않음
|
||||
.tFullPopupWrapper {
|
||||
// TFullPopup의 기본 동작 유지
|
||||
}
|
||||
|
||||
.voiceOverlayContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -173,6 +178,45 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listening 상태 (배경 투명, 테두리 ripple 애니메이션)
|
||||
&.listening {
|
||||
.microphoneCircle {
|
||||
background-color: transparent;
|
||||
border-color: transparent; // 테두리 투명
|
||||
box-shadow: none;
|
||||
|
||||
.microphoneIcon {
|
||||
filter: brightness(0) invert(1); // 아이콘은 흰색 유지
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ripple 애니메이션 (원형 테두리가 점에서 시작해서 그려짐)
|
||||
.rippleSvg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rippleCircle {
|
||||
stroke-dasharray: 295.3; // 2 * PI * 47 (원의 둘레)
|
||||
stroke-dashoffset: 295.3; // 초기값: 완전히 숨김
|
||||
transform-origin: center;
|
||||
transform: rotate(-90deg); // 12시 방향에서 시작
|
||||
animation: drawCircle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes drawCircle {
|
||||
0% {
|
||||
stroke-dashoffset: 295.3; // 점에서 시작
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0; // 원 완성 (계속 시계방향으로)
|
||||
}
|
||||
}
|
||||
|
||||
// 모드별 컨텐츠 영역 - 화면 중앙에 배치
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div style={{width: '100%', height: '100%', position: 'relative'}}>
|
||||
<div style={{width: 510, height: 25, left: 0, top: 0, position: 'absolute', background: '#424242', borderRadius: 100}} />
|
||||
<div style={{width: 510, height: 25, left: 0, top: 0, position: 'absolute', opacity: 0.10, background: '#C70850', borderRadius: 100}} />
|
||||
<div style={{width: 480, height: 25, left: 15, top: 0, position: 'absolute', opacity: 0.20, background: '#C70850', borderRadius: 100}} />
|
||||
<div style={{width: 390, height: 25, left: 60, top: 0, position: 'absolute', opacity: 0.30, background: '#C70850', borderRadius: 100}} />
|
||||
<div style={{width: 350, height: 25, left: 80, top: 0, position: 'absolute', opacity: 0.40, background: '#C70850', borderRadius: 100}} />
|
||||
<div style={{width: 320, height: 25, left: 95, top: 0, position: 'absolute', opacity: 0.50, background: '#C70850', borderRadius: 100}} />
|
||||
<div style={{width: 260, height: 25, left: 125, top: 0, position: 'absolute', background: '#C70850', borderRadius: 100}} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.jsx
|
||||
import React from 'react';
|
||||
import css from './VoiceListening.module.less';
|
||||
|
||||
/**
|
||||
* VoiceListening - 음성 입력 중 애니메이션 표시 컴포넌트
|
||||
* 화면 중앙에 표시되는 음성 입력 시각화 막대
|
||||
* 포커스를 받지 않으며 순수하게 시각적 피드백만 제공
|
||||
*/
|
||||
const VoiceListening = () => {
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<div className={css.listeningText}>
|
||||
Listening
|
||||
<span className={css.dotsContainer}>
|
||||
<span className={`${css.dot} ${css.dot1}`}>.</span>
|
||||
<span className={`${css.dot} ${css.dot2}`}>.</span>
|
||||
<span className={`${css.dot} ${css.dot3}`}>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className={css.visualizer}>
|
||||
<div className={`${css.bar} ${css.bar1}`} />
|
||||
<div className={`${css.bar} ${css.bar2}`} />
|
||||
<div className={`${css.bar} ${css.bar3}`} />
|
||||
<div className={`${css.bar} ${css.bar4}`} />
|
||||
<div className={`${css.bar} ${css.bar5}`} />
|
||||
<div className={`${css.bar} ${css.bar6}`} />
|
||||
<div className={`${css.bar} ${css.bar7}`} />
|
||||
<div className={`${css.bar} ${css.bar8}`} />
|
||||
<div className={`${css.bar} ${css.bar9}`} />
|
||||
<div className={`${css.bar} ${css.bar10}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceListening;
|
||||
@@ -0,0 +1,176 @@
|
||||
// src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.module.less
|
||||
@import "../../../../style/CommonStyle.module.less";
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none; // 포커스 받지 않음
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listeningText {
|
||||
font-size: 25px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin-bottom: 35px;
|
||||
text-align: center;
|
||||
|
||||
.dotsContainer {
|
||||
display: inline-block;
|
||||
width: 24px; // 점 3개 공간 확보 (고정 너비로 글자 안 움직임)
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dot {
|
||||
opacity: 0;
|
||||
animation: dotFade 1.5s infinite;
|
||||
}
|
||||
|
||||
.dot1 {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot2 {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.dot3 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
}
|
||||
|
||||
.visualizer {
|
||||
width: 510px;
|
||||
height: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 기본 막대 스타일
|
||||
.bar {
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border-radius: 100px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// 배경 막대 (투명, 고정)
|
||||
.bar1 {
|
||||
width: 510px;
|
||||
left: 0;
|
||||
background: transparent;
|
||||
animation: none; // 고정
|
||||
}
|
||||
|
||||
// 그라데이션 효과를 위한 막대들 (빨간색 계열) - 작은 것부터 큰 것으로 순차적으로 나타남
|
||||
.bar2 {
|
||||
width: 510px;
|
||||
left: 0;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 1.4s; // 가장 큰 막대 - 마지막
|
||||
opacity: 0; // 애니메이션으로 제어
|
||||
}
|
||||
|
||||
.bar3 {
|
||||
width: 480px;
|
||||
left: 15px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 1.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar4 {
|
||||
width: 390px;
|
||||
left: 60px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 1.0s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar5 {
|
||||
width: 350px;
|
||||
left: 80px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.8s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar6 {
|
||||
width: 320px;
|
||||
left: 95px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.6s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar7 {
|
||||
width: 260px;
|
||||
left: 125px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.4s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar8 {
|
||||
width: 200px;
|
||||
left: 155px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar9 {
|
||||
width: 150px;
|
||||
left: 180px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.1s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar10 {
|
||||
width: 100px;
|
||||
left: 205px;
|
||||
background: @PRIMARY_COLOR_RED;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0s; // 가장 작은 막대 - 처음 시작
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 애니메이션 정의 - 막대들이 차례대로 나타났다가 완전히 사라짐
|
||||
@keyframes waveAppear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 점 애니메이션 - 각 점이 차례로 나타남 (. → .. → ...)
|
||||
@keyframes dotFade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
33%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,20 @@ import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import { getShopperHouseSearch } from '../../../../actions/searchActions';
|
||||
import css from './VoicePromptScreen.module.less';
|
||||
|
||||
const SpottableBubble = Spottable('div');
|
||||
|
||||
const PromptContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
enterTo: 'default-element',
|
||||
restrict: 'self-only', // 포커스를 bubble 영역 내부로만 제한
|
||||
},
|
||||
'div'
|
||||
);
|
||||
|
||||
const VoicePromptScreen = ({ title = 'Try saying', suggestions = [] }) => {
|
||||
const dispatch = useDispatch();
|
||||
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
||||
@@ -32,7 +41,11 @@ const VoicePromptScreen = ({ title = 'Try saying', suggestions = [] }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<PromptContainer
|
||||
className={css.container}
|
||||
spotlightId="voice-prompt-container"
|
||||
style={{ pointerEvents: 'all' }}
|
||||
>
|
||||
<div className={css.title}>{title}</div>
|
||||
<div className={css.suggestionsContainer}>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
@@ -46,7 +59,7 @@ const VoicePromptScreen = ({ title = 'Try saying', suggestions = [] }) => {
|
||||
</SpottableBubble>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PromptContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
pointer-events: all; // 실제 컨텐츠는 클릭 가능
|
||||
// modeContent의 스타일을 여기로 이동
|
||||
margin-top: 100px;
|
||||
padding-right: 120px;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -30,11 +33,11 @@
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.bubbleMessage {
|
||||
margin-bottom: 15px;
|
||||
padding: 20px 30px;
|
||||
background: rgba(68, 68, 68, 0.5);
|
||||
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.35);
|
||||
@@ -74,3 +77,7 @@
|
||||
word-wrap: break-word;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.bubbleMessage:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user