대화 UI (패널 · 말풍선 · 입력줄)
메신저 · 채팅 위젯 · AI 어시스턴트가 함께 쓰는 대화 부품입니다. 진입점에서 패널을 열어 보고 종류·폭 축과 사용 규칙을 찾아보세요.
CSS 전용 · Web / Mobile
Interactive playground
.ds-chat-panel)·말풍선·입력줄 모두 실제 design-system.css 로 렌더합니다. 뒤에 표를 깔아 둔 건 그림자(--shadow-popover)를 보이게 하기 위해서예요 — 흰 바탕에 얹으면 껍데기 규격의 절반이 안 읽힙니다. 높이만 이 미리보기에서 440px 로 줄였고(실제 기본은 600px), 그게 --ds-chat-panel-h 축입니다. 폭은 말풍선이 아니라 목록이 정합니다 — --ds-bubble-max 를 컨테이너에 한 번 얹으면 안쪽이 다 따라와요. 패널 안의 대화/연락처 탭·공지 목록은 일부러 그리지 않았습니다 — 그건 Messenger 의 것이라, 여기 베껴 두면 그쪽이 바뀔 때 이 그림만 옛날에 남습니다.
<!-- 껍데기 — 폭·높이·모서리·그림자·테두리를 유틸로 다시 적지 않는다 -->
<div class="ds-fab-layer ds-fab--chat ds-chat-panel fixed bottom-26">
<!-- 메시지 목록 — 이 패널의 말풍선 폭은 여기서 한 번만 정한다 -->
<div class="flex-1 overflow-y-auto">
<div class="ds-bubble ds-bubble--sent ds-bubble--plain">…</div>
</div>
<!-- 입력줄 — 껍데기(윗선·면) + 줄(여백·간격) -->
<div class="ds-composer-bar">
<div class="ds-composer">
<input type="text" class="ds-input--40 ds-composer__field" />
<button type="button" class="ds-composer__btn ds-composer__btn--send"
:disabled="!input.trim()">…</button>
</div>
</div>
</div>왼쪽 아래 진입점(FAB) 을 눌러 패널을 열고 닫아 보세요. 껍데기 · 말풍선 · 입력줄 모두 실제 design-system.css 의 .ds-chat-panel · .ds-bubble · .ds-composer 로 렌더됩니다. 뒤에 표를 깔아 둔 것은 그림자를 보이게 하기 위해서예요 — 흰 바탕에 얹으면 껍데기 규격의 절반이 안 읽힙니다.
패널 안의 대화/연락처 탭·공지 목록은 일부러 그리지 않았습니다. 그건 Messenger 의 것이라, 여기 베껴 두면 그쪽이 바뀔 때 이 그림만 옛날에 남아요.
규칙 (강제) · 코딩 에이전트용
대화 부품의 강제 규칙. 상세·근거는 아래 본문.
떠 있는 패널의 껍데기는 .ds-chat-panel 하나다
- 폭·높이·모서리·그림자·테두리를 유틸로 적지 말 것. 채팅 위젯과 어시스턴트가 같은 껍데기를 쓴다 — 옮기기 전에는 두 벌이라 다섯 군데가 갈려 있었다(높이 600/560 ·
rounded-2xl/rounded-16·shadow-2xl/shadow-popover·neutral-200/neutral-100· 화면 상한 가드 유무). - ⚠️ 그림자는
--shadow-popover하나다. 팝오버·드롭다운과 같은 값이라야 「떠 있는 것」들이 한 화면에서 같은 높이로 읽힌다 — 여기만 진한 그림자(Tailwindshadow-2xl)를 쓰면 패널 하나가 유독 앞으로 튀어나와 보인다. - 화면 상한 가드(
max-height)는 둘 다 갖는다 — 없으면 낮은 화면에서 패널 머리가 잘린다(어시스턴트에만 있던 것을 채팅에도 준다). - 자리·층은 껍데기가 정하지 않는다 —
.ds-fab-layer+.ds-fab--chat/--ai가 진다 → FAB 계약. 도크로 전환될 때는.ds-ai-dock이 이 껍데기를 되덮는다(폭·높이·모서리·그림자 전부).
말풍선은 .ds-bubble + 방향 변형 하나다
- 종류는 셋뿐이다 —
--sent(내가 보낸) ·--received(상대·어시스턴트) ·--error(실패한 응답). 네 번째를 만들지 말 것. - 크기·여백·꼬리·폭을 유틸로 다시 적지 말 것.
text-b5·leading-relaxed·px-3 py-1.5·max-w-*·rounded-*는 전부 클래스가 진다. 예전에는 마크업에max-w-[280px]가 붙어 있었는데 파셜의<style>(unlayered)이 이겨서 죽은 선언이었고, 둘이 어긋난 채 방치됐다. - 폭은 말풍선이 아니라 «목록»이 정한다 — 컨테이너에
--ds-bubble-max를 한 번 얹으면 안쪽 말풍선이 다 따라온다(기본 75%, 어시스턴트 도크만 85%). 스피너의 다섯 축과 달리 이 변수들은 상속을 막지 않는다. 그게 이 부품의 정상 사용법이라서다. - ⚠️ 말풍선을 다른 상자로 감싸면 그 상자는 목록 폭 전체(
w-full)여야 한다.max-width: var(--ds-bubble-max)(%)는 부모 상자 기준이라, 감싸는 상자가 글자 폭으로 줄어 있으면(flex-col의items-end자식 등) 말풍선이 「자기 글자 폭의 85%」에서 꺾여 마지막 단어가 늘 다음 줄로 넘어간다(2026-10-08, AI 어시스턴트 보낸 말풍선 · 실패 말풍선에서 실측). 감싸는 상자는w-full로 펴고, 좌우 정렬은 그 안의items-end/items-start로 준다. 메신저처럼 줄이 이미w-full이면 해당 없다. --plain(줄바꿈 보존)은 텍스트 말풍선에만. 마크다운을 렌더하는 말풍선(x-html)에 붙이면 블록 요소가 만드는 개행과 겹쳐 문단 사이가 두 줄로 벌어진다.- 실패한 응답에
.ds-notice를 쓰지 말 것. 그건 화면 안내이고, 이건 대화의 한 턴이다 — 말풍선 자리를 지켜야 앞뒤 대화와 시간 순서가 읽힌다. - ⚠️ 본문 크기(b5·13px)는 두 대화창이 «같아야» 한다. 채팅 패널과 어시스턴트 도크는 한 화면에 나란히 뜨므로 한쪽만 바꾸면 바로 눈에 띈다. 어시스턴트의 마크다운 블록(
.ai-md)이 이 값에 물려 있어(제목 b4 > 본문 b5 > code b6), 본문을 바꾸면 그 블록을 통째로 다시 잡아야 한다. - 메신저 말풍선은 텔레그램식 묶음이다(2026-10-13). 보낸 사람 이름은 받은 말풍선 안 첫 줄(
.ds-bubble__sender, 사람마다 고정 색조.ds-sender-tone-N—senderTone()이름 해시)이고, 같은 사람이 5분 안에 이어 보내면 이름을 생략한다(isMessageContinuation). 아바타(.ds-chat-avatar32px — 사진 또는 색조 바탕 + 첫 글자)는 묶음의 마지막 말풍선 왼쪽 아래 한 번이고 누르면 메뉴(프로필 보기 · 1:1 채팅 · 멘션하기,senderMenuItems). 시각·읽음 영수증은 말풍선 안 오른쪽 아래(.ds-bubble__metafloat). 아바타는<button>이 아니라div[role=button]— 전역 button radius 강제로 원이 안 된다. - 줄 간격은 토큰 lh(1.4)가 아니라 1.625 다(
--ds-bubble-lh). 채팅은 짧은 줄이 겹겹이 쌓여서 토큰 값이면 빽빽하게 읽힌다. - 이모지 반응은 말풍선 «아래» 형제 블록
.ds-bubble__reactions+ 칩.ds-reaction-chip(내 것은--mine)이다(2026-10-20). 말풍선 종류를 늘리지 않는다. 메시지 메뉴 위에 따로 뜨는 빠른 반응 알약은.ds-reaction-bar+__item(묶음.ds-menu-stack, 텔레그램식 — 이모지 줄을 메뉴 안에 넣으면 메뉴 폭이 끌려 늘어난다), 허용 이모지(👍✅❤️😂😮😢🙏)는@hereby/core/constants의MESSAGE_REACTION_EMOJIS한 벌(서버 검사와 같은 배열). 모바일은 말풍선 두 번 두드리기 = 마지막으로 쓴 이모지(기본 👍) — 판정은messenger-helpers.ts의registerTap. 메뉴 「선택 복사」는 본문을.ds-select-copy(글자 선택 켬) 모달에 띄워 드래그한 부분만 복사한다(모바일 말풍선은 길게 누르기 때문에 선택이 꺼져 있다 —.ds-bubble--no-select). 고정 메시지 띠는 헤더 아래.ds-chat-pinned(notice 도 bar notice 도 아니다). 칩·반응 버튼의 원형은--ds-btn-radius로 blanket radius 를 이긴다.
입력줄은 껍데기(.ds-composer-bar)와 줄(.ds-composer)이 나뉜다
- 윗선과 면은 껍데기가, 여백과 간격은 줄이 진다. 첨부 미리보기처럼 입력줄 위에 얹히는 것이 같은 바 안에 들어가야 해서다. 둘을 한 클래스로 합치면 미리보기가 바 밖으로 떨어지거나 선이 두 줄로 그어진다.
- 입력칸에는
.ds-composer__field를 붙인다(flex:1+min-width:0). 크기는.ds-input--32/--40이 그대로 진다. - 여러 줄 입력(Shift+Enter 줄바꿈)이 필요하면
<textarea rows="1">+ 줄에.ds-composer--multiline. 칸은 32px 한 줄로 시작해 내용만큼 자라고 120px 에서 안으로 스크롤한다(높이는 호출부가scrollHeight로 준다). AI 어시스턴트와 메신저는ds-input--40(40px · 14px 글자 · 다섯 줄까지)을 쓴다(AI 는 2026-10-08 에 32 → 40). PC 는 Enter 전송, Shift+Enter 줄바꿈 — IME 조합 중 Enter 는 무시. 휴대폰 키보드(pointer: coarse)는 Enter 가 줄바꿈이고 보내기는 버튼뿐(2026-10-13, 모바일 대화 화면). 폼용textarea.ds-input--32(최소 4rem)와 다른 규격이라.ds-composer안에서만 그렇게 그려진다. - 메신저 · 직원 앱 AI 어시스턴트(페이지 모드) 입력줄은
<textarea rows="1" class="ds-input--40 ds-composer__field">— 한 줄짜리 입력칸과 같은 40px · 14px 에서 시작해 다섯 줄(20 × 5 + 20)에서 멈춘다. 직원 앱 화면 설정 「크게」는 네 줄(employee-appmain.css가 max-height 만 덮는다). PC 는 Enter 전송 · Shift+Enter 줄바꿈, 휴대폰 키보드는 Enter 가 줄바꿈이고 보내기는 버튼뿐(2026-10-13) — 판정은messenger-helpers.ts의shouldSendOnEnter(coarsePointer) 한 곳. - ⚠️ 원형 버튼을
rounded-full유틸로 만들 수 없다. blanket 규칙이 모든<button>의 radius 를4px !important로 강제해서 동그라미를 그린 줄 알았던 자리가 모난 사각으로 렌더된다 — 메신저의 전송·첨부 버튼이 실제로 그 상태였다(2026-09-09 발견)..ds-composer__btn이 그 규칙이 읽는--ds-btn-radius를 세워서 이긴다 → radius !important로 맞불을 놓거나<div role="button">으로 바꾸지 말 것. 여기는 진짜<button>이라야 한다(키보드·폼 의미).- 비활성은
:disabled로 준다 —opacity-40을 손으로 얹지 않는다. 클래스가 투명도와pointer-events를 함께 받는다. - ⚠️ 보내기 아이콘 색은 버튼이 아니라 안쪽
.ds-ico가 진다(늘 흰색). 전역 규칙button:not([class*="bg-"]):disabled가 비활성 버튼의 글자색을 회색(#c1c1c1 !important)·투명도 1 로 강제해서, 아직 아무것도 안 친 보내기 버튼의 글리프만 회색으로 보였다(2026-10-06). 같은!important로 맞서지 말고 자식에 색을 준다 —.ds-composer__btn--send > .ds-ico.
「새 메시지 n개」 알약은 .ds-chat-new-messages 하나다 → 본문
- 뜨는 조건은 하나다 — 사용자가 위로 올려 맨 아래(최신 메시지)를 벗어나 있는 동안 상대 메시지가 왔을 때. 맨 아래 근처(160px 안)에서 받으면 지금처럼 따라 내려가고 알약은 뜨지 않는다. 내가 보낸 메시지는 세지 않는다(보내면 늘 맨 아래로 간다 · 내 다른 기기에서 보낸 것도 안 센다).
- 맨 아래 판정은
messenger-helpers.ts의isChatNearBottom한 벌이다 — 자동 스크롤 · 재동기화 · 알약이 같은 값을 쓴다. 화면마다scrollHeight - scrollTop - …를 다시 쓰지 말 것. 판정은 새 메시지를 목록에 붙이기 전에 한다(붙인 뒤에 재면 새 말풍선 높이만큼 멀어져 늘 「벗어남」으로 읽힌다). - 사라지는 길은 넷이다 — 알약 탭(맨 아래로 부드럽게 · 움직임 줄이기 설정이면 바로) · 직접 맨 아래까지 스크롤 · 내가 보냄 · 방 나가기/전환. 넷 다 숫자를 0 으로 되돌린다.
- 숫자는 99 를 넘으면 「99+」 로 멈춘다(
formatNewMessageCount) — 알약 폭이 자릿수로 계속 늘지 않게. - 자리는 뷰포트가 아니라 입력줄 묶음에 붙는다 — 목록과 입력줄 사이의 높이 0 앵커(
.ds-chat-new-messages-anchor) 위 8px, 가로 가운데. 키보드가 열려 입력줄이 올라가면 같이 올라가고, 첨부 미리보기 · 답글 줄이 얹혀도 그 위에 선다.fixed·bottom-*로 화면에 박지 말 것. - 면은 흰 면 +
--color-label-normal글자 +--shadow-popover— 떠 있는 것들(채팅 패널 · 팝오버)과 같은 높이로 읽혀야 한다. 다른 그림자 단계나 색 면(primary)으로 바꾸지 말 것. - ⚠️ 알약 모서리는
--ds-btn-radius로 이긴다 —.ds-composer__btn과 같은 탈출구.rounded-full유틸은 blanket<button>강제에 져서 사각으로 렌더되고,!important·<div role="button">우회도 금지. - @멘션 후보가 열려 있으면 알약은 숨는다 — 같은 자리(입력줄 바로 위)를 쓴다.
모바일 입력줄(캡슐형 .ds-composer--capsule)과 AI 첫 화면은 모바일 대화 화면 이 정본이다
- 직원 앱 메신저 · AI 어시스턴트(페이지 모드)의 캡슐형 입력줄 · 보내기 숨김 · AI 첫 화면(
.ds-chat-start-action·.ds-chat-menu-action) 규칙은 그 페이지에 있다. 여기에 다시 적지 말 것. - PC 채팅 위젯 · 관리자 AI 패널은 캡슐형이 아니다(이 페이지의 입력줄 그대로). 같은 템플릿이라
useHostHeader·isPageMode일 때만 모바일 클래스가 붙는다.
정본은 design-system.css 한 곳이다
- ⚠️ 파셜 안
<style>블록에 다시 쓰지 말 것.messenger-content.html첫머리의<style>은 unlayered 라 앱 CSS 와 유틸을 모두 이긴다 — 여기 값이 있으면 정본이 조용히 밀린다. 하드코딩 hex 6개가 실제로 그렇게 화면을 그리고 있었다. - 앱
main.css에 사본을 만들지 말 것. employee-app 에 있던 사본은 문서 순서에서 파셜에 밀려 한 번도 그려지지 않았고, 그 사이 값까지 갈려 있었다. - 다크 모드(2026-10-13) — 패널
.ds-chat-panel· 새 메시지 알림 면은bg-elevated(다크 neutral-700), 입력줄.ds-composer-bar· 캡슐 안 고스트 버튼은bg-container, 캡슐 입력줄 · 받은 말풍선.ds-bubble--received는bg-sunken(다크 900 — 카드보다 가라앉음), 오류 말풍선은status-error-bg/status-error잉크, 보내기 hover 는primary-strong. 말풍선 · 입력줄에 팔레트 색을 다시 쓰지 않는다. 확인: Playground 「미리보기 테마 → 다크」.
세 화면이 같은 대화 부품을 씁니다 — 직원 앱의 전체화면 메신저, 나머지 앱의 채팅 위젯, 그리고 AI 어시스턴트 도크. 직원 앱(모바일)에서만 켜지는 모양은 모바일 대화 화면 에 따로 있습니다. 셋이 한 화면에 나란히 뜨는 경우가 있어서, 부품이 갈리면 바로 눈에 띕니다.
어디에 쓰이나
| 부품 | 클래스 | 쓰는 곳 |
|---|---|---|
| 패널 껍데기 | .ds-chat-panel | 채팅 위젯 · 어시스턴트 |
| 말풍선 | .ds-bubble + --sent / --received / --error | 메신저 · 어시스턴트 |
| 입력줄 껍데기 | .ds-composer-bar | 메신저 · 어시스턴트 |
| 입력줄 | .ds-composer · __field · __btn | 〃 |
| 접속 표시 점 | .ds-presence + --online/offline/busy/away | 메신저 연락처 |
| 새 메시지 알약 | .ds-chat-new-messages (+ 자리 .ds-chat-new-messages-anchor) | 메신저 대화방(직원 앱 · 채팅 위젯) |
| 진입점(FAB) | .ds-fab--chat · .ds-fab--ai | → FAB 계약 |
위로 올려 보는 중에 온 새 메시지
예전 대화를 보려고 위로 올려 둔 사이에 새 메시지가 오면, 목록을 맨 아래로 끌어내리지 않습니다. 읽던 자리를 잃기 때문이에요. 대신 입력줄 바로 위 가운데에 「새 메시지 n개 ⌄」 알약이 뜨고, 그 뒤로 오는 메시지마다 숫자가 늘어납니다.
- 알약을 누르면 맨 아래로 부드럽게 내려가고 알약은 사라집니다. 기기 설정에서 「동작 줄이기」를 켠 사람에게는 애니메이션 없이 바로 이동해요.
- 알약을 누르지 않고 직접 맨 아래까지 스크롤해도 사라집니다.
- 맨 아래 근처에서 보고 있었다면 예전처럼 새 메시지를 따라 내려가고 알약은 뜨지 않습니다.
- 내가 보낸 메시지는 새 소식이 아니라서 세지 않습니다 — 보내면 늘 맨 아래로 갑니다.
알약은 뷰포트가 아니라 입력줄 묶음에 붙어 있습니다. 직원 앱에서 키보드가 열려 입력줄이 올라가면 알약도 함께 올라가요. 대화방 검색 막대가 입력줄 자리를 대신할 때도 그 위에 섭니다.
꼬리는 방향이다
말풍선은 네 모서리가 16px 이고, 말하는 쪽 아래 모서리 하나만 4px 로 깎습니다. 보낸 말풍선은 오른쪽 아래, 받은 말풍선은 왼쪽 아래예요. 값은 --ds-bubble-radius 와 --ds-bubble-tail 두 축이고, 방향은 변형 클래스가 정합니다 — 마크업에서 border-radius 를 직접 쓰지 마세요.
폭은 목록이 정합니다
말풍선 폭은 패널마다 달라야 합니다. 채팅 위젯·메신저는 75%, 어시스턴트 도크는 85%예요 — 도크는 하한이 360px 이라 75%면 한 줄에 담기는 글자가 너무 적습니다.
그래서 폭은 말풍선이 아니라 메시지 목록이 정합니다.
<div class="flex flex-col gap-1.5" style="--ds-bubble-max: 85%">
<div class="ds-bubble ds-bubble--received">…</div>
</div>스피너의 다섯 축은 @property { inherits: false } 로 상속을 일부러 막았는데, 여기는 반대입니다. 스피너는 조상에 값을 얹으면 안에 든 부팅·임베드 스피너까지 끌고 가서 막았고, 말풍선은 한 목록 안의 말풍선을 한꺼번에 조정하는 것이 정상 사용법이라 열어 뒀어요.
실패한 응답도 말풍선입니다
어시스턴트 응답이 실패하면 --error 말풍선이 그 자리에 섭니다. 노티스를 쓰지 않는 이유는 위치예요 — 노티스는 화면에 얹히는 안내라 대화 흐름 밖으로 나가는데, 실패한 응답은 「그 질문에 대한 답」이라 앞뒤 대화 사이 제자리에 있어야 시간 순서가 읽힙니다.
말풍선 세 종류
--sent | --received | --error | |
|---|---|---|---|
| 누구 | 나 | 상대 · 어시스턴트 | 실패한 응답 |
| 면 | --color-primary | --color-neutral-75 | --color-red-50 |
| 글자 | --ds-bw-white | --color-label-normal | --color-red-600 |
| 꼬리 | 오른쪽 아래 | 왼쪽 아래 | 왼쪽 아래 |
--plain 은 종류가 아니라 직교 축입니다(줄바꿈 보존). 텍스트 말풍선에만 붙이세요.
조정 축
| 변수 | 기본값 | 무엇 |
|---|---|---|
--ds-chat-panel-w | 384px | 패널 폭 |
--ds-chat-panel-h | 600px | 패널 높이 (화면 상한 calc(100vh - 120px) 이 함께 물린다) |
--ds-bubble-max | 75% | 최대 폭 — 목록에 얹는다 |
--ds-bubble-radius | var(--radius-16) | 기본 모서리 |
--ds-bubble-tail | var(--radius-4) | 꼬리 쪽 모서리 |
--ds-bubble-pad-y · -pad-x | 6px · 12px | 안쪽 여백 |
--ds-bubble-lh | 1.625 | 줄 간격(토큰 lh 1.4 아님) |
입력줄 부품
| 클래스 | 무엇 |
|---|---|
.ds-composer-bar | 껍데기 — 윗선·면. 첨부 미리보기까지 감싼다 |
.ds-composer | 줄 — 여백 12px, 간격 8px, 세로 가운데 |
.ds-composer__field | 입력칸 — flex:1 + min-width:0 (크기는 .ds-input--*) |
.ds-composer--multiline | 여러 줄 입력줄 — 버튼을 아래 줄에 맞춘다. 칸은 <textarea rows="1">(32px → 최대 120px · 메신저 ds-input--40 은 40px → 다섯 줄, 직원 앱 「크게」 네 줄) |
.ds-composer__btn | 36px 원형 버튼 (radius 강제를 변수로 이긴다) |
.ds-composer__btn--send | 주 동작 — primary 면 + 흰 글리프 |
.ds-composer__btn--ghost | 보조(첨부) — 면 없이, hover 에서만 배경 |
.ds-chat-new-messages-anchor | 목록과 입력줄 사이의 높이 0 앵커 — 알약 자리(입력줄과 함께 움직인다) |
.ds-chat-new-messages | 「새 메시지 n개」 알약 — 높이 36 · 흰 면 · --color-label-normal · b5 600 · --shadow-popover · 앵커 위 8px 가운데 |
| 모바일 캡슐형 · AI 첫 화면 클래스 | → 모바일 대화 화면 — 클래스 |
접속 표시 점
| 상태 | 색 |
|---|---|
--online | --color-green-500 |
--offline | --color-neutral-300 |
--busy | --color-red-500 |
--away | --color-yellow-500 |
흰 링(border: 2px solid)은 장식이 아닙니다 — 아바타 사진 위에 바로 얹히면 경계가 뭉개져요.
패널 껍데기
<!-- 자리·층은 .ds-fab-layer + .ds-fab--chat 이, 껍데기는 .ds-chat-panel 이 진다 -->
<div class="ds-fab-layer ds-fab--chat ds-chat-panel fixed bottom-26">
…헤더 · 목록 · 입력줄…
</div>말풍선
<!-- 메시지 목록 — 폭 축은 여기서 한 번만 -->
<div class="flex flex-col gap-1.5" style="--ds-bubble-max: 85%">
<div class="ds-bubble ds-bubble--sent ds-bubble--plain" x-text="msg.content"></div>
<div class="ds-bubble ds-bubble--received ai-md" x-html="md(msg.content)"></div>
</div>마크다운 말풍선에는 --plain 을 붙이지 않습니다.
입력줄
<div class="ds-composer-bar">
<!-- (선택) 첨부 미리보기 — 껍데기 안, 줄 위 -->
<div class="ds-composer">
<button type="button" class="ds-composer__btn ds-composer__btn--ghost"
title="파일 첨부" aria-label="파일 첨부" data-ds-tip="top">
<span class="ds-ico w-5 h-5" aria-hidden="true" x-html="$icon('ic-paperclip')"></span>
</button>
<input type="text" class="ds-input--40 ds-composer__field" />
<button type="button" class="ds-composer__btn ds-composer__btn--send"
:disabled="!input.trim()"
title="보내기" aria-label="보내기" data-ds-tip="top">
<span class="ds-ico w-4 h-4" aria-hidden="true" x-html="$icon('ic-send')"></span>
</button>
</div>
</div>캡슐형(모바일) 마크업은 → 모바일 대화 화면 — 캡슐형 입력줄 마크업
새 메시지 알약
목록(#chat-messages)과 입력줄 묶음 사이에 앵커를 둡니다. 셈 · 판정 · 이동은 Messenger.ts 가 합니다(newMessagesBelow · onChatScroll · jumpToLatestMessages).
<div id="chat-messages" @scroll.passive="onChatScroll($el)">…</div>
<div class="ds-chat-new-messages-anchor">
<button type="button" class="ds-chat-new-messages" x-show="newMessagesBelow > 0" x-cloak
data-testid="messenger-new-messages"
:aria-label="$store.hereby.t('messenger.new_messages_jump', { count: newMessagesCountText })"
@click="jumpToLatestMessages()">
<span x-text="$store.hereby.t('messenger.new_messages_below', { count: newMessagesCountText })"></span>
<span class="ds-ico" aria-hidden="true" x-html="$icon('ic-chevron-down')"></span>
</button>
</div>
<div class="ds-composer-bar">…</div>아이콘 버튼은 .ds-btn--icon 이 아니라서 툴팁 셀렉터에 자동으로 걸리지 않습니다 — data-ds-tip="top" 을 명시하세요 → 툴팁
접속 표시 점
클래스는 컴포넌트가 만듭니다(Messenger.ts 의 statusClass()). 마크업에서 직접 조립하지 마세요.
단일 출처
- 구현은
packages/static/styles/design-system.css의.ds-chat-panel/.ds-bubble/.ds-composer*/.ds-presence뿐입니다. TS 컴포넌트도 공유 HTML 파셜도 없어요 — 마크업을 직접 씁니다. - 복제본 없음(2026-09-09 정리 완료). 그전에는 셋이었습니다.
| 있던 곳 | 상태 | 어떻게 됐나 |
|---|---|---|
messenger-content.html 의 <style> | 하드코딩 hex 6개. unlayered 라 실제로 이기던 것 | 삭제 + 「여기 쓰지 말 것」 주석 |
apps/employee-app/src/styles/main.css | 문서 순서에서 밀려 죽어 있던 사본(값도 갈림) | 삭제 + 주석 |
ai-assistant.html 의 유틸 조합 | 유일하게 DS 토큰을 쓰던 쪽 | 클래스로 전환 |
왜 파셜 <style> 이 이기나
messenger-content.html 첫머리의 <style> 은 어느 레이어에도 들어 있지 않습니다. Tailwind 유틸은 @layer utilities 안이고, 레이어 밖 규칙은 특이도와 무관하게 레이어 안 규칙을 이깁니다. 게다가 <body> 안에 있어 문서 순서로도 앱 main.css 뒤에 옵니다.
그래서 거기 값이 있으면 정본이 조용히 밀립니다. 하드코딩 hex 6개가 정확히 그 상태였고, employee-app 의 사본은 그것에 밀려 한 번도 그려지지 않았습니다.
정리 전후
| 전 | 후 | |
|---|---|---|
| 내 말풍선 면 | #ea580c — Tailwind 기본 orange-600(DS 팔레트 밖) | --color-primary (#f66014) |
| 받은 말풍선 면 | #f3f4f6 — Tailwind gray-100 | --color-neutral-75 (#f1f1f1) |
| 받은 말풍선 글자 | #1f2937 — Tailwind gray-800 | --color-label-normal (#1b1b1b) |
| 채팅 패널 그림자 | shadow-2xl — Tailwind 기본 | --shadow-popover (DS 토큰) |
| 채팅 패널 테두리·상한 | neutral-200, 화면 상한 없음 | --color-line-normal, max-height 가드 |
| 어시스턴트 패널 높이 | 560px | 600px (채팅과 같은 값) |
| 어시스턴트 말풍선 | rounded-12, 꼬리 없음 | 메신저와 같은 16px + 꼬리 |
| 전송·첨부 버튼 | rounded-full 을 달고 사각으로 렌더 | 원형(변수로 radius 강제를 이김) |
| 접속 표시 점 | Tailwind 기본 4색 하드코딩 | DS 색 토큰 |
변경 이력
2026-10-13 — 메신저 말풍선 텔레그램식 묶음: 이름을 말풍선 안으로(연속이면 생략) · 묶음당 아바타 한 번(누르면 메뉴) · 시각/읽음을 말풍선 안으로.
.ds-bubble__sender·.ds-bubble__meta·.ds-chat-avatar·.ds-sender-tone-N2026-10-12 — 「새 메시지 n개」 알약
.ds-chat-new-messages신설(위로 올려 보는 중에 온 상대 메시지를 세고, 탭하면 맨 아래로) · 맨 아래 판정을isChatNearBottom한 벌로 통일 → 본문2026-10-08 — 규칙에 「말풍선을 감싸는 상자는
w-full」 함정 추가(AI 보낸 말풍선이 일찍 꺾이던 원인) · AI 입력줄 32 → 402026-10-06 — 모바일(캡슐형 입력줄 · AI 첫 화면) 규칙과 클래스를 모바일 대화 화면 으로 분리. 이 페이지에는 링크만 남김
2026-09-09 —
.ds-chat-panel·.ds-bubble·.ds-composer·.ds-presence신설, 말풍선 정의 3곳·패널 껍데기 2곳을 정본 한 곳으로 통합, 전송·첨부 버튼 원형 복구, 이 페이지 신설 → 변경 이력
남은 것
- 타이핑 인디케이터(어시스턴트의 바운스 점 3개)는 아직 어시스턴트 마크업 안에 있습니다 — Tailwind
animate-bounce+ 임의값[animation-delay:150ms]조합이라 DS 축으로 뺄 후보입니다. 채팅 쪽에는 아직 없어서 「둘이 공유하는 부품」이 아니라 보류했습니다. - 첨부 미리보기 칩(파일 썸네일 + 제거 버튼)도 메신저에만 있습니다. 제거 버튼이
bg-red-500 rounded-full조합이라 같은 radius 강제에 걸립니다 — 다음 순번입니다.