@uniai-fe/uds-primitives 0.13.2 → 0.14.0

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/dist/styles.css CHANGED
@@ -5755,9 +5755,9 @@ figure.chip {
5755
5755
  }
5756
5756
 
5757
5757
  .segmented-control {
5758
- --segmented-height: 34px;
5759
- --segmented-padding: 2px;
5760
- --segmented-radius: 16px;
5758
+ --segmented-height: 32px;
5759
+ --segmented-padding: var(--spacing-padding-1);
5760
+ --segmented-radius: calc(var(--segmented-height) / 2);
5761
5761
  --segmented-bg: var(--color-bg-alternative-cool-gray, #f2f2f3);
5762
5762
  --segmented-indicator-bg: var(--color-common-100, #ffffff);
5763
5763
  --segmented-indicator-shadow: 2px 2px 4px rgba(0, 0, 0, 0.02);
@@ -5765,11 +5765,15 @@ figure.chip {
5765
5765
  --segmented-label-active-color: var(--color-label-strong, #181a1b);
5766
5766
  --segmented-disabled-opacity: 0.4;
5767
5767
  --segmented-gap: 2px;
5768
- --segmented-item-padding-x: 22px;
5769
- --segmented-item-padding-y: 4px;
5770
- --segmented-item-font-size: var(--font-heading-xxsmall-size, 15px);
5768
+ --segmented-item-height: calc(
5769
+ var(--segmented-height) - (var(--segmented-padding) * 2)
5770
+ );
5771
+ --segmented-item-padding-x: var(--spacing-padding-7);
5772
+ --segmented-item-padding-y: 0px;
5773
+ --segmented-item-font-size: 13px;
5771
5774
  --segmented-item-font-weight: 500;
5772
- --segmented-item-line-height: var(--font-heading-xxsmall-line-height, 1.5);
5775
+ --segmented-item-line-height: 1em;
5776
+ --segmented-item-letter-spacing: 0px;
5773
5777
  position: relative;
5774
5778
  display: block;
5775
5779
  box-sizing: border-box;
@@ -5782,6 +5786,40 @@ figure.chip {
5782
5786
  overflow: hidden;
5783
5787
  }
5784
5788
 
5789
+ .segmented-control:where([data-size="medium"]) {
5790
+ --segmented-height: 40px;
5791
+ --segmented-item-padding-x: var(--spacing-padding-8);
5792
+ --segmented-item-font-size: 15px;
5793
+ }
5794
+
5795
+ .segmented-control:where([data-size="large"]) {
5796
+ --segmented-height: 48px;
5797
+ --segmented-item-padding-x: var(--spacing-padding-9);
5798
+ --segmented-item-font-size: var(--font-label-large-size);
5799
+ }
5800
+
5801
+ .segmented-control:where([data-variant="squared"]) {
5802
+ --segmented-radius: var(--theme-radius-medium-4);
5803
+ --segmented-bg: rgba(227, 228, 232, 0.52);
5804
+ --segmented-indicator-bg: var(--color-common-100);
5805
+ --segmented-indicator-shadow:
5806
+ 0 2px 4px rgba(0, 0, 0, 0.06), 0 0 2px rgba(0, 0, 0, 0.12);
5807
+ --segmented-label-color: var(--color-cool-gray-30);
5808
+ --segmented-label-active-color: var(--color-cool-gray-30);
5809
+ --segmented-gap: 0px;
5810
+ overflow: visible;
5811
+ }
5812
+ .segmented-control:where([data-variant="squared"]):where([data-size="small"]) {
5813
+ --segmented-radius: var(--theme-radius-medium-3);
5814
+ }
5815
+ .segmented-control:where([data-variant="squared"]):where([data-size="small"])
5816
+ .segmented-control-button:where([data-state="off"]) {
5817
+ border-radius: var(--theme-radius-medium-3);
5818
+ }
5819
+ .segmented-control:where([data-variant="squared"]):where([data-size="large"]) {
5820
+ --segmented-radius: var(--theme-radius-large-1);
5821
+ }
5822
+
5785
5823
  .segmented-control:where([data-keep-selected="true"]) {
5786
5824
  --segmented-disabled-opacity: 0.3;
5787
5825
  }
@@ -5837,6 +5875,7 @@ figure.chip {
5837
5875
  background: transparent;
5838
5876
  cursor: pointer;
5839
5877
  min-width: 0;
5878
+ height: var(--segmented-item-height);
5840
5879
  border-radius: calc(var(--segmented-radius) - var(--segmented-padding));
5841
5880
  padding: var(--segmented-item-padding-y) var(--segmented-item-padding-x);
5842
5881
  transition: color 0.2s ease;
@@ -5854,8 +5893,9 @@ figure.chip {
5854
5893
  font-size: var(--segmented-item-font-size);
5855
5894
  font-weight: var(--segmented-item-font-weight);
5856
5895
  line-height: var(--segmented-item-line-height);
5857
- letter-spacing: 0.2px;
5896
+ letter-spacing: var(--segmented-item-letter-spacing);
5858
5897
  color: var(--segmented-label-color);
5898
+ white-space: nowrap;
5859
5899
  transition: color 0.2s ease;
5860
5900
  }
5861
5901
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -90,8 +90,8 @@
90
90
  "@uniai-fe/eslint-config": "0.4.5",
91
91
  "@uniai-fe/next-devkit": "0.4.1",
92
92
  "@uniai-fe/tsconfig": "0.2.0",
93
- "@uniai-fe/uds-foundation": "0.6.0",
94
- "@uniai-fe/util-functions": "0.4.3"
93
+ "@uniai-fe/util-functions": "0.4.3",
94
+ "@uniai-fe/uds-foundation": "0.6.0"
95
95
  },
96
96
  "scripts": {
97
97
  "check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
@@ -22,16 +22,18 @@ const toNullableValue = (value?: SegmentedControlValue) =>
22
22
  value === undefined || value === "" ? undefined : value;
23
23
 
24
24
  /**
25
- * @component SegmentedControl
26
- * @desc keepSelected 토글과 indicator/list 레이어를 native로 구성한 Segmented Control 루트 컴포넌트.
27
- * @param {SegmentedControlProps} props 루트에 전달되는 props.
28
- * @param {SegmentedControlOption[]} props.options 선택지 배열.
29
- * @param {string} props.ariaLabel 라디오 그룹 접근성 라벨.
30
- * @param {boolean} [props.keepSelected=true] 이미 on 상태인 항목을 다시 눌러도 선택 해제를 막을지 여부.
31
- * @param {SegmentedControlValue} [props.value] 제어형 값.
32
- * @param {SegmentedControlValue} [props.defaultValue] 비제어 초기 값.
33
- * @param {(value: SegmentedControlValue) => void} [props.onValueChange] 값 변경 콜백.
34
- * @param {string} [props.className] 최상위 className.
25
+ * SegmentedControl; 텍스트 옵션의 단일 선택
26
+ * @component
27
+ * @property {SegmentedControlOption[]} options 고유한 value와 텍스트 label을 가진 선택지
28
+ * @property {string} ariaLabel 라디오 그룹의 접근 가능한 이름
29
+ * @property {boolean} [keepSelected=true] 이미 선택한 항목의 선택 유지 여부
30
+ * @property {"rounded" | "squared"} [variant=rounded] 기존 둥근 형태 또는 각진 형태
31
+ * @property {"small" | "medium" | "large"} [size=small] 두 외형이 공유하는 크기
32
+ * @property {SegmentedControlValue} [value] 제어형 선택값. 빈 문자열은 선택 없음
33
+ * @property {SegmentedControlValue} [defaultValue] 비제어형 초기 선택값
34
+ * @property {(value: SegmentedControlValue) => void} [onValueChange] 선택값 변경. 해제하면 빈 문자열
35
+ * @property {string} [className] 최상위 컨테이너의 추가 className
36
+ * @desc native 라디오 그룹의 선택·키보드·disabled 계약을 유지하고 indicator와 목록을 구성한다.
35
37
  */
36
38
  const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
37
39
  (
@@ -39,6 +41,8 @@ const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
39
41
  options,
40
42
  ariaLabel,
41
43
  keepSelected = true,
44
+ variant = "rounded",
45
+ size = "small",
42
46
  className,
43
47
  onValueChange,
44
48
  value: valueProp,
@@ -185,10 +189,10 @@ const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
185
189
  });
186
190
  }, [options, resolvedValue]);
187
191
 
188
- // DOM이 렌더된 직후 선택된 항목 기준으로 indicator를 한 번 맞춘다.
192
+ // 같은 컨테이너에서 외형·크기만 바뀌어도 선택면을 새 버튼 geometry에 맞춘다.
189
193
  useLayoutEffect(() => {
190
194
  measureIndicator();
191
- }, [measureIndicator]);
195
+ }, [measureIndicator, variant, size]);
192
196
 
193
197
  useEffect(() => {
194
198
  if (!rootRef.current || typeof ResizeObserver === "undefined") {
@@ -227,6 +231,8 @@ const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
227
231
  "segmented-control segmented-control-container",
228
232
  className,
229
233
  )}
234
+ data-variant={variant}
235
+ data-size={size}
230
236
  data-keep-selected={keepSelected ? "true" : undefined}
231
237
  >
232
238
  {/* indicator와 list를 분리해 DOM 구조가 단순한 flex 계층을 유지한다. */}
@@ -1,7 +1,7 @@
1
1
  .segmented-control {
2
- --segmented-height: 34px;
3
- --segmented-padding: 2px;
4
- --segmented-radius: 16px;
2
+ --segmented-height: 32px;
3
+ --segmented-padding: var(--spacing-padding-1);
4
+ --segmented-radius: calc(var(--segmented-height) / 2);
5
5
  --segmented-bg: var(--color-bg-alternative-cool-gray, #f2f2f3);
6
6
  --segmented-indicator-bg: var(--color-common-100, #ffffff);
7
7
  --segmented-indicator-shadow: 2px 2px 4px rgba(0, 0, 0, 0.02);
@@ -9,12 +9,15 @@
9
9
  --segmented-label-active-color: var(--color-label-strong, #181a1b);
10
10
  --segmented-disabled-opacity: 0.4;
11
11
  --segmented-gap: 2px;
12
- --segmented-item-padding-x: 22px;
13
- --segmented-item-padding-y: 4px;
14
- --segmented-item-font-size: var(--font-heading-xxsmall-size, 15px);
15
- // 변경: SegmentedControl label font-weight는 디자인 규칙에 맞춰 500으로 고정한다.
12
+ --segmented-item-height: calc(
13
+ var(--segmented-height) - (var(--segmented-padding) * 2)
14
+ );
15
+ --segmented-item-padding-x: var(--spacing-padding-7);
16
+ --segmented-item-padding-y: 0px;
17
+ --segmented-item-font-size: 13px;
16
18
  --segmented-item-font-weight: 500;
17
- --segmented-item-line-height: var(--font-heading-xxsmall-line-height, 1.5);
19
+ --segmented-item-line-height: 1em;
20
+ --segmented-item-letter-spacing: 0px;
18
21
  position: relative;
19
22
  display: block;
20
23
  box-sizing: border-box;
@@ -27,6 +30,42 @@
27
30
  overflow: hidden;
28
31
  }
29
32
 
33
+ .segmented-control:where([data-size="medium"]) {
34
+ --segmented-height: 40px;
35
+ --segmented-item-padding-x: var(--spacing-padding-8);
36
+ --segmented-item-font-size: 15px;
37
+ }
38
+
39
+ .segmented-control:where([data-size="large"]) {
40
+ --segmented-height: 48px;
41
+ --segmented-item-padding-x: var(--spacing-padding-9);
42
+ --segmented-item-font-size: var(--font-label-large-size);
43
+ }
44
+
45
+ .segmented-control:where([data-variant="squared"]) {
46
+ --segmented-radius: var(--theme-radius-medium-4);
47
+ --segmented-bg: rgba(227, 228, 232, 0.52);
48
+ --segmented-indicator-bg: var(--color-common-100);
49
+ --segmented-indicator-shadow:
50
+ 0 2px 4px rgba(0, 0, 0, 0.06), 0 0 2px rgba(0, 0, 0, 0.12);
51
+ --segmented-label-color: var(--color-cool-gray-30);
52
+ --segmented-label-active-color: var(--color-cool-gray-30);
53
+ --segmented-gap: 0px;
54
+ overflow: visible;
55
+
56
+ &:where([data-size="small"]) {
57
+ --segmented-radius: var(--theme-radius-medium-3);
58
+
59
+ .segmented-control-button:where([data-state="off"]) {
60
+ border-radius: var(--theme-radius-medium-3);
61
+ }
62
+ }
63
+
64
+ &:where([data-size="large"]) {
65
+ --segmented-radius: var(--theme-radius-large-1);
66
+ }
67
+ }
68
+
30
69
  .segmented-control:where([data-keep-selected="true"]) {
31
70
  --segmented-disabled-opacity: 0.3;
32
71
  }
@@ -82,6 +121,7 @@
82
121
  background: transparent;
83
122
  cursor: pointer;
84
123
  min-width: 0;
124
+ height: var(--segmented-item-height);
85
125
  border-radius: calc(var(--segmented-radius) - var(--segmented-padding));
86
126
  padding: var(--segmented-item-padding-y) var(--segmented-item-padding-x);
87
127
  transition: color 0.2s ease;
@@ -99,8 +139,9 @@
99
139
  font-size: var(--segmented-item-font-size);
100
140
  font-weight: var(--segmented-item-font-weight);
101
141
  line-height: var(--segmented-item-line-height);
102
- letter-spacing: 0.2px;
142
+ letter-spacing: var(--segmented-item-letter-spacing);
103
143
  color: var(--segmented-label-color);
144
+ white-space: nowrap;
104
145
  transition: color 0.2s ease;
105
146
  }
106
147
 
@@ -26,22 +26,68 @@ export interface SegmentedControlOption {
26
26
  }
27
27
 
28
28
  /**
29
- * SegmentedControl props. native div props에 options/ariaLabel/keepSelected 등을 확장한다.
30
- * @property {SegmentedControlOption[]} options 렌더할 옵션 배열.
31
- * @property {string} ariaLabel 스크린리더용 라벨. 필수.
32
- * @property {boolean} [keepSelected=true] 선택된 항목을 다시 클릭했을 때 해제하지 않고 유지할지 여부.
29
+ * SegmentedControl의 외형. rounded는 둥근 형태, squared는 각진 형태다.
33
30
  */
34
- type SegmentedControlRootProps = Omit<
31
+ export type SegmentedControlVariant = "rounded" | "squared";
32
+
33
+ /**
34
+ * 두 외형이 공유하는 크기. small, medium, large를 지원한다.
35
+ */
36
+ export type SegmentedControlSize = "small" | "medium" | "large";
37
+
38
+ /**
39
+ * SegmentedControl 루트의 native div 속성. children은 options로 구성하므로 받지 않는다.
40
+ */
41
+ export type SegmentedControlRootProps = Omit<
35
42
  ComponentPropsWithoutRef<"div">,
36
43
  "children"
37
44
  >;
38
45
 
46
+ /**
47
+ * SegmentedControl 공개 입력
48
+ * @property {SegmentedControlOption[]} options 고유한 문자열 value와 텍스트 label을 가진 선택지
49
+ * @property {string} ariaLabel 라디오 그룹의 접근 가능한 이름
50
+ * @property {boolean} [keepSelected=true] 같은 항목을 다시 눌러도 선택을 유지할지 여부
51
+ * @property {"rounded" | "squared"} [variant=rounded] 기존 둥근 형태 또는 각진 형태
52
+ * @property {"small" | "medium" | "large"} [size=small] rounded와 squared가 공유하는 크기
53
+ * @property {SegmentedControlValue} [value] 제어형 선택값. 빈 문자열은 선택 없음
54
+ * @property {SegmentedControlValue} [defaultValue] 비제어형 초기 선택값
55
+ * @property {(value: SegmentedControlValue) => void} [onValueChange] 다음 선택값. 해제하면 빈 문자열
56
+ * @desc SegmentedControlRootProps의 native div 속성을 상속하며 선택 상태와 외형을 구성한다.
57
+ * @see SegmentedControlRootProps
58
+ */
39
59
  export interface SegmentedControlProps extends SegmentedControlRootProps {
60
+ /**
61
+ * 고유한 문자열 value와 텍스트 label을 가진 선택지
62
+ */
40
63
  options: SegmentedControlOption[];
64
+ /**
65
+ * 라디오 그룹의 접근 가능한 이름
66
+ */
41
67
  ariaLabel: string;
68
+ /**
69
+ * 기본 true. false이면 같은 항목을 다시 눌러 선택을 해제할 수 있다.
70
+ */
42
71
  keepSelected?: boolean;
72
+ /**
73
+ * 허용 값은 "rounded" | "squared". 기본 rounded는 둥근 모서리를 사용한다.
74
+ */
75
+ variant?: SegmentedControlVariant;
76
+ /**
77
+ * 허용 값은 "small" | "medium" | "large". 기본 small이며 두 외형에 같은 높이와 여백을 적용한다.
78
+ */
79
+ size?: SegmentedControlSize;
80
+ /**
81
+ * 제어형 선택값. 빈 문자열은 선택 없음이며 undefined는 비제어형으로 처리한다.
82
+ */
43
83
  value?: SegmentedControlValue;
84
+ /**
85
+ * 비제어형 초기 선택값
86
+ */
44
87
  defaultValue?: SegmentedControlValue;
88
+ /**
89
+ * 다음 선택값을 전달하며 선택 해제 시 빈 문자열을 전달한다.
90
+ */
45
91
  onValueChange?: (value: SegmentedControlValue) => void;
46
92
  }
47
93