@nurihaus/web-design-system 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +15 -4
  2. package/dist/ai/components.md +616 -10
  3. package/dist/ai/motion.md +429 -0
  4. package/dist/ai/tokens.md +90 -4
  5. package/dist/avatar.js +5 -3
  6. package/dist/bottom-bar.js +261 -0
  7. package/dist/bottom-nav.js +10 -57
  8. package/dist/breadcrumb.js +7 -5
  9. package/dist/callout.js +5 -3
  10. package/dist/card.js +278 -0
  11. package/dist/check-box.js +7 -5
  12. package/dist/chip-group.js +6 -4
  13. package/dist/chunk-3274.js +71 -0
  14. package/dist/chunk-345.js +72 -0
  15. package/dist/chunk-3547.js +56 -0
  16. package/dist/chunk-6459.js +107 -0
  17. package/dist/{chunk-6364.js → chunk-7335.js} +2 -67
  18. package/dist/chunk-7653.js +4 -0
  19. package/dist/chunk-910.js +17 -6
  20. package/dist/chunk-9497.js +66 -0
  21. package/dist/collapsible-menu.js +6 -4
  22. package/dist/components/badge/badge-props.d.ts +4 -4
  23. package/dist/components/badge/badge.stories.d.ts +1 -1
  24. package/dist/components/bottom-bar/base-bottom-bar.d.ts +4 -0
  25. package/dist/components/bottom-bar/bottom-bar-props.d.ts +1 -0
  26. package/dist/components/bottom-bar/bottom-bar-type.d.ts +3 -0
  27. package/dist/components/bottom-bar/bottom-bar.d.ts +6 -0
  28. package/dist/components/bottom-bar/bottom-bar.stories.d.ts +282 -0
  29. package/dist/components/button/button-type.d.ts +3 -1
  30. package/dist/components/button/button.stories.d.ts +1 -0
  31. package/dist/components/card/base-card.d.ts +3 -0
  32. package/dist/components/card/card-props.d.ts +9 -0
  33. package/dist/components/card/card-type.d.ts +8 -0
  34. package/dist/components/card/card.d.ts +5 -0
  35. package/dist/components/card/card.stories.d.ts +24 -0
  36. package/dist/components/description-list/base-description-list.d.ts +4 -0
  37. package/dist/components/description-list/description-list-props.d.ts +1 -0
  38. package/dist/components/description-list/description-list-type.d.ts +13 -0
  39. package/dist/components/description-list/description-list.d.ts +5 -0
  40. package/dist/components/description-list/description-list.stories.d.ts +20 -0
  41. package/dist/components/empty-state/base-empty-state.d.ts +4 -0
  42. package/dist/components/empty-state/empty-state-props.d.ts +10 -0
  43. package/dist/components/empty-state/empty-state-type.d.ts +14 -0
  44. package/dist/components/empty-state/empty-state.d.ts +5 -0
  45. package/dist/components/empty-state/empty-state.stories.d.ts +300 -0
  46. package/dist/components/icon/icon-paths.d.ts +4 -0
  47. package/dist/components/icon/icon.stories.d.ts +1 -1
  48. package/dist/components/list/base-list.d.ts +7 -0
  49. package/dist/components/list/list-group.d.ts +5 -0
  50. package/dist/components/list/list-group.stories.d.ts +14 -0
  51. package/dist/components/list/list-props.d.ts +2 -0
  52. package/dist/components/list/list-row.d.ts +5 -0
  53. package/dist/components/list/list-row.stories.d.ts +304 -0
  54. package/dist/components/list/list-type.d.ts +25 -0
  55. package/dist/components/media/base-media.d.ts +16 -0
  56. package/dist/components/media/media-props.d.ts +5 -0
  57. package/dist/components/media/media-thumbnail.d.ts +4 -0
  58. package/dist/components/media/media-thumbnail.stories.d.ts +34 -0
  59. package/dist/components/media/media-type.d.ts +43 -0
  60. package/dist/components/media/media-viewer.d.ts +4 -0
  61. package/dist/components/media/media-viewer.stories.d.ts +24 -0
  62. package/dist/components/message/base-message.d.ts +11 -0
  63. package/dist/components/message/message-event.d.ts +6 -0
  64. package/dist/components/message/message-props.d.ts +17 -0
  65. package/dist/components/message/message-thread.d.ts +6 -0
  66. package/dist/components/message/message-type.d.ts +21 -0
  67. package/dist/components/message/message.d.ts +5 -0
  68. package/dist/components/message/message.stories.d.ts +313 -0
  69. package/dist/components/message-composer/base-message-composer.d.ts +11 -0
  70. package/dist/components/message-composer/message-composer-props.d.ts +1 -0
  71. package/dist/components/message-composer/message-composer-type.d.ts +27 -0
  72. package/dist/components/message-composer/message-composer.d.ts +5 -0
  73. package/dist/components/message-composer/message-composer.stories.d.ts +44 -0
  74. package/dist/components/motion/motion-provider.d.ts +5 -0
  75. package/dist/components/motion/motion-type.d.ts +6 -0
  76. package/dist/components/motion/motion.d.ts +2 -0
  77. package/dist/components/motion/motion.stories.d.ts +23 -0
  78. package/dist/components/notice/base-notice.d.ts +4 -0
  79. package/dist/components/notice/notice-props.d.ts +16 -0
  80. package/dist/components/notice/notice-type.d.ts +19 -0
  81. package/dist/components/notice/notice.d.ts +5 -0
  82. package/dist/components/notice/notice.stories.d.ts +304 -0
  83. package/dist/components/radio/base-radio.d.ts +2 -1
  84. package/dist/components/radio/radio-props.d.ts +9 -1
  85. package/dist/components/radio/radio-type.d.ts +9 -0
  86. package/dist/components/radio/radio.stories.d.ts +10 -0
  87. package/dist/components/skeleton/skeleton.stories.d.ts +7 -7
  88. package/dist/components/tag/base-tag.d.ts +8 -0
  89. package/dist/components/tag/tag-props.d.ts +1 -0
  90. package/dist/components/tag/tag-type.d.ts +20 -0
  91. package/dist/components/tag/tag.d.ts +4 -0
  92. package/dist/components/tag/tag.stories.d.ts +14 -0
  93. package/dist/components/toast/toast.stories.d.ts +12 -12
  94. package/dist/components.css +1234 -171
  95. package/dist/cta-button.js +27 -14
  96. package/dist/data-table.js +5 -3
  97. package/dist/description-list.js +274 -0
  98. package/dist/drawer.js +7 -5
  99. package/dist/drop-down.js +8 -6
  100. package/dist/empty-state.js +300 -0
  101. package/dist/icon-text-badge.js +11 -9
  102. package/dist/icon-text-option-badge.js +8 -6
  103. package/dist/icon.js +7 -4
  104. package/dist/index.d.ts +28 -2
  105. package/dist/index.js +2297 -228
  106. package/dist/list-group.js +253 -0
  107. package/dist/list-row.js +302 -0
  108. package/dist/loading.js +6 -4
  109. package/dist/media-thumbnail.js +412 -0
  110. package/dist/media-viewer.js +274 -0
  111. package/dist/message-composer.js +395 -0
  112. package/dist/message-event.js +216 -0
  113. package/dist/message-thread.js +216 -0
  114. package/dist/message.js +271 -0
  115. package/dist/modal-with-double-button.js +11 -9
  116. package/dist/modal.js +9 -7
  117. package/dist/motion.js +52 -0
  118. package/dist/notice.js +342 -0
  119. package/dist/option-badge.js +11 -9
  120. package/dist/pagination.js +8 -6
  121. package/dist/radio.js +114 -30
  122. package/dist/skeleton.js +4 -2
  123. package/dist/step-progress.js +4 -2
  124. package/dist/styles.css +1124 -122
  125. package/dist/switch.js +6 -4
  126. package/dist/tabs.js +5 -3
  127. package/dist/tag.js +311 -0
  128. package/dist/text-area.js +8 -6
  129. package/dist/text-field.js +8 -6
  130. package/dist/text.js +5 -3
  131. package/dist/toast.js +12 -10
  132. package/dist/tokens/generated/index.d.ts +1 -0
  133. package/dist/tokens/generated/motion.d.ts +153 -0
  134. package/dist/tokens.css +117 -2
  135. package/dist/tokens.js +156 -1
  136. package/dist/tooltip.js +6 -4
  137. package/package.json +63 -1
@@ -170,6 +170,185 @@
170
170
  overflow-y: auto;
171
171
  }
172
172
 
173
+ /* 공용 키프레임. 컴포넌트 CSS 보다 앞에 둔다 */
174
+
175
+ /*
176
+ * 공용 키프레임. 컴포넌트 CSS 는 @keyframes 를 두지 않고 이 이름을 쓴다(빌드 R18).
177
+ * 시간·곡선은 쓰는 쪽 animation 에서 --motion-{토큰}-* 로 준다. 여기는 모양만 정한다.
178
+ * 쓰는 쪽이 두는 입력 변수: --motion-slide-offset(transform 값), --motion-swipe-from, --motion-swipe-to.
179
+ */
180
+
181
+ @keyframes motion-enter-fade {
182
+ from {
183
+ opacity: 0;
184
+ }
185
+ to {
186
+ opacity: 1;
187
+ }
188
+ }
189
+
190
+ @keyframes motion-exit-fade {
191
+ from {
192
+ opacity: 1;
193
+ }
194
+ to {
195
+ opacity: 0;
196
+ }
197
+ }
198
+
199
+ @keyframes motion-enter-scale {
200
+ from {
201
+ opacity: 0;
202
+ transform: scale(var(--motion-enter-scale-from));
203
+ }
204
+ to {
205
+ opacity: 1;
206
+ transform: none;
207
+ }
208
+ }
209
+
210
+ @keyframes motion-exit-scale {
211
+ from {
212
+ opacity: 1;
213
+ transform: none;
214
+ }
215
+ to {
216
+ opacity: 0;
217
+ transform: scale(var(--motion-enter-scale-from));
218
+ }
219
+ }
220
+
221
+ @keyframes motion-enter-slide {
222
+ from {
223
+ transform: var(--motion-slide-offset);
224
+ }
225
+ to {
226
+ transform: none;
227
+ }
228
+ }
229
+
230
+ @keyframes motion-exit-slide {
231
+ from {
232
+ transform: none;
233
+ }
234
+ to {
235
+ transform: var(--motion-slide-offset);
236
+ }
237
+ }
238
+
239
+ @keyframes motion-exit-swipe {
240
+ from {
241
+ transform: translateX(var(--motion-swipe-from));
242
+ }
243
+ to {
244
+ transform: translateX(var(--motion-swipe-to));
245
+ }
246
+ }
247
+
248
+ @keyframes motion-reject-wiggle {
249
+ 0%,
250
+ 100% {
251
+ transform: none;
252
+ }
253
+ 25% {
254
+ transform: translateX(var(--motion-reject-distance));
255
+ }
256
+ 50% {
257
+ transform: translateX(calc(var(--motion-reject-distance) * -1));
258
+ }
259
+ 75% {
260
+ transform: translateX(calc(var(--motion-reject-distance) / 2));
261
+ }
262
+ }
263
+
264
+ @keyframes motion-reject-shake {
265
+ 0%,
266
+ 100% {
267
+ transform: none;
268
+ }
269
+ 15%,
270
+ 45%,
271
+ 75% {
272
+ transform: translateX(calc(var(--motion-reject-distance) * -2));
273
+ }
274
+ 30%,
275
+ 60% {
276
+ transform: translateX(calc(var(--motion-reject-distance) * 2));
277
+ }
278
+ 90% {
279
+ transform: translateX(var(--motion-reject-distance));
280
+ }
281
+ }
282
+
283
+ @keyframes motion-attention-shine {
284
+ from {
285
+ transform: translateX(-100%);
286
+ }
287
+ to {
288
+ transform: translateX(100%);
289
+ }
290
+ }
291
+
292
+ @keyframes motion-attention-blink {
293
+ 0%,
294
+ 100% {
295
+ opacity: 1;
296
+ }
297
+ 50% {
298
+ opacity: 0;
299
+ }
300
+ }
301
+
302
+ @keyframes motion-busy-dots {
303
+ 0%,
304
+ 80%,
305
+ 100% {
306
+ opacity: 0;
307
+ }
308
+ 40% {
309
+ opacity: 1;
310
+ }
311
+ }
312
+
313
+ @keyframes motion-busy-ring {
314
+ from {
315
+ transform: rotate(0deg);
316
+ }
317
+ to {
318
+ transform: rotate(360deg);
319
+ }
320
+ }
321
+
322
+ @keyframes motion-busy-shimmer {
323
+ from {
324
+ transform: translateX(-100%);
325
+ }
326
+ to {
327
+ transform: translateX(100%);
328
+ }
329
+ }
330
+
331
+ @keyframes motion-enter-pop {
332
+ from {
333
+ opacity: 0;
334
+ transform: var(--motion-slide-offset) scale(var(--motion-enter-pop-scale));
335
+ }
336
+ to {
337
+ opacity: 1;
338
+ transform: none;
339
+ }
340
+ }
341
+
342
+ @keyframes motion-attention-float {
343
+ 0%,
344
+ 100% {
345
+ transform: none;
346
+ }
347
+ 50% {
348
+ transform: translateY(calc(var(--motion-attention-float-distance) * -1));
349
+ }
350
+ }
351
+
173
352
  /* Component */
174
353
 
175
354
  .base-avatar {
@@ -338,6 +517,10 @@
338
517
  display: flex;
339
518
  align-items: center;
340
519
  justify-content: center;
520
+ /* width="fit-content" 일 때 글자가 테두리에 붙지 않게 양옆을 남긴다 */
521
+ padding: 0 var(--spacing-12);
522
+ box-sizing: border-box;
523
+ white-space: nowrap;
341
524
 
342
525
  border-width: var(--hairline);
343
526
  border-style: solid;
@@ -354,6 +537,9 @@
354
537
  align-items: center;
355
538
  justify-content: center;
356
539
  gap: var(--spacing-4);
540
+ padding: 0 var(--spacing-12);
541
+ box-sizing: border-box;
542
+ white-space: nowrap;
357
543
 
358
544
  border-width: var(--hairline);
359
545
  border-style: solid;
@@ -403,6 +589,20 @@
403
589
  border-color: transparent;
404
590
  }
405
591
 
592
+ /* Toast 의 success·warning 과 같은 계열. 글자는 800 단계라 50 바탕에서 대비 6.8:1 */
593
+
594
+ .variant-success:is(.option,.icon-text,.icon-text-option) {
595
+ color: var(--color-accent-green-800);
596
+ background-color: var(--color-accent-green-50);
597
+ border-color: transparent;
598
+ }
599
+
600
+ .variant-warning:is(.option,.icon-text,.icon-text-option) {
601
+ color: var(--color-accent-orange-800);
602
+ background-color: var(--color-accent-orange-50);
603
+ border-color: transparent;
604
+ }
605
+
406
606
  /* icon-text-option 은 테두리가 없는 모양이라 두께·스타일까지 여기서 준다. */
407
607
 
408
608
  .variant-outlined:is(.option,.icon-text,.icon-text-option) {
@@ -411,6 +611,35 @@
411
611
  border: var(--hairline) solid var(--color-grey-scale-300);
412
612
  }
413
613
 
614
+ /* 스크롤하는 조상의 맨 아래에 붙는다. 모션은 없다 — 주 행동·입력을 담아 스크롤로 숨기지 않는다 */
615
+
616
+ .base-bottom-bar {
617
+ position: sticky;
618
+ bottom: 0;
619
+ z-index: var(--z-sticky);
620
+ display: flex;
621
+ }
622
+
623
+ .base-bottom-bar > * {
624
+ flex: 1;
625
+ min-width: 0;
626
+ }
627
+
628
+ /***************************************************************************************************
629
+ * *
630
+ * bottom-bar *
631
+ * *
632
+ ***************************************************************************************************/
633
+
634
+ .bottom-bar {
635
+ gap: var(--spacing-8);
636
+ padding: var(--spacing-12) var(--spacing-16);
637
+ /* 기기 홈 막대만큼 아래를 더 비운다. env() 의 대체값 0 은 단위가 있어야 calc 가 성립한다 */
638
+ padding-bottom: calc(var(--spacing-12) + env(safe-area-inset-bottom, 0px)); /* raw-px */
639
+ border-top: var(--hairline) solid var(--color-grey-scale-200);
640
+ background-color: var(--color-white);
641
+ }
642
+
414
643
  .base-bottom-nav:where(.root) {
415
644
  position: fixed;
416
645
  left: var(--spacing-16);
@@ -851,12 +1080,12 @@
851
1080
  /* 나타날 때 꼬리에서 튀어나온 뒤 세 번 둥실 움직이고 멈춘다 (ADR 0007). 계속 움직이면 거슬린다.
852
1081
  둥실 지연 = 팝 지연 + 팝 시간. 팝 쪽을 바꾸면 둥실 지연도 같이 바꾼다 */
853
1082
  --callout-origin: var(--spacing-16) 100%;
854
- --callout-enter: translateY(var(--spacing-4));
1083
+ --motion-slide-offset: translateY(var(--spacing-4));
855
1084
 
856
1085
  transform-origin: var(--callout-origin);
857
1086
  animation:
858
- callout-pop var(--motion-slow) var(--motion-spring) var(--motion-fast) both,
859
- callout-float calc(var(--motion-base) * 8) var(--motion-ease)
1087
+ motion-enter-pop var(--motion-slow) var(--motion-spring) var(--motion-fast) both,
1088
+ motion-attention-float calc(var(--motion-base) * 8) var(--motion-ease)
860
1089
  calc(var(--motion-fast) + var(--motion-slow)) 3;
861
1090
  }
862
1091
 
@@ -866,7 +1095,7 @@
866
1095
 
867
1096
  .base-callout:is(.tail-top-start,.tail-top-end) {
868
1097
  --callout-origin: var(--spacing-16) 0;
869
- --callout-enter: translateY(calc(var(--spacing-4) * -1));
1098
+ --motion-slide-offset: translateY(calc(var(--spacing-4) * -1));
870
1099
  }
871
1100
 
872
1101
  .base-callout.tail-top-end {
@@ -905,34 +1134,55 @@
905
1134
  right: var(--spacing-12);
906
1135
  }
907
1136
 
908
- @keyframes callout-pop {
909
- from {
910
- opacity: 0;
911
- transform: var(--callout-enter) scale(0.6);
1137
+ @media (prefers-reduced-motion: reduce) {
1138
+ .base-callout {
1139
+ animation: none;
912
1140
  }
1141
+ }
913
1142
 
914
- to {
915
- opacity: 1;
916
- transform: none;
917
- }
1143
+ .base-card {
1144
+ display: flex;
1145
+ flex-direction: column;
918
1146
  }
919
1147
 
920
- @keyframes callout-float {
921
- 0%,
922
- 100% {
923
- transform: none;
924
- }
1148
+ /***************************************************************************************************
1149
+ * *
1150
+ * card *
1151
+ * *
1152
+ ***************************************************************************************************/
925
1153
 
926
- 50% {
927
- transform: translateY(calc(var(--spacing-6) / -2));
928
- }
1154
+ .card {
1155
+ gap: var(--spacing-12);
1156
+ padding: var(--spacing-16);
1157
+ border: var(--hairline) solid var(--color-grey-scale-200);
1158
+ border-radius: var(--radius-16);
1159
+ background-color: var(--color-white);
929
1160
  }
930
1161
 
931
- @media (prefers-reduced-motion: reduce) {
932
- .base-callout {
933
- animation: none;
1162
+ .card.variant-tinted {
1163
+ border-color: var(--color-purple-100);
1164
+ background-color: var(--color-purple-50);
934
1165
  }
935
- }
1166
+
1167
+ /* 누르는 카드(링크·버튼)만 눌림 모션을 받는다. 보기만 하는 카드는 움직이지 않는다 */
1168
+
1169
+ .card:is(a[href],button) {
1170
+ width: 100%;
1171
+ color: inherit;
1172
+ text-align: start;
1173
+ text-decoration: none;
1174
+ cursor: pointer;
1175
+ transition: transform var(--motion-press-duration) var(--motion-press-ease);
1176
+ }
1177
+
1178
+ .card:is(a[href],button):active {
1179
+ transform: scale(var(--motion-press-scale));
1180
+ }
1181
+
1182
+ .card:is(a[href],button):focus-visible {
1183
+ outline: var(--hairline) solid var(--color-purple-600);
1184
+ outline-offset: var(--spacing-2);
1185
+ }
936
1186
 
937
1187
  .base-check-box.label {
938
1188
  font-size: var(--font-size-sm);
@@ -1182,6 +1432,63 @@
1182
1432
  margin-bottom: var(--spacing-4);
1183
1433
  }
1184
1434
 
1435
+ .base-description-list {
1436
+ display: flex;
1437
+ flex-direction: column;
1438
+ margin: 0;
1439
+ }
1440
+
1441
+ /***************************************************************************************************
1442
+ * *
1443
+ * description-list *
1444
+ * *
1445
+ ***************************************************************************************************/
1446
+
1447
+ .description-list {
1448
+ gap: var(--spacing-8);
1449
+ }
1450
+
1451
+ .description-list-item {
1452
+ display: flex;
1453
+ justify-content: space-between;
1454
+ gap: var(--spacing-12);
1455
+ }
1456
+
1457
+ .description-list-item > dt {
1458
+ color: var(--color-grey-scale-500);
1459
+ font-size: var(--font-size-sm);
1460
+ line-height: var(--line-height-sm);
1461
+ }
1462
+
1463
+ .description-list-item > dd {
1464
+ margin: 0;
1465
+ color: var(--color-grey-scale-900);
1466
+ font-size: var(--font-size-sm);
1467
+ line-height: var(--line-height-sm);
1468
+ font-weight: var(--font-weight-medium);
1469
+ text-align: end;
1470
+ }
1471
+
1472
+ /* 합계 줄. 구분선 아래 값이 한 단계 크고 굵다 */
1473
+
1474
+ .description-list-item.total {
1475
+ align-items: baseline;
1476
+ margin-top: var(--spacing-4);
1477
+ padding-top: var(--spacing-12);
1478
+ border-top: var(--hairline) solid var(--color-grey-scale-200);
1479
+ }
1480
+
1481
+ .description-list-item.total > dt {
1482
+ color: var(--color-grey-scale-900);
1483
+ font-weight: var(--font-weight-semibold);
1484
+ }
1485
+
1486
+ .description-list-item.total > dd {
1487
+ font-size: var(--font-size-base);
1488
+ line-height: var(--line-height-base);
1489
+ font-weight: var(--font-weight-bold);
1490
+ }
1491
+
1185
1492
  /* Title·Close 같은 조각도 base-drawer 를 달므로 상자 스타일은 overlay·content 에만 건다. */
1186
1493
 
1187
1494
  .base-drawer:where(.overlay) {
@@ -1195,11 +1502,11 @@
1195
1502
  }
1196
1503
 
1197
1504
  .base-drawer[data-state='open']:where(.overlay) {
1198
- animation: drawer-overlay-in var(--motion-base) var(--motion-ease);
1505
+ animation: motion-enter-fade var(--motion-base) var(--motion-ease);
1199
1506
  }
1200
1507
 
1201
1508
  .base-drawer[data-state='closed']:where(.overlay) {
1202
- animation: drawer-overlay-out var(--motion-base) var(--motion-ease);
1509
+ animation: motion-exit-fade var(--motion-base) var(--motion-ease);
1203
1510
  }
1204
1511
 
1205
1512
  .base-drawer:where(.content) {
@@ -1240,48 +1547,12 @@
1240
1547
  * *
1241
1548
  ***************************************************************************************************/
1242
1549
 
1243
- /* Radix 는 애니메이션이 끝나야 닫힌 패널을 지우므로 transition 이 아니라 keyframes 로 들어오고 나간다. */
1550
+ /* Radix 는 애니메이션이 끝나야 닫힌 패널을 지우므로 transition 이 아니라 keyframes 로 들어오고 나간다(styles/motion.css). */
1244
1551
 
1245
- @keyframes drawer-overlay-in {
1246
- from {
1247
- opacity: 0;
1248
- }
1249
- to {
1250
- opacity: 1;
1251
- }
1252
- }
1253
-
1254
- @keyframes drawer-overlay-out {
1255
- from {
1256
- opacity: 1;
1257
- }
1258
- to {
1259
- opacity: 0;
1260
- }
1261
- }
1262
-
1263
- @keyframes drawer-slide-in {
1264
- from {
1265
- transform: var(--drawer-offset);
1266
- }
1267
- to {
1268
- transform: translate(0, 0);
1269
- }
1270
- }
1271
-
1272
- @keyframes drawer-slide-out {
1273
- from {
1274
- transform: translate(0, 0);
1275
- }
1276
- to {
1277
- transform: var(--drawer-offset);
1278
- }
1279
- }
1280
-
1281
- .drawer {
1282
- gap: var(--spacing-8);
1283
- padding: var(--spacing-24);
1284
- box-sizing: border-box;
1552
+ .drawer {
1553
+ gap: var(--spacing-8);
1554
+ padding: var(--spacing-24);
1555
+ box-sizing: border-box;
1285
1556
  }
1286
1557
 
1287
1558
  .drawer.size-s {
@@ -1305,7 +1576,7 @@
1305
1576
  bottom: 0;
1306
1577
  width: var(--drawer-width);
1307
1578
  max-width: 100%;
1308
- --drawer-offset: translateX(100%);
1579
+ --motion-slide-offset: translateX(100%);
1309
1580
  }
1310
1581
 
1311
1582
  .drawer.side-left {
@@ -1314,7 +1585,7 @@
1314
1585
  bottom: 0;
1315
1586
  width: var(--drawer-width);
1316
1587
  max-width: 100%;
1317
- --drawer-offset: translateX(-100%);
1588
+ --motion-slide-offset: translateX(-100%);
1318
1589
  }
1319
1590
 
1320
1591
  .drawer.side-bottom {
@@ -1325,15 +1596,15 @@
1325
1596
  max-height: 100%;
1326
1597
  border-top-left-radius: var(--radius-16);
1327
1598
  border-top-right-radius: var(--radius-16);
1328
- --drawer-offset: translateY(100%);
1599
+ --motion-slide-offset: translateY(100%);
1329
1600
  }
1330
1601
 
1331
1602
  .drawer[data-state='open'] {
1332
- animation: drawer-slide-in var(--motion-base) var(--motion-ease);
1603
+ animation: motion-enter-slide var(--motion-base) var(--motion-ease);
1333
1604
  }
1334
1605
 
1335
1606
  .drawer[data-state='closed'] {
1336
- animation: drawer-slide-out var(--motion-base) var(--motion-ease);
1607
+ animation: motion-exit-slide var(--motion-base) var(--motion-ease);
1337
1608
  }
1338
1609
 
1339
1610
  .drawer-header {
@@ -2059,6 +2330,64 @@
2059
2330
  color: var(--color-accent-orange-900);
2060
2331
  }
2061
2332
 
2333
+ /* 빈 목록·오류·찾을 수 없음·준비 중 화면. 가운데 세로로 쌓는다 */
2334
+
2335
+ .base-empty-state {
2336
+ display: flex;
2337
+ flex-direction: column;
2338
+ align-items: center;
2339
+ text-align: center;
2340
+ }
2341
+
2342
+ /***************************************************************************************************
2343
+ * *
2344
+ * empty-state *
2345
+ * *
2346
+ ***************************************************************************************************/
2347
+
2348
+ .empty-state {
2349
+ gap: var(--spacing-8);
2350
+ padding: var(--spacing-24) var(--spacing-16);
2351
+
2352
+ /* variant 는 아이콘 원 색만 바꾼다 */
2353
+ }
2354
+
2355
+ .empty-state.variant-neutral .empty-state-icon {
2356
+ background-color: var(--color-grey-scale-100);
2357
+ color: var(--color-grey-scale-600);
2358
+ }
2359
+
2360
+ .empty-state-icon {
2361
+ display: inline-flex;
2362
+ align-items: center;
2363
+ justify-content: center;
2364
+ width: var(--empty-state-icon-box);
2365
+ height: var(--empty-state-icon-box);
2366
+ margin-bottom: var(--spacing-8);
2367
+ border-radius: var(--radius-full);
2368
+ background-color: var(--color-purple-50);
2369
+ color: var(--color-purple-900);
2370
+ }
2371
+
2372
+ .empty-state-title {
2373
+ font-size: var(--font-size-base);
2374
+ line-height: var(--line-height-base);
2375
+ font-weight: var(--font-weight-semibold);
2376
+ color: var(--color-grey-scale-900);
2377
+ word-break: break-word;
2378
+ }
2379
+
2380
+ .empty-state-description {
2381
+ font-size: var(--font-size-sm);
2382
+ line-height: var(--line-height-sm);
2383
+ color: var(--color-grey-scale-500);
2384
+ word-break: break-word;
2385
+ }
2386
+
2387
+ .empty-state-action {
2388
+ margin-top: var(--spacing-8);
2389
+ }
2390
+
2062
2391
  .base-icon {
2063
2392
  flex-shrink: 0;
2064
2393
  vertical-align: middle;
@@ -2089,6 +2418,136 @@
2089
2418
  height: var(--icon-size-xl);
2090
2419
  }
2091
2420
 
2421
+ .base-list:where(.group) {
2422
+ display: flex;
2423
+ flex-direction: column;
2424
+ }
2425
+
2426
+ /* 행은 div·button·링크 어느 것이든 같은 모양이다 */
2427
+
2428
+ .base-list:where(.row) {
2429
+ position: relative;
2430
+ display: flex;
2431
+ align-items: center;
2432
+ box-sizing: border-box;
2433
+ width: 100%;
2434
+ border: 0;
2435
+ background-color: transparent;
2436
+ color: inherit;
2437
+ text-align: start;
2438
+ text-decoration: none;
2439
+ }
2440
+
2441
+ /***************************************************************************************************
2442
+ * *
2443
+ * list-group *
2444
+ * *
2445
+ ***************************************************************************************************/
2446
+
2447
+ .list-group {
2448
+ gap: var(--spacing-8);
2449
+ }
2450
+
2451
+ .list-group-label {
2452
+ padding: 0 var(--spacing-4);
2453
+ color: var(--color-grey-scale-500);
2454
+ font-size: var(--font-size-sm);
2455
+ line-height: var(--line-height-sm);
2456
+ font-weight: var(--font-weight-semibold);
2457
+ }
2458
+
2459
+ .list-group-surface {
2460
+ display: flex;
2461
+ flex-direction: column;
2462
+ overflow: hidden;
2463
+ border: var(--hairline) solid var(--color-grey-scale-200);
2464
+ border-radius: var(--radius-16);
2465
+ background-color: var(--color-white);
2466
+ }
2467
+
2468
+ /***************************************************************************************************
2469
+ * *
2470
+ * list-row *
2471
+ * *
2472
+ ***************************************************************************************************/
2473
+
2474
+ .list-row {
2475
+ min-height: var(--list-row-height);
2476
+ gap: var(--spacing-12);
2477
+ padding: var(--spacing-10) var(--spacing-16);
2478
+
2479
+ /* 누르는 행(링크·버튼)은 줄어들지 않고 배경이 어두워진다. 폭이 꽉 찬 면이라 scale 이 어색하다 */
2480
+ }
2481
+
2482
+ .list-row:is(a[href],button) {
2483
+ cursor: pointer;
2484
+ transition: background-color var(--motion-press-duration) var(--motion-press-ease);
2485
+ }
2486
+
2487
+ .list-row:is(a[href],button):active {
2488
+ background-color: var(--color-grey-scale-100);
2489
+ }
2490
+
2491
+ /* 면이 overflow: hidden 이라 테두리 안쪽에 그린다 */
2492
+
2493
+ .list-row:is(a[href],button):focus-visible {
2494
+ outline: var(--hairline) solid var(--color-purple-600);
2495
+ outline-offset: calc(var(--spacing-2) * -1);
2496
+ }
2497
+
2498
+ /* 행 사이 구분선. 왼쪽은 안쪽 여백만큼 띄운다 */
2499
+
2500
+ .list-row + .list-row::before {
2501
+ content: '';
2502
+ position: absolute;
2503
+ top: 0;
2504
+ right: 0;
2505
+ left: var(--spacing-16);
2506
+ height: var(--hairline);
2507
+ background-color: var(--color-grey-scale-100);
2508
+ }
2509
+
2510
+ .list-row-leading {
2511
+ display: flex;
2512
+ flex: none;
2513
+ color: var(--color-grey-scale-700);
2514
+ }
2515
+
2516
+ .list-row-text {
2517
+ display: flex;
2518
+ flex: 1;
2519
+ flex-direction: column;
2520
+ gap: var(--spacing-2);
2521
+ min-width: 0;
2522
+ }
2523
+
2524
+ .list-row-label {
2525
+ color: var(--color-grey-scale-900);
2526
+ font-size: var(--font-size-base);
2527
+ line-height: var(--line-height-base);
2528
+ font-weight: var(--font-weight-medium);
2529
+ }
2530
+
2531
+ .list-row-description {
2532
+ color: var(--color-grey-scale-500);
2533
+ font-size: var(--font-size-sm);
2534
+ line-height: var(--line-height-sm);
2535
+ }
2536
+
2537
+ .list-row-trailing {
2538
+ display: flex;
2539
+ flex: none;
2540
+ align-items: center;
2541
+ color: var(--color-grey-scale-500);
2542
+ font-size: var(--font-size-sm);
2543
+ line-height: var(--line-height-sm);
2544
+ }
2545
+
2546
+ .list-row-chevron {
2547
+ flex: none;
2548
+ color: var(--color-grey-scale-400);
2549
+ }
2550
+
2092
2551
  .base-inline-loading {
2093
2552
  display: flex;
2094
2553
  width: 100%;
@@ -2123,76 +2582,497 @@
2123
2582
  gap: var(--spacing-16);
2124
2583
  }
2125
2584
 
2126
- /* 스피너를 레이아웃 크기로 키운다. transform: scale 은 보이는 크기만 키워 아래 글자와 겹친다 */
2585
+ /* 스피너를 레이아웃 크기로 키운다. transform: scale 은 보이는 크기만 키워 아래 글자와 겹친다 */
2586
+
2587
+ .page-loading-indicator {
2588
+ --spinner-stroke: var(--overlay-spinner-stroke);
2589
+ }
2590
+
2591
+ .page-loading-indicator .base-spinner,.page-loading-indicator .loading-indicator-ring {
2592
+ width: var(--overlay-spinner-size);
2593
+ height: var(--overlay-spinner-size);
2594
+ }
2595
+
2596
+ .page-loading-text {
2597
+ position: relative;
2598
+ }
2599
+
2600
+ .base-spinner {
2601
+ display: flex;
2602
+ width: var(--icon-size-m);
2603
+ height: var(--icon-size-m);
2604
+ align-items: center;
2605
+ justify-content: center;
2606
+ }
2607
+
2608
+ /* 우선순위 0 이라 넘겨받은 className 이나 style 의 color 가 늘 이긴다. */
2609
+
2610
+ :where(.base-spinner) {
2611
+ color: var(--color-grey-scale-200);
2612
+ }
2613
+
2614
+ .loading-indicator-ring {
2615
+ position: relative;
2616
+ width: var(--icon-size-m);
2617
+ height: var(--icon-size-m);
2618
+ }
2619
+
2620
+ .loading-indicator-single-ring {
2621
+ box-sizing: border-box;
2622
+ display: block;
2623
+ position: absolute;
2624
+ width: 100%;
2625
+ height: 100%;
2626
+ border: var(--spinner-stroke) solid;
2627
+ border-radius: 50%;
2628
+ animation: motion-busy-ring var(--spinner-duration) var(--spinner-ease) infinite;
2629
+ border-color: currentColor transparent transparent transparent;
2630
+ }
2631
+
2632
+ .loading-indicator-single-ring:nth-child(1) {
2633
+ animation-delay: calc(var(--spinner-duration) * -0.375);
2634
+ }
2635
+
2636
+ .loading-indicator-single-ring:nth-child(2) {
2637
+ animation-delay: calc(var(--spinner-duration) * -0.25);
2638
+ }
2639
+
2640
+ .loading-indicator-single-ring:nth-child(3) {
2641
+ animation-delay: calc(var(--spinner-duration) * -0.125);
2642
+ }
2643
+
2644
+ .loading-indicator-single-ring:nth-child(4) {
2645
+ animation-delay: 0s;
2646
+ }
2647
+
2648
+ /* Title·Close 같은 조각도 base-media 를 달므로 상자 스타일은 overlay·content 에만 건다. */
2649
+
2650
+ .base-media:where(.overlay) {
2651
+ position: fixed;
2652
+ inset: 0;
2653
+ z-index: var(--z-overlay);
2654
+ background-color: color-mix(in srgb, var(--color-black) 90%, transparent);
2655
+ }
2656
+
2657
+ .base-media[data-state='open']:where(.overlay) {
2658
+ animation: motion-enter-fade var(--motion-enter-duration) var(--motion-enter-ease);
2659
+ }
2660
+
2661
+ .base-media[data-state='closed']:where(.overlay) {
2662
+ animation: motion-exit-fade var(--motion-exit-duration) var(--motion-exit-ease);
2663
+ }
2664
+
2665
+ .base-media:where(.content) {
2666
+ position: fixed;
2667
+ inset: 0;
2668
+ z-index: var(--z-modal);
2669
+ display: flex;
2670
+ align-items: center;
2671
+ justify-content: center;
2672
+ }
2673
+
2674
+ .base-media:where(.close) {
2675
+ display: inline-flex;
2676
+ align-items: center;
2677
+ justify-content: center;
2678
+ padding: 0;
2679
+ border: none;
2680
+ background-color: transparent;
2681
+ cursor: var(--cursor-button);
2682
+ }
2683
+
2684
+ /***************************************************************************************************
2685
+ * *
2686
+ * media-viewer *
2687
+ * *
2688
+ ***************************************************************************************************/
2689
+
2690
+ /* Radix 는 애니메이션이 끝나야 닫힌 Content 를 지우므로 keyframes 로 들어오고 나간다(styles/motion.css). */
2691
+
2692
+ .media-viewer[data-state='open'] {
2693
+ animation: motion-enter-scale var(--motion-enter-duration) var(--motion-enter-ease);
2694
+ }
2695
+
2696
+ .media-viewer[data-state='closed'] {
2697
+ animation: motion-exit-scale var(--motion-exit-duration) var(--motion-exit-ease);
2698
+ }
2699
+
2700
+ .media-viewer-media {
2701
+ display: block;
2702
+ max-width: 100%;
2703
+ max-height: 100%;
2704
+ -o-object-fit: contain;
2705
+ object-fit: contain;
2706
+ }
2707
+
2708
+ /* 노치 아래로 숨지 않게 안전 영역만큼 내린다 */
2709
+
2710
+ .media-viewer-close {
2711
+ position: absolute;
2712
+ top: calc(env(safe-area-inset-top) + var(--spacing-8));
2713
+ right: calc(env(safe-area-inset-right) + var(--spacing-8));
2714
+ width: var(--touch-target);
2715
+ height: var(--touch-target);
2716
+ color: var(--color-white);
2717
+ }
2718
+
2719
+ /***************************************************************************************************
2720
+ * *
2721
+ * media-thumbnail *
2722
+ * *
2723
+ ***************************************************************************************************/
2724
+
2725
+ .media-thumbnail {
2726
+ position: relative;
2727
+ flex-shrink: 0;
2728
+ }
2729
+
2730
+ .media-thumbnail.size-s {
2731
+ width: var(--media-thumbnail-size-s);
2732
+ height: var(--media-thumbnail-size-s);
2733
+ }
2734
+
2735
+ .media-thumbnail.size-m {
2736
+ width: var(--media-thumbnail-size-m);
2737
+ height: var(--media-thumbnail-size-m);
2738
+ }
2739
+
2740
+ /* 말풍선 안. 폭을 채우되 넘지 않는다 */
2741
+
2742
+ .media-thumbnail.size-l {
2743
+ width: 100%;
2744
+ max-width: var(--media-thumbnail-max-width);
2745
+ aspect-ratio: 4 / 3;
2746
+ }
2747
+
2748
+ /* base-button(border·radius·focus 를 지운다)보다 우선해야 해서 .media-thumbnail 아래에 둔다 */
2749
+
2750
+ .media-thumbnail .media-thumbnail-open {
2751
+ position: relative;
2752
+ width: 100%;
2753
+ height: 100%;
2754
+ padding: 0;
2755
+ overflow: hidden;
2756
+ box-sizing: border-box;
2757
+ border-radius: var(--radius-6);
2758
+ background-color: var(--color-grey-scale-200);
2759
+ transition: transform var(--motion-press-duration) var(--motion-press-ease);
2760
+ }
2761
+
2762
+ :is(.media-thumbnail .media-thumbnail-open):active:not([data-disabled]) {
2763
+ transform: scale(var(--motion-press-scale));
2764
+ }
2765
+
2766
+ [data-invalid]:is(.media-thumbnail .media-thumbnail-open) {
2767
+ border: var(--media-invalid-border) solid var(--color-accent-red-500);
2768
+ }
2769
+
2770
+ :is(.media-thumbnail .media-thumbnail-open):focus-visible {
2771
+ outline: var(--hairline) solid var(--color-purple-600);
2772
+ outline-offset: var(--spacing-2);
2773
+ }
2774
+
2775
+ .media-thumbnail .media-thumbnail-remove {
2776
+ position: absolute;
2777
+ top: var(--spacing-4);
2778
+ right: var(--spacing-4);
2779
+ width: var(--media-remove-size);
2780
+ height: var(--media-remove-size);
2781
+ padding: 0;
2782
+ border-radius: var(--radius-full);
2783
+ background-color: color-mix(in srgb, var(--color-grey-scale-900) 80%, transparent);
2784
+ color: var(--color-white);
2785
+
2786
+ /* 20 원을 터치 44 로 넓힌다 */
2787
+ }
2788
+
2789
+ :is(.media-thumbnail .media-thumbnail-remove)::after {
2790
+ content: '';
2791
+ position: absolute;
2792
+ top: 50%;
2793
+ left: 50%;
2794
+ width: var(--touch-target);
2795
+ height: var(--touch-target);
2796
+ transform: translate(-50%, -50%);
2797
+ }
2798
+
2799
+ :is(.media-thumbnail .media-thumbnail-remove):focus-visible {
2800
+ outline: var(--hairline) solid var(--color-purple-600);
2801
+ outline-offset: var(--spacing-2);
2802
+ }
2803
+
2804
+ .media-thumbnail-media {
2805
+ display: block;
2806
+ width: 100%;
2807
+ height: 100%;
2808
+ -o-object-fit: cover;
2809
+ object-fit: cover;
2810
+ }
2811
+
2812
+ .media-thumbnail-play {
2813
+ position: absolute;
2814
+ top: 50%;
2815
+ left: 50%;
2816
+ display: flex;
2817
+ align-items: center;
2818
+ justify-content: center;
2819
+ width: var(--media-overlay-button-size);
2820
+ height: var(--media-overlay-button-size);
2821
+ border-radius: var(--radius-full);
2822
+ background-color: color-mix(in srgb, var(--color-black) 50%, transparent);
2823
+ color: var(--color-white);
2824
+ transform: translate(-50%, -50%);
2825
+ pointer-events: none;
2826
+ }
2827
+
2828
+ .media-thumbnail-loading {
2829
+ position: absolute;
2830
+ inset: 0;
2831
+ display: flex;
2832
+ align-items: center;
2833
+ justify-content: center;
2834
+ background-color: color-mix(in srgb, var(--color-white) 60%, transparent);
2835
+ }
2836
+
2837
+ /* 흰 막 위라 기본 링 색(grey-200)은 안 보인다 */
2838
+
2839
+ .media-thumbnail-spinner {
2840
+ color: var(--color-grey-scale-600);
2841
+ }
2842
+
2843
+ .base-message-composer:where(.root) {
2844
+ display: flex;
2845
+ flex-direction: column;
2846
+ }
2847
+
2848
+ .base-message-composer:where(.textarea) {
2849
+ box-sizing: border-box;
2850
+ margin: 0;
2851
+ font-family: inherit;
2852
+ resize: none;
2853
+ }
2854
+
2855
+ .base-message-composer[data-disabled]:where(.textarea) {
2856
+ cursor: var(--cursor-disabled);
2857
+ }
2858
+
2859
+ .base-message-composer:where(.send) {
2860
+ display: grid;
2861
+ place-items: center;
2862
+ flex: none;
2863
+ padding: 0;
2864
+ border: none;
2865
+ background: transparent;
2866
+ cursor: pointer;
2867
+ -webkit-tap-highlight-color: transparent;
2868
+ }
2869
+
2870
+ /***************************************************************************************************
2871
+ * *
2872
+ * message-composer *
2873
+ * *
2874
+ ***************************************************************************************************/
2875
+
2876
+ .message-composer {
2877
+ gap: var(--spacing-8);
2878
+ }
2879
+
2880
+ .message-composer-attachments {
2881
+ display: flex;
2882
+ gap: var(--spacing-8);
2883
+ overflow-x: auto;
2884
+ }
2885
+
2886
+ /* 입력 칸이 늘어나도 아이콘·보내기 버튼은 아래 줄에 붙는다 */
2887
+
2888
+ .message-composer-row {
2889
+ display: flex;
2890
+ align-items: flex-end;
2891
+ gap: var(--spacing-4);
2892
+ }
2893
+
2894
+ /* 16px 글자 — iOS 가 입력 칸에 포커스할 때 화면을 확대하지 않는다. 높이는 글자만큼 늘고 max-height 에서 스크롤한다 */
2895
+
2896
+ .message-composer-textarea {
2897
+ flex: 1;
2898
+ min-width: 0;
2899
+ min-height: var(--touch-target);
2900
+ max-height: var(--composer-max-height);
2901
+ field-sizing: content;
2902
+ padding: var(--spacing-10) var(--spacing-16);
2903
+ border: var(--hairline) solid var(--color-grey-scale-300);
2904
+ border-radius: var(--radius-20);
2905
+ outline: none;
2906
+ background-color: var(--color-white);
2907
+ color: var(--color-grey-scale-900);
2908
+ font-size: var(--font-size-base);
2909
+ line-height: var(--line-height-base);
2910
+ }
2911
+
2912
+ .message-composer-textarea::-moz-placeholder {
2913
+ color: var(--color-grey-scale-400);
2914
+ }
2915
+
2916
+ .message-composer-textarea::placeholder {
2917
+ color: var(--color-grey-scale-400);
2918
+ }
2919
+
2920
+ .message-composer-textarea:focus {
2921
+ border-color: var(--color-purple-900);
2922
+ }
2923
+
2924
+ .message-composer-textarea[data-disabled] {
2925
+ background-color: var(--color-grey-scale-100);
2926
+ }
2927
+
2928
+ /* 누르는 곳은 --touch-target, 보이는 원은 --composer-send-size */
2929
+
2930
+ .message-composer-send {
2931
+ width: var(--touch-target);
2932
+ height: var(--touch-target);
2933
+ transition: transform var(--motion-press-duration) var(--motion-press-ease);
2934
+ }
2935
+
2936
+ .message-composer-send:not([aria-disabled='true'],[aria-busy='true']):active {
2937
+ transform: scale(var(--motion-press-scale));
2938
+ }
2939
+
2940
+ .message-composer-send[aria-disabled='true'] .message-composer-send-circle {
2941
+ background-color: var(--color-grey-scale-200);
2942
+ }
2943
+
2944
+ .message-composer-send[data-rejecting] .message-composer-send-circle {
2945
+ animation: motion-reject-wiggle var(--motion-reject-duration) var(--motion-reject-ease);
2946
+ }
2947
+
2948
+ .message-composer-send-circle {
2949
+ display: grid;
2950
+ place-items: center;
2951
+ width: var(--composer-send-size);
2952
+ height: var(--composer-send-size);
2953
+ border-radius: var(--radius-full);
2954
+ background-color: var(--color-purple-900);
2955
+ color: var(--color-white);
2956
+ }
2957
+
2958
+ .message-composer-spinner {
2959
+ color: var(--color-white);
2960
+ }
2961
+
2962
+ .base-message:where(.root) {
2963
+ display: flex;
2964
+ flex-direction: column;
2965
+ margin: 0;
2966
+ padding: 0;
2967
+ list-style: none;
2968
+ }
2969
+
2970
+ .base-message:where(.item) {
2971
+ display: flex;
2972
+ flex-direction: column;
2973
+ }
2974
+
2975
+ .base-message:where(.bubble) {
2976
+ display: flex;
2977
+ flex-direction: column;
2978
+ min-width: 0;
2979
+ }
2980
+
2981
+ /***************************************************************************************************
2982
+ * *
2983
+ * message-thread · message · message-event *
2984
+ * *
2985
+ ***************************************************************************************************/
2986
+
2987
+ .message-thread {
2988
+ gap: var(--spacing-12);
2989
+ }
2990
+
2991
+ .message {
2992
+ gap: var(--spacing-4);
2993
+ align-items: flex-start;
2994
+ }
2995
+
2996
+ .message.side-end {
2997
+ align-items: flex-end;
2998
+ }
2127
2999
 
2128
- .page-loading-indicator {
2129
- --spinner-stroke: var(--overlay-spinner-stroke);
2130
- }
3000
+ .message.side-end .message-row {
3001
+ flex-direction: row-reverse;
3002
+ }
2131
3003
 
2132
- .page-loading-indicator .base-spinner,.page-loading-indicator .loading-indicator-ring {
2133
- width: var(--overlay-spinner-size);
2134
- height: var(--overlay-spinner-size);
3004
+ .message.side-end .message-bubble {
3005
+ border-radius: var(--radius-16) var(--radius-16) var(--radius-4) var(--radius-16);
3006
+ background-color: var(--color-purple-900);
3007
+ color: var(--color-white);
2135
3008
  }
2136
3009
 
2137
- .page-loading-text {
2138
- position: relative;
2139
- }
3010
+ /* 방금 보내거나 받은 메시지만 움직인다. 처음 그린 대화는 motion 이 없어 가만히 있다 */
2140
3011
 
2141
- .base-spinner {
2142
- display: flex;
2143
- width: var(--icon-size-m);
2144
- height: var(--icon-size-m);
2145
- align-items: center;
2146
- justify-content: center;
2147
- }
3012
+ .message:is(.motion-weak,.motion-strong) {
3013
+ --motion-slide-offset: translateY(var(--motion-enter-offset));
3014
+ }
2148
3015
 
2149
- /* 우선순위 0 이라 넘겨받은 className 이나 style 의 color 가 늘 이긴다. */
3016
+ .message.motion-weak {
3017
+ animation:
3018
+ motion-enter-fade var(--motion-enter-duration) var(--motion-enter-ease),
3019
+ motion-enter-slide var(--motion-enter-duration) var(--motion-enter-ease);
3020
+ }
2150
3021
 
2151
- :where(.base-spinner) {
2152
- color: var(--color-grey-scale-200);
2153
- }
3022
+ .message.motion-strong {
3023
+ animation:
3024
+ motion-enter-fade var(--motion-enter-strong-duration) var(--motion-enter-strong-ease),
3025
+ motion-enter-slide var(--motion-enter-strong-duration) var(--motion-enter-strong-ease);
3026
+ }
2154
3027
 
2155
- .loading-indicator-ring {
2156
- position: relative;
2157
- width: var(--icon-size-m);
2158
- height: var(--icon-size-m);
3028
+ .message-name {
3029
+ font-size: var(--font-size-xsm);
3030
+ line-height: var(--line-height-xsm);
3031
+ font-weight: var(--font-weight-semibold);
3032
+ color: var(--color-grey-scale-700);
2159
3033
  }
2160
3034
 
2161
- .loading-indicator-single-ring {
2162
- box-sizing: border-box;
2163
- display: block;
2164
- position: absolute;
2165
- width: 100%;
2166
- height: 100%;
2167
- border: var(--spinner-stroke) solid;
2168
- border-radius: 50%;
2169
- animation: lds-ring var(--spinner-duration) var(--spinner-ease) infinite;
2170
- border-color: currentColor transparent transparent transparent;
3035
+ .message-row {
3036
+ display: flex;
3037
+ align-items: flex-end;
3038
+ gap: var(--spacing-6);
3039
+ max-width: 80%;
2171
3040
  }
2172
3041
 
2173
- .loading-indicator-single-ring:nth-child(1) {
2174
- animation-delay: calc(var(--spinner-duration) * -0.375);
2175
- }
3042
+ /* 보낸 쪽 아래 모서리만 덜 둥글다 */
2176
3043
 
2177
- .loading-indicator-single-ring:nth-child(2) {
2178
- animation-delay: calc(var(--spinner-duration) * -0.25);
3044
+ .message-bubble {
3045
+ gap: var(--spacing-6);
3046
+ padding: var(--spacing-10) var(--spacing-14);
3047
+ border-radius: var(--radius-16) var(--radius-16) var(--radius-16) var(--radius-4);
3048
+ background-color: var(--color-grey-scale-100);
3049
+ color: var(--color-grey-scale-900);
3050
+ font-size: var(--font-size-base);
3051
+ line-height: var(--line-height-base);
3052
+ white-space: pre-wrap;
3053
+ overflow-wrap: anywhere;
2179
3054
  }
2180
3055
 
2181
- .loading-indicator-single-ring:nth-child(3) {
2182
- animation-delay: calc(var(--spinner-duration) * -0.125);
2183
- }
3056
+ .message-bubble.media-only {
3057
+ padding: var(--spacing-6);
3058
+ }
2184
3059
 
2185
- .loading-indicator-single-ring:nth-child(4) {
2186
- animation-delay: 0s;
3060
+ .message-time {
3061
+ flex: none;
3062
+ font-size: var(--font-size-xsm);
3063
+ line-height: var(--line-height-xsm);
3064
+ color: var(--color-grey-scale-500);
2187
3065
  }
2188
3066
 
2189
- @keyframes lds-ring {
2190
- 0% {
2191
- transform: rotate(0deg);
2192
- }
2193
- 100% {
2194
- transform: rotate(360deg);
2195
- }
3067
+ .message-event {
3068
+ align-self: center;
3069
+ padding: var(--spacing-2) var(--spacing-10);
3070
+ border-radius: var(--radius-full);
3071
+ background-color: var(--color-grey-scale-100);
3072
+ color: var(--color-grey-scale-600);
3073
+ font-size: var(--font-size-xsm);
3074
+ line-height: var(--line-height-xsm);
3075
+ font-weight: var(--font-weight-medium);
2196
3076
  }
2197
3077
 
2198
3078
  /* Title·Close 같은 조각도 base-modal 을 달므로 상자 스타일은 container·overlay 에만 건다. */
@@ -2246,6 +3126,12 @@ button.modal-with-double-button-close-button {
2246
3126
  color: var(--color-grey-scale-600);
2247
3127
  }
2248
3128
 
3129
+ .modal-with-double-button-container {
3130
+ /* 폰 폭(390)에서 고정 폭이 화면을 넘지 않게 Modal 과 같이 양옆을 남긴다 */
3131
+ max-width: calc(100% - var(--spacing-30) * 2);
3132
+ box-sizing: border-box;
3133
+ }
3134
+
2249
3135
  .modal-with-double-button-container.size-s {
2250
3136
  width: var(--modal-width-s);
2251
3137
  }
@@ -2383,6 +3269,87 @@ button.modal-with-double-button-close-button {
2383
3269
  gap: var(--spacing-10);
2384
3270
  }
2385
3271
 
3272
+ /* 카드·시트 안에 머무는 안내. 스스로 사라지는 알림은 Toast 다 */
3273
+
3274
+ .base-notice {
3275
+ display: flex;
3276
+ align-items: flex-start;
3277
+ gap: var(--spacing-8);
3278
+ }
3279
+
3280
+ /***************************************************************************************************
3281
+ * *
3282
+ * notice *
3283
+ * *
3284
+ ***************************************************************************************************/
3285
+
3286
+ /* variant 가 바탕색과 글자·아이콘 색을 정한다 */
3287
+
3288
+ .notice {
3289
+ padding: var(--spacing-12) var(--spacing-14);
3290
+ border-radius: var(--radius-6);
3291
+ background-color: var(--color-grey-scale-100);
3292
+ color: var(--color-grey-scale-700);
3293
+ font-size: var(--font-size-sm);
3294
+ line-height: var(--line-height-sm);
3295
+ }
3296
+
3297
+ .notice.variant-success {
3298
+ background-color: var(--color-accent-green-50);
3299
+ color: var(--color-accent-green-800);
3300
+ }
3301
+
3302
+ .notice.variant-warning {
3303
+ background-color: var(--color-accent-orange-50);
3304
+ color: var(--color-accent-orange-800);
3305
+ }
3306
+
3307
+ .notice.variant-danger {
3308
+ background-color: var(--color-accent-red-50);
3309
+ color: var(--color-accent-red-700);
3310
+ }
3311
+
3312
+ /* 조건부로 나타나는 안내만 motion 을 준다. 사라질 때는 움직이지 않는다 */
3313
+
3314
+ .notice.motion-weak {
3315
+ animation: motion-enter-fade var(--motion-enter-duration) var(--motion-enter-ease);
3316
+ }
3317
+
3318
+ .notice.motion-strong {
3319
+ animation: motion-enter-fade var(--motion-enter-strong-duration) var(--motion-enter-strong-ease);
3320
+ }
3321
+
3322
+ /* 아이콘(18)을 첫 줄 글(18)에 맞춘다 */
3323
+
3324
+ .notice-icon {
3325
+ flex: none;
3326
+ }
3327
+
3328
+ .notice-body {
3329
+ display: flex;
3330
+ flex: 1;
3331
+ flex-direction: column;
3332
+ gap: var(--spacing-10);
3333
+ min-width: 0;
3334
+ }
3335
+
3336
+ .notice-text {
3337
+ display: flex;
3338
+ flex-direction: column;
3339
+ gap: var(--spacing-2);
3340
+ word-break: break-word;
3341
+ }
3342
+
3343
+ .notice-title {
3344
+ font-weight: var(--font-weight-semibold);
3345
+ }
3346
+
3347
+ /* action 은 CTAButton(xs·neutral) 이라 모양은 버튼 CSS 가 정한다. 폭은 글자만큼 */
3348
+
3349
+ .notice-action {
3350
+ align-self: flex-start;
3351
+ }
3352
+
2386
3353
  .base-pagination:where(.item) {
2387
3354
  display: inline-flex;
2388
3355
  align-items: center;
@@ -2485,6 +3452,20 @@ button.modal-with-double-button-close-button {
2485
3452
  padding: 0;
2486
3453
  }
2487
3454
 
3455
+ .base-radio:where(.chip) {
3456
+ position: relative;
3457
+ display: inline-flex;
3458
+ align-items: center;
3459
+
3460
+ /* 36 높이를 터치 44 로 넓힌다 */
3461
+ }
3462
+
3463
+ .base-radio:where(.chip)::after {
3464
+ content: '';
3465
+ position: absolute;
3466
+ inset: calc(-1 * var(--spacing-4)) 0;
3467
+ }
3468
+
2488
3469
  /***************************************************************************************************
2489
3470
  * *
2490
3471
  * radio *
@@ -2542,6 +3523,54 @@ button.modal-with-double-button-close-button {
2542
3523
  background-color: var(--color-grey-scale-400);
2543
3524
  }
2544
3525
 
3526
+ /* chip: 줄을 바꿔 가로로 늘어놓는다 */
3527
+
3528
+ .radio.radio-group.variant-chip {
3529
+ flex-direction: row;
3530
+ flex-wrap: wrap;
3531
+ gap: var(--spacing-8);
3532
+ }
3533
+
3534
+ /* 모양은 ChipGroup 칩과 같다. .base-button 의 border·radius·focus 초기화를 이기려고 .radio.radio-chip 으로 건다 */
3535
+
3536
+ .radio.radio-chip {
3537
+ height: var(--control-height-s);
3538
+ padding: 0 var(--spacing-14);
3539
+ border: var(--hairline) solid var(--color-grey-scale-300);
3540
+ border-radius: var(--radius-full);
3541
+ background-color: var(--color-grey-scale-50);
3542
+ color: var(--color-grey-scale-600);
3543
+ font-size: var(--font-size-sm);
3544
+ font-weight: var(--font-weight-medium);
3545
+ transition:
3546
+ transform var(--motion-press-duration) var(--motion-press-ease),
3547
+ background-color var(--motion-confirm-weak-duration) var(--motion-confirm-weak-ease),
3548
+ color var(--motion-confirm-weak-duration) var(--motion-confirm-weak-ease),
3549
+ border-color var(--motion-confirm-weak-duration) var(--motion-confirm-weak-ease);
3550
+ }
3551
+
3552
+ .radio.radio-chip:active:not([data-disabled]) {
3553
+ transform: scale(var(--motion-press-scale));
3554
+ }
3555
+
3556
+ .radio.radio-chip[data-state='checked'] {
3557
+ border-color: var(--color-purple-900);
3558
+ background-color: var(--color-purple-900);
3559
+ color: var(--color-grey-scale-50);
3560
+ font-weight: var(--font-weight-semibold);
3561
+ }
3562
+
3563
+ .radio.radio-chip[data-disabled] {
3564
+ border-color: var(--color-grey-scale-200);
3565
+ color: var(--color-grey-scale-300);
3566
+ cursor: var(--cursor-disabled);
3567
+ }
3568
+
3569
+ .radio.radio-chip:focus-visible {
3570
+ outline: var(--hairline) solid var(--color-purple-600);
3571
+ outline-offset: var(--spacing-2);
3572
+ }
3573
+
2545
3574
  .base-skeleton {
2546
3575
  display: block;
2547
3576
  box-sizing: border-box;
@@ -2604,7 +3633,7 @@ button.modal-with-double-button-close-button {
2604
3633
  transparent 100%
2605
3634
  );
2606
3635
  transform: translateX(-100%);
2607
- animation: skeleton-shimmer var(--skeleton-duration) var(--motion-ease) infinite;
3636
+ animation: motion-busy-shimmer var(--skeleton-duration) var(--motion-ease) infinite;
2608
3637
  }
2609
3638
 
2610
3639
  @media (prefers-reduced-motion: reduce) {
@@ -2614,15 +3643,6 @@ button.modal-with-double-button-close-button {
2614
3643
  }
2615
3644
  }
2616
3645
 
2617
- @keyframes skeleton-shimmer {
2618
- 0% {
2619
- transform: translateX(-100%);
2620
- }
2621
- 100% {
2622
- transform: translateX(100%);
2623
- }
2624
- }
2625
-
2626
3646
  .base-step-progress:where(.root) {
2627
3647
  display: grid;
2628
3648
  grid-template-columns: repeat(var(--step-count), minmax(0, 1fr));
@@ -2951,6 +3971,77 @@ button.modal-with-double-button-close-button {
2951
3971
  z-index: var(--z-base);
2952
3972
  }
2953
3973
 
3974
+ .base-tag {
3975
+ display: inline-flex;
3976
+ align-items: center;
3977
+
3978
+ /* 지우기 버튼. 글자 옆 작은 X 지만 누르는 자리는 터치 44 로 넓힌다 */
3979
+ }
3980
+
3981
+ .base-tag:where(.remove) {
3982
+ position: relative;
3983
+ flex: none;
3984
+ display: inline-flex;
3985
+ align-items: center;
3986
+ justify-content: center;
3987
+ padding: 0;
3988
+ border: none;
3989
+ background: transparent;
3990
+ cursor: var(--cursor-button);
3991
+ }
3992
+
3993
+ .base-tag:where(.remove)::after {
3994
+ content: '';
3995
+ position: absolute;
3996
+ top: 50%;
3997
+ left: 50%;
3998
+ width: var(--touch-target);
3999
+ height: var(--touch-target);
4000
+ transform: translate(-50%, -50%);
4001
+ }
4002
+
4003
+ /***************************************************************************************************
4004
+ * *
4005
+ * tag *
4006
+ * *
4007
+ ***************************************************************************************************/
4008
+
4009
+ /* CTAButton secondary·Callout 과 같은 연보라 */
4010
+
4011
+ .tag {
4012
+ height: var(--label-height-s);
4013
+ padding: 0 var(--spacing-12);
4014
+ border-radius: var(--radius-full);
4015
+ background-color: var(--color-purple-50);
4016
+ color: var(--color-purple-900);
4017
+ font-size: var(--font-size-sm);
4018
+ line-height: var(--line-height-sm);
4019
+ font-weight: var(--font-weight-semibold);
4020
+ white-space: nowrap;
4021
+
4022
+ /* X 버튼 안에 여백이 있어 오른쪽을 줄인다 */
4023
+ }
4024
+
4025
+ .tag.removable {
4026
+ padding-right: var(--spacing-4);
4027
+ }
4028
+
4029
+ .tag-remove {
4030
+ width: var(--label-height-xs);
4031
+ height: var(--label-height-xs);
4032
+ border-radius: var(--radius-full);
4033
+ color: var(--color-purple-600);
4034
+ transition: transform var(--motion-press-duration) var(--motion-press-ease);
4035
+ }
4036
+
4037
+ .tag-remove:active {
4038
+ transform: scale(var(--motion-press-scale));
4039
+ }
4040
+
4041
+ .tag-remove:focus-visible {
4042
+ outline: var(--hairline) solid var(--color-purple-600);
4043
+ }
4044
+
2954
4045
  .base-text-area:where(.textarea) {
2955
4046
  display: block;
2956
4047
  width: 100%;
@@ -3156,11 +4247,13 @@ button.modal-with-double-button-close-button {
3156
4247
  }
3157
4248
 
3158
4249
  .base-toast[data-state='open']:where(:not(.viewport,.title,.description,.action,.close)) {
3159
- animation: toast-in var(--motion-base) var(--motion-ease);
4250
+ animation:
4251
+ motion-enter-slide var(--motion-base) var(--motion-ease),
4252
+ motion-enter-fade var(--motion-base) var(--motion-ease);
3160
4253
  }
3161
4254
 
3162
4255
  .base-toast[data-state='closed']:where(:not(.viewport,.title,.description,.action,.close)) {
3163
- animation: toast-out var(--motion-fast) var(--motion-ease) forwards;
4256
+ animation: motion-exit-fade var(--motion-fast) var(--motion-ease) forwards;
3164
4257
  }
3165
4258
 
3166
4259
  .base-toast[data-swipe='move']:where(:not(.viewport,.title,.description,.action,.close)) {
@@ -3173,7 +4266,9 @@ button.modal-with-double-button-close-button {
3173
4266
  }
3174
4267
 
3175
4268
  .base-toast[data-swipe='end']:where(:not(.viewport,.title,.description,.action,.close)) {
3176
- animation: toast-swipe-out var(--motion-fast) var(--motion-ease) forwards;
4269
+ --motion-swipe-from: var(--radix-toast-swipe-end-x);
4270
+ --motion-swipe-to: calc(100% + var(--spacing-24));
4271
+ animation: motion-exit-swipe var(--motion-fast) var(--motion-ease) forwards;
3177
4272
  }
3178
4273
 
3179
4274
  .base-toast:where(.viewport) {
@@ -3189,13 +4284,13 @@ button.modal-with-double-button-close-button {
3189
4284
  list-style: none;
3190
4285
  outline: none;
3191
4286
  /* 아래에서 올라오는 자리를 기본으로 두고 position 클래스가 덮는다 */
3192
- --toast-enter: translateY(var(--spacing-16));
4287
+ --motion-slide-offset: translateY(var(--spacing-16));
3193
4288
  }
3194
4289
 
3195
4290
  .base-toast.position-top-right:where(.viewport) {
3196
4291
  top: 0;
3197
4292
  right: 0;
3198
- --toast-enter: translateY(calc(-1 * var(--spacing-16)));
4293
+ --motion-slide-offset: translateY(calc(-1 * var(--spacing-16)));
3199
4294
  }
3200
4295
 
3201
4296
  .base-toast.position-bottom-right:where(.viewport) {
@@ -3221,38 +4316,6 @@ button.modal-with-double-button-close-button {
3221
4316
  cursor: var(--cursor-button);
3222
4317
  }
3223
4318
 
3224
- @keyframes toast-in {
3225
- from {
3226
- opacity: 0;
3227
- transform: var(--toast-enter);
3228
- }
3229
-
3230
- to {
3231
- opacity: 1;
3232
- transform: none;
3233
- }
3234
- }
3235
-
3236
- @keyframes toast-out {
3237
- from {
3238
- opacity: 1;
3239
- }
3240
-
3241
- to {
3242
- opacity: 0;
3243
- }
3244
- }
3245
-
3246
- @keyframes toast-swipe-out {
3247
- from {
3248
- transform: translateX(var(--radix-toast-swipe-end-x));
3249
- }
3250
-
3251
- to {
3252
- transform: translateX(calc(100% + var(--spacing-24)));
3253
- }
3254
- }
3255
-
3256
4319
  @media (prefers-reduced-motion: reduce) {
3257
4320
  .base-toast:where(:not(.viewport, .title, .description, .action, .close)) {
3258
4321
  animation: none;