@nurihaus/web-design-system 3.7.0 → 3.9.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 (143) hide show
  1. package/README.md +15 -4
  2. package/dist/ai/components.md +658 -12
  3. package/dist/ai/motion.md +431 -0
  4. package/dist/ai/tokens.md +93 -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-1176.js +24 -0
  14. package/dist/chunk-3274.js +71 -0
  15. package/dist/chunk-345.js +72 -0
  16. package/dist/chunk-3547.js +56 -0
  17. package/dist/chunk-6459.js +107 -0
  18. package/dist/{chunk-6364.js → chunk-7335.js} +2 -67
  19. package/dist/chunk-7653.js +4 -0
  20. package/dist/chunk-910.js +26 -13
  21. package/dist/chunk-9497.js +66 -0
  22. package/dist/collapsible-menu.js +6 -4
  23. package/dist/components/badge/badge-props.d.ts +4 -4
  24. package/dist/components/badge/badge.stories.d.ts +1 -1
  25. package/dist/components/bottom-bar/base-bottom-bar.d.ts +4 -0
  26. package/dist/components/bottom-bar/bottom-bar-props.d.ts +1 -0
  27. package/dist/components/bottom-bar/bottom-bar-type.d.ts +3 -0
  28. package/dist/components/bottom-bar/bottom-bar.d.ts +6 -0
  29. package/dist/components/bottom-bar/bottom-bar.stories.d.ts +282 -0
  30. package/dist/components/button/button-type.d.ts +3 -1
  31. package/dist/components/button/button.stories.d.ts +1 -0
  32. package/dist/components/card/base-card.d.ts +3 -0
  33. package/dist/components/card/card-props.d.ts +9 -0
  34. package/dist/components/card/card-type.d.ts +8 -0
  35. package/dist/components/card/card.d.ts +5 -0
  36. package/dist/components/card/card.stories.d.ts +24 -0
  37. package/dist/components/description-list/base-description-list.d.ts +4 -0
  38. package/dist/components/description-list/description-list-props.d.ts +1 -0
  39. package/dist/components/description-list/description-list-type.d.ts +13 -0
  40. package/dist/components/description-list/description-list.d.ts +5 -0
  41. package/dist/components/description-list/description-list.stories.d.ts +20 -0
  42. package/dist/components/empty-state/base-empty-state.d.ts +4 -0
  43. package/dist/components/empty-state/empty-state-props.d.ts +10 -0
  44. package/dist/components/empty-state/empty-state-type.d.ts +14 -0
  45. package/dist/components/empty-state/empty-state.d.ts +5 -0
  46. package/dist/components/empty-state/empty-state.stories.d.ts +300 -0
  47. package/dist/components/icon/icon-paths.d.ts +4 -0
  48. package/dist/components/icon/icon.stories.d.ts +1 -1
  49. package/dist/components/icon-button/base-icon-button.d.ts +3 -0
  50. package/dist/components/icon-button/icon-button-props.d.ts +12 -0
  51. package/dist/components/icon-button/icon-button-type.d.ts +16 -0
  52. package/dist/components/icon-button/icon-button.d.ts +5 -0
  53. package/dist/components/icon-button/icon-button.stories.d.ts +31 -0
  54. package/dist/components/list/base-list.d.ts +7 -0
  55. package/dist/components/list/list-group.d.ts +5 -0
  56. package/dist/components/list/list-group.stories.d.ts +14 -0
  57. package/dist/components/list/list-props.d.ts +2 -0
  58. package/dist/components/list/list-row.d.ts +5 -0
  59. package/dist/components/list/list-row.stories.d.ts +304 -0
  60. package/dist/components/list/list-type.d.ts +25 -0
  61. package/dist/components/media/base-media.d.ts +16 -0
  62. package/dist/components/media/media-props.d.ts +5 -0
  63. package/dist/components/media/media-thumbnail.d.ts +4 -0
  64. package/dist/components/media/media-thumbnail.stories.d.ts +34 -0
  65. package/dist/components/media/media-type.d.ts +43 -0
  66. package/dist/components/media/media-viewer.d.ts +4 -0
  67. package/dist/components/media/media-viewer.stories.d.ts +24 -0
  68. package/dist/components/message/base-message.d.ts +11 -0
  69. package/dist/components/message/message-event.d.ts +6 -0
  70. package/dist/components/message/message-props.d.ts +17 -0
  71. package/dist/components/message/message-thread.d.ts +6 -0
  72. package/dist/components/message/message-type.d.ts +21 -0
  73. package/dist/components/message/message.d.ts +5 -0
  74. package/dist/components/message/message.stories.d.ts +313 -0
  75. package/dist/components/message-composer/base-message-composer.d.ts +11 -0
  76. package/dist/components/message-composer/message-composer-props.d.ts +1 -0
  77. package/dist/components/message-composer/message-composer-type.d.ts +31 -0
  78. package/dist/components/message-composer/message-composer.d.ts +5 -0
  79. package/dist/components/message-composer/message-composer.stories.d.ts +46 -0
  80. package/dist/components/motion/motion-provider.d.ts +5 -0
  81. package/dist/components/motion/motion-type.d.ts +6 -0
  82. package/dist/components/motion/motion.d.ts +2 -0
  83. package/dist/components/motion/motion.stories.d.ts +23 -0
  84. package/dist/components/notice/base-notice.d.ts +4 -0
  85. package/dist/components/notice/notice-props.d.ts +16 -0
  86. package/dist/components/notice/notice-type.d.ts +21 -0
  87. package/dist/components/notice/notice.d.ts +5 -0
  88. package/dist/components/notice/notice.stories.d.ts +304 -0
  89. package/dist/components/radio/base-radio.d.ts +2 -1
  90. package/dist/components/radio/radio-props.d.ts +9 -1
  91. package/dist/components/radio/radio-type.d.ts +9 -0
  92. package/dist/components/radio/radio.stories.d.ts +10 -0
  93. package/dist/components/tag/base-tag.d.ts +8 -0
  94. package/dist/components/tag/tag-props.d.ts +1 -0
  95. package/dist/components/tag/tag-type.d.ts +20 -0
  96. package/dist/components/tag/tag.d.ts +4 -0
  97. package/dist/components/tag/tag.stories.d.ts +14 -0
  98. package/dist/components.css +1330 -171
  99. package/dist/cta-button.js +38 -21
  100. package/dist/data-table.js +5 -3
  101. package/dist/description-list.js +274 -0
  102. package/dist/drawer.js +7 -5
  103. package/dist/drop-down.js +8 -6
  104. package/dist/empty-state.js +300 -0
  105. package/dist/helpers/utils/fillLinkChild.d.ts +6 -0
  106. package/dist/icon-button.js +338 -0
  107. package/dist/icon-text-badge.js +11 -9
  108. package/dist/icon-text-option-badge.js +8 -6
  109. package/dist/icon.js +7 -4
  110. package/dist/index.d.ts +30 -2
  111. package/dist/index.js +2482 -223
  112. package/dist/list-group.js +253 -0
  113. package/dist/list-row.js +308 -0
  114. package/dist/loading.js +6 -4
  115. package/dist/media-thumbnail.js +412 -0
  116. package/dist/media-viewer.js +274 -0
  117. package/dist/message-composer.js +399 -0
  118. package/dist/message-event.js +216 -0
  119. package/dist/message-thread.js +216 -0
  120. package/dist/message.js +271 -0
  121. package/dist/modal-with-double-button.js +15 -11
  122. package/dist/modal.js +9 -7
  123. package/dist/motion.js +52 -0
  124. package/dist/notice.js +346 -0
  125. package/dist/option-badge.js +11 -9
  126. package/dist/pagination.js +8 -6
  127. package/dist/radio.js +114 -30
  128. package/dist/skeleton.js +4 -2
  129. package/dist/step-progress.js +4 -2
  130. package/dist/styles.css +1215 -130
  131. package/dist/switch.js +6 -4
  132. package/dist/tabs.js +5 -3
  133. package/dist/tag.js +311 -0
  134. package/dist/text-area.js +8 -6
  135. package/dist/text-field.js +8 -6
  136. package/dist/text.js +5 -3
  137. package/dist/toast.js +15 -11
  138. package/dist/tokens/generated/index.d.ts +1 -0
  139. package/dist/tokens/generated/motion.d.ts +153 -0
  140. package/dist/tokens.css +122 -2
  141. package/dist/tokens.js +156 -1
  142. package/dist/tooltip.js +6 -4
  143. package/package.json +67 -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,232 @@
2089
2418
  height: var(--icon-size-xl);
2090
2419
  }
2091
2420
 
2421
+ .base-icon-button {
2422
+ position: relative;
2423
+ flex: none;
2424
+ display: inline-flex;
2425
+ align-items: center;
2426
+ justify-content: center;
2427
+ padding: 0;
2428
+ border: none;
2429
+ cursor: var(--cursor-button);
2430
+
2431
+ /* 보이는 크기가 작아도(s 36) 누르는 자리는 터치 44 */
2432
+ }
2433
+
2434
+ .base-icon-button::after {
2435
+ content: '';
2436
+ position: absolute;
2437
+ top: 50%;
2438
+ left: 50%;
2439
+ width: var(--touch-target);
2440
+ height: var(--touch-target);
2441
+ transform: translate(-50%, -50%);
2442
+ }
2443
+
2444
+ .base-icon-button[data-disabled] {
2445
+ cursor: var(--cursor-disabled);
2446
+ }
2447
+
2448
+ /***************************************************************************************************
2449
+ * *
2450
+ * icon-button *
2451
+ * *
2452
+ ***************************************************************************************************/
2453
+
2454
+ .icon-button {
2455
+ border-radius: var(--radius-full);
2456
+ transition:
2457
+ transform var(--motion-press-duration) var(--motion-press-ease),
2458
+ background-color var(--motion-press-duration) var(--motion-press-ease);
2459
+ }
2460
+
2461
+ .icon-button:active:not([data-disabled]) {
2462
+ transform: scale(var(--motion-press-scale));
2463
+ }
2464
+
2465
+ .icon-button:focus-visible {
2466
+ outline: var(--hairline) solid var(--color-purple-600);
2467
+ outline-offset: var(--spacing-2);
2468
+ }
2469
+
2470
+ .icon-button.size-s {
2471
+ width: var(--icon-button-size-s);
2472
+ height: var(--icon-button-size-s);
2473
+ }
2474
+
2475
+ .icon-button.size-m {
2476
+ width: var(--icon-button-size-m);
2477
+ height: var(--icon-button-size-m);
2478
+ }
2479
+
2480
+ .icon-button.size-l {
2481
+ width: var(--icon-button-size-l);
2482
+ height: var(--icon-button-size-l);
2483
+ }
2484
+
2485
+ /* 바탕 없이 아이콘만. 누르는 동안 회색 원이 보인다 */
2486
+
2487
+ .icon-button.variant-ghost {
2488
+ background-color: transparent;
2489
+ color: var(--color-grey-scale-700);
2490
+ }
2491
+
2492
+ .icon-button.variant-ghost:active:not([data-disabled]) {
2493
+ background-color: var(--color-grey-scale-100);
2494
+ }
2495
+
2496
+ /* CTAButton secondary 와 같은 연보라 */
2497
+
2498
+ .icon-button.variant-secondary {
2499
+ background-color: var(--color-purple-50);
2500
+ color: var(--color-purple-900);
2501
+ }
2502
+
2503
+ .icon-button.variant-primary {
2504
+ background-color: var(--color-purple-900);
2505
+ color: var(--color-white);
2506
+ }
2507
+
2508
+ .icon-button[data-disabled] {
2509
+ color: var(--color-grey-scale-300);
2510
+ }
2511
+
2512
+ .icon-button[data-disabled]:is(.variant-secondary,.variant-primary) {
2513
+ background-color: var(--color-grey-scale-200);
2514
+ color: var(--color-white);
2515
+ }
2516
+
2517
+ .base-list:where(.group) {
2518
+ display: flex;
2519
+ flex-direction: column;
2520
+ }
2521
+
2522
+ /* 행은 div·button·링크 어느 것이든 같은 모양이다 */
2523
+
2524
+ .base-list:where(.row) {
2525
+ position: relative;
2526
+ display: flex;
2527
+ align-items: center;
2528
+ box-sizing: border-box;
2529
+ width: 100%;
2530
+ border: 0;
2531
+ background-color: transparent;
2532
+ color: inherit;
2533
+ text-align: start;
2534
+ text-decoration: none;
2535
+ }
2536
+
2537
+ /***************************************************************************************************
2538
+ * *
2539
+ * list-group *
2540
+ * *
2541
+ ***************************************************************************************************/
2542
+
2543
+ .list-group {
2544
+ gap: var(--spacing-8);
2545
+ }
2546
+
2547
+ .list-group-label {
2548
+ padding: 0 var(--spacing-4);
2549
+ color: var(--color-grey-scale-500);
2550
+ font-size: var(--font-size-sm);
2551
+ line-height: var(--line-height-sm);
2552
+ font-weight: var(--font-weight-semibold);
2553
+ }
2554
+
2555
+ .list-group-surface {
2556
+ display: flex;
2557
+ flex-direction: column;
2558
+ overflow: hidden;
2559
+ border: var(--hairline) solid var(--color-grey-scale-200);
2560
+ border-radius: var(--radius-16);
2561
+ background-color: var(--color-white);
2562
+ }
2563
+
2564
+ /***************************************************************************************************
2565
+ * *
2566
+ * list-row *
2567
+ * *
2568
+ ***************************************************************************************************/
2569
+
2570
+ .list-row {
2571
+ min-height: var(--list-row-height);
2572
+ gap: var(--spacing-12);
2573
+ padding: var(--spacing-10) var(--spacing-16);
2574
+
2575
+ /* 누르는 행(링크·버튼)은 줄어들지 않고 배경이 어두워진다. 폭이 꽉 찬 면이라 scale 이 어색하다 */
2576
+ }
2577
+
2578
+ .list-row:is(a[href],button) {
2579
+ cursor: pointer;
2580
+ transition: background-color var(--motion-press-duration) var(--motion-press-ease);
2581
+ }
2582
+
2583
+ .list-row:is(a[href],button):active {
2584
+ background-color: var(--color-grey-scale-100);
2585
+ }
2586
+
2587
+ /* 면이 overflow: hidden 이라 테두리 안쪽에 그린다 */
2588
+
2589
+ .list-row:is(a[href],button):focus-visible {
2590
+ outline: var(--hairline) solid var(--color-purple-600);
2591
+ outline-offset: calc(var(--spacing-2) * -1);
2592
+ }
2593
+
2594
+ /* 행 사이 구분선. 왼쪽은 안쪽 여백만큼 띄운다 */
2595
+
2596
+ .list-row + .list-row::before {
2597
+ content: '';
2598
+ position: absolute;
2599
+ top: 0;
2600
+ right: 0;
2601
+ left: var(--spacing-16);
2602
+ height: var(--hairline);
2603
+ background-color: var(--color-grey-scale-100);
2604
+ }
2605
+
2606
+ .list-row-leading {
2607
+ display: flex;
2608
+ flex: none;
2609
+ color: var(--color-grey-scale-700);
2610
+ }
2611
+
2612
+ .list-row-text {
2613
+ display: flex;
2614
+ flex: 1;
2615
+ flex-direction: column;
2616
+ gap: var(--spacing-2);
2617
+ min-width: 0;
2618
+ }
2619
+
2620
+ .list-row-label {
2621
+ color: var(--color-grey-scale-900);
2622
+ font-size: var(--font-size-base);
2623
+ line-height: var(--line-height-base);
2624
+ font-weight: var(--font-weight-medium);
2625
+ }
2626
+
2627
+ .list-row-description {
2628
+ color: var(--color-grey-scale-500);
2629
+ font-size: var(--font-size-sm);
2630
+ line-height: var(--line-height-sm);
2631
+ }
2632
+
2633
+ .list-row-trailing {
2634
+ display: flex;
2635
+ flex: none;
2636
+ align-items: center;
2637
+ color: var(--color-grey-scale-500);
2638
+ font-size: var(--font-size-sm);
2639
+ line-height: var(--line-height-sm);
2640
+ }
2641
+
2642
+ .list-row-chevron {
2643
+ flex: none;
2644
+ color: var(--color-grey-scale-400);
2645
+ }
2646
+
2092
2647
  .base-inline-loading {
2093
2648
  display: flex;
2094
2649
  width: 100%;
@@ -2123,76 +2678,497 @@
2123
2678
  gap: var(--spacing-16);
2124
2679
  }
2125
2680
 
2126
- /* 스피너를 레이아웃 크기로 키운다. transform: scale 은 보이는 크기만 키워 아래 글자와 겹친다 */
2681
+ /* 스피너를 레이아웃 크기로 키운다. transform: scale 은 보이는 크기만 키워 아래 글자와 겹친다 */
2682
+
2683
+ .page-loading-indicator {
2684
+ --spinner-stroke: var(--overlay-spinner-stroke);
2685
+ }
2686
+
2687
+ .page-loading-indicator .base-spinner,.page-loading-indicator .loading-indicator-ring {
2688
+ width: var(--overlay-spinner-size);
2689
+ height: var(--overlay-spinner-size);
2690
+ }
2691
+
2692
+ .page-loading-text {
2693
+ position: relative;
2694
+ }
2695
+
2696
+ .base-spinner {
2697
+ display: flex;
2698
+ width: var(--icon-size-m);
2699
+ height: var(--icon-size-m);
2700
+ align-items: center;
2701
+ justify-content: center;
2702
+ }
2703
+
2704
+ /* 우선순위 0 이라 넘겨받은 className 이나 style 의 color 가 늘 이긴다. */
2705
+
2706
+ :where(.base-spinner) {
2707
+ color: var(--color-grey-scale-200);
2708
+ }
2709
+
2710
+ .loading-indicator-ring {
2711
+ position: relative;
2712
+ width: var(--icon-size-m);
2713
+ height: var(--icon-size-m);
2714
+ }
2715
+
2716
+ .loading-indicator-single-ring {
2717
+ box-sizing: border-box;
2718
+ display: block;
2719
+ position: absolute;
2720
+ width: 100%;
2721
+ height: 100%;
2722
+ border: var(--spinner-stroke) solid;
2723
+ border-radius: 50%;
2724
+ animation: motion-busy-ring var(--spinner-duration) var(--spinner-ease) infinite;
2725
+ border-color: currentColor transparent transparent transparent;
2726
+ }
2727
+
2728
+ .loading-indicator-single-ring:nth-child(1) {
2729
+ animation-delay: calc(var(--spinner-duration) * -0.375);
2730
+ }
2731
+
2732
+ .loading-indicator-single-ring:nth-child(2) {
2733
+ animation-delay: calc(var(--spinner-duration) * -0.25);
2734
+ }
2735
+
2736
+ .loading-indicator-single-ring:nth-child(3) {
2737
+ animation-delay: calc(var(--spinner-duration) * -0.125);
2738
+ }
2739
+
2740
+ .loading-indicator-single-ring:nth-child(4) {
2741
+ animation-delay: 0s;
2742
+ }
2743
+
2744
+ /* Title·Close 같은 조각도 base-media 를 달므로 상자 스타일은 overlay·content 에만 건다. */
2745
+
2746
+ .base-media:where(.overlay) {
2747
+ position: fixed;
2748
+ inset: 0;
2749
+ z-index: var(--z-overlay);
2750
+ background-color: color-mix(in srgb, var(--color-black) 90%, transparent);
2751
+ }
2752
+
2753
+ .base-media[data-state='open']:where(.overlay) {
2754
+ animation: motion-enter-fade var(--motion-enter-duration) var(--motion-enter-ease);
2755
+ }
2756
+
2757
+ .base-media[data-state='closed']:where(.overlay) {
2758
+ animation: motion-exit-fade var(--motion-exit-duration) var(--motion-exit-ease);
2759
+ }
2760
+
2761
+ .base-media:where(.content) {
2762
+ position: fixed;
2763
+ inset: 0;
2764
+ z-index: var(--z-modal);
2765
+ display: flex;
2766
+ align-items: center;
2767
+ justify-content: center;
2768
+ }
2769
+
2770
+ .base-media:where(.close) {
2771
+ display: inline-flex;
2772
+ align-items: center;
2773
+ justify-content: center;
2774
+ padding: 0;
2775
+ border: none;
2776
+ background-color: transparent;
2777
+ cursor: var(--cursor-button);
2778
+ }
2779
+
2780
+ /***************************************************************************************************
2781
+ * *
2782
+ * media-viewer *
2783
+ * *
2784
+ ***************************************************************************************************/
2785
+
2786
+ /* Radix 는 애니메이션이 끝나야 닫힌 Content 를 지우므로 keyframes 로 들어오고 나간다(styles/motion.css). */
2787
+
2788
+ .media-viewer[data-state='open'] {
2789
+ animation: motion-enter-scale var(--motion-enter-duration) var(--motion-enter-ease);
2790
+ }
2791
+
2792
+ .media-viewer[data-state='closed'] {
2793
+ animation: motion-exit-scale var(--motion-exit-duration) var(--motion-exit-ease);
2794
+ }
2795
+
2796
+ .media-viewer-media {
2797
+ display: block;
2798
+ max-width: 100%;
2799
+ max-height: 100%;
2800
+ -o-object-fit: contain;
2801
+ object-fit: contain;
2802
+ }
2803
+
2804
+ /* 노치 아래로 숨지 않게 안전 영역만큼 내린다 */
2805
+
2806
+ .media-viewer-close {
2807
+ position: absolute;
2808
+ top: calc(env(safe-area-inset-top) + var(--spacing-8));
2809
+ right: calc(env(safe-area-inset-right) + var(--spacing-8));
2810
+ width: var(--touch-target);
2811
+ height: var(--touch-target);
2812
+ color: var(--color-white);
2813
+ }
2814
+
2815
+ /***************************************************************************************************
2816
+ * *
2817
+ * media-thumbnail *
2818
+ * *
2819
+ ***************************************************************************************************/
2820
+
2821
+ .media-thumbnail {
2822
+ position: relative;
2823
+ flex-shrink: 0;
2824
+ }
2825
+
2826
+ .media-thumbnail.size-s {
2827
+ width: var(--media-thumbnail-size-s);
2828
+ height: var(--media-thumbnail-size-s);
2829
+ }
2830
+
2831
+ .media-thumbnail.size-m {
2832
+ width: var(--media-thumbnail-size-m);
2833
+ height: var(--media-thumbnail-size-m);
2834
+ }
2835
+
2836
+ /* 말풍선 안. 폭을 채우되 넘지 않는다 */
2837
+
2838
+ .media-thumbnail.size-l {
2839
+ width: 100%;
2840
+ max-width: var(--media-thumbnail-max-width);
2841
+ aspect-ratio: 4 / 3;
2842
+ }
2843
+
2844
+ /* base-button(border·radius·focus 를 지운다)보다 우선해야 해서 .media-thumbnail 아래에 둔다 */
2845
+
2846
+ .media-thumbnail .media-thumbnail-open {
2847
+ position: relative;
2848
+ width: 100%;
2849
+ height: 100%;
2850
+ padding: 0;
2851
+ overflow: hidden;
2852
+ box-sizing: border-box;
2853
+ border-radius: var(--radius-6);
2854
+ background-color: var(--color-grey-scale-200);
2855
+ transition: transform var(--motion-press-duration) var(--motion-press-ease);
2856
+ }
2857
+
2858
+ :is(.media-thumbnail .media-thumbnail-open):active:not([data-disabled]) {
2859
+ transform: scale(var(--motion-press-scale));
2860
+ }
2861
+
2862
+ [data-invalid]:is(.media-thumbnail .media-thumbnail-open) {
2863
+ border: var(--media-invalid-border) solid var(--color-accent-red-500);
2864
+ }
2865
+
2866
+ :is(.media-thumbnail .media-thumbnail-open):focus-visible {
2867
+ outline: var(--hairline) solid var(--color-purple-600);
2868
+ outline-offset: var(--spacing-2);
2869
+ }
2870
+
2871
+ .media-thumbnail .media-thumbnail-remove {
2872
+ position: absolute;
2873
+ top: var(--spacing-4);
2874
+ right: var(--spacing-4);
2875
+ width: var(--media-remove-size);
2876
+ height: var(--media-remove-size);
2877
+ padding: 0;
2878
+ border-radius: var(--radius-full);
2879
+ background-color: color-mix(in srgb, var(--color-grey-scale-900) 80%, transparent);
2880
+ color: var(--color-white);
2881
+
2882
+ /* 20 원을 터치 44 로 넓힌다 */
2883
+ }
2884
+
2885
+ :is(.media-thumbnail .media-thumbnail-remove)::after {
2886
+ content: '';
2887
+ position: absolute;
2888
+ top: 50%;
2889
+ left: 50%;
2890
+ width: var(--touch-target);
2891
+ height: var(--touch-target);
2892
+ transform: translate(-50%, -50%);
2893
+ }
2894
+
2895
+ :is(.media-thumbnail .media-thumbnail-remove):focus-visible {
2896
+ outline: var(--hairline) solid var(--color-purple-600);
2897
+ outline-offset: var(--spacing-2);
2898
+ }
2899
+
2900
+ .media-thumbnail-media {
2901
+ display: block;
2902
+ width: 100%;
2903
+ height: 100%;
2904
+ -o-object-fit: cover;
2905
+ object-fit: cover;
2906
+ }
2907
+
2908
+ .media-thumbnail-play {
2909
+ position: absolute;
2910
+ top: 50%;
2911
+ left: 50%;
2912
+ display: flex;
2913
+ align-items: center;
2914
+ justify-content: center;
2915
+ width: var(--media-overlay-button-size);
2916
+ height: var(--media-overlay-button-size);
2917
+ border-radius: var(--radius-full);
2918
+ background-color: color-mix(in srgb, var(--color-black) 50%, transparent);
2919
+ color: var(--color-white);
2920
+ transform: translate(-50%, -50%);
2921
+ pointer-events: none;
2922
+ }
2923
+
2924
+ .media-thumbnail-loading {
2925
+ position: absolute;
2926
+ inset: 0;
2927
+ display: flex;
2928
+ align-items: center;
2929
+ justify-content: center;
2930
+ background-color: color-mix(in srgb, var(--color-white) 60%, transparent);
2931
+ }
2932
+
2933
+ /* 흰 막 위라 기본 링 색(grey-200)은 안 보인다 */
2934
+
2935
+ .media-thumbnail-spinner {
2936
+ color: var(--color-grey-scale-600);
2937
+ }
2938
+
2939
+ .base-message-composer:where(.root) {
2940
+ display: flex;
2941
+ flex-direction: column;
2942
+ }
2943
+
2944
+ .base-message-composer:where(.textarea) {
2945
+ box-sizing: border-box;
2946
+ margin: 0;
2947
+ font-family: inherit;
2948
+ resize: none;
2949
+ }
2950
+
2951
+ .base-message-composer[data-disabled]:where(.textarea) {
2952
+ cursor: var(--cursor-disabled);
2953
+ }
2954
+
2955
+ .base-message-composer:where(.send) {
2956
+ display: grid;
2957
+ place-items: center;
2958
+ flex: none;
2959
+ padding: 0;
2960
+ border: none;
2961
+ background: transparent;
2962
+ cursor: pointer;
2963
+ -webkit-tap-highlight-color: transparent;
2964
+ }
2965
+
2966
+ /***************************************************************************************************
2967
+ * *
2968
+ * message-composer *
2969
+ * *
2970
+ ***************************************************************************************************/
2971
+
2972
+ .message-composer {
2973
+ gap: var(--spacing-8);
2974
+ }
2975
+
2976
+ .message-composer-attachments {
2977
+ display: flex;
2978
+ gap: var(--spacing-8);
2979
+ overflow-x: auto;
2980
+ }
2981
+
2982
+ /* 입력 칸이 늘어나도 아이콘·보내기 버튼은 아래 줄에 붙는다 */
2983
+
2984
+ .message-composer-row {
2985
+ display: flex;
2986
+ align-items: flex-end;
2987
+ gap: var(--spacing-4);
2988
+ }
2989
+
2990
+ /* 16px 글자 — iOS 가 입력 칸에 포커스할 때 화면을 확대하지 않는다. 높이는 글자만큼 늘고 max-height 에서 스크롤한다 */
2991
+
2992
+ .message-composer-textarea {
2993
+ flex: 1;
2994
+ min-width: 0;
2995
+ min-height: var(--touch-target);
2996
+ max-height: var(--composer-max-height);
2997
+ field-sizing: content;
2998
+ padding: var(--spacing-10) var(--spacing-16);
2999
+ border: var(--hairline) solid var(--color-grey-scale-300);
3000
+ border-radius: var(--radius-20);
3001
+ outline: none;
3002
+ background-color: var(--color-white);
3003
+ color: var(--color-grey-scale-900);
3004
+ font-size: var(--font-size-base);
3005
+ line-height: var(--line-height-base);
3006
+ }
3007
+
3008
+ .message-composer-textarea::-moz-placeholder {
3009
+ color: var(--color-grey-scale-400);
3010
+ }
3011
+
3012
+ .message-composer-textarea::placeholder {
3013
+ color: var(--color-grey-scale-400);
3014
+ }
3015
+
3016
+ .message-composer-textarea:focus {
3017
+ border-color: var(--color-purple-900);
3018
+ }
3019
+
3020
+ .message-composer-textarea[data-disabled] {
3021
+ background-color: var(--color-grey-scale-100);
3022
+ }
3023
+
3024
+ /* 누르는 곳은 --touch-target, 보이는 원은 --composer-send-size */
3025
+
3026
+ .message-composer-send {
3027
+ width: var(--touch-target);
3028
+ height: var(--touch-target);
3029
+ transition: transform var(--motion-press-duration) var(--motion-press-ease);
3030
+ }
3031
+
3032
+ .message-composer-send:not([aria-disabled='true'],[aria-busy='true']):active {
3033
+ transform: scale(var(--motion-press-scale));
3034
+ }
3035
+
3036
+ .message-composer-send[aria-disabled='true'] .message-composer-send-circle {
3037
+ background-color: var(--color-grey-scale-200);
3038
+ }
3039
+
3040
+ .message-composer-send[data-rejecting] .message-composer-send-circle {
3041
+ animation: motion-reject-wiggle var(--motion-reject-duration) var(--motion-reject-ease);
3042
+ }
3043
+
3044
+ .message-composer-send-circle {
3045
+ display: grid;
3046
+ place-items: center;
3047
+ width: var(--composer-send-size);
3048
+ height: var(--composer-send-size);
3049
+ border-radius: var(--radius-full);
3050
+ background-color: var(--color-purple-900);
3051
+ color: var(--color-white);
3052
+ }
3053
+
3054
+ .message-composer-spinner {
3055
+ color: var(--color-white);
3056
+ }
3057
+
3058
+ .base-message:where(.root) {
3059
+ display: flex;
3060
+ flex-direction: column;
3061
+ margin: 0;
3062
+ padding: 0;
3063
+ list-style: none;
3064
+ }
3065
+
3066
+ .base-message:where(.item) {
3067
+ display: flex;
3068
+ flex-direction: column;
3069
+ }
3070
+
3071
+ .base-message:where(.bubble) {
3072
+ display: flex;
3073
+ flex-direction: column;
3074
+ min-width: 0;
3075
+ }
3076
+
3077
+ /***************************************************************************************************
3078
+ * *
3079
+ * message-thread · message · message-event *
3080
+ * *
3081
+ ***************************************************************************************************/
3082
+
3083
+ .message-thread {
3084
+ gap: var(--spacing-12);
3085
+ }
3086
+
3087
+ .message {
3088
+ gap: var(--spacing-4);
3089
+ align-items: flex-start;
3090
+ }
3091
+
3092
+ .message.side-end {
3093
+ align-items: flex-end;
3094
+ }
2127
3095
 
2128
- .page-loading-indicator {
2129
- --spinner-stroke: var(--overlay-spinner-stroke);
2130
- }
3096
+ .message.side-end .message-row {
3097
+ flex-direction: row-reverse;
3098
+ }
2131
3099
 
2132
- .page-loading-indicator .base-spinner,.page-loading-indicator .loading-indicator-ring {
2133
- width: var(--overlay-spinner-size);
2134
- height: var(--overlay-spinner-size);
3100
+ .message.side-end .message-bubble {
3101
+ border-radius: var(--radius-16) var(--radius-16) var(--radius-4) var(--radius-16);
3102
+ background-color: var(--color-purple-900);
3103
+ color: var(--color-white);
2135
3104
  }
2136
3105
 
2137
- .page-loading-text {
2138
- position: relative;
2139
- }
3106
+ /* 방금 보내거나 받은 메시지만 움직인다. 처음 그린 대화는 motion 이 없어 가만히 있다 */
2140
3107
 
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
- }
3108
+ .message:is(.motion-weak,.motion-strong) {
3109
+ --motion-slide-offset: translateY(var(--motion-enter-offset));
3110
+ }
2148
3111
 
2149
- /* 우선순위 0 이라 넘겨받은 className 이나 style 의 color 가 늘 이긴다. */
3112
+ .message.motion-weak {
3113
+ animation:
3114
+ motion-enter-fade var(--motion-enter-duration) var(--motion-enter-ease),
3115
+ motion-enter-slide var(--motion-enter-duration) var(--motion-enter-ease);
3116
+ }
2150
3117
 
2151
- :where(.base-spinner) {
2152
- color: var(--color-grey-scale-200);
2153
- }
3118
+ .message.motion-strong {
3119
+ animation:
3120
+ motion-enter-fade var(--motion-enter-strong-duration) var(--motion-enter-strong-ease),
3121
+ motion-enter-slide var(--motion-enter-strong-duration) var(--motion-enter-strong-ease);
3122
+ }
2154
3123
 
2155
- .loading-indicator-ring {
2156
- position: relative;
2157
- width: var(--icon-size-m);
2158
- height: var(--icon-size-m);
3124
+ .message-name {
3125
+ font-size: var(--font-size-xsm);
3126
+ line-height: var(--line-height-xsm);
3127
+ font-weight: var(--font-weight-semibold);
3128
+ color: var(--color-grey-scale-700);
2159
3129
  }
2160
3130
 
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;
3131
+ .message-row {
3132
+ display: flex;
3133
+ align-items: flex-end;
3134
+ gap: var(--spacing-6);
3135
+ max-width: 80%;
2171
3136
  }
2172
3137
 
2173
- .loading-indicator-single-ring:nth-child(1) {
2174
- animation-delay: calc(var(--spinner-duration) * -0.375);
2175
- }
3138
+ /* 보낸 쪽 아래 모서리만 덜 둥글다 */
2176
3139
 
2177
- .loading-indicator-single-ring:nth-child(2) {
2178
- animation-delay: calc(var(--spinner-duration) * -0.25);
3140
+ .message-bubble {
3141
+ gap: var(--spacing-6);
3142
+ padding: var(--spacing-10) var(--spacing-14);
3143
+ border-radius: var(--radius-16) var(--radius-16) var(--radius-16) var(--radius-4);
3144
+ background-color: var(--color-grey-scale-100);
3145
+ color: var(--color-grey-scale-900);
3146
+ font-size: var(--font-size-base);
3147
+ line-height: var(--line-height-base);
3148
+ white-space: pre-wrap;
3149
+ overflow-wrap: anywhere;
2179
3150
  }
2180
3151
 
2181
- .loading-indicator-single-ring:nth-child(3) {
2182
- animation-delay: calc(var(--spinner-duration) * -0.125);
2183
- }
3152
+ .message-bubble.media-only {
3153
+ padding: var(--spacing-6);
3154
+ }
2184
3155
 
2185
- .loading-indicator-single-ring:nth-child(4) {
2186
- animation-delay: 0s;
3156
+ .message-time {
3157
+ flex: none;
3158
+ font-size: var(--font-size-xsm);
3159
+ line-height: var(--line-height-xsm);
3160
+ color: var(--color-grey-scale-500);
2187
3161
  }
2188
3162
 
2189
- @keyframes lds-ring {
2190
- 0% {
2191
- transform: rotate(0deg);
2192
- }
2193
- 100% {
2194
- transform: rotate(360deg);
2195
- }
3163
+ .message-event {
3164
+ align-self: center;
3165
+ padding: var(--spacing-2) var(--spacing-10);
3166
+ border-radius: var(--radius-full);
3167
+ background-color: var(--color-grey-scale-100);
3168
+ color: var(--color-grey-scale-600);
3169
+ font-size: var(--font-size-xsm);
3170
+ line-height: var(--line-height-xsm);
3171
+ font-weight: var(--font-weight-medium);
2196
3172
  }
2197
3173
 
2198
3174
  /* Title·Close 같은 조각도 base-modal 을 달므로 상자 스타일은 container·overlay 에만 건다. */
@@ -2246,6 +3222,12 @@ button.modal-with-double-button-close-button {
2246
3222
  color: var(--color-grey-scale-600);
2247
3223
  }
2248
3224
 
3225
+ .modal-with-double-button-container {
3226
+ /* 폰 폭(390)에서 고정 폭이 화면을 넘지 않게 Modal 과 같이 양옆을 남긴다 */
3227
+ max-width: calc(100% - var(--spacing-30) * 2);
3228
+ box-sizing: border-box;
3229
+ }
3230
+
2249
3231
  .modal-with-double-button-container.size-s {
2250
3232
  width: var(--modal-width-s);
2251
3233
  }
@@ -2383,6 +3365,87 @@ button.modal-with-double-button-close-button {
2383
3365
  gap: var(--spacing-10);
2384
3366
  }
2385
3367
 
3368
+ /* 카드·시트 안에 머무는 안내. 스스로 사라지는 알림은 Toast 다 */
3369
+
3370
+ .base-notice {
3371
+ display: flex;
3372
+ align-items: flex-start;
3373
+ gap: var(--spacing-8);
3374
+ }
3375
+
3376
+ /***************************************************************************************************
3377
+ * *
3378
+ * notice *
3379
+ * *
3380
+ ***************************************************************************************************/
3381
+
3382
+ /* variant 가 바탕색과 글자·아이콘 색을 정한다 */
3383
+
3384
+ .notice {
3385
+ padding: var(--spacing-12) var(--spacing-14);
3386
+ border-radius: var(--radius-6);
3387
+ background-color: var(--color-grey-scale-100);
3388
+ color: var(--color-grey-scale-700);
3389
+ font-size: var(--font-size-sm);
3390
+ line-height: var(--line-height-sm);
3391
+ }
3392
+
3393
+ .notice.variant-success {
3394
+ background-color: var(--color-accent-green-50);
3395
+ color: var(--color-accent-green-800);
3396
+ }
3397
+
3398
+ .notice.variant-warning {
3399
+ background-color: var(--color-accent-orange-50);
3400
+ color: var(--color-accent-orange-800);
3401
+ }
3402
+
3403
+ .notice.variant-danger {
3404
+ background-color: var(--color-accent-red-50);
3405
+ color: var(--color-accent-red-700);
3406
+ }
3407
+
3408
+ /* 조건부로 나타나는 안내만 motion 을 준다. 사라질 때는 움직이지 않는다 */
3409
+
3410
+ .notice.motion-weak {
3411
+ animation: motion-enter-fade var(--motion-enter-duration) var(--motion-enter-ease);
3412
+ }
3413
+
3414
+ .notice.motion-strong {
3415
+ animation: motion-enter-fade var(--motion-enter-strong-duration) var(--motion-enter-strong-ease);
3416
+ }
3417
+
3418
+ /* 아이콘(18)을 첫 줄 글(18)에 맞춘다 */
3419
+
3420
+ .notice-icon {
3421
+ flex: none;
3422
+ }
3423
+
3424
+ .notice-body {
3425
+ display: flex;
3426
+ flex: 1;
3427
+ flex-direction: column;
3428
+ gap: var(--spacing-10);
3429
+ min-width: 0;
3430
+ }
3431
+
3432
+ .notice-text {
3433
+ display: flex;
3434
+ flex-direction: column;
3435
+ gap: var(--spacing-2);
3436
+ word-break: break-word;
3437
+ }
3438
+
3439
+ .notice-title {
3440
+ font-weight: var(--font-weight-semibold);
3441
+ }
3442
+
3443
+ /* action 은 CTAButton(xs·neutral) 이라 모양은 버튼 CSS 가 정한다. 폭은 글자만큼 */
3444
+
3445
+ .notice-action {
3446
+ align-self: flex-start;
3447
+ }
3448
+
2386
3449
  .base-pagination:where(.item) {
2387
3450
  display: inline-flex;
2388
3451
  align-items: center;
@@ -2485,6 +3548,20 @@ button.modal-with-double-button-close-button {
2485
3548
  padding: 0;
2486
3549
  }
2487
3550
 
3551
+ .base-radio:where(.chip) {
3552
+ position: relative;
3553
+ display: inline-flex;
3554
+ align-items: center;
3555
+
3556
+ /* 36 높이를 터치 44 로 넓힌다 */
3557
+ }
3558
+
3559
+ .base-radio:where(.chip)::after {
3560
+ content: '';
3561
+ position: absolute;
3562
+ inset: calc(-1 * var(--spacing-4)) 0;
3563
+ }
3564
+
2488
3565
  /***************************************************************************************************
2489
3566
  * *
2490
3567
  * radio *
@@ -2542,6 +3619,54 @@ button.modal-with-double-button-close-button {
2542
3619
  background-color: var(--color-grey-scale-400);
2543
3620
  }
2544
3621
 
3622
+ /* chip: 줄을 바꿔 가로로 늘어놓는다 */
3623
+
3624
+ .radio.radio-group.variant-chip {
3625
+ flex-direction: row;
3626
+ flex-wrap: wrap;
3627
+ gap: var(--spacing-8);
3628
+ }
3629
+
3630
+ /* 모양은 ChipGroup 칩과 같다. .base-button 의 border·radius·focus 초기화를 이기려고 .radio.radio-chip 으로 건다 */
3631
+
3632
+ .radio.radio-chip {
3633
+ height: var(--control-height-s);
3634
+ padding: 0 var(--spacing-14);
3635
+ border: var(--hairline) solid var(--color-grey-scale-300);
3636
+ border-radius: var(--radius-full);
3637
+ background-color: var(--color-grey-scale-50);
3638
+ color: var(--color-grey-scale-600);
3639
+ font-size: var(--font-size-sm);
3640
+ font-weight: var(--font-weight-medium);
3641
+ transition:
3642
+ transform var(--motion-press-duration) var(--motion-press-ease),
3643
+ background-color var(--motion-confirm-weak-duration) var(--motion-confirm-weak-ease),
3644
+ color var(--motion-confirm-weak-duration) var(--motion-confirm-weak-ease),
3645
+ border-color var(--motion-confirm-weak-duration) var(--motion-confirm-weak-ease);
3646
+ }
3647
+
3648
+ .radio.radio-chip:active:not([data-disabled]) {
3649
+ transform: scale(var(--motion-press-scale));
3650
+ }
3651
+
3652
+ .radio.radio-chip[data-state='checked'] {
3653
+ border-color: var(--color-purple-900);
3654
+ background-color: var(--color-purple-900);
3655
+ color: var(--color-grey-scale-50);
3656
+ font-weight: var(--font-weight-semibold);
3657
+ }
3658
+
3659
+ .radio.radio-chip[data-disabled] {
3660
+ border-color: var(--color-grey-scale-200);
3661
+ color: var(--color-grey-scale-300);
3662
+ cursor: var(--cursor-disabled);
3663
+ }
3664
+
3665
+ .radio.radio-chip:focus-visible {
3666
+ outline: var(--hairline) solid var(--color-purple-600);
3667
+ outline-offset: var(--spacing-2);
3668
+ }
3669
+
2545
3670
  .base-skeleton {
2546
3671
  display: block;
2547
3672
  box-sizing: border-box;
@@ -2604,7 +3729,7 @@ button.modal-with-double-button-close-button {
2604
3729
  transparent 100%
2605
3730
  );
2606
3731
  transform: translateX(-100%);
2607
- animation: skeleton-shimmer var(--skeleton-duration) var(--motion-ease) infinite;
3732
+ animation: motion-busy-shimmer var(--skeleton-duration) var(--motion-ease) infinite;
2608
3733
  }
2609
3734
 
2610
3735
  @media (prefers-reduced-motion: reduce) {
@@ -2614,15 +3739,6 @@ button.modal-with-double-button-close-button {
2614
3739
  }
2615
3740
  }
2616
3741
 
2617
- @keyframes skeleton-shimmer {
2618
- 0% {
2619
- transform: translateX(-100%);
2620
- }
2621
- 100% {
2622
- transform: translateX(100%);
2623
- }
2624
- }
2625
-
2626
3742
  .base-step-progress:where(.root) {
2627
3743
  display: grid;
2628
3744
  grid-template-columns: repeat(var(--step-count), minmax(0, 1fr));
@@ -2951,6 +4067,77 @@ button.modal-with-double-button-close-button {
2951
4067
  z-index: var(--z-base);
2952
4068
  }
2953
4069
 
4070
+ .base-tag {
4071
+ display: inline-flex;
4072
+ align-items: center;
4073
+
4074
+ /* 지우기 버튼. 글자 옆 작은 X 지만 누르는 자리는 터치 44 로 넓힌다 */
4075
+ }
4076
+
4077
+ .base-tag:where(.remove) {
4078
+ position: relative;
4079
+ flex: none;
4080
+ display: inline-flex;
4081
+ align-items: center;
4082
+ justify-content: center;
4083
+ padding: 0;
4084
+ border: none;
4085
+ background: transparent;
4086
+ cursor: var(--cursor-button);
4087
+ }
4088
+
4089
+ .base-tag:where(.remove)::after {
4090
+ content: '';
4091
+ position: absolute;
4092
+ top: 50%;
4093
+ left: 50%;
4094
+ width: var(--touch-target);
4095
+ height: var(--touch-target);
4096
+ transform: translate(-50%, -50%);
4097
+ }
4098
+
4099
+ /***************************************************************************************************
4100
+ * *
4101
+ * tag *
4102
+ * *
4103
+ ***************************************************************************************************/
4104
+
4105
+ /* CTAButton secondary·Callout 과 같은 연보라 */
4106
+
4107
+ .tag {
4108
+ height: var(--label-height-s);
4109
+ padding: 0 var(--spacing-12);
4110
+ border-radius: var(--radius-full);
4111
+ background-color: var(--color-purple-50);
4112
+ color: var(--color-purple-900);
4113
+ font-size: var(--font-size-sm);
4114
+ line-height: var(--line-height-sm);
4115
+ font-weight: var(--font-weight-semibold);
4116
+ white-space: nowrap;
4117
+
4118
+ /* X 버튼 안에 여백이 있어 오른쪽을 줄인다 */
4119
+ }
4120
+
4121
+ .tag.removable {
4122
+ padding-right: var(--spacing-4);
4123
+ }
4124
+
4125
+ .tag-remove {
4126
+ width: var(--label-height-xs);
4127
+ height: var(--label-height-xs);
4128
+ border-radius: var(--radius-full);
4129
+ color: var(--color-purple-600);
4130
+ transition: transform var(--motion-press-duration) var(--motion-press-ease);
4131
+ }
4132
+
4133
+ .tag-remove:active {
4134
+ transform: scale(var(--motion-press-scale));
4135
+ }
4136
+
4137
+ .tag-remove:focus-visible {
4138
+ outline: var(--hairline) solid var(--color-purple-600);
4139
+ }
4140
+
2954
4141
  .base-text-area:where(.textarea) {
2955
4142
  display: block;
2956
4143
  width: 100%;
@@ -3156,11 +4343,13 @@ button.modal-with-double-button-close-button {
3156
4343
  }
3157
4344
 
3158
4345
  .base-toast[data-state='open']:where(:not(.viewport,.title,.description,.action,.close)) {
3159
- animation: toast-in var(--motion-base) var(--motion-ease);
4346
+ animation:
4347
+ motion-enter-slide var(--motion-base) var(--motion-ease),
4348
+ motion-enter-fade var(--motion-base) var(--motion-ease);
3160
4349
  }
3161
4350
 
3162
4351
  .base-toast[data-state='closed']:where(:not(.viewport,.title,.description,.action,.close)) {
3163
- animation: toast-out var(--motion-fast) var(--motion-ease) forwards;
4352
+ animation: motion-exit-fade var(--motion-fast) var(--motion-ease) forwards;
3164
4353
  }
3165
4354
 
3166
4355
  .base-toast[data-swipe='move']:where(:not(.viewport,.title,.description,.action,.close)) {
@@ -3173,7 +4362,9 @@ button.modal-with-double-button-close-button {
3173
4362
  }
3174
4363
 
3175
4364
  .base-toast[data-swipe='end']:where(:not(.viewport,.title,.description,.action,.close)) {
3176
- animation: toast-swipe-out var(--motion-fast) var(--motion-ease) forwards;
4365
+ --motion-swipe-from: var(--radix-toast-swipe-end-x);
4366
+ --motion-swipe-to: calc(100% + var(--spacing-24));
4367
+ animation: motion-exit-swipe var(--motion-fast) var(--motion-ease) forwards;
3177
4368
  }
3178
4369
 
3179
4370
  .base-toast:where(.viewport) {
@@ -3189,13 +4380,13 @@ button.modal-with-double-button-close-button {
3189
4380
  list-style: none;
3190
4381
  outline: none;
3191
4382
  /* 아래에서 올라오는 자리를 기본으로 두고 position 클래스가 덮는다 */
3192
- --toast-enter: translateY(var(--spacing-16));
4383
+ --motion-slide-offset: translateY(var(--spacing-16));
3193
4384
  }
3194
4385
 
3195
4386
  .base-toast.position-top-right:where(.viewport) {
3196
4387
  top: 0;
3197
4388
  right: 0;
3198
- --toast-enter: translateY(calc(-1 * var(--spacing-16)));
4389
+ --motion-slide-offset: translateY(calc(-1 * var(--spacing-16)));
3199
4390
  }
3200
4391
 
3201
4392
  .base-toast.position-bottom-right:where(.viewport) {
@@ -3221,38 +4412,6 @@ button.modal-with-double-button-close-button {
3221
4412
  cursor: var(--cursor-button);
3222
4413
  }
3223
4414
 
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
4415
  @media (prefers-reduced-motion: reduce) {
3257
4416
  .base-toast:where(:not(.viewport, .title, .description, .action, .close)) {
3258
4417
  animation: none;