@uniai-fe/uds-primitives 0.3.19 → 0.3.21

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.
@@ -31,9 +31,7 @@ const normalizeSelectedIdsByMode = (
31
31
  * @param {DropdownTemplateProps} props Dropdown template props
32
32
  * @param {ReactNode} props.trigger trigger 요소
33
33
  * @param {DropdownTemplateItem[]} props.items 렌더링할 menu item 리스트
34
- * @param {string[]} [props.selectedIds] 선택된 item id 배열
35
34
  * @param {(payload: DropdownTemplateChangePayload) => void} [props.onChange] 선택 결과 변경 콜백
36
- * @param {(item: DropdownTemplateItem) => void} [props.onSelect] item 선택 콜백
37
35
  * @param {"small" | "medium" | "large"} [props.size="medium"] menu size scale
38
36
  * @param {"match" | "fit-content" | "max-content" | string | number} [props.width="match"] panel width 옵션
39
37
  * @param {DropdownMenuProps} [props.rootProps] Dropdown.Root 전달 props
@@ -44,9 +42,7 @@ const normalizeSelectedIdsByMode = (
44
42
  const DropdownTemplate = ({
45
43
  trigger,
46
44
  items,
47
- selectedIds,
48
45
  onChange,
49
- onSelect,
50
46
  size = "medium",
51
47
  width = "match",
52
48
  rootProps,
@@ -55,15 +51,7 @@ const DropdownTemplate = ({
55
51
  alt,
56
52
  }: DropdownTemplateProps) => {
57
53
  /**
58
- * 1) 선택 상태 모드 결정
59
- * - selectedIds prop이 들어오면 외부가 상태를 소유하는 controlled 모드다.
60
- * - selectedIds prop이 없으면 Template 내부 state가 상태를 소유하는 uncontrolled 모드다.
61
- * - 이 분기 기준은 렌더마다 동일해야 하므로 가장 먼저 계산한다.
62
- */
63
- const isSelectionControlled = selectedIds !== undefined;
64
-
65
- /**
66
- * 2) 선택 정책(single/multiple) 결정
54
+ * 1) 선택 정책(single/multiple) 결정
67
55
  * - item 중 하나라도 multiple=true이면 multiple 정책으로 해석한다.
68
56
  * - multiple 정책에서는 item id 배열 전체를 유지한다.
69
57
  * - single 정책에서는 언제나 최대 1개 id만 유지한다.
@@ -71,7 +59,7 @@ const DropdownTemplate = ({
71
59
  const isMultiple = items.some(item => item.multiple);
72
60
 
73
61
  /**
74
- * 3) uncontrolled 초기 선택값 계산
62
+ * 2) 초기 선택값 계산
75
63
  * - source of truth: items[].selected
76
64
  * - multiple: selected=true인 모든 id를 초기값으로 사용한다.
77
65
  * - single: selected=true가 여러 개여도 첫 번째 id 하나만 사용한다.
@@ -86,9 +74,8 @@ const DropdownTemplate = ({
86
74
  }, [isMultiple, items]);
87
75
 
88
76
  /**
89
- * 4) uncontrolled 내부 state 선언
90
- * - controlled 모드에서는 state가 실제 화면 결정에 사용되지 않는다.
91
- * - uncontrolled 모드에서는 이 state가 선택 결과의 단일 source가 된다.
77
+ * 3) 내부 선택 state 선언
78
+ * - Template은 items[].selected를 초기값으로 받아 내부 상태를 관리한다.
92
79
  * - 초기값은 selectedIdsFromItems를 그대로 사용한다.
93
80
  */
94
81
  const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState<
@@ -96,19 +83,14 @@ const DropdownTemplate = ({
96
83
  >(() => selectedIdsFromItems);
97
84
 
98
85
  /**
99
- * 5) options(items) 변경 동기화
100
- * - controlled 모드에서는 외부 selectedIds신뢰해야 하므로 내부 state를 건드리지 않는다.
101
- * - uncontrolled 모드에서는 내부 state를 items 변경에 맞춰 정합성 보정한다.
86
+ * 4) options(items) 변경 동기화
87
+ * - 내부 stateitems 변경에 맞춰 정합성 보정한다.
102
88
  * 1) 기존 선택 id 중 현재 items에 존재하는 id만 필터링한다.
103
89
  * 2) 유효 id가 남아 있으면 그대로 유지한다(single은 1개만 유지).
104
90
  * 3) 유효 id가 없으면 selectedIdsFromItems(초기 선택 규칙)로 재초기화한다.
105
91
  * - 이 effect는 외부 데이터 재조회/필터 변경 시 stale id를 자동 정리하는 역할을 한다.
106
92
  */
107
93
  useEffect(() => {
108
- if (isSelectionControlled) {
109
- return;
110
- }
111
-
112
94
  setUncontrolledSelectedIds(previousSelectedIds => {
113
95
  const itemIdSet = new Set(items.map(item => item.id));
114
96
  const filteredIds = previousSelectedIds.filter(selectedId =>
@@ -128,38 +110,36 @@ const DropdownTemplate = ({
128
110
 
129
111
  return nextSelectedIds;
130
112
  });
131
- }, [isMultiple, isSelectionControlled, items, selectedIdsFromItems]);
113
+ }, [isMultiple, items, selectedIdsFromItems]);
132
114
 
133
115
  /**
134
- * 6) 최종 선택 id 계산
135
- * - controlled: selectedIds prop을 그대로 사용한다(single id만 허용).
136
- * - uncontrolled: 내부 state(uncontrolledSelectedIds)를 그대로 사용한다.
116
+ * 5) 최종 선택 id 계산
117
+ * - 내부 state(uncontrolledSelectedIds)를 single/multiple 정책으로 정규화한다.
137
118
  * - 렌더/이벤트/selected 스타일 계산은 이 값만 참조한다.
138
119
  */
139
120
  const resolvedSelectedIds = normalizeSelectedIdsByMode(
140
- isSelectionControlled ? (selectedIds ?? []) : uncontrolledSelectedIds,
121
+ uncontrolledSelectedIds,
141
122
  isMultiple,
142
123
  );
143
124
 
144
125
  /**
145
- * 7) empty panel 분기
126
+ * 6) empty panel 분기
146
127
  * - item이 비어 있으면 alt 또는 기본 문구를 disabled item으로 렌더링한다.
147
128
  * - 별도 li를 만들지 않고 MenuItem을 재사용해 구조를 통일한다.
148
129
  */
149
130
  const hasItems = items.length > 0;
150
131
 
151
132
  /**
152
- * 8) item 선택 처리
133
+ * 7) item 선택 처리
153
134
  * - 클릭된 item(id) 기준으로 다음 선택 배열을 계산한다.
154
135
  * - multiple: 토글(add/remove)
155
136
  * - single: 클릭한 id 하나로 교체
156
- * - uncontrolled 모드면 내부 state를 즉시 반영한다.
137
+ * - 내부 state를 즉시 반영한다.
157
138
  * - onChange(payload)로 "현재 결과 전체"를 전달한다.
158
- * - onSelect(item)은 하위호환을 위해 마지막에 유지 호출한다.
159
139
  */
160
140
  const handleItemSelect = (itemId: string) => {
161
141
  /**
162
- * 8-1) 클릭된 item 조회
142
+ * 7-1) 클릭된 item 조회
163
143
  * - items 목록에 없는 id면 안전하게 종료한다.
164
144
  * - 외부 데이터 race 조건에서 방어적으로 동작하기 위한 가드다.
165
145
  */
@@ -170,7 +150,7 @@ const DropdownTemplate = ({
170
150
  }
171
151
 
172
152
  /**
173
- * 8-2) 다음 selectedIds 계산
153
+ * 7-2) 다음 selectedIds 계산
174
154
  * - wasSelected: 토글 기준 값
175
155
  * - nextSelectedIds: 모드 정책을 적용한 다음 상태
176
156
  * - nextSelectedItems: payload 전달용 상세 데이터
@@ -186,16 +166,13 @@ const DropdownTemplate = ({
186
166
  );
187
167
 
188
168
  /**
189
- * 8-3) 내부 state 업데이트(uncontrolled 전용)
190
- * - controlled 모드에서는 외부 selectedIds prop이 곧 source of truth이므로
191
- * 내부 state를 갱신하지 않는다.
169
+ * 7-3) 내부 state 업데이트
170
+ * - Template 내부 선택 상태를 즉시 반영한다.
192
171
  */
193
- if (!isSelectionControlled) {
194
- setUncontrolledSelectedIds(nextSelectedIds);
195
- }
172
+ setUncontrolledSelectedIds(nextSelectedIds);
196
173
 
197
174
  /**
198
- * 8-4) 신규 계약 이벤트(onChange)
175
+ * 7-4) 선택 결과 이벤트(onChange)
199
176
  * - 서비스/상위 컴포넌트가 선택 결과 전체를 즉시 사용할 수 있게
200
177
  * 현재 item + 선택 결과 배열을 함께 제공한다.
201
178
  */
@@ -206,17 +183,10 @@ const DropdownTemplate = ({
206
183
  selectedItems: nextSelectedItems,
207
184
  multiple: isMultiple,
208
185
  });
209
-
210
- /**
211
- * 8-5) legacy 계약 이벤트(onSelect)
212
- * - 하위호환 경로를 유지하기 위해 onChange 이후 동일 선택을 전달한다.
213
- * - 추후 마이그레이션 완료 시 제거 대상이다.
214
- */
215
- onSelect?.(currentItem);
216
186
  };
217
187
 
218
188
  /**
219
- * 9) 렌더 구성
189
+ * 8) 렌더 구성
220
190
  * - Root → Trigger → Container → MenuList depth를 유지한다.
221
191
  * - 각 item은 resolvedSelectedIds 기준으로 selected 스타일을 결정한다.
222
192
  * - disabled item은 select 이벤트를 차단하고 상태만 노출한다.
@@ -1,39 +1,60 @@
1
1
  .dropdown-panel {
2
2
  display: flex;
3
3
  flex-direction: column;
4
- gap: var(--dropdown-panel-gap);
5
- padding: var(--dropdown-panel-padding);
6
- border-radius: var(--dropdown-panel-radius);
4
+ gap: var(--dropdown-panel-gap-selected);
5
+ padding: var(--dropdown-panel-padding-selected);
6
+ border-radius: var(--dropdown-panel-radius-selected);
7
7
  border: 1px solid var(--dropdown-panel-border-color);
8
8
  background-color: var(--dropdown-panel-background);
9
- box-shadow: var(--dropdown-panel-shadow);
9
+ box-shadow: var(--dropdown-panel-shadow-selected);
10
10
  max-height: var(--dropdown-panel-max-height);
11
11
  overflow-y: auto;
12
12
  }
13
13
 
14
+ .dropdown-panel-xsmall {
15
+ --dropdown-option-height-selected: var(--dropdown-option-height-xsmall);
16
+ --dropdown-text-size-selected: var(--dropdown-text-xsmall-size);
17
+ --dropdown-text-line-height-selected: var(--dropdown-text-xsmall-line-height);
18
+ --dropdown-text-letter-spacing-selected: var(
19
+ --dropdown-text-xsmall-letter-spacing
20
+ );
21
+ --dropdown-option-padding-inline-selected: var(
22
+ --dropdown-option-padding-inline-xsmall
23
+ );
24
+ --dropdown-option-padding-block-selected: var(
25
+ --dropdown-option-padding-block-xsmall
26
+ );
27
+ --dropdown-option-radius-selected: var(--dropdown-option-radius-xsmall);
28
+ --dropdown-panel-padding-selected: var(--dropdown-panel-padding-xsmall);
29
+ --dropdown-panel-gap-selected: var(--dropdown-panel-gap-xsmall);
30
+ --dropdown-panel-radius-selected: var(--dropdown-panel-radius-xsmall);
31
+ --dropdown-panel-shadow-selected: var(--dropdown-panel-shadow-xsmall);
32
+ --dropdown-text-weight-selected: var(--dropdown-text-weight-selected-xsmall);
33
+ }
34
+
14
35
  .dropdown-panel-small {
15
- --dropdown-option-height-current: var(--dropdown-option-height-small);
16
- --dropdown-text-size-current: var(--dropdown-text-small-size);
17
- --dropdown-text-line-height-current: var(--dropdown-text-small-line-height);
18
- --dropdown-text-letter-spacing-current: var(
36
+ --dropdown-option-height-selected: var(--dropdown-option-height-small);
37
+ --dropdown-text-size-selected: var(--dropdown-text-small-size);
38
+ --dropdown-text-line-height-selected: var(--dropdown-text-small-line-height);
39
+ --dropdown-text-letter-spacing-selected: var(
19
40
  --dropdown-text-small-letter-spacing
20
41
  );
21
42
  }
22
43
 
23
44
  .dropdown-panel-medium {
24
- --dropdown-option-height-current: var(--dropdown-option-height-medium);
25
- --dropdown-text-size-current: var(--dropdown-text-medium-size);
26
- --dropdown-text-line-height-current: var(--dropdown-text-medium-line-height);
27
- --dropdown-text-letter-spacing-current: var(
45
+ --dropdown-option-height-selected: var(--dropdown-option-height-medium);
46
+ --dropdown-text-size-selected: var(--dropdown-text-medium-size);
47
+ --dropdown-text-line-height-selected: var(--dropdown-text-medium-line-height);
48
+ --dropdown-text-letter-spacing-selected: var(
28
49
  --dropdown-text-medium-letter-spacing
29
50
  );
30
51
  }
31
52
 
32
53
  .dropdown-panel-large {
33
- --dropdown-option-height-current: var(--dropdown-option-height-large);
34
- --dropdown-text-size-current: var(--dropdown-text-large-size);
35
- --dropdown-text-line-height-current: var(--dropdown-text-large-line-height);
36
- --dropdown-text-letter-spacing-current: var(
54
+ --dropdown-option-height-selected: var(--dropdown-option-height-large);
55
+ --dropdown-text-size-selected: var(--dropdown-text-large-size);
56
+ --dropdown-text-line-height-selected: var(--dropdown-text-large-line-height);
57
+ --dropdown-text-letter-spacing-selected: var(
37
58
  --dropdown-text-large-letter-spacing
38
59
  );
39
60
  }
@@ -61,12 +82,12 @@
61
82
  gap: var(--dropdown-option-gap-inline);
62
83
  width: 100%;
63
84
  min-height: var(
64
- --dropdown-option-height-current,
85
+ --dropdown-option-height-selected,
65
86
  var(--dropdown-option-height-medium)
66
87
  );
67
- padding: var(--dropdown-option-padding-block)
68
- var(--dropdown-option-padding-inline);
69
- border-radius: var(--dropdown-option-radius);
88
+ padding: var(--dropdown-option-padding-block-selected)
89
+ var(--dropdown-option-padding-inline-selected);
90
+ border-radius: var(--dropdown-option-radius-selected);
70
91
  background-color: transparent;
71
92
  color: var(--dropdown-option-color);
72
93
  cursor: pointer;
@@ -132,15 +153,15 @@
132
153
  min-width: 0;
133
154
  color: inherit;
134
155
  font-size: var(
135
- --dropdown-text-size-current,
156
+ --dropdown-text-size-selected,
136
157
  var(--dropdown-text-medium-size)
137
158
  );
138
159
  line-height: var(
139
- --dropdown-text-line-height-current,
160
+ --dropdown-text-line-height-selected,
140
161
  var(--dropdown-text-medium-line-height)
141
162
  );
142
163
  letter-spacing: var(
143
- --dropdown-text-letter-spacing-current,
164
+ --dropdown-text-letter-spacing-selected,
144
165
  var(--dropdown-text-medium-letter-spacing)
145
166
  );
146
167
  font-weight: var(--dropdown-text-weight);
@@ -6,11 +6,27 @@
6
6
  --dropdown-panel-padding: var(--spacing-padding-3);
7
7
  --dropdown-panel-gap: var(--spacing-gap-2);
8
8
  --dropdown-panel-max-height: 32rem;
9
+ --dropdown-panel-radius-selected: var(--dropdown-panel-radius);
10
+ --dropdown-panel-shadow-selected: var(--dropdown-panel-shadow);
11
+ --dropdown-panel-padding-selected: var(--dropdown-panel-padding);
12
+ --dropdown-panel-gap-selected: var(--dropdown-panel-gap);
13
+ --dropdown-panel-radius-xsmall: var(--theme-radius-medium-2);
14
+ --dropdown-panel-shadow-xsmall: 2px 2px 8px rgba(0, 0, 0, 0.05);
15
+ --dropdown-panel-padding-xsmall: var(--spacing-padding-4)
16
+ var(--spacing-padding-3);
17
+ --dropdown-panel-gap-xsmall: var(--spacing-gap-2);
9
18
 
10
19
  --dropdown-option-radius: var(--theme-radius-medium-1);
11
20
  --dropdown-option-gap-inline: var(--spacing-gap-3);
12
21
  --dropdown-option-padding-inline: var(--spacing-padding-6);
13
22
  --dropdown-option-padding-block: var(--spacing-padding-2);
23
+ --dropdown-option-radius-selected: var(--dropdown-option-radius);
24
+ --dropdown-option-padding-inline-selected: var(
25
+ --dropdown-option-padding-inline
26
+ );
27
+ --dropdown-option-padding-block-selected: var(
28
+ --dropdown-option-padding-block
29
+ );
14
30
  --dropdown-option-color: var(--color-label-standard);
15
31
  --dropdown-option-color-hover: var(--color-label-strong);
16
32
  --dropdown-option-color-selected: var(--color-primary-default);
@@ -22,6 +38,9 @@
22
38
  --dropdown-text-small-size: 15px;
23
39
  --dropdown-text-small-line-height: 24px;
24
40
  --dropdown-text-small-letter-spacing: 0;
41
+ --dropdown-text-xsmall-size: 12px;
42
+ --dropdown-text-xsmall-line-height: 18px;
43
+ --dropdown-text-xsmall-letter-spacing: 0;
25
44
  --dropdown-text-medium-size: 16px;
26
45
  --dropdown-text-medium-line-height: 24px;
27
46
  --dropdown-text-medium-letter-spacing: 0;
@@ -34,7 +53,13 @@
34
53
  --dropdown-description-size: 14px;
35
54
  --dropdown-description-line-height: 22px;
36
55
 
56
+ --dropdown-option-height-selected: var(--dropdown-option-height-medium);
57
+ --dropdown-option-height-xsmall: 24px;
37
58
  --dropdown-option-height-small: var(--theme-size-medium-1, 40px);
38
59
  --dropdown-option-height-medium: var(--theme-size-medium-2, 48px);
39
60
  --dropdown-option-height-large: var(--theme-size-medium-3, 56px);
61
+ --dropdown-option-padding-inline-xsmall: var(--spacing-padding-3);
62
+ --dropdown-option-padding-block-xsmall: 0px;
63
+ --dropdown-option-radius-xsmall: 4px;
64
+ --dropdown-text-weight-selected-xsmall: var(--dropdown-text-weight);
40
65
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Dropdown size scale
3
- * @typedef {"small" | "medium" | "large"} DropdownSize
3
+ * @typedef {"xsmall" | "small" | "medium" | "large"} DropdownSize
4
4
  */
5
- export type DropdownSize = "small" | "medium" | "large";
5
+ export type DropdownSize = "xsmall" | "small" | "medium" | "large";
6
6
 
7
7
  /**
8
8
  * Dropdown panel width 옵션
@@ -167,9 +167,7 @@ export interface DropdownTemplateChangePayload {
167
167
  * Dropdown template props
168
168
  * @property {ReactNode} trigger trigger 요소
169
169
  * @property {DropdownTemplateItem[]} items 렌더링할 menu item 리스트
170
- * @property {string[]} [selectedIds] 선택된 item id 배열
171
170
  * @property {(payload: DropdownTemplateChangePayload) => void} [onChange] 선택 결과 변경 콜백
172
- * @property {(item: DropdownTemplateItem) => void} [onSelect] item 선택 콜백
173
171
  * @property {DropdownSize} [size="medium"] surface height scale
174
172
  * @property {DropdownPanelWidth} [width="match"] panel width 옵션
175
173
  * @property {DropdownMenuProps} [rootProps] Root 에 전달할 props
@@ -180,20 +178,10 @@ export interface DropdownTemplateChangePayload {
180
178
  export interface DropdownTemplateProps {
181
179
  trigger: ReactNode;
182
180
  items: DropdownTemplateItem[];
183
- /**
184
- * 선택된 item id 배열
185
- * @deprecated onChange + items[].selected 기반 계약을 우선 사용한다.
186
- */
187
- selectedIds?: string[];
188
181
  /**
189
182
  * 선택 결과 변경 콜백
190
183
  */
191
184
  onChange?: (payload: DropdownTemplateChangePayload) => void;
192
- /**
193
- * item 선택 콜백
194
- * @deprecated onChange로 대체되며 하위호환을 위해 유지된다.
195
- */
196
- onSelect?: (item: DropdownTemplateItem) => void;
197
185
  size?: DropdownSize;
198
186
  width?: DropdownPanelWidth;
199
187
  /**
@@ -13,7 +13,7 @@ import type {
13
13
  * @param {UseSelectDropdownOpenStateParams} params open 제어 옵션
14
14
  * @param {boolean} [params.open] 외부 제어형 open 상태
15
15
  * @param {boolean} [params.defaultOpen] 비제어형 초기 open 상태
16
- * @param {(open: boolean) => void} [params.onOpenChange] open 상태 변경 콜백
16
+ * @param {(open: boolean) => void} [params.onOpen] open 상태 변경 콜백
17
17
  * @returns {{
18
18
  * open: boolean;
19
19
  * setOpen: (next: boolean) => void;
@@ -23,13 +23,13 @@ import type {
23
23
  * const { open, setOpen } = useSelectDropdownOpenState({
24
24
  * open: controlledOpen,
25
25
  * defaultOpen: false,
26
- * onOpenChange: onOpenChangeHandler,
26
+ * onOpen: onOpenHandler,
27
27
  * });
28
28
  */
29
29
  export const useSelectDropdownOpenState = ({
30
30
  open,
31
31
  defaultOpen,
32
- onOpenChange,
32
+ onOpen,
33
33
  }: UseSelectDropdownOpenStateParams): UseSelectDropdownOpenStateReturn => {
34
34
  // 1) 제어형/비제어형 분기 기준을 먼저 확정한다.
35
35
  const isControlled = useMemo(() => typeof open === "boolean", [open]);
@@ -53,9 +53,9 @@ export const useSelectDropdownOpenState = ({
53
53
  (nextOpen: boolean) => {
54
54
  if (!isControlled) setUncontrolledOpen(nextOpen);
55
55
 
56
- onOpenChange?.(nextOpen);
56
+ onOpen?.(nextOpen);
57
57
  },
58
- [isControlled, onOpenChange],
58
+ [isControlled, onOpen],
59
59
  );
60
60
 
61
61
  return { open: resolvedOpen, setOpen, isControlled };
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M4.17139 6L6.99981 8.82843L9.82824 6" stroke="#BCBEC2" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>