@uniai-fe/uds-templates 0.0.9 → 0.0.11

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 (44) hide show
  1. package/README.md +77 -1
  2. package/dist/styles.css +212 -267
  3. package/package.json +6 -4
  4. package/src/components/auth/index.tsx +11 -0
  5. package/src/components/auth/login/index.tsx +1 -1
  6. package/src/components/auth/login/markup/FormField.tsx +2 -2
  7. package/src/components/auth/login/types/props.ts +12 -12
  8. package/src/components/auth/login/types.ts +2 -2
  9. package/src/components/auth/signup/hooks/index.ts +3 -0
  10. package/src/components/auth/signup/hooks/useSignupAccountForm.ts +77 -0
  11. package/src/components/auth/signup/hooks/useSignupUserInfoForm.ts +81 -0
  12. package/src/components/auth/signup/hooks/useSignupVerificationForm.ts +77 -0
  13. package/src/components/auth/signup/index.ts +24 -0
  14. package/src/components/auth/signup/markup/AccountForm.tsx +124 -0
  15. package/src/components/auth/signup/markup/Complete.tsx +61 -0
  16. package/src/components/auth/signup/markup/UserInfoForm.tsx +97 -0
  17. package/src/components/auth/signup/markup/VerificationForm.tsx +155 -0
  18. package/src/components/auth/signup/markup/index.ts +4 -0
  19. package/src/components/auth/signup/styles/signup.scss +135 -0
  20. package/src/components/auth/signup/types/hooks.ts +85 -0
  21. package/src/components/auth/signup/types/index.ts +2 -0
  22. package/src/components/auth/signup/types/props.ts +105 -0
  23. package/src/components/auth/signup/utils/composeFieldProps.ts +50 -0
  24. package/src/components/modal/core/components/Container.tsx +41 -0
  25. package/src/components/modal/core/components/FooterButtons.tsx +132 -0
  26. package/src/components/modal/core/components/Provider.tsx +28 -0
  27. package/src/components/modal/core/components/Root.tsx +93 -0
  28. package/src/components/modal/core/hooks/useModal.ts +136 -0
  29. package/src/components/modal/core/jotai/atoms.ts +10 -0
  30. package/src/components/modal/index.scss +4 -0
  31. package/src/components/modal/index.tsx +16 -0
  32. package/src/components/modal/styles/animations.scss +24 -0
  33. package/src/components/modal/styles/base.scss +45 -0
  34. package/src/components/modal/styles/container.scss +138 -0
  35. package/src/components/modal/styles/dimmer.scss +23 -0
  36. package/src/components/modal/templates/Alert.tsx +104 -0
  37. package/src/components/modal/templates/Dialog.tsx +112 -0
  38. package/src/components/modal/types/footer.ts +36 -0
  39. package/src/components/modal/types/index.ts +21 -0
  40. package/src/components/modal/types/options.ts +6 -0
  41. package/src/components/modal/types/state.ts +31 -0
  42. package/src/components/modal/types/templates.ts +32 -0
  43. package/src/index.scss +1 -0
  44. package/src/index.tsx +1 -0
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { Button } from "@uniai-fe/uds-primitives";
5
+
6
+ import { useModal } from "../hooks/useModal";
7
+
8
+ import type {
9
+ ModalFooterButton,
10
+ ModalStackKey,
11
+ ModalFooterButtonAppearance,
12
+ ModalFooterButtonLinkOptions,
13
+ } from "../../types";
14
+
15
+ const resolveAppearance = (
16
+ appearance?: ModalFooterButtonAppearance,
17
+ ): ModalFooterButtonAppearance => appearance ?? "default";
18
+
19
+ const createAnchorProps = (
20
+ linkOptions?: ModalFooterButtonLinkOptions,
21
+ ):
22
+ | {
23
+ as: "a";
24
+ href: string;
25
+ target?: string;
26
+ rel?: string;
27
+ }
28
+ | Record<string, never> => {
29
+ if (!linkOptions) return {};
30
+ return {
31
+ as: "a",
32
+ href: linkOptions.href,
33
+ target: linkOptions.target,
34
+ rel: linkOptions.rel,
35
+ };
36
+ };
37
+
38
+ /**
39
+ * 모달 footer 버튼 리스트 렌더러.
40
+ * @component
41
+ * @param {{ stackKey: ModalStackKey; buttons: ModalFooterButton[] }} props 버튼 정의 목록
42
+ */
43
+ export function ModalFooterButtons({
44
+ stackKey,
45
+ buttons,
46
+ }: {
47
+ stackKey: ModalStackKey;
48
+ buttons: ModalFooterButton[];
49
+ }) {
50
+ const { closeModal } = useModal();
51
+ // appearance 정보를 선계산해 footer group 스타일을 판별한다.
52
+ const resolvedButtons = buttons.map(button => ({
53
+ ...button,
54
+ appearance: resolveAppearance(button.defaultOptions.appearance),
55
+ }));
56
+ const groupAppearance = resolvedButtons.every(
57
+ button => button.appearance === "text",
58
+ )
59
+ ? "text"
60
+ : "default";
61
+
62
+ return (
63
+ <div
64
+ className="uds-modal-footer-buttons"
65
+ data-count={resolvedButtons.length}
66
+ data-appearance={groupAppearance}
67
+ >
68
+ {resolvedButtons.map((button, index) => {
69
+ const {
70
+ defaultOptions,
71
+ linkOptions,
72
+ closeOnClick = button.role === "close",
73
+ appearance,
74
+ } = button;
75
+ const anchorProps = createAnchorProps(linkOptions);
76
+ const closeAndCleanup = () => {
77
+ // footer 버튼마다 주입된 이벤트 → close 순서를 동일하게 유지한다.
78
+ defaultOptions.onClick?.();
79
+ if (closeOnClick) {
80
+ closeModal({ stackKey });
81
+ }
82
+ };
83
+ const resolvedBlock =
84
+ typeof defaultOptions.block === "boolean"
85
+ ? defaultOptions.block
86
+ : true;
87
+
88
+ const baseClassName = clsx(
89
+ "uds-modal-footer-button",
90
+ appearance === "text"
91
+ ? "uds-modal-footer-button-text"
92
+ : "uds-modal-footer-button-solid",
93
+ defaultOptions.className,
94
+ );
95
+ const commonProps = {
96
+ key: `uds-modal-footer-button-${button.stackKey}-${button.role}-${index}`,
97
+ className: baseClassName,
98
+ disabled: defaultOptions.disabled,
99
+ block: resolvedBlock,
100
+ onClick: closeAndCleanup,
101
+ ...anchorProps,
102
+ };
103
+
104
+ if (appearance === "text") {
105
+ const textPriority =
106
+ defaultOptions.priority && defaultOptions.priority !== "primary"
107
+ ? defaultOptions.priority
108
+ : "secondary";
109
+ return (
110
+ <Button.Text
111
+ {...commonProps}
112
+ size={defaultOptions.textSize ?? "large"}
113
+ priority={textPriority}
114
+ >
115
+ {defaultOptions.label}
116
+ </Button.Text>
117
+ );
118
+ }
119
+
120
+ return (
121
+ <Button.Default
122
+ {...commonProps}
123
+ scale={defaultOptions.scale ?? "solid-large"}
124
+ priority={defaultOptions.priority ?? "primary"}
125
+ >
126
+ {defaultOptions.label}
127
+ </Button.Default>
128
+ );
129
+ })}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { Fragment } from "react";
4
+
5
+ import { useModal } from "../hooks/useModal";
6
+ import { ModalRoot } from "./Root";
7
+
8
+ /**
9
+ * 서비스 루트에서 1회 배치되는 모달 Provider.
10
+ * @component
11
+ */
12
+ export function ModalProvider() {
13
+ const { modalStacks } = useModal();
14
+
15
+ if (!modalStacks.length) return null;
16
+
17
+ return (
18
+ <Fragment>
19
+ {modalStacks.map((stack, index) => (
20
+ <ModalRoot
21
+ key={`uds-modal-${stack.stackKey}`}
22
+ {...stack}
23
+ index={index}
24
+ />
25
+ ))}
26
+ </Fragment>
27
+ );
28
+ }
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo } from "react";
4
+ import type { CSSProperties } from "react";
5
+ import clsx from "clsx";
6
+
7
+ import { useModal } from "../hooks/useModal";
8
+ import { ModalContainer } from "./Container";
9
+
10
+ import type { ModalProps, ModalShowState, ModalState } from "../../types";
11
+
12
+ type ModalRootProps = ModalState & {
13
+ index: number;
14
+ };
15
+
16
+ const resolveDataState = (show: ModalShowState): string => {
17
+ if (show === "init") return "init";
18
+ return show ? "open" : "closed";
19
+ };
20
+
21
+ /**
22
+ * 모달 Root; overlay 클릭 닫기와 상태 전환을 담당한다.
23
+ * @component
24
+ * @param {ModalRootProps} props 루트 속성
25
+ * @param {string} props.stackKey 모달 스택 식별자
26
+ * @param {ModalProps} props.modalProps 모달 렌더링에 필요한 상태
27
+ * @param {number} props.index Provider에서 전달한 스택 index
28
+ * @param {string} [props.className] 사용자 정의 className
29
+ */
30
+ export function ModalRoot({
31
+ stackKey,
32
+ modalProps,
33
+ index,
34
+ className,
35
+ }: ModalRootProps) {
36
+ const { updateModal, closeModal } = useModal();
37
+
38
+ useEffect(() => {
39
+ if (modalProps.show === "init") {
40
+ updateModal({ stackKey, modalProps: { show: true } });
41
+ }
42
+ }, [modalProps.show, stackKey, updateModal]);
43
+
44
+ const handleClose = useCallback(() => {
45
+ closeModal({ stackKey });
46
+ }, [closeModal, stackKey]);
47
+
48
+ const stopPropagation = useCallback(
49
+ (event: React.MouseEvent<HTMLDivElement>) => {
50
+ event.stopPropagation();
51
+ },
52
+ [],
53
+ );
54
+
55
+ const dataState = useMemo(
56
+ () => resolveDataState(modalProps.show),
57
+ [modalProps.show],
58
+ );
59
+
60
+ const layerStyle = useMemo(
61
+ () =>
62
+ ({
63
+ "--uds-modal-index": index,
64
+ }) as CSSProperties,
65
+ [index],
66
+ );
67
+
68
+ return (
69
+ <div
70
+ className={clsx("uds-modal-root", className)}
71
+ data-state={dataState}
72
+ style={layerStyle}
73
+ onClick={handleClose}
74
+ role="presentation"
75
+ >
76
+ <div className="uds-modal-dimmer" />
77
+ <div
78
+ className="uds-modal-surface"
79
+ role="dialog"
80
+ aria-modal="true"
81
+ onClick={stopPropagation}
82
+ >
83
+ <ModalContainer
84
+ stackKey={stackKey}
85
+ header={modalProps.header}
86
+ body={modalProps.body}
87
+ footer={modalProps.footer}
88
+ footerButtons={modalProps.footerButtons}
89
+ />
90
+ </div>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { useAtom } from "jotai";
5
+
6
+ import { modalStackAtom } from "../jotai/atoms";
7
+
8
+ import type {
9
+ ModalCloseRequest,
10
+ ModalState,
11
+ ModalStatePatch,
12
+ ModalStackKey,
13
+ } from "../../types";
14
+
15
+ const DEFAULT_CLOSE_DELAY = 400;
16
+
17
+ type CloseFlagState = {
18
+ stackKey: ModalStackKey;
19
+ showDelay: number;
20
+ callback?: () => void;
21
+ };
22
+
23
+ const ensureDelay = (value?: number): number =>
24
+ typeof value === "number" ? value : DEFAULT_CLOSE_DELAY;
25
+
26
+ type UseModalReturn = {
27
+ modalStacks: ModalState[];
28
+ newModal: (newStack: ModalState) => void;
29
+ updateModal: (nextStack: ModalStatePatch) => void;
30
+ closeModal: (request: ModalCloseRequest) => void;
31
+ };
32
+
33
+ /**
34
+ * ui-legacy 스펙을 최대한 유지한 모달 스택 훅.
35
+ * @hook
36
+ * @returns {UseModalReturn} 스택 상태와 조작자(new/open/close)
37
+ * @desc
38
+ * - modalStacks: 현재 열린 스택 배열
39
+ * - newModal: stackKey 중복 검증 후 "init" 상태로 푸시
40
+ * - updateModal: 부분 업데이트(클래스/슬롯 변경)
41
+ * - closeModal: show=false → delay 후 제거
42
+ */
43
+ export function useModal(): UseModalReturn {
44
+ const [modalStacks, updateModalStack] = useAtom(modalStackAtom);
45
+ const [closeFlag, setCloseFlag] = useState<CloseFlagState | null>(null);
46
+
47
+ const newModal = useCallback(
48
+ (newStack: ModalState) => {
49
+ updateModalStack((stacks: ModalState[]) => {
50
+ const exists = stacks.some(
51
+ stack => stack.stackKey === newStack.stackKey,
52
+ );
53
+ if (exists) {
54
+ console.warn(
55
+ `[useModal] stack "${newStack.stackKey}" already exists; ignored.`,
56
+ );
57
+ return stacks;
58
+ }
59
+ return [
60
+ ...stacks,
61
+ {
62
+ ...newStack,
63
+ modalProps: { ...newStack.modalProps, show: "init" },
64
+ },
65
+ ];
66
+ });
67
+ },
68
+ [updateModalStack],
69
+ );
70
+
71
+ const updateModal = useCallback(
72
+ (nextStack: ModalStatePatch) => {
73
+ updateModalStack((stacks: ModalState[]) =>
74
+ stacks.map(stack => {
75
+ if (stack.stackKey !== nextStack.stackKey) return stack;
76
+ const mergedProps = nextStack.modalProps
77
+ ? { ...stack.modalProps, ...nextStack.modalProps }
78
+ : stack.modalProps;
79
+ return {
80
+ ...stack,
81
+ ...(typeof nextStack.className !== "undefined"
82
+ ? { className: nextStack.className }
83
+ : null),
84
+ modalProps: mergedProps,
85
+ };
86
+ }),
87
+ );
88
+ },
89
+ [updateModalStack],
90
+ );
91
+
92
+ const closeModal = useCallback(
93
+ ({ stackKey, callback }: ModalCloseRequest) => {
94
+ let resolvedDelay = DEFAULT_CLOSE_DELAY;
95
+ let hasTarget = false;
96
+
97
+ updateModalStack((stacks: ModalState[]) =>
98
+ stacks.map(stack => {
99
+ if (stack.stackKey !== stackKey) return stack;
100
+ hasTarget = true;
101
+ resolvedDelay = ensureDelay(stack.modalProps.showDelay);
102
+ return {
103
+ ...stack,
104
+ modalProps: { ...stack.modalProps, show: false },
105
+ };
106
+ }),
107
+ );
108
+
109
+ if (!hasTarget) {
110
+ console.warn(
111
+ `[useModal] stack "${stackKey}" not found; close skipped.`,
112
+ );
113
+ return;
114
+ }
115
+
116
+ setCloseFlag({ stackKey, showDelay: resolvedDelay, callback });
117
+ },
118
+ [updateModalStack],
119
+ );
120
+
121
+ useEffect(() => {
122
+ if (!closeFlag) return;
123
+
124
+ const timer = setTimeout(() => {
125
+ updateModalStack((stacks: ModalState[]) =>
126
+ stacks.filter(stack => stack.stackKey !== closeFlag.stackKey),
127
+ );
128
+ closeFlag.callback?.();
129
+ setCloseFlag(null);
130
+ }, closeFlag.showDelay);
131
+
132
+ return () => clearTimeout(timer);
133
+ }, [closeFlag, updateModalStack]);
134
+
135
+ return { modalStacks, newModal, updateModal, closeModal };
136
+ }
@@ -0,0 +1,10 @@
1
+ import { atom } from "jotai";
2
+
3
+ import type { ModalState } from "../../types";
4
+
5
+ /**
6
+ * 모달 스택 상태
7
+ * @state
8
+ * @desc ui-legacy 스택 정책을 유지한다.
9
+ */
10
+ export const modalStackAtom = atom<ModalState[]>([]);
@@ -0,0 +1,4 @@
1
+ @use "./styles/base.scss" as *;
2
+ @use "./styles/dimmer.scss" as *;
3
+ @use "./styles/container.scss" as *;
4
+ @use "./styles/animations.scss" as *;
@@ -0,0 +1,16 @@
1
+ import "./index.scss";
2
+
3
+ import { ModalProvider } from "./core/components/Provider";
4
+ import { useModal } from "./core/hooks/useModal";
5
+ import { createAlertModal } from "./templates/Alert";
6
+ import { createDialogModal } from "./templates/Dialog";
7
+
8
+ export const Modal = {
9
+ Provider: ModalProvider,
10
+ useModal,
11
+ Alert: createAlertModal,
12
+ Dialog: createDialogModal,
13
+ };
14
+
15
+ export { ModalProvider, useModal, createAlertModal, createDialogModal };
16
+ export type * from "./types";
@@ -0,0 +1,24 @@
1
+ .uds-modal-surface {
2
+ opacity: 0;
3
+ transform: translate3d(0, 12px, 0);
4
+ transition:
5
+ opacity 0.2s ease,
6
+ transform 0.2s ease;
7
+
8
+ .uds-modal-root[data-state="open"] & {
9
+ opacity: 1;
10
+ transform: translate3d(0, 0, 0);
11
+ }
12
+
13
+ .uds-modal-root[data-state="closed"] & {
14
+ opacity: 0;
15
+ transform: translate3d(0, 12px, 0);
16
+ pointer-events: none;
17
+ }
18
+ }
19
+
20
+ @media (prefers-reduced-motion: reduce) {
21
+ .uds-modal-surface {
22
+ transition: none;
23
+ }
24
+ }
@@ -0,0 +1,45 @@
1
+ @use "@uniai-fe/uds-foundation/css";
2
+
3
+ :where(.radix-themes, .theme-root, :root) {
4
+ --uds-modal-overlay-bg: var(--dialog-overlay-bg, rgba(5, 6, 12, 0.55));
5
+ --uds-modal-surface-bg: var(--color-bg-surface-static-white);
6
+ --uds-modal-surface-radius: var(--theme-radius-large-1);
7
+ --uds-modal-surface-shadow: 0px 18px 40px rgba(8, 11, 30, 0.18);
8
+ --uds-modal-max-width: min(
9
+ 360px,
10
+ calc(100vw - var(--spacing-padding-10) * 2)
11
+ );
12
+ --uds-modal-max-height: calc(100vh - var(--spacing-padding-10) * 2);
13
+ }
14
+
15
+ .uds-modal-root {
16
+ position: fixed;
17
+ inset: 0;
18
+ z-index: calc(400 + var(--uds-modal-index, 0));
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ padding: var(--spacing-padding-6);
23
+ pointer-events: none;
24
+ }
25
+
26
+ .uds-modal-surface {
27
+ position: relative;
28
+ width: 100%;
29
+ max-width: var(--uds-modal-max-width);
30
+ max-height: var(--uds-modal-max-height);
31
+ background-color: var(--uds-modal-surface-bg);
32
+ border-radius: var(--uds-modal-surface-radius);
33
+ box-shadow: var(--uds-modal-surface-shadow);
34
+ pointer-events: auto;
35
+ display: flex;
36
+ flex-direction: column;
37
+ overflow: hidden;
38
+ }
39
+
40
+ .uds-modal-dimmer {
41
+ position: absolute;
42
+ inset: 0;
43
+ background-color: var(--uds-modal-overlay-bg);
44
+ pointer-events: auto;
45
+ }
@@ -0,0 +1,138 @@
1
+ .uds-modal-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ }
6
+
7
+ .uds-modal-header,
8
+ .uds-modal-body {
9
+ padding: 0;
10
+ margin: 0;
11
+ }
12
+
13
+ .uds-modal-footer {
14
+ padding: 0;
15
+ border-top: none;
16
+ }
17
+
18
+ .uds-modal-footer-buttons {
19
+ display: flex;
20
+ width: 100%;
21
+ gap: var(--spacing-gap-4, 16px);
22
+ padding: var(--spacing-padding-6, 16px);
23
+ align-items: stretch;
24
+
25
+ &[data-count="1"] {
26
+ .uds-modal-footer-button {
27
+ width: 100%;
28
+ }
29
+ }
30
+
31
+ &[data-count="2"] {
32
+ .uds-modal-footer-button {
33
+ flex: 1 1 0;
34
+ }
35
+ }
36
+
37
+ &[data-appearance="text"] {
38
+ padding: 0;
39
+ gap: 0;
40
+ min-height: var(--notice-dialog-action-height, 56px);
41
+ border-top: 1px solid
42
+ var(
43
+ --uds-modal-footer-border-color,
44
+ var(
45
+ --dialog-border-color,
46
+ var(--color-border-standard-cool-gray, #e4e5e7)
47
+ )
48
+ );
49
+
50
+ .uds-modal-footer-button {
51
+ border-radius: 0;
52
+ }
53
+
54
+ .uds-modal-footer-button + .uds-modal-footer-button {
55
+ border-left: 1px solid
56
+ var(
57
+ --uds-modal-footer-border-color,
58
+ var(
59
+ --dialog-border-color,
60
+ var(--color-border-standard-cool-gray, #e4e5e7)
61
+ )
62
+ );
63
+ }
64
+ }
65
+ }
66
+
67
+ .uds-modal-footer-button-solid {
68
+ min-height: 48px;
69
+ }
70
+
71
+ .uds-modal-footer-button-text {
72
+ min-height: 56px;
73
+ justify-content: center;
74
+ border-radius: 0;
75
+ }
76
+
77
+ .uds-modal-alert-message {
78
+ padding: var(--spacing-padding-10, 32px) var(--spacing-padding-8, 24px);
79
+ text-align: center;
80
+ color: var(--dialog-body-color);
81
+ word-break: keep-all;
82
+
83
+ > p,
84
+ > span,
85
+ > strong,
86
+ > em {
87
+ margin: 0;
88
+ font-size: var(--dialog-body-font-size);
89
+ line-height: 1.5em;
90
+ font-weight: var(--font-body-small-weight, 400);
91
+ word-break: inherit;
92
+ }
93
+
94
+ > * + * {
95
+ margin-top: var(--spacing-gap-2, 8px);
96
+ }
97
+ }
98
+
99
+ .uds-modal-dialog-header,
100
+ .uds-modal-dialog-body {
101
+ padding: 0;
102
+ margin: 0;
103
+ }
104
+
105
+ .uds-modal-dialog-header-content {
106
+ padding: var(--spacing-padding-7, 20px) var(--spacing-padding-6, 16px);
107
+ text-align: center;
108
+
109
+ > h3 {
110
+ margin: 0;
111
+ color: var(--dialog-title-color);
112
+ font-size: var(--dialog-title-font-size);
113
+ line-height: var(--dialog-title-line-height);
114
+ font-weight: var(--dialog-title-weight);
115
+ }
116
+ }
117
+
118
+ .uds-modal-dialog-body-content {
119
+ padding: var(--spacing-padding-7, 20px) var(--spacing-padding-6, 16px);
120
+ text-align: center;
121
+ color: var(--dialog-body-color);
122
+ word-break: keep-all;
123
+
124
+ > p,
125
+ > span,
126
+ > strong,
127
+ > em {
128
+ margin: 0;
129
+ font-size: var(--dialog-body-font-size);
130
+ line-height: 1.5em;
131
+ font-weight: var(--font-body-small-weight, 400);
132
+ word-break: inherit;
133
+ }
134
+
135
+ > * + * {
136
+ margin-top: var(--spacing-gap-2, 8px);
137
+ }
138
+ }
@@ -0,0 +1,23 @@
1
+ .uds-modal-root {
2
+ &[data-state="init"],
3
+ &[data-state="open"] {
4
+ pointer-events: auto;
5
+ }
6
+
7
+ &[data-state="closed"] {
8
+ pointer-events: none;
9
+ }
10
+ }
11
+
12
+ .uds-modal-dimmer {
13
+ opacity: 0;
14
+ transition: opacity 0.2s ease;
15
+
16
+ .uds-modal-root[data-state="open"] & {
17
+ opacity: 1;
18
+ }
19
+
20
+ .uds-modal-root[data-state="closed"] & {
21
+ opacity: 0;
22
+ }
23
+ }