@conscience-technology/ctds 0.1.3 → 0.1.5
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.
- package/assets/components/ChatInput/ChatInput.module.css +29 -17
- package/assets/components/Drawer/Drawer.module.css +21 -98
- package/assets/components/Drawer/Drawer.tsx +3 -3
- package/assets/components/IconButton/IconButton.tsx +5 -4
- package/assets/components/ListItem/ListItem.tsx +1 -1
- package/assets/components/Onboarding/OnboardingPage.tsx +12 -0
- package/assets/components/_data.ts +8 -0
- package/assets/prompts/components/drawer.md +42 -62
- package/package.json +1 -1
|
@@ -5,23 +5,19 @@
|
|
|
5
5
|
.root {
|
|
6
6
|
display: flex;
|
|
7
7
|
flex-direction: column;
|
|
8
|
-
gap:
|
|
9
|
-
padding: var(--space-2) var(--space-3);
|
|
8
|
+
gap: 0;
|
|
10
9
|
background: var(--bg);
|
|
11
10
|
border: var(--hairline) solid var(--border);
|
|
12
11
|
border-radius: var(--radius-sm);
|
|
13
12
|
transition: var(--transition-colors), var(--transition-ring);
|
|
13
|
+
overflow: hidden; /* root border radius로 자식 clip (scroll 끝이 root border에서) */
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/*
|
|
17
|
-
|
|
18
|
-
.shape-multi
|
|
19
|
-
padding: var(--space-2) var(--space-3) var(--space-2);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/* SingleLine은 상하 대칭 유지 */
|
|
16
|
+
/* Multi/Single 공통 — root 자체는 padding 0. 각 섹션(.attachments/.quotation/.body)이
|
|
17
|
+
자신의 상하 padding을 책임져서 scroll 트랙이 root border까지 닿도록. */
|
|
18
|
+
.shape-multi,
|
|
23
19
|
.shape-single {
|
|
24
|
-
padding:
|
|
20
|
+
padding: 0;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
/* radius variant — Shape 축에 반응 */
|
|
@@ -43,6 +39,7 @@
|
|
|
43
39
|
display: grid;
|
|
44
40
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
45
41
|
gap: var(--space-1);
|
|
42
|
+
padding: var(--space-3) var(--space-3) var(--space-1);
|
|
46
43
|
}
|
|
47
44
|
/* Thumb은 셀 안에서 자체 폭 유지(좌측 정렬), chip은 셀 폭 채움. */
|
|
48
45
|
.attachmentCell {
|
|
@@ -60,6 +57,11 @@
|
|
|
60
57
|
/* Quotation */
|
|
61
58
|
.quotation {
|
|
62
59
|
display: block;
|
|
60
|
+
padding: var(--space-3) var(--space-3) var(--space-1);
|
|
61
|
+
}
|
|
62
|
+
/* attachments 아래에 quotation이 오면 상단 패딩 제거 (attachments bottom이 책임) */
|
|
63
|
+
.attachments + .quotation {
|
|
64
|
+
padding-top: 0;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/* body */
|
|
@@ -75,26 +77,34 @@
|
|
|
75
77
|
.multiStack {
|
|
76
78
|
display: flex;
|
|
77
79
|
flex-direction: column;
|
|
78
|
-
gap:
|
|
80
|
+
gap: 0;
|
|
79
81
|
width: 100%;
|
|
80
82
|
min-width: 0;
|
|
81
83
|
}
|
|
82
|
-
/* multiScroll —
|
|
83
|
-
룰:
|
|
84
|
-
|
|
84
|
+
/* multiScroll — 세로 스크롤 컨테이너. root border까지 닿음(root padding 0).
|
|
85
|
+
스크롤 룰:
|
|
86
|
+
- padding-top은 wrapper, padding-bottom은 textarea 내부에 (크로스브라우저 scroll padding 이슈 회피).
|
|
87
|
+
- 짧은 글: wrapper 안에서 스크롤 없음 → padding이 "숨 공간"처럼 보임.
|
|
88
|
+
- 긴 글: wrapper scroll 발생 → 글이 상·하 padding 영역을 통과해 root border에서 자연스레 잘림. */
|
|
85
89
|
.multiScroll {
|
|
86
90
|
overflow-y: auto;
|
|
87
91
|
max-height: calc(var(--text-base) * 1.5 * 8); /* ~8 lines */
|
|
88
|
-
padding: var(--space-
|
|
92
|
+
padding: var(--space-3) var(--space-3) 0;
|
|
93
|
+
}
|
|
94
|
+
/* attachments/quotation 아래에 오면 상단 패딩 절반 (이미 attachments/quotation에 bottom padding 있음) */
|
|
95
|
+
.attachments + .body .multiScroll,
|
|
96
|
+
.quotation + .body .multiScroll {
|
|
97
|
+
padding-top: var(--space-1);
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
/* textarea
|
|
100
|
+
/* textarea는 flat + bottom 패딩만 — padding-bottom이 scroll content의 일부가 되어
|
|
101
|
+
스크롤 끝에서 글이 wrapper(=root) border까지 흘러감. */
|
|
92
102
|
.multiField {
|
|
93
103
|
display: block;
|
|
94
104
|
width: 100%;
|
|
95
105
|
min-width: 0;
|
|
96
106
|
min-height: calc(var(--text-base) * 1.5);
|
|
97
|
-
padding: 0;
|
|
107
|
+
padding: 0 0 var(--space-3);
|
|
98
108
|
background: transparent;
|
|
99
109
|
border: 0;
|
|
100
110
|
outline: none;
|
|
@@ -114,6 +124,7 @@
|
|
|
114
124
|
align-items: center;
|
|
115
125
|
justify-content: space-between;
|
|
116
126
|
gap: var(--space-1);
|
|
127
|
+
padding: 0 var(--space-3) var(--space-2);
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
/* SingleLine */
|
|
@@ -121,6 +132,7 @@
|
|
|
121
132
|
flex-direction: row;
|
|
122
133
|
align-items: center;
|
|
123
134
|
gap: var(--space-1);
|
|
135
|
+
padding: var(--space-2) var(--space-3);
|
|
124
136
|
}
|
|
125
137
|
.singleField {
|
|
126
138
|
flex: 1 1 auto;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* Drawer ────────────────────────────────────────────────────────
|
|
2
|
-
화면 가장자리에서 밀고 들어오는
|
|
2
|
+
화면 좌/우 가장자리에서 밀고 들어오는 세로 full-height 패널.
|
|
3
|
+
흐름을 끊지 않고 상세·편집·설정을 띄울 때.
|
|
3
4
|
구조: [header: title · close] · [body: 자유 children] · [footer: actions]
|
|
4
|
-
|
|
5
|
-
방향(side) × 사이즈(size) × backdrop 정책이 variant 축. */
|
|
5
|
+
방향(side: right/left) × 사이즈(size: 뷰포트 폭 비율) × backdrop 정책이 variant 축. */
|
|
6
6
|
|
|
7
7
|
/* Overlay ───────────────────────────────────────────────────── */
|
|
8
8
|
.overlay {
|
|
@@ -33,100 +33,41 @@
|
|
|
33
33
|
will-change: transform, opacity;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/* side별
|
|
36
|
+
/* side별 자리·테두리 — 바깥쪽 radius는 떼고 안쪽만 둥글게.
|
|
37
|
+
공통: 세로 full-height. 폭은 size로 결정. */
|
|
37
38
|
.content[data-side="right"] {
|
|
38
39
|
top: 0;
|
|
39
40
|
right: 0;
|
|
40
41
|
bottom: 0;
|
|
41
|
-
width: 100vw;
|
|
42
|
-
max-width: calc(100vw - var(--space-8));
|
|
43
42
|
border-right: 0;
|
|
44
43
|
border-radius: var(--radius-lg) 0 0 var(--radius-lg);
|
|
44
|
+
animation: drawer-in-right var(--duration-slow) var(--ease-emphasized);
|
|
45
|
+
}
|
|
46
|
+
.content[data-side="right"][data-state="closed"] {
|
|
47
|
+
animation: drawer-out-right var(--duration-base) var(--ease-in);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
.content[data-side="left"] {
|
|
48
51
|
top: 0;
|
|
49
52
|
left: 0;
|
|
50
53
|
bottom: 0;
|
|
51
|
-
width: 100vw;
|
|
52
|
-
max-width: calc(100vw - var(--space-8));
|
|
53
54
|
border-left: 0;
|
|
54
55
|
border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.content[data-side="top"] {
|
|
58
|
-
top: 0;
|
|
59
|
-
left: 0;
|
|
60
|
-
right: 0;
|
|
61
|
-
height: 100vh;
|
|
62
|
-
max-height: calc(100vh - var(--space-8));
|
|
63
|
-
border-top: 0;
|
|
64
|
-
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.content[data-side="bottom"] {
|
|
68
|
-
bottom: 0;
|
|
69
|
-
left: 0;
|
|
70
|
-
right: 0;
|
|
71
|
-
height: 100vh;
|
|
72
|
-
max-height: calc(100vh - var(--space-8));
|
|
73
|
-
border-bottom: 0;
|
|
74
|
-
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* size — right/left는 width, top/bottom은 height.
|
|
78
|
-
값은 space-2(8) 배수 조합: sm 44*8=352, md 55*8=440, lg 72*8=576. */
|
|
79
|
-
.content[data-side="right"][data-size="sm"],
|
|
80
|
-
.content[data-side="left"][data-size="sm"] {
|
|
81
|
-
width: calc(var(--space-2) * 44);
|
|
82
|
-
}
|
|
83
|
-
.content[data-side="right"][data-size="md"],
|
|
84
|
-
.content[data-side="left"][data-size="md"] {
|
|
85
|
-
width: calc(var(--space-2) * 55);
|
|
86
|
-
}
|
|
87
|
-
.content[data-side="right"][data-size="lg"],
|
|
88
|
-
.content[data-side="left"][data-size="lg"] {
|
|
89
|
-
width: calc(var(--space-2) * 72);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.content[data-side="top"][data-size="sm"],
|
|
93
|
-
.content[data-side="bottom"][data-size="sm"] {
|
|
94
|
-
height: calc(var(--space-2) * 36);
|
|
95
|
-
}
|
|
96
|
-
.content[data-side="top"][data-size="md"],
|
|
97
|
-
.content[data-side="bottom"][data-size="md"] {
|
|
98
|
-
height: calc(var(--space-2) * 50);
|
|
99
|
-
}
|
|
100
|
-
.content[data-side="top"][data-size="lg"],
|
|
101
|
-
.content[data-side="bottom"][data-size="lg"] {
|
|
102
|
-
height: calc(var(--space-2) * 64);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/* 등장·퇴장 애니 — side별 slide 방향.
|
|
106
|
-
거리는 --motion-distance-scale로 Motion 축 스왑. */
|
|
107
|
-
.content[data-side="right"] {
|
|
108
|
-
animation: drawer-in-right var(--duration-slow) var(--ease-emphasized);
|
|
109
|
-
}
|
|
110
|
-
.content[data-side="right"][data-state="closed"] {
|
|
111
|
-
animation: drawer-out-right var(--duration-base) var(--ease-in);
|
|
112
|
-
}
|
|
113
|
-
.content[data-side="left"] {
|
|
114
56
|
animation: drawer-in-left var(--duration-slow) var(--ease-emphasized);
|
|
115
57
|
}
|
|
116
58
|
.content[data-side="left"][data-state="closed"] {
|
|
117
59
|
animation: drawer-out-left var(--duration-base) var(--ease-in);
|
|
118
60
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.content[data-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
animation: drawer-out-bottom var(--duration-base) var(--ease-in);
|
|
61
|
+
|
|
62
|
+
/* size — 뷰포트 폭 비율. md가 기본 50%, 좌우로 좁게/넓게.
|
|
63
|
+
좁은 화면에선 max-width로 가장자리 여백 보장. */
|
|
64
|
+
.content[data-size="sm"] { width: 33vw; }
|
|
65
|
+
.content[data-size="md"] { width: 50vw; }
|
|
66
|
+
.content[data-size="lg"] { width: 67vw; }
|
|
67
|
+
|
|
68
|
+
.content {
|
|
69
|
+
min-width: calc(var(--space-2) * 40);
|
|
70
|
+
max-width: calc(100vw - var(--space-8));
|
|
130
71
|
}
|
|
131
72
|
|
|
132
73
|
/* Header ────────────────────────────────────────────────────── */
|
|
@@ -221,30 +162,12 @@
|
|
|
221
162
|
}
|
|
222
163
|
|
|
223
164
|
@keyframes drawer-in-left {
|
|
224
|
-
from { opacity: 0; transform: translateX(calc(-var(--space-6) * var(--motion-distance-scale, 1) - 100%)); }
|
|
165
|
+
from { opacity: 0; transform: translateX(calc(-1 * var(--space-6) * var(--motion-distance-scale, 1) - 100%)); }
|
|
225
166
|
to { opacity: 1; transform: translateX(0); }
|
|
226
167
|
}
|
|
227
168
|
@keyframes drawer-out-left {
|
|
228
169
|
from { opacity: 1; transform: translateX(0); }
|
|
229
|
-
to { opacity: 0; transform: translateX(calc(-var(--space-6) * var(--motion-distance-scale, 1) - 100%)); }
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
@keyframes drawer-in-top {
|
|
233
|
-
from { opacity: 0; transform: translateY(calc(-var(--space-6) * var(--motion-distance-scale, 1) - 100%)); }
|
|
234
|
-
to { opacity: 1; transform: translateY(0); }
|
|
235
|
-
}
|
|
236
|
-
@keyframes drawer-out-top {
|
|
237
|
-
from { opacity: 1; transform: translateY(0); }
|
|
238
|
-
to { opacity: 0; transform: translateY(calc(-var(--space-6) * var(--motion-distance-scale, 1) - 100%)); }
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
@keyframes drawer-in-bottom {
|
|
242
|
-
from { opacity: 0; transform: translateY(calc(var(--space-6) * var(--motion-distance-scale, 1) + 100%)); }
|
|
243
|
-
to { opacity: 1; transform: translateY(0); }
|
|
244
|
-
}
|
|
245
|
-
@keyframes drawer-out-bottom {
|
|
246
|
-
from { opacity: 1; transform: translateY(0); }
|
|
247
|
-
to { opacity: 0; transform: translateY(calc(var(--space-6) * var(--motion-distance-scale, 1) + 100%)); }
|
|
170
|
+
to { opacity: 0; transform: translateX(calc(-1 * var(--space-6) * var(--motion-distance-scale, 1) - 100%)); }
|
|
248
171
|
}
|
|
249
172
|
|
|
250
173
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -8,7 +8,7 @@ import { IconButton } from "../IconButton";
|
|
|
8
8
|
|
|
9
9
|
import styles from "./Drawer.module.css";
|
|
10
10
|
|
|
11
|
-
export type DrawerSide = "right" | "left"
|
|
11
|
+
export type DrawerSide = "right" | "left";
|
|
12
12
|
export type DrawerSize = "sm" | "md" | "lg";
|
|
13
13
|
export type DrawerBackdrop = "scrim" | "locked" | "none";
|
|
14
14
|
|
|
@@ -33,7 +33,7 @@ export interface DrawerProps {
|
|
|
33
33
|
disableEscape?: boolean;
|
|
34
34
|
/** 열리는 방향. 기본 right. */
|
|
35
35
|
side?: DrawerSide;
|
|
36
|
-
/**
|
|
36
|
+
/** 가로 폭. 기본 md = 뷰포트 50%. 데이터·관리 도구는 sm, 복잡한 편집은 lg. */
|
|
37
37
|
size?: DrawerSize;
|
|
38
38
|
/**
|
|
39
39
|
* backdrop(dim) 정책.
|
|
@@ -57,7 +57,7 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
|
|
|
57
57
|
dismissLabel,
|
|
58
58
|
disableEscape,
|
|
59
59
|
side = "right",
|
|
60
|
-
size = "
|
|
60
|
+
size = "md",
|
|
61
61
|
backdrop = "scrim",
|
|
62
62
|
},
|
|
63
63
|
ref,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
forwardRef,
|
|
5
5
|
isValidElement,
|
|
6
6
|
cloneElement,
|
|
7
|
+
type ReactElement,
|
|
7
8
|
type ReactNode,
|
|
8
9
|
type ButtonHTMLAttributes,
|
|
9
10
|
} from "react";
|
|
@@ -44,12 +45,12 @@ const variantClassMap: Record<IconButtonVariant, string> = {
|
|
|
44
45
|
destructive: styles.destructive,
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
type PhosphorIconProps = { weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" };
|
|
49
|
+
|
|
47
50
|
function withPhosphorWeight(node: ReactNode): ReactNode {
|
|
48
51
|
if (!isValidElement(node)) return node;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
weight: (node as any).props?.weight ?? "regular",
|
|
52
|
-
});
|
|
52
|
+
const el = node as ReactElement<PhosphorIconProps>;
|
|
53
|
+
return cloneElement(el, { weight: el.props.weight ?? "regular" });
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
@@ -185,7 +185,7 @@ const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(
|
|
|
185
185
|
);
|
|
186
186
|
|
|
187
187
|
if (asChild) {
|
|
188
|
-
const only = Children.only(rest.children as ReactElement
|
|
188
|
+
const only = Children.only(rest.children) as ReactElement<HTMLAttributes<HTMLElement>>;
|
|
189
189
|
return cloneElement(only, { className: rootClass, ...rest }, content);
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -57,6 +57,8 @@ const STEPS: { key: StepKey; title: string; hint: string }[] = [
|
|
|
57
57
|
|
|
58
58
|
export function OnboardingPage() {
|
|
59
59
|
const {
|
|
60
|
+
theme,
|
|
61
|
+
setTheme,
|
|
60
62
|
setShape,
|
|
61
63
|
setMotionMode,
|
|
62
64
|
setPalette,
|
|
@@ -241,6 +243,16 @@ export function OnboardingPage() {
|
|
|
241
243
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
242
244
|
}, []);
|
|
243
245
|
|
|
246
|
+
// 온보딩은 설정 전 단계라 OS prefers-color-scheme이나 저장된 ctds-mode가 dark여도
|
|
247
|
+
// 무조건 light로 고정 (SelectableCard 안 글자 가독성 확보).
|
|
248
|
+
// theme이 dark로 돌아올 때마다 다시 light로 끌어오는 게 아니라, 마운트 시 한 번만
|
|
249
|
+
// light로 세팅 — 사용자가 온보딩 안에서 축을 바꿔도 ThemeController sync effect가
|
|
250
|
+
// light를 재적용함.
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (theme !== "light") setTheme("light");
|
|
253
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
244
256
|
const step = STEPS[stepIdx];
|
|
245
257
|
const canGoNext = useMemo(() => {
|
|
246
258
|
if (step.key === "theme") return answers.theme != null;
|
|
@@ -581,6 +581,7 @@ export const componentSlugs: Record<string, string> = {
|
|
|
581
581
|
Drawer: "feedback/drawer",
|
|
582
582
|
Toast: "feedback/toast",
|
|
583
583
|
"Table Header": "data/table-header",
|
|
584
|
+
"Table Row": "data/table-row",
|
|
584
585
|
"Table Row Select": "data/table-row-select",
|
|
585
586
|
"Table Row Edit": "data/table-row-edit",
|
|
586
587
|
"List Item": "data/list-item",
|
|
@@ -592,8 +593,15 @@ export const componentSlugs: Record<string, string> = {
|
|
|
592
593
|
"Attachment Thumb": "chat/attachment-thumb",
|
|
593
594
|
"Attachment Chip": "chat/attachment-chip",
|
|
594
595
|
"Quotation Bar": "chat/quotation-bar",
|
|
596
|
+
"Chat Input": "chat/chat-input",
|
|
595
597
|
};
|
|
596
598
|
|
|
599
|
+
/** 작업중 — 사이드바에서 접근은 가능하지만 "작업중" 뱃지로 표시해 사용 자제 유도. */
|
|
600
|
+
export const componentsInProgress: Set<string> = new Set([
|
|
601
|
+
"Table Row",
|
|
602
|
+
"Chat Input",
|
|
603
|
+
]);
|
|
604
|
+
|
|
597
605
|
export const components: { id: string; title: string; items: string[] }[] = [
|
|
598
606
|
{
|
|
599
607
|
id: "general",
|
|
@@ -1,37 +1,36 @@
|
|
|
1
1
|
# Drawer
|
|
2
2
|
|
|
3
3
|
### 원칙
|
|
4
|
-
화면 가장자리에서 밀고 들어오는 패널. **흐름을 끊지 않고**
|
|
4
|
+
화면 좌/우 가장자리에서 밀고 들어오는 세로 full-height 패널. **흐름을 끊지 않고** 본문과 연관된 보조 작업을 띄울 때. 결정 하나에 집중시키는 중앙 자리는 [[components/modal]], 가벼운 미리보기는 [[components/popover]].
|
|
5
5
|
|
|
6
6
|
### Do
|
|
7
|
-
- **본문과 연결된 보조 작업**에
|
|
8
|
-
- `title` 필수. 명사형
|
|
9
|
-
- 설명 문구는 `description` prop
|
|
10
|
-
- children
|
|
7
|
+
- **본문과 연결된 보조 작업**에 쓴다. Modal처럼 흐름을 끊는 자리와 구분.
|
|
8
|
+
- `title` 필수. 명사형 짧게. 질문형은 `description`으로.
|
|
9
|
+
- 설명 문구는 `description` prop — 헤더 안에서 title 바로 아래 바짝 붙어 하나의 "소개" 묶음으로 읽힘. `children`에 `<p>`로 설명 넣지 않는다.
|
|
10
|
+
- **`children`은 완전 자유** — 호출부가 원하는 어떤 ReactNode든. Drawer는 자리와 축만 담당하고 콘텐츠 타입에 간섭하지 않는다. 세로 스크롤은 body가 담당.
|
|
11
11
|
- `footer`는 액션 Button. **우측 정렬 · 최대 2~3개**. 순서는 **좌: outline(취소/닫기), 우: primary(저장/적용)**.
|
|
12
|
-
- 방향(`side`)은
|
|
13
|
-
- `backdrop="scrim"`(기본)은 모달형 — 뒤 영역 비활성 + 바깥 클릭 닫힘. `locked`은 저장·업로드 중 실수 방지. `none`은
|
|
12
|
+
- 방향(`side`)은 한 제품/맥락 안에서 **하나로 통일**. right가 기본, 특별한 이유 있을 때만 left.
|
|
13
|
+
- `backdrop="scrim"`(기본)은 모달형 — 뒤 영역 비활성 + 바깥 클릭 닫힘. `locked`은 저장·업로드 중 실수 방지. `none`은 상주형(뒤 영역 계속 조작) non-modal.
|
|
14
14
|
- dismiss는 **헤더 우측 X · ESC 키** 두 가지. 둘 다 기본 on.
|
|
15
|
-
- primary Button은 `md`가 기본 — Drawer 안 Input도 `md`로 맞춰 수직 리듬 유지.
|
|
16
15
|
|
|
17
16
|
### Don't
|
|
18
|
-
- Modal 자리에 Drawer 금지 — 결정 하나에 집중시키는 자리는 Modal.
|
|
19
|
-
- Drawer 안에 Drawer
|
|
20
|
-
-
|
|
21
|
-
- footer에 primary 2개
|
|
17
|
+
- Modal 자리에 Drawer 금지 — 결정 하나에 집중시키는 자리는 Modal.
|
|
18
|
+
- Drawer 안에 Drawer 금지.
|
|
19
|
+
- 짧은 알림에 Drawer 금지 — [[components/toast]] 자리.
|
|
20
|
+
- footer에 primary 2개 금지.
|
|
22
21
|
- 사이즈 조절한다고 `width` 인라인 override 금지. `size` prop만 사용. 더 넓게 필요하면 그건 페이지 자리.
|
|
23
22
|
- title을 `text-transform: uppercase`로 꾸미지 않는다.
|
|
24
|
-
-
|
|
23
|
+
- 같은 페이지에서 right/left Drawer 혼용 — 방향 통일.
|
|
24
|
+
- children 안의 "콘텐츠 타입"을 Drawer가 미리 제한하지 않는다 (알림 전용·폼 전용 variant 같은 거 만들지 말 것).
|
|
25
25
|
|
|
26
26
|
### Fallback
|
|
27
27
|
- 결정이 끝나야 다음 흐름이 가능하면 → [[components/modal]].
|
|
28
28
|
- 짧은 알림이면 → [[components/toast]].
|
|
29
29
|
- 가볍게 미리보기면 → [[components/popover]].
|
|
30
|
-
-
|
|
31
|
-
- 애매하면 `side="right"`, `size="sm"`, `backdrop="scrim"`.
|
|
30
|
+
- 애매하면 `side="right"`, `size="md"`, `backdrop="scrim"`.
|
|
32
31
|
|
|
33
32
|
### 관련
|
|
34
|
-
- [[components/modal]] — 흐름 중단·결정 집중.
|
|
33
|
+
- [[components/modal]] — 흐름 중단·결정 집중.
|
|
35
34
|
- [[components/toast]] — 휘발성 알림.
|
|
36
35
|
- [[components/button]] — footer 액션. 파괴적 확인은 `destructive` variant.
|
|
37
36
|
- [[primitives/slide]] — 가장자리 등장 애니. `--motion-distance-scale`로 축 스왑.
|
|
@@ -45,17 +44,17 @@
|
|
|
45
44
|
open={boolean}
|
|
46
45
|
onOpenChange={(open: boolean) => void}
|
|
47
46
|
title={ReactNode} // required
|
|
48
|
-
description={ReactNode} // 헤더
|
|
47
|
+
description={ReactNode} // 헤더 안, title 바로 아래 한두 줄
|
|
49
48
|
titleSrOnly={boolean} // 시각적 숨김
|
|
50
49
|
footer={ReactNode} // Button들을 넘김. 없으면 footer 미렌더
|
|
51
50
|
dismissible={boolean} // default: true (헤더 X 버튼)
|
|
52
51
|
dismissLabel={string} // X 버튼 스크린리더 라벨
|
|
53
52
|
disableEscape={boolean} // ESC 닫힘 차단
|
|
54
|
-
side="right" | "left"
|
|
55
|
-
size="sm" | "md" | "lg"
|
|
56
|
-
backdrop="scrim" | "locked" | "none"
|
|
53
|
+
side="right" | "left" // default: right
|
|
54
|
+
size="sm" | "md" | "lg" // default: md
|
|
55
|
+
backdrop="scrim" | "locked" | "none" // default: scrim
|
|
57
56
|
>
|
|
58
|
-
{children} // 자유 ReactNode
|
|
57
|
+
{children} // 자유 ReactNode — 콘텐츠 타입 제한 없음
|
|
59
58
|
</Drawer>
|
|
60
59
|
```
|
|
61
60
|
|
|
@@ -63,38 +62,37 @@
|
|
|
63
62
|
|
|
64
63
|
| prop | 값 | 결과 |
|
|
65
64
|
|---|---|---|
|
|
66
|
-
| `side` | `right`(기본) |
|
|
67
|
-
| | `left` | 좌측에서.
|
|
68
|
-
|
|
|
69
|
-
| | `bottom` | 하단에서 올라옴. 모바일 액션 시트·필터 바텀시트. |
|
|
70
|
-
| `size` | 아래 **Size 가이드** 참조 | right·left는 가로 폭을, top·bottom은 세로 높이를 결정. |
|
|
65
|
+
| `side` | `right`(기본) | 우측에서 밀고 들어옴. 기본 자리. |
|
|
66
|
+
| | `left` | 좌측에서. 특별한 이유 있을 때만. |
|
|
67
|
+
| `size` | 아래 **Size 가이드** 참조 | 뷰포트 폭 기준 비율. |
|
|
71
68
|
| `backdrop` | `scrim`(기본) | dim O · 바깥 클릭 닫힘. 일반 보조 작업. |
|
|
72
69
|
| | `locked` | dim O · 바깥 클릭 무시. 저장·업로드 중 실수 방지. ESC도 막고 싶으면 `disableEscape`. |
|
|
73
|
-
| | `none` | dim 없음 · 뒤 영역 계속 조작 가능 (non-modal).
|
|
70
|
+
| | `none` | dim 없음 · 뒤 영역 계속 조작 가능 (non-modal). |
|
|
74
71
|
| `dismissible` | `true`(기본) / `false` | 헤더 X 노출/제거. |
|
|
75
72
|
| `disableEscape` | `false`(기본) / `true` | ESC 닫힘 활성/차단. |
|
|
76
|
-
| `titleSrOnly` | `false`(기본) / `true` | 헤더 title 시각적
|
|
73
|
+
| `titleSrOnly` | `false`(기본) / `true` | 헤더 title 시각적 숨김. |
|
|
77
74
|
|
|
78
75
|
### Size 가이드
|
|
79
|
-
| size |
|
|
80
|
-
|
|
81
|
-
| `sm` |
|
|
82
|
-
| `md` |
|
|
83
|
-
| `lg` |
|
|
76
|
+
| size | width | 언제 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `sm` | 33vw | 좁은 보조 패널. 데이터·관리 도구. |
|
|
79
|
+
| `md` | 50vw | **기본.** 뷰포트의 절반. 대부분의 경우. |
|
|
80
|
+
| `lg` | 67vw | 복잡한 편집·탭 포함 보조 뷰. 이보다 넓어야 하면 페이지 자리. |
|
|
81
|
+
|
|
82
|
+
> 좁은 화면에선 `min-width`(space-2 × 40)와 `max-width`(100vw − space-8)가 자동 보정 — 좌/우 가장자리 여백 유지.
|
|
84
83
|
|
|
85
84
|
**금지 조합**
|
|
86
85
|
- `backdrop="none"` + `size="lg"` — 뒤 영역을 거의 가려놓고 non-modal 흉내. `locked` 또는 `scrim`으로.
|
|
87
86
|
- `dismissible=false` + `disableEscape=true` + `footer` 없음 — 사용자가 Drawer를 닫을 방법이 전혀 없음.
|
|
88
|
-
- 같은 페이지에서 서로 다른 `side
|
|
87
|
+
- 같은 페이지에서 서로 다른 `side` 혼용.
|
|
89
88
|
|
|
90
89
|
### 조합 예시
|
|
91
90
|
```tsx
|
|
92
|
-
//
|
|
91
|
+
// 기본 — 우측 보조 패널
|
|
93
92
|
<Drawer
|
|
94
93
|
open={open}
|
|
95
94
|
onOpenChange={setOpen}
|
|
96
|
-
title="
|
|
97
|
-
description="주문번호 #28491"
|
|
95
|
+
title="상세 보기"
|
|
98
96
|
dismissLabel="닫기"
|
|
99
97
|
footer={
|
|
100
98
|
<>
|
|
@@ -103,50 +101,32 @@
|
|
|
103
101
|
</>
|
|
104
102
|
}
|
|
105
103
|
>
|
|
106
|
-
|
|
104
|
+
{/* children 자유 — 리스트·탭·폼·이미지 어떤 것이든 */}
|
|
107
105
|
</Drawer>
|
|
108
106
|
|
|
109
|
-
//
|
|
107
|
+
// 상주형 사이드 패널 (non-modal)
|
|
110
108
|
<Drawer
|
|
111
109
|
open={open}
|
|
112
110
|
onOpenChange={setOpen}
|
|
113
111
|
side="right"
|
|
114
112
|
size="sm"
|
|
115
113
|
backdrop="none"
|
|
116
|
-
title="
|
|
114
|
+
title="사이드 패널"
|
|
117
115
|
dismissLabel="닫기"
|
|
118
116
|
>
|
|
119
|
-
|
|
120
|
-
</Drawer>
|
|
121
|
-
|
|
122
|
-
// 모바일 필터 바텀시트
|
|
123
|
-
<Drawer
|
|
124
|
-
open={open}
|
|
125
|
-
onOpenChange={setOpen}
|
|
126
|
-
side="bottom"
|
|
127
|
-
size="md"
|
|
128
|
-
title="필터"
|
|
129
|
-
dismissLabel="닫기"
|
|
130
|
-
footer={
|
|
131
|
-
<>
|
|
132
|
-
<Button variant="outline" onClick={reset}>초기화</Button>
|
|
133
|
-
<Button variant="primary" onClick={apply}>적용</Button>
|
|
134
|
-
</>
|
|
135
|
-
}
|
|
136
|
-
>
|
|
137
|
-
<FilterGroup>…</FilterGroup>
|
|
117
|
+
{/* 뒤 영역 계속 조작 가능 */}
|
|
138
118
|
</Drawer>
|
|
139
119
|
|
|
140
120
|
// 저장 중 실수 방지
|
|
141
121
|
<Drawer
|
|
142
122
|
open={saving}
|
|
143
123
|
onOpenChange={() => {}}
|
|
144
|
-
title="
|
|
145
|
-
description="
|
|
124
|
+
title="진행 중"
|
|
125
|
+
description="창을 닫으면 작업이 중단될 수 있어요."
|
|
146
126
|
backdrop="locked"
|
|
147
127
|
dismissible={false}
|
|
148
128
|
disableEscape
|
|
149
129
|
>
|
|
150
|
-
|
|
130
|
+
{/* ProgressBar 등 */}
|
|
151
131
|
</Drawer>
|
|
152
132
|
```
|