// 말풍선 공용 조각. // // 꼬리가 달린 판(`FoxPopover` · `FoxTooltipRich`)이 같은 그림을 쓴다. 그 되풀이를 여기로 모은다. // 믹스인만 있고 규칙이 없어 @use 해도 CSS가 늘지 않는다 — 부르는 자리에서만 나온다. // // ── 꼬리를 CSS로 그리는 이유 ─────────────────────────────────────────────────── // // 시안은 꼬리를 22×12 SVG로 준다. 그 그림에는 평평한 끝을 덮는 1px 흰 띠가 들어 있는데, // 꼬리를 판 위로 1px 겹쳐 올려 그 띠로 판의 1px 테두리를 지우는 구조다. Figma에서는 맞지만 // 브라우저에서는 **1px 두 개를 정확히 겹쳐야 하는 방식**이라, 소수 배율 화면(윈도우 125% 등)에서 // 둘이 서로 다른 물리 픽셀에 떨어지면 이음새에 실선이 비친다. 실제로 그렇게 보였다. // // 그래서 CSS로 그린다. **회전한 정사각형의 절반을 판 위에 걸치는** 방식이다: // // 1. 한 변이 22 ÷ √2(≈15.56px)인 정사각형을 45° 돌린다 → 밑변 22px, 높이 11px의 마름모 // 2. 그 마름모의 **가운데를 판과 맞닿는 변 위에** 둔다 // 3. 바깥을 향하는 두 변에만 1px 테두리를 준다 (안쪽 두 변은 테두리 없음) // // 그러면 안쪽 절반(11px)이 판의 테두리 선을 자기 배경으로 덮는다 — 1px끼리 맞추는 것이 아니라 // 11px짜리 면이 1px 선을 덮으므로 어떤 배율에서도 이음새가 남지 않는다. 바깥 두 변의 테두리는 // 마름모의 좌우 꼭짓점, 즉 판의 테두리와 같은 높이에서 끝나므로 선이 자연스럽게 이어진다. // // 45°로 돌리면 밑변과 높이의 비가 2:1이라 22×11이 된다. 시안은 22×12이므로 돌린 뒤 세로로 // 12/11만큼 늘인다(CSS 변환은 오른쪽부터 적용되므로 `scaleY(...) rotate(45deg)` 순서다). // 빗변 테두리가 그만큼 두꺼워지지만 1.09배라 눈에 띄지 않는다. // // SVG를 쓰지 않으니 마스크 id가 문서에서 겹칠 일도 없다(시안 SVG를 그대로 넣으면 말풍선이 // 둘 이상 뜰 때 같은 id를 서로 참조한다). // // 토큰이 아닌 값: // - ⚠️ 꼬리가 차지하는 자리 22×12px: 시안이 준 그림의 크기이고 토큰 스케일에 없다. // - ⚠️ 정사각형의 한 변 `1.5556rem`: 밑변이 22px이 되는 값(22 ÷ √2)이다. 밑변을 바꾸려면 // 이 값을 √2로 나눠 다시 구한다. // - ⚠️ `scaleY(12 / 11)`: 45° 마름모의 높이(11px)를 시안의 12px로 맞추는 보정이다. // - 시안의 `gap: -1px`은 옮기지 않았다 — 정사각형의 안쪽 절반이 이미 판 위에 걸쳐 테두리를 // 덮으므로 따로 당길 필요가 없다. @use "@fox/styles/abstracts" as fox; /// 말풍선 바깥 틀. 꼬리와 판이 세로로 서고, 꼬리의 가로 위치를 이 정렬이 정한다. @mixin root($inline-size: 36rem) { box-sizing: border-box; display: flex; flex-direction: column; inline-size: $inline-size; } /// 꼬리와 방향·위치 규칙 한 벌. /// /// `$block`은 BEM 블록 이름(`"fox-popover"` 등)이고, `$border`·`$fill`은 판과 **같은 값**을 /// 넘긴다 — 꼬리는 판이 뾰족하게 튀어나온 부분이라 색이 갈리면 안 된다. /// /// 부르는 쪽은 다음 마크업을 갖는다: ///
@mixin arrow($block, $border, $fill) { $root: ".#{$block}"; // 좌우 여백이 꼬리를 판 가장자리에서 띄운다. **늘리지 않는다** — 폭이 내용(22px + 여백 // 48px)만큼이라야 루트의 `align-items`가 이 덩어리를 왼쪽·가운데·오른쪽으로 옮길 수 있다. // // 판보다 위에 칠해진다. 마름모의 안쪽 절반이 판의 테두리를 덮어야 하는데, `--top`에서는 // 꼬리가 DOM에서 먼저 와 판이 나중에 덧칠되기 때문이다. #{$root}__arrow-area { position: relative; z-index: 1; box-sizing: border-box; display: flex; flex-direction: column; align-items: flex-start; padding-inline: fox.padding(8); } // 자리만 잡는다 — 그림은 아래 `::before`가 그린다(파일 상단 주석 참고). #{$root}__arrow { position: relative; display: block; inline-size: 2.2rem; block-size: 1.2rem; } #{$root}__arrow::before { content: ""; position: absolute; inset-inline-start: 50%; // 밑변이 22px이 되는 정사각형의 한 변: 22 ÷ √2. inline-size: 1.5556rem; block-size: 1.5556rem; border-style: solid; border-color: $border; border-width: 0; background: $fill; } // 정사각형의 가운데를 판과 맞닿는 변에 두고, 바깥을 향하는 두 변에만 테두리를 준다. // 45° 돌리면 원래의 위·왼쪽 변이 위쪽 두 빗변이 되고, 아래·오른쪽 변이 아래쪽 두 빗변이 된다. #{$root}--top #{$root}__arrow::before { inset-block-start: 100%; border-block-start-width: fox.border(1); border-inline-start-width: fox.border(1); transform: translate(-50%, -50%) scaleY(calc(12 / 11)) rotate(45deg); } #{$root}--bottom { justify-content: flex-end; } #{$root}--bottom #{$root}__arrow::before { inset-block-start: 0; border-block-end-width: fox.border(1); border-inline-end-width: fox.border(1); transform: translate(-50%, -50%) scaleY(calc(12 / 11)) rotate(45deg); } // 꼬리의 가로 위치는 루트의 정렬이 정한다(시안 지정). 판은 `align-self: stretch`라 이 값에 // 영향받지 않고 언제나 폭을 꽉 채운다 — 움직이는 것은 꼬리뿐이다. #{$root}--arrow-left { align-items: flex-start; } #{$root}--arrow-center { align-items: center; } #{$root}--arrow-right { align-items: flex-end; } }