@nurihaus/web-design-system 3.5.0 → 3.7.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.
@@ -164,7 +164,7 @@ import { Callout } from '@nurihaus/web-design-system/callout'; // 이 컴포
164
164
  import { Callout } from '@nurihaus/web-design-system'; // 루트. 첫 로드 JS 가 커진다
165
165
  ```
166
166
 
167
- **언제 쓴다.** 버튼 같은 대상에 붙어 늘 떠 있는 짧은 라벨 말풍선에 쓴다(예: 로그인 버튼의 "최근 사용"). 꼬리 자리는 `tail`(`bottom-start` 기본·`bottom-end`·`top-start`·`top-end`)로 고른다. 버튼에 붙일 때는 Callout 을 직접 놓지 말고 CTAButton 의 `callout` prop 을 쓴다 — 모서리 자리를 DS 가 잡는다. 그 밖의 대상에 놓을 때는 자리(`position: absolute`·위치)를 쓰는 쪽이 `className` 으로 정한다. `.base-callout` 이 꼬리 기준으로 `position: relative` 를 가지므로 한 단계 구체적인 선택자(`.slot > .x`)로 덮는다.
167
+ **언제 쓴다.** 버튼 같은 대상에 붙어 늘 떠 있는 짧은 라벨 말풍선에 쓴다(예: 로그인 버튼의 "최근 사용"). 꼬리 자리는 `tail`(`bottom-start` 기본·`bottom-end`·`top-start`·`top-end`)로 고른다. 나타날 때 꼬리에서 튀어나온 뒤 세 번 둥실 움직이고 멈춘다(동작 줄이기 설정이면 움직이지 않는다). 모션은 쓰는 쪽이 고르지 않는다. 버튼에 붙일 때는 Callout 을 직접 놓지 말고 CTAButton 의 `callout` prop 을 쓴다 — 모서리 자리를 DS 가 잡는다. 그 밖의 대상에 놓을 때는 자리(`position: absolute`·위치)를 쓰는 쪽이 `className` 으로 정한다. 위치는 `top`·`right` 같은 값으로 잡는다 — `transform` 은 등장 모션이 쓰므로 덮어써진다. `.base-callout` 이 꼬리 기준으로 `position: relative` 를 가지므로 한 단계 구체적인 선택자(`.slot > .x`)로 덮는다.
168
168
 
169
169
  **쓰지 않는다.** 마우스를 올리거나 포커스했을 때만 설명을 보여 주려면 Tooltip 을 쓴다. 상태값·분류 라벨은 OptionBadge 를 쓴다.
170
170
 
@@ -1019,7 +1019,22 @@ import { ModalWithDoubleButton } from '@nurihaus/web-design-system';
1019
1019
 
1020
1020
  **언제 쓴다.** 삭제 확인처럼 제목, 설명, 버튼 두 개로 된 확인 대화상자에 쓴다. `ModalWithDoubleButton.Row`는 버튼을 가로로, `ModalWithDoubleButton.Column`은 세로로 배치하며, `left`와 `right`는 CTAButton props를 그대로 받는다.
1021
1021
 
1022
- **쓰지 않는다.** `open` prop이 없고 렌더되는 동안 항상 보이므로, 표시 여부는 부모가 조건부 렌더링으로 정하고 버튼 `onClick`에서 언마운트한다. `showCloseIcon`의 X 아이콘은 클릭해도 동작이 없으므로 닫기 수단으로 쓰지 않는다. 일반 모달은 Modal 을 쓴다.
1022
+ **열고 닫기.** `open`·`onOpenChange`를 넘기면 부모가 열림을 정한다. 열리면 포커스가 창 안으로 들어가고, Esc·바깥 누름·X 아이콘(`showCloseIcon`)·두 버튼이 `onOpenChange(false)`를 부르며, 닫히면 포커스가 연 요소로 돌아온다. 바텀시트 안의 삭제 확인처럼 키보드로 닿아야 하는 곳은 이 방식으로 쓴다.
1023
+
1024
+ ```tsx
1025
+ <ModalWithDoubleButton.Row
1026
+ open
1027
+ onOpenChange={() => {}}
1028
+ showCloseIcon
1029
+ title="항목을 삭제할까요?"
1030
+ left={{ text: '취소' }}
1031
+ right={{ text: '삭제', onClick: () => {} }}
1032
+ />
1033
+ ```
1034
+
1035
+ `open`을 넘기지 않으면 예전처럼 렌더되는 동안 항상 보이고 포커스를 가두지 않는다. 이때는 부모가 조건부 렌더링으로 정하고 버튼 `onClick`에서 언마운트하며, X 아이콘은 동작이 없다.
1036
+
1037
+ **쓰지 않는다.** 제목·설명·버튼 두 개를 벗어나는 일반 모달은 Modal 을 쓴다.
1023
1038
 
1024
1039
  ```tsx
1025
1040
  <ModalWithDoubleButton.Row
@@ -1034,6 +1049,8 @@ import { ModalWithDoubleButton } from '@nurihaus/web-design-system';
1034
1049
 
1035
1050
  | prop | 타입 | 필수 | 기본값 |
1036
1051
  | --- | --- | --- | --- |
1052
+ | `open` | `boolean` | | |
1053
+ | `onOpenChange` | `((open: boolean) => void)` | | |
1037
1054
  | `title` | `string` | | |
1038
1055
  | `subTitle` | `ReactNode` | | |
1039
1056
  | `titleIcon` | `ReactNode` | | |
@@ -1050,6 +1067,8 @@ import { ModalWithDoubleButton } from '@nurihaus/web-design-system';
1050
1067
 
1051
1068
  | prop | 타입 | 필수 | 기본값 |
1052
1069
  | --- | --- | --- | --- |
1070
+ | `open` | `boolean` | | |
1071
+ | `onOpenChange` | `((open: boolean) => void)` | | |
1053
1072
  | `title` | `string` | | |
1054
1073
  | `subTitle` | `ReactNode` | | |
1055
1074
  | `titleIcon` | `ReactNode` | | |
@@ -10,6 +10,9 @@ export interface ModalRootProps {
10
10
  }
11
11
  export interface ModalContainerProps extends AsChildProps, StyleProps {
12
12
  forceMount?: true;
13
+ /** 열릴 때·닫힐 때 포커스를 옮기기 직전. Radix `Dialog.Content` 에 그대로 넘긴다 */
14
+ onOpenAutoFocus?: (event: Event) => void;
15
+ onCloseAutoFocus?: (event: Event) => void;
13
16
  }
14
17
  export interface ModalTriggerProps extends AsChildProps {
15
18
  className?: string;
@@ -27,6 +30,14 @@ export interface ModalCloseProps extends AsChildProps {
27
30
  className?: string;
28
31
  }
29
32
  export interface ModalWithDoubleButtonProps {
33
+ /**
34
+ * 넘기면 열림을 부모가 정한다. 열리면 포커스가 창 안으로 들어가고 닫히면 연 요소로 돌아오며,
35
+ * Esc·바깥 누름·X 아이콘·두 버튼이 `onOpenChange(false)` 를 부른다.
36
+ * 안 넘기면 예전처럼 렌더되는 동안 항상 보인다 (포커스를 가두지 않는다).
37
+ * `onOpenChange` 를 함께 넘겨야 닫힌다.
38
+ */
39
+ open?: boolean;
40
+ onOpenChange?: (open: boolean) => void;
30
41
  title?: string;
31
42
  subTitle?: string | ReactNode;
32
43
  titleIcon?: ReactNode;
@@ -38,10 +38,9 @@ declare const meta: {
38
38
  animated: true;
39
39
  };
40
40
  decorators: ((Story: import("storybook/internal/csf").PartialStoryFn<import("@storybook/react-webpack5").ReactRenderer, {
41
- slot?: string | undefined | undefined;
41
+ className?: string | undefined | undefined;
42
42
  style?: import("react").CSSProperties | undefined;
43
- title?: string | undefined | undefined;
44
- key?: import("react").Key | null | undefined;
43
+ children?: import("react").ReactNode;
45
44
  defaultChecked?: boolean | undefined | undefined;
46
45
  defaultValue?: string | number | readonly string[] | undefined;
47
46
  suppressContentEditableWarning?: boolean | undefined | undefined;
@@ -49,7 +48,6 @@ declare const meta: {
49
48
  accessKey?: string | undefined | undefined;
50
49
  autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters" | undefined | (string & {}) | undefined;
51
50
  autoFocus?: boolean | undefined | undefined;
52
- className?: string | undefined | undefined;
53
51
  contentEditable?: (boolean | "true" | "false") | "inherit" | "plaintext-only" | undefined;
54
52
  contextMenu?: string | undefined | undefined;
55
53
  dir?: string | undefined | undefined;
@@ -59,8 +57,10 @@ declare const meta: {
59
57
  id?: string | undefined | undefined;
60
58
  lang?: string | undefined | undefined;
61
59
  nonce?: string | undefined | undefined;
60
+ slot?: string | undefined | undefined;
62
61
  spellCheck?: (boolean | "true" | "false") | undefined;
63
62
  tabIndex?: number | undefined | undefined;
63
+ title?: string | undefined | undefined;
64
64
  translate?: "yes" | "no" | undefined | undefined;
65
65
  radioGroup?: string | undefined | undefined;
66
66
  role?: import("react").AriaRole | undefined;
@@ -143,7 +143,6 @@ declare const meta: {
143
143
  "aria-valuemin"?: number | undefined | undefined;
144
144
  "aria-valuenow"?: number | undefined | undefined;
145
145
  "aria-valuetext"?: string | undefined | undefined;
146
- children?: import("react").ReactNode;
147
146
  dangerouslySetInnerHTML?: {
148
147
  __html: string | TrustedHTML;
149
148
  } | undefined | undefined;
@@ -305,6 +304,7 @@ declare const meta: {
305
304
  onAnimationIterationCapture?: import("react").AnimationEventHandler<HTMLSpanElement> | undefined;
306
305
  onTransitionEnd?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
307
306
  onTransitionEndCapture?: import("react").TransitionEventHandler<HTMLSpanElement> | undefined;
307
+ key?: import("react").Key | null | undefined;
308
308
  height?: number | string | undefined;
309
309
  width?: number | string | undefined;
310
310
  shape?: (typeof shapes)[number] | undefined;
@@ -17,11 +17,8 @@ declare const meta: {
17
17
  variant: "default";
18
18
  };
19
19
  decorators: ((Story: import("storybook/internal/csf").PartialStoryFn<import("@storybook/react-webpack5").ReactRenderer, {
20
- slot?: string | undefined | undefined;
20
+ className?: string | undefined;
21
21
  style?: import("react").CSSProperties | undefined;
22
- title: import("react").ReactNode;
23
- track?: string | undefined;
24
- key?: import("react").Key | null | undefined;
25
22
  defaultChecked?: boolean | undefined | undefined;
26
23
  defaultValue?: string | number | readonly string[] | undefined;
27
24
  suppressContentEditableWarning?: boolean | undefined | undefined;
@@ -29,7 +26,6 @@ declare const meta: {
29
26
  accessKey?: string | undefined | undefined;
30
27
  autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters" | undefined | (string & {}) | undefined;
31
28
  autoFocus?: boolean | undefined | undefined;
32
- className?: string | undefined;
33
29
  contentEditable?: (boolean | "true" | "false") | "inherit" | "plaintext-only" | undefined;
34
30
  contextMenu?: string | undefined | undefined;
35
31
  dir?: string | undefined | undefined;
@@ -39,8 +35,10 @@ declare const meta: {
39
35
  id?: string | undefined | undefined;
40
36
  lang?: string | undefined | undefined;
41
37
  nonce?: string | undefined | undefined;
38
+ slot?: string | undefined | undefined;
42
39
  spellCheck?: (boolean | "true" | "false") | undefined;
43
40
  tabIndex?: number | undefined | undefined;
41
+ title: import("react").ReactNode;
44
42
  translate?: "yes" | "no" | undefined | undefined;
45
43
  radioGroup?: string | undefined | undefined;
46
44
  role?: import("react").AriaRole | undefined;
@@ -284,12 +282,14 @@ declare const meta: {
284
282
  onAnimationIterationCapture?: import("react").AnimationEventHandler<HTMLLIElement> | undefined;
285
283
  onTransitionEnd?: import("react").TransitionEventHandler<HTMLLIElement> | undefined;
286
284
  onTransitionEndCapture?: import("react").TransitionEventHandler<HTMLLIElement> | undefined;
285
+ track?: string | undefined;
286
+ key?: import("react").Key | null | undefined;
287
+ value?: string | number | readonly string[] | undefined;
287
288
  variant?: (typeof variants)[number] | undefined;
288
- defaultOpen?: boolean | undefined;
289
+ trackProps?: Record<string, string | number | boolean> | undefined;
289
290
  open?: boolean | undefined;
291
+ defaultOpen?: boolean | undefined;
290
292
  onOpenChange?: ((open: boolean) => void) | undefined;
291
- trackProps?: Record<string, string | number | boolean> | undefined;
292
- value?: string | number | readonly string[] | undefined;
293
293
  onResume?: (() => void) | undefined;
294
294
  duration?: number | undefined;
295
295
  description?: import("react").ReactNode;
@@ -848,9 +848,33 @@
848
848
  font-weight: var(--font-weight-bold);
849
849
  white-space: nowrap;
850
850
 
851
- /* 꼬리: --spacing-8 정사각형을 45도 돌린다. 한 변의 절반에 테두리 두께만큼 더 밖으로 내어 몸통 테두리를 덮는다 */
851
+ /* 나타날 때 꼬리에서 튀어나온 뒤 세 번 둥실 움직이고 멈춘다 (ADR 0007). 계속 움직이면 거슬린다.
852
+ 둥실 지연 = 팝 지연 + 팝 시간. 팝 쪽을 바꾸면 둥실 지연도 같이 바꾼다 */
853
+ --callout-origin: var(--spacing-16) 100%;
854
+ --callout-enter: translateY(var(--spacing-4));
855
+
856
+ transform-origin: var(--callout-origin);
857
+ 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)
860
+ calc(var(--motion-fast) + var(--motion-slow)) 3;
852
861
  }
853
862
 
863
+ .base-callout.tail-bottom-end {
864
+ --callout-origin: calc(100% - var(--spacing-16)) 100%;
865
+ }
866
+
867
+ .base-callout:is(.tail-top-start,.tail-top-end) {
868
+ --callout-origin: var(--spacing-16) 0;
869
+ --callout-enter: translateY(calc(var(--spacing-4) * -1));
870
+ }
871
+
872
+ .base-callout.tail-top-end {
873
+ --callout-origin: calc(100% - var(--spacing-16)) 0;
874
+ }
875
+
876
+ /* 꼬리: --spacing-8 정사각형을 45도 돌린다. 한 변의 절반에 테두리 두께만큼 더 밖으로 내어 몸통 테두리를 덮는다 */
877
+
854
878
  .base-callout::after {
855
879
  content: '';
856
880
  position: absolute;
@@ -881,6 +905,35 @@
881
905
  right: var(--spacing-12);
882
906
  }
883
907
 
908
+ @keyframes callout-pop {
909
+ from {
910
+ opacity: 0;
911
+ transform: var(--callout-enter) scale(0.6);
912
+ }
913
+
914
+ to {
915
+ opacity: 1;
916
+ transform: none;
917
+ }
918
+ }
919
+
920
+ @keyframes callout-float {
921
+ 0%,
922
+ 100% {
923
+ transform: none;
924
+ }
925
+
926
+ 50% {
927
+ transform: translateY(calc(var(--spacing-6) / -2));
928
+ }
929
+ }
930
+
931
+ @media (prefers-reduced-motion: reduce) {
932
+ .base-callout {
933
+ animation: none;
934
+ }
935
+ }
936
+
884
937
  .base-check-box.label {
885
938
  font-size: var(--font-size-sm);
886
939
  font-weight: var(--font-weight-medium);
@@ -2185,6 +2238,14 @@
2185
2238
  width: 100%;
2186
2239
  }
2187
2240
 
2241
+ /* open 을 넘기면 X 가 진짜 닫기 버튼이 된다. 버튼 기본 모양을 지워 예전 아이콘과 같게 보이게 한다 */
2242
+
2243
+ button.modal-with-double-button-close-button {
2244
+ padding: 0;
2245
+ background-color: transparent;
2246
+ color: var(--color-grey-scale-600);
2247
+ }
2248
+
2188
2249
  .modal-with-double-button-container.size-s {
2189
2250
  width: var(--modal-width-s);
2190
2251
  }
package/dist/index.js CHANGED
@@ -3183,9 +3183,28 @@ var GHOST_SIDE = {
3183
3183
  row: 'left',
3184
3184
  column: 'right'
3185
3185
  };
3186
+ // Radix 는 Dialog.Trigger 로 열었을 때만 포커스를 돌려준다. 부모가 연 경우를 위해 연 요소를 기억한다
3187
+ var useReturnFocus = function useReturnFocus() {
3188
+ var opener = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
3189
+ var rememberOpener = function rememberOpener() {
3190
+ opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3191
+ };
3192
+ var returnFocus = function returnFocus(event) {
3193
+ var _a;
3194
+ if (!((_a = opener.current) === null || _a === void 0 ? void 0 : _a.isConnected)) return;
3195
+ event.preventDefault();
3196
+ opener.current.focus();
3197
+ };
3198
+ return {
3199
+ rememberOpener: rememberOpener,
3200
+ returnFocus: returnFocus
3201
+ };
3202
+ };
3186
3203
  var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1__.forwardRef)(function (props, ref) {
3187
3204
  var _a = (0,_helpers_utils_getMergedProps__WEBPACK_IMPORTED_MODULE_4__/* .getMergedProps */ .p)(props, _modal_props__WEBPACK_IMPORTED_MODULE_8__/* .modalWithDoubleButtonProps */ .r4),
3188
3205
  layout = _a.layout,
3206
+ open = _a.open,
3207
+ onOpenChange = _a.onOpenChange,
3189
3208
  content = _a.content,
3190
3209
  title = _a.title,
3191
3210
  subTitle = _a.subTitle,
@@ -3195,16 +3214,32 @@ var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1
3195
3214
  right = _a.right,
3196
3215
  className = _a.className;
3197
3216
  var ghostSide = GHOST_SIDE[layout];
3217
+ // open 을 넘기면 Radix 가 열림·포커스·Esc 를 맡는다. 안 넘기면 예전처럼 forceMount 로 늘 보인다.
3218
+ var controlled = open !== undefined;
3219
+ var _b = useReturnFocus(),
3220
+ rememberOpener = _b.rememberOpener,
3221
+ returnFocus = _b.returnFocus;
3198
3222
  return (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A, {
3223
+ open: open,
3224
+ onOpenChange: onOpenChange,
3199
3225
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Container, {
3200
3226
  ref: ref,
3201
3227
  className: (0,_helpers_utils_classNames__WEBPACK_IMPORTED_MODULE_3__/* .classNames */ .x)(className, 'modal-with-double-button-container'),
3202
- forceMount: true,
3203
- children: [showCloseIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
3228
+ forceMount: controlled ? undefined : true,
3229
+ onOpenAutoFocus: controlled ? rememberOpener : undefined,
3230
+ onCloseAutoFocus: controlled ? returnFocus : undefined,
3231
+ children: [showCloseIcon && (controlled ? (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Close, {
3232
+ className: 'modal-with-double-button-close-button',
3233
+ "aria-label": "\uB2EB\uAE30",
3234
+ asChild: true,
3235
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
3236
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
3237
+ })
3238
+ }) : (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
3204
3239
  className: 'modal-with-double-button-close-button',
3205
3240
  asChild: true,
3206
3241
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
3207
- }), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
3242
+ })), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
3208
3243
  className: 'modal-with-double-button-title-icon',
3209
3244
  asChild: true,
3210
3245
  children: titleIcon
@@ -62,9 +62,28 @@ var GHOST_SIDE = {
62
62
  row: 'left',
63
63
  column: 'right'
64
64
  };
65
+ // Radix 는 Dialog.Trigger 로 열었을 때만 포커스를 돌려준다. 부모가 연 경우를 위해 연 요소를 기억한다
66
+ var useReturnFocus = function useReturnFocus() {
67
+ var opener = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
68
+ var rememberOpener = function rememberOpener() {
69
+ opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
70
+ };
71
+ var returnFocus = function returnFocus(event) {
72
+ var _a;
73
+ if (!((_a = opener.current) === null || _a === void 0 ? void 0 : _a.isConnected)) return;
74
+ event.preventDefault();
75
+ opener.current.focus();
76
+ };
77
+ return {
78
+ rememberOpener: rememberOpener,
79
+ returnFocus: returnFocus
80
+ };
81
+ };
65
82
  var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1__.forwardRef)(function (props, ref) {
66
83
  var _a = (0,_helpers_utils_getMergedProps__WEBPACK_IMPORTED_MODULE_4__/* .getMergedProps */ .p)(props, _modal_props__WEBPACK_IMPORTED_MODULE_8__/* .modalWithDoubleButtonProps */ .r4),
67
84
  layout = _a.layout,
85
+ open = _a.open,
86
+ onOpenChange = _a.onOpenChange,
68
87
  content = _a.content,
69
88
  title = _a.title,
70
89
  subTitle = _a.subTitle,
@@ -74,16 +93,32 @@ var ModalWithDoubleButtonBody = /*#__PURE__*/(0,react__WEBPACK_IMPORTED_MODULE_1
74
93
  right = _a.right,
75
94
  className = _a.className;
76
95
  var ghostSide = GHOST_SIDE[layout];
96
+ // open 을 넘기면 Radix 가 열림·포커스·Esc 를 맡는다. 안 넘기면 예전처럼 forceMount 로 늘 보인다.
97
+ var controlled = open !== undefined;
98
+ var _b = useReturnFocus(),
99
+ rememberOpener = _b.rememberOpener,
100
+ returnFocus = _b.returnFocus;
77
101
  return (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A, {
102
+ open: open,
103
+ onOpenChange: onOpenChange,
78
104
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Container, {
79
105
  ref: ref,
80
106
  className: (0,_helpers_utils_classNames__WEBPACK_IMPORTED_MODULE_3__/* .classNames */ .x)(className, 'modal-with-double-button-container'),
81
- forceMount: true,
82
- children: [showCloseIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
107
+ forceMount: controlled ? undefined : true,
108
+ onOpenAutoFocus: controlled ? rememberOpener : undefined,
109
+ onCloseAutoFocus: controlled ? returnFocus : undefined,
110
+ children: [showCloseIcon && (controlled ? (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Close, {
111
+ className: 'modal-with-double-button-close-button',
112
+ "aria-label": "\uB2EB\uAE30",
113
+ asChild: true,
114
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
115
+ children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
116
+ })
117
+ }) : (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_button_base_button__WEBPACK_IMPORTED_MODULE_5__/* ["default"] */ .A, {
83
118
  className: 'modal-with-double-button-close-button',
84
119
  asChild: true,
85
120
  children: (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_assets_icons_multifly_svg__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A, {})
86
- }), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
121
+ })), titleIcon && (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(_base_modal__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A.Title, {
87
122
  className: 'modal-with-double-button-title-icon',
88
123
  asChild: true,
89
124
  children: titleIcon
package/dist/styles.css CHANGED
@@ -1161,8 +1161,28 @@ p {
1161
1161
  font-weight: var(--font-weight-bold);
1162
1162
  white-space: nowrap;
1163
1163
 
1164
- /* 꼬리: --spacing-8 정사각형을 45도 돌린다. 한 변의 절반에 테두리 두께만큼 더 밖으로 내어 몸통 테두리를 덮는다 */
1164
+ /* 나타날 때 꼬리에서 튀어나온 뒤 세 번 둥실 움직이고 멈춘다 (ADR 0007). 계속 움직이면 거슬린다.
1165
+ 둥실 지연 = 팝 지연 + 팝 시간. 팝 쪽을 바꾸면 둥실 지연도 같이 바꾼다 */
1166
+ --callout-origin: var(--spacing-16) 100%;
1167
+ --callout-enter: translateY(var(--spacing-4));
1168
+
1169
+ transform-origin: var(--callout-origin);
1170
+ animation:
1171
+ callout-pop var(--motion-slow) var(--motion-spring) var(--motion-fast) both,
1172
+ callout-float calc(var(--motion-base) * 8) var(--motion-ease)
1173
+ calc(var(--motion-fast) + var(--motion-slow)) 3;
1165
1174
  }
1175
+ .base-callout.tail-bottom-end {
1176
+ --callout-origin: calc(100% - var(--spacing-16)) 100%;
1177
+ }
1178
+ .base-callout:is(.tail-top-start,.tail-top-end) {
1179
+ --callout-origin: var(--spacing-16) 0;
1180
+ --callout-enter: translateY(calc(var(--spacing-4) * -1));
1181
+ }
1182
+ .base-callout.tail-top-end {
1183
+ --callout-origin: calc(100% - var(--spacing-16)) 0;
1184
+ }
1185
+ /* 꼬리: --spacing-8 정사각형을 45도 돌린다. 한 변의 절반에 테두리 두께만큼 더 밖으로 내어 몸통 테두리를 덮는다 */
1166
1186
  .base-callout::after {
1167
1187
  content: '';
1168
1188
  position: absolute;
@@ -1188,6 +1208,32 @@ p {
1188
1208
  .base-callout:is(.tail-bottom-end,.tail-top-end)::after {
1189
1209
  right: var(--spacing-12);
1190
1210
  }
1211
+ @keyframes callout-pop {
1212
+ from {
1213
+ opacity: 0;
1214
+ transform: var(--callout-enter) scale(0.6);
1215
+ }
1216
+
1217
+ to {
1218
+ opacity: 1;
1219
+ transform: none;
1220
+ }
1221
+ }
1222
+ @keyframes callout-float {
1223
+ 0%,
1224
+ 100% {
1225
+ transform: none;
1226
+ }
1227
+
1228
+ 50% {
1229
+ transform: translateY(calc(var(--spacing-6) / -2));
1230
+ }
1231
+ }
1232
+ @media (prefers-reduced-motion: reduce) {
1233
+ .base-callout {
1234
+ animation: none;
1235
+ }
1236
+ }
1191
1237
  .base-check-box.label {
1192
1238
  font-size: var(--font-size-sm);
1193
1239
  font-weight: var(--font-weight-medium);
@@ -2234,6 +2280,12 @@ p {
2234
2280
  align-items: center;
2235
2281
  width: 100%;
2236
2282
  }
2283
+ /* open 을 넘기면 X 가 진짜 닫기 버튼이 된다. 버튼 기본 모양을 지워 예전 아이콘과 같게 보이게 한다 */
2284
+ button.modal-with-double-button-close-button {
2285
+ padding: 0;
2286
+ background-color: transparent;
2287
+ color: var(--color-grey-scale-600);
2288
+ }
2237
2289
  .modal-with-double-button-container.size-s {
2238
2290
  width: var(--modal-width-s);
2239
2291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurihaus/web-design-system",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "description": "Nurihaus 디자인 시스템 — 컴포넌트와 디자인 토큰",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",