Skip to content

대화 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 하나다. 팝오버·드롭다운과 같은 값이라야 「떠 있는 것」들이 한 화면에서 같은 높이로 읽힌다 — 여기만 진한 그림자(Tailwind shadow-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-avatar 32px — 사진 또는 색조 바탕 + 첫 글자)는 묶음의 마지막 말풍선 왼쪽 아래 한 번이고 누르면 메뉴(프로필 보기 · 1:1 채팅 · 멘션하기, senderMenuItems). 시각·읽음 영수증은 말풍선 안 오른쪽 아래(.ds-bubble__meta float). 아바타는 <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-app main.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%면 한 줄에 담기는 글자가 너무 적습니다.

그래서 폭은 말풍선이 아니라 메시지 목록이 정합니다.

html
<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-w384px패널 폭
--ds-chat-panel-h600px패널 높이 (화면 상한 calc(100vh - 120px) 이 함께 물린다)
--ds-bubble-max75%최대 폭 — 목록에 얹는다
--ds-bubble-radiusvar(--radius-16)기본 모서리
--ds-bubble-tailvar(--radius-4)꼬리 쪽 모서리
--ds-bubble-pad-y · -pad-x6px · 12px안쪽 여백
--ds-bubble-lh1.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__btn36px 원형 버튼 (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)은 장식이 아닙니다 — 아바타 사진 위에 바로 얹히면 경계가 뭉개져요.

패널 껍데기 ​

html
<!-- 자리·층은 .ds-fab-layer + .ds-fab--chat 이, 껍데기는 .ds-chat-panel 이 진다 -->
<div class="ds-fab-layer ds-fab--chat ds-chat-panel fixed bottom-26">
  …헤더 · 목록 · 입력줄…
</div>

말풍선 ​

html
<!-- 메시지 목록 — 폭 축은 여기서 한 번만 -->
<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 을 붙이지 않습니다.

입력줄 ​

html
<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).

html
<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 가드
어시스턴트 패널 높이560px600px (채팅과 같은 값)
어시스턴트 말풍선rounded-12, 꼬리 없음메신저와 같은 16px + 꼬리
전송·첨부 버튼rounded-full 을 달고 사각으로 렌더원형(변수로 radius 강제를 이김)
접속 표시 점Tailwind 기본 4색 하드코딩DS 색 토큰

변경 이력 ​

  • 2026-10-13 — 메신저 말풍선 텔레그램식 묶음: 이름을 말풍선 안으로(연속이면 생략) · 묶음당 아바타 한 번(누르면 메뉴) · 시각/읽음을 말풍선 안으로. .ds-bubble__sender · .ds-bubble__meta · .ds-chat-avatar · .ds-sender-tone-N

  • 2026-10-12 — 「새 메시지 n개」 알약 .ds-chat-new-messages 신설(위로 올려 보는 중에 온 상대 메시지를 세고, 탭하면 맨 아래로) · 맨 아래 판정을 isChatNearBottom 한 벌로 통일 → 본문

  • 2026-10-08 — 규칙에 「말풍선을 감싸는 상자는 w-full」 함정 추가(AI 보낸 말풍선이 일찍 꺾이던 원인) · AI 입력줄 32 → 40

  • 2026-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 강제에 걸립니다 — 다음 순번입니다.