@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.
@@ -5,23 +5,19 @@
5
5
  .root {
6
6
  display: flex;
7
7
  flex-direction: column;
8
- gap: var(--space-2);
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
- /* MultiLine — root 상하 padding은 좁게. textarea 내부 padding이 숨 쉴 공간 책임.
17
- 룰: 긴 글 스크롤 시 textarea 안의 bottom padding 영역까지 흘러 들어가 자연스레 잘림. */
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: var(--space-2) var(--space-3);
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: var(--space-1);
80
+ gap: 0;
79
81
  width: 100%;
80
82
  min-width: 0;
81
83
  }
82
- /* multiScroll — 바깥 스크롤 컨테이너. padding은 안쪽 트랙 역할.
83
- 룰: 긴 글이 들어오면 textarea가 자람 → 이 wrapper가 max-height로 넘침 →
84
- 스크롤 시 텍스트가 padding 영역을 통과해 wrapper border에서 자연스레 잘림. */
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-1) var(--space-1);
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 자체는 flat — padding/scroll 없음. 콘텐츠 높이로만 자람. */
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
- Modal과 달리 가장자리에 붙어 full-length(세로 또는 가로)로 자람.
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별 자리·사이즈·테두리 — radius는 바깥쪽은 떼고 안쪽만 둥글게. */
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
- .content[data-side="top"] {
120
- animation: drawer-in-top var(--duration-slow) var(--ease-emphasized);
121
- }
122
- .content[data-side="top"][data-state="closed"] {
123
- animation: drawer-out-top var(--duration-base) var(--ease-in);
124
- }
125
- .content[data-side="bottom"] {
126
- animation: drawer-in-bottom var(--duration-slow) var(--ease-emphasized);
127
- }
128
- .content[data-side="bottom"][data-state="closed"] {
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" | "top" | "bottom";
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
- /** 가로/세로 사이즈. right/left는 가로 폭, top/bottom은 세로 높이. 기본 sm. */
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 = "sm",
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
- return cloneElement(node as React.ReactElement, {
50
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- 화면 가장자리에서 밀고 들어오는 패널. **흐름을 끊지 않고** 상세 보기·편집·설정·필터처럼 **본문과 연관된 보조 작업**을 띄울 때. 결정 하나에 집중시키는 중앙 자리는 [[components/modal]], 가벼운 미리보기는 [[components/popover]].
4
+ 화면 좌/우 가장자리에서 밀고 들어오는 세로 full-height 패널. **흐름을 끊지 않고** 본문과 연관된 보조 작업을 띄울 때. 결정 하나에 집중시키는 중앙 자리는 [[components/modal]], 가벼운 미리보기는 [[components/popover]].
5
5
 
6
6
  ### Do
7
- - **본문과 연결된 보조 작업**에 쓴다 — 선택한 항목의 상세·편집, 필터 그룹, 설정 패널, 알림 센터.
8
- - `title` 필수. 명사형 짧게(상세 보기 · 알림 · 필터). 질문형은 `description`으로.
9
- - 설명 문구는 `description` prop. 헤더 영역 안에서 title 바로 아래 바짝 붙어 하나의 "소개" 묶음으로 읽힘. `children`에 `<p>`로 설명 넣지 않는다.
10
- - children은 자유 ReactNode — Modal보다 세로 길이에 여유가 있어 긴 폼·리스트·탭도 OK. 세로 스크롤은 body가 담당.
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`)은 **용도에 맞게 하나**로 고정: 상세·편집·설정은 `right`, 네비게이션·목록은 `left`, 모바일 액션/필터 바텀시트는 `bottom`, 알림·커맨드는 `top`.
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. Drawer는 흐름을 **잇는** 보조 패널.
19
- - Drawer 안에 Drawer 금지 — ESC가 어느 쪽을 닫는지 모호해지고 side가 같으면 겹침.
20
- - 세로가 짧은 짧은 알림·메시지에 Drawer 금지. 그건 [[components/toast]] 자리.
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
- - `side` 아무 방향에나 열지 않는다 — 한 제품/맥락 안에서 **용도별로 하나의 방향**만 쓴다. 같은 페이지에서 right/left/bottom을 섞으면 멀미.
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
- - 세로가 짧고 상단 커맨드 느낌이면 → `side="top"`.
31
- - 애매하면 `side="right"`, `size="sm"`, `backdrop="scrim"`.
30
+ - 애매하면 `side="right"`, `size="md"`, `backdrop="scrim"`.
32
31
 
33
32
  ### 관련
34
- - [[components/modal]] — 흐름 중단·결정 집중. Drawer는 흐름 유지.
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} // 헤더 영역 안, title 바로 아래 한두 줄
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" | "top" | "bottom" // default: right
55
- size="sm" | "md" | "lg" // default: sm
56
- backdrop="scrim" | "locked" | "none" // default: scrim
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
- | | `top` | 상단에서 내려옴. 알림·커맨드 팔레트 — 드문 자리. |
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 | right/left (width) | top/bottom (height) | 언제 |
80
- |---|---|---|---|
81
- | `sm` | 352 | 288 | **기본.** 상세 보기·알림 센터·간단한 편집. 데이터·개발자 도구일수록 유지. |
82
- | `md` | 440 | 400 | 폼이 길거나 리스트 항목이 많을 때. |
83
- | `lg` | 576 | 512 | 복잡한 편집·탭 포함 보조 뷰. 이보다 넓어야 하면 페이지 자리. |
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`의 Drawer 혼용 — 방향 통일.
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
- <DescriptionList>…</DescriptionList>
104
+ {/* children 자유 — 리스트·탭·폼·이미지 어떤 것이든 */}
107
105
  </Drawer>
108
106
 
109
- // 알림 센터 — non-modal, 뒤 계속 작업
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
- <List>…</List>
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
- <ProgressBar value={progress} />
130
+ {/* ProgressBar 등 */}
151
131
  </Drawer>
152
132
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conscience-technology/ctds",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CTDS — 하이브리드 디자인 시스템 설치 · 온보딩 CLI",
5
5
  "type": "module",
6
6
  "bin": {