@uniai-fe/uds-primitives 0.3.16 → 0.3.18

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.
@@ -1,127 +1,57 @@
1
1
  import { forwardRef, type Ref } from "react";
2
- import RemoveIcon from "../img/remove.svg";
3
2
  import type { ChipInputProps, ChipProps } from "../types";
4
- import {
5
- CHIP_LABEL_CLASSNAME,
6
- CHIP_LEADING_CLASSNAME,
7
- CHIP_REMOVE_BUTTON_CLASSNAME,
8
- composeChipClassName,
9
- } from "../utils";
3
+ import ChipDefaultStyle from "./DefaultStyle";
4
+ import ChipInputStyle from "./InputStyle";
10
5
 
11
6
  // kind === "input" 조합에서 props를 ChipInputProps로 좁히기 위한 type guard.
12
7
  const isChipInputProps = (props: ChipProps): props is ChipInputProps =>
13
8
  props.kind === "input";
14
9
 
15
10
  /**
16
- * Chip 종류별로 interactive/input 구조가 달라 별도 분기한다.
11
+ * Chip Markup; Kind별 렌더 분기 Orchestrator 컴포넌트
17
12
  * @component
18
- * @param {ChipProps} props
19
- * @param {"filter" | "filter-rounded" | "assist" | "input"} [props.kind="filter"] chip kind.
20
- * @param {boolean} [props.selected] 선택 상태. filter 계열에서 aria-pressed로 노출.
21
- * @param {React.ReactNode} [props.leading] assist kind 전용 leading slot.
22
- * @param {"default" | "table"} [props.size="default"] chip size 축. table은 셀 콘텐츠용 compact 규격이다.
23
- * @param {string} [props.removeButtonLabel="선택 항목 삭제"] input kind 제거 버튼 라벨.
24
- * @param {boolean} [props.disabled] input kind 상호작용 비활성화 여부. true이면 remove 버튼을 렌더링하지 않는다.
25
- * @param {(event: MouseEvent<HTMLButtonElement>) => void} [props.onRemove] input kind 제거 핸들러.
26
- * @param {string} [props.className] root className.
13
+ * @param {ChipProps} props Chip 공통/종류별 props
14
+ * @param {"filter" | "filter-rounded" | "assist" | "input"} [props.kind="filter"] Chip 종류
15
+ * @param {React.ReactNode} [props.children] Chip 라벨/콘텐츠
16
+ * @param {boolean} [props.selected] 선택 상태; filter 계열에서 `aria-pressed`로 반영
17
+ * @param {React.ReactNode} [props.leading] assist kind 좌측 slot
18
+ * @param {"default" | "table"} [props.size="default"] 사이즈
19
+ * @param {string} [props.removeButtonLabel="선택 항목 삭제"] input kind remove 버튼 라벨
20
+ * @param {boolean} [props.disabled] input kind 비활성화 여부
21
+ * @param {(event: MouseEvent<HTMLButtonElement>) => void} [props.onRemove] input kind remove 핸들러
22
+ * @param {(event: MouseEvent<HTMLButtonElement>) => void} [props.onClick] interactive kind click 핸들러
23
+ * @param {"button" | "submit" | "reset"} [props.type] interactive kind button type
24
+ * @param {string} [props.className] root className
25
+ * @param {string} [props.id] root id
26
+ * @param {string} [props.title] root title
27
+ * @description
28
+ * - `kind="input"`이면 `<figure>`를 렌더하고, 나머지 kind는 `<button>`을 렌더한다.
29
+ * - `kind="input"`일 때 remove 버튼 클릭 이벤트는 root click으로 버블링되지 않도록 내부에서 차단한다.
30
+ * - native HTML attrs는 kind에 맞는 root 요소로 그대로 전달된다.
31
+ * @returns {JSX.Element} kind에 맞는 Chip root 요소
32
+ * @example
33
+ * <Chip kind="filter" selected>
34
+ * 전체
35
+ * </Chip>
36
+ * @example
37
+ * <Chip kind="input" onRemove={event => console.log(event.type)}>
38
+ * 첨부파일.pdf
39
+ * </Chip>
27
40
  */
28
41
  const Chip = forwardRef<HTMLElement, ChipProps>((props, ref) => {
29
42
  if (isChipInputProps(props)) {
30
- // input kind는 figure + remove 버튼 조합으로 구성한다.
31
- const {
32
- children,
33
- className,
34
- removeButtonLabel = "선택 항목 삭제",
35
- onRemove,
36
- disabled = false,
37
- selected,
38
- leading,
39
- size = "default",
40
- ...restProps
41
- } = props;
42
- const removable = !disabled && typeof onRemove === "function";
43
- const hasLeading = Boolean(leading);
44
- const combinedClassName = composeChipClassName({
45
- kind: "input",
46
- selected,
47
- hasLeading,
48
- removable,
49
- className,
50
- });
51
-
52
43
  return (
53
- <figure
54
- {...restProps}
44
+ <ChipInputStyle
45
+ {...props}
46
+ // 변경: 하위 스타일 컴포넌트가 forwardRef를 사용하므로 ref를 직접 전달한다.
55
47
  ref={ref as Ref<HTMLElementTagNameMap["figure"]>}
56
- className={combinedClassName}
57
- data-kind="input"
58
- data-removable={removable ? "true" : "false"}
59
- data-size={size}
60
- data-selected={selected ? "true" : undefined}
61
- data-has-leading={hasLeading ? "true" : undefined}
62
- data-disabled={disabled ? "true" : undefined}
63
- aria-disabled={disabled ? "true" : undefined}
64
- >
65
- <span className={CHIP_LABEL_CLASSNAME}>{children}</span>
66
- {removable ? (
67
- <button
68
- type="button"
69
- className={CHIP_REMOVE_BUTTON_CLASSNAME}
70
- aria-label={removeButtonLabel}
71
- onClick={onRemove}
72
- >
73
- <RemoveIcon aria-hidden="true" />
74
- </button>
75
- ) : null}
76
- </figure>
48
+ />
77
49
  );
78
50
  }
79
51
 
80
- const {
81
- children,
82
- selected,
83
- leading,
84
- className,
85
- kind,
86
- size = "default",
87
- type,
88
- ...restProps
89
- } = props;
90
- const resolvedKind = kind ?? "filter";
91
- const isAssist = resolvedKind === "assist";
92
- const hasLeading = isAssist && Boolean(leading);
93
- const combinedClassName = composeChipClassName({
94
- kind: resolvedKind,
95
- selected,
96
- hasLeading,
97
- removable: false,
98
- className,
99
- });
100
-
101
52
  return (
102
- <button
103
- {...restProps}
104
- ref={ref as Ref<HTMLButtonElement>}
105
- type={type ?? "button"}
106
- className={combinedClassName}
107
- data-kind={resolvedKind}
108
- data-size={size}
109
- data-selected={selected ? "true" : undefined}
110
- data-has-leading={hasLeading ? "true" : undefined}
111
- aria-pressed={
112
- typeof selected === "boolean" &&
113
- (resolvedKind === "filter" || resolvedKind === "filter-rounded")
114
- ? selected
115
- : undefined
116
- }
117
- >
118
- {isAssist && leading ? (
119
- <span className={CHIP_LEADING_CLASSNAME} aria-hidden="true">
120
- {leading}
121
- </span>
122
- ) : null}
123
- <span className={CHIP_LABEL_CLASSNAME}>{children}</span>
124
- </button>
53
+ // 변경: interactive 경로도 forwardRef 기반 ref 전달로 통일한다.
54
+ <ChipDefaultStyle {...props} ref={ref as Ref<HTMLButtonElement>} />
125
55
  );
126
56
  });
127
57
 
@@ -0,0 +1,75 @@
1
+ import { forwardRef } from "react";
2
+ import clsx from "clsx";
3
+ import type { ChipDefaultStyleComponentProps } from "../types";
4
+
5
+ /**
6
+ * Chip Markup; Default/Interactive 스타일 렌더 컴포넌트
7
+ * @component
8
+ * @param {ChipDefaultStyleComponentProps} props interactive 스타일 props
9
+ * @param {Ref<HTMLButtonElement>} ref button ref
10
+ * @param {React.ReactNode} [props.children] chip 라벨 콘텐츠
11
+ * @param {boolean} [props.selected] 선택 상태
12
+ * @param {React.ReactNode} [props.leading] assist kind 좌측 slot
13
+ * @param {string} [props.className] root className
14
+ * @param {"filter" | "filter-rounded" | "assist"} [props.kind] interactive kind
15
+ * @param {"default" | "table"} [props.size] size 축
16
+ * @param {"button" | "submit" | "reset"} [props.type] button type
17
+ * @example
18
+ * <ChipDefaultStyle kind="assist" ref={buttonRef}>
19
+ * Assist
20
+ * </ChipDefaultStyle>
21
+ */
22
+ const ChipDefaultStyle = forwardRef<
23
+ HTMLButtonElement,
24
+ ChipDefaultStyleComponentProps
25
+ >(
26
+ (
27
+ {
28
+ children,
29
+ selected,
30
+ leading,
31
+ className,
32
+ kind,
33
+ size = "default",
34
+ type,
35
+ ...restProps
36
+ },
37
+ ref,
38
+ ) => {
39
+ // 변경: props 구조분해는 함수 시그니처에서만 수행하고, 본문 내 `const { ... } = props` 패턴을 제거한다.
40
+ const resolvedKind = kind ?? "filter";
41
+ const isAssist = resolvedKind === "assist";
42
+ const hasLeading = isAssist && Boolean(leading);
43
+
44
+ return (
45
+ <button
46
+ {...restProps}
47
+ // 변경: forwardedRef custom prop 대신 forwardRef의 ref를 직접 연결한다.
48
+ ref={ref}
49
+ type={type ?? "button"}
50
+ className={clsx("chip", className)}
51
+ data-kind={resolvedKind}
52
+ data-size={size}
53
+ data-selected={selected ? "true" : undefined}
54
+ data-has-leading={hasLeading ? "true" : undefined}
55
+ aria-pressed={
56
+ typeof selected === "boolean" &&
57
+ (resolvedKind === "filter" || resolvedKind === "filter-rounded")
58
+ ? selected
59
+ : undefined
60
+ }
61
+ >
62
+ {isAssist && leading ? (
63
+ <span className="chip-leading" aria-hidden="true">
64
+ {leading}
65
+ </span>
66
+ ) : null}
67
+ <span className="chip-label">{children}</span>
68
+ </button>
69
+ );
70
+ },
71
+ );
72
+
73
+ ChipDefaultStyle.displayName = "ChipDefaultStyle";
74
+
75
+ export default ChipDefaultStyle;
@@ -0,0 +1,85 @@
1
+ import { forwardRef } from "react";
2
+ import clsx from "clsx";
3
+ import type { MouseEvent } from "react";
4
+ import type { ChipInputStyleProps } from "../types";
5
+ import ChipRemoveButton from "./RemoveButton";
6
+
7
+ /**
8
+ * Chip Markup; Input 스타일 렌더 컴포넌트
9
+ * @component
10
+ * @param {ChipInputStyleProps} props input 스타일 props
11
+ * @param {Ref<HTMLElementTagNameMap["figure"]>} ref figure ref
12
+ * @param {React.ReactNode} [props.children] chip 라벨 콘텐츠
13
+ * @param {string} [props.className] root className
14
+ * @param {string} [props.removeButtonLabel="선택 항목 삭제"] remove 버튼 라벨
15
+ * @param {(event: MouseEvent<HTMLButtonElement>) => void} [props.onRemove] remove 클릭 핸들러
16
+ * @param {boolean} [props.disabled] 비활성화 여부
17
+ * @param {boolean} [props.selected] 선택 상태
18
+ * @param {React.ReactNode} [props.leading] leading slot 존재 여부 판단용 값
19
+ * @param {"default" | "table"} [props.size] size 축
20
+ * @example
21
+ * <ChipInputStyle
22
+ * ref={figureRef}
23
+ * onRemove={event => console.log(event.type)}
24
+ * >
25
+ * report.pdf
26
+ * </ChipInputStyle>
27
+ */
28
+ const ChipInputStyle = forwardRef<
29
+ HTMLElementTagNameMap["figure"],
30
+ ChipInputStyleProps
31
+ >(
32
+ (
33
+ {
34
+ children,
35
+ className,
36
+ removeButtonLabel = "선택 항목 삭제",
37
+ onRemove,
38
+ disabled = false,
39
+ selected,
40
+ leading,
41
+ size = "default",
42
+ ...restProps
43
+ },
44
+ ref,
45
+ ) => {
46
+ // 변경: props 구조분해는 함수 시그니처에서만 수행하고, 본문 내 `const { ... } = props` 패턴을 제거한다.
47
+ const removable = !disabled && typeof onRemove === "function";
48
+ const hasLeading = Boolean(leading);
49
+ /**
50
+ * 변경: remove 버튼 클릭이 chip root click으로 버블링되지 않도록 차단한다.
51
+ */
52
+ const handleRemoveClick = (event: MouseEvent<HTMLButtonElement>) => {
53
+ event.stopPropagation();
54
+ onRemove?.(event);
55
+ };
56
+
57
+ return (
58
+ <figure
59
+ {...restProps}
60
+ // 변경: forwardedRef custom prop 대신 forwardRef의 ref를 직접 연결한다.
61
+ ref={ref}
62
+ className={clsx("chip", className)}
63
+ data-kind="input"
64
+ data-removable={removable ? "true" : "false"}
65
+ data-size={size}
66
+ data-selected={selected ? "true" : undefined}
67
+ data-has-leading={hasLeading ? "true" : undefined}
68
+ data-disabled={disabled ? "true" : undefined}
69
+ aria-disabled={disabled ? "true" : undefined}
70
+ >
71
+ <span className="chip-label">{children}</span>
72
+ {removable ? (
73
+ <ChipRemoveButton
74
+ removeButtonLabel={removeButtonLabel}
75
+ onRemove={handleRemoveClick}
76
+ />
77
+ ) : null}
78
+ </figure>
79
+ );
80
+ },
81
+ );
82
+
83
+ ChipInputStyle.displayName = "ChipInputStyle";
84
+
85
+ export default ChipInputStyle;
@@ -0,0 +1,30 @@
1
+ import RemoveIcon from "../img/remove.svg";
2
+ import type { ChipRemoveButtonProps } from "../types";
3
+
4
+ /**
5
+ * Chip Markup; Input remove 버튼 컴포넌트
6
+ * @component
7
+ * @param {ChipRemoveButtonProps} props remove 버튼 props
8
+ * @param {string} props.removeButtonLabel 접근성 라벨
9
+ * @param {(event: MouseEvent<HTMLButtonElement>) => void} props.onRemove remove 클릭 핸들러
10
+ * @example
11
+ * <ChipRemoveButton
12
+ * removeButtonLabel="첨부파일 삭제"
13
+ * onRemove={event => console.log(event.type)}
14
+ * />
15
+ */
16
+ export default function ChipRemoveButton({
17
+ removeButtonLabel,
18
+ onRemove,
19
+ }: ChipRemoveButtonProps) {
20
+ return (
21
+ <button
22
+ type="button"
23
+ className="chip-remove-button"
24
+ aria-label={removeButtonLabel}
25
+ onClick={onRemove}
26
+ >
27
+ <RemoveIcon aria-hidden="true" />
28
+ </button>
29
+ );
30
+ }
@@ -23,6 +23,7 @@
23
23
  box-sizing: border-box;
24
24
  margin: 0;
25
25
  width: fit-content;
26
+ max-width: 100%;
26
27
  transition:
27
28
  background-color 0.16s ease,
28
29
  color 0.16s ease,
@@ -132,12 +133,16 @@ figure.chip {
132
133
  }
133
134
 
134
135
  .chip-label {
135
- display: flex;
136
- align-items: center;
137
- gap: var(--theme-chip-label-gap);
136
+ // display: flex;
137
+ // align-items: center;
138
+ // gap: var(--theme-chip-label-gap);
138
139
  color: inherit;
139
- line-height: 1;
140
+ line-height: 1em;
140
141
  white-space: nowrap;
142
+ width: fit-content;
143
+ max-width: 100%;
144
+ overflow: hidden;
145
+ text-overflow: ellipsis;
141
146
  }
142
147
 
143
148
  .chip-remove-button {
@@ -1,2 +1,3 @@
1
1
  export * from "./options";
2
2
  export type * from "./props";
3
+ export type * from "./props-internal";
@@ -0,0 +1,43 @@
1
+ import type { MouseEvent } from "react";
2
+ import type { ChipInputProps, ChipProps } from "./props";
3
+
4
+ /**
5
+ * Chip Types; Interactive 스타일 props
6
+ * @typedef {Exclude<ChipProps, ChipInputProps>} ChipDefaultStyleProps
7
+ * @description
8
+ * - `ChipProps`에서 `kind="input"` 조합을 제외한 interactive 전용 props 집합이다.
9
+ * - `button` native attrs(`onClick`, `id`, `title`, `aria-*`, `data-*`)를 함께 포함한다.
10
+ */
11
+ export type ChipDefaultStyleProps = Exclude<ChipProps, ChipInputProps>;
12
+
13
+ /**
14
+ * Chip Types; Input 스타일 컴포넌트 props
15
+ * @typedef {ChipInputProps} ChipInputStyleProps
16
+ * @description
17
+ * - Input 스타일 렌더 컴포넌트는 `forwardRef<figure>`로 ref를 직접 수신한다.
18
+ */
19
+ export type ChipInputStyleProps = ChipInputProps;
20
+
21
+ /**
22
+ * Chip Types; remove 버튼 props
23
+ * @property {string} removeButtonLabel 접근성 라벨
24
+ * @property {(event: MouseEvent<HTMLButtonElement>) => void} onRemove remove 클릭 핸들러
25
+ */
26
+ export interface ChipRemoveButtonProps {
27
+ /**
28
+ * 접근성 라벨
29
+ */
30
+ removeButtonLabel: string;
31
+ /**
32
+ * remove 클릭 핸들러
33
+ */
34
+ onRemove: (event: MouseEvent<HTMLButtonElement>) => void;
35
+ }
36
+
37
+ /**
38
+ * Chip Types; Interactive 스타일 컴포넌트 props
39
+ * @typedef {ChipDefaultStyleProps} ChipDefaultStyleComponentProps
40
+ * @description
41
+ * - Interactive 스타일 렌더 컴포넌트는 `forwardRef<button>`으로 ref를 직접 수신한다.
42
+ */
43
+ export type ChipDefaultStyleComponentProps = ChipDefaultStyleProps;
@@ -80,10 +80,20 @@
80
80
 
81
81
  &[data-size="small"] {
82
82
  min-height: var(--input-default-height-small);
83
+ // Figma small 규격(40px/15px/1.4/0.2px)을 size token으로 고정한다.
84
+ --input-font-size: var(--input-text-small-size);
85
+ --input-line-height: var(--input-text-small-line-height);
86
+ --input-font-weight: var(--input-text-small-weight);
87
+ --input-letter-spacing: var(--input-text-small-letter-spacing);
83
88
  }
84
89
 
85
90
  &[data-size="large"] {
86
91
  min-height: var(--input-default-height-large);
92
+ // Figma large 규격(56px/19px)을 size token으로 고정한다.
93
+ --input-font-size: var(--input-text-large-size);
94
+ --input-line-height: var(--input-text-large-line-height);
95
+ --input-font-weight: var(--input-text-large-weight);
96
+ --input-letter-spacing: var(--input-text-large-letter-spacing);
87
97
  }
88
98
 
89
99
  &[data-priority="secondary"] {
@@ -121,6 +131,11 @@
121
131
  border-radius: var(--input-tertiary-radius-base);
122
132
  background-color: var(--input-surface-color);
123
133
  min-height: var(--input-tertiary-height-base);
134
+ // tertiary는 단일 사이즈지만 Figma에서 body-medium typography를 사용한다.
135
+ --input-font-size: var(--input-text-medium-size);
136
+ --input-line-height: var(--input-text-medium-line-height);
137
+ --input-font-weight: var(--input-text-medium-weight);
138
+ --input-letter-spacing: var(--input-text-medium-letter-spacing);
124
139
  row-gap: var(--input-tertiary-row-gap);
125
140
  column-gap: var(--input-default-gap);
126
141
  flex-wrap: wrap;
@@ -30,6 +30,19 @@
30
30
  --input-table-text-large-size: var(--font-body-small-size);
31
31
  --input-table-text-large-line-height: var(--font-body-small-line-height);
32
32
  --input-table-text-large-weight: var(--font-body-small-weight);
33
+ /* Figma size 매핑: small/medium/large typography 토큰 */
34
+ --input-text-small-size: var(--font-body-xsmall-size);
35
+ --input-text-small-line-height: var(--font-label-medium-line-height);
36
+ --input-text-small-weight: 400;
37
+ --input-text-small-letter-spacing: 0.2px;
38
+ --input-text-medium-size: var(--font-body-medium-size);
39
+ --input-text-medium-line-height: var(--font-body-medium-line-height);
40
+ --input-text-medium-weight: 400;
41
+ --input-text-medium-letter-spacing: 0px;
42
+ --input-text-large-size: var(--font-body-large-size);
43
+ --input-text-large-line-height: var(--font-body-large-line-height);
44
+ --input-text-large-weight: 400;
45
+ --input-text-large-letter-spacing: 0px;
33
46
 
34
47
  /* Label/helper colors */
35
48
  --input-label-color: var(--color-label-standard);
@@ -53,9 +66,9 @@
53
66
  --input-placeholder-disabled-color: var(--color-label-disabled);
54
67
  /* 변경: readonly 입력은 placeholder를 숨겨 value 텍스트 대비를 고정한다. */
55
68
  --input-placeholder-readonly-color: transparent;
56
- --input-font-size: var(--font-body-medium-size);
57
- --input-line-height: var(--font-body-medium-line-height);
58
- --input-font-weight: 400;
69
+ --input-font-size: var(--input-text-medium-size);
70
+ --input-line-height: var(--input-text-medium-line-height);
71
+ --input-font-weight: var(--input-text-medium-weight);
59
72
  --input-letter-spacing: 0px;
60
73
 
61
74
  /* Border tokens */