@uniai-fe/uds-primitives 0.2.8 → 0.2.10

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 (72) hide show
  1. package/dist/styles.css +419 -0
  2. package/package.json +2 -1
  3. package/src/components/calendar/types/calendar.ts +5 -0
  4. package/src/components/dropdown/markup/Template.tsx +41 -17
  5. package/src/components/dropdown/markup/foundation/Container.tsx +14 -2
  6. package/src/components/dropdown/markup/foundation/MenuItem.tsx +20 -6
  7. package/src/components/dropdown/markup/foundation/Root.tsx +8 -1
  8. package/src/components/dropdown/markup/foundation/Trigger.tsx +7 -1
  9. package/src/components/dropdown/styles/dropdown.scss +4 -0
  10. package/src/components/dropdown/types/props.ts +5 -0
  11. package/src/components/input/markup/date/Template.tsx +36 -5
  12. package/src/components/input/markup/date/Trigger.tsx +22 -4
  13. package/src/components/input/markup/foundation/Input.tsx +19 -11
  14. package/src/components/input/markup/foundation/Utility.tsx +11 -7
  15. package/src/components/input/styles/date.scss +21 -0
  16. package/src/components/input/styles/foundation.scss +30 -0
  17. package/src/components/input/styles/variables.scss +11 -0
  18. package/src/components/input/types/date.ts +15 -0
  19. package/src/components/input/types/foundation.ts +18 -11
  20. package/src/components/input/utils/date.ts +15 -1
  21. package/src/components/select/hooks/index.ts +1 -45
  22. package/src/components/select/hooks/interaction.ts +62 -0
  23. package/src/components/select/markup/Default.tsx +59 -35
  24. package/src/components/select/markup/foundation/Base.tsx +12 -4
  25. package/src/components/select/markup/foundation/Container.tsx +37 -34
  26. package/src/components/select/markup/foundation/Icon.tsx +6 -1
  27. package/src/components/select/markup/multiple/Multiple.tsx +62 -35
  28. package/src/components/select/markup/multiple/SelectedChip.tsx +5 -2
  29. package/src/components/select/styles/select.scss +50 -0
  30. package/src/components/select/styles/variables.scss +26 -0
  31. package/src/components/select/types/base.ts +3 -2
  32. package/src/components/select/types/icon.ts +7 -6
  33. package/src/components/select/types/index.ts +1 -0
  34. package/src/components/select/types/interaction.ts +30 -0
  35. package/src/components/select/types/props.ts +8 -0
  36. package/src/components/select/types/trigger.ts +4 -0
  37. package/src/components/table/hooks/index.ts +0 -3
  38. package/src/components/table/index.tsx +5 -3
  39. package/src/components/table/markup/Container.tsx +126 -0
  40. package/src/components/table/markup/foundation/Body.tsx +24 -0
  41. package/src/components/table/markup/foundation/Cell.tsx +72 -0
  42. package/src/components/table/markup/foundation/Col.tsx +22 -0
  43. package/src/components/table/markup/foundation/Colgroup.tsx +29 -0
  44. package/src/components/table/markup/foundation/Foot.tsx +24 -0
  45. package/src/components/table/markup/foundation/Head.tsx +24 -0
  46. package/src/components/table/markup/foundation/Root.tsx +32 -0
  47. package/src/components/table/markup/foundation/Row.tsx +32 -0
  48. package/src/components/table/markup/foundation/Td.tsx +37 -0
  49. package/src/components/table/markup/foundation/Th.tsx +39 -0
  50. package/src/components/table/markup/foundation/index.tsx +30 -0
  51. package/src/components/table/markup/index.tsx +8 -2
  52. package/src/components/table/styles/foundation.scss +247 -0
  53. package/src/components/table/styles/index.scss +2 -0
  54. package/src/components/table/styles/variables.scss +29 -0
  55. package/src/components/table/types/foundation.ts +250 -0
  56. package/src/components/table/types/index.ts +1 -4
  57. package/src/components/tooltip/img/info.svg +5 -0
  58. package/src/components/tooltip/img/information.svg +9 -0
  59. package/src/components/tooltip/index.scss +1 -0
  60. package/src/components/tooltip/index.tsx +4 -0
  61. package/src/components/tooltip/markup/Message.tsx +70 -0
  62. package/src/components/tooltip/markup/Root.tsx +32 -0
  63. package/src/components/tooltip/markup/Template.tsx +46 -0
  64. package/src/components/tooltip/markup/Trigger.tsx +32 -0
  65. package/src/components/tooltip/markup/index.tsx +18 -0
  66. package/src/components/tooltip/styles/index.scss +2 -0
  67. package/src/components/tooltip/styles/tooltip.scss +47 -0
  68. package/src/components/tooltip/styles/variables.scss +14 -0
  69. package/src/components/tooltip/types/index.ts +1 -0
  70. package/src/components/tooltip/types/props.ts +118 -0
  71. package/src/index.scss +1 -0
  72. package/src/index.tsx +1 -0
@@ -10,7 +10,7 @@ import type { FormFieldWidth } from "../../form/types/props";
10
10
  /**
11
11
  * input; priority option
12
12
  */
13
- export type InputPriority = "primary" | "secondary" | "tertiary";
13
+ export type InputPriority = "primary" | "secondary" | "tertiary" | "table";
14
14
  /**
15
15
  * input; size option
16
16
  */
@@ -34,6 +34,7 @@ export const INPUT_PRIORITIES: InputPriority[] = [
34
34
  "primary",
35
35
  "secondary",
36
36
  "tertiary",
37
+ "table",
37
38
  ];
38
39
  /**
39
40
  * size 축은 높이/타이포/spacing을 결정한다.
@@ -56,11 +57,11 @@ type NativeInputProps = ComponentPropsWithoutRef<"input">;
56
57
 
57
58
  /**
58
59
  * input; icon options
59
- * @property {React.ReactNode} [left] input 왼쪽 컨텐츠
60
- * @property {React.ReactNode} [right] input 오른쪽 컨텐츠
61
- * @property {React.ReactNode} [clear] input reset버튼 커스텀 컨텐츠
62
- * @property {React.ReactNode} [success] input 입력상태 성공시 커스텀 컨텐츠
63
- * @property {React.ReactNode} [error] input 입력상태 에러시 커스텀 컨텐츠
60
+ * @property {ReactNode} [left] input 왼쪽 컨텐츠
61
+ * @property {ReactNode} [right] input 오른쪽 컨텐츠
62
+ * @property {ReactNode} [clear] input reset버튼 커스텀 컨텐츠
63
+ * @property {ReactNode} [success] input 입력상태 성공시 커스텀 컨텐츠
64
+ * @property {ReactNode} [error] input 입력상태 에러시 커스텀 컨텐츠
64
65
  */
65
66
  export interface InputIcon {
66
67
  /**
@@ -95,12 +96,13 @@ export interface InputIcon {
95
96
  * @property {string} [inputClassName]
96
97
  * @property {string} [boxClassName]
97
98
  * @property {UseFormRegisterReturn} [register]
98
- * @property {React.ReactNode} [left] input 왼쪽 컨텐츠
99
- * @property {React.ReactNode} [right] input 오른쪽 컨텐츠
100
- * @property {React.ReactNode} [clear] input reset버튼 커스텀 컨텐츠
101
- * @property {React.ReactNode} [success] input 입력상태 성공시 커스텀 컨텐츠
102
- * @property {React.ReactNode} [error] input 입력상태 에러시 커스텀 컨텐츠
99
+ * @property {ReactNode} [left] input 왼쪽 컨텐츠
100
+ * @property {ReactNode} [right] input 오른쪽 컨텐츠
101
+ * @property {ReactNode} [clear] input reset버튼 커스텀 컨텐츠
102
+ * @property {ReactNode} [success] input 입력상태 성공시 커스텀 컨텐츠
103
+ * @property {ReactNode} [error] input 입력상태 에러시 커스텀 컨텐츠
103
104
  * @property {FormFieldWidth} [width] width preset 옵션
105
+ * @property {InputState} [data-simulated-state] Storybook 시각 상태 강제용
104
106
  */
105
107
  export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
106
108
  /**
@@ -147,6 +149,7 @@ export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
147
149
  * @property {ReactNode} [clear] clear 버튼 아이콘
148
150
  * @property {ReactNode} [success] success 상태 아이콘
149
151
  * @property {ReactNode} [error] error 상태 아이콘
152
+ * @property {InputPriority} priority input priority
150
153
  * @property {InputState} state 현재 상태
151
154
  * @property {boolean} isDisabled disabled 여부
152
155
  * @property {boolean} isFocused focus 여부
@@ -171,6 +174,10 @@ export interface InputUtilityProps {
171
174
  * error 상태 아이콘
172
175
  */
173
176
  error?: ReactNode;
177
+ /**
178
+ * priority 축
179
+ */
180
+ priority: InputPriority;
174
181
  /**
175
182
  * 현재 input 상태
176
183
  */
@@ -51,7 +51,21 @@ export const serializeCalendarValue = (value: CalendarValue) => value ?? "";
51
51
  * @param {CalendarValue} value 현재 값
52
52
  * @returns {string} 표시 문자열
53
53
  */
54
- export const formatTriggerValue = (value: CalendarValue) => value ?? "";
54
+ export const formatTriggerValue = (
55
+ value: CalendarValue,
56
+ format = DATE_FORMAT,
57
+ ) => {
58
+ if (!value) {
59
+ return "";
60
+ }
61
+
62
+ const parsed = dayjs(value);
63
+ if (!parsed.isValid()) {
64
+ return value;
65
+ }
66
+
67
+ return parsed.format(format);
68
+ };
55
69
 
56
70
  /**
57
71
  * columns 값을 Mantine numberOfColumns와 맞춘다.
@@ -1,45 +1 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
2
-
3
- import type { SelectDropdownBehaviorProps } from "../types/props";
4
-
5
- /**
6
- * Select dropdown open 상태를 제어하는 hook
7
- * @hook
8
- * @param {SelectDropdownBehaviorProps} props open 제어 옵션
9
- * @returns {{
10
- * open: boolean;
11
- * setOpen: (next: boolean) => void;
12
- * isControlled: boolean;
13
- * }} resolved open state
14
- */
15
- export const useSelectDropdownOpenState = ({
16
- open,
17
- defaultOpen,
18
- onOpenChange,
19
- }: SelectDropdownBehaviorProps) => {
20
- const isControlled = useMemo(() => typeof open === "boolean", [open]);
21
- const [uncontrolledOpen, setUncontrolledOpen] = useState(
22
- defaultOpen ?? false,
23
- );
24
-
25
- useEffect(() => {
26
- if (isControlled) {
27
- return;
28
- }
29
- setUncontrolledOpen(defaultOpen ?? false);
30
- }, [defaultOpen, isControlled]);
31
-
32
- const resolvedOpen = isControlled ? (open as boolean) : uncontrolledOpen;
33
-
34
- const setOpen = useCallback(
35
- (nextOpen: boolean) => {
36
- if (!isControlled) {
37
- setUncontrolledOpen(nextOpen);
38
- }
39
- onOpenChange?.(nextOpen);
40
- },
41
- [isControlled, onOpenChange],
42
- );
43
-
44
- return { open: resolvedOpen, setOpen, isControlled };
45
- };
1
+ export * from "./interaction";
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+
5
+ import type {
6
+ UseSelectDropdownOpenStateParams,
7
+ UseSelectDropdownOpenStateReturn,
8
+ } from "../types/interaction";
9
+
10
+ /**
11
+ * Select Hook; Dropdown open 상태 제어 Hook
12
+ * @hook
13
+ * @param {UseSelectDropdownOpenStateParams} params open 제어 옵션
14
+ * @param {boolean} [params.open] 외부 제어형 open 상태
15
+ * @param {boolean} [params.defaultOpen] 비제어형 초기 open 상태
16
+ * @param {(open: boolean) => void} [params.onOpenChange] open 상태 변경 콜백
17
+ * @returns {{
18
+ * open: boolean;
19
+ * setOpen: (next: boolean) => void;
20
+ * isControlled: boolean;
21
+ * }} resolved open state
22
+ * @example
23
+ * const { open, setOpen } = useSelectDropdownOpenState({
24
+ * open: controlledOpen,
25
+ * defaultOpen: false,
26
+ * onOpenChange: onOpenChangeHandler,
27
+ * });
28
+ */
29
+ export const useSelectDropdownOpenState = ({
30
+ open,
31
+ defaultOpen,
32
+ onOpenChange,
33
+ }: UseSelectDropdownOpenStateParams): UseSelectDropdownOpenStateReturn => {
34
+ // 1) 제어형/비제어형 분기 기준을 먼저 확정한다.
35
+ const isControlled = useMemo(() => typeof open === "boolean", [open]);
36
+ // 2) 비제어형일 때만 내부 open state를 소유한다.
37
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(
38
+ defaultOpen ?? false,
39
+ );
40
+
41
+ // 3) defaultOpen 변경 시, 비제어형일 때만 내부 state를 동기화한다.
42
+ useEffect(() => {
43
+ if (isControlled) return;
44
+
45
+ setUncontrolledOpen(defaultOpen ?? false);
46
+ }, [defaultOpen, isControlled]);
47
+
48
+ // 4) 최종 open state는 제어형 우선, 아니면 내부 state를 사용한다.
49
+ const resolvedOpen = isControlled ? (open as boolean) : uncontrolledOpen;
50
+
51
+ // 5) setOpen은 내부 state 갱신 + 외부 콜백 브릿지를 동시에 담당한다.
52
+ const setOpen = useCallback(
53
+ (nextOpen: boolean) => {
54
+ if (!isControlled) setUncontrolledOpen(nextOpen);
55
+
56
+ onOpenChange?.(nextOpen);
57
+ },
58
+ [isControlled, onOpenChange],
59
+ );
60
+
61
+ return { open: resolvedOpen, setOpen, isControlled };
62
+ };
@@ -18,11 +18,22 @@ import type { SelectDefaultComponentProps } from "../types/props";
18
18
  * @param {SelectDropdownOption[]} [props.options] dropdown option 목록
19
19
  * @param {string[]} [props.selectedOptionIds] 선택된 option id 리스트
20
20
  * @param {(option: SelectDropdownOption) => void} [props.onOptionSelect] option 선택 콜백
21
- * @param {"primary" | "secondary"} [props.priority="primary"] priority 스케일
21
+ * @param {"primary" | "secondary" | "table"} [props.priority="primary"] priority 스케일
22
22
  * @param {"small" | "medium" | "large"} [props.size="medium"] size 스케일
23
23
  * @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
24
24
  * @param {boolean} [props.block] block 여부
25
+ * @param {FormFieldWidth} [props.width] container width preset
25
26
  * @param {boolean} [props.disabled] disabled 여부
27
+ * @param {SelectTriggerButtonType} [props.buttonType] trigger button type
28
+ * @param {"small" | "medium" | "large"} [props.dropdownSize] dropdown panel size
29
+ * @param {"match" | "fit-content" | "max-content" | string | number} [props.dropdownWidth="match"] dropdown panel width
30
+ * @param {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [props.dropdownRootProps] Dropdown.Root 전달 props
31
+ * @param {Omit<DropdownContainerProps, "children" | "size" | "width">} [props.dropdownContainerProps] Dropdown.Container 전달 props
32
+ * @param {DropdownMenuListProps} [props.dropdownMenuListProps] Dropdown.Menu.List 전달 props
33
+ * @param {ReactNode} [props.alt] empty 상태 대체 콘텐츠
34
+ * @param {boolean} [props.open] controlled open 상태
35
+ * @param {boolean} [props.defaultOpen] uncontrolled 초기 open 상태
36
+ * @param {(open: boolean) => void} [props.onOpenChange] open 상태 변경 콜백
26
37
  */
27
38
  const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
28
39
  (
@@ -46,6 +57,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
46
57
  dropdownRootProps,
47
58
  dropdownContainerProps,
48
59
  dropdownMenuListProps,
60
+ alt,
49
61
  open,
50
62
  defaultOpen,
51
63
  onOpenChange,
@@ -53,6 +65,9 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
53
65
  },
54
66
  ref,
55
67
  ) => {
68
+ // 변경: table priority는 width 미지정 시 기본 full width를 사용한다.
69
+ const resolvedBlock =
70
+ block || (priority === "table" && width === undefined);
56
71
  const resolvedSelectedIds = selectedOptionIds ?? [];
57
72
 
58
73
  const resolvedDisplayLabel =
@@ -71,7 +86,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
71
86
  defaultOpen,
72
87
  onOpenChange,
73
88
  });
74
- // Dropdown open 상태를 trigger data-state와 동기화한다.
89
+ // 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
75
90
 
76
91
  const handleOptionSelect = (option: SelectDropdownOption) => {
77
92
  onOptionSelect?.(option);
@@ -79,12 +94,12 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
79
94
  };
80
95
 
81
96
  const panelSize = (dropdownSize ?? size) as DropdownSize;
82
- const shouldRenderDropdown = options.length > 0;
97
+ const hasOptions = options.length > 0;
83
98
 
84
99
  return (
85
100
  <Container
86
101
  className={clsx("select-trigger-container", className)}
87
- block={block}
102
+ block={resolvedBlock}
88
103
  width={width}
89
104
  >
90
105
  <Dropdown.Root
@@ -100,7 +115,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
100
115
  priority={priority}
101
116
  size={size}
102
117
  state={disabled ? "disabled" : state}
103
- block={block}
118
+ block={resolvedBlock}
104
119
  open={dropdownOpen}
105
120
  disabled={disabled}
106
121
  buttonType={buttonType}
@@ -113,36 +128,45 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
113
128
  />
114
129
  </SelectTriggerBase>
115
130
  </Dropdown.Trigger>
116
- {shouldRenderDropdown ? (
117
- <Dropdown.Container
118
- {...dropdownContainerProps}
119
- size={panelSize}
120
- width={dropdownWidth}
121
- >
122
- <Dropdown.Menu.List {...dropdownMenuListProps}>
123
- {/* Dropdown menu option들을 그대로 매핑해 선택 이벤트를 전달한다. */}
124
- {options.map(option => (
125
- <Dropdown.Menu.Item
126
- key={option.id}
127
- label={option.label}
128
- description={option.description}
129
- disabled={option.disabled}
130
- left={option.left}
131
- right={option.right}
132
- multiple={Boolean(option.multiple)}
133
- isSelected={resolvedSelectedIds.includes(option.id)}
134
- onSelect={event => {
135
- if (option.disabled) {
136
- event.preventDefault();
137
- return;
138
- }
139
- handleOptionSelect(option);
140
- }}
141
- />
142
- ))}
143
- </Dropdown.Menu.List>
144
- </Dropdown.Container>
145
- ) : null}
131
+ <Dropdown.Container
132
+ {...dropdownContainerProps}
133
+ size={panelSize}
134
+ width={dropdownWidth}
135
+ >
136
+ <Dropdown.Menu.List {...dropdownMenuListProps}>
137
+ {hasOptions ? (
138
+ <>
139
+ {/* Dropdown menu option들을 그대로 매핑해 선택 이벤트를 전달한다. */}
140
+ {options.map(option => (
141
+ <Dropdown.Menu.Item
142
+ key={option.id}
143
+ label={option.label}
144
+ description={option.description}
145
+ disabled={option.disabled}
146
+ left={option.left}
147
+ right={option.right}
148
+ multiple={Boolean(option.multiple)}
149
+ isSelected={resolvedSelectedIds.includes(option.id)}
150
+ onSelect={event => {
151
+ if (option.disabled) {
152
+ event.preventDefault();
153
+ return;
154
+ }
155
+ handleOptionSelect(option);
156
+ }}
157
+ />
158
+ ))}
159
+ </>
160
+ ) : (
161
+ <Dropdown.Menu.Item
162
+ // 변경: 사용처 1회 상수 대신 인라인 fallback으로 empty label을 처리한다.
163
+ label={alt ?? "선택할 항목이 없습니다."}
164
+ disabled
165
+ className="dropdown-menu-alt"
166
+ />
167
+ )}
168
+ </Dropdown.Menu.List>
169
+ </Dropdown.Container>
146
170
  </Dropdown.Root>
147
171
  </Container>
148
172
  );
@@ -8,16 +8,24 @@ import { SelectIcon } from "./Icon";
8
8
  import type { SelectTriggerBaseProps } from "../../types/trigger";
9
9
 
10
10
  /**
11
- * Select trigger foundation; priority/size/state를 data attribute로 노출하고
12
- * Chevron 아이콘을 자동 연결하는 기본 요소다.
11
+ * Select Foundation; Trigger Base 슬롯 렌더링 컴포넌트
13
12
  * @component
14
13
  * @param {SelectTriggerBaseProps} props trigger base props
15
- * @param {"primary" | "secondary"} [props.priority="primary"] 스타일 우선순위
14
+ * @param {"primary" | "secondary" | "table"} [props.priority="primary"] 스타일 우선순위
16
15
  * @param {"small" | "medium" | "large"} [props.size="medium"] 높이 스케일
17
16
  * @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
18
- * @param {boolean} [props.multiple] multi select 여부
17
+ * @param {boolean} [props.open=false] dropdown open 상태
18
+ * @param {boolean} [props.block=false] block 레이아웃 여부
19
+ * @param {boolean} [props.multiple=false] multi select 여부
20
+ * @param {boolean} [props.disabled=false] disabled 여부
19
21
  * @param {ElementType} [props.as="button"] polymorphic 태그
20
22
  * @param {"button" | "submit" | "reset"} [props.buttonType="button"] native button type
23
+ * @param {string} [props.className] trigger className
24
+ * @param {React.ReactNode} props.children trigger 콘텐츠
25
+ * @example
26
+ * <SelectTriggerBase open={false} size="medium">
27
+ * <span>옵션 선택</span>
28
+ * </SelectTriggerBase>
21
29
  */
22
30
  const SelectTriggerBase = forwardRef<HTMLElement, SelectTriggerBaseProps>(
23
31
  (
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import clsx from "clsx";
4
+ import { forwardRef } from "react";
4
5
 
5
6
  import type { SelectContainerProps } from "../../types/props";
6
7
  import {
@@ -14,40 +15,42 @@ import {
14
15
  * @param {SelectContainerProps} props Select container props
15
16
  * @param {string} [props.className] 사용자 정의 className
16
17
  * @param {boolean} [props.block] wrapper 전체 폭 확장 여부
18
+ * @param {FormFieldWidth} [props.width] Form.Field width preset
19
+ * @param {CSSProperties} [props.style] wrapper inline style
17
20
  * @param {React.ReactNode} props.children trigger 및 dropdown 콘텐츠
18
21
  */
19
- export default function SelectContainer({
20
- className,
21
- children,
22
- block = false,
23
- width,
24
- style,
25
- ...restProps
26
- }: SelectContainerProps) {
27
- const widthAttr =
28
- width !== undefined
29
- ? getFormFieldWidthAttr(width)
30
- : block
31
- ? "full"
32
- : undefined;
33
- const widthValue =
34
- width !== undefined ? getFormFieldWidthValue(width) : undefined;
35
- const mergedStyle =
36
- widthValue !== undefined
37
- ? { ...(style ?? {}), ["--select-width" as const]: widthValue }
38
- : style;
22
+ const SelectContainer = forwardRef<HTMLDivElement, SelectContainerProps>(
23
+ ({ className, children, block = false, width, style, ...restProps }, ref) => {
24
+ const widthAttr =
25
+ width !== undefined
26
+ ? getFormFieldWidthAttr(width)
27
+ : block
28
+ ? "full"
29
+ : undefined;
30
+ const widthValue =
31
+ width !== undefined ? getFormFieldWidthValue(width) : undefined;
32
+ const mergedStyle =
33
+ widthValue !== undefined
34
+ ? { ...(style ?? {}), ["--select-width" as const]: widthValue }
35
+ : style;
39
36
 
40
- return (
41
- <div
42
- className={clsx("select select-container", className, {
43
- "select-block": block,
44
- })}
45
- data-width={widthAttr}
46
- style={mergedStyle}
47
- {...restProps}
48
- >
49
- {/** dropdown root 및 dropdown menu 등 포함 예정 */}
50
- {children}
51
- </div>
52
- );
53
- }
37
+ return (
38
+ <div
39
+ ref={ref}
40
+ className={clsx("select select-container", className, {
41
+ "select-block": block,
42
+ })}
43
+ data-width={widthAttr}
44
+ style={mergedStyle}
45
+ {...restProps}
46
+ >
47
+ {/** dropdown root 및 dropdown menu 등 포함 예정 */}
48
+ {children}
49
+ </div>
50
+ );
51
+ },
52
+ );
53
+
54
+ SelectContainer.displayName = "SelectContainer";
55
+
56
+ export default SelectContainer;
@@ -41,10 +41,13 @@ const SelectChevronSecondaryIcon: SelectIconSizeMap = {
41
41
  * Select; Chevron 아이콘 컬렉션
42
42
  * - primary (small, medium, large)
43
43
  * - secondary (small, medium, large)
44
+ * - table (small, medium, large)
44
45
  */
45
46
  const SelectChevronIcon: SelectIconPriorityMap = {
46
47
  primary: SelectChevronPrimaryIcon,
47
48
  secondary: SelectChevronSecondaryIcon,
49
+ // 변경: table priority는 secondary chevron 자산을 재사용한다.
50
+ table: SelectChevronSecondaryIcon,
48
51
  };
49
52
 
50
53
  /**
@@ -61,10 +64,12 @@ const SelectMultipleRemoveIcon: SelectIconSizeMap = {
61
64
 
62
65
  /**
63
66
  * Select; Remove 아이콘 컬렉션
64
- * - primary (small, medium, large)
67
+ * - primary/secondary/table (small, medium, large)
65
68
  */
66
69
  const SelectRemoveIcon: SelectIconRemovePriorityMap = {
67
70
  primary: SelectMultipleRemoveIcon,
71
+ secondary: SelectMultipleRemoveIcon,
72
+ table: SelectMultipleRemoveIcon,
68
73
  };
69
74
 
70
75
  /**
@@ -17,13 +17,27 @@ import { useSelectDropdownOpenState } from "../../hooks";
17
17
  * @component
18
18
  * @param {SelectMultipleComponentProps} props multi trigger props
19
19
  * @param {SelectMultipleTag[]} [props.tags] 선택된 tag 리스트
20
+ * @param {SelectDropdownOption[]} [props.options] dropdown option 목록
21
+ * @param {string[]} [props.selectedOptionIds] 선택된 option id 리스트
22
+ * @param {(option: SelectDropdownOption) => void} [props.onOptionSelect] option 선택 콜백
20
23
  * @param {React.ReactNode} [props.displayLabel] fallback 라벨
21
24
  * @param {React.ReactNode} [props.placeholder] placeholder 텍스트
22
- * @param {"primary" | "secondary"} [props.priority="primary"] priority scale
25
+ * @param {"primary" | "secondary" | "table"} [props.priority="primary"] priority scale
23
26
  * @param {"small" | "medium" | "large"} [props.size="medium"] size scale
27
+ * @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
24
28
  * @param {boolean} [props.block] block 여부
29
+ * @param {FormFieldWidth} [props.width] container width preset
25
30
  * @param {boolean} [props.isOpen] dropdown open 여부
26
31
  * @param {boolean} [props.disabled] disabled 여부
32
+ * @param {"small" | "medium" | "large"} [props.dropdownSize] dropdown panel size
33
+ * @param {"match" | "fit-content" | "max-content" | string | number} [props.dropdownWidth="match"] dropdown panel width
34
+ * @param {Omit<DropdownMenuProps, "open" | "defaultOpen" | "onOpenChange">} [props.dropdownRootProps] Dropdown.Root 전달 props
35
+ * @param {Omit<DropdownContainerProps, "children" | "size" | "width">} [props.dropdownContainerProps] Dropdown.Container 전달 props
36
+ * @param {DropdownMenuListProps} [props.dropdownMenuListProps] Dropdown.Menu.List 전달 props
37
+ * @param {ReactNode} [props.alt] empty 상태 대체 콘텐츠
38
+ * @param {boolean} [props.open] controlled open 상태
39
+ * @param {boolean} [props.defaultOpen] uncontrolled 초기 open 상태
40
+ * @param {(open: boolean) => void} [props.onOpenChange] open 상태 변경 콜백
27
41
  */
28
42
  const SelectMultipleTrigger = forwardRef<
29
43
  HTMLElement,
@@ -50,6 +64,7 @@ const SelectMultipleTrigger = forwardRef<
50
64
  dropdownRootProps,
51
65
  dropdownContainerProps,
52
66
  dropdownMenuListProps,
67
+ alt,
53
68
  open,
54
69
  defaultOpen,
55
70
  onOpenChange,
@@ -57,6 +72,9 @@ const SelectMultipleTrigger = forwardRef<
57
72
  },
58
73
  ref,
59
74
  ) => {
75
+ // 변경: table priority는 width 미지정 시 기본 full width를 사용한다.
76
+ const resolvedBlock =
77
+ block || (priority === "table" && width === undefined);
60
78
  // hook dependency 안정화를 위해 memoized selected id 배열을 유지한다.
61
79
  const resolvedSelectedIds = useMemo(
62
80
  () => selectedOptionIds ?? [],
@@ -100,10 +118,10 @@ const SelectMultipleTrigger = forwardRef<
100
118
  defaultOpen,
101
119
  onOpenChange,
102
120
  });
103
- // multi select에서도 동일한 open 상태를 유지하기 위해 공통 hook을 사용한다.
121
+ // 변경: outside close는 Radix onOpenChange 기본 동작을 사용한다.
104
122
 
105
123
  const panelSize = (dropdownSize ?? size) as DropdownSize;
106
- const shouldRenderDropdown = options.length > 0;
124
+ const hasOptions = options.length > 0;
107
125
  const MAX_VISIBLE_TAGS = 3;
108
126
  const visibleTags = hasTags ? derivedTags.slice(0, MAX_VISIBLE_TAGS) : [];
109
127
  const overflowCount = hasTags
@@ -113,7 +131,7 @@ const SelectMultipleTrigger = forwardRef<
113
131
  return (
114
132
  <Container
115
133
  className={clsx("select-trigger-multiple", className)}
116
- block={block}
134
+ block={resolvedBlock}
117
135
  width={width}
118
136
  >
119
137
  <Dropdown.Root
@@ -129,7 +147,7 @@ const SelectMultipleTrigger = forwardRef<
129
147
  priority={priority}
130
148
  size={size}
131
149
  state={disabled ? "disabled" : state}
132
- block={block}
150
+ block={resolvedBlock}
133
151
  open={dropdownOpen}
134
152
  multiple
135
153
  disabled={disabled}
@@ -166,36 +184,45 @@ const SelectMultipleTrigger = forwardRef<
166
184
  )}
167
185
  </SelectTriggerBase>
168
186
  </Dropdown.Trigger>
169
- {shouldRenderDropdown ? (
170
- <Dropdown.Container
171
- {...dropdownContainerProps}
172
- size={panelSize}
173
- width={dropdownWidth}
174
- >
175
- <Dropdown.Menu.List {...dropdownMenuListProps}>
176
- {/* multi select 전용 옵션을 Dropdown.Menu.Item으로 노출한다. */}
177
- {options.map(option => (
178
- <Dropdown.Menu.Item
179
- key={option.id}
180
- label={option.label}
181
- description={option.description}
182
- disabled={option.disabled}
183
- left={option.left}
184
- right={option.right}
185
- multiple
186
- isSelected={resolvedSelectedIds.includes(option.id)}
187
- onSelect={event => {
188
- if (option.disabled) {
189
- event.preventDefault();
190
- return;
191
- }
192
- onOptionSelect?.(option);
193
- }}
194
- />
195
- ))}
196
- </Dropdown.Menu.List>
197
- </Dropdown.Container>
198
- ) : null}
187
+ <Dropdown.Container
188
+ {...dropdownContainerProps}
189
+ size={panelSize}
190
+ width={dropdownWidth}
191
+ >
192
+ <Dropdown.Menu.List {...dropdownMenuListProps}>
193
+ {hasOptions ? (
194
+ <>
195
+ {/* multi select 전용 옵션을 Dropdown.Menu.Item으로 노출한다. */}
196
+ {options.map(option => (
197
+ <Dropdown.Menu.Item
198
+ key={option.id}
199
+ label={option.label}
200
+ description={option.description}
201
+ disabled={option.disabled}
202
+ left={option.left}
203
+ right={option.right}
204
+ multiple
205
+ isSelected={resolvedSelectedIds.includes(option.id)}
206
+ onSelect={event => {
207
+ if (option.disabled) {
208
+ event.preventDefault();
209
+ return;
210
+ }
211
+ onOptionSelect?.(option);
212
+ }}
213
+ />
214
+ ))}
215
+ </>
216
+ ) : (
217
+ <Dropdown.Menu.Item
218
+ // 변경: 사용처 1회 상수 대신 인라인 fallback으로 empty label을 처리한다.
219
+ label={alt ?? "선택할 항목이 없습니다."}
220
+ disabled
221
+ className="dropdown-menu-alt"
222
+ />
223
+ )}
224
+ </Dropdown.Menu.List>
225
+ </Dropdown.Container>
199
226
  </Dropdown.Root>
200
227
  </Container>
201
228
  );